FreeRTOS推迟中断到守护任务
到目前为止,所演示的延迟中断处理示例都要求编程人员为使用延迟处理技术的每个中断创建一个任务。此外,还可以使用 xTimerPendFunctionCallFromISR()(守护任务最初被称为定时器服务任务,因为它最初仅用于执行软件定时器回调函数。因此,xTimerPendFunctionCall() 在 timers.c 文件中实现,并且按照在函数名前面添加函数所在文件名的命名约定,该函数名以"Timer"为前缀。)API 函数将中断处理延迟到 RTOS 守护任务中,从而无需为每个中断创建单独的任务。将中断处理延迟到守护任务中被称为"集中式推迟中断处理"。
软件定时器相关的 FreeRTOS API 函数如何通过定时器命令队列向守护任务发送命令。xTimerPendFunctionCall() 和 xTimerPendFunctionCallFromISR() API 函数使用相同的定时器命令队列向守护任务发送"执行函数"命令。然后,发送到守护任务的函数将在守护任务的上下文中执行。
集中式推迟中断处理的优点包括:
- 使用资源更少,它消除了为每个延迟中断创建单独任务的需要。
- 简化用户模型,推迟中断处理函数是一个标准的C函数。
集中式推迟中断处理的缺点包括:
- 降低灵活性,不能单独设置每个推迟中断处理任务的优先级。每个推迟中断处理函数都以守护任务的优先级执行。注意守护任务的优先级由 FreeRTOSConfig.h 文件中的 configTIMER_TASK_PRIORITY 编译时配置常量设置。
- 降低确定性,xTimerPendFunctionCallFromISR() API函数将命令发送到定时器命令队列的末尾。已经在定时器命令队列中的命令,将由守护任务在 xTimerPendFunctionCallFromISR() 向队列发送"执行函数"命令之前处理。
不同的中断具有不同的时间约束,因此,在同一应用程序中同时使用两种延迟中断处理方法是很常见的。

函数xTimerPendFunctionCallFromISR()
xTimerPendFunctionCallFromISR() 是 xTimerPendFunctionCall() API函数的中断安全版本。这两个 API 函数都允许RTOS 守护任务执行由编程人员提供的函数,因此是在RTOS守护任务的上下文中执行。要执行的函数以及函数输入参数是都通过定时器命令队列发送到守护任务。因此,函数实际执行的时间取决于守护任务相对于应用程序中其他任务的优先级。
xTimerPendFunctionCallFromISR() API 函数的原型如下所示:
cpp
BaseType_t xTimerPendFunctionCallFromISR(PendedFunction_t
xFunctionToPend,
void *pvParameter1,
uint32_t ulParameter2,
BaseType_t *pxHigherPriorityTaskWoken );
xTimerPendFunctionCallFromISR() API函数的 xFunctionToPend 参数传递的函数必须符合的函数原型如下所示:
cpp
void vPendableFunction( void *pvParameter1, uint32_t ulParameter2 );
xTimerPendFunctionCallFromISR() API函数的参数与返回值说明如下所示:
| 参数名称 | 参数说明 |
|---|---|
| xFunctionToPend | 指向将在守护任务中执行的函数的指针(实际上只是函数名)。该函数的原型必须与上面所示的原型相同。 |
| pvParameter1 | 将该值作为函数的pvParameter1 参数传递到守护任务执行的函数。该参数的类型为 void*,允许其用于传递任何数据类型。例如,整数类型可以直接转换为 void*,或者 void* 也可以用于指向一个结构体。 |
| ulParameter2 | 将该值作为函数的ulParameter2 参数传递给守护任务执行的函数。 |
| pxHigherPriorityTaskWoken | 如果守护任务的优先级高于当前执行的任务的优先级,那么xTimerPendFunctionCallFromISR() 函数将把*pxHigherPriorityTaskWoken 设置为 pdTRUE。如果 xTimerPendFunctionCallFromISR() 函数将此值设置为 pdTRUE,那么在退出中断之前必须执行上下文切换。这将确保中断直接返回到守护任务,因为守护任务将是优先级最高的就绪状态任务。 |
| 返回值 | 如果已将"执行函数"命令写入定时器命令队列,则会返回 pdPASS。如果定时器命令队列已满,无法将"执行函数"命令写入定时器命令队列,则会返回 pdFAIL |
FreeRTOS在中断中使用队列
二进制信号量和计数信号量用于事件通信。队列既用于事件通信也用于数据传输。
xQueueSendToFrontFromISR() 是 xQueueSendToFront() API函数在中断服务程序中安全使用的版本; xQueueSendToBackFromISR() 是 xQueueSendToBack() API函数在中断服务程序中安全使用的版本;而 xQueueReceiveFromISR() 是 xQueueReceive() API函数在中断服务程序中安全使用的版本。下面介绍 xQueueSendToFrontFromISR()和xQueueSendToBackFromISR() API函数。
函数xQueueSendToFrontFromISR()xQueueSendToBackFromISR()
xQueueSendToFrontFromISR () API 函数的原型如下所示:
cpp
BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue,
const void *pvItemToQueue
BaseType_t *pxHigherPriorityTaskWoken );
xQueueSendToBackFromISR () API 函数的原型如下所示:
cpp
BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue,
const void *pvItemToQueue
BaseType_t *pxHigherPriorityTaskWoken );
xQueueSendFromISR() API函数和 xQueueSendToBackFromISR() API函数在功能上是等价的。xQueueSendToFrontFromISR () 和xQueueSendToBackFromISR () API函数的参数与返回值说明如下所示:
| 参数名称 | 参数说明 |
|---|---|
| xQueue | 用于接收发送(写入)数据的队列的句柄。该队列句柄是通过调用 xQueueCreate() API函数创建队列时返回的。 |
| pvItemToQueue | 指向将被复制到队列上的数据的指针。 |
| pxHigherPriorityTaskWoken | API 函数内部会将 *pxHigherPriorityTaskWoken 设置为 pdTRUE。如果 xQueueSendToFrontFromISR() 或 xQueueSendToBackFromISR() API函数将此值设置为 pdTRUE,则应在退出中断之前执行上下文切换。这将确保中断直接返回到最高优先级的就绪状态任务。 |
| 返回值 | 只有当成功将数据发送到队列时,才会返回pdPASS。如果队列已满而不能将数据发送到队列,则返回errQUEUE_FULL。 |
在ISR中使用队列时的注意事项
队列提供了一种简单且方便的方式,用于将数据从中断传递到任务中,但是,如果数据以高频率到达,使用队列则不是高效的。在演示程序中,使用队列有两个原因:一是演示如何在ISR中使用队列,二是为了测试FreeRTOS移植而故意给系统增加负担。以这种方式使用队列的ISR绝对不代表高效设计,除非数据到达得很慢,因此建议实际产品代码不要采用这种技术。适用于实际产品代码的更高效技术包括:
- 使用直接内存访问(DMA)硬件来接收和缓冲字符。这种方法几乎没有软件开销。然后,可以使用直接任务通知来解除阻塞任务,该任务只有在检测到传输中断后才会处理缓冲区。
- 将接收到的字符复制到线程安全的RAM缓冲区。同样,可以使用直接任务通知来解除阻塞任务,该任务将在接收到完整的消息或检测到传输中断后处理缓冲区。
- 在中断服务程序(ISR)中直接处理接收到的字符,然后使用队列仅将数据处理结果(而不是原始数据)发送到任务。
FreeRTOS的中断嵌套及注意事项
中断嵌套
任务优先级和中断优先级之间常常会出现混淆。分配给任务的优先级与分配给中断的优先级没有任何关系。硬件决定何时执行ISR,而软件决定何时执行任务。响应硬件中断而执行的ISR会中断任务,但任务不能抢占ISR。
支持中断嵌套的FreeRTOS移植需要在FreeRTOSConfig.h中定义下面表格中详细的一个或两个常量,configMAX_SYSCALL_INTERRUPT_PRIORITY和configMAX_API_CALL_INTERRUPT_PRIORITY都定义了相同的属性。较老的FreeRTOS移植使用configMAX_SYSCALL_INTERRUPT_PRIORITY,而较新的FreeRTOS移植使用configMAX_API_CALL_INTERRUPT_PRIORITY。控制中断嵌套的常量如下表所示:
| 常量 | 说明 |
|---|---|
| configMAX_SYSCALL_INTERRUPT_PRIORITY或configMAX_API_CALL_INTERRUPT_PRIORITY | 设置了以从其中调用中断安全的FreeRTOS API函数的最高中断优先级。 |
| configKERNEL_INTERRUPT_PRIORITY | 设置滴答中断使用的中断优先级,并且必须始终设置为可能的最低中断优先级。如果正在使用的FreeRTOS移植没有使用configMAX_SYSCALL_INTERRUPT_PRIORITY常量,那么任何使用中断安全的FreeRTOS API函数的中断也必须以configKERNEL_INTERRUPT_PRIORITY定义的优先级执行。 |
每个中断源都有一个数字优先级和一个逻辑优先级,两种优先级的含义和关系如下。
- 数字优先级
数字优先级就是分配给中断优先级的数字。例如,如果一个中断被分配了7的优先级,那么它的数字优先级就是7。同样地,如果一个中断被分配了200的优先级,那么它的数字优先级就是200。
- 逻辑优先级
中断的逻辑优先级描述了该中断相对于其他中断的优先级。
如果两个优先级不同的中断同时发生,那么处理器将先执行具有更高逻辑优先级的中断的服务程序(ISR),然后再执行具有较低逻辑优先级的中断的服务程序。
一个中断可以中断(嵌套)任何具有较低逻辑优先级的中断,但不能中断(嵌套)具有相同或更高逻辑优先级的中断。
中断的数字优先级和逻辑优先级之间的关系取决于处理器架构。在某些处理器上,分配给中断的数字优先级越高,该中断的逻辑优先级就越高,而在其他处理器架构上,分配给中断的数字优先级越高,该中断的逻辑优先级就越低。
通过把configMAX_SYSCALL_INTERRUPT_PRIORITY设置为比configKERNEL_INTERRUPT_PRIORITY更高的逻辑中断优先级,可以创建完整的中断嵌套模型。这在下图中得到了展示,该图描述了一个场景,其中:
- 该处理器有7个唯一的中断优先级。
- 分配数字优先级为7的中断,比分配数字优先级为1的中断具有更高的逻辑优先级。
- 将configKERNEL_INTERRUPT_PRIORITY设置为1。
- 将configMAX_SYSCALL_INTERRUPT_PRIORITY设置为3。

参考上图:
- 在内核或应用程序处于临界区时,使用优先级1~3(含)的中断将被阻止执行。在这些优先级下运行的ISR(中断服务程序)可以使用中断安全的FreeRTOS API函数。
- 使用优先级4或以上的中断不受临界区的影响,因此调度器所做的任何事情都不会阻止这些中断立即执行,这受限于硬件本身的能力。在这些优先级下执行的ISR不能使用任何FreeRTOS API函数。
- 通常情况下,对时间精度要求非常严格的功能(例如电机控制)会使用高于configMAX_SYSCALL_INTERRUPT_PRIORITY的优先级,以确保调度器不会将抖动引入中断响应时间。