1、FreeRTOS 的任务调度
FreeRTOS 的内核是一个 基于优先级的多任务调度器,它负责在多个任务之间分配 CPU 执行时间,FreeRTOS 支持三种调度模式:
|
模式 |
宏定义 |
含义 |
|
抢占式调度 |
configUSE_PREEMPTION = 1 |
高优先级任务可随时打断低优先级任务 |
|
协作式调度 |
configUSE_PREEMPTION = 0 |
任务必须调用taskYIELD() 或 vTaskDelay()“主动让出”CPU |
|
时间片轮询(时间片调度) |
configUSE_TIME_SLICING = 1 |
相同优先级的任务按时间片轮流运行 |
一般我们只会同时开启抢占式调度和时间片调度。
2、FreeRTOS 的优先级
Cortex-M处理器的中断优先级配置寄存器叫AIRCR,是一个8Bit的寄存器,并且该寄存器分为了2段,然而STM32只使用了其中的4Bit,所以STM32的中断优先级有5个分组,分别是:
|
优先级分组 |
抢占优先级 |
子优先级 |
优先级配置寄存器高 4 位 |
|
NVIC_PriorityGroup_0 |
0 级抢占优先级 |
0-15 级子优先级 |
0bit 抢占优先级 4bit子优先级 |
|
NVIC_PriorityGroup_1 |
0-1 级抢占优先级 |
0-7 级子优先级 |
1bit 抢占优先级 3bit 子优先级 |
|
NVIC_PriorityGroup_2 |
0-3 级抢占优先级 |
0-3 级子优先级 |
2bit 抢占优先级 2bit 子优先级 |
|
NVIC_PriorityGroup_3 |
0-7 级抢占优先级 |
0-1 级子优先级 |
3bit 抢占优先级 1bit 子优先级 |
|
NVIC_PriorityGroup_4 |
0-15 级抢占优先级 |
0 级子优先级 |
4bit 抢占优先级 0bit 子优先级 |
FreeRTOS的优先级是没有子优先级的,所以STM32移植FreeRTOS只能使用NVIC_PRIORITYGROUP_4,这样STM32的中断直接就有16个抢占优先级。
抢占优先级:抢占优先级高的中断可以打断正在执行但抢占优先级低的中断,即中断嵌套。
子优先级:抢占优先级相同时,子优先级高的中断不能打断正在执行但子优先级低的中的中断,
即子优先级不支持中断嵌套。

注意:
1.在STM32 中,中断优先级是:数值越小优先级越高!
2.在FreeRTOS中,任务优先级是:数值越大优先级越高!
3.FreeRTOS 的开关中断就是操作 BASEPRI 寄存器来实现的!
它可以关闭低于某个阈值的中断,高于这个阈值的中断就不会被关闭!
4.在 FreeRTOS 中 PendSV 和 SysTick 的中断优先级都是最低的!
3、FreeRTOS 的临界区
临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段,比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。FreeRTOS在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断。FreeRTOS系统本身就有很多的临界段代码,这些代码都加了临界段代码保护,我们在写自己的用户程序的时候有些地方也需要添加临界段代码保护。
临界段代码保护有关的函数有 4 个:
taskENTER_CRITICAL()、taskEXIT_CRITICAL()、taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()
下面是进出临界区函数接口的原型:

可以看出在进入函数 vPortEnterCritical()以后会首先关闭中断,然后给变量 uxCriticalNesting 加一, uxCriticalNesting 是个全局变量,用来记录临界段嵌套次数的。函数vPortExitCritical()是退出临界段调用的,函数每次将uxCriticalNesting减一,只有当uxCriticalNesting为0的时候才 会调用函数 portENABLE_INTERRUPTS()使能中断。这样保证了在有多个临界段代码的时候不会因为某一个临界段代码的退出而打乱其他临界段的保护,只有所有的临界段代码都退出以后才会使能中断!(带ISR的就是用在中断服务函数的)
注意:临界区代码一定要精简!因为进入临界区会关闭中断,这样会导致优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY的中断得不到及时的响应!并且中断中进入临界区时不支持嵌套的。
4、FreeRTOS 的开关中断
ARM Cortex-M 有三个用于屏蔽中断的寄存器,分别为 PRIMASK、 FAULTMASK 和BASEPRI,下面就分别来看一下这三个寄存器的作用。
1. PRIMASK
作用: PRIMASK 寄存器有 32bit,但只有 bit0 有效, 是可读可写的,将 PRIMASK 寄存器设置为 1 用于屏蔽除 NMI 和 HardFault 外的所有异常和中断,将 PRIMASK 寄存器清 0 用于使能中断。

2. FAULTMASK
作用: FAULTMASK 寄存器有 32bit,但只有 bit0 有效, 也是可读可写的,将 FAULTMASK寄存器设置为 1 用于屏蔽除 NMI 外的所有异常和中断,将 FAULTMASK 寄存器清零用于使能中断。

3. BASEPRI
作用:BASEPRI有32bit,但只有低8位[7:0]有效,也是可读可写的。BASEPRI寄存器比起PRIMASK和FAULTMASK 寄存器直接屏蔽掉大部分中断的方式,BASEPRI寄存器的功能显得更加细腻,BASEPRI 用于设置一个中断屏蔽的阈值,设置好BASEPRI后,中断优先级低于BASEPRI的中断就都会被屏蔽掉,FreeRTOS就是使用BASEPRI寄存器来管理受FreeRTOS管理的中断的,而不受FreeRTOS管理的中断,则不受FreeRTOS的影响。

5、FreeRTOS 的任务状态
FreeRTOS 中任务存在四种任务状态,分别为运行态、就绪态、阻塞态和挂起态。
1. 运行态
如果一个任务得到 CPU 的使用权,即任务被实际执行时,那么这个任务处于运行态。如果运行 RTOS 的 MCU 只有一个处理器核心,那么在任务时刻,都只能有一个任务处理运行态。
2. 就绪态
如果一个任务已经能够被执行(不处于阻塞态后挂起态),但当前还未被执行(具有相同优先级或更高优先级的任务正持有 CPU 使用权),那么这个任务就处于就绪态。
3. 阻塞态
如果一个任务因延时一段时间或等待外部事件发生,那么这个任务就处理阻塞态。例如任务调用了函数 vTaskDelay(),进行一段时间的延时,那么在延时超时之前,这个任务就处理阻塞态。任务也可以处于阻塞态以等待队列、信号量、事件组、通知或信号量等外部事件。通常情况下,处于阻塞态的任务都有一个阻塞的超时时间,在任务阻塞达到或超过这个超时时间后,即使任务等待的外部事件还没有发生,任务的阻塞态也会被解除。要注意的是,处于阻塞态的任务是无法被运行的。
4. 挂起态
任务一般通过函数 vTaskSuspend()和函数 vTaskResums()进入和退出挂起态与阻塞态一样,处于挂起态的任务也无法被运行。

6、FreeRTOS 的任务优先级
任务优先级是决定任务调度器如何分配 CPU 使用权的因素之一。 每一个任务都被分配一个0~(configMAX_PRIORITIES-1)的任务优先级,宏 configMAX_PRIORITIES 在 FreeRTOSConfig.h文件中定义 。FreeRTOS 则会使用特殊方法计算下一个要运行的任务,这种特殊方法一般是使用硬件计算前导零指令,对于 STM32 而言,硬件计算前导零的指令,最大支持 32 位的数,因此宏 configMAX_PRIORITIES 的值不能超过 32。
FreeRTOS 的任务优先级高低与其对应的优先级数值,是成正比的,也就是说任务优先级数值为 0 的任务优先级是最低的任务优先级,任务优先级数值为(configMAX_PRIORITIES-1)的任务优先级是最高的任务优先级。 FreeRTOS 的任务优先级高低与其对应数值的逻辑关系正好与STM32 的中断优先级高低与其对应数值的逻辑关系相反,如下图所示:

7、FreeRTOS 的任务栈
函数中的局部变量、函数调用时的现场保护和函数的返回地址等都是存放在栈空间中的。 对于 FreeRTOS,当使用静态方式创建任务时,需要用户自行分配一块内存,作为任务的栈空间,而使用动态方式创建任务时,系统则会自动从系统堆中分配一块内存,作为任务的栈空间。任务栈大小,实际上是以字为单位的,并非以字节为单位。对于静态方式创建任务的函数 xTaskCreateStatic(),参数 usStackDepth表示的是作为任务栈且其数据类型为 StackType_t 的数组 puxStackBuffer 中元素的个数。


静态和动态创建任务时,任务栈的大小都与数据类型 StackType_t 有关, 对于STM32 而言,该数据类型的相关定义,如下所示:

因此,不论是使用静态方式创建任务还是使用动态方式创建任务,任务的任务栈大小都应该为 ulStackDepth*sizeof(uint32_t)字节,即ulStackDepth字。
8、FreeRTOS 的列表和列表项
FreeRTOS 中的列表和列表项就是数据结构中的链表和节点,其在物理存储单元上是非连续、非顺序的。要注意的是,FreeRTOS中的列表是一个双向链表,在list.h 文件中,有列表的相关定义,具体代码如下所示:

列表项是列表中用于存放数据的地方,在 list.h 文件中,有列表项的相关定义,具体代码如下所示:

1. 如同列表一样,列表项中也包含了两个用于检测列表项数据完整性的宏定义。
2. 成员变量 xItemValue 为列表项的值,这个值多用于按升序对列表中的列表项进行排序。
3. 成员变量 pxNext 和 pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列表项。
4. 成员变量 pxOwner 用于指向包含列表项的对象(通常是任务控制块),因此,列表项和包含列表项的对象之间存在双向链接。
5. 成员变量 pxContainer 用于指向列表项所在列表。
迷你列表项也是列表项,但迷你列表项仅用于标记列表的末尾和挂载其他插入列表中的列表项,用户是用不到迷你列表项的,在 list.h 文件中,有迷你列表项的相关定义,具体的代码录下所示:

1. 迷你列表项中也同样包含用于检测列表项数据完整性的宏定义。
2. 成员变量 xItemValue 为列表项的值,这个值多用于按升序对列表中的列表项进行排序。
3. 成员变量 pxNext 和 pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列表项。
4. 迷你列表项相比于列表项,因为只用于标记列表的末尾和挂载其他插入列表中的列表项,因此不需要成员变量 pxOwner 和 pxContainer,以节省内存开销。

9、FreeRTOS 的任务调度器
函数 vTaskStartScheduler()用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度,除非调用函数 xTaskEndScheduler()停止任务调度器, 否则不会再返回。
函数 vTaskStartScheduler()主要做了六件事情。
1. 创建空闲任务,根据是否支持静态内存管理,使用静态方式或动态方式创建空闲任务。
2. 创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务任务,同样是根据是否配置支持静态内存管理,使用静态或动态方式创建定时器服务任务。
3. 关闭中断,使用 portDISABLE_INTERRUPT()关闭中断,这种方式只关闭受 FreeRTOS 管理的中断。关闭中断主要是为了防止 SysTick 中断在任务调度器开启之前或过程中,产生中断。FreeRTOS 会在开始运行第一个任务时,重新打开中断。
4. 初始化一些全局变量,并将任务调度器的运行标志设置为已运行。
5. 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
6. 最后就是调用函数 xPortStartScheduler()。
10、FreeRTOS 的空闲任务
空闲任务是一个FreeRTOS在启动调度器 (vTaskStartScheduler()) 时自动创建的,具有最低优先级的特殊任务,主要用于处理待删除任务列表和低功耗。当系统中没有其他任务可运行时,调度器就会切换执行空闲任务。
|
作用 |
说明 |
|
回收已删除任务的内存 |
当你调用 vTaskDelete() 删除任务时,任务的 TCB 和堆栈不会立刻释放,而是由空闲任务统一清理(防止在删除任务的上下文中释放内存导致问题)。 |
|
节能(进入低功耗) |
如果你启用了钩子函数 vApplicationIdleHook(),你可以在里面执行 __WFI() 或 __WFE() 让 MCU 进入低功耗模式。 |
|
CPU 空闲时间统计 |
可以利用空闲任务的执行时间来衡量 CPU 利用率。 |
|
背景维护工作 |
一些后台维护(如内存整理、看门狗喂狗等)也可以放在这里执行。 |
11、FreeRTOS 的任务切换
RTOS 的核心是任务管理,而任务管理的重中之重任务切换,系统中任务切换的过程决定了操作系统的运行效率和稳定性,尤其是对于实时操作系统。
要理解 FreeRTOS 的任务切换(Task Context Switch),一定要从 SysTick → SVC → PendSV 这三大核心中断来理解它们的分工与衔接。
|
异常 |
原始MCU作用 |
被RTOS"借用"后的作用 |
|
SysTick |
系统时间基准、精确延时,周期性触发系统心跳中断(比如1ms) |
作为 RTOS 的系统时基(tick),周期性触发调度 |
|
SVC |
提供“系统调用”机制(类似操作系统内核陷入) |
RTOS 用来启动第一个任务或执行系统调用(如创建任务、切换上下文)SVC只执行一次 |
|
PendSV |
用于延后执行的系统级服务 |
被 RTOS 用来延迟触发上下文切换(任务切换) |
利用 PendSV 的这个可挂起特性,在设计 RTOS 时,可以将 PendSV 的中断优先级设置为最低的中断优先级,这么一来, PendSV 的中断服务函数就会在其他所有中断处理完成后才执行。
上电→main()→创建任务→vTaskStartScheduler()→创建空闲任务和定时器任务,配置SysTick和 SVC,调用vPortStartFirstTask()启动首任务→SVC_Handler → 加载第一个任务上下文→任务A运行中…→SysTick中断 → 触发PendSV→PendSV_Handler → 保存A → 切换到B→任务B运行中... →(循环往复)
在STM32的优先级上,SVC(相对较高) > SysTick(稍高PendSV) >= PendSV(最低),程序在启动任务调度器时,里面会调用vPortStartFirstTask(),里面就会触发SVC中断,然后在其中断服务函数里面会实现启动第一个RTOS任务。后面就是PendSV和SysTick循环往复来进行任务切换的。
FreeRTOS有提供多个任务切换的宏定义,实际上最终都是调用了函数 portYIELD():

其实就是portYIELD里面会给中断状态状态寄存器(ICSR地址为 0xE000ED04)的PENDSVSET位写入1,触发PendSV实现任务切换。
例子1:如果当前程序一个A任务,一个B任务,优先级A=B,但是A任务又还没有执行完,此时滴答定时器已经到时间了,那系统会在滴答定时器中断里面切换到B任务吗? 答案:会切换到B任务
例子2:如果当前程序一个A任务,但是A任务又还没有执行完,此时滴答定时器已经到时间了,那系统会在滴答定时器中断里面切换到空闲任务吗? 答案:不会切换到空闲任务
空闲任务只有在下面情况下会执行:
- 所有任务都阻塞
- 所有任务被挂起
- 所有任务优先级都比空闲任务低
注意:SVC和PendSV都是通过操作BASEPRI寄存器来开关中断的
12、STM32 的MSP和PSP
13、FreeRTOS 任务状态与信息查询
|
函数 |
作用 |
|
uxTaskGetNumberOfTasks() |
获取系统中任务的数量 |
|
uxTaskGetSystemState() |
获取所有任务的状态信息(configUSE_TRACE_FACILITY==1) |
|
xTaskGetHandle() | |
|
vTaskGetInfo() |
获取指定任务的任务信息(configUSE_TRACE_FACILITY==1) |
|
eTaskGetState() |
获取指定任务的状态(INCLUDE_eTaskGetState ==1) |
|
vTaskList() |
以“表格”的形式获取系统中任务的信息(configUSE_STATS_FORMATTING_FUNCTIONS ==1) |
|
uxTaskPriorityGet() |
获取指定任务的任务优先级(INCLUDE_uxTaskPriorityGet ==1) |
|
uxTaskGetStackHighWaterMark() |
获取指定任务的任务栈的历史剩余最小值(INCLUDE_uxTaskGetStackHighWaterMark==1) |
|
vTaskGetRunTimeStats() |
获取指定任务的运行时间、 运行状态等信息(configGENERATE_RUN_TIME_STATS ==1 configUSE_STATS_FORMATTING_FUNCTIONS ==1 configSUPPORT_DYNAMIC_ALLOCATION ==1) |
14、FreeRTOS任务延时函数
FreeRTOS 提供了与任务延时相关的 API 函数,入下表所示:

- vTaskDelay() —— 相对延时(从当前时间起延时 X tick,周期会漂移)
- vTaskDelayUntil() —— 绝对延时(按照固定周期调度任务,不会漂移)
- xTaskAbortDelay() —— 中止一个被延时的任务(例如:任务A调用它去唤醒在延时/阻塞的任B)
例子1:Do_work()不一定是每1000ticks运行(比如Do_work()内部运行时间不定)
for(;;)
{
Do_work();
vTaskDelay (100);
}
例子2:Do_work()一定是每1000ticks运行一次,就算Do_work()内部运行时间不定也是1000ticks周期运行
TickType_t last = xTaskGetTickCount();
for(;;)
{
Do_work();
vTaskDelayUntil(&last, 1000); // 每 100 tick 一次
}
例子3:Do_work()在进入延时时,另一个任务要是调用了xTaskAbortDelay,那Do_work()就会提前退出延时变成运行。
for(;;)
{
Do_work();
vTaskDelay (1000);
}
xTaskAbortDelay(xTaskHandle); //在其他任务调用
15、FreeRTOS队列
队列是一种任务到任务、任务到中断、中断到任务数据交流的一种机制,因此队列也叫做消息队列。基于队列, FreeRTOS 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、 递归互斥信号量。
1. 数据存储:队列通常采用 FIFO(先进先出)的存储缓冲机制,当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据。但同时 FreeRTOS的队列也支持将数据写入到队列的头部, 并且还可以指定是否覆盖先前已经在队列头部的数据。
2. 多任务访问:队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。
3. 队列读取阻塞:任务从队列读取消息时,可以指定一个阻塞超时时间。如果任务在读取队列时,队列为空,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列中有可用的消息。当有其他任务或中断将消息写入队列中, 因等待队列而阻塞任务将会被添加到就绪态任务列表中,并读取队列中可用的消息。如果任务因等待队列而阻塞的时间超过指定的阻塞超时时间,那么任务也将自动被转移到就绪态任务列表中,但不再读取队列中的数据。
4. 队列写入阻塞:任务往队列写入消息时,也可以指定一个阻塞超时时间。如果任务在写入队列时,队列已经满了,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列有空闲的位置可以写入消息。指定的阻塞超时时间为任务阻塞的最大时间,如果在阻塞超时时间到达之前,队列有空闲的位置,那么队列写入阻塞任务将会解除阻塞,并往队列中写入消息,如果达到指定的阻塞超时时间,队列依旧没有空闲的位置写入消息,那么队列写入阻塞任务将会自动转移到就绪态任务列表中,但不会往队列中写入消息。
补充理解:
xQueueReceive和xQueueSend在阻塞时会发生任务切换,即有就绪任务就切换到就绪任务,没有就切换到空闲任务。(发送消息队列时,当队列已满 或者 接收消息队列时,当队列为空 ,这2种情况会进入阻塞(xTicksToWait不为0时))
xQueueReceive读取成功后,会将消息从队列中移除。消息的读取是通过拷贝的形式传递的,具体拷贝数据的大小,为队列项目的大小。
16、FreeRTOS队列集
使用队列进行任务之间的“沟通交流”时,一个队列只允许任务间传递的消息为同一种数据类型,如果需要在任务间传递不同数据类型的消息时,那么就可以使用队列集。
FreeRTOS提供的队列集功能可以对多个队列进行“监听”,只要被监听的队列中有一个队列有有效的消息,那么队列集的读取任务都可以读取到消息,如果读取任务因读取队列集而被阻塞,那么队列集将解除读取任务的阻塞。使用队列集的好处在于,队列集可以使得任务可以读取多个队列中的消息,而无需遍历所有待读取的队列,以确定具体读取哪一个队列。
注意:
- 在 FreeRTOS 中,队列、二值信号量、计数信号量、互斥量 等都是使用 QueueHandle_t来定义句柄的,队列集是用QueueSetHandle_t来定义句柄的。
- 队列在添加到队列集之前和从队列集移除之前,队列中不能有有效的消息,队列必须是空的。(因为如果添加前不为空,那队列集就不知道有消息,接收队列集时就会丢失这个数据,队列如果有消息就从队列集移除了,就会导致队列集无法同步该队列的状态了,导致接收队列集时出现数据错误。)
17、FreeRTOS信号量
信号量是解决任务间同步的问题的一种机制,可以实现对共享资源的有序访问。FreeRTOS 提供了多种信号量,按信号量的功能可分为二值信号量、计数型信号量、互斥信号量和递归互斥信号量。
- 二值信号量
信号量是基于队列实现的,二值信号量也不例外,二值信号量实际上就是一个队列长度为 1 的队列,在这种情况下,队列就只有空和满两种情况。二值信号量通常用于互斥访问或任务同步,可能会导致优先级翻转的问题。
优先级翻转问题:指的是,当一个高优先级任务因获取一个被低优先级任务获取而处于没有资源状态的二值信号量时,这个高优先级的任务将被阻塞,直到低优先级的任务释放二值信号量,而在这之前,如果有一个优先级介于这个高优先级任务和低优先级任务之间的任务就绪,那么这个中等优先级的任务就会抢占低优先级任务的运行, 这么一来,这三个任务中优先级最高的任务反而要最后才运行,这就是二值信号量带来的优先级翻转问题,用户在实际开发中要注意这种问题。

优先级翻转示意图,如上图所示,定义:任务 H 为优先级最高的任务,任务 L 为优先级中最低的任务,任务 M 为优先级在任务 H 与任务 L 之间的任务。
(1) 任务 H 和任务 M 为阻塞状态,等待某一事件发生,此时任务 L 正在运行。
(2) 此时任务 L 要访问共享资源,因此需要获取信号量。
(3) 任务 L 成功获取信号量,并且此时信号量已无资源,任务 L 开始访问共享资源。
(4) 此时任务 H 就绪,抢占任务 L 运行。
(5) 任务 H 开始运行。
(6) 此时任务 H 要访问共享资源,因此需要获取信号量,但信号量已无资源,因此任务 H阻塞等待信号量资源。
(7) 任务 L 继续运行。
(8) 此时任务 M 就绪,抢占任务 L 运行。
(9) 任务 M 正在运行。
(10) 任务 M 运行完毕,继续阻塞。
(11) 任务 L 继续运行。
(12) 此时任务 L 对共享资源的访问操作完成,释放信号量,虽有任务 H 因成功获取信号量,解除阻塞并抢占任务 L 运行。
(13) 任务 H 得以运行。
注意:函数 xQueueGiveFromISR()和xSemaphoreTakeFromISR()只能用于释放和获取二值信号量和计数型信号量,而不能用于获取互斥信号量,因为互斥信号量会有优先级继承的处理,而中断不属于任务,没法进行优先级继承。
2.计数型信号量
计数型信号量相当于长度大于0 的队列,因此计数型信号量能够容纳多个资源,这是在计数型信号量被创建的时候确定的。
计数型信号量通常用于一下两种场合:
(1)事件计数
在这种场合下,每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的资源数加 1),其他等待事件发生的任务获取计数型信号量(计数型信号量的资源数减 1),这么一来等待事件发生的任务就可以在成功获取到计数型信号量之后执行相应的操作。在这种场合下,计数型信号量的资源数一般在创建时设置为 0。
(2)资源管理
在这种场合下,计数型信号量的资源数代表着共享资源的可用数量,例如前面举例中停车场中的空车位。一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之后在成功获取了计数型信号量之后,才可以对这个共享资源进行访问操作,当然,在使用完共享资源后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般在创建时设置为受其管理的共享资源的最大可用数量。
3.互斥信号量
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。
当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的减少了高优先级任务处于阻塞态的时间,并且将“优先级翻转”的影响降到最低。(优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响。)
互斥信号量不能用于中断服务函数中,原因如下:
(1) 互斥信号量有任务优先级继承的机制,但是中断不是任务,没有任务优先级,所以互斥信号量只能用与任务中,不能用于中断服务函数。
(2) 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
4.递归信号量
递归互斥信号量是特殊的互斥信号量,与互斥信号量不同的是,递归互斥信号量在被获取后,可以被其持有者重复获取,当然其持有者需要释放递归互斥信号量与之获取递归互斥信号量相同的次数,递归互斥信号量才算被释放(获取多少次就要释放多少次才算释放成功)。递归互斥信号量与互斥信号量一样,也具备优先级继承机制,因此也不能在中断服务函数中使用递归互斥信号量。
|
类型 |
句柄类型定义 |
创建函数 |
特点&备注 |
典型用途 |
|
队列 Queue |
QueueHandle_t |
xQueueCreate(uxQueueLength, uxItemSize) |
任务之间传递数据(可结构体) 中断中可发送,不可接收 |
数据传递、事件队列、UART/LVGL消息队列 (中断中只能Give不能Take) |
|
队列集 Queue Set |
QueueSetHandle_t |
xQueueCreateSet(uxEventQueueLength) |
任务可以等待多个队列事件 |
一个任务等待多个队列/信号量事件 (中断中只能Give不能Take) |
|
二值信号量 Binary Semaphore |
SemaphoreHandle_t (QueueHandle_t) |
xSemaphoreCreateBinary() |
非0即1,不带数据,会导致优先级翻转,可用于中断中 |
中断服务→任务同步(中断中只能Give不能Take) |
|
计数型信号量 Counting Semaphore |
SemaphoreHandle_t (QueueHandle_t) |
xSemaphoreCreateCounting(maxCount, initialCount) |
任何任务都可获取和释放 |
管理资源池和事件计数 |
|
互斥信号量 Mutex |
SemaphoreHandle_t (QueueHandle_t) |
xSemaphoreCreateMutex() |
优先级继承,不可以在中断中使用 |
资源互斥(SPI/I2C/FATFS)+ 优先级继承 |
|
递归互斥信号量 Recursive Mutex |
SemaphoreHandle_t (QueueHandle_t) |
xSemaphoreCreateRecursiveMutex() |
优先级继承,不可以在中断中使用,必须由同一任务获取和释放相同次数才算完成 |
保护递归调用和嵌套访问 |
备注:由于FreeRTOS有typedef QueueHandle_t SemaphoreHandle_t;所以SemaphoreHandle_t本质还是QueueHandle_t。
18、FreeRTOS软件定时器
软件定时器在定时器精度上肯定是不如硬件定时器的,但是软件定时器的误差范围在对于对定时器精度要求不高的周期性任务而言,都是可以接受的,并且软件定时器也有使用简单、成本低等优点。
FreeRTOS 提供的软件定时器允许在创建前设置一个软件定时器定时超时时间,在软件定时器成功创建并启动后,软件定时器开始定时,当软件定时器的定时时间达到或超过先前设置好的软件定时器定时器超时时间时,软件定时器就处于超时状态,此时软件定时器就会调用相应的回调函数,一般这个回调函数的处理的事务就是需要周期处理的事务。
FreeRTOS 提供的软件定时器还能够根据需要设置成单次定时器和周期定时器。当单次定时器定时超时后,不会自动启动下一个周期的定时,而周期定时器在定时超时后,会自动地启动下一个周期的定时。
软件定时器的超时回调函数是由软件定时器服务任务调用的,软件定定时器的超时回调函数本身不是任务,因此不能在该回调函数中使用可能会导致任务阻塞的 API 函数,例如 vTaskDelay()、 vTaskDelayUntil()和一些会到时任务阻塞的等到事件函数,这些函数将会导致软件定时器服务任务阻塞,这是不可以出现的。
开启任务调度器的时候,RTOS会创建一个用于管理软件定时器的任务,这个任务就叫做软件定时器服务任务,主要负责软件定时器超时的逻辑判断、调用超时软件定时器的超时回调函数以及处理软件定时器命令队列。
FreeRTOS 提供了两种软件定时器:单次定时器 和 周期定时器 。单次定时器的一旦定时超时,只会执行一次其软件定时器超时回调函数,超时后可以被手动重新开启,但单次定时器不会自动重新开启定时。周期定时器的一旦被开启,会在每次超时时,自动地重新启动定时器,从而周期地执行其软件定时器回调函数。(除了开启和停止软件定时器的定时,还可以对软件定时器进行复位。复位软件定时器会使软件定时器的重新开启定时,复位后的软件定时器以复位时的时刻作为开启时刻重新定时)
单次软件定时器的状态转换图:

周期软件定时器的状态转换图:

软件定时器相关配置:
使能软件定时器: #define configUSE_TIMERS 1
定义软件定时器任务的优先级: #define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 1 )
定义软件定时器命令队列的长度: #define configTIMER_QUEUE_LENGTH 5
定义软件定时器任务的栈空间大小: #define configTIMER_TASK_STACK_DEPTH ( configMINIMAL_STACK_SIZE * 2)
创建软件定时器:
创建软件定时器,分别为动态方式创建软件定时器和静态方式创建软件定时器, 静态方式需要用户提供创建软件定时器所需的内存空间,而使用动态方式创建软件定时器时, FreeRTOS会自动从FreeRTOS管理的堆中分配创建软件定时器所需的内存空间。
19、FreeRTOS事件标志组
事件标志组与信号量一样属于任务间同步的机制,但是信号量一般用于任务间的单事件同步,事件标志组可以很好的处理多事件情况下的任务同步。
事件标志是一个用于指示事件是否发生的布尔值,一个事件标志只有 0或1 两种状态, FreeRTOS 将多个事件标志储存在一个变量类型为 EventBits_t 变量中,这个变量就是事件组,是一组事件标志的集合 。
当configUSE_16_BIT_TICKS=0 时,EventBits_t 是一个32位无符号的数据类型。
当configUSE_16_BIT_TICKS=1 时, EventBits_t是一个16位无符号的数据类型。
EventBits_t并不全是存放事件标志,FreeRTOS 将这个EventBits_t数据类型的变量拆分成两部分,其中:
低24位[23:0](configUSE_16_BIT_TICKS=1时是低8位[7:0])用于存储事件标志。
高8位[31:24](configUSE_16_BIT_TICKS=1 时是高8位[15:8])用作存储事件标志组控制信息。也就是说一个事件组最多可以存储24个事件标志。 如图:

变量中低 24 位中的每一位都是一个事件标志,当某一位被置1时,就表示这一位对应的事件发生了,这每1位对应什么事件,是由用户自己定义的,用户通过xEventGroupSetBits() 来标记事件,处理事件的任务可以通过xEventGroupWaitBits()来接收事件。
事件标志和全局变量的区别:
|
事件标志 |
全局变量 | |
|
本质 |
RTOS 内核对象,用于多任务间同步或通信 |
普通 C 语言变量,所有任务都能访问。 |
|
类型 |
EventGroupHandle_t |
int, bool, uint8_t等 |
|
特点 |
位图结构,可设置多个标志位(位级别通信) |
结构简单,但需手动同步 |
|
线程安全 |
安全(内部使用临界区保护) |
不安全(需手动加锁或禁中断) |
|
操作接口 |
xEventGroupSetBits()、xEventGroupWaitBits()等 |
直接读写变量 |
备注: xEventGroupSetBits设置事件位时,返回的是整个事件组的值,并且如果xEventGroupWaitBits()的xClearOnExit形参设置为pdTRUE时,就会出现xEventGroupSetBits返回值里面没有对应的事件位。所以最好还是自己手动调用xEventGroupClearBits清除对应的事件位,避免使用xEventGroupWaitBits()的自动清楚事件位功能。(原因:xEventGroupWaitBits在阻塞等待设置的位,xEventGroupSetBits设置事件位后立马就会被xEventGroupWaitBits获取,并立马被清除,然后程序才跳转回去xEventGroupSetBits返回事件组,此时返回值里面就没有了刚才设置的事件位)。
20、FreeRTOS任务通知
任务通知也是用于任务间进行同步和通讯的一种机制,但是相对于队列、事件标志组和信号量等而言,任务通知在内存占用和效率方面都有很大的优势。
在 FreeRTOS 中,每一个任务都有两个用于任务通知功能的数组,分别为任务通知数组和任务通知状态数组。其中任务通知数组中的每一个元素都是一个 32 位无符号类型的通知值;而任务通知状态数组中的元素则表示与之对应的任务通知的状态。
任务通知数组中的 32 位无符号通知值,用于任务到任务或中断到任务发送通知的“媒介”。当通知值为 0 时,表示没有任务通知;当通知值不为 0 时,表示有任务通知,并且通知值就是通知的内容。
任务通知状态数组中的元素,用于标记任务通知数组中通知的状态,任务通知有三种状态:未等待通知状态、等待通知状态和等待接收通知状态。 (其中未等待通知状态为任务通知的复位状态;当任务在没有通知的时候接收通知时,在任务阻塞等待任务通知的这段时间内,任务所等待的任务通知就处于等待通知状态;当有其他任务向任务发送通知,但任务还未接收这一通知的这段期间内,任务通知就处于等待接收通知状态。)
任务通知功能所使用到的任务通知数组和任务通知状态数组为任务控制块中的成员变量,因此任务通知的传输是直接传出到任务中的,不同于通过队列、事件标志组和信号量这些间接的方式通讯方式。
任务通知的优缺点:
优点:向任务发送事件或数据比使用队列、事件标志组或信号量快得多且更节省大量的内存。
缺点:任务通知不能适用在以下情况:
(1)不能发送事件或数据到中断(因为中断不是任务,没有任务控制块)。
(2)不能同时发送给多个接收任务,只能一对一。
(3)不能像队列一样缓存多个数据,一个任务通知值只能保存一次。
(4)发送通知不能阻塞等待接收任务接收通知,而消息队列的发送是可以阻塞等待接收任务接收消息队列(如消息队列已满时就会阻塞等待)。
如何使用任务通知实现模拟实现 二值信号量 、计数型信号量、消息邮箱和事件标志组?
任务通知实现模拟二值信号量和计数型信号量,主要还是基于等待任务通知的接口函数ulTaskNotifyTake()的xClearCountOnExit形参的灵活运用。
xClearCountOnExit = pdTRUE 表示 Take 后自动清零(变成标准二值信号量)
xClearCountOnExit = pdFALSE表示 Take 后不自动清零(变成计数型信号量)
例如:
释放端
xReturn = xTaskNotifyGive(Task3_Handler_TaskNotifyTest); //该函数不会发送数据只是每Give一次,NotifyValue+1。
获取端
NotifyValue = ulTaskNotifyTake(pdTRUE,portMAX_DELAY);/*Take完就清除任务通知值,模拟二值信号量*/
NotifyValue = ulTaskNotifyTake(pdFALSE,portMAX_DELAY);/*Take完任务通知值减1但不清除,模拟计数型信号量*/
任务通知实现模拟消息邮箱和事件标志组,主要还是基于等待任务通知的接口函数xTaskNotify()的eAction形参的灵活运用。
发送任务通知函数:xTaskNotify( xTaskToNotify, ulValue, eAction )
形参:xTaskToNotify接收任务通知的任务
ulValue 任务通知值
eAction通知方式
特别:
eAction = eSetBits表示设置任务通知值的某一位(变成事件标志组)
eAction = eSetValueWithOverwrite表示覆盖设置任务通知值(变成消息邮箱)
接收任务通知函数:xTaskNotifyWait( ulBitsToClearOnEntry, ulBitsToClearOnExit, pulNotificationValue, xTicksToWait )
形参:ulBitsToClearOnEntry 等待前指定清零的任务通知通知值比特位
ulBitsToClearOnExit 成功等待后指定清零的任务通知通知值比特位
pulNotificationValue任务通知的通知值
xTicksToWait阻塞等待任务通知值的最大时间
特别:
ulBitsToClearOnEntry = ulBitsToClearOnExit=通知值的bit数表示清除任务通知值的某些位(变成变成消息邮箱)
ulBitsToClearOnEntry = ulBitsToClearOnExit=某1bit 表示覆盖设置任务通知值某一位(变成事件标志组)
实际例子:
/*按键事件标志位相关定义*/
#define key1EventBit (1 << 0) /*按键1事件标志位 */
#define key2EventBit (1 << 1) /*按键2事件标志位 */
#define key3EventBit (1 << 2) /*按键3事件标志位 */
#define key4EventBit (1 << 3) /*按键4事件标志位 */
#define keyNotifyMask 0x0000000F /*按键任务通知掩码 */
发送端:
xReturn = xTaskNotify(Task5_Handler_TaskNotifyTest,key1EventBit,eSetBits); //事件标志组
xReturn = xTaskNotify(Task2_Handler_TaskNotifyTest,key_value & keyNotifyMask,eSetValueWithOverwrite);//消息邮箱
接收端:
xReturn = xTaskNotifyWait(keyNotifyMask,keyNotifyMask,&NotifyValue,portMAX_DELAY);//模拟任务通知
xReturn = xTaskNotifyWait(key1EventBit,key1EventBit ,&NotifyValue,portMAX_DELAY);//模拟事件标志组
21、FreeRTOS低功耗模式
FreeRTOS提供了用于低功耗的Tickless机制,该机制是基于硬件层面的相应低功耗模式实现的。
在了解Tickless机制前我们先复习下STM32的低功耗模式:
|
模式 |
进入 |
如何唤醒 |
内核电路时钟 |
唤醒后如何执行 |
功耗 |
|
睡眠 Sleep |
WFI |
任意中断 |
仅仅CPU停止运行,所有时钟依然保持工作,外设正常运行,SRAM、寄存器全部保持。 |
从进入 Sleep 前的下一条指令继续执行。 |
轻度省电 |
|
WFE |
唤醒事件 | ||||
|
停止 Stop |
PDDS和LPDS位+SLEEPDEEP位+WFI或WFE |
任意外部中断(EXTI、RTC、USART等) |
CPU停止运行,大多时钟都关闭,只有LSI/LSE(低速内部时钟和低速外部晶振)可继续工作,SRAM、寄存器全部保持。 |
从进入 Stop 前的下一条指令继续执行。 |
中等省电 |
|
待机 Standby |
PDDS位+SLEEPDEEP位+WFI或WFE |
WKUP引脚的上升沿、RTC闹钟(唤醒/入侵/时间戳)事件、NRST引脚上的外部复位、IWDG复位 |
CPU停止运行,所有时钟都关闭,SRAM、寄存器全部丢失。 |
相当于重新上电,程序从复位向量表开始执行。 |
极其省电 |
|
待机 Standby |
PDDS位+SLEEPDEEP位+WFI或WFE |
WKUP引脚的上升沿、RTC闹钟(唤醒/入侵/时间戳)事件、NRST引脚上的外部复位、IWDG复位 |
CPU停止运行,所有时钟都关闭,SRAM、寄存器全部丢失。 |
相当于重新上电,程序从复位向量表开始执行。 |
极其省电 |
简洁来说:
Sleep:CPU 睡觉,高速时钟不停,外设都能跑 → 唤醒快,接着原来的程序继续执行→ 功耗最低降低
Stop:CPU 和 高速速时钟停,SRAM寄存器保留 → 唤醒中等,接着原来的程序继续执行→ 最常用低功耗模式
Standby:芯片几乎断电,SRAM 寄存器全丢 → 唤醒相当于上电复位重启 → 功耗最低
FreeRTOS的Tickless机制用简单明了的话来概括就是:在系统进入空闲任务时,FreeRTOS会关闭SysTick中断,让MCU进入低功耗睡眠模式,此时低功耗定时器(一般是LPTIM或者RTC)继续计数的,MCU被唤醒后读取低功耗定时器计数差值,计算睡了多少 tick,并一次性补齐,由此来实现低功耗的。
注意:
用户只需要开启 configUSE_TICKLESS_IDLE,Tickless低功耗机制就会自动工作!不需要用户手动控制 MCU 低功耗的进入和退出,FreeRTOS 会自动完成。(因为port.c里面已经实现了这些操作)
如何验证你的 STM32 已经进入真正的 Tickless Idle?用 LED、逻辑分析仪、调试方法来看 SysTick 是否停止。
2万+

被折叠的 条评论
为什么被折叠?



