FreeRTOS
1、什么是FreeRTOS?应用领域?
RTOS,即实时操作系统(Real-Time Operating System),是一种专门为实时应用程序设计的操作系统,强调的是对外部事件的快速响应和可预测性。实时系统通常要求在严格的时限内完成关键人物,因此RTOS具有优先级调度、确定性执行、快速响应中断等特点,广泛应用于工业控制、航空航天、汽车电子、医疗设备、通信系统等领域。
FreeRTOS是一个轻量级、开源的RTOS,专为资源有限的嵌入式系统设计。它提供了包括任务管理、时间管理、中断管理、内存管理在内的基本实时操作系统功能,并且具有高度可移植性和可配置性,能够运行在各种微处理器上。FreeRTOS因其开源、免费、文档齐全和活跃的社区支持,成为嵌入式开发领域的首选RTOS之一,特别适合于物联网(IOT)设备、智能家居、穿戴设备、传感器网络等对成本敏感和资源受限的应用。
2、FreeRTOS内核的基本组成有哪些?
FreeRTOS内核主要包括以下核心组件:
- **任务管理器(Task Manager):**负责任务的创建、删除、挂起、恢复、优先级调整以及基于优先级的调度。
- **时间管理器(Time Manager):**基于时间片的调度机制,管理系统的时钟节拍(ticks),提供诸如延时、定时器等功能
- **中断管理器(Interrupt Manager):**确保RTOS能够高效、安全地与硬件中断交互,允许中断服务例程(ISRs)与任务进行通信
- **内存管理器(Memory Manager):**提供动态内存分配和回收机制,如堆栈管理、内存池等,以支持任务堆栈和动态数据结构的分配
- **同步和通信机制:**包括信号量(二值信号量、计数型信号量、互斥信号量)、事件标志、消息队列等,用于任务间的数据交换和同步
- **软件定时器(Software Timers):**提供非周期性的定时器服务,允许任务在未来的某个时间点执行特定操作
- **钩子函数(Hook Functions):**允许用户插入自定义代码,在系统特定事件发生时执行,如任务切换前后、空闲任务执行时等
3、FreeRTOS中的“优先级调度”是如何工作的?
FreeRTOS采用的是固定优先级抢占式调度策略。这意味着每个任务都被赋予一个优先级,优先级较高的任务可以打断正在运行的优先级低的任务。
调度器始终选择当前就绪列表中优先级最高的任务来执行。当一个更高优先级的任务变为就绪状态时,调度器会立即保存当前任务的上下文(即CPU寄存器状态),并恢复新任务的上下文,从而实现任务之间的切换。
这种机制保证了关键任务能够及时得到处理,满足实时性要求。
4、解释一下FreeRTOS中的“上下文切换”
上下文切换是指在RTOS中,当调度器决定从一个任务切换到另一个任务时,保存当前任务的状态(如程序计数器、栈指针和其他寄存器的值)并恢复下一个要执行的任务的状态的过程。
在FreeRTOS中,上下文切换发生在以下情况:高优先级任务就绪、任务主动放弃CPU(如调用延时函数)、系统调用导致任务挂起、中断处理结束。
上下文切换是RTOS中最消耗资源的操作之一,因为它涉及到存储和恢复任务的运行环境,因此频繁的上下文切换会影响系统的整体性能。
5、任务状态
- 运行态:当任务正在运行时,处于运行态;处于运行态的任务就是当前正在使用处理器的任务
- 就绪态:准备就绪,可以运行的任务;有同优先级或更高优先级的任务正在运行占用CPU
- 阻塞态:任务正在等待某个外部事件即处于阻塞态;阻塞态有超时时间,若超时会退出阻塞态
- 挂起态:进入挂起态任务不能被调度器调用进入运行态;挂起态的任务没有超时时间
-
创建任务——>就绪态:任务创建完成后进入就绪态,表明任务已经就绪,随时可以运行,只等待调度器进行调度
-
就绪态——>运行态:发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态
-
运行态——>就绪态:有更高优先级任务创建或恢复后,会发生任务调度,此时就绪列表中最高优先级的任务变为运行态。原来运行的任务由运行态变为就绪态,依旧在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是CPU使用权被更高优先级任务抢占了)
-
运行态——>阻塞态:正在运行的任务发生阻塞(挂起、延时、读信号量等待),该任务就会从就绪列表中删除,任务状态由运行状态变为阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务
-
阻塞态——>就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务就会被加入到就绪列表,从而由阻塞态变为就绪态。若此时被恢复的任务的优先级高于正在运行任务的优先级,则会发生任务切换,该任务由就绪态变为运行态。
-
就绪态、阻塞态、运行态——>挂起态:任务可以通过调用vTaskSuspend()的API函数将处于任何状态的任务挂起,被挂起的任务得不到CPU的使用权,也不会参与调度,除非它从挂起态中解除
-
挂起态——>就绪态:把一个挂起状态的任务恢复的唯一途径就是调用vTaskResume()或vTaskResumeFromISR()的API函数,如果此时被恢复的任务的优先级高于正在运行任务的优先级,则会发生任务切换,该任务由就绪态变为运行态。
6、FreeRTOS中的“任务”和“线程”有何区别?
在FreeRTOS中,“任务”和“线程”经过被交替使用,但实际上FreeRTOS只使用“任务”这一术语。
在更广泛的操作系统理论中,进程:最小的分配资源单元;线程:进化成呢个内的一个执行单元,最小的执行单元,共享同一地址空间和某些资源。
然而,在FreeRTOS这样的RTOS中,任务实际上扮演着类似线程的角色,但通常每个 任务都拥有独立的堆栈和优先级,它们之间通过消息传递和同步原语进行通信。
由于FreeRTOS针对嵌入式系统设计,其任务模型更侧重于独立执行单元的管理和资源的高效利用,而非多进程模型下的资源共享。
7、何为“空闲任务”(Idle Task)?它的作用是什么?
空闲任务是FreeRTOS自动创建的第一个任务,它具有最低的优先级。空闲任务的作用主要体现在两个方面:
首先,当没有更高优先级的任务处于就绪状态时,空闲任务会运行,通常执行一个无限循环,防止CPU空闲。
其次,它承担了系统维护的职责,如释放已删除任务的内存资源(堆栈和任务控制块TCB)。在某些应用中,开发者还会再空闲任务中添加代码来执行低优先级的工作,比如监控系统状态、进行资源回收或是执行低功耗模式切换,以优化系统能耗。
8、如何在FreeRTOS中创建一个任务?
在FreeRTOS中创建任务,通常使用xTaskCreate()
或xTaskCreateStatic()
函数。其中,前者是动态分配任务,后者是静态分配堆栈。以xTaskCreate()
为例
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 任务函数入口
const char * const pcName, // 任务名称
uint32_t usStackDepth, // 任务堆栈大小
void *pvParameters, // 传递给任务的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 创建的任务句柄,可选
);
开发者需要提供任务执行函数、任务名称、堆栈大小、传递给任务的参数、优先级等信息。成功创建后,任务会自动加入就绪队列,等待调度器执行
9、如何设置和更改任务的优先级?
设置任务优先级通常在任务创建时通过uxPriority参数指定。若要更改现有任务的优先级,可以使用vTaskPrioritySet()
函数:
void vTaskPrioritySet(TaskHandle_t xTask, UBaseType_t uxNewPriority);
其中,xTask
是需要修改优先级的任务句柄,uxNewPriority
是要设置的新优先级。
需要注意的是,更改任务优先级可能影响系统的实时行为,特别是当涉及优先级翻转问题时,应谨慎操作,并考虑使用互斥锁或优先级继承机制等避免潜在的死锁。
此外,更改任务优先级是在任务运行时进行的,应当确保在安全的上下文中执行,避免引起系统不稳定。
10、什么是任务的“堆栈大小”?如何确定一个任务所需的堆栈大小?
在FreeRTOS中,每个任务都有自己的独立堆栈空间,用于存储局部变量、函数参数、返回地址以及其他CPU寄存器的值。
任务的“堆栈大小”指的是为该任务分配的内存区域的大小,它直接影响到任务能使用的资源量已经系统稳定性。过小的堆栈可能导致溢出,引起未知的系统行为或崩溃;而过大的堆栈则会浪费宝贵的RAM资源。
确定任务所需堆栈大小的方法包括
- **静态分析:**根据任务代码中最大递归深度、局部变量数量、函数调用链等因素股市
- **动态分析:**使用FreeRTOS提供的堆栈溢出检测工具。如
configCHECK_FOR_STACK_OVERFLOW
配置项,可以在运行时监测堆栈使用情况,帮助确定实际需求 - **经验法则:**对于简单任务,可以参考通用经验值(如200-500字节),复杂的任务则需要更精确计算
- **测试与调整:**在开发过程中,通过实际运行和调试,观察任务行为,逐步调整堆栈大小至合理水平
11、如何实现任务间通信?
FreeRTOS提供了多种任务间通信机制,包括
- 消息队列(Message Queues):允许任务之间通过发送和接收包含数据的消息来通信。发送任务将消息放入队列,接收任务从队列中取出消息
- 信号量(Semaphores):用来表示资源的可用性或事件的发生,分为二进制信号量、计数信号量、互斥信号量
- 事件标志组(EventGroups):一组可以独立设置和清除的位标志,多个任务可以通过等待特定位组合来同步。
- 任务通知(Task Notifications):每个任务都有一个通知值和一个通知标志,用于轻量级的通信和同步。
12、介绍一种FreeRTOS中的任务同步机制
互斥锁(Mutex),互斥信号量。是一种常用的任务同步机制。
互斥锁用于保护共享资源,确保同一时刻只有一个任务可以访问该资源,防止数据竞争和不一致性。
在FreeRTOS中,通过xSemaphoreCreateMutex()
函数创建互斥锁,任务在访问共享资源前获取互斥锁,访问完毕后释放互斥锁。
当一个互斥信号量正被一个低优先级任务使用时,若有高优先级的任务也尝试获取该互斥信号量的话就会被阻塞。不过此时高优先级任务会将低优先级任务提升到与自己相同的等级(优先级继承)。优先级继承只是尽可能降低优先级翻转带来的影响,并不能完全消除优先级翻转
13、什么是“任务删除”?如何安全地删除一个任务?
“任务删除”是指在运行时从系统中移除一个不再需要的任务,释放其占用的资源。安全地删除一个任务需要考虑以下几个方面
- **确保任务已经停止运行:**任务必须处于挂起、阻塞或完成状态,不能在任务正在运行时直接删除。
- **清理资源:**在删除前,任务应该释放所有分配的资源,如动态内存,互斥锁等
- **使用API:**使用FreeRTOS提供的
vTaskDelete(NULL)
函数来删除当前任务,或vTaskDelete(xTaskToDel)
来删除指定的任务句柄所指向的任务 - **处理任务删除后的逻辑:**如果任务间存在依赖,需要考虑删除任务队其他任务的影响,必要时重新安排任务优先级或进行相应的错误处理。
14、解释“任务挂起”和“任务恢复”的概念及其应用场景
“任务挂起”意味着暂时禁止一个任务的执行,使其不参与调度,直到被显式恢复。
“任务恢复”则是将已经挂起的任务重新加入到就绪队列,使其可以被调度执行。
这些操作通过vTaskSuspend()
和vTaskResume()
函数来完成。
应用场景包括:
- **资源等待:**当任务需要等待外部资源(如I/O完成)时,可以挂起自身,减少CPU占用
- **节省资源:**在资源紧张时,挂起低优先级或非关键人物,优先保障关键人物执行
- **同步控制:**作为任务间同步的一种手段。例如:任务A挂起自身等待任务B的某个条件,任务B完成后恢复任务A
15、FreeRTOS的时间片(tick)是如何工作的?
FreeRTOS的事件基础单位是时间片(tick),也称心跳或节拍。每个tick代表一段固定的系统时间,通常由硬件定时器产生。
时间片的频率(tick频率)在配置时确定,如每毫秒或每10毫秒一个tick。
FreeRTOS通过定期中断(tick中断)更新系统时间,检查任务调度需求,执行超时处理,以及触发软件定时器等
16、如何使用FreeRTOS的延时函数vTaskDelay()?
vTaskDelay()函数可以让当前任务延时特定的tick数后再继续执行。
函数内部通过将任务置于延时状态并将其加入延迟列表中实现延时,直到延时时间到达,任务才被重新加入就绪队列,等待调度。
17、什么是“软件定时器”?如何在FreeRTOS中使用软件定时器
软件定时器是FreeRTOS提供的一个功能,用于在不占用硬件定时器资源的情况下,实现周期性或一次性时间触发事件。相比于硬件定时器,软件定时器更加灵活,易于管理和配置。
使用步骤包括
- **配置软件定时器:**在FreeRTOS的配置文件
FreeRTOSConfig.h
中,启用软件定时器 - **创建软件定时器:**使用
xTimerCreate()
创建一个软件定时器实例,指定定时器的名字、回调函数、超时周期、自动重载标志等 - **启动定时器:**通过
xTimerStart()
或xTimerReset()
函数启动定时器。前者用于首次启动,后者用于重启已停止的定时器 - **处理定时事件:**定时器到期后,对应的回调函数会被调度执行,进行预定的任务处理
- **管理定时器:**使用
xTimerStop()
、xTimerChangePeriod()
等函数来停止、改变周期或删除定时器
18、如何实现基于时间的事件触发?
基于时间的事件触发通常借助软件定时器或系统tick。
例如,使用软件定时器,开发者可以为每个需要定时触发的事件创建一个或多个定时器,设置各自的超时周期和回调函数。当定时器到期,系统自动调用回调函数,执行预定义的动作,如数据采集、状态更新、任务唤醒等。
19、内存管理算法
FreeRTOS提供了5种动态内存管理算法,分别为: heap_1、heap_2、heap_3、heap_4、heap_5 。在我们FreeRTOS例程中,使用的均为heap_4内存管理算法
算法 | 优点 | 缺点 | 应用场合 |
---|---|---|---|
heap_1 | 分配简单,事件确定 | 只能申请内存,不允许释放内存 | 工程创建的任务等不需要删除 |
heap_2 | 最适应算法,允许申请/释放内存 | **不能合并相邻的空闲内存块,产生碎片、**时间不定 | 频繁创建/删除,且创建的任务堆栈相同 |
heap_3 | 直接调用c库malloc()、free() | 速度慢、时间不定 | |
heap_4 | 首次适应算法,允许申请释放,相邻空闲合并减少内存碎片的产生 | 时间不定 | 频繁分配/释放不同大小的内存 |
heap_5 | 能够管理多个非连续内存区域的heap_4 | 时间不定 | 内存地址不连续的场景 |
20、FreeRTOS如何处理中断服务程序(ISR)?
有以下几种机制
- **从ISR调用API:**FreeRTOS提供了一组专门设计用于在ISR中安全调用的函数,如
xQueueSendFromISR()、xSemaphoreGiveFromISR()
等,这些函数允许ISR安全地向队列发送消息,释放信号量,并且可以请求任务调度 - **中断锁定:**FreeRTOS使用中断锁定机制来保护临界区,防止ISR在执行关键操作时被打断。在进入和退出临界区分别调用
taskENTER_CRITICAL()、taskEXIT_CRITICAL()
函数,确保ISR在这些区间内不会执行 - **中断延迟:**FreeRTOS允许ISR通过调用
vTaskNotifyGiveFromISR()
等函数间接唤醒或通知任务,而不是直接执行任务切换,从而减少了中断延迟。
任务通知
xTaskNotify()、xTaskNotifyFromISR():
发送通知xTaskNotifyTake():
等待通知,如果通知可用,返回通知值;如果通知不可用,任务进入阻塞状态
任务唤醒
将一个阻塞的任务重新置于就绪态
xTaskNotifyGive()、xTaskNotifyGiveFromISR():
发送通知,若任务因等待通知而阻塞,该函数将唤醒任务xTaskNotifyWait():
等待通知,如果通知可用,任务被唤醒并返回;如果通知不可用,任务进入阻塞状态
21、FreeRTOS低功耗操作
支持低功耗操作的特性主要包括:
- **Tickless Idle:**允许系统在没有任务就绪或无需处理器定时器中断时,延长CPU空闲周期,减少系统时钟中断,从而节约资源
- **任务挂起:**允许任务在等待某个事件或条件时挂起自身,减少不必要的CPU活动
- **动态时钟频率调整:**配合硬件支持,可以动态调整CPU的时钟速度,根据任务负载调整功率消耗
- **事件驱动编程:**鼓励使用事件标志、消息队列等机制,使任务仅在需要时运行,减少无谓的唤醒。
如何在FreeRTOS中实现基于任务的低功耗模式切换?
- **识别低功耗实际:**设计任务逻辑,当系统进入空闲或等待状态时,触发低功耗模式
- **使用Tickless Idle:**配置FreeRTOS的Tickless Idle 模式,使得在没有任务就绪时,RTOS可以延长CPU的空闲周期,减少系统时钟中断
- **任务挂起与唤醒:**在任务中使用
vTaskSuspend()
挂起当前任务,当特定事件发生时,通过信号量、消息队列或事件标志等机制唤醒相关任务 - **硬件配置:**在进入低功耗模式前,通过硬件接口配置降低CPU时钟频率、关闭不必要的外设等
FreeRTOSV1
1、FreeRTOS任务调度的简要过程
- 任务创建:
- **确定初始运行任务:**FreeRTOS在启动时会自动 **启动空闲任务,**它负责管理系统中没有其他可运行任务时的情况
- **调度器启动:**调度器负责根据各个任务的优先级和状态来进行合适的上下文切换。然后SVC中断
- **抢占式调度:**FreeRTOS使用基于优先级的抢占式调度算法。当有更高优先级的就绪任务准备好运行时,当前正在运行的低优先级任务将被暂停,并且控制权转移到高优先级任务。
- **时间片轮转:**当任务优先级相等时使用。操作系统为每个任务分配一个时间片。在时间片轮转调度方式下,每个任务可以执行一个时间片,然后系统将控制权移交给下一个就绪任务。 **如果一个任务在其时间片结束前没有完成,系统会暂停该任务,将控制权交给下一个就绪任务。**这种调度方式有助于确保任务之间的公平性,避免某些任务长时间占用处理器,同时允许多个任务分享处理时间。
- **任务A阻塞:**调用OSDelay或vTaskDelay阻塞
- 保存任务A的上下文:
PendSV
中断用于执行上下文切换,被sysTick
中断或osdelay
主动触发,优先级最低 - 选择下一个任务:
PendSV
中断服务程序会调用vTaskSwitchContext
函数。这个函数是FreeRTOS的核心调度函数,它负责选择下一个将要运行的任务,并更新当前任务控制块(TCB)指针pxCurrentTCB
- 恢复任务B的上下文
- **延迟和阻塞:**FreeRTOS允许在等待事件发生或特定条件满足时使得一个或多个仙鹤草呢个进入延迟或阻塞状态,条件满足后重新激活这些线程。
2、FreeRTOS链表
任务列表数组的每一个元素是一个链表,链表中的节点关联到TCB。
1、就绪链表(Ready List)
就绪链表包含所有处于就绪状态的任务。就绪状态的任务是指已经准备好运行,但由于当前执行的任务正在占用 CPU 资源,它们暂时无法立即执行。这些任务按照优先级被组织在就绪链表中。当当前正在执行的任务释放 CPU(例如,由于时间片用完、任务阻塞或挂起等原因)时,调度器从就绪链表中选择优先级最高的任务来执行。
2、阻塞链表(Blocked List)
阻塞链表包含哪些由于某种原因而无法立即执行的任务。这些原因可能包括等待某个事件、资源不可用、延时等情况。当任务处于阻塞状态时,它们不会被调度器所执行。这些任务会在特定条件满足之后重新加入就绪列表,等待调度器选择其执行。
3、挂起链表(Suspended List)
挂起链表包含已经被显式挂起的任务。当任务被挂起时,它们暂时停止运行,不再参与调度。这些任务不会出现在就绪链表或阻塞链表中,因为它们被明确地挂起,不参与任务调度。
3、优先级翻转
使用信号量时。(信号量、队列)
高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。—从现象上来看,好像时中优先级的任务比高优先级的任务具有更高的优先权。
- FreeRTOS的 优先级继承机制是一种用于解决优先级翻转问题的手段
优先级继承机制原理
- **资源请求:**高优先级任务尝试获取一个被低优先级任务持有的互斥锁
- **优先级提升:**如果高优先级任务发现互斥锁被低优先级任务持有,FreeRTOS会将低优先级任务的优先级提升到高优先级任务相同的级别。这保证低优先级任务能够尽快完成对资源的使用
- **资源释放:**当低优先级任务释放互斥锁,它的优先级会回复为原来的级别
- **调度:**高优先级任务可以获取互斥锁并继续执行
4、每个任务的东西
每个任务的关键组成部分
- 任务控制块(TCB Task Control Block)
- 每个任务都有一个TCB,包含该任务的所有信息,如任务状态、优先级、任务堆栈的起始地址和当前堆栈指针等
- TCB在FreeRTOS中用于管理和调度任务
- 任务堆栈
- 每个任务都有自己的堆栈空间,用于存储局部变量、函数返回地址和任务上下文(CPU寄存器的值)
- 堆栈空间是在创建任务时从共享内存(通常是SRAM)中分配的
- 采用heap4内存管理,分配的堆栈是连续的
共享内存和堆栈
虽然STM32的内存是共享的,但任务堆栈的分配是通过分配内存区域来实现的,每个任务在创建时从共享内存中分配一块独立的堆栈空间。这种分配通常由FreeRTOS的内存管理函数(如 pvPortMalloc )处理。
5、SVC中断和PendSV
- SVC
SVC
中断为软中断,给用户提供一个访问硬件的接口- 主要用于启动第一个任务
- 通过
SVC
指令触发并进入SVC异常处理程序
- PendSV
- 专门用于任务上下文切换
- 被SysTick和taskyeild中断触发,优先级最低,在调度器需要切换任务时触发
- 通过设置NVIC的PensSV位触发,并在异常处理程序中保存和恢复任务上下文
SVC
SVC用于产生系统函数的调用请求
**当用户想要控制特定的硬件时,它就会产生一个SVC异常,**然后操作系统根据提供的SVC异常服务程序得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。
系统调用处理异常,用户与内核进行交互,用户想做一些内核相关功能的时候必须通过SVC异常,让内核处于异常模式,才能调用执行内核的源码。触发SVC异常,会立即执行SVC异常代码。
void triggerSVC(void)
{
__asm volatile ("svc 0");
}
PendSV
PendSV
中断是一个用来处理上下文切换的中断,可以由多种方式触发:
SysTick
**定时器中断:**最常见的触发方式,用于实现时间片轮转调度- **其他中断:**其他中断处理程序也可以根据需要显式触发
PendSV
中断 - **任务调用:**任务本身也可以通过调用
taskYIELD()
或其他调度相关函数来触发PendSV
中断。osdelay
也可以触发,在阻塞任务的过程中,会将调度器先挂起,然后进行移动任务到阻塞链表的操作,再恢复调度器。恢复调度器后会自动检查是否需要进行任务切换,会触发portYIELD_WITHIN_API
进行上下文切换
6、上下文切换
当操作系统决定暂停当前执行的任务并开始执行另一个任务时,就会发生上下文切换。
上下文切换的内容:
- 通用寄存器:R0-R12
- 堆栈指针:PSP
- 程序计数器:PC
- 链接寄存器:LR
- 程序状态寄存器:xPSR
- 浮点寄存器
切换步骤
- 保存当前任务的上下文
- 选择要运行的下一个任务
- 加载新任务的上下文
- 更新调度器状态
- 执行新选定的任务
_FROM_ISR
作用:在中断中调用API,其禁用了调度器,无延时等阻塞操作,保证临界区资源快进快出访问
7、中断和任务的关系
中断和任务是操作系统处理并发性的两种基本机制。
中断
- 中断通常由硬件设备(如定时器、外部设备,硬件中断)或软件请求(如系统调用,软件中断)触发
- 中断会暂停当前正在执行的程序,保存其上下文,并跳转到一个中断服务程序(ISR)处,完成后恢复程序继续执行
- FreeRTOS允许用户编写中断服务程序(ISR),用于响应硬件事件或外部触发中断
- 中断服务程序通常要尽可能快速地完成执行,以便尽快恢复正常任务调度。
任务
- 任务是操作系统调度和管理的基本执行单元,通常指进程(Process)或线程(Thread)
- FreeRTOS通过调度器负责管理和调度多个任务,并确保它们按照优先级和就绪状态正确执行
- 每个任务都有自己的堆栈空间、上下文信息等,由调度器进行管理
关系
- 并发性:中断允许在某些事件发生时及时响应,而任务则允许多个代码块同时运行
- 中断服务程序可能需要与任务进行通信或共享数据。这种情况需要谨慎设计数据共享和同步机制
- 中断可以唤醒阻塞状态下的任务,例如通过发送信号量或使用消息队列、消息通知等机制
- 任务可以在其执行过程中禁用某些中断(通过临界区保护)以确保关键代码段不被打断
FreeRTOS中的应用
- 中断通常用于处理硬件事件(如定时器、外设输入输出等),而任务用于实现复杂的功能模块或业务逻辑
资源共享和同步
- 在FreeRTOS中,需要注意,在中断服务程序和任务之间共享资源时可能出现竟态条件问题
- 可以使用信号量、消息队列等机制来实现资源共享和同步,在避免数据竞争方面起到重要作用。
8、高优先级打断低优先级中断
NVIC
- NVIC(嵌套向量中断控制器)NVIC是ARM Cortex-M系列处理器的一部分,是一种硬件结构
- NVIC通过 中断向量表来管理中断
- 当中断发生时,处理器会依据 **中断号从向量表中获取对应的中断服务程序的地址,**然后开始执行相应的中断处理程序
- NVIC还负责中断优先级的管理,它可以根据 中断类型和配置的优先级来确定哪个中断应该被优先处理。
中断抢占
当一个低优先级中断正在执行时,如果一个高优先级的中断触发。通常会发生以下情况:
- **中断抢占:**当一个高优先级的中断触发时,如果其优先级高于当前正在执行的低优先级中断,处理器会立即中断当前正在执行的低优先级中断,并开始执行高优先级中断的中断服务程序。
- **中断嵌套:**在一些处理器中,包括使用NVIC的ARM Cortex-M系列处理器,支持中断嵌套。这意味着当高优先级中断发生时,它可以抢占正在执行的低优先级中断,并执行其自己的中断服务程序。但是,一旦高优先级中断处理完毕,处理器会返回到被抢占的低优先级中断处继续执行。
9、消息队列和共享内存的区别是什么,消息队列如何做到防止两个任务同时使用的
消息队列
初始化:创建消息队列
发送任务/中断:发送数据到消息队列
接收任务/中断:从队列接收数据
// 创建消息队列
QueueHandle_t xQueue = xQueueCreate(10, sizeof(int));
// 发送数据到队列
int sensorData = 123;
if(xQueueSend(xQueue, &sensorData, (TickType_t)10) != pdPASS) {
// 处理发送失败的情况
}
// 从队列接收数据
int receivedData;
if(xQueueReceive(xQueue, &receivedData, (TickType_t)10) == pdPASS) {
// 使用接收到的数据
}
**消息队列和共享内存是两种常见的进程或任务间通信(IPC)机制,**它们在用途、实现和使用场景上有显著的区别。
消息队列
消息队列是一个先入先出(FIFO)的数据结构,用于存储待处理的消息。进程或任务可以将消息发送到队列,其他进程或任务可以从队列接收消息。
消息队列提供了一种松耦合的通信方式,发送者和接收者不需要同时在线,也不需要知道对方的存在。
优点
- 同步与异步通信
- 松耦合
- 消息传递:可以跨不同进程安全地传递复杂的数据结构
缺点
- 性能开销:每条消息的发送和接收都涉及到系统调用,可能比共享内存方式更慢
- 资源开销:系统对可用消息队列的数量和大小可能有限制
共享内存
共享内存允许两个或多个进程共享一个给定的存储区,是最快的IPC方法之一。所有共享内存的进程都可以直接读写这块内存。
优点
- 性能:数据不需要再进程间复制,传输速率高
- 直接访问:进程直接对内存进行读写操作,减少了中间层的开销
缺点
- 同步复杂性:当多个进程需要访问共享内存时,需要额外的同步机制(互斥锁、信号量)来防止数据竞态和一致性问题
- 安全性和健壮性:不当的内存访问可能导致数据损坏或程序崩溃
总结
选择消息队列还是共享内存取决于具体的应用场景。如果IPC涉及复杂的数据结构或者需要保证通信双方的解耦,消息队列可能是更好的选择。如果性能是首要考虑因素,并且开发者可以妥善管理同步问题,共享内存可能是更合适的选择。在实际应用中,这两种机制有时会结合使用,以达到既快速又可靠的通信。
10、消息队列,入队和出队时发生了什么
消息队列是一种重要的进程间通信(IPC)机制,允许进程或线程安全地交换信息。这种机制通过在内存中创建一个队列来实现,进程可以向队列中添加消息,并从中读取消息。
传入(发送)消息时发生了什么
- **消息序列化:**如果消息不是原始二进制形式,它首先会被序列化打包成一种标准格式,以便安全地通过队列传输。
- **队列访问:**发送进程通过操作系统API请求项指定的消息队列发送消息。如果队列实现了 访问控制,操作系统想验证进程是否有权写入队列
- **消息排队:**RTOS将消息放在队列尾部。如果队列设置了消息优先级,系统会根据优先级将消息插入到合适的位置。
- **阻塞和超时:**如果队列已满,发送进程可能会根据API调用的具体参数被阻塞,直到队列中有足够的空间,或者操作超时
- **通知接收者:**一旦消息被成功排队,操作系统可能会通知一个或多个等待消息的进程,告诉它们现在队列中有消息可用。
传出(接收)消息时发生了什么
- 队列访问
- **消息检索:**RTOS从队列头部检索消息,如果队列时空的,接收进程会根据API调用的参数可能会阻塞,直到有消息到达,或者操作超时
- **消息反序列化:**接收到的消息如果需要,将被反序列化或解包,转换回适用于接收进程的数据结构
- **确认处理:**在某些系统中,接收进程可能需要显式确认它已成功处理消息,特别是在需要消息可靠性的场景中。这样可以允许系统在消息未成功处理时重试传递。
- **资源管理:**消息被成功接收并处理后,相关的系统资源(如内存)将被回收,以便再次使用。
11、临界区
作用是确保某段代码在执行过程中不会被中断或调度器切换到其他任务,从而保护共享资源免受并发访问的影响。
两种方式
taskENTER_CRITICAL();
//code
taskEXIT_CRITICAL();
vTaskSupendAll();
//代码
xTaskResumeAll();
**临界区应尽可能小:**进入临界区会禁止中断或暂停调度,这可能会影响系统的实时性和响应性。因此,临界区应尽可能小,只保护必要的代码段。
**避免在临界区中调用可能阻塞的函数:**在临界区中调用可能阻塞的函数(如等待信号量、消息队列等)会导致系统死锁或任务调度问题
**选择合适的方法:**全局中断禁止的临界区适用于需要保证极高安全性的场合,但会影响所有中断;调度器暂停的方法更温和,只会影响任务调度,不会影响中断处理。
什么时候使用临界区
- **访问共享硬件资源:**多个任务需要访问同一个硬件外设(例如UART、I2C、SPI等)
- **操作共享数据结构:**多个任务需要操作同一个链表、队列或其他复杂数据结构。
- **更新全局配置:**多个任务可能需要读取和更新全局配置参数。
12、互斥锁Mutex、自旋锁SpinLock
当加锁失败时,互斥锁用 线程切换 来应对,自旋锁用 忙等待 来应对
互斥锁:独占锁,谁上锁谁有权释放,申请上锁失败后阻塞,不能在中断中调用
自旋锁:申请上锁失败后,一直判断是否上锁成功,消耗CPU资源,可以在中断中调用
13、死锁与递归
死锁会发生在以下情况下
- **非递归锁:**如果任务已经有某个锁,递归调用函数时再次获取通用锁,则会导致死锁。因为该任务会无限期等待自己释放该锁,而自己被阻塞在等待锁释放的状态
- **锁的顺序不当:**如果在递归调用过程中,不同的锁按照不同的顺序被获取,可能导致死锁。
递归锁的解决方案
- 递归锁(可重入锁):使用递归锁,它允许同一个任务多次获取同一个锁,而不会被阻塞。每次锁被获取,内部计数器增加;每次锁被释放,计数器减少,只有计数器为零时,锁才会真正被释放。
- 避免在递归中使用锁
14、临界区与锁的对比
互斥锁与临界区的作用非常相似,但互斥锁(mutex)是可以命名的,也就是说它可以跨越进程使用。所以创建互斥锁需要的资源更多,所以如果只为了在进程内部使用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥锁是跨进程的互斥锁一旦被创建,就可以通过名字打开它。
临界区是一种轻量级的同步机制,与互斥和事件这些内核同步对象相比,临界区是用户态下的对象,即只能在同一进程中实现线程互斥。因无需在用户态和核心态之间切换,所以工作效率比较互斥来说要高很多

15、同步互斥锁在消息队列如何应用
互斥:
互斥是通过某种锁机制(如互斥锁、信号量等)实现的,它确保在任何给定时刻,只有一个线程或进程可以操作消息队列。
- **加锁:**任务操作消息队列时,首先尝试获得与队列关联的锁。如果锁已被其他线程占用,该线程将等待直到锁变为可用状态。
- **执行操作:**一旦获得锁,线程就可以安全地执行其操作。
- **释放锁:**操作完成后,线程释放锁。
同步:
确保生产者不会在队列满时尝试添加消息,消费者不会在队列空时尝试读取消息。
- **实际操作:**读取函数会有返回值,等于ture的时候才允许操作,否则挂起。
实际应用
-
FreeRTOS内部已经实现了线程安全机制,因此不需要额外的锁来保护对队列的访问。
-
具体来说,FreeRTOS的队列在内部使用了信号量(Semaphore)来保证多个任务(Task)对队列的并发访问是安全的。
-
在任务或者中断中调用消息队列传输的时候会进入临界区。
-
中断函数中有专门的ISR函数,不会执行阻塞操作,而且尝试直接写入,如果写入失败会返回false。
16、如何使消息队列性能更优(索引方式)
使用索引来优化消息队列的传输效率,而不是直接传输大量数据。通过这种方式,减少了在消息队列中的传输数据量,同时提高了系统的整体性能。
传统方法:
css复制代码消息队列
[ 数据块1 ][ 数据块2 ][ 数据块3 ] ...
- 每次传输大量数据,内存占用大、传输时间长
使用索引的方法
css复制代码消息队列
[ 索引1 ][ 索引2 ][ 索引3 ] ...
内存池
[ 数据块1 ][ 数据块2 ][ 数据块3 ] ...
- 消息队列中仅传输索引,内存池中存储实际数据
实现步骤
- **预分配内存块:**在系统初始化时,分配一定数量的固定大小的内存块。这些内存块用于存储实际的数据。
- **空闲队列:**维护一个空闲内存块的队列。 当需要发送数据时,从空闲队列中取出一个内存块索引。
- **填充数据:**依据索引 ,将数据填充到取出的内存块中。
- **索引入队:**将内存块的索引(或指针)放入消息队列中。
- **消费数据:**接收任务从消息队列中取出索引,通过索引找到对应的内存块,处理其中的数据。
- **释放内存块:**处理完数据后,将内存块归还到空闲队列中。
17、如果有一个高频任务,需要实现
1、确定任务优先级
2、设计任务执行时间
3、使用定时器或中断
4、使用信号量或事件组同步
18、FreeRTOS和Linux的区别
最大的区别在于实时性,RTOS的实时性更好。
最大的区别-实时性
在实时性、中断处理、应用场景三个方面的差异较大。
中断造成的实时性差异
Linux是软中断,RTOS的中断是由NVIC来实现的,中断是实时的,会立即响应或者不响应。Linux响应有延时。
19、FreeRTOS内核中的驱动隔离
实现驱动隔离的主要目的是为了提高代码的模块性和可维护性,同时减少上层应用与硬件之间的直接依赖。
驱动隔离通常意味着创建一个抽象层,使应用代码与硬件操作解耦。
MCU上实现
-
驱动抽象层(HAL)
-
STM32通过其硬件抽象层(HAL)库提供了一个很好的起点,这些库封装了对硬件的直接访问,如GPIO, ADC, UART等。
-
创建自定义的驱动抽象函数或接口,这些接口函数封装HAL函数,为应用层提供统一的调用接口。
-
-
设备驱动接口
-
定义设备驱动结构体,包含指向功能函数的指针(如初始化、读、写、关闭等)。
-
应用层通过调用这些结构体中的函数指针来操作硬件,而无需关心具体的硬件细节。
-
-
中间件层
- 可以在HAL和应用层之间实现一个中间件层,这层处理更复杂的逻辑,如设备的状态管理、错误处理等
- 中间件层同样提供API,这些API基于设备驱动层的功能,向上提供更高层的服务
-
任务和信号量
-
在FreeRTOS中,利用任务(Tasks)来隔离不同功能模块,每个任务处理特定的逻辑。
-
使用信号量(Semaphores)或互斥锁(Mutexes)来管理对共享资源的访问,确保数据访问的线程安全。
-
-
消息队列
- 使用消息队列(Queues)来在任务之间传递数据,减少直接的任务间交互,可以通过队列发送消息或命令,从而驱动硬件操作。
-
事件组和中断
- 利用事件组来处理多个任务或中断之间的事件同步。在中断服务例程(ISR)中,尽量减少执行时间和复杂操作,使用中断来通知任务处理具体事件。
// UART驱动接口
typedef struct {
void (*init)(uint32_t baud_rate);
void (*transmit)(const uint8_t* buffer, size_t size);
void (*receive)(uint8_t* buffer, size_t size);
} UART_DriverInterface;
// 实现该接口
void UART_Init(uint32_t baud_rate) {}
void UART_Transmit(const uint8_t* buffer, size_t size)}
void UART_Receive(uint8_t* buffer, size_t size) {}
// 创建一个驱动结构体实例
UART_DriverInterface uart_driver = {
.init = UART_Init,
.transmit = UART_Transmit,
.receive = UART_Receive
};
// 应用层调用
void app_function() {
uart_driver.init(9600);
uart_driver.transmit("Hello", 5);
}
通过这样的结构,应用代码与底层硬件操作解耦,便于维护和可扩展性。
而驱动硬件操作。
-
事件组和中断
- 利用事件组来处理多个任务或中断之间的事件同步。在中断服务例程(ISR)中,尽量减少执行时间和复杂操作,使用中断来通知任务处理具体事件。
// UART驱动接口
typedef struct {
void (*init)(uint32_t baud_rate);
void (*transmit)(const uint8_t* buffer, size_t size);
void (*receive)(uint8_t* buffer, size_t size);
} UART_DriverInterface;
// 实现该接口
void UART_Init(uint32_t baud_rate) {}
void UART_Transmit(const uint8_t* buffer, size_t size)}
void UART_Receive(uint8_t* buffer, size_t size) {}
// 创建一个驱动结构体实例
UART_DriverInterface uart_driver = {
.init = UART_Init,
.transmit = UART_Transmit,
.receive = UART_Receive
};
// 应用层调用
void app_function() {
uart_driver.init(9600);
uart_driver.transmit("Hello", 5);
}
通过这样的结构,应用代码与底层硬件操作解耦,便于维护和可扩展性。