在RTOS中,需要应对各种事件,这些事件是通过硬件中断产生的。当系统运行Task1的时候,用户按下了按键,触发按键中断,这个中断的处理流程如下:
- CPU 跳到固定地址去执行代码,这个固定地址通常被称为中断向量,这个跳转时硬件实现的 。
- 执行代码做什么?
- 保存现场:Task1 被打断,需要先保存 Task1 的运行环境,比如各类寄存器的值 。
- 分辨中断、调用处理函数(这个函数就被称为 ISR,interrupt service routine) 。
- 恢复现场:继续运行 Task1,或者运行其他优先级更高的任务。
ISR是在内核中被调用的,ISR执行过程中,用户的任务无法执行,ISR要尽量快,否则:
- 其他优先级的中断无法被处理,实时性无法保证;
- 用户任务无法执行,显得系统卡顿。
如果硬件中断的处理就是非常耗时间,对于这类中断的处理就分为两部分:
- ISR:尽快做清理,记录工作,然后触发某个任务;
- 任务:更复杂的事情放在任务中处理。
所以,ISR和任务之间需要进行通信。
要在FreeRTOS中熟练使用中断,有几个原则要先说明:
- FreeRTOS 把任务认为是硬件无关的,任务的优先级由程序员决定,任务何时运行由调度器决定
- ISR 虽然也是使用软件实现的,但是它被认为是硬件特性的一部分,因为它跟硬件密切相关
- 何时执行?由硬件决定
- 哪个 ISR 被执行?由硬件决定
- ISR 的优先级高于任务:即使是优先级最低的中断,它的优先级也高于任 务。任务只有在没有中断的情况下,才能执行。
一、引入两套API函数
FreeRTOS中很多API函数都有两套:一套在任务中使用,另一套在ISR中使用。后者的函数名含有"FromISR"后缀。
为什么要引入两套API函数?
- 很多 API 函数会导致任务计入阻塞状态;
- 运行这个函数的任务进入阻塞状态 ;
- 比如写队列时,如果队列已满,可以进入阻塞状态等待一会 ;
- ISR 调用 API 函数时,ISR 不是"任务",ISR 不能进入阻塞状态 ;
所以,在任务中、在 ISR 中,这些函数的功能是有差别的
FreeRTOS 使用两套函数,而不是使用一套函数,是因为有如下好处:
- 使用同一套函数的话,需要增加额外的判断代码、增加额外的分支,是的函 数更长、更复杂、难以测试 ;
在任务、ISR 中调用时,需要的参数不一样,比如:
- 在任务中调用:需要指定超时时间,表示如果不成功就阻塞一会 ;
- 在 ISR 中调用:不需要指定超时时间,无论是否成功都要即刻返回 ;
- 如果强行把两套函数揉在一起,会导致参数臃肿、无效
- 移植 FreeRTOS 时,还需要提供监测上下文的函数,比如 is_in_isr() ;
- 有些处理器架构没有办法轻易分辨当前是处于任务中,还是处于 ISR 中,就 需要额外添加更多、更复杂的代码 。
使用两套函数可以让程序更高效,但是也有一些缺点,比如你要使用第三方库函数时, 即会在任务中调用它,也会在ISR总调用它。这个第三方库函数用到了FreeRTOS的API函数, 你无法修改库函数。这个问题可以解决:
- 把中断的处理推迟到任务中进行(Defer interrupt processing),在任务中调用库函数;
- 尝试在库函数中使用"FromISR"函数;
- 在任务中、在 ISR 中都可以调用"FromISR"函数 ;
- 反过来就不行,非 FromISR 函数无法在 ISR 中使用;
- 第三方库函数也许会提供 OS 抽象层,自行判断当前环境是在任务还是在ISR中,分别调用不同的函数。
二、两套API函数列表
类型 | 在任务中 | 在 ISR 中 |
---|---|---|
队列(queue) | xQueueSendToBack xQueueSendToFront xQueueReceive xQueueOverwrite xQueuePeek | xQueueSendToBackFromISR xQueueSendToFrontFromISR xQueueReceiveFromISR xQueueOverwriteFromISR xQueuePeekFromISR |
信号量(semaphore) | xSemaphoreGive xSemaphoreTake | xSemaphoreGiveFromISR xSemaphoreTakeFromISR |
事件组(event group) | xEventGroupSetBits xEventGroupGetBits | xEventGroupSetBitsFromISR xEventGroupGetBitsFromISR |
任务通知(task notification) | xTaskNotifyGive xTaskNotify | vTaskNotifyGiveFromISR xTaskNotifyFromISR |
软件定时器(software timer) | xTimerStart xTimerStop xTimerReset xTimerChangePeriod | xTimerStartFromISR xTimerStopFromISR xTimerResetFromISR xTimerChangePeriodFromISR |
2.1 xHigherPriorityTaskWoken 参数
xHigherPriorityTaskWoken 的含义是:是否有更高优先级的任务被唤醒了。如果为 pdTRUE,则意味着后面要进行任务切换。 以写队列为例:
任务A调用xQueueSendToBack()写队列,有几种情况发生:
- 队列满了,任务 A 阻塞等待,另一个任务 B 运行;
- 队列没满,任务 A 成功写入队列,但是它导致另一个任务 B 被唤醒,任务 B的优先级更高:任务 B 先运行 ;
- 队列没满,任务 A 成功写入队列,即刻返回 。
可以看到,在任务中调用 API 函数可能导致任务阻塞、任务切换,这叫做"context switch", 上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换。
pxHigherPriorityTaskWoken 参数,就是用来保存函数的结果:是否需要切换
- *pxHigherPriorityTaskWoken 等于 pdTRUE:函数的操作导致更高优先级的任务就绪了,ISR 应该进行任务切换。
- *pxHigherPriorityTaskWoken 等于 pdFALSE:没有进行任务切换的必要。
xQueueSendToBackFromISR()函数也可能导致任务切换,但是不会在函数内部进行切换,而是返回一个参数:表示是否需要切换,函数原型与用法如下:
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken);
用法示例:
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
在ISR中调用API时不进行任务切换,而只是在"xHigherPriorityTaskWoken"中标记一下, 除了效率,还有多种好处:
- 效率高:避免不必要的任务切换
- 让 ISR 更可控:中断随机产生,在 API 中进行任务切换的话,可能导致问题 更复杂
- 可移植性
- 在 Tick 中断中,调用 vApplicationTickHook():它运行与 ISR,只能使用"FromISR"的函数
2.2 切换任务
FreeRTOS 的 ISR 函数中,使用两个宏进行任务切换:
这两个宏做的事情是完全一样的,在老版本的 FreeRTOS 中,
/* 使用汇编实现 */
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
/*
*或使用
*/
/* 使用 C 语言实现 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
新版本都统一使用 portYIELD_FROM_ISR。
示例如下:
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < N; i++)
{
/* 被多次调用 */
xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken);
}
/* 最后再决定是否进行任务切换
* xHigherPriorityTaskWoken 为 pdTRUE 时才切换
*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
三、中断延迟处理
处理流程:
解释说明:
- t1:任务 1 运行,任务 2 阻塞
- t2:发生中断,
- 该中断的 ISR 函数被执行,任务 1 被打断
- ISR 函数要尽快能快速地运行,它做一些必要的操作(比如清除中断),然后唤 醒任务 2
- t3:在创建任务时设置任务 2 的优先级比任务 1 高(这取决于设计者),所以ISR 返回后,运行的是任务 2,它要完成中断的处理。任务 2 就被称为"deferred processing task",中断的延迟处理任务。
- t4:任务 2 处理完中断后,进入阻塞态以等待下一个中断,任务 1 重新运行
四、中断与任务间通信
前面讲解过的队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。要注意的是,在ISR中使用的函数要有"FromISR"后缀。
4.1 优化实时性
以前,在中断函数里写队列时,代码如下:
static void DispatchKey (struct ir_data *pidata)
{
#if 0
extern QueueHandle_t g_xQueueCar1;
extern QueueHandle_t g_xQueueCar2;
extern QueueHandle_t g_xQueueCar3;
xQueueSendToBackFromISR(g_xQueueCar1,pidata,NULL);
xQueueSendToBackFromISR(g_xQueueCar2,pidata,NULL);
xQueueSendToBackFromISR(g_xQueueCar3,pidata,NULL);
#endif
int i;
for(i=0;i<g_queue_cnt;i++)
{
xQueueSendToBackFromISR(g_xQueues[i], pidata, NULL);
}
}
假设当前运行的是任务A,它的优先级比较低,在它运行过程中发生了中断,中断函数调用了DispatchKey函数写了队列,使得任务B被唤醒了。任务B的优先级比较高,它应该在中断执行完后马上就能运行。但是上述代码无法实现这个目标,xQueueSendFromISR函数会把任务B调整为就绪态,但是不会发起一次调度。
修改代码之后:
static void DispatchKey (struct ir_data *pidata)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
#if 0
extern QueueHandle_t g_xQueueCar1;
extern QueueHandle_t g_xQueueCar2;
extern QueueHandle_t g_xQueueCar3;
xQueueSendToBackFromISR(g_xQueueCar1,pidata,NULL);
xQueueSendToBackFromISR(g_xQueueCar2,pidata,NULL);
xQueueSendToBackFromISR(g_xQueueCar3,pidata,NULL);
#endif
int i;
for(i=0;i<g_queue_cnt;i++)
{
xQueueSendFromISR(g_xQueues[i], pidata, &xHigherPriorityTaskWoken);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
在修改后代码的for循环中传入一个变量的地址:&xHigherPriorityTaskWoken,它的初始值是pdFALSE,表示无需发起调度。如果xQueueSendFromISR函数发现唤醒了更高优先级的任务,那么就会把这个变量设置为pdTRUE。
在调用portYIELD_FROM_ISR()函数,如果xHigherPriorityTaskWoken为pdTRUE,它就会发起一次调度。
本程序上机时,我们感觉不到有什么不同。