任务通知
任务通知是 FreeRTOS 内置的轻量级任务间同步通信机制,它的内核对象不需要单独创建,因为每个任务的任务控制块中天生自带一个 32 位的通知值和通知状态,其他任务或中断可以直接向指定任务发送通知,目标任务可以阻塞等待通知
它是 FreeRTOS 中性能最高、内存开销最小的 IPC 机制,官方测试数据显示,它比二值信号量的同步速度快约 45%,核心原因是省去了内核对象的创建、链表挂载等额外开销
优势与局限性
任务通知的优势
- 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高
- 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体
- 功能复用性强:一套机制可模拟二值信号量、计数信号量、事件组、轻量消息邮箱四种常用场景
任务通知的局限性
- 一对一接收:只有目标任务自己能等待自己的通知,不能多个任务同时等待同一个通知源
- 无数据缓冲:仅一个 32 位通知值,不支持多条数据排队缓存;未处理的新通知要么覆盖旧值,要么发送失败
- 发送永不阻塞:发送通知是直接写目标任务的通知值,永远立即返回,发送方不会阻塞
- 定向发送:发送时必须明确指定目标任务的句柄,不能像信号量那样
任务通知机制
- 每个任务都有一个结构体:TCB,里面有2个成员:
一个是uint8_t类型,用来表示通知状态
一个是uint32_t类型,用来表示通知值
c
typedef struct tskTaskControlBlock
{
......
/* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */
volatileuint32_tulNotifiedValue[configTASK_NOTIFICATION_ARRAY_ENTRIES];
volatileuint8_tucNotifyState[configTASK_NOTIFICATION_ARRAY_ENTRIES];
......
}tskTCB;
- 通知状态有3种取值:
taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
taskWAITING_NOTIFICATION:任务在等待通知
taskNOTIFICATION_RECEIVED:任务接收到了通知
当任务处于未等待状态时,可以自己进入等待状态阻塞,直到接收到了通知退出阻塞同时重新回到未等待状态。
当任务处于未等待状态时,接收到了通知,任务会进入接收状态,当任务自己要进入等待状态时不会阻塞,而是直接回到为等待状态。
任务通知的使用
| 简化版 | 专业版 | |
|---|---|---|
| 发出通知 | xTaskNotifyGive | xTaskNotify |
| 中断发出通知 | vTaskNotifyGiveFromISR | xTaskNotifyFromISR |
| 取出通知 | ulTaskNotifyTake | xTaskNotifyWait |
简化版
c
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
voidvTaskNotifyGiveFromISR(TaskHandle_t xTaskHandle,BaseType_t*pxHigherPriorityTa
skWoken);
uint32_tulTaskNotifyTake(BaseType_t xClearCountOnExit,TickType_t xTicksToWait);
在任务中使用xTaskNotifyGive函数,在ISR中使用vTaskNotifyGiveFromISR函数,都是直接给其他任务发送通知:
- 使得通知值加一
- 并使得通知状态变为"pending",也就是
taskNOTIFICATION_RECEIVED,表示有数据了、待处理
可以使用ulTaskNotifyTake函数来取出通知值:
- 如果通知值等于0,则阻塞(可以指定超时时间)
- 当通知值大于0时,任务从阻塞态进入就绪态
- 在ulTaskNotifyTake返回之前,还可以做些清理工作:把通知值减一,或者把通知值清零
xTaskNotifyGive
| 参数 | 说明 |
|---|---|
| xTaskToNotify | 任务句柄,给哪个任务发通知 |
| 返回值 | 必定返回pdPASS |
vTaskNotifyGiveFromISR
| 参数 | 说明 |
|---|---|
| xTaskToNotify | 任务句柄,给哪个任务发通知 |
| 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 |
专业版
c
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,uint32_tulValue,eNotifyActio
n eAction);
BaseType_t xTaskNotifyFromISR(TaskHandle_t xTaskToNotify,
uint32_tulValue,
eNotifyAction eAction,
BaseType_t*pxHigherPriorityTaskWoken);
BaseType_t xTaskNotifyWait(uint32_tulBitsToClearOnEntry,
uint32_tulBitsToClearOnExit,
uint32_t*pulNotificationValue,
TickType_t xTicksToWait);
xTaskNotify函数功能更强大,可以使用不同参数实现各类功能,比如:
- 让接收任务的通知值加一:这时xTaskNotify()等同于xTaskNotifyGive()
- 设置接收任务的通知值的某一位、某些位,这就是一个轻量级的、更高效的
事件组 - 把一个新值写入接收任务的通知值:上一次的通知值被读走后,写入才成
功。这就是轻量级的、长度为1的队列 - 用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖
都成功。类似xQueueOverwrite()函数,这就是轻量级的邮箱。
xTaskNotify()比xTaskNotifyGive()更灵活、强大,使用上也就更复杂。xTaskNotifyFromISR()是它对应的ISR版本。这两个函数用来发出任务通知,使用哪个函数来取出任务通知呢?使用xTaskNotifyWait()函数!它比ulTaskNotifyTake()更复杂:
- 可以让任务等待(可以加上超时时间),等到任务状态为"pending"(也就是有数
据) - 还可以在函数进入、退出时,清除通知值的指定位
xTaskNotify
| 参数 | 说明 |
|---|---|
| xTaskToNotify | 任务句柄,给哪个任务发通知 |
| ulValue | 怎么使用ulValue,由eAction参数决定 |
| eAction | 见下表 |
| 返回值 | pdPASS:成功,大部分调用都会成功pdFAIL:只有一种情况会失败,当eAction为eSetValueWithoutOverwrite,并且通知状态为"pending"(表示有新数据未读),这时就会失败 |
eNotifyAction
| eNotifyAction取值 | 说明 |
|---|---|
| eNoAction | 仅仅是更新通知状态为"pending",未使用ulValue。 这个选项相当于轻量级的、更高效的二进制信号量。 |
| eSetBits | 通知值=原来的通知值相;当于轻量级的、更高效的事件组。 |
| 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:没有得到通知。 |