主要学习韦东山FreeRTOS的操作。
FreeRTOS概述与体验
FreeRTOS主要内容
| FreeRTOS/Source下的文件 | 作用 |
|---|---|
| task.c | 任务操作 |
| list.c | 列表 |
| queue.c | 提供队列操作、信号量操作 |
| timer.c | 软件定时功能 |
| event_groups.c | 通过事件组功能 |
数据类型
- TickType_t:
- FreeRTOS配置了一个周期性的时钟中断:Tick Interrupt
- 每发生一次中断,中断次数累加,这被称为tick count
- tick count这个变量的类型就是TickType_t
- TickType_t可以是16位的,也可以是32位的
- FreeRTOSConfig.h中定义configUSE_16_BIT_TICKS时,TickType_t就是uint16_t
- 否则TickType_t就是uint32_t
- BaseType_t:
- 这是该架构最高效的数据类型
- 32位架构中,它就是uint32_t
- 16位架构中,它就是uint16_t
- 8位架构中,它就是uint8_t
- BaseType_t通常用作简单的返回值的类型,还有逻辑值,比如
pdTRUE/pdFALSE
变量名
| 变量名前缀 | 含义 |
|---|---|
| c | char |
| s | int16_t, short |
| l | int32_t, long |
| x | BaseType_t,结构体等 |
| u | unsigned |
| p | 指针 |
| uc | uint8_t,unsigned char |
| pc | char指针 |
函数名
| 函数名前缀 | 含义 |
|---|---|
| vTaskPrioritySet | 返回值类型:void,在task.c中定义 |
| xQueueReceive | 返回值类型:BaseType_t,在queue.c中定义 |
| pvTimerGetTimerID | 返回值类型:pointer to void,在timer.c中定义 |
宏名
| 宏的前缀 | 在哪个文件定义 |
|---|---|
| port (比如portMAX_DELAY) | portable.h或portmacro.h |
| task (比如taskENTER_CRITICAL()) | task.h |
| pd (比如pdTRUE) | projdefs.h |
| config (比如configUSE_PREEMPTION) | FreeRTOSConfig.h |
| err (比如errQUEUE_FULL) | projdefs.h |
一般的宏定义
| 宏 | 值 |
|---|---|
| pdTRUE | 1 |
| pdFALSE | 0 |
| pdPASS | 1 |
| pdFAIL | 0 |
内存管理
FreeRTOS的内存管理包括静态内存管理和动态内存管理。
- 堆:heap,由程序员自己malloc一块空间,用完后free标记为"空闲"
- 栈:stack,函数调用时局部变量保存在栈中,当前程序环境也是保存在栈中。
五种内存管理方法
| 文件 | 优点 | 缺点 |
|---|---|---|
| heap_1.c | 分配简单,时间确定,没有碎片 | 只分配,不回收 |
| heap_2.c | 动态分配,最佳匹配 | 有碎片、时间不懂 |
| heap_3.c | 调用标准库函数 | 速度慢、时间不定 |
| heap_4.c | 相邻的空闲碎片可合并 | 时间不定 |
| heap_5.c | 在heap_4.c基础上支持分隔的内存块 | 时间不定 |
FreeRTOS在创建任务时,需要2个内核对象:task control block(TCB)、stack。
heap_1
-
只实现了pvPortMalloc,并没有实现vPortFree。
-
如果程序不需要删除内核对象,可以使用它
heap_2
相较于heap_1
- Heap_2使用最佳匹配算法(best fit)来分配内存
- 它支持vPortFree
Q:这里的最佳匹配是怎么操作的?
举例,一个5 10 15内存的空间,此时需要存入一个6大小的数据,那么就存放在10里面,变成5 4 15;后续又需要存放一个4大小的数据,就变成5 0 15。
heap_3
- 使用标准C库里的malloc、free函数
Q: heap_3线程安全吗?
C库里的malloc、free函数并非线程安全的,heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。
heap_4
- heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。
Q:这里的首次匹配是怎么操作的?
举例,一个10 5 3内存的空间,此时需要存入一个6大小的数据,那么就存放在10里面,变成4 5 3;后续又需要存放一个3大小的数据,就变成1 5 3。
heap_5
- 相比于heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。
heap相关的函数
pvPortMalloc/vPortFree
1 | void * pvPortMalloc( size_t xWantedSize ); // 分配内存,如果分配内存不成功,则返回值为NULL。 |
作用:分配内存、释放内存。
如果分配内存不成功,则返回值为NULL。
xPortGetFreeHeapSize
1 | size_t xPortGetFreeHeapSize( void ); |
作用:当前还有多少空闲内存,heap_3中无法使用。
xPortGetMinimumEverFreeHeapSize
1 | size_t xPortGetMinimumEverFreeHeapSize( void ); |
作用:空闲内存容量的最小值。
注意:只有heap_4、heap_5支持此函数
任务管理
任务状态:就绪态、阻塞态、挂起态、进行态
任务创建
1 | BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数 |
同时创建两个程序,同优先级下,先执行最后创建的。
1 | xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL); |
多个任务可以使用同一个函数,怎么体现它们的差别?
- 栈不同
- 创建任务时可以传入不同的参数
1 | static const char *pcTextForTask1 = "T1 run\r\n"; |
任务删除
1 | void vTaskDelete( TaskHandle_t xTaskToDelete ); |
Q:怎么删除任务?
- 自杀:vTaskDelete(NULL)
- 被杀:别的任务执行vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄
- 杀人:执行vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄
任务优先级与Tick
高优先级的任务先运行,可选择0 ~ (configMAX_PRIORITIES – 1)
- FreeRTOS会确保最高优先级的、可运行的任务立马执行
- 对于相同优先级的可执行任务,轮流执行
对于相同优先级的,通过Tick(滴答)中断实现,但它并不精确。
1 | vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms |
可以使用uxTaskPriorityGet来获得任务的优先级:
1 | UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask ); |
使用vTaskPrioritySet 来设置任务的优先级:
1 | void vTaskPrioritySet( TaskHandle_t xTask, //具体任务 |
任务状态
Delay函数
- vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
1 | void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给Tick */ |
空闲任务及其钩子函数
空闲任务(Idle任务)的作用:释放被删除的任务的内存
在使用vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务:
- 空闲任务优先级为0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
如果使用vTaskDelete() 来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。
钩子函数作用
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
- 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
钩子函数限制
- 不能导致空闲任务进入阻塞状态、暂停状态
- 如果你会使用
vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。
如何使用钩子函数
FreeRTOS\Source\tasks.c中设置configUSE_IDLE_HOOK = 1,实现vApplicationIdleHook函数。
调度算法
| 配置项 | 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:合作调度
同步互斥与通信
能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)
- 队列:
- 里面可以放任意数据,可以放多个数据
- 任务、ISR都可以放入数据;任务、ISR都可以从中读出数据
- 事件组:
- 一个事件用一bit表示,1表示事件发生了,0表示事件没发生
- 可以用来表示事件、事件的组合发生了,不能传递数据
- 有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
- 信号量:
- 核心是"计数值"
- 任务、ISR释放信号量时让计数值加1
- 任务、ISR获得信号量时,让计数值减1
- 任务通知:
- 核心是任务的TCB里的数值
- 会被覆盖
- 发通知给谁?必须指定接收任务
- 只能由接收任务本身获取该通知
- 互斥量:
- 数值只有0或1
- 谁获得互斥量,就必须由谁释放同一个互斥量
队列
特性
- 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
- 创建队列时就要指定长度、数据大小(大小固定)
- 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
- 也可以强制写队列头部:覆盖头部数据
队列传输数据两种方法:
- 拷贝:把数据、把变量的值复制进队列
- 引用:把数据、把变量的地址复制进队列
有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?
- 优先级最高的任务
- 如果大家的优先级相同,那等待时间最久的任务会进入就绪态
队列函数
创建队列
- 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配
1 | QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize ); |
| 参数 | 说明 |
|---|---|
| uxQueueLength | 队列长度,最多能存放多少个数据(item) |
| uxItemSize | 每个数据(item)的大小:以字节为单位 |
| 返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
- 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好
1 | QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength, |
| 参数 | 说明 |
|---|---|
| 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 ); |
写队列
1 | /* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait */ |
| 参数 | 说明 |
|---|---|
| xQueue | 队列句柄,要写哪个队列 |
| pvItemToQueue | 数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小 |
| xTicksToWait | 如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 |
| 返回值 | pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了。 |
读队列
1 | // 在任务中使用 |
| 参数 | 说明 |
|---|---|
| xQueue | 队列句柄,要读哪个队列 |
| pvBuffer | bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
| xTicksToWait | 果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
| 返回值 | pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。 |
查询
可以查询队列中有多少个数据、有多少空余空间
1 | /*返回队列中可用数据的个数*/ |
覆盖/偷看
当队列长度为1时,可以使用xQueueOverwrite()或xQueueOverwriteFromISR()来覆盖数据。
1 | /* 覆盖队列 |
如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,那么可以使用"窥视",也就是xQueuePeek()或xQueuePeekFromISR()。
1 | /* 偷看队列 |
信息量
特性
- 信号:起通知作用
- 量:还可以用来表示资源的数量
- 当"量"只有0、1两个取值时,它就是"二进制信号量"(Binary Semaphores)
- 当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)
- 支持的动作:"give"给出资源,计数值加1;"take"获得资源,计数值减1
函数
创建
创建二进制信号量:
1 | /* 创建一个二进制信号量,返回它的句柄。 |
创建计数型信号量:
1 | /* 创建一个计数型信号量,返回它的句柄。 |
删除
动态创建的信号量,不再需要它们时,可以删除它们以回收内存。
1 | /* xSemaphore: 信号量句柄,你要删除哪个信号量 */ |
give/take
1 | // 在任务中使用 |
互斥值
特性
FreeRTOS的互斥锁,并没有在代码上实现谁上锁,就只能由谁开锁,只是约定。
- 互斥量初始值为1
- 任务A想访问临界资源,先获得并占有互斥量,然后开始访问
- 任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞
- 任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源
- 任务B使用完毕,释放互斥量
函数
要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:
1 |
创建
1 | /* 创建一个互斥量,返回它的句柄。 |
其他函数
互斥量不能在ISR中使用,ISR需要快速运行,不能阻塞太久。
1 | /* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量 */ |
优先级反转
假设任务A、B都想使用串口,A优先级比B低:
- 任务A获得了串口的互斥量
- 任务B也想使用串口,它将会阻塞、等待A释放互斥量
- 高优先级的任务,被低优先级的任务延迟,这被称为"优先级反转"(priority inversion)
互斥量可以通过"优先级继承",临时提高有锁且低优先级的程序,可以很大程度**解决"优先级反转"**的问题
递归锁
递归锁实现了:谁上锁就由谁解锁
死锁
假设有2个互斥量M1、M2,2个任务A、B:
- A获得了互斥量M1
- B获得了互斥量M2
- A还要获得互斥量M2才能运行,结果A阻塞
- B还要获得互斥量M1才能运行,结果B阻塞
- A、B都阻塞,再无法释放它们持有的互斥量
- 死锁发生!
解决这样问题可以使用递归锁
- 任务A获得递归锁M后,它还可以多次去获得这个锁
- "take"了N次,要"give"N次,这个锁才会被释放
函数
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位
函数
创建
1 | /* 创建一个事件组,返回它的句柄。 |
删除
1 | /* xEventGroup: 事件组句柄,你要删除哪个事件组 */ |
设置事件
- 在任务中使用
xEventGroupSetBits() - 在ISR中使用
xEventGroupSetBitsFromISR()
1 | /* 设置事件组中的位 |
xEventGroupSetBitsFromISR函数不是直接去设置事件组,而是给一个FreeRTOS后台任务(daemon task)发送队列数据,由这个任务来设置事件组。
等待事件
使用xEventGroupWaitBits来等待事件,可以等待某一位、某些位中的任意一个,也可以等待多位;等到期望的事件后,还可以清除某些位。
1 | EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, |
| 参数 | 说明 |
|---|---|
| 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 |
| 返回值 | 返回的是事件值, 如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值; 如果是超时退出,返回的是超时时刻的事件值。 |
同步点
使用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 |
| 返回值 | 返回的是事件值, 如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值; 如果是超时退出,返回的是超时时刻的事件值。 |
任务通知
特性
优势
- 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
- 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
限制
- 不能发送数据给ISR: ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
- 数据只能给该任务独享: 使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。 在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
- 无法缓冲数据 :使用队列时,假设队列深度为N,那么它可以保持N个数据。 使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
- 无法广播给多个任务 :使用事件组可以同时给多个任务发送事件。 使用任务通知,只能发个一个任务。
- 如果发送受阻,发送方无法进入阻塞状态等待: 假设队列已经满了,使用
xQueueSendToBack()给队列发送数据时,任务可以进入阻塞状态等待发送完成。 使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。
通知状态和通知值
每个人物都有一个结构体:TCB,里面存在2个成员:
- 一个是uint8_t类型,用来表示通知状态
- 一个是uint32_t类型,用来表示通知值
1 | typedef struct tskTaskControlBlock |
通知状态有3种取值:
- taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
- taskWAITING_NOTIFICATION:任务在等待通知
- taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为pending(有数据了,待处理)
任务通知的使用
使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。
函数
1 | // xTaskToNotify和xTaskHandle为任务句柄(创建任务时得到),给哪个任务发通知 |
eNotifyAction参数说明:
| eNotifyAction取值 | 说明 |
|---|---|
| eNoAction | 仅仅是更新通知状态为"pending",未使用ulValue。 这个选项相当于轻量级的、更高效的二进制信号量。 |
| eSetBits | 通知值 = 原来的通知值 | ulValue,按位或。 相当于轻量级的、更高效的事件组。 |
| eIncrement | 通知值 = 原来的通知值 + 1,未使用ulValue。 相当于轻量级的、更高效的二进制信号量、计数型信号量。 相当于xTaskNotifyGive()函数。 |
| eSetValueWithoutOverwrite | 不覆盖。 如果通知状态为"pending"(表示有数据未读), 则此次调用xTaskNotify不做任何事,返回pdFAIL。 如果通知状态不是"pending"(表示没有新数据), 则:通知值 = ulValue。 |
| eSetValueWithOverwrite | 覆盖。 无论如何,不管通知状态是否为"pendng", 通知值 = ulValue。 |
软件定时器
特性
跟闹钟类似,只响一次/每隔多少时间就自动操作。
两种状态
- 运行(Running、Active):运行态的定时器,当指定时间到达之后,它的回调函数会被调用
- 冬眠(Dormant):冬眠态的定时器还可以通过句柄来访问它,但是它不再运行,它的回调函数不会被调用
软件定时器的上下文
守护任务
FreeRTOS中有一个Tick中断,软件定时器基于Tick来运行,但不在Tick中断中执行定时器函数,而是在RTOS Damemon Task中执行,即守护任务。
守护任务的优先级:configTIMER_TASK_PRIORITY
定时器命令队列的长度:configTIMER_QUEUE_LENGTH
回调函数
如下是定时器的回调函数原型:
1 | void ATimerCallback( TimerHandle_t xTimer ); |
定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。
因此回调任务不能影响其他人:
- 回调函数要尽快实行,不能进入阻塞状态
- 不要调用会导致阻塞的API函数,比如
vTaskDelay() - 可以调用
xQueueReceive()之类的函数,但是超时时间要设为0:即刻返回,不可阻塞
函数
创建
1 | /* 使用动态分配内存的方法创建定时器 |
回调函数的类型是:
1 | void ATimerCallback( TimerHandle_t xTimer ); |
删除
1 | /* 删除定时器 |
启动/停止
启动定时器就是设置它的状态为运行态(Running、Active)。
停止定时器就是设置它的状态为冬眠(Dormant),让它不能运行。
1 | /* 启动定时器 |
创建定时器时,设置了它的周期(period)。xTimerStart()函数是用来启动定时器。假设调用xTimerStart()的时刻是tX,定时器的周期是n,那么在tX+n时刻定时器的回调函数被调用。
如果定时器已经被启动,但是它的函数尚未被执行,再次执行xTimerStart()函数相当于执行xTimerReset(),重新设定它的启动时间。
复位
从定时器的状态转换图可以知道,使用xTimerReset()函数可以让定时器的状态从冬眠态转换为运行态,相当于使用xTimerStart()函数。
如果定时器已经处于运行态,使用xTimerReset()函数就相当于重新确定超时时间。
1 | /* 复位定时器 |
修改周期
从定时器的状态转换图可以知道,使用xTimerChangePeriod()函数,处理能修改它的周期外,还可以让定时器的状态从冬眠态转换为运行态。
修改定时器的周期时,会使用新的周期重新计算它的超时时间。
1 | /* 修改定时器的周期 |
定时器ID
- 可以用来标记定时器,表示自己是什么定时器
- 可以用来保存参数,给回调函数使用
它的初始值在创建定时器时由xTimerCreate()这类函数传入。
- 更新ID:使用
vTimerSetTimerID()函数 - 查询ID:查询
pvTimerGetTimerID()函数
1 | /* 获得定时器的ID |
一般使用配置
要使用定时器,需要做些准备工作:
1 | /* 1. 工程中 */ |
中断管理
特性
中断流程,ISR要尽量快
- 保存现场:Task1被打断,需要先保存Task1的运行环境,比如各类寄存器的值
- 分辨中断、调用处理函数(这个函数就被称为ISR,interrupt service routine)
- 恢复现场:继续运行Task1,或者运行其他优先级更高的任务
ISR的优先级高于任务:即使是优先级最低的中断,它的优先级也高于任务。任务只有在没有中断的情况下,才能执行。
Q:为什么要引入两套API函数?
-
很多API函数会导致任务计入阻塞状态:
-
ISR调用API函数时,ISR不是"任务",ISR不能进入阻塞状态
-
所以,在任务中、在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 |
怎么切换任务
FreeRTOS的ISR函数中,使用两个宏进行任务切换:
1 | portEND_SWITCHING_ISR( xHigherPriorityTaskWoken ); //汇编实现 |
中断与任务间的通信
队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。
要注意的是,在ISR中使用的函数要有"FromISR"后缀。
资源管理
要独占式地访问临界资源,有3种方法:
-
公平竞争:比如使用互斥量等
-
谁要跟我抢,我就灭掉谁:
-
中断要跟我抢?我屏蔽中断
-
其他任务要跟我抢?我禁止调度器,不运行任务切换
-
屏蔽中断
- 任务中使用:
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函数,所以:这两段代码之间,不会有任务调度产生
在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函数,所以:这两段代码之间,不会有任务调度产生
暂停调度器
如果有别的任务来跟你竞争临界资源,可以把中断关掉:这当然可以禁止别的任务运行,但是这代价太大了。它会影响到中断的处理。
1 | /* 暂停调度器 */ |
示例操作,下面可以递归使用。
1 | vTaskSuspendScheduler(); |
调试方法
调试
- 打印
- 断言:
configASSERT - Trace
- Hook函数(回调函数)
打印
1 | int fputc( int ch, FILE *f ); |
断言
1 |
Trace
| trace宏 | 描述 |
|---|---|
| traceTASK_INCREMENT_TICK(xTickCount) | 当tick计数自增之前此宏函数被调用。参数xTickCount当前的Tick值,它还没有增加。 |
| traceTASK_SWITCHED_OUT() | vTaskSwitchContext中,把当前任务切换出去之前调用此宏函数。 |
| traceTASK_SWITCHED_IN() | vTaskSwitchContext中,新的任务已经被切换进来了,就调用此函数。 |
| traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue) | 当正在执行的当前任务因为试图去读取一个空的队列、信号或者互斥量而进入阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图读取的目标队列、信号或者互斥量的句柄,传递给此宏函数。 |
| traceBLOCKING_ON_QUEUE_SEND(pxQueue) | 当正在执行的当前任务因为试图往一个已经写满的队列或者信号或者互斥量而进入了阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图写入的目标队列、信号或者互斥量的句柄,传递给此宏函数。 |
| traceQUEUE_SEND(pxQueue) | 当一个队列或者信号发送成功时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。 |
| traceQUEUE_SEND_FAILED(pxQueue) | 当一个队列或者信号发送失败时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。 |
| traceQUEUE_RECEIVE(pxQueue) | 当读取一个队列或者接收信号成功时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。 |
| traceQUEUE_RECEIVE_FAILED(pxQueue) | 当读取一个队列或者接收信号失败时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。 |
| traceQUEUE_SEND_FROM_ISR(pxQueue) | 当在中断中发送一个队列成功时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
| traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue) | 当在中断中发送一个队列失败时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
| traceQUEUE_RECEIVE_FROM_ISR(pxQueue) | 当在中断中读取一个队列成功时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
| traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue) | 当在中断中读取一个队列失败时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
| traceTASK_DELAY_UNTIL() | 当一个任务因为调用了vTaskDelayUntil()进入了阻塞状态的前一刻此宏函数会在vTaskDelayUntil()中被立即调用。 |
| traceTASK_DELAY() | 当一个任务因为调用了vTaskDelay()进入了阻塞状态的前一刻此宏函数会在vTaskDelay中被立即调用。 |
Malloc Hook函数
编程时,一般的逻辑错误都容易解决。难以处理的是内存越界、栈溢出等。
内存越界经常发生在堆的使用过程总:堆,就是使用malloc得到的内存。
并没有很好的方法检测内存越界,但是可以提供一些回调函数:
1 | void vApplicationMallocFailedHook( void ); |
栈溢出Hook函数
在切换任务(vTaskSwitchContext)时调用taskCHECK_FOR_STACK_OVERFLOW来检测栈是否溢出,如果溢出会调用:
1 | void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName ); |
怎么判断栈溢出?
方法1:
- 当前任务被切换出去之前,它的整个运行现场都被保存在栈里,这时很可能就是它对栈的使用到达了峰值。
- 这方法很高效,但是并不精确
- 比如:任务在运行过程中调用了函数A大量地使用了栈,调用完函数A后才被调度。

方法2:
- 创建任务时,它的栈被填入固定的值,比如:0xa5
- 检测栈里最后16字节的数据,如果不是0xa5的话表示栈即将、或者已经被用完了
- 没有方法1快速,但是也足够快
- 能捕获几乎所有的栈溢出
- 为什么是几乎所有?可能有些函数使用栈时,非常凑巧地把栈设置为0xa5:几乎不可能

优化
栈使用情况
在创建任务时分配了栈,可以填入固定的数值比如0xa5,以后可以使用以下函数查看"栈的高水位",也就是还有多少空余的栈空间:
1 | UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask ); |
原理:从栈底往栈顶逐个字节地判断,它们的值持续是0xa5就表示它是空闲的。
使用运行时间统计
涉及的宏定义头
1 |
函数说明
- uxTaskGetSystemState:获得任务的统计信息
1 | UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray, |
| 参数 | 描述 |
|---|---|
| pxTaskStatusArray | 指向一个TaskStatus_t结构体数组,用来保存任务的统计信息。 有多少个任务?可以用uxTaskGetNumberOfTasks()来获得。 |
| uxArraySize | 数组大小、数组项个数,必须大于或等于uxTaskGetNumberOfTasks() |
| pulTotalRunTime | 用来保存当前总的运行时间(更快的定时器),可以传入NULL |
| 返回值 | 传入的pxTaskStatusArray数组,被设置了几个数组项。 注意:如果传入的uxArraySize小于uxTaskGetNumberOfTasks(),返回值就是0 |
- vTaskList :获得任务的统计信息,形式为可读的字符串。注意,pcWriteBuffer必须足够大。
1 | void vTaskList( signed char *pcWriteBuffer ); |

- vTaskGetRunTimeStats:获得任务的运行信息,形式为可读的字符串。注意,pcWriteBuffer必须足够大。
1 | void vTaskGetRunTimeStats( signed char *pcWriteBuffer ); |

来源
百问网《FreeRTOS入门与工程实践-基于STM32F103》教程-基于DShanMCU-103(STM32F103) | 百问网