大部分内容都为摘抄
其实已经看过很多遍,但一直没用过。
内存管理
task、queue、semaphores和event group等。为了让FreeRTOS更容易使用,这些内核对象一般都是动态分配:用到时分配,不使用时释放。使用内存的动态管理功能,简化了程序设计:不再需要小心翼翼地提前规划各类对象,简化API函数的涉及,甚至可以减少内存的使用。
FreeRTOS的5种内存管理方法
文件 | 优点 | 缺点 |
---|---|---|
heap_1.c | 分配简单,时间确定 | 只分配、不回收 |
heap_2.c | 动态分配、最佳匹配 | 碎片、时间不定 |
heap_3.c | 调用标准库函数 | 速度慢、时间不定 |
heap_4.c | 相邻空闲内存可合并 | 可解决碎片问题、时间不定 |
heap_5.c | 在heap_4基础上支持分隔的内存块 | 可解决碎片问题、时间不定 |
heap_1.c
实现最简单
没有碎片问题
一些要求非常严格的系统里,不允许使用动态内存,就可以使用heap_1
heap_2.c 弃用
Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。
heap_3.c
Heap_3使用标准C库里的malloc、free函数,所以堆大小由链接器的配置决定,配置项configTOTAL_HEAP_SIZE不再起作用。
C库里的malloc、free函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。
heap_4.c
跟Heap_1、Heap_2一样,Heap_4也是使用大数组来分配内存。
Heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。
heap_5.c
Heap_5分配内存、释放内存的算法跟Heap_4是一样的。
相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。
在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。
既然内存是分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
在使用pvPortMalloc之前,必须先指定内存块的信息
使用vPortDefineHeapRegions来指定这些信息
Heap相关的函数
函数 | 说明 |
---|
void *pvPortMalloc( size_t xWantedSize ) | 分配内存
void vPortFree( void *pv ) | 释放内存
size_t xPortGetFreeHeapSize( void ) | 获取剩余内存
size_t xPortGetMinimumEverFreeHeapSize( void ) | 获取最小剩余内存
malloc失败的钩子函数
在pvPortMalloc函数内部
1 | void * pvPortMalloc( size_t xWantedSize ) |
所以,如果想使用这个钩子函数:
在FreeRTOSConfig.h中,把configUSE_MALLOC_FAILED_HOOK定义为1
提供vApplicationMallocFailedHook函数
pvPortMalloc失败时,才会调用此函数
任务管理
在FreeRTOS中,任务就是一个函数,原型如下:
1 | void ATaskFunction( void *pvParameters ); |
要注意的是:
- 这个函数不能返回
- 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
- 函数内部,尽量使用局部变量:
- 每个任务都有自己的栈
- 每个任务运行这个函数时
- 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
- 不同任务的局部变量,有自己的副本
- 函数使用全局变量、静态变量的话
- 只有一个副本:多个任务使用的是同一个副本
- 要防止冲突(后续会讲)
创建任务
创建任务时使用的函数如下:
1 | BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数 |
参数说明:
参数 | 描述 |
---|---|
pvTaskCode | 函数指针,可以简单地认为任务就是一个C函数。 它稍微特殊一点:永远不退出,或者退出时要调用”vTaskDelete(NULL)” |
pcName | 任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。 长度为:configMAX_TASK_NAME_LEN |
usStackDepth | 每个任务都有自己的栈,这里指定栈大小。 单位是word,比如传入100,表示栈大小为100 word,也就是400字节。 最大值为uint16_t的最大值。 怎么确定栈的大小,并不容易,很多时候是估计。 精确的办法是看反汇编码。 |
pvParameters | 调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters) |
uxPriority | 优先级范围:0~(configMAX_PRIORITIES – 1) 数值越小优先级越低, 如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1) |
pxCreatedTask | 用来保存xTaskCreate的输出结果:task handle。 以后如果想操作这个任务,比如修改它的优先级,就需要这个handle。 如果不想使用该handle,可以传入NULL。 |
返回值 | 成功:pdPASS; 失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足) 注意:文档里都说失败时返回值是pdFAIL,这不对。 pdFAIL是0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY是-1。 |
任务的删除
删除任务时使用的函数如下:
1 | void vTaskDelete( TaskHandle_t xTaskToDelete ); |
参数说明:
参数 | 描述 |
---|---|
pvTaskCode | 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。 也可传入NULL,这表示删除自己。 |
怎么删除任务?举个不好的例子:
- 自杀:
vTaskDelete(NULL)
- 被杀:别的任务执行
vTaskDelete(pvTaskCode)
,pvTaskCode是自己的句柄 - 杀人:执行
vTaskDelete(pvTaskCode)
,pvTaskCode是别的任务的句柄
任务优先级和Tick
任务优先级
在上个示例中我们体验过优先级的使用:高优先级的任务先运行。
优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。
FreeRTOS的调度器可以使用2种方法来快速找出优先级最高的、可以运行的任务。使用不同的方法时,configMAX_PRIORITIES 的取值有所不同。
- 通用方法
使用C函数实现,对所有的架构都是同样的代码。对configMAX_PRIORITIES的取值没有限制。但是configMAX_PRIORITIES的取值还是尽量小,因为取值越大越浪费内存,也浪费时间。
configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为0、或者未定义时,使用此方法。 - 架构相关的优化的方法
架构相关的汇编指令,可以从一个32位的数里快速地找出为1的最高位。使用这些指令,可以快速找出优先级最高的、可以运行的任务。
使用这种方法时,configMAX_PRIORITIES的取值不能超过32。
configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为1时,使用此方法。
在学习调度方法之前,你只要初略地知道:
- FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行
- 对于相同优先级的、可运行的任务,轮流执行
这无需记忆,就像我们举的例子:
- 厨房着火了,当然优先灭火
- 喂饭、回复信息同样重要,轮流做
Tick
对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。
任务状态
以前我们很简单地把任务的状态分为2中:运行(Runing)、非运行(Not Running)。
对于非运行的状态,还可以继续细分,比如前面的FreeRTOS_04_task_priority
中:
- Task3执行vTaskDelay后:处于非运行状态,要过3秒种才能再次运行
- Task3运行期间,Task1、Task2也处于非运行状态,但是它们随时可以运行
- 这两种”非运行”状态就不一样,可以细分为:
- 阻塞状态(Blocked)
- 暂停状态(Suspended)
- 就绪状态(Ready)
Delay函数
有两个Delay函数:
- vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
空闲任务及其钩子函数
为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务:
空闲任务优先级为0:它不能阻碍用户任务运行
空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级为0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务”抢占”(pre-empt)了空闲任务,这是由调度器实现的。
我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:
执行一些低优先级的、后台的、需要连续执行的函数
测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
空闲任务的钩子函数的限制:
不能导致空闲任务进入阻塞状态、暂停状态
如果你会使用vTaskDelete() 来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。
调度算法
配置项 | A | B | C | D | E |
---|---|---|---|---|---|
configUSE_PREEMPTION | 1 | 1 | 1 | 1 | 0 |
configUSE_TIME_SLICING | 1 | 1 | 0 | 0 | x |
configIDLE_SHOULD_YIELD | 1 | 0 | 1 | 0 | x |
说明 | 常用 | 很少用 | 很少用 | 很少用 | 几乎不用 |
注:
A:可抢占+时间片轮转+空闲任务让步
B:可抢占+时间片轮转+空闲任务不让步
C:可抢占+非时间片轮转+空闲任务让步
D:可抢占+非时间片轮转+空闲任务不让步
E:合作调度
同步互斥与通信
队列:
里面可以放任意数据,可以放多个数据
任务、ISR都可以放入数据;任务、ISR都可以从中读出数据
事件组:
一个事件用一bit表示,1表示事件发生了,0表示事件没发生
可以用来表示事件、事件的组合发生了,不能传递数据
有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
信号量:
核心是”计数值”
任务、ISR释放信号量时让计数值加1
任务、ISR获得信号量时,让计数值减1
任务通知:
核心是任务的TCB里的数值
会被覆盖
发通知给谁?必须指定接收任务
只能由接收任务本身获取该通知
互斥量:
数值只有0或1
谁获得互斥量,就必须由谁释放同一个互斥量
队列
队列(queue)可以用于”任务到任务”、”任务到中断”、”中断到任务”直接传输信息。
队列可以包含若干个数据:队列中有若干项,这被称为”长度”(length)
每个数据大小固定
创建队列时就要指定长度、数据大小
数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
也可以强制写队列头部:覆盖头部数据
队列的特性
传输数据的两种方法
FreeRTOS使用拷贝值的方法,这更简单:
局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
无需分配buffer来保存数据,队列中有buffer
局部变量可以马上再次使用
发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
如果数据实在太大,你还是可以使用队列传输它的地址
队列的空间有FreeRTOS内核分配,无需任务操心
对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。
队列阻塞访问
只要知道队列的句柄,谁都可以读、写该队列。任务、ISR都可读、写队列。可以多个任务读写队列。
任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。
某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。
既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?
优先级最高的任务
如果大家的优先级相同,那等待时间最久的任务会进入就绪态
跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。
既然写队列的任务个数没有限制,那么当多个任务写”满队列”时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?
优先级最高的任务
如果大家的优先级相同,那等待时间最久的任务会进入就绪态
队列函数
使用队列的流程:创建队列、写队列、读队列、删除队列。
创建
队列的创建有两种方法:动态分配内存、静态分配内存,
- 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配
函数原型如下:
1 | QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize ); |
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
- 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好
函数原型如下:
1 | QueueHandle_t xQueueCreateStatic( |
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
pucQueueStorageBuffer | 如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组, 此数组大小至少为”uxQueueLength * uxItemSize” |
pxQueueBuffer | 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL |
示例代码:
1 | // 示例代码 |
复位
队列刚被创建时,里面没有数据;使用过程中可以调用xQueueReset()
把队列恢复为初始状态,此函数原型为:
1 | /* pxQueue : 复位哪个队列; |
删除
删除队列的函数为vQueueDelete()
,只能删除使用动态方法创建的队列,它会释放内存。原型如下:
1 | void vQueueDelete( QueueHandle_t xQueue ); |
写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
1 | /* 等同于xQueueSendToBack |
这些函数用到的参数是类似的,统一说明如下:
参数 | 说明 |
---|---|
xQueue | 队列句柄,要写哪个队列 |
pvItemToQueue | 数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 |
返回值 | pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了。 |
读队列
使用xQueueReceive()
函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
1 | BaseType_t xQueueReceive( QueueHandle_t xQueue, |
参数说明如下:
参数 | 说明 |
---|---|
xQueue | 队列句柄,要读哪个队列 |
pvBuffer | bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。 |
查询
可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
1 | /* |
覆盖/偷看
当队列长度为1时,可以使用xQueueOverwrite()
或xQueueOverwriteFromISR()
来覆盖数据。
注意,队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也以为着这些函数不会被阻塞。
函数原型如下:
1 | /* 覆盖队列 |
如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用”窥视”,也就是xQueuePeek()
或xQueuePeekFromISR()
。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么”偷看”时会导致阻塞;一旦队列中有数据,以后每次”偷看”都会成功。
函数原型如下:
1 | /* 偷看队列 |
信号量
前面介绍的队列(queue)可以用于传输数据:在任务之间、任务和中断之间。
有时候我们只需要传递状态,并不需要传递具体的信息,比如:
我的事做完了,通知一下你
卖包子了、卖包子了,做好了1个包子!做好了2个包子!做好了3个包子!
这个停车位我占了,你们只能等着
在这种情况下我们可以使用信号量(semaphore),它更节省内存。
信号量的常规操作
两种信号量的对比
信号量的计数值都有限制:限定了最大值。如果最大值被限定为1,那么它就是二进制信号量;如果最大值不是1,它就是计数型信号量。
差别列表如下:
二进制信号量 | 技术型信号量 |
---|---|
被创建时初始值为0 | 被创建时初始值可以设定 |
其他操作是一样的 | 其他操作是一样的 |
信号量函数
创建
使用信号量之前,要先创建,得到一个句柄;使用信号量时,要使用句柄来表明使用哪个信号量。
对于二进制信号量、计数型信号量,它们的创建函数不一样:
二进制信号量 | 计数型信号量 | |
---|---|---|
动态创建 | xSemaphoreCreateBinary 计数值初始值为0 | xSemaphoreCreateCounting |
vSemaphoreCreateBinary(过时了) 计数值初始值为1 | ||
静态创建 | xSemaphoreCreateBinaryStatic | xSemaphoreCreateCountingStatic |
创建二进制信号量的函数原型如下:
1 | /* 创建一个二进制信号量,返回它的句柄。 |
创建计数型信号量的函数原型如下:
1 | /* 创建一个计数型信号量,返回它的句柄。 |
删除
对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存。
vSemaphoreDelete可以用来删除二进制信号量、计数型信号量,函数原型如下:
1 | /* |
give/take
二进制信号量、计数型信号量的give、take操作函数是一样的。这些函数也分为2个版本:给任务使用,给ISR使用。列表如下:
在任务中使用 | 在ISR中使用 | |
---|---|---|
give | xSemaphoreGive | xSemaphoreGiveFromISR |
take | xSemaphoreTake | xSemaphoreTakeFromISR |
xSemaphoreGive的函数原型如下:
1 | BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ); |
xSemaphoreGive函数的参数与返回值列表如下:
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,释放哪个信号量 |
返回值 | pdTRUE表示成功, 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败 |
pxHigherPriorityTaskWoken的函数原型如下:
1 | BaseType_t xSemaphoreGiveFromISR( |
xSemaphoreGiveFromISR函数的参数与返回值列表如下:
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,释放哪个信号量 |
pxHigherPriorityTaskWoken | 如果释放信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功, 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败 |
xSemaphoreTake的函数原型如下:
1 | BaseType_t xSemaphoreTake( |
xSemaphoreTake函数的参数与返回值列表如下:
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,获取哪个信号量 |
xTicksToWait | 如果无法马上获得信号量,阻塞一会: 0:不阻塞,马上返回 portMAX_DELAY: 一直阻塞直到成功 其他值: 阻塞的Tick个数,可以使用 pdMS_TO_TICKS() 来指定阻塞时间为若干ms |
返回值 | pdTRUE表示成功 |
xSemaphoreTakeFromISR的函数原型如下:
1 | BaseType_t xSemaphoreTakeFromISR( |
xSemaphoreTakeFromISR函数的参数与返回值列表如下:
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,获取哪个信号量 |
pxHigherPriorityTaskWoken | 如果获取信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功 |
互斥量
在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。
比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结果就是A、B的信息混杂在一起。
上述问题的解决方法是:任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变量、函数代码必须被独占地使用,它们被称为临界资源。
互斥量也被称为互斥锁,使用过程如下:
互斥量初始值为1
任务A想访问临界资源,先获得并占有互斥量,然后开始访问
任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞
任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源
任务B使用完毕,释放互斥量
正常来说:在任务A占有互斥量的过程中,任务B、任务C等等,都无法释放互斥量。
但是FreeRTOS未实现这点:任务A占有互斥量的情况下,任务B也可释放互斥量。
函数
创建
互斥量是一种特殊的二进制信号量。
使用互斥量时,先创建、然后去获得、释放它。使用句柄来表示一个互斥量。
创建互斥量的函数有2种:动态分配内存,静态分配内存,函数原型如下:
1 | /* 创建一个互斥量,返回它的句柄。 |
要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:
1 |
其他函数
要注意的是,互斥量不能在ISR中使用。
各类操作函数,比如删除、give/take,跟一般是信号量是一样的。
1 | /* |
事件组
事件组可以简单地认为就是一个整数:
- 每一位表示一个事件
- 每一位事件的含义由程序员决定,比如:Bit0表示用来串口是否就绪,Bit1表示按键是否被按下
- 这些位,值为1表示事件发生了,值为0表示事件没发生
- 一个或多个任务、ISR都可以去写这些位;一个或多个任务、ISR都可以去读这些位
- 可以等待某一位、某些位中的任意一个,也可以等待多位
事件组用一个整数来表示,其中的高8位留给内核使用,只能用其他的位来表示事件。那么这个整数是多少位的?
- 如果configUSE_16_BIT_TICKS是1,那么这个整数就是16位的,低8位用来表示事件
- 如果configUSE_16_BIT_TICKS是0,那么这个整数就是32位的,低24位用来表示事件
- configUSE_16_BIT_TICKS是用来表示Tick Count的,怎么会影响事件组?这只是基于效率来考虑
- 如果configUSE_16_BIT_TICKS是1,就表示该处理器使用16位更高效,所以事件组也使用16位
- 如果configUSE_16_BIT_TICKS是0,就表示该处理器使用32位更高效,所以事件组也使用32位
事件组的操作
事件组和队列、信号量等不太一样,主要集中在2个地方:
- 唤醒谁?
- 队列、信号量:事件发生时,只会唤醒一个任务
- 事件组:事件发生时,会唤醒所有符号条件的任务,简单地说它有”广播”的作用
- 是否清除事件?
- 队列、信号量:是消耗型的资源,队列的数据被读走就没了;信号量被获取后就减少了
- 事件组:被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件
以上图为列,事件组的常规操作如下:
先创建事件组
任务C、D等待事件:
- 等待什么事件?可以等待某一位、某些位中的任意一个,也可以等待多位。简单地说就是”或”、”与”的关系。
- 得到事件时,要不要清除?可选择清除、不清除。
任务A、B产生事件:设置事件组里的某一位、某些位
事件组函数
创建
使用事件组之前,要先创建,得到一个句柄;使用事件组时,要使用句柄来表明使用哪个事件组。
有两种创建方法:动态分配内存、静态分配内存。函数原型如下:
1 | /* 创建一个事件组,返回它的句柄。 |
删除
对于动态创建的事件组,不再需要它们时,可以删除它们以回收内存。
vEventGroupDelete可以用来删除事件组,函数原型如下:
1 | /* |
设置事件
可以设置事件组的某个位、某些位,使用的函数有2个:
- 在任务中使用
xEventGroupSetBits()
- 在ISR中使用
xEventGroupSetBitsFromISR()
有一个或多个任务在等待事件,如果这些事件符合这些任务的期望,那么任务还会被唤醒。
函数原型如下:
1 | /* 设置事件组中的位 |
值得注意的是,ISR中的函数,比如队列函数xQueueSendToBackFromISR
、信号量函数xSemaphoreGiveFromISR
,它们会唤醒某个任务,最多只会唤醒1个任务。
但是设置事件组时,有可能导致多个任务被唤醒,这会带来很大的不确定性。所以xEventGroupSetBitsFromISR
函数不是直接去设置事件组,而是给一个FreeRTOS后台任务(daemon task)发送队列数据,由这个任务来设置事件组。
如果后台任务的优先级比当前被中断的任务优先级高,xEventGroupSetBitsFromISR
会设置*pxHigherPriorityTaskWoken
为pdTRUE。
如果daemon task成功地把队列数据发送给了后台任务,那么xEventGroupSetBitsFromISR
的返回值就是pdPASS。
等待事件
使用xEventGroupWaitBits
来等待事件,可以等待某一位、某些位中的任意一个,也可以等待多位;等到期望的事件后,还可以清除某些位。
函数原型如下:
1 | EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, |
先引入一个概念:unblock condition。一个任务在等待事件发生时,它处于阻塞状态;当期望的时间发生时,这个状态就叫”unblock condition”,非阻塞条件,或称为”非阻塞条件成立”;当”非阻塞条件成立”后,该任务就可以变为就绪态。
函数参数说明列表如下:
参数 | 说明 |
---|---|
xEventGroup | 等待哪个事件组? |
uxBitsToWaitFor | 等待哪些位?哪些位要被测试? |
xWaitForAllBits | 怎么测试?是”AND”还是”OR”? pdTRUE: 等待的位,全部为1; pdFALSE: 等待的位,某一个为1即可 |
xClearOnExit | 函数提出前是否要清除事件? pdTRUE: 清除uxBitsToWaitFor指定的位 pdFALSE: 不清除 |
xTicksToWait | 如果期待的事件未发生,阻塞多久。 可以设置为0:判断后即刻返回; 可设置为portMAX_DELAY:一定等到成功才返回; 可以设置为期望的Tick Count,一般用 pdMS_TO_TICKS() 把ms转换为Tick Count |
返回值 | 返回的是事件值, 如果期待的事件发生了,返回的是”非阻塞条件成立”时的事件值; 如果是超时退出,返回的是超时时刻的事件值。 |
举例如下:
事件组的值 | uxBitsToWaitFor | xWaitForAllBits | 说明 |
---|---|---|---|
0100 | 0101 | pdTRUE | 任务期望bit0,bit2都为1, 当前值只有bit2满足,任务进入阻塞态; 当事件组中bit0,bit2都为1时退出阻塞态 |
0100 | 0110 | pdFALSE | 任务期望bit0,bit2某一个为1, 当前值满足,所以任务成功退出 |
0100 | 0110 | pdTRUE | 任务期望bit1,bit2都为1, 当前值不满足,任务进入阻塞态; 当事件组中bit1,bit2都为1时退出阻塞态 |
你可以使用xEventGroupWaitBits()
等待期望的事件,它发生之后再使用xEventGroupClearBits()
来清除。但是这两个函数之间,有可能被其他任务或中断抢占,它们可能会修改事件组。
可以使用设置xClearOnExit
为pdTRUE,使得对事件组的测试、清零都在xEventGroupWaitBits()
函数内部完成,这是一个原子操作。
同步点
有一个事情需要多个任务协同,比如:
- 任务A:炒菜
- 任务B:买酒
- 任务C:摆台
- A、B、C做好自己的事后,还要等别人做完;大家一起做完,才可开饭
使用xEventGroupSync()
函数可以同步多个任务:
- 可以设置某位、某些位,表示自己做了什么事
- 可以等待某位、某些位,表示要等等其他任务
- 期望的时间发生后,
xEventGroupSync()
才会成功返回。 xEventGroupSync
成功返回后,会清除事件
xEventGroupSync
函数原型如下:
1 | EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, |
参数列表如下:
参数 | 说明 |
---|---|
xEventGroup | 哪个事件组? |
uxBitsToSet | 要设置哪些事件?我完成了哪些事件? 比如0x05(二进制为0101)会导致事件组的bit0,bit2被设置为1 |
uxBitsToWaitFor | 等待那个位、哪些位? 比如0x15(二级制10101),表示要等待bit0,bit2,bit4都为1 |
xTicksToWait | 如果期待的事件未发生,阻塞多久。 可以设置为0:判断后即刻返回; 可设置为portMAX_DELAY:一定等到成功才返回; 可以设置为期望的Tick Count,一般用 pdMS_TO_TICKS() 把ms转换为Tick Count |
返回值 | 返回的是事件值, 如果期待的事件发生了,返回的是”非阻塞条件成立”时的事件值; 如果是超时退出,返回的是超时时刻的事件值。 |
任务通知
所谓”任务通知”,你可以反过来读”通知任务”。
使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的”通知”:
优势及限制
任务通知的优势:
- 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
- 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
任务通知的限制:
- 不能发送数据给ISR:
ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。 - 数据只能给该任务独享
使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。 - 无法缓冲数据
使用队列时,假设队列深度为N,那么它可以保持N个数据。
使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。 - 无法广播给多个任务
使用事件组可以同时给多个任务发送事件。
使用任务通知,只能发个一个任务。 - 如果发送受阻,发送方无法进入阻塞状态等待
假设队列已经满了,使用xQueueSendToBack()
给队列发送数据时,任务可以进入阻塞状态等待发送完成。
使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。
通知状态和通知值
每个任务都有一个结构体:TCB(Task Control Block),里面有2个成员:
- 一个是uint8_t类型,用来表示通知状态
- 一个是uint32_t类型,用来表示通知值
1 | typedef struct tskTaskControlBlock |
通知状态有3种取值:
- taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
- taskWAITING_NOTIFICATION:任务在等待通知
- taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为pending(有数据了,待处理)
1 |
通知值可以有很多种类型:
- 计数值
- 位(类似事件组)
- 任意数值
任务通知的使用
使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。
两类函数
任务通知有2套函数,简化版、专业版,列表如下:
- 简化版函数的使用比较简单,它实际上也是使用专业版函数实现的
- 专业版函数支持很多参数,可以实现很多功能
简化版 | 专业版 | |
---|---|---|
发出通知 | xTaskNotifyGive vTaskNotifyGiveFromISR | xTaskNotify xTaskNotifyFromISR |
取出通知 | ulTaskNotifyTake | xTaskNotifyWait |
xTaskNotifyGive/ulTaskNotifyTake
在任务中使用xTaskNotifyGive函数,在ISR中使用vTaskNotifyGiveFromISR函数,都是直接给其他任务发送通知:
- 使得通知值加一
- 并使得通知状态变为”pending”,也就是
taskNOTIFICATION_RECEIVED
,表示有数据了、待处理
可以使用ulTaskNotifyTake函数来取出通知值:
- 如果通知值等于0,则阻塞(可以指定超时时间)
- 当通知值大于0时,任务从阻塞态进入就绪态
- 在ulTaskNotifyTake返回之前,还可以做些清理工作:把通知值减一,或者把通知值清零
使用ulTaskNotifyTake函数可以实现轻量级的、高效的二进制信号量、计数型信号量。
这几个函数的原型如下:
1 | BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); |
xTaskNotifyGive函数的参数说明如下:
参数 | 说明 |
---|---|
xTaskToNotify | 任务句柄(创建任务时得到),给哪个任务发通知 |
返回值 | 必定返回pdPASS |
vTaskNotifyGiveFromISR函数的参数说明如下:
参数 | 说明 |
---|---|
xTaskHandle | 任务句柄(创建任务时得到),给哪个任务发通知 |
pxHigherPriorityTaskWoken | 被通知的任务,可能正处于阻塞状态。 此函数发出通知后,会把它从阻塞状态切换为就绪态。 如果被唤醒的任务的优先级,高于当前任务的优先级, 则”*pxHigherPriorityTaskWoken”被设置为pdTRUE, 这表示在中断返回之前要进行任务切换。 |
ulTaskNotifyTake函数的参数说明如下:
参数 | 说明 |
---|---|
xClearCountOnExit | 函数返回前是否清零: pdTRUE:把通知值清零 pdFALSE:如果通知值大于0,则把通知值减一 |
xTicksToWait | 任务进入阻塞态的超时时间,它在等待通知值大于0。 0:不等待,即刻返回; portMAX_DELAY:一直等待,直到通知值大于0; 其他值:Tick Count,可以用 pdMS_TO_TICKS() 把ms转换为Tick Count |
返回值 | 函数返回之前,在清零或减一之前的通知值。 如果xTicksToWait非0,则返回值有2种情况: 1. 大于0:在超时前,通知值被增加了 2. 等于0:一直没有其他任务增加通知值,最后超时返回0 |
xTaskNotify/xTaskNotifyWait
xTaskNotify
函数功能更强大,可以使用不同参数实现各类功能,比如:
- 让接收任务的通知值加一:这时
xTaskNotify()
等同于xTaskNotifyGive()
- 设置接收任务的通知值的某一位、某些位,这就是一个轻量级的、更高效的事件组
- 把一个新值写入接收任务的通知值:上一次的通知值被读走后,写入才成功。这就是轻量级的、长度为1的队列
- 用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。类似
xQueueOverwrite()
函数,这就是轻量级的邮箱。
xTaskNotify()
比xTaskNotifyGive()
更灵活、强大,使用上也就更复杂。xTaskNotifyFromISR()
是它对应的ISR版本。
这两个函数用来发出任务通知,使用哪个函数来取出任务通知呢?
使用xTaskNotifyWait()
函数!它比ulTaskNotifyTake()
更复杂:
- 可以让任务等待(可以加上超时时间),等到任务状态为”pending”(也就是有数据)
- 还可以在函数进入、退出时,清除通知值的指定位
这几个函数的原型如下:
1 | BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction ); |
xTaskNotify函数的参数说明如下:
参数 | 说明 |
---|---|
xTaskToNotify | 任务句柄(创建任务时得到),给哪个任务发通知 |
ulValue | 怎么使用ulValue,由eAction参数决定 |
eAction | 见下表 |
返回值 | pdPASS:成功,大部分调用都会成功 pdFAIL:只有一种情况会失败,当eAction为eSetValueWithoutOverwrite, 并且通知状态为”pending”(表示有新数据未读),这时就会失败。 |
eNotifyAction参数说明:
eNotifyAction取值 | 说明 |
---|---|
eNoAction | 仅仅是更新通知状态为”pending”,未使用ulValue。 这个选项相当于轻量级的、更高效的二进制信号量。 |
eSetBits | 通知值 = 原来的通知值 | ulValue,按位或。 相当于轻量级的、更高效的事件组。 |
eIncrement | 通知值 = 原来的通知值 + 1,未使用ulValue。 相当于轻量级的、更高效的二进制信号量、计数型信号量。 相当于 xTaskNotifyGive() 函数。 |
eSetValueWithoutOverwrite | 不覆盖。 如果通知状态为”pending”(表示有数据未读), 则此次调用xTaskNotify不做任何事,返回pdFAIL。 如果通知状态不是”pending”(表示没有新数据), 则:通知值 = ulValue。 |
eSetValueWithOverwrite | 覆盖。 无论如何,不管通知状态是否为”pendng”, 通知值 = ulValue。 |
xTaskNotifyFromISR函数跟xTaskNotify很类似,就多了最后一个参数pxHigherPriorityTaskWoken
。在很多ISR函数中,这个参数的作用都是类似的,使用场景如下:
- 被通知的任务,可能正处于阻塞状态
xTaskNotifyFromISR
函数发出通知后,会把接收任务从阻塞状态切换为就绪态- 如果被唤醒的任务的优先级,高于当前任务的优先级,则”*pxHigherPriorityTaskWoken”被设置为pdTRUE,这表示在中断返回之前要进行任务切换。
xTaskNotifyWait函数列表如下:
参数 | 说明 |
---|---|
ulBitsToClearOnEntry | 在xTaskNotifyWait入口处,要清除通知值的哪些位? 通知状态不是”pending”的情况下,才会清除。 它的本意是:我想等待某些事件发生,所以先把”旧数据”的某些位清零。 能清零的话:通知值 = 通知值 & ~(ulBitsToClearOnEntry)。 比如传入0x01,表示清除通知值的bit0; 传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0 |
ulBitsToClearOnExit | 在xTaskNotifyWait出口处,如果不是因为超时推出,而是因为得到了数据而退出时: 通知值 = 通知值 & ~(ulBitsToClearOnExit)。 在清除某些位之前,通知值先被赋给”*pulNotificationValue”。 比如入0x03,表示清除通知值的bit0、bit1; 传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0 |
pulNotificationValue | 用来取出通知值。 在函数退出时,使用ulBitsToClearOnExit清除之前,把通知值赋给”*pulNotificationValue”。 如果不需要取出通知值,可以设为NULL。 |
xTicksToWait | 任务进入阻塞态的超时时间,它在等待通知状态变为”pending”。 0:不等待,即刻返回; portMAX_DELAY:一直等待,直到通知状态变为”pending”; 其他值:Tick Count,可以用 pdMS_TO_TICKS() 把ms转换为Tick Count |
返回值 | 1. pdPASS:成功 这表示xTaskNotifyWait成功获得了通知: 可能是调用函数之前,通知状态就是”pending”; 也可能是在阻塞期间,通知状态变为了”pending”。 2. pdFAIL:没有得到通知。 |
软件定时器
软件定时器的特性
我们在手机上添加闹钟时,需要指定时间、指定类型(一次性的,还是周期性的)、指定做什么事;还有一些过时的、不再使用的闹钟。
使用定时器跟使用手机闹钟是类似的:
- 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)。
- 指定类型,定时器有两种类型:
- 一次性(One-shot timers):
这类定时器启动后,它的回调函数只会被调用一次;
可以手工再次启动它,但是不会自动启动它。 - 自动加载定时器(Auto-reload timers ):
这类定时器启动后,时间到之后它会自动启动它;
这使得回调函数被周期性地调用。
- 一次性(One-shot timers):
- 指定要做什么事,就是指定回调函数
实际的闹钟分为:有效、无效两类。软件定时器也是类似的,它由两种状态:
- 运行(Running、Active):运行态的定时器,当指定时间到达之后,它的回调函数会被调用
- 冬眠(Dormant):冬眠态的定时器还可以通过句柄来访问它,但是它不再运行,它的回调函数不会被调用
定时器运行情况示例如下:
- Timer1:它是一次性的定时器,在t1启动,周期是6个Tick。经过6个tick后,在t7执行回调函数。它的回调函数只会被执行一次,然后该定时器进入冬眠状态。
- Timer2:它是自动加载的定时器,在t1启动,周期是5个Tick。每经过5个tick它的回调函数都被执行,比如在t6、t11、t16都会执行。
软件定时器的上下文
守护任务
要理解软件定时器API函数的参数,特别是里面的xTicksToWait
,需要知道定时器执行的过程。
FreeRTOS中有一个Tick中断,软件定时器基于Tick来运行。在哪里执行定时器函数?第一印象就是在Tick中断里执行:
- 在Tick中断中判断定时器是否超时
- 如果超时了,调用它的回调函数
FreeRTOS是RTOS,它不允许在内核、在中断中执行不确定的代码:如果定时器函数很耗时,会影响整个系统。
所以,FreeRTOS中,不在Tick中断中执行定时器函数。
在哪里执行?在某个任务里执行,这个任务就是:RTOS Damemon Task,RTOS守护任务。以前被称为”Timer server”,但是这个任务要做并不仅仅是定时器相关,所以改名为:RTOS Damemon Task。
当FreeRTOS的配置项configUSE_TIMERS
被设置为1时,在启动调度器时,会自动创建RTOS Damemon Task。
我们自己编写的任务函数要使用定时器时,是通过”定时器命令队列”(timer command queue)和守护任务交互,如下图所示:
守护任务的优先级为:configTIMER_TASK_PRIORITY;定时器命令队列的长度为configTIMER_QUEUE_LENGTH。
守护任务的调度
守护任务的调度,跟普通的任务并无差别。当守护任务是当前优先级最高的就绪态任务时,它就可以运行。它的工作有两类:
- 处理命令:从命令队列里取出命令、处理
- 执行定时器的回调函数
能否及时处理定时器的命令、能否及时执行定时器的回调函数,严重依赖于守护任务的优先级。下面使用2个例子来演示。
例子1:守护任务的优先性级较低
t1:Task1处于运行态,守护任务处于阻塞态。
守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。
至于守护任务能否马上执行,取决于它的优先级。t2:Task1调用
xTimerStart()
要注意的是,xTimerStart()
只是把”start timer”的命令发给”定时器命令队列”,使得守护任务退出阻塞态。
在本例中,Task1的优先级高于守护任务,所以守护任务无法抢占Task1。t3:Task1执行完
xTimerStart()
但是定时器的启动工作由守护任务来实现,所以xTimerStart()
返回并不表示定时器已经被启动了。t4:Task1由于某些原因进入阻塞态,现在轮到守护任务运行。
守护任务从队列中取出”start timer”命令,启动定时器。t5:守护任务处理完队列中所有的命令,再次进入阻塞态。Idel任务时优先级最高的就绪态任务,它执行。
注意:假设定时器在后续某个时刻tX超时了,超时时间是”tX-t2”,而非”tX-t4”,从
xTimerStart()
函数被调用时算起。
例子2:守护任务的优先性级较高
t1:Task1处于运行态,守护任务处于阻塞态。
守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。
至于守护任务能否马上执行,取决于它的优先级。t2:Task1调用
xTimerStart()
要注意的是,xTimerStart()
只是把”start timer”的命令发给”定时器命令队列”,使得守护任务退出阻塞态。
在本例中,守护任务的优先级高于Task1,所以守护任务抢占Task1,守护任务开始处理命令队列。
Task1在执行xTimerStart()
的过程中被抢占,这时它无法完成此函数。t3:守护任务处理完命令队列中所有的命令,再次进入阻塞态。
此时Task1是优先级最高的就绪态任务,它开始执行。t4:Task1之前被守护任务抢占,对
xTimerStart()
的调用尚未返回。现在开始继续运行次函数、返回。t5:Task1由于某些原因进入阻塞态,进入阻塞态。Idel任务时优先级最高的就绪态任务,它执行。
注意,定时器的超时时间是基于调用xTimerStart()
的时刻tX,而不是基于守护任务处理命令的时刻tY。假设超时时间是10个Tick,超时时间是”tX+10”,而非”tY+10”。
回调函数
定时器的回调函数的原型如下:
1 | void ATimerCallback( TimerHandle_t xTimer ); |
定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。
所以,定时器的回调函数不要影响其他人:
回调函数要尽快实行,不能进入阻塞状态
不要调用会导致阻塞的API函数,比如
vTaskDelay()
可以调用
xQueueReceive()
之类的函数,但是超时时间要设为0:即刻返回,不可阻塞
软件定时器的函数
根据定时器的状态转换图,就可以知道所涉及的函数:
创建
要使用定时器,需要先创建它,得到它的句柄。
有两种方法创建定时器:动态分配内存、静态分配内存。函数原型如下:
1 | /* 使用动态分配内存的方法创建定时器 |
回调函数的类型是:
1 | void ATimerCallback( TimerHandle_t xTimer ); |
删除
动态分配的定时器,不再需要时可以删除掉以回收内存。删除函数原型如下:
1 | /* 删除定时器 |
定时器的很多API函数,都是通过发送”命令”到命令队列,由守护任务来实现。
如果队列满了,”命令”就无法即刻写入队列。我们可以指定一个超时时间xTicksToWait
,等待一会。
启动/停止
启动定时器就是设置它的状态为运行态(Running、Active)。
停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。
涉及的函数原型如下:
1 | /* 启动定时器 |
注意,这些函数的xTicksToWait
表示的是,把命令写入命令队列的超时时间。命令队列可能已经满了,无法马上把命令写入队列里,可以等待一会。
xTicksToWait
不是定时器本身的超时时间,不是定时器本身的”周期”。
创建定时器时,设置了它的周期(period)。xTimerStart()
函数是用来启动定时器。假设调用xTimerStart()
的时刻是tX,定时器的周期是n,那么在tX+n
时刻定时器的回调函数被调用。
如果定时器已经被启动,但是它的函数尚未被执行,再次执行xTimerStart()
函数相当于执行xTimerReset()
,重新设定它的启动时间。
复位
从定时器的状态转换图可以知道,使用xTimerReset()
函数可以让定时器的状态从冬眠态转换为运行态,相当于使用xTimerStart()
函数。
如果定时器已经处于运行态,使用xTimerReset()
函数就相当于重新确定超时时间。假设调用xTimerReset()
的时刻是tX,定时器的周期是n,那么tX+n
就是重新确定的超时时间。
复位函数的原型如下:
1 | /* 复位定时器 |
修改周期
从定时器的状态转换图可以知道,使用xTimerChangePeriod()
函数,处理能修改它的周期外,还可以让定时器的状态从冬眠态转换为运行态。
修改定时器的周期时,会使用新的周期重新计算它的超时时间。假设调用xTimerChangePeriod()
函数的时间tX,新的周期是n,则tX+n
就是新的超时时间。
相关函数的原型如下:
1 | /* 修改定时器的周期 |
定时器ID
定时器的结构体如下,里面有一项pvTimerID
,它就是定时器ID:
怎么使用定时器ID,完全由程序来决定:
- 可以用来标记定时器,表示自己是什么定时器
- 可以用来保存参数,给回调函数使用
它的初始值在创建定时器时由xTimerCreate()
这类函数传入,后续可以使用这些函数来操作:
- 更新ID:使用
vTimerSetTimerID()
函数 - 查询ID:查询
pvTimerGetTimerID()
函数
这两个函数不涉及命令队列,它们是直接操作定时器结构体。
函数原型如下:
1 | /* 获得定时器的ID |
中断管理
两套API函数
为什么需要两套API
在任务函数中,我们可以调用各类API函数,比如队列操作函数:xQueueSendToBack。但是在ISR中使用这个函数会导致问题,应该使用另一个函数:xQueueSendToBackFromISR,它的函数名含有后缀”FromISR”,表示”从ISR中给队列发送数据”。
FreeRTOS中很多API函数都有两套:一套在任务中使用,另一套在ISR中使用。后者的函数名含有”FromISR”后缀。
为什么要引入两套API函数?
- 很多API函数会导致任务计入阻塞状态:
- 运行这个函数的任务进入阻塞状态
- 比如写队列时,如果队列已满,可以进入阻塞状态等待一会
- ISR调用API函数时,ISR不是”任务”,ISR不能进入阻塞状态
- 所以,在任务中、在ISR中,这些函数的功能是有差别的
为什么不使用同一套函数,比如在函数里面分辨当前调用者是任务还是ISR呢?示例代码如下:
1 | BaseType_t xQueueSend(...) |
FreeRTOS使用两套函数,而不是使用一套函数,是因为有如下好处:
使用同一套函数的话,需要增加额外的判断代码、增加额外的分支,是的函数更长、更复杂、难以测试
在任务、ISR中调用时,需要的参数不一样,比如:
- 在任务中调用:需要指定超时时间,表示如果不成功就阻塞一会
- 在ISR中调用:不需要指定超时时间,无论是否成功都要即刻返回
- 如果强行把两套函数揉在一起,会导致参数臃肿、无效
移植FreeRTOS时,还需要提供监测上下文的函数,比如
is_in_isr()
有些处理器架构没有办法轻易分辨当前是处于任务中,还是处于ISR中,就需要额外添加更多、更复杂的代码
使用两套函数可以让程序更高效,但是也有一些缺点,比如你要使用第三方库函数时,即会在任务中调用它,也会在ISR总调用它。这个第三方库函数用到了FreeRTOS的API函数,你无法修改库函数。这个问题可以解决:
- 把中断的处理推迟到任务中进行(Defer interrupt processing),在任务中调用库函数
- 尝试在库函数中使用”FromISR”函数:
- 在任务中、在ISR中都可以调用”FromISR”函数
- 反过来就不行,非FromISR函数无法在ISR中使用
- 第三方库函数也许会提供OS抽象层,自行判断当前环境是在任务还是在ISR中,分别调用不同的函数
两套API函数列表
类型 | 在任务中 | 在ISR中 |
---|---|---|
队列(queue) | xQueueSendToBack | xQueueSendToBackFromISR |
xQueueSendToFront | xQueueSendToFrontFromISR | |
xQueueReceive | xQueueReceiveFromISR | |
xQueueOverwrite | xQueueOverwriteFromISR | |
xQueuePeek | xQueuePeekFromISR | |
信号量(semaphore) | xSemaphoreGive | xSemaphoreGiveFromISR |
xSemaphoreTake | xSemaphoreTakeFromISR | |
事件组(event group) | xEventGroupSetBits | xEventGroupSetBitsFromISR |
xEventGroupGetBits | xEventGroupGetBitsFromISR | |
任务通知(task notification) | xTaskNotifyGive | vTaskNotifyGiveFromISR |
xTaskNotify | xTaskNotifyFromISR | |
软件定时器(software timer) | xTimerStart | xTimerStartFromISR |
xTimerStop | xTimerStopFromISR | |
xTimerReset | xTimerResetFromISR | |
xTimerChangePeriod | xTimerChangePeriodFromISR |
xHigherPriorityTaskWoken参数
xHigherPriorityTaskWoken的含义是:是否有更高优先级的任务被唤醒了。如果为pdTRUE,则意味着后面要进行任务切换。
还是以写队列为例。
任务A调用xQueueSendToBack()
写队列,有几种情况发生:
- 队列满了,任务A阻塞等待,另一个任务B运行
- 队列没满,任务A成功写入队列,但是它导致另一个任务B被唤醒,任务B的优先级更高:任务B先运行
- 队列没满,任务A成功写入队列,即刻返回
可以看到,在任务中调用API函数可能导致任务阻塞、任务切换,这叫做”context switch”,上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换。
xQueueSendToBackFromISR()
函数也可能导致任务切换,但是不会在函数内部进行切换,而是返回一个参数:表示是否需要切换,函数原型与用法如下:
1 | /* |
pxHigherPriorityTaskWoken参数,就是用来保存函数的结果:是否需要切换
- *pxHigherPriorityTaskWoken等于pdTRUE:函数的操作导致更高优先级的任务就绪了,ISR应该进行任务切换
- *pxHigherPriorityTaskWoken等于pdFALSE:没有进行任务切换的必要
为什么不在”FromISR”函数内部进行任务切换,而只是标记一下而已呢?为了效率!示例代码如下:
1 | void XXX_ISR() |
ISR中有可能多次调用”FromISR”函数,如果在”FromISR”内部进行任务切换,会浪费时间。解决方法是:
- 在”FromISR”中标记是否需要切换
- 在ISR返回之前再进行任务切换
- 示例代码如下
1 | void XXX_ISR() |
上述的例子很常见,比如UART中断:在UART的ISR中读取多个字符,发现收到回车符时才进行任务切换。
在ISR中调用API时不进行任务切换,而只是在”xHigherPriorityTaskWoken”中标记一下,除了效率,还有多种好处:
- 效率高:避免不必要的任务切换
- 让ISR更可控:中断随机产生,在API中进行任务切换的话,可能导致问题更复杂
- 可移植性
- 在Tick中断中,调用
vApplicationTickHook()
:它运行与ISR,只能使用”FromISR”的函数
使用”FromISR”函数时,如果不想使用xHigherPriorityTaskWoken参数,可以设置为NULL。
怎么切换任务
FreeRTOS的ISR函数中,使用两个宏进行任务切换:
1 | portEND_SWITCHING_ISR( xHigherPriorityTaskWoken ); |
这两个宏做的事情是完全一样的,在老版本的FreeRTOS中,
portEND_SWITCHING_ISR
使用汇编实现portYIELD_FROM_ISR
使用C语言实现
新版本都统一使用portYIELD_FROM_ISR
。
使用示例如下:
1 | void XXX_ISR() |
中断的延迟处理
前面讲过,ISR要尽量快,否则:
- 其他低优先级的中断无法被处理:实时性无法保证
- 用户任务无法被执行:系统显得很卡顿
- 如果运行中断嵌套,这会更复杂,ISR越快执行约有助于中断嵌套
如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为2部分:
- ISR:尽快做些清理、记录工作,然后触发某个任务
- 任务:更复杂的事情放在任务中处理
这种处理方式叫”中断的延迟处理”(Deferring interrupt processing),处理流程如下图所示:
- t1:任务1运行,任务2阻塞
- t2:发生中断,
- 该中断的ISR函数被执行,任务1被打断
- ISR函数要尽快能快速地运行,它做一些必要的操作(比如清除中断),然后唤醒任务2
- t3:在创建任务时设置任务2的优先级比任务1高(这取决于设计者),所以ISR返回后,运行的是任务2,它要完成中断的处理。任务2就被称为”deferred processing task”,中断的延迟处理任务。
- t4:任务2处理完中断后,进入阻塞态以等待下一个中断,任务1重新运行
中断与任务间的通信
前面讲解过的队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。
要注意的是,在ISR中使用的函数要有”FromISR”后缀。
资源管理
屏蔽中断
屏蔽中断有两套宏:任务中使用、ISR中使用:
- 任务中使用:
taskENTER_CRITICA()/taskEXIT_CRITICAL()
- ISR中使用:
taskENTER_CRITICAL_FROM_ISR()/taskEXIT_CRITICAL_FROM_ISR()
在任务中屏蔽中断
在任务中屏蔽中断的示例代码如下:
1 | /* 在任务中,当前时刻中断是使能的 |
在taskENTER_CRITICA()/taskEXIT_CRITICAL()
之间:
- 低优先级的中断被屏蔽了:优先级低于、等于
configMAX_SYSCALL_INTERRUPT_PRIORITY
- 高优先级的中断可以产生:优先级高于
configMAX_SYSCALL_INTERRUPT_PRIORITY
- 但是,这些中断ISR里,不允许使用FreeRTOS的API函数
- 任务调度依赖于中断、依赖于API函数,所以:这两段代码之间,不会有任务调度产生
这套taskENTER_CRITICA()/taskEXIT_CRITICAL()
宏,是可以递归使用的,它的内部会记录嵌套的深度,只有嵌套深度变为0时,调用taskEXIT_CRITICAL()
才会重新使能中断。
使用taskENTER_CRITICA()/taskEXIT_CRITICAL()
来访问临界资源是很粗鲁的方法:
- 中断无法正常运行
- 任务调度无法进行
- 所以,之间的代码要尽可能快速地执行
在ISR中屏蔽中断
要使用含有”FROM_ISR”后缀的宏,示例代码如下:
1 | void vAnInterruptServiceRoutine( void ) |
在taskENTER_CRITICA_FROM_ISR()/taskEXIT_CRITICAL_FROM_ISR()
之间:
- 低优先级的中断被屏蔽了:优先级低于、等于
configMAX_SYSCALL_INTERRUPT_PRIORITY
- 高优先级的中断可以产生:优先级高于
configMAX_SYSCALL_INTERRUPT_PRIORITY
- 但是,这些中断ISR里,不允许使用FreeRTOS的API函数
- 任务调度依赖于中断、依赖于API函数,所以:这两段代码之间,不会有任务调度产生
暂停调度器
如果有别的任务来跟你竞争临界资源,你可以把中断关掉:这当然可以禁止别的任务运行,但是这代价太大了。它会影响到中断的处理。
如果只是禁止别的任务来跟你竞争,不需要关中断,暂停调度器就可以了:在这期间,中断还是可以发生、处理。
使用这2个函数来暂停、恢复调度器:
1 | /* 暂停调度器 */ |
示例代码如下:
1 | vTaskSuspendScheduler(); |
这套vTaskSuspendScheduler()/xTaskResumeScheduler()
宏,是可以递归使用的,它的内部会记录嵌套的深度,只有嵌套深度变为0时,调用taskEXIT_CRITICAL()
才会重新使能中断。
本文作者:wxy
本文链接: https://c.undf.top/posts/1rvt88k/
文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。
本站不设评论,网站底部邮件图标可以联系博主
评论
如评论失效,请点击最下方的邮件图标联系作者。