FreeRTOS-使用 task notification
使用Task Notification
需要在工程配置文件FreeRTOSConfig.h
中定义:
1 |
在文章FreeRTOS-Task Notification中,我们提到在FreeRTOS版本FreeRTOSv202112
之后,内核任务控制块结构的Task Notification
储存区,不再是仅能存储单个uint32_t
值,而是定义为一个可配置的数据 uint32_t ulNotifiedValue [ configTASK_NOTIFICATION_ARRAY_ENTRIES ]
(默认配置大小为1)。
因此,如果有需求,可以将数组设置更大,使得Task Notifacation
可以存储多个 uint32_t
值,每个值用作不同的目的。
Task Notification
提供的API,每次仅能读/写ulNotifiedValue
数组中的单个值。因此,Task Notification
的 API 中包含一个index
参数用来指定操作数组中的哪个值。同时,为了和历史版本的Task Notification
兼容,也保留了旧版本中不需要指定index
参数的 API(因为旧版本中就是单个uint32_t值),它们操作ulNotifiedValue
中的第 0 个值:
例如,两个发送数据相关的API:
xTaskNotify——默认是将数据写入到目标任务ulNotifiedValue
数组中的第0个值。
xTaskNotifyIndexed——需要指定数据是写入目标任务ulNotifiedValue
数组中的哪个值。
例如,两个获取数据相关的API:
ulTaskNotifyTake——默认是获取目标任务ulNotifiedValue
数组中的第 0 个值。
ulTaskNotifyTakeIndexed——需要指定获取目标任务ulNotifiedValue
数组中的哪个值。
在下文中,我们均使用不需要指定index
参数的API,即操作ulNotifiedValue
数组中的第 0 个值。
后文中提及的ulNotifiedValue
也默认指的是该数组的第 0 个值,而不是实际的数组。
1 使用Task Notifacation 模拟二值信号量
二值信号量的外在表现就是两种状态:
- 值为0: 表示不可用,此时如果任务请求获取信号量,就会进入阻塞状态,等待其可用(当设置了等待超时时间)。
- 值为1:表示可用。此时任务请求信号量时,可以立刻获得,并且信号量会变为不可用状态(值为0)
当使用Task Notification
来模拟二值信号量时,API:
1 | uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ); |
可以用来模拟任务获取信号量,实际上该函数内部实现是等待任务自身控制块中的ulNotifiedValue
值大于0。调用该函数时如果ulNotifiedValue
的值:
- 等于0,可以认为当前“信号量不可用”,任务会阻塞并等待其值大于0(参数
xTicksToWait
为等待时间)。 - 大于0,可以看做“信号量可用”了,该函数成功返回(等于拿到信号量),而二值信号量的特性是一旦被获取,就会再次变为“不可用状态”。为了实现该特性,该函数的参数
xClearCountOnExit
需要指定为pdTRUE
,表示成功获取到非0值时,就将任务控制块中的ulNotifiedValue
归零(变为“不可用状态”)。
用来模拟设置信号量的 API为:
1 | BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); |
- 参数
xTaskToNotify
:为目标任务句柄。该函数内部实现就是将目标任务控制块中的ulNotifiedValue
值加1(非0,变为可用了)。如果目标任务当前正在等待“信号量可用”(等待ulNotifiedValue
值大于0),那么调用该API 会使得目标任务恢复为就绪态。 - 参数
pxHigherPriorityTaskWoken
:作用见下文代码中的注释。
我们用上述两个API,来实现二值信号量一文中的任务等待按键事件:
任务代码和main函数如下所示:
1 |
|
按键中断处理函数中调用通知 API:vTaskNotifyGiveFromISR
1 |
|
运行如下:
1 | start FreeRTOS |
2 使用Task Notifacation 模拟计数信号量
二值信号量的特性是:非 “0” 即 “1” ,信号量一旦被获取,立刻又会变为不可用状态(“0”)。因此,我们在使用ulTaskNotifyTake( xClearCountOnExit, xTicksToWait )
模拟获取信号量时,设置参数xClearCountOnExit = pdTRUE
,使其实现“信号量被获取后就立刻变为不可用”。也是因为该特性,二值信号量不具备计数特性。
计数信号量相比二值信号量多了计数功能。
使用Task Notification
模拟计数信号量的方式和上文一样,只是在调用
1 | uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ); |
时,设置参数xClearCountOnExit = pdFALSE
,这样该函数的行为就是将目标任务控制块中的ulNotifiedValue
值减 1(没有清零,如果“信号量”被设置多次,那么就可以调用对应次数的ulTaskNotifyTake
,每次都能立刻获得到“信号量”)。
我们沿用文章FreeRTOS-使用计数信号量中使用计数信号量来记录按键次数的例子。使用Task Notification
模拟计数信号量,并记录按键次数。这样,任务即使因为忙,无法立即响应短时间内发生的多次按键,但由于事件发生的次数已经被记录,使得任务在忙完之后仍旧能够感知到每个发生的事件。
按键中断函数,以及main 函数均不用改动,沿用上文的代码。
按键处理任务修改如下:
1 | void button_pressed_handler( void *pvParameters ) { |
运行如下:在按键处理任务正在做其它工作时,这期间发生了三次按键,由于按键处理任务正在“忙”,无法立即响应。但事件发生的次数被记录下来了。并在之后,任务会依次处理每个发生过的事件。
1 | start FreeRTOS |
3 使用Task Notifacation 模拟 event group
在32位MCU中,event group本质就是一个uint32_t
数值,其高8位是内部使用的控制位,不可以使用。低24位开发者可以使用,开发者可以使用其中的任意一个位来表示一个自定义的事件。
任务可以等待某个事件发生(等待相应位被置位),等待多个事件同时发生(等待多个位同时被置位),或者等待多个事件中的任意一个事件发生(等待多个位中任意一个被置位)。
Task Notification
的ulNotifiedValue
也是一个uint32_t
数值,我们也可以使用其中的位,表示一些自定义的事件,来实现模拟event group
的功能。
使用Task Notification
来模拟event group
时,使用 API:
1 | BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, |
可以实现“等待事件发生”功能。
其中:
- ulBitsToClearOnEntry:用来指定在开始等待前,哪些位需要清除。例如,应用中存在一个交易功能,在发起交易后,调用xTaskNotifyWait来等待按键事件。这种情况下设置ulBitsToClearOnEntry=1<<n(第n位表示按键事件),使得等待前确保按键事件位被清零,避免之前遗留的数据对这里按键事件造成误判。
- ulBitsToClearOnExit:当收到其它任务发送的notification数据后,任务解除阻塞恢复就绪,同时会自动清零任务控制块中
ulNotifiedValue
的那些由ulBitsToClearOnExit
指定的位。(等待的事件发生之后,清除事件标记位,避免重复触发) - pulNotificationValue:当函数返回时(收到数据或等待超时),会将任务控制块中当前的
ulBitsToClearOnExit
值保存在该参数中。(注意,是保存之后,才会根据第二个参数 ulBitsToClearOnExit 来清零需要清零的位) - xTicksToWait:最长等待时间。
- 返回值:返回
pdTRUE
表示收到数据,返回pdFALSE
表示等待超时。
而设置事件,则通过Task Notification
的发送 notification 数据来实现:
1 | BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, |
- xTaskToNotify:目标任务的句柄。
- ulValue:需要发送的值
- eAction:对于模拟
event group
来说,设置eAction = eSetBits
,表示根据参数ulValue
设置目标任务ulNotifiedValue
中的相应位(表示这些位对应的自定义事件发生了)。
需要注意的是,Task Notification
的等待数据 API:xTaskNotifyWait
,在收到任意数据时后就会返回(解除阻塞状态),因此用该函数无法实现event group
的等待多个事件同时发生才返回的特性。
我们使用Task Notification
作为event group
来实现下面的例子:
每当task_a
完成一些特定工作,就会设置事件TASK_A_DONE
每当task_b
完成一些特定工作,就会设置事件TASK_B_DONE
task_c
等待TASK_A_DONE
、TASK_B_DONE
任意一个事件发生,则执行相关处理,代码如下所示:
任务代码:
1 | // 使用第 0 位来表示事件:task_a工作执行完 |
main函数实现如下:
1 |
|
运行结果如下所示:
1 | start FreeRTOS |
使用Task Notifacation 作为一个轻量级消息邮箱
如上一节文所述,任务控制块中的ulNotifiedValue
为一个uint32_t
数值,我们可以将其作为一个4字节的存储空间,因此每次可以发送单个uint32_t
数值,或发送4个char
类型数值,使其成为一个轻量级的消息邮箱(消息直接发送给目标,不需要创建额外对象,非常适合在任务间传递数值,或几个字节的数据)。
对于发送notification的 API
1 | BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, |
其参数eAction
除了可以设置为上一节所述为eSetBits
,使其能单独设置某些位。
还可以设置为:
- eIncrement:让目标任务的
ulNotifiedValue
值自增1。实际上前文所述的xTaskNotifyGive
就是一个宏,其本质就是使用eAction= eIncrement
的xTaskNotify
。 - eSetValueWithOverwrite:设置目标任务的
ulNotifiedValue
值为参数中的ulValue
。 - eSetValueWithoutOverwrite:同上,但如果目标任务没有读取过上一次设置的值,则本次不会执行写入。
因此,发送Notification时,将xTaskNotify
参数eAction
设置为eSetValueWithOverwrite
或eSetValueWithoutOverwrite
即可用Task Notification
作为轻量级邮箱使用。
如下所示样例:
创建任务task_a
,周期性地向任务task_b
发送数据。(例如周期采样某个传感器数据,发送给任务,如果是中断服务函数中向目标任务发送数据,需要使用发送函数的中断版本xTaskNotifyFromISR
)
1 |
|
运行输出如下:
1 | start FreeRTOS |
ps:需要注意文章代码中的日志输出函数,产品代码中如果需要使用的话,需要考虑多线程安全性(多任务安全性),因为中断/任务切换可能发生在另一个任务正在输出日志但还未输出完的时候,这就可能造成日志错乱
FreeRTOS交流QQ群-663806972