FreeRTOS 入门与实践 —— 实践(3)
任务通知(Task Notifications)
一,任务通知(Task Notifications)
使用队列、信号量、事件组等方法时,并不知道对方是谁。使用任务通知时,可以明确指定 -通知哪个任务。
队列、信号量、事件组需要事先创建对应的结构体,双方通过中间的结构体通信:

任务通知可直接通知对方(前提是知道对方)
任务通知中,任务结构体TCB中就包含了内部对象,可直接接收别人发来的“通知”
涉及内容:通知状态、通知值、使用场合,任务通知的优势
1,特性
优势:发送事件、数据的效率更高;无需额外创建结构体,更节省内存
限制:不能发送数据给 ISR,数据只能该任务独享,无法缓冲数据,无法广播给多任务,发送受阻时发送方无法进入阻塞状态等待
通知状态和通知值:
每个任务都有一个结构体 TCB(Task Control Block),里面有 2 个成员:
- 一个是 uint8_t 类型,用来表示通知状态
- 一个是 uint32_t 类型,用来表示通知值
typedef struct tskTaskControlBlock { ...... /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */ volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; ...... } tskTCB;通知状态有3种取值:
- taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
- taskWAITING_NOTIFICATION:任务在等待通知
- taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为 pending(待处理)
##define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 ) /* 也是初始状态 */ ##define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 ) ##define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 )通知值可以有多种类型:计数值、位(类似事件组)、任意数值
2,使用
任务通知可以实现轻量级的队列(长度为 1)、邮箱(覆盖的队列)、计数型信号量、 二进制信号量、事件组等。
(1)两类函数
任务通知有两套函数:简化版(使用简单,实际上也由专业版函数实现),专业版(支持很多参数,可实现很多功能)

【简化版】
Give 可以给其他任务发送通知:
- 使得通知值加一
- 并使通知状态变为“pending”(表示有数据 待处理),即 taskNOTIFICATION_RECEIVED
Take 可以取出通知值

- 如果通知 等于0,则阻塞(可指定超时时间)
- 当通知值 大于0,任务从阻塞态进入就绪态
- 当 ulTaskNotifyTake 返回之前,还可以做清理工作(通知值减一 或 清零)
使用 ulTaskNotifyTake 函数可以实现轻量级的、高效的二进制信号量、计数型信号量。
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle,
BaseType_t *pxHigherPriorityTaskWoken );
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
【专业版】(详细说明见手册15章)
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction );
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken );
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );
3,基本操作
示例:car1运行到终点后,给car2发送轻量级信号量,给car3发送数值。car2等待轻量级信号量, car3等待特定的通知值。(car1用任务通知car2/3,car2任务用xTaskNotifyGive函数通知,car3用xTaskNotify)
被通知的任务,需要在创建任务时记录任务句柄
40 static TaskHandle_t g_TaskHandleCar2;
41 static TaskHandle_t g_TaskHandleCar3;
/* 省略 */
315 xTaskCreate(Car1Task, "car1", 128, &g_cars[0], osPriorityNormal, NULL);
316 xTaskCreate(Car2Task, "car2", 128, &g_cars[1], osPriorityNormal+2, &g_TaskHandleCar2);
317 xTaskCreate(Car3Task, "car3", 128, &g_cars[2], osPriorityNormal+2, &g_TaskHandleCar3);
car2 等待轻量级信号量
176 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
car3 等待通知值位 100
224 uint32_t val;
/* 省略 */
241 do
242 {
243 xTaskNotifyWait(~0, ~0, &val, portMAX_DELAY);
244 } while (val != 100);
car1 到达终点后,向 car2, car3 发出任务通知
/* 发出任务通知给car2,car3 */
146 xTaskNotifyGive(g_TaskHandleCar2);
147
148 xTaskNotify(g_TaskHandleCar3, 100,eSetValueWithOverwrite);
二,软件定时器(software timer)
【定时器的本质是结构体(flag, period, func, params, 链表项(处理多个定时器))】
软件定时器类似于 “闹钟”,可以完成两类事情:
- 在 “未来” 某个时间点,运行函数
- 周期性地运行函数
FreeRTOS里,可以设很多“软件定时器”,它们基于 系统滴答中断(Tick Interrupt)
涉及内容:特性,DaemonTask,定时器命令队列,一次性/周期性 定时器差别,定时器操作(创建、启动、复位、修改周期)
1,特性
添加闹钟时,需要指定时间、指定类型(一次性 or 周期性)、指定做什么事、还有是否有效
- 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)
- 指定类型:一次性 One-shot timers(可手工再次启用);自动加载定时器 Auto-reload timers(周期性地调用)
- 指定做什么事:即回调函数
- 是否有效:运行(Running、Active),冬眠(Dormant) (区别只在于是否调用回调函数)
示例:
Timer1:它是一次性的定时器,在 t1 启动,周期是 6 个 Tick。经过 6 个 tick 后,在 t7 执行回调函数。它的回调函数只会被执行一次,然后该定时器进入 冬眠 状态。
Timer2:它是自动加载的定时器,在 t1 启动,周期是 5 个 Tick。每经过 5 个 tick 它的回调函数都被执行,比如在 t6、t11、t16 都会执行。![]()
2,上下文
(1)守护任务
软件定时器基于 Tick 来运行,但是 不在 Tick 中断中执行回调函数。
FreeRTOS 是 RTOS,它不允许在内核、在中断中执行不确定的代码:如果定时器函数很耗时,会影响整个系统。而在 RTOS Damemon Task,RTOS守护任务 中执行。(需要FreeRTOS的配置项configUSE_TIMERS为1,则启动调度器时自动创建守护任务 优先级为 configTIMER_TASK_PRIORITY,定时器命令队列的长度为configTIMER_QUEUE_LENGTH)![]()
- 处理命令:从命令队列中取出命令 并处理
- 执行定时器的回调函数
能否及时处理定时器的命令、回调函数,严重依赖于守护任务的优先级。
下面是优先级较低和较高的不同场景案例:
注意,定时器的超时时间是基于调用xTimerStart()的时刻tX(本例中的 t2),而不是基于守护任务处 理命令的时刻tY。常见处理:把定时器任务设置到足够高 或者 其它任务用事件驱动的方式运行中会阻塞(CubeMX可配置TimerTask的中断优先级)
(3)回调函数
void ATimerCallback( TimerHandle_t xTimer );
定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,还要处理其他定时器。【定时器的回调函数不要运行其他人】
- 要尽快执行,不能阻塞
- 不要调用会阻塞的 API 函数,如 vTaskDelay()
- 可以调用xQueueReceive()之类的函数,但是超时时间要设为 0:即刻返回, 不可阻塞
3,函数
根据定时器的状态转换图,就可以知道所涉及的函数:

(1)创建:动态分配内存、静态分配内存
/* 使用动态分配内存的方法创建定时器
* pcTimerName:定时器名字, 用处不大, 尽在调试时用到
* xTimerPeriodInTicks: 周期, 以 Tick 为单位
* uxAutoReload: 类型, pdTRUE 表示自动加载, pdFALSE 表示一次性
* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
* pxCallbackFunction: 回调函数
* 返回值: 成功则返回 TimerHandle_t, 否则返回 NULL
*/
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
/* 使用静态分配内存的方法创建定时器
* pcTimerName:定时器名字, 用处不大, 尽在调试时用到
* xTimerPeriodInTicks: 周期, 以 Tick 为单位
* uxAutoReload: 类型, pdTRUE 表示自动加载, pdFALSE 表示一次性
* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
* pxCallbackFunction: 回调函数
* pxTimerBuffer: 传入一个 StaticTimer_t 结构体, 将在上面构造定时器
* 返回值: 成功则返回 TimerHandle_t, 否则返回 NULL
*/
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName,
TickType_t xTimerPeriodInTicks,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t *pxTimerBuffer );
回调函数的类型是:
void ATimerCallback( TimerHandle_t xTimer ); //通过传入的句柄区分是哪个定时器触发的回调
typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );
// 给“特定函数指针”起别名TimerCallbackFunction_t,而不用重复写void (*)(TimerHandle_t)
(2)删除:动态分配的定时器,不再需要时可以删除掉以回收内存
/* 删除定时器
* xTimer: 要删除哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL 表示"删除命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );
定时器的很多 API 函数,都是通过发送"命令"到命令队列,由守护任务来实现。(队列满时"命令"就无法即刻写入队列,可以指定一个超时时间xTicksToWait)
(3)启动 / 停止
/* 启动定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL 表示"启动命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
/* 启动定时器(ISR 版本)
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要进行任务调度
* 返回值: pdFAIL 表示"启动命令"无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );
/* 停止定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL 表示"停止命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
/* 停止定时器(ISR 版本) * xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要进行任务调度
* 返回值: pdFAIL 表示"停止命令"无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );
注意:
- xTicksToWait不是定时器本身的超时时间,不是定时器本身的"周期"
- 创建定时器时,设置了它的周期(period)。xTimerStart()函数是用来启动定时器。假设 调用xTimerStart()的时刻是 tX ,定时器的周期是 n ,那么在 tX+n 时刻定时器的回调函数被调用。
- 如果定时器已经被启动,但是它的函数尚未被执行,再次执行xTimerStart()函数相当 于执行xTimerReset(),重新设定它的启动时间。
(4)复位:
- 使用 xTimerReset()函数可以让定时器的状态从冬眠 态转换为运行态,相当于使用 xTimerStart()函数。
- 如果定时器已经处于运行态,使用xTimerReset()函数就相当于重新确定超时时间。
/* 复位定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL 表示"复位命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
/* 复位定时器(ISR 版本)
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要进行任务调度
* 返回值: pdFAIL 表示"停止命令"无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );
(5)修改周期:使用 xTimerChangePeriod()函数,处理能修改它 的周期外,还可以让定时器的状态从冬眠态转换为运行态。(假 设 调 用 xTimerChangePeriod() 函数的时间tX,新的周期是n,则tX+n就是新的超时时间。)
/* 修改定时器的周期
* xTimer: 哪个定时器
* xNewPeriod: 新周期
* xTicksToWait: 超时时间, 命令写入队列的超时时间
* 返回值: pdFAIL 表示"修改周期命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait );
/* 修改定时器的周期
* xTimer: 哪个定时器
* xNewPeriod: 新周期
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒,
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE",
* 表示需要进行任务调度
* @return: pdPASS - 修改周期的命令成功写入定时器命令队列;
* pdFAIL - 在定时器命令队列满的情况下,命令无法写入(ISR 版本无等待时间,立即返回)
*/
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer,
TickType_t xNewPeriod,
BaseType_t *pxHigherPriorityTaskWoken );
(6)定时器 ID:定时器结构体中有一项 pvTimerID

怎么使用定时器ID,完全由程序来决定:
- 可以用来标记定时器,表示自己是什么定时器
- 可以用来保存参数,给回调函数使用
- 更新 ID:使用 vTimerSetTimerID() 函数
- 查询 ID:查询 pvTimerGetTimerID() 函数
【这两个函数不涉及命令队列,它们是直接操作定时器结构体。】
/* 获得定时器的 ID
* xTimer: 哪个定时器
* 返回值: 定时器的 ID
*/
void *pvTimerGetTimerID( TimerHandle_t xTimer );
/* 设置定时器的 ID
* xTimer: 哪个定时器
* pvNewID: 新 ID
* 返回值: 无
*/
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID )
4,示例:实现游戏音效
在game1游戏中,什么时 候发出声音?球与挡球板、转块碰撞时发出声音。什么时候停止声音?发出声音后,过一阵子就应该停止声音。通过使用软件定时器来实现。
- 挡球板任务,参考音乐任务了解蜂鸣器设置(初始化后 修改频率发声)
- 创建beep.c实现蜂鸣器操作,不同功能操作中对应不同buzzer_buzz(频率, 时间)
- 启动PWM发出声音,启动定时器;定时器的时间到后,停止PWM以静音
详见手册 16.4
三,中断管理 (Interrupt Management)
中断的处理流程如下:
- CPU 跳转到固定地址(中断向量)去执行代码,跳转由硬件实现。
- 执行代码完成:保存现场 (各类寄存器值等),分辨中断、调用 ISR处理函数 (interrupt service routine),恢复现场 (并继续运行任务 或 运行优先级更高的任务)
ISR在内核中被调用,要尽量快(ISR 执行过程中用户的任务无法执行),否则:
- 其他低优先级的中断无法被处理:实时性无法保证
- 用户任务无法被执行:系统显得卡顿
- 不利于中断嵌套
对于复杂的中断处理,要分为两部分
- ISR:尽快做些清理、记录工作,然后触发任务
- 任务:处理复杂的事件【需要 ISR 与 任务 间的通信】
【 FreeRTOS 中用中断的原则:】
- 把任务认为是硬件无关的,任务优先级由程序员决定(运行由调度器)
- ISR 用软件实现的,但被认为是硬件特性(跟硬件密切相关)
- ISR 的优先级高于任务(任务只在没有中断的情况下,才执行)
1,两套 API 函数
任务函数中,可以调用各类 API 函数,但是在 ISR 中直接使用会导致问题,所以另外引入了 ISR 中使用的 API 函数:
很多 API 函数会导致任务阻塞 (如写队列时,队列已满,阻塞等待)
ISR 不能进入阻塞状态
BaseType_t xQueueSend(...)
{
if (is_in_isr())
{
/* 把数据放入队列 */
/* 不管是否成功都直接返回 */
}
else /* 在任务中 */
{
/* 把数据放入队列 */
/* 不成功就等待一会再重试 */
}
}
FreeRTOS 使用两套函数,而不是使用一套函数的原因:
- 使用同一套函数的话,需要增加额外的判断代码、增加额外的分支
- 在任务、ISR 中调用时,需要的参数不一样
- 移植 FreeRTOS 时,还需要提供监测上下文的函数
- 有些处理器架构没有办法轻易分辨当前是处于任务中,还是处于 ISR 中
使用两套函数可以让程序更高效,但是在用第三方库时有麻烦(如ISR中调用库函数)可用以下方法解决:
- 把中断的处理推迟到任务中进行(Defer interrupt processing)
- 尝试在库函数中使用"FromISR"函数【在任务中、在 ISR 中都可以调用"FromISR"函数】
- 第三方库函数也许会提供 OS 抽象层【判断 任务 / ISR】
| 类型 | 在任务中 | 在 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 参数:是否由更高优先级的任务被唤醒。(若为 pdTRUE,则意味着后面要进行任务切换;不想用时此参数可设为 NULL )
/* 用法示例 */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 可被多次调用(多次切换为TRUE, 且不影响效率) */
xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);
/* 最后再决定是否进行任务切换 */
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
在任务中调用 API函数可能导致任务阻塞、任务切换,这叫做"context switch",上下文切换。这个函数可能很长时间才返回,在函数内部实现任务切换。在 ISR 中的函数也可能导致任务切换,但不在函数内部进行,而是返回一个参数表示是否切换。(切换涉及到寄存器修改,耗时的操作不在中断中进行)【提高 ISR 内运行效率、可控性强、可移植性好、Tick 中断中调用 vApplicationTickHook() 只能用"FromISR"的函数】
【切换任务】
用两个宏进行任务切换: ( 这两个宏做的事情是完全一样的 )
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
或
portYIELD_FROM_ISR( xHigherPriorityTaskWoken )'
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */
}
/* 最后再决定是否进行任务切换
* xHigherPriorityTaskWoken 为 pdTRUE 时才切换
*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
2,中断的延迟处理
中断的延迟处理 可防止处理中断太过耗时的问题(ISR 尽快做清理、记录然后触发任务,任务中处理更复杂的事情)。

3,中断与任务间的通信
即前面讲解过的 队列、信号量、互斥量、事件组、任务通知 等等方法。要注意 ISR 中用的函数要有“FromISR”的后缀。
4,示例:优化实时性
static void DispatchKey(struct ir_data *pidata)
{
#if 0
extern QueueHandle_t g_xQueueCar1;
extern QueueHandle_t g_xQueueCar2;
extern QueueHandle_t g_xQueueCar3;
xQueueSendFromISR(g_xQueueCar1, pidata, NULL);
xQueueSendFromISR(g_xQueueCar2, pidata, NULL);
xQueueSendFromISR(g_xQueueCar3, pidata, NULL);
#else
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < g_queue_cnt; i++)
{
xQueueSendFromISR(g_xQueues[i], pidata, &xHigherPriorityTaskWoken);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
#endif
}
此场景是想要优先级较低的 任务A 在运行过程中发生中断,中断中调用 DispatchKey 函数写了队列,使得任务B 被唤醒(优先级较高,中断后马上运行)。 &xHigherPriorityTaskWoken 若为 NULL,则会把任务B 调整为就绪态,但不会发起一次调度。
上述代码通过把 &xHigherPriorityTaskWoken 设为 pdTRUE,发起一次调度,让任务B 被唤醒。(修改后感觉不出,时间差异太快了,对高精度设备很有用)
不发起调度的问题:直到 tick 中断后才切换,不符合 rtos 的定义
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐





所有评论(0)