掌握FreeRTOS 实时内核 - Mastering the FreeRTOS Real Time Kernel
- 十三章全文翻译自:《 Mastering-the-FreeRTOS-Real-Time-Kernel.v1.1.0.pdf 》;
- 内核版本:FreeRTOS V10.6.2;
- 原文 repo:FreeRTOS-Kernel-Book;
- 部分复制粘贴自:Mastering-the-FreeRTOS-Real-Time-Kernel-CN;
- 本文开源协议:CC BY-SA 4.0;
- 神TM 撞到CSDN 的字数上限了,后面九到十三章只能切成两篇发;
翻译决定:
- 本文将 stack 翻译为“栈”,而不是“堆栈”。什么鬼呀,堆栈;
- 不翻译 port,可以理解为“分舵”[doge];
- 不翻译图片中的文字;
- 不翻译 tick;
- 适当修改原文的某些表述和组织方式,不会额外说明修改的内容;
- 由于互斥量是一种信号量,所以存在“给出(Give)” 和“获取(Take)”操作。互斥量的“Give” 操作也会被翻译成“释放”,意思是释放了对资源的占有权,而 “Take” 就是上锁;
FreeRTOS V10 更新:
- 新的流缓冲区
stream_buffer
和消息缓冲区message_buffer
; - 每个任务可以接收多个任务通知,实现多发送-单接收;
- heap_2,heap_4,heap_5 中新增标准库
calloc
的替代函数pvPortCalloc
; - 传入任务创建函数的
pcName
参数可以是NULL
, 而此前必须提供一个名称; - ……
富哥可以 V 我 50 乃至 5 毛看看实力。
——Hope · OneDay · 我买的 typora 能回本
一至八章在另一篇
掌握FreeRTOS 实时内核 - Mastering the FreeRTOS Real Time Kernel(一至八章)
九、事件组
9.1 介绍和范围
实时嵌入式系统一定要对事件采取行动,这一点已经在前文提及。前几章描述了 FreeRTOS 中任务和事件交互的特性。这类特性的例子包括信号量和队列,它们都具有以下属性:
- 它们允许任务在阻塞状态中等待单个事件的发生。
- 当事件发生时,等待事件的最高优先级任务会被解除阻塞。
事件组是 FreeRTOS 的另一个特性,它也允许事件和任务交互,但与队列和信号量不同:
- 事件组允许任务在阻塞状态下等待多个事件的组合。
- 当一组事件发生时,等待它们的所有任务都会被解除阻塞。
事件组的这些独特属性使它们在以下方面非常有用:
- 同步多个任务。
- 向多个任务广播事件。
- 允许任务在阻塞状态下等待一组事件中的任何一个发生。
- 允许任务在阻塞状态下等待多个操作完成。
事件组还能减少应用程序使用的 RAM,因为通常可以用一个事件组取代多个二值信号量。
事件组功能是可选的。要包含事件组功能,请将 FreeRTOS 源文件 event_groups.c 加入项目参与构建。
9.1.1 范围
本章旨在让读者充分了解以下内容:
- 事件组的实际用途。
- 事件组相对于其他 FreeRTOS 功能的优缺点。
- 如何设置事件组中的位。
- 如何在阻塞(Blocked)状态下等待事件组中的位被设置。
- 如何使用事件组同步一组任务。
9.2 事件组的特征
9.2.1 事件组、事件标志(Flag)和事件位(Bit)
事件 “标志(flag)”是一个布尔值(1 或 0),用于指示事件是否发生。事件 “组 ”是一组事件标志。
一个事件标志只能是 1 或 0,因此一个事件标志的状态可以存储在一个位(bit)中,而一个事件组中所有事件标志的状态可以存储在一个变量中;事件组中每个事件标志的状态由 EventBits_t
类型变量中的一个位表示。因此,事件标志也被称为事件 “位”。如果 EventBits_t
变量中的某位被置 1,则表示该位所代表的事件已经发生。如果 EventBits_t
变量中的某位被设置为 0,则表示该位所代表的事件没有发生。
图 9.1 显示了各个事件标志如何映射到 EventBits_t
类型变量中的各个位。
举例来说,如果事件组的值为 0x92(二进制 1001 0010),那么只有事件位 1、4 和 7 被置位,也就是只有位 1、4 和 7 所代表的事件发生了。图 9.2 显示了一个 EventBits_t
类型的变量,该变量的事件位 1、4 和 7 被设置,所有其他事件位清零,因此事件组的值为 0x92。
事件组中的各个位的意义取决于应用程序内的约定。例如,应用程序编写者可以创建一个事件组,然后:
- 将事件组内的第 0 位定义为 “已从网络接收到一条信息”。
- 第 1 位表示 “一条信息已准备好发送到网络”。
- 第 2 位表示 “中止当前网络连接”。
9.2.2 EventBits_t
数据类型的细节
事件组中的事件标志数量取决于 FreeRTOSConfig.h
中的 configTICK_TYPE_WIDTH_IN_BITS
编译时配置常量:
- 如果
configTICK_TYPE_WIDTH_IN_BITS 为 TICK_TYPE_WIDTH_16_BITS
,则每个事件组包含 8 个可用的事件位。 - 如果
configTICK_TYPE_WIDTH_IN_BITS 为 TICK_TYPE_WIDTH_32_BITS
,则每个事件组包含 24 个可用事件位。 - 如果
configTICK_TYPE_WIDTH_IN_BITS 为 TICK_TYPE_WIDTH_64_BITS
,则每个事件组包含 56 个可用事件位。
注:高 8 位被用作其他用途
9.2.3 被多个任务访问
事件组本身就是一个对象,可以被任何知道其存在的任务或 ISR 访问。任何数量的任务都可以读写同一事件组中的位。
9.2.4 使用事件组的一个实际例子
FreeRTOS+TCP TCP/IP 协议栈的实现提供了一个实际例子,说明如何使用事件组来简化设计并减少资源使用。
TCP socket 必须响应许多不同的事件。事件的例子包括接受事件、绑定事件、读取事件和关闭事件。socket 在任何给定时间期望接收的事件取决于当时的状态。例如,一个 socket 已经创建,但尚未绑定到一个地址,那么它将期待接收绑定事件,但不会期待接收读取事件(如果没有地址,就无法读取数据)。
FreeRTOS+TCP socket 的状态保存在一个名为 FreeRTOS_Socket_t
的结构中。该结构包含一个事件组,socket 要处理的每个事件都在其中定义了一个事件位。调用 FreeRTOS+TCP API 等待一个事件或一组事件时,只需阻塞式等待事件组即可。
事件组还包含一个 “中止 ”位,允许中止 TCP 连接,无论 socket 当时正在等待哪个事件。
9.3 管理事件组
9.3.1 xEventGroupCreate()
API 函数
FreeRTOS 还包含 xEventGroupCreateStatic()
函数,该函数可用预先分配的内存创建事件组。
事件组使用 EventGroupHandle_t
类型的变量作为句柄。xEventGroupCreate()
API 函数创建一个事件组,并返回 EventGroupHandle_t
来引用它所创建的事件组。
EventGroupHandle_t xEventGroupCreate( void );
xEventGroupCreate()
API 函数的原型
xEventGroupCreate()
返回值:
-
返回值
如果返回
NULL
,则表示无法创建事件组,因为没有足够的堆内存供 FreeRTOS 分配事件组数据结构。第 3 章提供了有关堆内存管理的更多信息。如果返回的值不是
NULL
,则表示已成功创建了事件组。返回值应作为已创建事件组的句柄存储。
9.3.2 xEventGroupSetBits()
API 函数
xEventGroupSetBits()
API 函数用于设置事件组中的一个或多个位,通常用于通知任务对应的事件已经发生。
注意:切勿在中断服务例程中调用 xEventGroupSetBits()。应使用中断安全版本 xEventGroupSetBitsFromISR() 代替它。
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet );
xEventGroupSetBits()
API 函数的原型
xEventGroupSetBits()
参数和返回值:
-
xEventGroup
目标事件组的句柄。调用
xEventGroupCreate()
创建事件组时将返回事件组句柄。 -
uxBitsToSet
位掩码,用于指定事件组中要设置为 1 的一个或多个事件位。事件组的当前值将与
uxBitsToSet
传递的掩码按位或运算,结果为事件组的新值。例如,将
uxBitsToSet
设置为 0x04(二进制 0100)后,事件组中的事件位 3 将被置位(如果尚未置位),而事件组中的所有其他事件位则保持不变。 -
返回值
更新后事件组的值。请注意,返回值中的位不一定与
uxBitsToSet
一致,因为这些位可能已被其他任务清除(注:就是函数返回前被其他任务抢占了)。
9.3.3 xEventGroupSetBitsFromISR()
API 函数
xEventGroupSetBitsFromISR()
是 xEventGroupSetBits()
的中断安全版本。
给出信号量是一种确定性操作,因为事先已知给出信号量最多只能导致一个任务离开阻塞状态。而在事件组中置位时,事先并不知道有多少任务会离开阻塞状态,因此在事件组中置位并不是确定性操作。
FreeRTOS 的设计和实现标准不允许在中断服务例程内或中断被禁用时执行非确定性操作。因此,xEventGroupSetBitsFromISR()
不会直接在中断服务例程中设置事件位,而是将该操作推迟到 RTOS 守护任务中进行。
注:也就是和软件定时器一样,要给守护任务发送指令,等守护任务处理
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken );
xEventGroupSetBitsFromISR()
API 函数的原型
xEventGroupSetBitsFromISR()
参数和返回值:
-
xEventGroup
目标事件组句柄。事件组句柄将从用于创建事件组的
xEventGroupCreate()
调用中返回。 -
uxBitsToSet
位掩码,用于指定事件组中要设置为 1 的一个或多个事件位。事件组的当前值将与
uxBitsToSet
传递的掩码按位或运算,结果为事件组的新值。例如,将
uxBitsToSet
设置为 0x04(二进制 0100)后,事件组中的事件位 3 将被置位(如果尚未置位),而事件组中的所有其他事件位则保持不变。 -
pxHigherPriorityTaskWoken
xEventGroupSetBitsFromISR()
不会直接在中断服务例程中设置事件位,而是向定时器命令队列发送命令,将该操作推迟到 RTOS 守护任务。如果守护任务正处于阻塞状态等待定时器命令队列,那么写入定时器命令队列将导致守护任务离开阻塞状态。如果守护任务的优先级高于当前执行任务(被中断的任务)的优先级,那么在内部,xEventGroupSetBitsFromISR()
将把*pxHigherPriorityTaskWoken
设置为pdTRUE
。如果
xEventGroupSetBitsFromISR()
将该值设置为pdTRUE
,则应在退出中断前执行上下文切换。这将确保中断直接切换到守护任务,因为守护任务将是优先级最高的就绪状态任务。 -
返回值
有两种可能的返回值:
- 只有当数据成功发送到定时器命令队列时,才会返回
pdPASS
。 - 如果 “设置位 ”命令因队列已满而无法写入定时器命令队列,则将返回
pdFALSE
。
- 只有当数据成功发送到定时器命令队列时,才会返回
9.3.4 xEventGroupWaitBits()
API 函数
xEventGroupWaitBits()
API 函数允许任务读取事件组的值,并可选择在 “阻塞 ”状态下等待事件组中的一个或多个事件位被置位。
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
xEventGroupWaitBits()
API 函数的原型
调度器根据 “解除阻塞条件” 确定任务是否应该进入 “阻塞 ”状态,以及何时离开 “阻塞 ”状态 。解除阻塞条件由 uxBitsToWaitFor
和 xWaitForAllBits
参数值组合指定:
uxBitsToWaitFor
指定要测试事件组中的哪些事件位xWaitForAllBits
需要所有指定的位都被置位,还是只需一个位或以上
如果调用 xEventGroupWaitBits()
时满足了任务的解除阻塞条件,则任务不会进入阻塞状态。
表 6 举例说明了会导致任务进入阻塞状态或退出阻塞状态的条件。表 6 只显示了事件组和 uxBitsToWaitFor
最低四位二进制位,这两个值的其他位被假定为零。
事件组现有值 | uxBitsToWaitFor | xWaitForAllBits | 导致的行为 |
---|---|---|---|
0000 | 0101 | pdFALSE | 由于事件组中的第 0 位或第 2 位均未被设置,调用任务将进入 “阻塞”状态;当事件组中的第 0 位或第 2 位被设置时,调用任务将离开 “阻塞”状态。 |
0100 | 0101 | pdTRUE | 由于事件组中的第 0 位和第 2 位未同时置位,调用任务将进入 “阻塞 ”状态;当事件组中的第 0 位和第 2 位都置位时,调用任务将离开 “阻塞 ”状态。 |
0100 | 0110 | pdFALSE | 调用任务不会进入阻塞状态,因为 xWaitForAllBits 为 pdFALSE ,而且 uxBitsToWaitFor 指定的两个位中的一个已经在事件组中置位。 |
0100 | 0110 | pdTRUE | 调用任务将进入阻塞状态,因为 xWaitForAllBits 为 pdTRUE ,且事件组中 uxBitsToWaitFor 指定的两个位中只有一位已置位。当事件组中的第 1 位和第 2 位都被置位,任务将退出阻塞状态。 |
uxBitsToWaitFor
和 xWaitForAllBits
的效果
调用者可使用 uxBitsToWaitFor
参数指定要测试的位,如果需要在响应事件后清零这些位,可以使用 xEventGroupClearBits()
API 函数清除事件位,但如果使用该函数手动清除事件位,将导致应用代码中出现竞争条件:
- 有多个任务使用同一个事件组。
- 不同任务或中断服务例程在事件组中设置或清零了事件位。
提供 xClearOnExit
参数就是为了避免这些潜在的竞争条件。如果将 xClearOnExit
设为 pdTRUE
,那么在调用者看来,事件位的测试和清除将是一个原子操作(不会被其他任务或中断中断)。
xEventGroupWaitBits()
参数和返回值:
-
xEventGroup
目标事件组的句柄。事件组句柄将从用于创建事件组的xEventGroupCreate()
调用中返回。 -
uxBitsToWaitFor
位掩码,用于指定要在事件组中测试的一个或多个事件位。更多示例请参见表 6。
-
xClearOnExit
如果解锁条件已满足,且
xClearOnExit
设置为pdTRUE
,那么在调用者退出xEventGroupWaitBits()
API 函数之前,事件组中由uxBitsToWaitFor
指定的事件位将被清零。
如果xClearOnExit
设置为pdFALSE
,则事件组中事件位的状态不会被xEventGroupWaitBits()
API 函数修改。 -
xWaitForAllBits
uxBitsToWaitFor
参数指定要在事件组中测试的事件位。xWaitForAllBits
指定解除阻塞条件要等待全部事件位都被置位,还是只需其中一个位。如果
xWaitForAllBits
设置为pdFALSE
,则任务将在uxBitsToWaitFor
指定的任何位被置位离开阻塞状态。如果
xWaitForAllBits
设置为pdTRUE
,则进入阻塞状态等待解阻塞条件满足的任务,只有在uxBitsToWaitFor
指定的所有位都被置位时,才会离开阻塞状态。示例请参见表 6。
-
xTicksToWait
:任务在阻塞状态以等待事件的最长时间。
如果
xTicksToWait
为零,或者调用xEventGroupWaitBits()
时解锁条件已满足,则函数将立即返回。阻塞时间是以 tick 周期为单位指定的,因此它所代表的绝对时间取决于 tick 频率。宏
pdMS_TO_TICKS()
可用来将以毫秒为单位指定的时间转换为以 tick 为单位指定的时间。如果在 FreeRTOSConfig.h 中将
INCLUDE_vTaskSuspend
设置为 1,将xTicksToWait
设置为portMAX_DELAY
,将导致任务无限期等待。 -
返回值
如果
xEventGroupWaitBits()
是因为满足了解除阻塞条件才返回,那么返回值就是满足条件时事件组的值(如果xClearOnExit
为pdTRUE
,返回值是清零之前的值),在这种情况下,返回值也符合解除阻塞条件。如果
xEventGroupWaitBits()
返回的原因是xTicksToWait
参数指定的阻塞时间已过,那么返回值就是阻塞时间过期时事件组的值。在这种情况下,返回值将不符合解除阻塞条件。
注:xWaitForAllBits
的位运算逻辑
其实这个逻辑很简单,用伪代码表示:
EventBits_t flags; // 事件组当前值
if (xWaitForAllBits) { // 要求指定的所有 bit 都置位
if ( (flags & uxBitsToWaitFor) == (uxBitsToWaitFor) ) {
// 满足条件,不阻塞
}
}
else { // 指定的所有 bit 有一个置位就够了
if ( flags & uxBitsToWaitFor ) {
// 满足条件,不阻塞
}
}
9.3.5 xEventGroupGetStaticBuffer()
API 函数
xEventGroupGetStaticBuffer()
API 函数提供了一种方法,可以获取指向静态创建的事件组缓冲区的指针。它与创建事件组时提供的缓冲区相同。
注意:切勿在中断服务例程中调用 xEventGroupGetStaticBuffer()。
BaseType_t xEventGroupGetStaticBuffer( EventGroupHandle_t xEventGroup, StaticEventGroup_t ** ppxEventGroupBuffer );
xEventGroupGetStaticBuffer()
API 函数的原型
xEventGroupGetStaticBuffer()
参数和返回值:
-
xEventGroup
目标事件组的句柄。该事件组必须由
xEventGroupCreateStatic()
创建。 -
ppxEventGroupBuffer
用于返回指向事件组数据结构缓冲区的指针。它与创建时提供的缓冲区相同。
-
返回值
有两种可能的返回值:
- 如果成功检索到缓冲区,将返回
pdTRUE
。 - 如果缓冲区未被成功检索,则返回
pdFALSE
。
- 如果成功检索到缓冲区,将返回
例 9.1 尝试使用事件组
本例演示了:
- 创建一个事件组。
- 在中断服务例程中设置事件标志。
- 在任务中设置事件组中的位。
- 阻塞等待事件组。
首先执行 xWaitForAllBits
设置为 pdFALSE
的示例,然后执行 xWaitForAllBits
设置为 pdTRUE
的示例,从而演示该参数的作用。
事件位 0 和事件位 1 由任务置位。事件位 2 由中断服务例程置位。使用清单 9.6 中的 #define
语句为这三个位命名。
/* Definitions for the event bits in the event group. */
#define mainFIRST_TASK_BIT ( 1UL << 0UL ) /* Event bit 0, set by a task */
#define mainSECOND_TASK_BIT ( 1UL << 1UL ) /* Event bit 1, set by a task */
#define mainISR_BIT ( 1UL << 2UL ) /* Event bit 2, set by an ISR */
清单 9.7 显示了控制事件位 0 和 1 的任务的实现。它先设置一个位,再设置另一个,以此循环往复。每次调用 xEventGroupSetBits()
间隔为 200 毫秒。在设置每个位之前都会打印出一个字符串,以便在控制台中查看执行顺序。
static void vEventBitSettingTask( void *pvParameters )
{
const TickType_t xDelay200ms = pdMS_TO_TICKS( 200UL );
for( ;; )
{
/* Delay for a short while before starting the next loop. */
vTaskDelay( xDelay200ms );
/* Print out a message to say event bit 0 is about to be set by the
task, then set event bit 0. */
vPrintString( "Bit setting task -\t about to set bit 0.\r\n" );
xEventGroupSetBits( xEventGroup, mainFIRST_TASK_BIT );
/* Delay for a short while before setting the other bit. */
vTaskDelay( xDelay200ms );
/* Print out a message to say event bit 1 is about to be set by the
task, then set event bit 1. */
vPrintString( "Bit setting task -\t about to set bit 1.\r\n" );
xEventGroupSetBits( xEventGroup, mainSECOND_TASK_BIT );
}
}
清单 9.8 显示了置位事件组第 2 位的中断服务例程的实现。同样,在该位被设置之前会打印出一个字符串,以便在控制台中看到执行顺序。但在本例中,由于控制台输出不应在中断服务例程中直接执行,因此使用 xTimerPendFunctionCallFromISR()
在 RTOS 守护任务中延迟处理。
与前面的示例一样,中断由周期性触发软件中断的任务触发。在本例中,中断每 500 毫秒产生一次。
static uint32_t ulEventBitSettingISR( void )
{
/* The string is not printed within the interrupt service routine, but is
instead sent to the RTOS daemon task for printing. It is therefore
declared static to ensure the compiler does not allocate the string on
the stack of the ISR, as the ISR's stack frame will not exist when the
string is printed from the daemon task. */
static const char *pcString = "Bit setting ISR -\t about to set bit 2.\r\n";
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* Print out a message to say bit 2 is about to be set. Messages cannot
be printed from an ISR, so defer the actual output to the RTOS daemon
task by pending a function call to run in the context of the RTOS
daemon task. */
xTimerPendFunctionCallFromISR( vPrintStringFromDaemonTask,
( void * ) pcString,
0,
&xHigherPriorityTaskWoken );
/* Set bit 2 in the event group. */
xEventGroupSetBitsFromISR( xEventGroup,
mainISR_BIT,
&xHigherPriorityTaskWoken );
/* xTimerPendFunctionCallFromISR() and xEventGroupSetBitsFromISR() both
write to the timer command queue, and both used the same
xHigherPriorityTaskWoken variable. If writing to the timer command
queue resulted in the RTOS daemon task leaving the Blocked state, and
if the priority of the RTOS daemon task is higher than the priority of
the currently executing task (the task this interrupt interrupted) then
xHigherPriorityTaskWoken will have been set to pdTRUE.
xHigherPriorityTaskWoken is used as the parameter to
portYIELD_FROM_ISR(). If xHigherPriorityTaskWoken equals pdTRUE, then
calling portYIELD_FROM_ISR() will request a context switch. If
xHigherPriorityTaskWoken is still pdFALSE, then calling
portYIELD_FROM_ISR() will have no effect.
The implementation of portYIELD_FROM_ISR() used by the Windows port
includes a return statement, which is why this function does not
explicitly return a value. */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
清单 9.9 显示了调用 xEventGroupWaitBits()
阻塞式等待事件组的任务的实现。该任务为事件组中置位的每个位打印出一个字符串。
函数 xEventGroupWaitBits()
的 xClearOnExit
参数被设置为 pdTRUE
,因此在 xEventGroupWaitBits()
返回之前,导致 xEventGroupWaitBits()
返回的事件位将被自动清除。
static void vEventBitReadingTask( void *pvParameters )
{
EventBits_t xEventGroupValue;
const EventBits_t xBitsToWaitFor = ( mainFIRST_TASK_BIT |
mainSECOND_TASK_BIT |
mainISR_BIT );
for( ;; )
{
/* Block to wait for event bits to become set within the event
group. */
xEventGroupValue = xEventGroupWaitBits( /* The event group to read */
xEventGroup,
/* Bits to test */
xBitsToWaitFor,
/* Clear bits on exit if the
unblock condition is met */
pdTRUE,
/* Don't wait for all bits. This
parameter is set to pdTRUE for the
second execution. */
pdFALSE,
/* Don't time out. */
portMAX_DELAY );
/* Print a message for each bit that was set. */
if( ( xEventGroupValue & mainFIRST_TASK_BIT ) != 0 )
{
vPrintString( "Bit reading task -\t Event bit 0 was set\r\n" );
}
if( ( xEventGroupValue & mainSECOND_TASK_BIT ) != 0 )
{
vPrintString( "Bit reading task -\t Event bit 1 was set\r\n" );
}
if( ( xEventGroupValue & mainISR_BIT ) != 0 )
{
vPrintString( "Bit reading task -\t Event bit 2 was set\r\n" );
}
}
}
在启动调度程序之前,main()
函数会创建事件组和任务。具体实现请参见清单 9.10。从事件组读取数据的任务的优先级高于向事件组写入数据的任务的优先级,以确保每次读取任务的解锁条件满足时,读取任务会抢占写入任务。
int main( void )
{
/* Before an event group can be used it must first be created. */
xEventGroup = xEventGroupCreate();
/* Create the task that sets event bits in the event group. */
xTaskCreate( vEventBitSettingTask, "Bit Setter", 1000, NULL, 1, NULL );
/* Create the task that waits for event bits to get set in the event
group. */
xTaskCreate( vEventBitReadingTask, "Bit Reader", 1000, NULL, 2, NULL );
/* Create the task that is used to periodically generate a software
interrupt. */
xTaskCreate( vInterruptGenerator, "Int Gen", 1000, NULL, 3, NULL );
/* Install the handler for the software interrupt. The syntax necessary
to do this is dependent on the FreeRTOS port being used. The syntax
shown here can only be used with the FreeRTOS Windows port, where such
interrupts are only simulated. */
vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulEventBitSettingISR );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
/* The following line should never be reached. */
for( ;; );
return 0;
}
main()
函数的实现
在将 xEventGroupWaitBits()
函数参数 xWaitForAllBits
设置为 pdFALSE
时,例 9.1 所产生的输出如图 9.3 所示。从图中可以看出,每次只要任何一个事件位被设置,等待事件组的任务就会离开阻塞状态并立即执行。
Bit setting task - about to set bit 1.
Bit reading task - event bit 1 was set.
Bit setting task - about to set bit 0.
Bit reading task - event bit 0 was set.
Bit setting task - about to set bit 1.
Bit reading task - event bit 1 was set.
Bit setting task - about to set bit 2.
Bit reading task - event bit 2 was set.
Bit setting task - about to set bit 0.
Bit reading task - event bit 0 was set.
xWaitForAllBits == pdFALSE
时的输出
将 xWaitForAllBits
参数设置为 pdTRUE
时,执行例 9.1 产生的输出如图 9.4 所示。从图 9.4 中可以看出,等待事件组的任务只有在所有三个事件位都被置位后才会离开阻塞状态。
Bit setting task - about to set bit 1.
Bit setting task - about to set bit 0.
Bit setting task - about to set bit 2.
Bit reading task - event bit 0 was set.
Bit reading task - event bit 1 was set.
Bit reading task - event bit 2 was set.
Bit setting task - about to set bit 1.
Bit setting task - about to set bit 0.
Bit setting task - about to set bit 2.
Bit reading task - event bit 0 was set.
Bit reading task - event bit 1 was set.
Bit reading task - event bit 2 was set.
xWaitForAllBits == pdTRUE
时的输出
9.4 用事件组实现任务间同步
有时,应用程序的设计需要多个任务相互同步。例如,在设计中,任务 A 接收到一个事件,然后将该事件所需的部分处理工作委托给其他三个任务,即任务 B、任务 C 和任务 D,任务 A 要在任务 B、C 和 D 全部完成处理后才能接着处理下一个事件,那么所有四个任务都需要相互同步。每个任务的同步点都是在该任务完成处理之后,在其他每个任务都完成同样的处理之前,该任务不能继续处理。
FreeRTOS+TCP 演示项目中的一个例子不太抽象地说明了这种任务同步的必要性。该演示在两个任务之间共享一个 TCP socket ;一个任务向该 socket 发送数据,另一个任务从同一 socket 接收数据。任何一个任务单独关闭 TCP socket 都是不安全的,必须先确认 socket 的持有者都不再使用它。如果两个任务中的任何一个希望关闭 socket ,则必须将其意图告知另一个任务,然后等待另一个任务停止使用 socket 后再关闭。清单 9.10 中的伪代码演示了向 socket 发送数据的任务希望关闭 socket 的情况。
清单 9.10 演示的情况比较简单,因为只有两个任务需要相互同步,但很容易看出,如果还有其他任务依赖打开的 socket ,那么情况就会变得更加复杂,需要更多任务加入同步。
注:这种情况更简单的方法是用之前的 gatekeeper 和引用计数
void SocketTxTask( void *pvParameters )
{
xSocket_t xSocket;
uint32_t ulTxCount = 0UL;
for( ;; )
{
/* Create a new socket. This task will send to this socket, and another
task will receive from this socket. */
xSocket = FreeRTOS_socket( ... );
/* Connect the socket. */
FreeRTOS_connect( xSocket, ... );
/* Use a queue to send the socket to the task that receives data. */
xQueueSend( xSocketPassingQueue, &xSocket, portMAX_DELAY );
/* Send 1000 messages to the socket before closing the socket. */
for( ulTxCount = 0; ulTxCount < 1000; ulTxCount++ )
{
if( FreeRTOS_send( xSocket, ... ) < 0 )
{
/* Unexpected error - exit the loop, after which the socket
will be closed. */
break;
}
}
/* Let the Rx task know the Tx task wants to close the socket. */
TxTaskWantsToCloseSocket();
/* This is the Tx task's synchronization point. The Tx task waits here
for the Rx task to reach its synchronization point. The Rx task will
only reach its synchronization point when it is no longer using the
socket, and the socket can be closed safely. */
xEventGroupSync( ... );
/* Neither task is using the socket. Shut down the connection, then
close the socket. */
FreeRTOS_shutdown( xSocket, ... );
WaitForSocketToDisconnect();
FreeRTOS_closesocket( xSocket );
}
}
/*-----------------------------------------------------------*/
void SocketRxTask( void *pvParameters )
{
xSocket_t xSocket;
for( ;; )
{
/* Wait to receive a socket that was created and connected by the Tx
task. */
xQueueReceive( xSocketPassingQueue, &xSocket, portMAX_DELAY );
/* Keep receiving from the socket until the Tx task wants to close the
socket. */
while( TxTaskWantsToCloseSocket() == pdFALSE )
{
/* Receive then process data. */
FreeRTOS_recv( xSocket, ... );
ProcessReceivedData();
}
/* This is the Rx task's synchronization point - it only reaches here
when it is no longer using the socket, and it is therefore safe for
the Tx task to close the socket. */
xEventGroupSync( ... );
}
}
事件组可用于创建同步点:
-
每个参与同步的任务都在事件组内分配一个事件位。
-
每个任务到达同步点时置位自己的事件位。
-
在设置了自己的事件位后,每个任务都会阻塞式等待事件组,等待其他同步任务的事件位也被置位。
在这种情况下不能使用 xEventGroupSetBits()
和 xEventGroupWaitBits()
API 函数。如果使用这两个 API 函数,那么位的设置(表示任务已达到同步点)和位的测试(确定其他同步任务是否已达到同步点)将作为两个单独的操作来执行。要了解为什么会出现这样的问题,请考虑任务 A、任务 B 和任务 C 尝试使用事件组同步的情况:
- 任务 A 和任务 B 已经达到同步点,因此它们的事件位在事件组中被置位,它们处于阻塞状态,等待任务 C 的事件位也被设置。
- 任务 C 到达同步点,并使用
xEventGroupSetBits()
置位其在事件组中的标志。一旦任务 C 置位,任务 A 和任务 B 就会离开阻塞状态,并清除所有三个事件位。 - 然后,任务 C 调用
xEventGroupWaitBits()
等待所有三个事件位,但此时所有三个事件位都已被清除,任务 A 和任务 B 已离开各自的同步点,因此同步失败。
要成功使用事件组创建同步点,事件位的置位以检测必须是单个不可中断操作。为此,要使用 xEventGroupSync()
API 函数。
9.4.1 xEventGroupSync()
API 函数
xEventGroupSync()
允许多个任务使用一个事件组相互同步。任务可用该函数在事件组中置位多个事件位,然后等待该事件组中的事件位组合被置位,这是一个不会中断的操作。
xEventGroupSync()
函数用 uxBitsToWaitFor
参数指定了调用者的解锁条件。如果 xEventGroupSync()
返回是因为解锁条件已满足,那么在 xEventGroupSync()
返回前,uxBitsToWaitFor
指定的事件位将被清零。
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );
xEventGroupSync()
API 函数的原型
xEventGroupSync()
参数和返回值:
-
xEventGroup
目标事件组的句柄,在该事件组中要置位,然后测试并等待。调用
xEventGroupCreate()
创建事件组时将返回事件组句柄。 -
uxBitsToSet
位掩码,用于指定事件组中要置 1 的一个或多个事件位。事件组的现有值和
uxBitsToSet
按位或运算,结果作为事件组的新值。例如,将
uxBitsToSet
设置为 0x04(二进制 0100)将导致事件位 2 被置位(如果它尚未被置位),而事件组中的所有其他事件位则保持不变。 -
uxBitsToWaitFor
位掩码,用于指定事件组中要测试的一个或多个事件位。
例如,调用者希望等待事件组中的事件位 0、1 和 2 被置位,则应将
uxBitsToWaitFor
设置为 0x07(二进制 111)。 -
xTicksToWait
任务等待解除阻塞条件的最长时间。
如果
xTicksToWait
为零,或者调用xEventGroupSync()
时解锁条件已满足,则xEventGroupSync()
将立即返回。阻塞时间是以 tick 周期为单位指定的,因此它所代表的绝对时间取决于 tick 频率。宏
pdMS_TO_TICKS()
可用来将以毫秒为单位指定的时间转换为以 tick 为单位指定的时间。如果在
FreeRTOSConfig.h
中将INCLUDE_vTaskSuspend
设置为 1,将xTicksToWait
设置为portMAX_DELAY
,将导致任务无限期等待(不会超时)。 -
返回值
如果
xEventGroupSync()
返回是因为满足了调用者的解除阻塞条件,那么返回值就是被清零之前的事件组的值,该值也满足解除阻塞条件。如果
xEventGroupSync()
返回的原因是xTicksToWait
参数指定的阻塞时间已过,那么返回值就是阻塞时间过期时事件组的值。在这种情况下,返回值将不符合解除阻塞条件。
例 9.2 多任务同步
例 9.2 使用 xEventGroupSync()
同步了单个函数实现的三个任务实例。任务参数用于向每个实例传递调用 xEventGroupSync()
时要设置的事件位,每个任务持有不同的事件位。
任务会在调用 xEventGroupSync()
之前打印一条信息,并在 xEventGroupSync()
返回后再次打印。每条信息都包含一个时间戳。这样就可以从输出结果中观察到执行顺序。为防止所有任务同时到达同步点,任务中添加了伪随机延迟。
有关任务的实现,请参见清单 9.13。
static void vSyncingTask( void *pvParameters )
{
const TickType_t xMaxDelay = pdMS_TO_TICKS( 4000UL );
const TickType_t xMinDelay = pdMS_TO_TICKS( 200UL );
TickType_t xDelayTime;
EventBits_t uxThisTasksSyncBit;
const EventBits_t uxAllSyncBits = ( mainFIRST_TASK_BIT |
mainSECOND_TASK_BIT |
mainTHIRD_TASK_BIT );
/* Three instances of this task are created - each task uses a different
event bit in the synchronization. The event bit to use is passed into
each task instance using the task parameter. Store it in the
uxThisTasksSyncBit variable. */
uxThisTasksSyncBit = ( EventBits_t ) pvParameters;
for( ;; )
{
/* Simulate this task taking some time to perform an action by delaying
for a pseudo random time. This prevents all three instances of this
task reaching the synchronization point at the same time, and so
allows the example's behavior to be observed more easily. */
xDelayTime = ( rand() % xMaxDelay ) + xMinDelay;
vTaskDelay( xDelayTime );
/* Print out a message to show this task has reached its synchronization
point. pcTaskGetTaskName() is an API function that returns the name
assigned to the task when the task was created. */
vPrintTwoStrings( pcTaskGetTaskName( NULL ), "reached sync point" );
/* Wait for all the tasks to have reached their respective
synchronization points. */
xEventGroupSync( /* The event group used to synchronize. */
xEventGroup,
/* The bit set by this task to indicate it has reached
the synchronization point. */
uxThisTasksSyncBit,
/* The bits to wait for, one bit for each task taking
part in the synchronization. */
uxAllSyncBits,
/* Wait indefinitely for all three tasks to reach the
synchronization point. */
portMAX_DELAY );
/* Print out a message to show this task has passed its synchronization
point. As an indefinite delay was used the following line will only
be executed after all the tasks reached their respective
synchronization points. */
vPrintTwoStrings( pcTaskGetTaskName( NULL ), "exited sync point" );
}
}
main()
函数创建事件组和三个任务,然后启动调度程序。具体实现请参见清单 9.14。
/* Definitions for the event bits in the event group. */
#define mainFIRST_TASK_BIT ( 1UL << 0UL ) /* Event bit 0, set by the 1st task */
#define mainSECOND_TASK_BIT( 1UL << 1UL ) /* Event bit 1, set by the 2nd task */
#define mainTHIRD_TASK_BIT ( 1UL << 2UL ) /* Event bit 2, set by the 3rd task */
/* Declare the event group used to synchronize the three tasks. */
EventGroupHandle_t xEventGroup;
int main( void )
{
/* Before an event group can be used it must first be created. */
xEventGroup = xEventGroupCreate();
/* Create three instances of the task. Each task is given a different
name, which is later printed out to give a visual indication of which
task is executing. The event bit to use when the task reaches its
synchronization point is passed into the task using the task parameter. */
xTaskCreate( vSyncingTask, "Task 1", 1000, mainFIRST_TASK_BIT, 1, NULL );
xTaskCreate( vSyncingTask, "Task 2", 1000, mainSECOND_TASK_BIT, 1, NULL );
xTaskCreate( vSyncingTask, "Task 3", 1000, mainTHIRD_TASK_BIT, 1, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
/* As always, the following line should never be reached. */
for( ;; );
return 0;
}
main()
函数实现
执行例 9.2 时的输出如图 9.5 所示。可以看出,尽管每个任务到达同步点的时间不同(伪随机),但每个任务退出同步点的时间相同(即最后一个任务到达同步点的时间)。
At time 211664: Task 1 reached sync point
At time 211664: Task 1 exited sync point
At time 211664: Task 2 exited sync point
At time 211664: Task 3 exited sync point
At time 212702: Task 2 reached sync point
At time 214400: Task 1 reached sync point
At time 215439: Task 3 reached sync point
At time 215439: Task 3 exited sync point
At time 215439: Task 2 exited sync point
At time 215440: Task 1 exited sync point
At time 217671: Task 2 reached sync point
At time 218622: Task 1 reached sync point
At time 219402: Task 3 reached sync point
At time 219402: Task 3 exited sync point
At time 219402: Task 2 exited sync point
At time 219402: Task 1 exited sync point
十、任务通知
10.1 介绍
FreeRTOS 应用程序的结构通常是一系列独立的任务,这些任务相互通信,共同提供系统功能。任务通知是一种高效机制,实现任务到任务的通知。
10.1.1 通过中间对象通讯
本书已经介绍了任务之间通信的多种方法。迄今为止所描述的方法都需要创建通信对象。通信对象的例子包括队列、事件组和各种不同类型的信号量。
使用通信对象时,事件和数据不会直接发送到接收者,而是发送到通信对象。同样,任务和 ISR 从通信对象接收事件和数据,而不是直接从发送者接收。如图 10.1 所示。
10.1.2 任务通知 —— 直接与任务通信
“任务通知"无需中间对象,任务可以直接与其他任务交互,并与 ISR 同步。使用任务通知,任务或 ISR 可以直接向接收任务发送事件。如图 10.2 所示。
任务通知功能是可选的。要包含任务通知功能,请在 FreeRTOSConfig.h
中将 configUSE_TASK_NOTIFICATIONS
设为 1。
当 configUSE_TASK_NOTIFICATIONS
设置为 1 时,每个任务至少有一个 “通知状态”(可以是 “挂起(pending)”或 “未挂起”)和一个 “通知值”(32 位无符号整数)。当任务收到通知时,其通知状态会被设置为挂起。当任务读取其通知值时,其通知状态会被设置为未挂起。如果 configTASK_NOTIFICATION_ARRAY_ENTRIES
设置为 > 1,那么一个任务可以有多个通知状态和对应的值。
任务可以在 “阻塞 ”状态下等待其通知状态变为 “挂起”,并可选择超时。
注:V10 版本以前,每个任务只能由一个通知状态和值
10.1.3 范围
本章讨论了:
- 任务的通知状态和通知值。
- 如何以及何时使用任务通知来代替通信对象(如信号量)。
- 使用任务通知代替通信对象的优点。
10.2 任务通知的优点和局限性
10.2.1 任务通知的性能优势
与使用队列、信号量或事件组执行同等操作相比,使用任务通知向任务发送事件或数据的速度要快得多。
10.2.2 任务通知的内存占用优势
同样,与使用队列、信号量或事件组执行同等操作相比,使用任务通知向任务发送事件或数据所需的 RAM 要少得多。这是因为每个通信对象(队列、信号量或事件组)在使用前都必须先创建,而任务通知功能的占用是固定的,随任务一同创建。 使用任务通知会让每个任务多占用 configTASK_NOTIFICATION_ARRAY_ENTRIES * 5
字节的RAM。 默认每个任务只使用一条通知,所以 configTASK_NOTIFICATION_ARRAY_ENTRIES
默认值为 1。
10.2.3 任务通知的局限性
与通信对象相比,任务通知速度更快,占用内存更少,但任务通知并不万能。本节记录了不能使用任务通知的情况:
-
与 ISR 通信
通信对象可用于 ISR 和任务之间的双向通信。任务通知只能从 ISR 向任务发送事件和数据。
-
启用多个接收任务
任何持有通信对象句柄(可能是队列句柄、信号量句柄或事件组句柄)的任务或 ISR 都可以访问通信对象。任何数量的任务和 ISR 都可以处理发送到任何给定通信对象的事件或数据。
任务通知是直接发送给接收任务的,因此只能有一个接收任务。不过,这在实际情况中很少成为限制因素,因为多个任务和 ISR 向同一通信对象发送通知的情况很常见,而多个任务和 ISR 从同一通信对象接收通知的情况却很少见。
-
缓冲多个数据
队列是一种通信对象,可以同时容纳多个数据项。已发送到队列但尚未被接收的数据会被缓冲在队列对象中。
任务通知有用通知值向任务发送数据,一个任务的一个通知值只能保存一个值。
-
广播到多个任务
事件组是一种通信对象,可用于同时向多个任务发送一个事件。
任务通知直接发送给接收任务,因此只能由一个接收任务处理。
-
发送者要等待接收完毕
如果通信对象暂时处于无法向其写入更多数据或事件的状态(例如,队列已满,无法向队列发送更多数据),发送任务可以选择进入 “阻塞”状态,等待写入操作完成。
如果一个任务试图任务通知,那么发送任务不可能在 “阻塞 ”状态下等待接收任务先处理完上一条通知。但是正如我们将看到的,在使用任务通知的实际情况中,这种限制很少成为问题。
10.3 使用任务通知
10.3.1 多种任务通知 API
任务通知是一项非常强大的功能,通常可以用来代替二值信号量、计数信号量、事件组,有时甚至可以代替队列。使用 xTaskNotify()
API 函数发送任务通知,以及使用 xTaskNotifyWait()
API 函数接收任务通知,可以实现广泛的使用场景。
不过,在大多数情况下,并不需要 xTaskNotify()
和 xTaskNotifyWait()
API 函数提供的全部灵活性,使用更简单的函数就足够了。因此,我们提供了 xTaskNotifyGive()
API 函数,作为 xTaskNotify()
的一个更简单但灵活性较低的替代函数;相应的,用 ulTaskNotifyTake()
API 函数作为 xTaskNotifyWait()
的简化替代。
任务通知系统并不局限于单一通知事件。配置参数 configTASK_NOTIFICATION_ARRAY_ENTRIES
默认设置为 1。如果该值大于 1,则会在每个任务内部创建一个通知数组。这样就可以按索引管理通知。每个任务通知 API 函数都有带索引的版本。使用不带索引的版本固定访问索引为 0 的通知。每个 API 函数的索引版本由后缀 “Indexed” 标识,因此函数 xTaskNotify
变成了 xTaskNotifyIndexed
。为简单起见,本书中将只使用每个函数的非索引版本。
任务通知 API 以宏的形式实现,宏调用每种 API 函数类型的底层通用版本。为简单起见,API 宏在本书中将被称为函数。
注:多个通知项
多个发送者对一个接收者时,有可能需要用到多个通知项。比如接受任务 L 正在等待发送任务 F1 的通知,此时另一个发送任务 F2 发送了通知,使 L 退出阻塞。L 阻塞的位置可能无法处理 F2 发来的数据,于是 L 内部就要添加额外判断逻辑,识别通知来源。
手动添加识别代码是可行的,但是 L 至少会因为无关的 F2 通知被唤醒一次,产生任务上下文切换开销,浪费处理时间。所以不如在唤醒 L 之前就明确 L 当前需要的通知来源。使用多个项,就可以让 L 等待 F1 时不被 F2 唤醒。
不过每多一个通知项都会让系统中每个任务多占用至少 5 字节 RAM,即使是不用任务通知的任务也一样。
10.3.1.1 API 函数的完整列表
- xTaskNotifyGive
- xTaskNotifyGiveIndexed
- vTaskNotifyGiveFromISR
- vTaskNotifyGiveIndexedFromISR
- vTaskNotifyTake
- vTaskNotifyTakeIndexed
- xTaskNotify
- xTaskNotifyIndexed
- xTaskNotifyWait
- xTaskNotifyWaitIndexed
- xTaskNotifyStateClear
- xTaskNotifyStateClearIndexed
- ulTaskNotifyValueClear
- ulTaskNotifyValueClearIndexed
- xTaskNotifyAndQueryIndexedFromISR
- xTaskNotifyAndQueryIndexedFromISR
- xTaskNotifyFromISR
- xTaskNotifyIndexedFromISR
- xTaskNotifyAndQuery
- xTaskNotifyAndQueryIndexed
这些函数实际上是宏。
注意:FromISR 函数不存在用于接收通知的函数,因为通知总是发送给任务,中断不能接收。
10.3.2 xTaskNotifyGive()
API 函数
xTaskNotifyGive()
会直接向任务发送通知,并使接收者的通知值加一。调用 xTaskNotifyGive()
会将接收任务的通知状态设置为挂起状态(如果尚未挂起)。
提供 xTaskNotifyGive()
API 函数是为了让任务通知能更轻便、更快速地替代二值信号量和计数信号量。
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
BaseType_t xTaskNotifyGiveIndexed( TaskHandle_t xTaskToNotify, UBaseType_t uxIndexToNotify );
xTaskNotifyGive()
API 函数的原型
xTaskNotifyGive()
和 xTaskNotifyGiveIndexed()
参数和返回值:
-
xTaskToNotify
目标任务的句柄。有关获取任务句柄的信息,请参阅
xTaskCreate()
API 函数的pxCreatedTask
参数。 -
uxIndexToNotify
数组索引
-
返回值
xTaskNotifyGive()
是一个调用xTaskNotify()
的宏。因为传入xTaskNotify()
的参数,pdPASS
是唯一可能的返回值。
10.3.3 vTaskNotifyGiveFromISR()
API 函数
vTaskNotifyGiveFromISR()
是 xTaskNotifyGive()
的中断安全版本。
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify,
BaseType_t *pxHigherPriorityTaskWoken );
void vTaskNotifyGiveIndexedFromISR( TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
BaseType_t *pxHigherPriorityTaskWoken );
xTaskNotifyGiveFromISR()
API 函数的原型
vTaskNotifyGiveFromISR()
和 vTaskNotifyGiveInexedFromISR()
参数和返回值:
-
xTaskToNotify
目标任务的句柄。有关获取任务句柄的信息,请参阅
xTaskCreate()
API 函数的pxCreatedTask
参数。 -
uxIndexToNotify
数组索引。
-
pxHigherPriorityTaskWoken
如果目标任务正在阻塞状态等待接收通知,那么发送通知将导致该任务离开阻塞状态。
如果调用
vTaskNotifyGiveFromISR()
会导致任务离开阻塞状态,且该任务的优先级高于当前正在执行的任务(被中断的任务)的优先级,那么vTaskNotifyGiveFromISR()
将在内部把*pxHigherPriorityTaskWoken
设为pdTRUE
。如果
vTaskNotifyGiveFromISR()
将该值设置为pdTRUE
,则应在退出中断前执行上下文切换。这将确保中断直接返回到优先级最高的就绪状态任务。与所有中断安全 API 函数一样,
*pxHigherPriorityTaskWoken
调用函数前必须设置为pdFALSE
。
10.3.4 ulTaskNotifyTake()
API 函数
ulTaskNotifyTake()
允许任务在阻塞状态下等待其通知值大于零,并在返回前将通知值清零或减一。
提供 ulTaskNotifyTake()
API 函数的目的是让任务通知作为二值信号量或计数信号量的高效替代。
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
TickType_t xTicksToWait );
uint32_t ulTaskNotifyTakeIndexed( UBaseType_t uxIndexToWaitOn,
BaseType_t xClearCountOnExit,
TickType_t xTicksToWait );
xTaskNotifyTake()
API 函数的原型
vTaskNotifyTake()
和 vTaskNotifyTake()
参数和返回值:
-
uxIndexToNotify
数组索引。
-
xClearCountOnExit
如果
xClearCountOnExit
设置为pdTRUE
,那么在调用ulTaskNotifyTake()
返回之前,调用者的通知值将被清零。如果
xClearCountOnExit
设置为pdFALSE
,且调用任务的通知值大于零,那么在ulTaskNotifyTake()
调用返回之前,调用任务的通知值将减一。 -
xTicksToWait
调用者为等待通知值变成大于零而保持阻塞状态的最长时间。
阻塞时间以 tick 周期为单位指定,因此它所代表的绝对时间取决于 tick 频率。宏
pdMS_TO_TICKS()
可用来将以毫秒为单位的时间转换为以 tick 为单位的时间。如果在
FreeRTOSConfig.h
中将INCLUDE_vTaskSuspend
设置为 1,将xTicksToWait
设置为portMAX_DELAY
,将导致任务无限期等待(不会超时)。 -
返回值
返回值是调用者的通知值被清零或减一之前的值,具体由
xClearCountOnExit
参数的值指定。如果指定了阻塞时间(
xTicksToWait
不为零),且返回值不为零,则可能是调用任务进入了阻塞状态,等待其通知值大于零,并在阻塞时间结束前更新了通知值。如果返回值为零,则调用任务被置入阻塞状态以等待其通知值大于零,但在此之前指定的阻塞时间已过期。
例 10.1 使用任务通知替代信号量,方法 1
例 7.1 使用二值信号量从中断服务例程中解锁任务,使任务与中断同步。本例复制了例 7.1 的功能,但使用了任务通知代替二值信号量。
清单 10.4 显示了与中断同步的任务的实现。例 7.1 中对 xSemaphoreTake()
的调用已被 ulTaskNotifyTake()
的调用所取代。
ulTaskNotifyTake()
的函数参数 xClearCountOnExit
被设置为 pdTRUE
,这将导致接收任务的通知值在 ulTaskNotifyTake()
返回前被清零。因此,有必要在每次调用 ulTaskNotifyTake()
前后处理所有已有事件。在例 7.1 中,由于使用了二值信号量,必须从硬件上确定待处理事件的数量,而这并不总是切实可行的。在例 10.1 中,待处理事件的数量由 ulTaskNotifyTake()
返回。
在调用 ulTaskNotifyTake
时发生的通知事件会被锁存在任务的通知值中,如果存在尚未处理的通知,调用 ulTaskNotifyTake()
会立即返回该通知的值。
/* The rate at which the periodic task generates software interrupts. */
const TickType_t xInterruptFrequency = pdMS_TO_TICKS( 500UL );
static void vHandlerTask( void *pvParameters )
{
/* xMaxExpectedBlockTime is set to be a little longer than the maximum
expected time between events. */
const TickType_t xMaxExpectedBlockTime = xInterruptFrequency +
pdMS_TO_TICKS( 10 );
uint32_t ulEventsToProcess;
/* As per most tasks, this task is implemented within an infinite loop. */
for( ;; )
{
/* Wait to receive a notification sent directly to this task from the
interrupt service routine. */
ulEventsToProcess = ulTaskNotifyTake( pdTRUE, xMaxExpectedBlockTime );
if( ulEventsToProcess != 0 )
{
/* To get here at least one event must have occurred. Loop here
until all the pending events have been processed (in this case,
just print out a message for each event). */
while( ulEventsToProcess > 0 )
{
vPrintString( "Handler task - Processing event.\r\n" );
ulEventsToProcess--;
}
}
else
{
/* If this part of the function is reached then an interrupt did
not arrive within the expected time, and (in a real application)
it may be necessary to perform some error recovery operations. */
}
}
}
用于产生软件中断的周期性任务会在中断产生前后各打印一条信息。这样就可以从输出结果中观察到执行顺序。
清单 10.5 显示了中断服务程序。它的工作只是向延迟中断处理任务发送通知。
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
/* The xHigherPriorityTaskWoken parameter must be initialized to pdFALSE as
it will get set to pdTRUE inside the interrupt safe API function if a
context switch is required. */
xHigherPriorityTaskWoken = pdFALSE;
/* Send a notification directly to the task to which interrupt processing
is being deferred. */
vTaskNotifyGiveFromISR( /* The handle of the task to which the notification
is being sent. The handle was saved when the task
was created. */
xHandlerTask,
/* xHigherPriorityTaskWoken is used in the usual
way. */
&xHigherPriorityTaskWoken );
/* Pass the xHigherPriorityTaskWoken value into portYIELD_FROM_ISR(). If
xHigherPriorityTaskWoken was set to pdTRUE inside vTaskNotifyGiveFromISR()
then calling portYIELD_FROM_ISR() will request a context switch. If
xHigherPriorityTaskWoken is still pdFALSE then calling
portYIELD_FROM_ISR() will have no effect. The implementation of
portYIELD_FROM_ISR() used by the Windows port includes a return statement,
which is why this function does not explicitly return a value. */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
执行例 10.1 时的输出如图 10.3 所示。vHandlerTask()
在中断产生后立即进入运行状态,因此该任务的输出与周期性任务完全相同。图 10.4 提供了进一步解释。
Handler task - Processing event.
Periodic task - Interrupt generated.
Perodic task - About to generate an interrupt.
Handler task - Processing event.
Periodic task - Interrupt generated.
Perodic task - About to generate an interrupt.
Handler task - Processing event.
Periodic task - Interrupt generated.
Perodic task - About to generate an interrupt.
Handler task - Processing event.
Periodic task - Interrupt generated.
Perodic task - About to generate an interrupt.
Handler task - Processing event.
Periodic task - Interrupt generated.
例 10.2 使用任务通知替代信号量,方法 2
在例 10.1 中,ulTaskNotifyTake()
的参数 xClearOnExit
被设置为 pdTRUE。例 10.2 对例 10.1 稍作修改,演示了当 xClearOnExit
参数设置为 pdFALSE
时的行为。
当 xClearOnExit
为 pdFALSE
时,调用 ulTaskNotifyTake()
只会减一调用任务的通知值,而不是将其清零。因此,通知计数是已发生事件数与已处理事件数之间的差值。这样一来,vHandlerTask()
的结构就可以从两个方面简化:
- 等待处理的事件数保存在通知值中,因此无需复制到局部变量。
- 每次调用
ulTaskNotifyTake()
只需处理一个事件。
例 10.2 中 vHandlerTask()
的实现如清单 10.6 所示。
static void vHandlerTask( void *pvParameters )
{
/* xMaxExpectedBlockTime is set to be a little longer than the maximum
expected time between events. */
const TickType_t xMaxExpectedBlockTime = xInterruptFrequency +
pdMS_TO_TICKS( 10 );
/* As per most tasks, this task is implemented within an infinite loop. */
for( ;; )
{
/* Wait to receive a notification sent directly to this task from the
interrupt service routine. The xClearCountOnExit parameter is now
pdFALSE, so the task's notification value will be decremented by
ulTaskNotifyTake(), and not cleared to zero. */
if( ulTaskNotifyTake( pdFALSE, xMaxExpectedBlockTime ) != 0 )
{
/* To get here an event must have occurred. Process the event (in
this case just print out a message). */
vPrintString( "Handler task - Processing event.\r\n" );
}
else
{
/* If this part of the function is reached then an interrupt did
not arrive within the expected time, and (in a real application)
it may be necessary to perform some error recovery operations. */
}
}
}
为了演示,还修改了中断服务例程,以便在每次中断时发送一个以上的任务通知,从而模拟高频率发生的多个中断。例 10.2 中使用的中断服务例程的实现如清单 10.7 所示。
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE;
/* Send a notification to the handler task multiple times. The first
'give' will unblock the task, the following 'gives' are to demonstrate
that the receiving task's notification value is being used to count
(latch) events - allowing the task to process each event in turn. */
vTaskNotifyGiveFromISR( xHandlerTask, &xHigherPriorityTaskWoken );
vTaskNotifyGiveFromISR( xHandlerTask, &xHigherPriorityTaskWoken );
vTaskNotifyGiveFromISR( xHandlerTask, &xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
执行例 10.2 时产生的输出如图 10.5 所示。可以看出,每次中断发生时,vHandlerTask()
都会处理所有三个事件。
Handler task - Processing event.
Handler task - Processing event.
Handler task - Processing event.
Periodic task - Interrupt generated.
Perodic task - About to generate an interrupt.
Handler task - Processing event.
Handler task - Processing event.
Handler task - Processing event.
Periodic task - Interrupt generated.
Perodic task - About to generate an interrupt.
Handler task - Processing event.
Handler task - Processing event.
Handler task - Processing event.
Periodic task - Interrupt generated.
Perodic task - About to generate an interrupt.
Handler task - Processing event.
Handler task - Processing event.
Handler task - Processing event.
10.3.5 xTaskNotify()
和 xTaskNotifyFromISR()
API 函数
xTaskNotify()
是功能更强的 xTaskNotifyGive()
版本,可用于以下列任一方式更新接收者的通知值:
- 将接收任务的通知值加一,在这种情况下,
xTaskNotify()
等同于xTaskNotifyGive()
。 - 在接收任务的通知值中设置一个或多个位,从而替代事件组。
- 在接收任务的通知值中写入新值,如果接收任务已处理上一个值,就可以用通知值作为长度为 1 的队列。
- 向接收任务的通知值中写入新值,即使接收任务没有处理上一个值。这样就能实现
xQueueOverwrite()
API 函数的功能。这种功能有时被称为 “邮箱”。
xTaskNotify()
比 xTaskNotifyGive()
更灵活、功能更强大,也正因为这种额外的灵活性和强大功能,它的使用也更复杂一些。
xTaskNotifyFromISR()
是可在中断服务例程中使用的 xTaskNotify()
版本,因此多了 pxHigherPriorityTaskWoken
参数。
调用 xTaskNotify()
时,如果目标任务的通知状态不为挂起,则将其设置为挂起状态。
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction );
BaseType_t xTaskNotifyIndexed( TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
uint32_t ulValue,
eNotifyAction eAction );
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken );
BaseType_t xTaskNotifyIndexedFromISR( TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken );
xTaskNotify()
API 函数的原型
vTaskNotifyGiveFromISR()
和 vTaskNotifyGiveInexedFromISR()
参数和返回值:
-
xTaskToNotify
目标任务的句柄。有关获取任务句柄的信息,请参阅
xTaskCreate()
API 函数的pxCreatedTask
参数。 -
uxIndexToNotify
数组索引。
-
ulValue
ulValue
的意义取决于eNotifyAction
的值。 -
eAction
枚举值,指定更新通知值的方式。
-
pxHigherPriorityTaskWoken
如果目标任务正在阻塞状态等待接收通知,那么发送通知将导致该任务离开阻塞状态。
如果调用
vTaskNotifyFromISR()
会导致任务离开阻塞状态,且该任务的优先级高于当前正在执行的任务(被中断的任务)的优先级,那么vTaskNotifyFromISR()
将在内部把*pxHigherPriorityTaskWoken
设为pdTRUE
。如果
vTaskNotifyFromISR()
将该值设置为pdTRUE
,则应在退出中断前执行上下文切换。这将确保中断直接返回到优先级最高的就绪状态任务。与所有中断安全 API 函数一样,
*pxHigherPriorityTaskWoken
调用函数前必须设置为pdFALSE
。 -
返回值
除了下文提及的一种情况,函数总是返回
pdPass
。
10.3.5.1 有效的 eAction
取值及其效果
-
eNoAction
接收任务的通知状态被设置为挂起,通知值不更新。不使用
ulValue
参数。
eNoAction
操作允许将任务通知用作更快、更轻量级的二值信号量替代品。 -
eSetBits
目标任务的通知值与
ulValue
的值按位或运算,结果作为通知值的新值。例如,ulValue
设置为 0x01,那么目标任务的通知值第 0 位会被置位。再比如,ulValue
为 0x06(二进制 0110),那么通知值中的位 1 和位 2 将被置位。使用
eSetBits
操作,任务通知可以更快、更轻量级地替代事件组。 -
eIncrement
目标任务的通知值会加一。不使用
ulValue
参数。eIncrement
操作允许将任务通知用作二值信号量或计数信号量的替代品,效果等同于调用xTaskNotifyGive()
API 函数。 -
eSetValueWithoutOverwrite
如果目标任务在调用
xTaskNotify()
之前有一个挂起的通知,则不会采取任何操作,xTaskNotify()
将返回pdFAIL
。如果在调用
xTaskNotify()
之前目标任务没有挂起的通知,那么目标任务的通知值将被设置为ulValue
参数的值。 -
eSetValueWithOverwrite
目标任务的通知值将被设置为 ulValue 参数中的值,无论是否有待处理的通知。
10.3.6 xTaskNotifyWait()
API 函数
xTaskNotifyWait()
是功能更强的 ulTaskNotifyTake()
。xTaskNotifyWait()
提供了选项,可在进入函数和退出函数时清除调用任务通知值中位。
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );
BaseType_t xTaskNotifyWaitIndexed( UBaseType_t uxIndexToWaitOn,
uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );
xTaskNotifyWait()
API 函数的原型
xTaskNotifyWait()
函数参数和返回值:
-
uxIndexToWaitOn
数组索引。
-
ulBitsToClearOnEntry
如果调用者在调用
xTaskNotifyWait()
之前没有待处理的通知,那么在进入函数时,任务通知值中ulBitsToClearOnEntry
指定的任何位都将被清除。例如,
ulBitsToClearOnEntry
为 0x01,则任务通知值的第 0 位将被清除。再比如,将ulBitsToClearOnEntry
设置为 0xffffffffff(ULONG_MAX
)将清除任务通知值中的所有位,从而将通知值清零。 -
ulBitsToClearOnExit
如果调用任务退出
xTaskNotifyWait()
是因为收到了通知,或者是因为在调用xTaskNotifyWait()
时已经有通知等待处理,那么在任务退出xTaskNotifyWait()
函数之前,将清除任务通知值中用ulBitsToClearOnExit
指定的任何位。将通知值保存到
*pulNotificationValue
(请参阅下文对 pulNotificationValue 的描述)再清除通知值。例如,
ulBitsToClearOnExit
为 0x03,那么任务通知值的第 0 位和第 1 位将在函数退出前被清除。将
ulBitsToClearOnExit
设置为 0xffffffffff(ULONG_MAX
)将清除任务通知值中的所有位,从而将通知值清零。 -
pulNotificationValue
用于传出任务的通知值。复制到
*pulNotificationValue
的值是被ulBitsToClearOnExit
清零之前的通知值。pulNotificationValue
是可选的,不需要时可设为NULL
。 -
xTicksToWait
调用者为等待通知变为挂起状态而保持阻塞状态的最长时间。
阻塞时间以 tick 周期为单位指定,因此它所代表的绝对时间取决于 tick 频率。宏
pdMS_TO_TICKS()
可用来将以毫秒为单位的时间转换为以 tick 为单位的时间。如果在
FreeRTOSConfig.h
中将INCLUDE_vTaskSuspend
设置为 1,将xTicksToWait
设置为portMAX_DELAY
,将导致任务无限期等待(不会超时)。 -
返回值
有两种可能的返回值:
-
pdTRUE
这表明
xTaskNotifyWait()
返回的原因是收到了通知,或者调用xTaskNotifyWait()
前已有通知等待处理。如果指定了阻塞时间(
xTicksToWait
不为零),那么调用任务就有可能进入阻塞状态,等待其通知状态变为挂起,在超时到期前,任务收到了通知。 -
pdFALSE
这表明
xTaskNotifyWait()
返回时,调用任务没有收到任务通知。如果
xTicksToWait
不为零,那么调用任务将处于阻塞状态,等待其通知状态变为待处理,但在超时到期前,任务没有收到通知。
-
10.3.7 在外设驱动中使用任务通知:UART 的例子
外设驱动程序库提供了在硬件接口上执行常见操作的函数。通常此类库支持的外设包括通用异步收发器(UART)、串行外设接口(SPI)、模数转换器(ADC)和以太网口。这类库通常提供的功能包括初始化外设、向外设发送数据和从外设接收数据。
外设上的某些操作需要较长的时间才能完成,比如高精度 ADC 转换和在 UART 上传输大数据包。在这种情况下,驱动程序库函数可以轮询外设的状态寄存器,以确定操作何时完成。然而,这种轮询方式占用了处理器 100% 的时间,却没有执行任何有效的处理。在多任务系统中,这种浪费尤其严重,因为正在轮询外设的任务可能会妨碍优先级较低的任务的执行,而后者有工作要处理。
为了避免潜在的处理时间浪费,在 RTOS 下,高效的设备驱动程序应该是中断驱动的,让启动冗长操作的任务可以在阻塞状态等待操作完成。这样,当执行冗长操作的任务处于阻塞状态时,优先级较低的任务就可以执行,处理时间都被有效的利用。
使用二值信号量将任务置入阻塞状态,这是 RTOS 优化的驱动程序库的常见做法。清单 10.10 中的伪代码演示了这种技术,它提供了一个在 UART 端口上传输数据的库函数的概要。在清单 10.10 中
xUART
是一个描述 UART 外设并保存状态信息的结构。结构体的xTxSemaphore
成员是SemaphoreHandle_t
类型的变量。假设该信号量已预先创建。xUART_Send()
函数不包含任何互斥逻辑。如果有多个任务要使用xUART_Send()
函数,应用程序就必须在任务内部管理互斥。例如,可能需要任务在调用xUART_Send()
之前获取互斥量。xSemaphoreTake()
API 函数用于在启动 UART 传输后将调用者置入阻塞状态。xSemaphoreGiveFromISR()
API 函数用于在传输完成后(即 UART 外设的发送结束中断服务例程执行时)将任务从阻塞状态移除。
/* Driver library function to send data to a UART. */
BaseType_t xUART_Send( xUART *pxUARTInstance,
uint8_t *pucDataSource,
size_t uxLength )
{
BaseType_t xReturn;
/* Ensure the UART's transmit semaphore is not already available by
attempting to take the semaphore without a timeout. */
xSemaphoreTake( pxUARTInstance->xTxSemaphore, 0 );
/* Start the transmission. */
UART_low_level_send( pxUARTInstance, pucDataSource, uxLength );
/* Block on the semaphore to wait for the transmission to complete. If
the semaphore is obtained then xReturn will get set to pdPASS. If the
semaphore take operation times out then xReturn will get set to pdFAIL.
Note that, if the interrupt occurs between UART_low_level_send() being
called, and xSemaphoreTake() being called, then the event will be
latched in the binary semaphore, and the call to xSemaphoreTake() will
return immediately. */
xReturn = xSemaphoreTake( pxUARTInstance->xTxSemaphore,
pxUARTInstance->xTxTimeout );
return xReturn;
}
/*-----------------------------------------------------------*/
/* The service routine for the UART's transmit end interrupt, which executes
after the last byte has been sent to the UART. */
void xUART_TransmitEndISR( xUART *pxUARTInstance )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* Clear the interrupt. */
UART_low_level_interrupt_clear( pxUARTInstance );
/* Give the Tx semaphore to signal the end of the transmission. If a task
is Blocked waiting for the semaphore then the task will be removed from
the Blocked state. */
xSemaphoreGiveFromISR( pxUARTInstance->xTxSemaphore,
&xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
清单 10.10 中演示的技术是完全可行的,实际上也是常见的做法,但它有一些缺点:
- 该库使用了多个信号量,这增加了其 RAM 占用。
- 信号量在创建之前不能使用,因此使用信号量的库在显式初始化之前不能使用。
- 信号量是一种通用对象,适用于各种使用情况;它包括允许任意数量的任务在阻塞状态下等待信号量可用的逻辑,以及在信号量可用时选择(以确定的方式)将哪个任务从阻塞状态中移除的逻辑。执行这些逻辑需要一定的时间,而在清单 10.10 所示的情况下,这种处理开销是不必要的,因为在该情况下,任何给定时间都不会有一个以上的任务在等待信号量。
清单 10.11 使用任务通知代替二值信号量,从而避免这些缺点。
注:如果库使用任务通知,则库的文档必须明确说明调用库函数会改变调用任务的通知状态和通知值。注:还有必要明确设置要使用的通知索引,以及使用通知值的方式
在清单 10.11 中
xTaskToNotify
是一个TaskHandle_t
类型的变量,用来引用与 UART 操作完成事件同步的任务。xTaskGetCurrentTaskHandle()
API 函数用于获取处于运行状态的任务句柄。- 该库不会创建任何 FreeRTOS 对象,因此不会产生 RAM 开销,也无需显式初始化。
- 任务通知会直接发送给正在等待 UART 操作完成的任务,因此减少了不必要的逻辑。
xUART
结构的 xTaskToNotify
成员可通过任务和中断服务例程访问,因此需要考虑更新该值的方式:
- 如果
xTaskToNotify
通过单次内存写入操作更新,那么就可以在临界区之外更新,如清单 10.11 所示。如果xTaskToNotify
是一个 32 位变量(TaskHandle_t
是一个 32 位类型),而运行 FreeRTOS 的处理器是一个 32 位处理器,就会是这种情况。 - 如果更新
xTaskToNotify
需要不止一次内存写入操作,那么xTaskToNotify
必须在临界区内更新,否则中断服务例程可能会在xTaskToNotify
被修改的中途访问它。如果xTaskToNotify
是一个 16 位变量,而运行 FreeRTOS 的处理器是一个 8 位处理器,就会出现这种情况,因为更新所有 16 位变量需要两次 8 位内存写入操作。
在 FreeRTOS 的内部实现中,TaskHandle_t
是一个指针,因此 sizeof( TaskHandle_t )
始终等于 sizeof( void * )
。
/* Driver library function to send data to a UART. */
BaseType_t xUART_Send( xUART *pxUARTInstance,
uint8_t *pucDataSource,
size_t uxLength )
{
BaseType_t xReturn;
/* Save the handle of the task that called this function. The book text
contains notes as to whether the following line needs to be protected
by a critical section or not. */
pxUARTInstance->xTaskToNotify = xTaskGetCurrentTaskHandle();
/* Ensure the calling task does not already have a notification pending by
calling ulTaskNotifyTake() with the xClearCountOnExit parameter set to
pdTRUE, and a block time of 0 (don't block). */
ulTaskNotifyTake( pdTRUE, 0 );
/* Start the transmission. */
UART_low_level_send( pxUARTInstance, pucDataSource, uxLength );
/* Block until notified that the transmission is complete. If the
notification is received then xReturn will be set to 1 because the ISR
will have incremented this task's notification value to 1 (pdTRUE). If
the operation times out then xReturn will be 0 (pdFALSE) because this
task's notification value will not have been changed since it was
cleared to 0 above. Note that, if the ISR executes between the calls to
UART_low_level_send() and the call to ulTaskNotifyTake(), then the
event will be latched in the task's notification value, and the call to
ulTaskNotifyTake() will return immediately. */
xReturn = ( BaseType_t ) ulTaskNotifyTake( pdTRUE,
pxUARTInstance->xTxTimeout );
return xReturn;
}
/*-----------------------------------------------------------*/
/* The ISR that executes after the last byte has been sent to the UART. */
void xUART_TransmitEndISR( xUART *pxUARTInstance )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* This function should not execute unless there is a task waiting to be
notified. Test this condition with an assert. This step is not strictly
necessary, but will aid debugging. configASSERT() is described in
section 12.2. */
configASSERT( pxUARTInstance->xTaskToNotify != NULL );
/* Clear the interrupt. */
UART_low_level_interrupt_clear( pxUARTInstance );
/* Send a notification directly to the task that called xUART_Send(). If
the task is Blocked waiting for the notification then the task will be
removed from the Blocked state. */
vTaskNotifyGiveFromISR( pxUARTInstance->xTaskToNotify,
&xHigherPriorityTaskWoken );
/* Now there are no tasks waiting to be notified. Set the xTaskToNotify
member of the xUART structure back to NULL. This step is not strictly
necessary but will aid debugging. */
pxUARTInstance->xTaskToNotify = NULL;
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
任务通知也可以取代接收函数中的信号量,如伪代码清单 10.12 所示,它提供了一个在 UART 端口接收数据的库函数的大纲。请参考清单 10.12:
xUART_Receive()
函数不包含任何互斥逻辑。如果不止一个任务要使用xUART_Receive()
函数,那么应用程序编写者就必须在任务内部管理互斥。例如,任务可能需要在调用xUART_Receive()
之前获得互斥量。- UART 的接收中断服务例程会将 UART 接收到的字符放入 RAM 缓冲区。
xUART_Receive()
函数从 RAM 缓冲区返回字符。 xUART_Receive()
函数的参数uxWantedBytes
用于指定要接收的字符数。如果 RAM 缓冲区中尚未包含所请求的字符数,则调用任务将进入阻塞状态,等待缓冲区中字符足量的通知。while()
循环用于重复此序列,直到接收缓冲区包含所请求的字符数,或出现超时为止。- 调用任务可能不止一次进入阻塞状态。因此,会根据调用
xUART_Receive()
后已经过去的时间调整阻塞时间。这些调整可确保在xUART_Receive()
中花费的总时间不超过xUART
结构中xRxTimeout
成员指定的阻塞时间。阻塞时间可使用vTaskSetTimeOutState()
和xTaskCheckForTimeOut()
辅助函数进行调整。
/* Driver library function to receive data from a UART. */
size_t xUART_Receive( xUART *pxUARTInstance,
uint8_t *pucBuffer,
size_t uxWantedBytes )
{
size_t uxReceived = 0;
TickType_t xTicksToWait;
TimeOut_t xTimeOut;
/* Record the time at which this function was entered. */
vTaskSetTimeOutState( &xTimeOut );
/* xTicksToWait is the timeout value - it is initially set to the maximum
receive timeout for this UART instance. */
xTicksToWait = pxUARTInstance->xRxTimeout;
/* Save the handle of the task that called this function. The book text
contains notes as to whether the following line needs to be protected
by a critical section or not. */
pxUARTInstance->xTaskToNotify = xTaskGetCurrentTaskHandle();
/* Loop until the buffer contains the wanted number of bytes, or a
timeout occurs. */
while( UART_bytes_in_rx_buffer( pxUARTInstance ) < uxWantedBytes )
{
/* Look for a timeout, adjusting xTicksToWait to account for the time
spent in this function so far. */
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) != pdFALSE )
{
/* Timed out before the wanted number of bytes were available,
exit the loop. */
break;
}
/* The receive buffer does not yet contain the required amount of
bytes. Wait for a maximum of xTicksToWait ticks to be notified that
the receive interrupt service routine has placed more data into the
buffer. It does not matter if the calling task already had a
notification pending when it called this function, if it did, it
would just iteration around this while loop one extra time. */
ulTaskNotifyTake( pdTRUE, xTicksToWait );
}
/* No tasks are waiting for receive notifications, so set xTaskToNotify
back to NULL. The book text contains notes as to whether the following
line needs to be protected by a critical section or not. */
pxUARTInstance->xTaskToNotify = NULL;
/* Attempt to read uxWantedBytes from the receive buffer into pucBuffer.
The actual number of bytes read (which might be less than uxWantedBytes)
is returned. */
uxReceived = UART_read_from_receive_buffer( pxUARTInstance,
pucBuffer,
uxWantedBytes );
return uxReceived;
}
/*-----------------------------------------------------------*/
/* The interrupt service routine for the UART's receive interrupt */
void xUART_ReceiveISR( xUART *pxUARTInstance )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* Copy received data into this UART's receive buffer and clear the
interrupt. */
UART_low_level_receive( pxUARTInstance );
/* If a task is waiting to be notified of the new data then notify it now. */
if( pxUARTInstance->xTaskToNotify != NULL )
{
vTaskNotifyGiveFromISR( pxUARTInstance->xTaskToNotify,
&xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
}
注:多次阻塞
意思是,调用接收函数时,指定的超时时间是接受完指定数量数据的总时间,接收函数可能需要多次接收这么多数据。比如,一共要接收 10 个字节,超时时间 10ms;函数第一次唤醒时接收了 5 字节,此时已经过了 4ms,那么接收剩下的 5 字节的时限就只剩下 6ms。
10.3.8 在外设驱动中使用任务通知:ADC 的例子
上一节演示了如何使用 vTaskNotifyGiveFromISR()
从中断向任务发送任务通知。vTaskNotifyGiveFromISR()
是一个简单易用的函数,但功能有限;它只能发送事件,不能发送数据。本节将演示如何使用 xTaskNotifyFromISR()
发送数据。清单 10.13 所示的伪代码演示了这一技术,该代码提供了模数转换器 (ADC) 中断服务例程的概要。在清单 10.13 中
- 假定 ADC 转换至少每 50 毫秒启动一次。
ADC_ConversionEndISR()
是 ADC 转换结束中断的中断服务例程,即每次有新的 ADC 值时执行的中断。vADCTask()
实现的任务处理 ADC 生成的每个值。假设在创建任务时,任务句柄已存储在xADCTaskToNotify
中。ADC_ConversionEndISR()
调用xTaskNotifyFromISR()
,将eAction
参数设置为eSetValueWithoutOverwrite
,从而向vADCTask()
任务发送任务通知,并将 ADC 转换结果写入任务的通知值。vADCTask()
任务使用xTaskNotifyWait()
等待 ADC 转换完成的通知,并从其通知值中获取 ADC 转换的结果。
/* A task that uses an ADC. */
void vADCTask( void *pvParameters )
{
uint32_t ulADCValue;
BaseType_t xResult;
/* The rate at which ADC conversions are triggered. */
const TickType_t xADCConversionFrequency = pdMS_TO_TICKS( 50 );
for( ;; )
{
/* Wait for the next ADC conversion result. */
xResult = xTaskNotifyWait(
/* The new ADC value will overwrite the old value, so there
is no need to clear any bits before waiting for the new
notification value. */
0,
/* Future ADC values will overwrite the existing value, so
there is no need to clear any bits before exiting
xTaskNotifyWait(). */
0,
/* The address of the variable into which the task's
notification value (which holds the latest ADC
conversion result) will be copied. */
&ulADCValue,
/* A new ADC value should be received every
xADCConversionFrequency ticks. */
xADCConversionFrequency * 2 );
if( xResult == pdPASS )
{
/* A new ADC value was received. Process it now. */
ProcessADCResult( ulADCValue );
}
else
{
/* The call to xTaskNotifyWait() did not return within the expected
time, something must be wrong with the input that triggers the
ADC conversion, or with the ADC itself. Handle the error here. */
}
}
}
/*-----------------------------------------------------------*/
/* The interrupt service routine that executes each time an ADC conversion
completes. */
void ADC_ConversionEndISR( xADC *pxADCInstance )
{
uint32_t ulConversionResult;
BaseType_t xHigherPriorityTaskWoken = pdFALSE, xResult;
/* Read the new ADC value and clear the interrupt. */
ulConversionResult = ADC_low_level_read( pxADCInstance );
/* Send a notification, and the ADC conversion result, directly to
vADCTask(). */
xResult = xTaskNotifyFromISR( xADCTaskToNotify, /* xTaskToNotify parameter */
ulConversionResult, /* ulValue parameter */
eSetValueWithoutOverwrite, /* eAction parameter. */
&xHigherPriorityTaskWoken );
/* If the call to xTaskNotifyFromISR() returns pdFAIL then the task is not
keeping up with the rate at which ADC values are being generated.
configASSERT() is described in section 11.2. */
configASSERT( xResult == pdPASS );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
10.3.9 在应用中直接使用任务通知
本节将演示任务通知在一个假设应用程序中的使用,加强任务通知的应用能力,该应用程序包括以下功能:
- 该应用程序通过缓慢的互联网连接通信,向远程数据服务器发送数据和请求数据。下文将远程数据服务器称为云服务器。
- 从云服务器请求数据后,请求任务必须在阻塞状态下等待响应数据。
- 向云服务器发送数据后,发送任务必须在阻塞状态下等待云服务器确认数据正确接收的响应。
软件设计如图 10.6 所示:
- 与云服务器通信的多个互联网连接的复杂性被封装在一个 FreeRTOS 任务中。该任务在 FreeRTOS 应用程序中充当代理服务器,被称为服务器任务。
- 应用程序任务调用
CloudRead()
从云服务器读取数据。CloudRead()
不会直接与云服务器通信,而是将读取请求发送到服务器任务监听的队列中,并以任务通知的形式从服务器任务接收所请求的数据。 - 应用任务调用
CloudWrite()
向云服务器写入数据。CloudWrite()
不会直接与云服务器通信,而是将写请求发送到服务器任务的队列,并以任务通知的形式从服务器任务接收写操作的结果。
CloudRead()
和 CloudWrite()
函数发给服务器任务的结构体如清单 10.14 所示。
typedef enum CloudOperations
{
eRead, /* Send data to the cloud server. */
eWrite /* Receive data from the cloud server. */
} Operation_t;
typedef struct CloudCommand
{
Operation_t eOperation; /* The operation to perform (read or write). */
uint32_t ulDataID; /* Identifies the data being read or written. */
uint32_t ulDataValue; /* Only used when writing data to the cloud server. */
TaskHandle_t xTaskToNotify;/* The handle of the task performing the operation. */
} CloudCommand_t;
CloudRead()
的伪代码如清单 10.15 所示。该函数向服务器任务发送请求,然后调用 xTaskNotifyWait()
在阻塞状态下等待,直到收到响应数据的通知。
清单 10.16 所示的伪代码显示了服务器任务处理 read 请求的逻辑。从云服务器收到数据后,服务器任务会解除对应任务的阻塞,并调用 xTaskNotify()
将收到的数据发送给应用任务,同时将 eAction
参数设置为 eSetValueWithOverwrite
。
清单 10.16 显示了一个简化的场景,因为它假定 GetCloudData()
无需等待即可从云服务器获取值。
/* ulDataID identifies the data to read. pulValue holds the address of the
variable into which the data received from the cloud server is to be written. */
BaseType_t CloudRead( uint32_t ulDataID, uint32_t *pulValue )
{
CloudCommand_t xRequest;
BaseType_t xReturn;
/* Set the CloudCommand_t structure members to be correct for this read
request. */
xRequest.eOperation = eRead; /* This is a request to read data. */
xRequest.ulDataID = ulDataID; /* A code that identifies the data to read. */
xRequest.xTaskToNotify = xTaskGetCurrentTaskHandle(); /* Handle of the
calling task. */
/* Ensure there are no notifications already pending by reading the
notification value with a block time of 0, then send the structure to
the server task. */
xTaskNotifyWait( 0, 0, NULL, 0 );
xQueueSend( xServerTaskQueue, &xRequest, portMAX_DELAY );
/* Wait for a notification from the server task. The server task writes
the value received from the cloud server directly into this task's
notification value, so there is no need to clear any bits in the
notification value on entry to or exit from the xTaskNotifyWait()
function. The received value is written to *pulValue, so pulValue is
passed as the address to which the notification value is written. */
xReturn = xTaskNotifyWait( 0, /* No bits cleared on entry */
0, /* No bits to clear on exit */
pulValue, /* Notification value into *pulValue */
pdMS_TO_TICKS( 250 ) ); /* Wait 250ms maximum */
/* If xReturn is pdPASS, then the value was obtained. If xReturn is pdFAIL,
then the request timed out. */
return xReturn;
}
CloudRead()
函数的伪代码实现
void ServerTask( void *pvParameters )
{
CloudCommand_t xCommand;
uint32_t ulReceivedValue;
for( ;; )
{
/* Wait for the next CloudCommand_t structure to be received from a task */
xQueueReceive( xServerTaskQueue, &xCommand, portMAX_DELAY );
switch( xCommand.eOperation ) /* Was it a read or write request? */
{
case eRead:
/* Obtain the requested data item from the remote cloud server */
ulReceivedValue = GetCloudData( xCommand.ulDataID );
/* Call xTaskNotify() to send both a notification and the value
received from the cloud server to the task that made the
request. The handle of the task is obtained from the
CloudCommand_t structure. */
xTaskNotify( xCommand.xTaskToNotify, /* The task's handle is in
the structure */
ulReceivedValue, /* Cloud data sent as notification
value */
eSetValueWithOverwrite );
break;
/* Other switch cases go here. */
}
}
}
实现 CloudWrite()
的伪代码如清单 10.17 所示。为便于演示,CloudWrite()
返回二进制 bit 状态码,状态码中的每一位都有唯一的含义。清单 10.17 顶部的 #define
语句显示了四个示例状态位。
任务清除四个状态位,向服务器任务发送请求,然后调用 xTaskNotifyWait()
在阻塞状态下等待状态通知。
/* Status bits used by the cloud write operation. */
#define SEND_SUCCESSFUL_BIT ( 0x01 << 0 )
#define OPERATION_TIMED_OUT_BIT ( 0x01 << 1 )
#define NO_INTERNET_CONNECTION_BIT ( 0x01 << 2 )
#define CANNOT_LOCATE_CLOUD_SERVER_BIT ( 0x01 << 3 )
/* A mask that has the four status bits set. */
#define CLOUD_WRITE_STATUS_BIT_MASK ( SEND_SUCCESSFUL_BIT |
OPERATION_TIMED_OUT_BIT |
NO_INTERNET_CONNECTION_BIT |
CANNOT_LOCATE_CLOUD_SERVER_BIT )
uint32_t CloudWrite( uint32_t ulDataID, uint32_t ulDataValue )
{
CloudCommand_t xRequest;
uint32_t ulNotificationValue;
/* Set the CloudCommand_t structure members to be correct for this
write request. */
xRequest.eOperation = eWrite; /* This is a request to write data */
xRequest.ulDataID = ulDataID; /* A code that identifies the data being
written */
xRequest.ulDataValue = ulDataValue; /* Value of the data written to the
cloud server. */
xRequest.xTaskToNotify = xTaskGetCurrentTaskHandle(); /* Handle of the
calling task. */
/* Clear the three status bits relevant to the write operation by calling
xTaskNotifyWait() with the ulBitsToClearOnExit parameter set to
CLOUD_WRITE_STATUS_BIT_MASK, and a block time of 0. The current
notification value is not required, so the pulNotificationValue
parameter is set to NULL. */
xTaskNotifyWait( 0, CLOUD_WRITE_STATUS_BIT_MASK, NULL, 0 );
/* Send the request to the server task. */
xQueueSend( xServerTaskQueue, &xRequest, portMAX_DELAY );
/* Wait for a notification from the server task. The server task writes
a bitwise status code into this task's notification value, which is
written to ulNotificationValue. */
xTaskNotifyWait( 0, /* No bits cleared on entry. */
CLOUD_WRITE_STATUS_BIT_MASK, /* Clear relevant bits to 0 on exit. */
&ulNotificationValue, /* Notified value. */
pdMS_TO_TICKS( 250 ) ); /* Wait a maximum of 250ms. */
/* Return the status code to the calling task. */
return ( ulNotificationValue & CLOUD_WRITE_STATUS_BIT_MASK );
}
CloudWrite()
函数的伪代码实现
清单 10.18 所示的伪代码演示了服务器任务管理 write 请求的逻辑。数据发送到云服务器后,服务器任务会解锁对应任务,调用 xTaskNotify()
将 eAction
参数设置为 eSetBits
,向应用任务发送二进制 bit 状态码。在接收任务的通知值中,只有 CLOUD_WRITE_STATUS_BIT_MASK
常量定义的位会被更改,因此接收任务可以将其通知值中的其他位用于其他目的。
清单 10.18 显示了一个简化的场景,因为它假定 SetCloudData()
无需等待即可从远程云服务器获取确认。
void ServerTask( void *pvParameters )
{
CloudCommand_t xCommand;
uint32_t ulBitwiseStatusCode;
for( ;; )
{
/* Wait for the next message. */
xQueueReceive( xServerTaskQueue, &xCommand, portMAX_DELAY );
/* Was it a read or write request? */
switch( xCommand.eOperation )
{
case eWrite:
/* Send the data to the remote cloud server. SetCloudData() returns
a bitwise status code that only uses the bits defined by the
CLOUD_WRITE_STATUS_BIT_MASK definition (shown in Listing 10.17). */
ulBitwiseStatusCode = SetCloudData( xCommand.ulDataID,
xCommand.ulDataValue );
/* Send a notification to the task that made the write request.
The eSetBits action is used so any status bits set in
ulBitwiseStatusCode will be set in the notification value of
the task being notified. All the other bits remain unchanged.
The handle of the task is obtained from the CloudCommand_t
structure. */
xTaskNotify( xCommand.xTaskToNotify, /* The task's handle is in
the structure. */
ulBitwiseStatusCode, /* Cloud data sent as
notification value. */
eSetBits );
break;
/* Other switch cases go here. */
}
}
}
十一、低功耗支持
11.1 低功耗介绍
FreeRTOS 通过空闲任务钩子和无 tick 空闲模式提供了进入低功耗模式的简便方法。
使用空闲任务钩子将微控制器置于低功耗状态,降低运行功耗,这是很常见的方法。由于必须周期性地退出然后重新进入低功耗状态以处理 tick 中断,这种方法所能实现的节点效果较差。此外,如果 tick 中断的频率过高(从空闲唤醒的频率过高),则除了最轻量省电模式外的所有模式,每次 tick 中断时进出低功耗状态所消耗的能量和时间将超过任何潜在的收益。
FreeRTOS 支持低功耗状态,允许微控制器周期性地进出低功耗状态。FreeRTOS 的无 tick 空闲模式会在空闲期间(没有能够执行的应用任务时)停止周期性的 tick 中断,从而使微控制器保持在深度省电状态,直到发生中断,或者到了 RTOS 内核将任务过渡到就绪状态的时间。然后,当重新启动 tick 中断时,它会修正调整 RTOS tick 计数值。FreeRTOS 无 tick 模式的原理是在 MCU 执行空闲任务时,使 MCU 进入低功耗模式以节省系统功耗。
11.2 FreeRTOS 睡眠模式
FreeRTOS 支持三种睡眠模式:
-
eAbortSleep
该模式下调度程序被挂起,上下文切换和 tick 中断都将被挂起。它向 RTOS 发出中止信号,进入睡眠模式。
-
eStandardSleep
该模式允许在空闲状态保持睡眠模式,直到预设的最大空闲时间。
-
eNoTasksWaitingTimeout
如果没有任务正在等待超时,就可以安全地进入该睡眠模式,只有外部中断或复位才能退出睡眠模式。
11.3 内置的无 tick 空闲模式的功能和生效方式
在 FreeRTOSConfig.h
中将 configUSE_TICKLESS_IDLE
定义为 1,可启用内置的无 tick 空闲功能(适用于支持此功能的 port)。在 FreeRTOSConfig.h
中将 configUSE_TICKLESS_IDLE
定义为 2,可为任何 FreeRTOS port 提供用户定义的无 tick 空闲功能。
启用无 tick 空闲功能后,当满足以下两个条件时,内核将调用 portSUPPRESS_TICKS_AND_SLEEP()
宏:
- 空闲任务是唯一能够运行的任务,因为所有应用程序任务要么处于阻塞状态,要么处于挂起(Suspended)状态。
- 在内核将一个应用任务从阻塞状态转换出来之前,至少还要经过 n 个完整的 tick 时间段,其中 n 由
FreeRTOSConfig.h
中的configEXPECTED_IDLE_TIME_BEFORE_SLEEP
设置。
11.3.1 portSUPPRESS_TICKS_AND_SLEEP()
宏函数
portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime )
portSUPRESS_TICK_AND_SLEEP()
宏
portSUPPRESS_TICKS_AND_SLEEP()
的 xExpectedIdleTime
参数值等于任务将要进入就绪状态的预估 tick 周期总数。因此,该参数值就是微控制器在抑制 tick 中断的情况下,可以安全地保持深度睡眠状态的时间。
11.3.2 vPortSuppressTicksAndSleep()
函数
FreeRTOS 中定义了 vPortSuppressTicksAndSleep()
函数,可用于实现无 tick 模式。该函数在 FreeRTOS Cortex-M port 中弱定义,可由应用程序编写者重写。
void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime );
vPortSuppressTicksAndSleep()
函数名称和原型
11.3.3 eTaskConfirmSleepModeStatus()
API 函数
eTaskConfirmSleepModeStatus
API 函数返回睡眠模式状态,以确定是否可以继续睡眠,以及是否可以无限期睡眠。该功能仅在 configUSE_TICKLESS_IDLE
设置为 1 时可用。
eSleepModeStatus eTaskConfirmSleepModeStatus( void );
eTaskConfirmSleepModeStatus()
API 函数原型
如果在 portSUPPRESS_TICKS_AND_SLEEP()
中调用 eTaskConfirmSleepModeStatus()
时返回 eNoTasksWaitingTimeout
,则微控制器可以无限期地保持深度睡眠状态:
- 未使用软件定时器,因此调度程序不会在未来任何时间执行定时器回调函数。
- 所有应用任务要么处于挂起状态,要么处于超时值为
portMAX_DELAY
的阻塞状态,因此调度程序不会在未来任何时间将任务从阻塞状态过渡出来。
为避免出现竞争条件,portSUPPRESS_TICKS_AND_SLEEP()
被调用前会挂起 FreeRTOS 调度程序,并在 portSUPPRESS_TICKS_AND_SLEEP()
完成后恢复。这样可以确保微控制器退出低功耗状态到 portSUPPRESS_TICKS_AND_SLEEP()
完成执行之间,应用任务不会执行。此外,portSUPPRESS_TICKS_AND_SLEEP()
函数有必要在定时器停止和进入睡眠模式之间创建一小段临界区,以确保可以进入睡眠模式。eTaskConfirmSleepModeStatus()
会在这个临界区内被调用。
此外,FreeRTOS 还允许在 FreeRTOSConfig.h
中定义另外两个接口函数。这些宏允许应用程序编写者分别在 MCU 进入低功耗状态之前和之后添加额外的步骤。
11.3.4 configPRE_SLEEP_PROCESSING()
宏函数
configPRE_SLEEP_PROCESSING( xExpectedIdleTime )
configPRE_SLEEP_PROCESSING()
宏函数
在用户使 MCU 进入低功耗模式之前,configPRE_SLEEP_PROCESSING()
将被调用,可在其中配置系统参数以降低系统功耗,如关闭其他外设时钟、降低系统频率等。
11.3.5 configPOST_SLEEP_PROCESSING()
宏函数
configPOST_SLEEP_PROCESSING( xExpectedIdleTime )
configPOST_SLEEP_PROCESSING()
宏函数
退出低功耗模式后,configPOST_SLEEP_PROCESSING()
将被调用,可用于恢复系统的主频和外设功能。
11.4 实现 portSUPPRESS_TICKS_AND_SLEEP()
宏函数
如果应用的 FreeRTOS port 没有提供 portSUPPRESS_TICKS_AND_SLEEP()
的默认实现, 则应用程序编写者可以在 FreeRTOSConfig.h
中定义自己的实现。如果存在 portSUPPRESS_TICKS_AND_SLEEP()
的默认实现,那么应用程序编写者也可以在 FreeRTOSConfig.h
中定义 portSUPPRESS_TICKS_AND_SLEEP()
,从而覆盖默认实现。
下面的源代码是实现 portSUPPRESS_TICKS_AND_SLEEP()
的示例。该示例是粗糙的实现,会在内核的 tick 时间和实际时间之间引入一些误差。在示例中显示的函数调用中,只有 vTaskStepTick()
和 eTaskConfirmSleepModeStatus()
是 FreeRTOS API 的一部分。其他函数都是针对所使用硬件的时钟和省电模式的,因此必须由应用程序编写者提供。
/* First define the portSUPPRESS_TICKS_AND_SLEEP() macro. The parameter is the
time, in ticks, until the kernel next needs to execute. */
#define portSUPPRESS_TICKS_AND_SLEEP( xIdleTime ) vApplicationSleep( xIdleTime )
/* Define the function that is called by portSUPPRESS_TICKS_AND_SLEEP(). */
void vApplicationSleep( TickType_t xExpectedIdleTime )
{
unsigned long ulLowPowerTimeBeforeSleep, ulLowPowerTimeAfterSleep;
eSleepModeStatus eSleepStatus;
/* Read the current time from a time source that will remain operational
while the microcontroller is in a low power state. */
ulLowPowerTimeBeforeSleep = ulGetExternalTime();
/* Stop the timer that is generating the tick interrupt. */
prvStopTickInterruptTimer();
/* Enter a critical section that will not effect interrupts bringing the MCU
out of sleep mode. */
disable_interrupts();
/* Ensure it is still ok to enter the sleep mode. */
eSleepStatus = eTaskConfirmSleepModeStatus();
if( eSleepStatus == eAbortSleep )
{
/* A task has been moved out of the Blocked state since this macro was
executed, or a context siwth is being held pending. Do not enter a
sleep state. Restart the tick and exit the critical section. */
prvStartTickInterruptTimer();
enable_interrupts();
}
else
{
if( eSleepStatus == eNoTasksWaitingTimeout )
{
/* It is not necessary to configure an interrupt to bring the
microcontroller out of its low power state at a fixed time in
the future. */
prvSleep();
}
else
{
/* Configure an interrupt to bring the microcontroller out of its low
power state at the time the kernel next needs to execute. The
interrupt must be generated from a source that remains operational
when the microcontroller is in a low power state. */
vSetWakeTimeInterrupt( xExpectedIdleTime );
/* Enter the low power state. */
prvSleep();
/* Determine how long the microcontroller was actually in a low power
state for, which will be less than xExpectedIdleTime if the
microcontroller was brought out of low power mode by an interrupt
other than that configured by the vSetWakeTimeInterrupt() call.
Note that the scheduler is suspended before
portSUPPRESS_TICKS_AND_SLEEP() is called, and resumed when
portSUPPRESS_TICKS_AND_SLEEP() returns. Therefore no other tasks will
execute until this function completes. */
ulLowPowerTimeAfterSleep = ulGetExternalTime();
/* Correct the kernels tick count to account for the time the
microcontroller spent in its low power state. */
vTaskStepTick( ulLowPowerTimeAfterSleep - ulLowPowerTimeBeforeSleep );
}
/* Exit the critical section - it might be possible to do this immediately
after the prvSleep() calls. */
enable_interrupts();
/* Restart the timer that is generating the tick interrupt. */
prvStartTickInterruptTimer();
}
}
portSUPPRESS_TICKS_AND_SLEEP()
宏函数的示例实现
11.5 空闲 hook 函数
可设置由空闲任务调用应用程序定义的 hook(或回调)函数。空闲任务以最低优先级运行,因此只有在没有更高优先级的任务可以运行时,空闲 hook 函数才会被执行。这使得空闲 hook 函数成为将处理器置于低功耗状态的理想选择,不需要处理任务时自动省电。只有在 FreeRTOSConfig.h
中将 configUSE_IDLE_HOOK
设置为 1 时,空闲 hook 函数才会被调用。
void vApplicationIdleHook( void );
vApplicationIdleHook
函数的名称和原型
只要空闲任务在运行,空闲 hook 函数就会被反复调用。重要的是,空闲 hook 函数不能调用任何可能导致阻塞的 API 函数。此外,如果应用程序使用了 vTaskDelete()
API 函数,则空闲 hook 函数必须及时返回,因为空闲任务需要负责清理 RTOS 内核分配给已删除任务的资源。
十二、开发者支持
12.1 介绍
本章重点介绍一系列功能,用以最大限度地提高工作效率:
- 深入了解应用程序的运行情况。
- 优化的可能性。
- 在错误发生时就地捕获。
12.2 configASSERT()
宏函数
在 C 语言中,宏 assert()
用于验证程序中的断言。断言是 C 语言表达式,如果表达式求值为 false
或 0,则断言失败。例如,清单 12.1 断言指针 pxMyPointer
不是 NULL
。
/* Test the assertion that pxMyPointer is not NULL */
assert( pxMyPointer != NULL );
assert()
使用方式
应用程序编写者通过提供 assert()
宏的实现来指定断言失败时应采取的措施。
FreeRTOS 源代码不调用 assert()
,因为所有编译 FreeRTOS 的编译器都无法使用标准 assert()
。相反,FreeRTOS 源代码中包含大量对 configASSERT()
宏的调用,应用程序编写者可在 FreeRTOSConfig.h
中定义该宏,其行为与标准 C assert()
完全相同。
断言失败必须被视为致命错误,不要尝试往下执行。
使用
configASSERT()
可以立即捕获并识别许多最常见的错误,从而提高工作效率。强烈建议在开发或调试 FreeRTOS 应用程序时定义configASSERT()
。
定义 configASSERT()
将大大有助于运行时调试,但也会增加应用程序的代码量,减慢其执行速度。如果没有定义 configASSERT()
,则将使用默认的空定义,C 预处理器将完全删除对 configASSERT()
的所有调用。
12.2.1 定义 configASSERT()
的示例
当应用程序在调试器的控制下执行时,清单 12.2 中的 configASSERT()
定义非常有用。它会在断言失败的地方停止执行,因此只需暂停调试执行,就能显示断言失败的位置。
/* Disable interrupts so the tick interrupt stops executing, then sit
in a loop so execution does not move past the line that failed the
assertion. If the hardware supports a debug break instruction, then the
debug break instruction can be used in place of the for() loop. */
#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for(;;); }
configASSERT()
的示例定义
当应用程序没有连接调试器时,可以采用清单 12.3 中定义的 configASSERT()
。它会记录或打印显示断言失败的源代码行。使用标准 C 语言 _FILE_ 宏获取源文件的名称, _LINE_ 宏获取源文件的行号,从而确定断言失败的行。
/* This function must be defined in a C source file, not the FreeRTOSConfig.h
header file. */
void vAssertCalled( const char *pcFile, uint32_t ulLine )
{
/* Inside this function, pcFile holds the name of the source file that
contains the line that detected the error, and ulLine holds the line
number in the source file. The pcFile and ulLine values can be printed
out, or otherwise recorded, before the following infinite loop is
entered. */
RecordErrorInformationHere( pcFile, ulLine );
/* Disable interrupts so the tick interrupt stops executing, then sit in a
loop so execution does not move past the line that failed the assertion. */
taskDISABLE_INTERRUPTS();
for( ;; );
}
/*-----------------------------------------------------------*/
/* These following two lines must be placed in FreeRTOSConfig.h. */
extern void vAssertCalled( const char *pcFile, unsigned long ulLine );
#define configASSERT( x ) if( ( x ) == 0 ) vAssertCalled( __FILE__, __LINE__ )
configASSERT()
示例定义
12.3 适用 FreeRTOS 的 Tracealyzer
注:这节不翻译,可以自己找 Tracealyzer 的介绍。这是个收费的调试工具,估计没多少人用。
12.4 用于 debug 的 hook 函数
12.4.1 malloc
失败 hook 函数
第 3 章 “堆内存管理 ” 中介绍了 malloc
失败 hook(或回调)。
定义 malloc
失败 hook 可在堆内存分配失败时执行缓解措施,或通知开发人员。分配失败的原因可能是创建任务、队列、信号量或事件组。
12.4.2 栈溢出 hook 函数
有关栈溢出 hook 的详细信息,请参见第 13.3 节 “栈溢出”。
定义栈溢出 hook 可确保在任务发生栈溢出时通知开发人员。
12.5 查看任务运行时间和状态信息
12.5.1 任务运行时间统计
任务的运行时间是指应用程序启动后任务处于运行状态的总时间。
运行时间统计的目的是在项目开发阶段作为剖析和调试辅助工具使用,它们提供的信息仅在用作时间的计数器溢出前有效。收集运行时统计信息会增加任务上下文切换时间。
要获取原始统计信息,请调用 uxTaskGetSystemState()
API 函数。要以人类可读的 ASCII 表格形式获取运行时间统计信息,请调用 vTaskGetRunTimeStatistics()
辅助函数。
12.5.2 用于统计运行时间的时钟
运行时间统计需要测量小于 tick 周期的时间,因此,RTOS 的 tick 计数不用作运行时统计时钟。建议让运行时间统计时钟的频率比 tick 中断快 10 到 100 倍。运行时间统计时钟越快,统计数据就越准确,但时间值溢出也就越快。
理想情况下,时间值由一个独立运行的 32 位外设定时器/计数器生成,读取其值时无需其他处理开销。如果可用的外设和时钟速度不允许采用这种技术,那么可以采用其他效率较低的技术,包括
-
配置一个外设,使其在所需的频率下产生周期性中断,然后将中断计数作为运行时间统计时钟。
如果周期性中断仅用于提供运行时统计时钟,那么这种方法的效率非常低。但是,如果应用程序已经使用了频率合适的周期性中断,那么在现有的中断服务例程中加入对所产生的中断次数的计数就会变得简单而高效。
-
将自由运行的 16 位外设定时器的当前值作为 32 位时间值的低 16 位,将定时器溢出的次数作为 32 位值的高 16 位,从而生成一个 32 位值。
通过适当的操作,可以将 RTOS tick 计数与 ARM Cortex-M SysTick 定时器的当前值相结合,生成运行时统计时钟。FreeRTOS 分发中的一些演示项目演示了如何实现这一点。
12.5.3 统计运行时间所需的应用配置
下面详细介绍了收集任务运行时间统计数据所需的配置。最初计划将这些宏包含在 RTOS 的 port 支持文件中,所以宏的前缀是 “port”,但事实证明在 FreeRTOSConfig.h
中定义这些宏更为实用。
用于配置运行时间统计的宏:
-
configGENERATE_RUN_TIME_STATS
此宏必须在
FreeRTOSConfig.h
中设置为 1。当此宏设置为 1 时,调度程序将在适当的时间调用本节中详细介绍的其他宏。 -
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
必须提供此宏来初始化独立时钟。
-
portGET_RUN_TIME_COUNTER_VALUE()
,或portALT_GET_RUN_TIME_COUNTER_VALUE(Time)
必须提供这两个宏中的一个,用于返回当前的运行时间统计时钟值。这是应用程序自首次启动以来的总运行时间,以运行时间统计时钟的单位表示。
第一个宏必须返回当前时钟值。第二个宏则必须将其
Time
参数代表的变量(不是指针)设置为当前时钟值,变量在调用该宏时在外部创建。
12.5.4 uxTaskGetSystemState()
API 函数
uxTaskGetSystemState()
生成 FreeRTOS 调度器控制的每个任务的状态信息快照。这些信息以 TaskStatus_t
结构数组的形式提供,每个任务对应数组中的一个索引。TaskStatus_t
在清单 12.5 和下文中有描述。
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
configRUN_TIME_COUNTER_TYPE * const pulTotalRunTime );
uxTaskGetSystemState()
API 函数原型
注意:为了向后兼容,
configRUN_TIME_COUNTER_TYPE
默认为uint32_t
,但如果uint32_t
范围不足,可在FreeRTOSConfig.h
中修改。
uxTaskGetSystemState()
参数和返回值:
-
pxTaskStatusArray
指向
TaskStatus_t
结构数组的指针。数组所需的内存由调用者分配。每个任务都会在数组中产生一个
TaskStatus_t
结构。可使用uxTaskGetNumberOfTasks()
API 函数确定任务数。TaskStatus_t
结构如清单 12.5 所示,结构成员将在下一列表中介绍。 -
uxArraySize
pxTaskStatusArray
参数指向的数组的大小。该大小为数组中结构个数,而不是数组的总字节数。 -
pulTotalRunTime
如果在
FreeRTOSConfig.h
中将configGENERATE_RUN_TIME_STATS
设置为 1,则uxTaskGetSystemState()
会将*pulTotalRunTime
设置为自系统启动以来的总运行时间(由应用程序提供的运行时间统计时钟定义)。pulTotalRunTime
是可选项,如果不需要总运行时间,则可设置为NULL
。 -
返回值
返回由
uxTaskGetSystemState()
构造的TaskStatus_t
结构的数量。返回值应等于
uxTaskGetNumberOfTasks()
API 函数返回的数量,但如果uxArraySize
参数传递的数值太小,返回值将为零。
typedef struct xTASK_STATUS
{
TaskHandle_t xHandle;
const char *pcTaskName;
UBaseType_t xTaskNumber;
eTaskState eCurrentState;
UBaseType_t uxCurrentPriority;
UBaseType_t uxBasePriority;
configRUN_TIME_COUNTER_TYPE ulRunTimeCounter;
StackType_t * pxStackBase;
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t * pxTopOfStack;
StackType_t * pxEndOfStack;
#endif
uint16_t usStackHighWaterMark;
#if ( ( configUSE_CORE_AFFINITY == 1 ) && ( configNUMBER_OF_CORES > 1 ) )
UBaseType_t uxCoreAffinityMask;
#endif
} TaskStatus_t;
TaskStatus_t
结构体的定义
TaskStatus_t
结构体成员:
-
xHandle
结构体中的信息所涉及的任务句柄。
-
pcTaskName
任务的名称字符串。
-
xTaskNumber
每个任务都有一个唯一的
xTaskNumber
值。提供
xTaskNumber
是为了让应用程序代码和内核调试器区分仍然有效的任务和与有效任务具有相同句柄的已删除任务。 -
eCurrentState
eCurrentState
可以是以下值之一:eRunning
eReady
eBlocked
eSuspended
eDeleted
在调用
vTaskDelete()
删除任务后,到空闲任务释放已删除任务的内存为止,任务才会被报告为处于eDeleted
状态。在这段时间后,任务将不再以任何方式存在,尝试使用其句柄也是无效的。 -
uxCurrentPriority
调用
uxTaskGetSystemState()
时任务运行的优先级。只有在任务因优先级继承机制被临时分配了更高优先级的情况下,uxCurrentPriority
才会高于应用程序编写者分配给任务的优先级。 -
uxBasePriority
应用程序编写者分配给任务的优先级。只有在
FreeRTOSConfig.h
中将configUSE_MUTEXES
设置为 1 时,uxBasePriority
才有效。 -
ulRunTimeCounter
任务创建后总运行时间。
ulRunTimeCounter
仅在FreeRTOSConfig.h
中将configGENERATE_RUN_TIME_STATS
设为 1 时有效。 -
pxStackBase
指向分配给此任务的栈内存的基地址。
-
pxTopOfStack
指向分配给此任务的栈内存的当前Top地址。只有当栈向上增长(即
portSTACK_GROWTH
大于 0)或在FreeRTOSConfig.h
中将configRECORD_STACK_HIGH_ADDRESS
设为 1 时,字段pxTopOfStack
才有效。 -
pxEndOfStack
指向分配给此任务的栈内存的结束地址。只有当栈向上增长(即
portSTACK_GROWTH
大于零)或在FreeRTOSConfig.h
中将configRECORD_STACK_HIGH_ADDRESS
设为 1 时,字段pxEndOfStack
才有效。 -
usStackHighWaterMark
这是自创建任务以来,该任务所剩栈空间的最小值。
usStackHighWaterMark
以字节为单位指定。 -
uxCoreAffinityMask
二进制 bit 值,表示任务亲和的 CPU 内核。内核编号从 0 到
configNUMBER_OF_CORES - 1
。 例如,一个任务可以在内核 0 和内核 1 上运行,则其uxCoreAffinityMask
设置为 0x03。只有在FreeRTOSConfig.h
中将configUSE_CORE_AFFINITY
设置为 1 和configNUMBER_OF_CORES
设置为大于 1 时,字段uxCoreAffinityMask
才可用。
12.5.5 vTaskListTasks()
辅助函数
vTaskListTasks()
提供的任务状态信息与 uxTaskGetSystemState()
类似,但它以人类可读的 ASCII 表格形式显示,而不是二进制数组。
vTaskListTasks()
是一个非常耗费处理器时间的函数,会让调度程序长时间挂起。因此,建议仅将该函数用于调试目的,而不要在生产中使用。
如果在 FreeRTOSConfig.h
中将 configUSE_TRACE_FACILITY
设为 1 且将 configUSE_STATS_FORMATTING_FUNCTIONS
设为大于 0,则 vTaskListTasks()
可用。
void vTaskListTasks( char * pcWriteBuffer, size_t uxBufferLength );
vTaskListTasks()
API 函数的原型
vTaskListTasks()
参数:
-
pcWriteBuffer
指向字符缓冲区的指针,格式化后的可读表格将写入该缓冲区。假定该缓冲区足够大,以包含生成的报告。
每个任务大约 40 字节就足够了。
-
uxBufferLength
缓冲区的长度,单位是字节。
vTaskListTasks()
生成的输出示例如图 12.7 所示。输出格式为:
- 每行代表一个任务的信息。
- 第一列是任务名称。
- 第二列是任务的状态,其中 “X ”表示正在运行,“R ”表示已就绪,“B ”表示已阻塞,“S ”表示已挂起,“D ”表示任务已被删除。在调用
vTaskDelete()
删除任务后,到空闲任务释放已删除任务的内存为止,任务才会被报告为处于已删除状态。在此之后,该任务将不再以任何方式存在,尝试使用其句柄也是无效的。 - 第三列是任务的优先级。
- 第四列是任务的栈高水位线。请参阅
usStackHighWaterMark
的描述。 - 第五列为分配给任务的唯一编号。请参阅
xTaskNumber
的描述。
tcpip R 3 393 0
Tmr Svc R 3 111 48
QConsB1 R 1 143 3
QProdB5 R 0 144 7
QConsB6 R 0 143 8
PolSEM1 R 0 145 11
PolSEM2 R 0 145 12
GenQ R 0 155 17
MuLOw R 0 147 18
Rec3 R 0 141 30
SUSP_RX R 0 148 36
Math1 R 0 167 38
Math2 R 0 167 39
xTaskListTasks()
输出示例
注意:
vTaskListTasks()
的旧版本是vTaskList()
,它将pcWriteBuffer
的长度固定为configSTATS_BUFFER_MAX_LENGTH
。该函数仅用于向后兼容,建议新应用程序使用vTaskListTasks
并明确提供pcWriteBuffer
的长度。
void vTaskList( signed char *pcWriteBuffer );
vTaskList()
API 函数的原型
vTaskList()
参数:
-
pcWriteBuffer
指向字符缓冲区的指针,格式化后的表格字符串将写入该缓冲区。缓冲区必须足够大,以容纳整个表格,因为函数内部不会执行边界检查。
12.5.6 vTaskGetRunTimeStatistics()
辅助函数
vTaskGetRunTimeStatistics()
会将收集到的运行时间统计数据格式化为人类可读的 ASCII 表格。
vTaskGetRunTimeStatistics()
是一个非常耗费处理器的函数,会让调度程序长时间挂起。因此,建议仅将该函数用于调试目的,而不要在生产中使用。
在 FreeRTOSConfig.h
中,将 configGENERATE_RUN_TIME_STATS
设置为 1、configUSE_STATS_FORMATTING_FUNCTIONS
设置为大于 0,configUSE_TRACE_FACILITY
设置为 1 ,此时 vTaskGetRunTimeStatistics()
可用。
void vTaskGetRunTimeStatistics( char * pcWriteBuffer, size_t uxBufferLength );
vTaskGetRunTimeStatistics()
API 函数的原型
vTaskGetRunTimeStatistics()
参数:
-
pcWriteBuffer
指向字符缓冲区的指针,格式化后的可读表格将写入该缓冲区。假定该缓冲区足够大,以包含生成的报告。
每个任务大约 40 字节就足够了。
-
uxBufferLength
缓冲区的长度,单位是字节。
vTaskGetRunTimeStatistics()
生成的输出示例如图 12.8 所示。在输出中:
- 每行代表一个任务的信息。
- 第一列是任务名称。
- 第二列是任务在运行状态下花费的总时间。请参阅
ulRunTimeCounter
的说明。 - 第三列是任务运行的时间占系统启动后总时间的百分比。显示的时间百分比总和通常会小于预期的 100%,因为统计信息的收集和计算使用的是四舍五入到最接近整数值的整数计算方法。
PolSEM1 994 <1%
PolSEM2 23248 1%
GenQ 194479 16%
MuLOw 3690 <1%
Rec3 229450 18%
CNT1 242720 19%
PeekL 94 <1%
CNT_INC 165 <1%
CNT2 243166 20%
SUSP_RX 243192 20%
IDLE 55 <1%
xTaskGetRunTimeStatistics()
输出示例
注意:
vTaskGetRunTimeStats()
是xTaskGetRunTimeStatistics()
的旧版本,该函数假设pcWriteBuffer
的长度固定为configSTATS_BUFFER_MAX_LENGTH
。该函数仅用于向后兼容。建议新应用程序使用vTaskGetRunTimeStatistics
并明确提供 pcWriteBuffer 的长度。
void vTaskGetRunTimeStats( signed char *pcWriteBuffer );
xTaskGetRunTimeStats()
输出示例
vTaskGetRunTimeStats()
参数:
-
pcWriteBuffer
指向字符缓冲区的指针,格式化后的表格字符串将写入该缓冲区。缓冲区必须足够大,以容纳整个表格,因为函数内部不会执行边界检查。
12.5.7 生成并展示运行时间统计信息的可用示例
本例假设存在 16 位定时器,用它来生成一个 32 位运行时统计时钟。计数器每 次累计到 16 位最大值时产生一个中断,即溢出中断。中断服务例程计算溢出发生的次数。
将溢出计数作为 32 位时间值的高 16 位,16 位定时器的当前值是时间值的低 16 位,从而合成 32 位值。中断服务例程的伪代码如清单 12.10 所示。
void TimerOverflowInterruptHandler( void )
{
/* Just count the number of interrupts. */
ulOverflowCount++;
/* Clear the interrupt. */
ClearTimerInterrupt();
}
清单 12.11 显示了启用运行时间统计所需的 FreeRTOSConfig.h
配置项。
/* Set configGENERATE_RUN_TIME_STATS to 1 to enable collection of run-time
statistics. When this is done, both portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
and portGET_RUN_TIME_COUNTER_VALUE() or
portALT_GET_RUN_TIME_COUNTER_VALUE(x) must also be defined. */
#define configGENERATE_RUN_TIME_STATS 1
/* portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() is defined to call the function
that sets up the hypothetical 16-bit timer (the function's implementation
is not shown). */
void vSetupTimerForRunTimeStats( void );
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() vSetupTimerForRunTimeStats()
/* portALT_GET_RUN_TIME_COUNTER_VALUE() is defined to set its parameter to the
current run-time counter/time value. The returned time value is 32-bits
long, and is formed by shifting the count of 16-bit timer overflows into
the top two bytes of a 32-bit number, then bitwise ORing the result with
the current 16-bit counter value. */
#define portALT_GET_RUN_TIME_COUNTER_VALUE( ulCountValue ) \
{ \
extern volatile unsigned long ulOverflowCount; \
\
/* Disconnect the clock from the counter so it does not change \
while its value is being used. */ \
PauseTimer(); \
\
/* The number of overflows is shifted into the most significant \
two bytes of the returned 32-bit value. */ \
ulCountValue = ( ulOverflowCount << 16UL ); \
\
/* The current counter value is used as the two least significant \
bytes of the returned 32-bit value. */ \
ulCountValue |= ( unsigned long ) ReadTimerCount(); \
\
/* Reconnect the clock to the counter. */ \
ResumeTimer(); \
}
清单 12.12 显示了每隔 5 秒打印输出统计信息的任务。
#define RUN_TIME_STATS_STRING_BUFFER_LENGTH 512
/* For clarity, calls to fflush() have been omitted from this code listing. */
static void prvStatsTask( void *pvParameters )
{
TickType_t xLastExecutionTime;
/* The buffer used to hold the formatted run-time statistics text needs to
be quite large. It is therefore declared static to ensure it is not
allocated on the task stack. This makes this function non re-entrant. */
static signed char cStringBuffer[ RUN_TIME_STATS_STRING_BUFFER_LENGTH ];
/* The task will run every 5 seconds. */
const TickType_t xBlockPeriod = pdMS_TO_TICKS( 5000 );
/* Initialize xLastExecutionTime to the current time. This is the only
time this variable needs to be written to explicitly. Afterwards it is
updated internally within the vTaskDelayUntil() API function. */
xLastExecutionTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Wait until it is time to run this task again. */
xTaskDelayUntil( &xLastExecutionTime, xBlockPeriod );
/* Generate a text table from the run-time stats. This must fit into
the cStringBuffer array. */
vTaskGetRunTimeStatistics( cStringBuffer, RUN_TIME_STATS_STRING_BUFFER_LENGTH );
/* Print out column headings for the run-time stats table. */
printf( "\nTask\t\tAbs\t\t\t%%\n" );
printf( "-------------------------------------------------------------\n" );
/* Print out the run-time stats themselves. The table of data contains
multiple lines, so the vPrintMultipleLines() function is called
instead of calling printf() directly. vPrintMultipleLines() simply
calls printf() on each line individually, to ensure the line
buffering works as expected. */
vPrintMultipleLines( cStringBuffer );
}
}
12.6 用于跟踪内核代码的 hook 宏
跟踪宏是放置在 FreeRTOS 源代码关键点上的宏。默认情况下,这些宏是空的,因此不会生成任何代码,也没有运行时开销。应用程序编写者可以覆盖默认的空实现,从而:
- 在 FreeRTOS 中插入代码,而无需修改 FreeRTOS 源文件。
- 在目标硬件上以任何可用的方式输出详细的执行顺序信息。跟踪宏在 FreeRTOS 源代码中大量使用,允许使用它们来创建完整而详细的调度器活动跟踪和剖析日志。
12.6.1 可用的跟踪 hook 宏
如果在此详述每个宏,将耗费太多篇幅。下面的列表详细列出了对应用程序编写者最有用一部分宏。
下表中的很多描述引用了pxCurrentTCB
变量,这是一个 FreeRTOS 私有(static
)变量,用于保存处于运行状态的任务句柄,任何从 FreeRTOS/Source/tasks.c 源文件中调用的宏都可以使用它。
最常用的一部分跟踪 hook 宏:
-
traceTASK_INCREMENT_TICK(xTickCount)
在 tick 中断期间,tick 计数递增之前调用。参数
xTickCount
将新的 tick 计数值传入宏。 -
traceTASK_SWITCHED_OUT()
在选择运行新任务前调用。此时,
pxCurrentTCB
是即将离开运行状态的任务句柄。 -
traceTASK_SWITCHED_IN()
在任务被选中运行后调用。此时,
pxCurrentTCB
是即将进入运行状态的任务句柄。 -
traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue)
在尝试从空队列读取数据,或尝试 “获取 ”空信号量或互斥量后,当前执行任务进入阻塞状态前立即调用。参数
pxQueue
将目标队列或信号量的句柄传入宏。 -
traceBLOCKING_ON_QUEUE_SEND(pxQueue)
在当前执行的任务尝试向已满的队列写入后进入阻塞状态之前立即调用。参数
pxQueue
将目标队列的句柄传入宏。 -
traceQUEUE_SEND(pxQueue)
向队列发送数据、或信号量 “Give”成功时,在
xQueueSend()
、xQueueSendToFront()
、xQueueSendToBack()
或任何信号量 "Give”函数中调用。参数pxQueue
将目标队列或信号量的句柄传入宏。 -
traceQUEUE_SEND_FAILED(pxQueue)
当队列发送或信号量 “Give”操作失败时,从
xQueueSend()
、xQueueSendToFront()
、xQueueSendToBack()
或任何信号量 “Give”函数内部调用。如果队列已满,并且在指定的阻塞时间内队列一直满,队列发送或信号量 “Give”操作就会失败。pxQueue
参数将目标队列或信号量的句柄传入宏。 -
traceQUEUE_RECEIVE(pxQueue)
当队列接收或信号量 “Take” 成功时,在
xQueueReceive()
或任何一个信号量 “Take” 函数中调用。参数pxQueue
将目标队列或信号量的句柄传入宏。 -
traceQUEUE_RECEIVE_FAILED(pxQueue)
当队列接收或信号量 “Take” 操作失败时,在
xQueueReceive()
或任何一个信号量 “Take”函数中调用。如果队列或信号量为空,并且在指定的阻塞时间内一直为空,队列接收或信号量 “Take”操作就会失败。参数pxQueue
将目标队列或信号量的句柄传入宏。 -
traceQUEUE_SEND_FROM_ISR(pxQueue)
从 ISR 向队列发送成功时,在
xQueueSendFromISR()
中调用。参数pxQueue
将目标队列的句柄传入宏。 -
traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue)
从 ISR 向队列发送失败时,从
xQueueSendFromISR()
中调用。如果队列已满,发送操作就会失败。参数pxQueue
将目标队列的句柄传入宏。 -
traceQUEUE_RECEIVE_FROM_ISR(pxQueue)
从 ISR 读取队列操作成功时,在
xQueueReceiveFromISR()
中调用。参数pxQueue
将目标队列的句柄传入宏。 -
traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue)
从 ISR 读取队列失败时,在
xQueueReceiveFromISR()
中调用。如果队列为空,读取就会失败。参数pxQueue
将目标队列的句柄传入宏。 -
traceTASK_DELAY_UNTIL( xTimeToWake )
调用
xTaskDelayUntil()
后,在任务阻塞之前,从xTaskDelayUntil()
中调用。 -
traceTASK_DELAY()
调用
xTaskDelay()
后,在任务阻塞之前,从xTaskDelay()
中调用。
12.6.2 定义跟踪 hook 宏
每个跟踪宏都有一个默认的空定义。如果自定义的跟踪宏内容冗长或复杂,可以在一个单独头文件中实现,将该头文件包含在 FreeRTOSConfig.h
中。
根据软件工程最佳实践,FreeRTOS 保持严格的数据隐藏策略。跟踪宏允许将用户代码添加到 FreeRTOS 源文件中,因此应用程序代码不可见的数据可以被跟踪宏访问:
- 在 FreeRTOS/Source/tasks.c 源文件中,任务句柄是指向描述任务的数据结构(任务的任务控制块或 TCB)的指针。在 FreeRTOS/Source/tasks.c 源文件之外,任务句柄是指向 void 的指针。
- 在 FreeRTOS/Source/queue.c 源文件中,队列句柄是指向描述队列的数据结构的指针。在 FreeRTOS/Source/queue.c 源文件之外,队列句柄是指向 void 的指针。
如果跟踪宏直接访问私有的 FreeRTOS 数据结构,则需要格外小心,因为私有数据结构可能会在不同的 FreeRTOS 版本之间发生变化。
12.6.3 支持 FreeRTOS 内核调试的调试器插件
下列集成开发环境可使用支持 FreeRTOS 内核调试的调试器插件。此列表可能并不详尽:
- Eclipse (StateViewer)
- Eclipse (ThreadSpy)
- IAR
- ARM DS-5
- Atollic TrueStudio
- Microchip MPLAB
- iSYSTEM WinIDEA
- STM32CubeIDE
十三、故障排除
13.1 章节介绍和范围
本章重点介绍了 FreeRTOS 新手最常遇到的问题。首先,本章重点讨论了多年来支持请求中最常见的三个问题:
-
中断优先级分配错误;
-
栈溢出;
-
printf()
使用不当;
除此之外,也简要介绍了其他常见错误可能的原因和解决方案。
使用
configASSERT()
可以立即捕获并识别许多最常见的错误源,从而提高工作效率。强烈建议在开发或调试 FreeRTOS 应用程序时定义configASSERT()
。
13.2 中断优先级
这个问题是引起支持请求的头号原因,大部分 port 下,只需定义
configASSERT()
就能立即捕获错误。
如果使用中的 FreeRTOS port 支持中断嵌套,并且在 ISR 中使用了 FreeRTOS API,则必须将中断的优先级必须低于 configMAX_SYSCALL_INTERRUPT_PRIORITY
或 configMAX_SYSCALL_INTERRUPT_PRIORITY
,如第 7.8 节 “中断嵌套”所述。否则将导致临界区失效,进而导致偶发性故障。
如果目标平台属于以下的所述的情况,则要格外注意中断优先级的问题。
13.2.1 中断优先级默认为最高
在某些 ARM Cortex 处理器(可能还有其他处理器)上,中断优先级默认为最高优先级。在此类处理器上,必须显式设置使用了 FreeRTOS API 的中断的优先级。
13.2.2 优先级数值与实际等级相反
数字上的高优先级代表逻辑上的低优先级,这可能比较反直觉,会引起混淆。ARM Cortex 处理器就是这种情况,其他处理器也可能如此。
例如,在这种处理器上,优先级为 5 的中断可能会被优先级为 4 的中断打断。 因此,如果 configMAX_SYSCALL_INTERRUPT_PRIORITY
设置为 5,则任何使用 FreeRTOS API 的中断只能被分配一个数字上大于等于 5 的优先级。 在这种情况下,中断优先级为 5 或 6 是有效的,但中断优先级为 3 肯定是无效的。
在较新的 FreeRTOS port 中,配置常量 configMAX_SYSCALL_INTERRUPT_PRIORITY
更名 为configMAX_API_CALL_INTERRUPT_PRIORITY
。
13.2.3 不同代码库使用不同的优先级定义方式
不同的代码库实现可能希望以不同的方式指定中断的优先级。同样,这与以 ARM Cortex 处理器为目标的程序库尤其相关,在这些程序库中,中断优先级在写入硬件寄存器之前要按位左移。有些库会自行执行位移,而其他库则希望在将优先级传入库函数之前执行位移。
13.2.4 可用的中断优先级数量不同
同一架构的不同硬件实现会有不同数量的中断优先级位。例如,一家制造商的 Cortex-M 处理器可能实现 3 个优先位,而另一家制造商的 Cortex-M 处理器可能实现 4 个优先位。
13.2.5 中断优先级分组
Cortex-M 架构下,中断优先级可分为抢占优先级和响应优先级(sub priority)。确保所有优先级位都用于抢占优先级,避免使用响应优先级。
13.3 栈溢出
栈溢出是引起支持请求的第二大原因。FreeRTOS 提供了一些工具,可用于发现和处理栈溢出相关的问题。
13.3.1 uxTaskGetStackHighWaterMark()
API 函数
uxTaskGetStackHighWaterMark()
用于查询任务接近栈空间用尽的程度。该值被称为堆栈 “高水位线”。
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
uxTaskGetStatckHighWaterMark()
API 函数
uxTaskGetStackHighWaterMark()
参数和返回值:
-
xTask
目标任务的句柄。有关获取任务句柄的信息,请参阅
xTaskCreate()
API 函数的pxCreatedTask
参数。任务可以传入
NULL
作为实参,从而查询自己的信息。 -
返回值
uxTaskGetStackHighWaterMark()
返回任务开始执行后可用的最小剩余栈空间。这是当栈使用率达到最大值(或最深值)时剩余的栈空间。返回值越接近零,任务越接近栈溢出。
可以使用 uxTaskGetStackHighWaterMark2()
API 代替 uxTaskGetStackHighWaterMark()
,两者的区别仅在于返回类型不同。使用 configSTACK_DEPTH_TYPE
可以让应用程序编写者控制栈深度所使用的类型。
configSTACK_DEPTH_TYPE uxTaskGetStackHighWaterMark2( TaskHandle_t xTask );
uxTaskGetStatckHighWaterMark2()
API 函数
13.3.2 运行时栈溢出检查
FreeRTOS 包含三种可选的运行时栈检查机制。这些方法都会增加执行上下文切换所需的时间。
栈溢出 hook 函数(或栈溢出回调)是内核在检测到栈溢出时调用的函数。要使用栈溢出 hook 函数,需要:
- 在
FreeRTOSConfig.h
中将configCHECK_FOR_STACK_OVERFLOW
设置为 1、2 或 3,如后续小节所述。 - 使用清单 13.3 中所示的函数名称和原型提供 hook 函数的实现。
void vApplicationStackOverflowHook( TaskHandle_t *pxTask, signed char *pcTaskName );
提供栈溢出 hook 是为了更容易地捕获和调试栈错误,但当栈溢出发生时,不存在有效的方法使应用从错误状态恢复。该函数的参数将栈溢出的任务句柄和名称传递给 hook 函数。
栈溢出 hook 是从中断的上下文中调用的。
有些微控制器在检测到不正确的内存访问时会产生故障异常,因此有可能在内核有机会调用栈溢出 hook 函数之前就触发了中断。
13.3.3 运行时栈溢出检查方法 1
当 configCHECK_FOR_STACK_OVERFLOW
设置为 1 时,将选择方法 1。
每次切换任务时,任务的整个执行上下文都会保存到栈中,这很可能是栈使用量达到峰值的时候。当 configCHECK_FOR_STACK_OVERFLOW
设置为 1 时,内核会检查栈指针是否仍在有效栈空间内。如果发现栈指针超出有效范围,就会调用栈溢出 hook。
方法 1 执行速度很快,但可能会错过上下文切换之间发生的栈溢出。
注:任务可能在中途使用了大量栈空间,但到切换时,溢出的栈空间已经释放,栈指针回退了。方法 1 检测不到这种情况,某个任务在溢出时可能修改了其他任务的数据,然后系统继续带病运行
13.3.4 运行时栈溢出检查方法 2
方法 2 在方法 1 的基础上执行额外的检查。当 configCHECK_FOR_STACK_OVERFLOW
设置为 2 时,将选择该方法。
创建任务时,任务栈空间的最后 20 个字节会被填充为已知的数据。如果这 20 个字节中的任何一个与预期值不同,则会调用栈溢出 hook 函数。
方法 2 的执行速度不如方法 1 快,但仍然相对较快,因为只测试 20 个字节。这种方法大概率能检测到所有栈溢出的情况。
注:如果任务在栈上创建了一个大数组,然后只修改了其中一部分数据,就有可能跳过这 20 字节,直接修改到栈空间外面的数据。这种情况方法 2 无能为力,方法 1 则有可能检测到
13.3.4 运行时栈溢出检查方法 3
当 configCHECK_FOR_STACK_OVERFLOW
设置为 3 时,将选择方法 3。
此方法仅适用于选定的 port。该方法可用时,将启用 ISR 栈检查。检测到 ISR 栈溢出时,将触发断言。请注意,在这种情况下不会调用堆栈溢出钩子函数,因为它是针对任务栈而不是 ISR 栈的。
13.4 使用 printf()
和 sprintf()
通过 printf()
输出日志是常见的错误源,应用程序开发人员通常会在没有意识到这一点的情况下,进一步调用 printf()
来帮助调试,结果使问题更加严重。
许多交叉编译器供应商会提供适合小型嵌入式系统使用的 printf()
实现。即使是这样,该实现可能也不是线程安全的,可能不适合在中断服务例程中使用,执行时间会相对较长。
如果没有专门为小型嵌入式系统设计的 printf()
实现,而使用通用 printf()
实现,则必须特别小心:
- 只要调用
printf()
或sprintf()
,就会大大增加应用程序镜像文件的大小。 printf()
和sprintf()
可能会调用malloc()
,如果使用的是 heap_3 以外的内存分配方案,malloc()
可能无效。更多信息请参见第 3.2 节 “内存分配方案示例”。- 执行
printf()
和sprintf()
可能需要较大的栈空间。
13.4.1 Printf-stdarg.c
许多 FreeRTOS 演示项目都使用了一个名为 printf-stdarg.c 的文件,它提供了一个最小且使用栈较少的 sprintf()
实现,可以用来代替标准库版本。在大多数情况下,这将允许为每个调用 sprintf()
和相关函数的任务分配一个小得多的栈。
printf-stdarg.c 还提供了一种机制,可将 printf()
的输出逐个字符引导到端口,虽然速度较慢,但可进一步减少栈的使用。
请注意,FreeRTOS 分发中包含的 printf-stdarg.c 并非都实现了 snprintf()
。snprintf()
没有单独的实现时,会忽略缓冲区大小参数,因为它会直接映射到 sprintf()
。
printf-stdarg.c 是开放源代码,但归第三方所有,因此与 FreeRTOS 的许可证是分开的。许可证条款包含在源文件的顶部。
13.5 其他常见错误情况
13.5.1 在示例项目中添加一个简单的任务,导致项目崩溃
创建任务需要从堆中获取内存。许多演示应用程序项目都会要求堆的大小刚好足够创建演示任务,因此在创建任务后,堆中剩余的内存将不足以再添加任何任务、队列、事件组或信号量。
调用 vTaskStartScheduler()
时会自动创建空闲任务,可能还会创建 RTOS 守护任务。只有在剩余堆内存不足以创建这些任务时,vTaskStartScheduler()
才会返回。在调用 vTaskStartScheduler()
之后加入一个空循环 [ for(;;);
],可以使这个错误更容易调试。
要想添加更多任务,必须增加堆大小,或者删除一些现有的演示任务。堆大小的增加始终受可用内存量的限制。更多信息请参见 第 3.2 节 “内存分配方案示例”。
13.5.2 在中断服务例程中调用 API 函数,导致应用崩溃
不要在中断服务例程中使用 API 函数,除非 API 函数的名称以 “…FromISR() ”结尾。特别是,除非使用中断安全的宏,否则不要在中断中创建临界区。更多信息请参见第 7.2 节 “从 ISR 使用 FreeRTOS API”。
在支持中断嵌套的 FreeRTOS port 中, 不要在中断优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY
的中断中使用任何 API 函数。有关更多信息,请参见第 7.8 节 “中断嵌套”。
13.5.3 应用有时在中断服务例程中崩溃
首先要检查 ISR 中是否发生了栈溢出。有些 port 只检查任务内部的栈溢出,而不检查中断。
不同 port 和不同编译器定义和使用中断的方式也不尽相同。因此,要检查的第二件事是中断服务例程中使用的语法、宏和调用约定是否与所使用的 port 的文档页面中描述的完全一致,以及是否与随 port 提供的演示应用程序中的完全一致。
如果程序运行在使用低优先级数字表示逻辑上高优先级的处理器上,则应确保分配给每个中断的优先级都考虑到这一点,因为这似乎有违直觉。如果应用程序运行在将每个中断的优先级默认为最大可能优先级的处理器上,则应确保每个中断的优先级不是默认值。更多信息请参见第 7.8 节 “中断嵌套 ”和第 13.2 节 “中断优先级”。
13.5.4 调度器在启动第一个任务时崩溃
确保已安装 FreeRTOS 中断处理程序。有关信息,请参阅所用 FreeRTOS port 的文档页面,以及为该 port 提供的演示应用程序示例。
某些处理器必须在特权模式下才能启动调度程序。最简单的方法是在调用 main()
之前,在 C 启动代码中将处理器置于特权模式。
13.5.5 中断被意外关闭或临界区嵌套处理不正确
如果在调度程序启动前调用了 FreeRTOS API 函数,那么中断将被故意禁用,直到第一个任务开始执行后才会重新启用。这样做是为了保护系统,避免在系统初始化期间、调度程序启动之前以及调度程序可能处于不一致状态时,由于中断试图使用 FreeRTOS API 函数而导致系统崩溃。
除了调用 taskENTER_CRITICAL()
和 taskEXIT_CRITICAL()
之外,请勿使用任何其他方法更改微控制器中断使能位或优先级标志。这些宏会对其调用嵌套深度进行计数,以确保只有当调用嵌套完全解除为零时,中断才会重新启用。请注意,某些库函数本身可能会启用或禁用中断。
13.5.6 应用在调度器启动前就崩溃了
在调度程序启动之前,不得执行可能导致上下文切换的中断服务例程。这同样适用于任何试图发送或接收 FreeRTOS 对象(如队列或信号量)的中断服务例程。只有在调度程序启动后才能进行上下文切换。
许多 API 函数在调度程序启动后才能调用。在调用 vTaskStartScheduler()
之前,最好将 API 的使用限制在创建任务、队列和信号量等对象,而不是使用这些对象。
13.5.7 在临界区或调度器挂起时调用 API 函数导致崩溃
调用 vTaskSuspendAll()
可挂起调度器,调用 xTaskResumeAll()
可恢复调度器运行。调用 taskENTER_CRITICAL()
可进入临界区,调用 taskEXIT_CRITICAL()
可退出临界区。
请勿在调度程序挂起时或在临界区内调用 API 函数。
13.6 其他 debug 步骤
如果遇到上述常见情况未涵盖的问题,可以尝试使用以下调试步骤。
- 在应用程序的
FreeRTOSConfig
文件中定义configASSERT()
,启用malloc
失败 hook 和栈溢出 hook。 - 检查 FreeRTOS API 函数的返回值,确保这些返回值是成功的。
- 检查调度器相关配置(如
configUSE_TIME_SLICING
、configUSE_PREEMPTION
)是否按照应用程序要求正确设置。 - 这个页面 提供了有关调试 Cortex-M 微控制器 HardFault 的详细信息。