(1)在RTOS中,需要应对各类事件。这些事件很多时候是通过硬件中断产生,怎么处理中断呢?假设当前系统正在运行Task1时,用户按下了按键,触发了按键中断。这个中断的处理流程如下:
CPU跳到固定地址去执行代码,这个固定地址被称为中断向量,这个跳转是硬件实现的,执行代码做了如下内容:
(2)需要注意的是,ISR是在内核中被调用的,ISR执行过程中,用户的任务无法执行。ISR要尽量快,否则:
(3)如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为2部分:
(4)要在FreeRTOS中熟练使用中断,有几个原则需要先说明:
(5)本文涉及内容:
(1)在任务函数中,我们可以调用给类API函数,比如队列操作函数:xQueueSendToBack。但是在ISR中使用这个函数会导致问题,应该使用另一个函数:xQueueSendToBackFromISR,它的函数名含有后缀"FromISR",表示"从ISR中给队列发送数据"。
(2)FreeRTOS中很多API函数都有两套:一套在任务中使用,另一套在ISR中使用。后者的函数名含有"FromISR"后缀。
(3)为什么要引入两套API函数?
(4)为什么不适用同一套函数,比如在函数里面分辨当前调用者是任务还是ISR呢?示例代码如下:
BaseType_t xQueueSend(...)
{
if (is_in_isr())
{
/* 把数据放入队列 */
/* 不管是否成功都直接返回 */
} else /* 在任务中 */
{
/* 把数据放入队列 */
/* 不成功就等待一会再重试 */
}
}
(5)FreeRTOS使用两套函数,而不是使用一套函数,是因为有如下好处:
(6)使用两套函数可以让程序更高效,但是也有一些缺点,比如你要使用第三方库函数时,即会在任务中调用它,也会在ISR总调用它。这个第三方库函数用到了FreeRTOS的API函数,你无法修改库函数。这个问题可以解决:
类型 | 在任务中 | 在ISR中 |
---|---|---|
队列(queue) | xQueueSendToBack | xQueueSendToBackFromISR |
xQueueSendToFront | xQueueSendToFrontFromISR | |
xQueueReceive | xQueueReceiveFromISR | |
xQueueOverwrite | xQueueOverwriteFromISR | |
xQueuePeek | xQueuePeekFromISR | |
信号量(semaphore) | xSemaphoreGive | xSemaphoreGiveFromISR |
xSemaphoreTake | xSemaphoreTakeFromISR | |
事件组(event group) | xEventGroupSetBits | xEventGroupSetBitsFromISR |
xEventGroupGetBits | xEventGroupGetBitsFromISR | |
任务通知(task notification) | xTaskNotifyGive | vTaskNotifyGiveFromISR |
xTaskNotify | xTaskNotifyFromISR | |
软件定时器(software timer) | xTimerStart | xTimerStartFromISR |
xTimerStop | xTimerStopFromISR | |
xTimerReset | xTimerResetFromISR | |
xTimerChangePeriod | xTimerChangePeriodFromISR |
(1)xHigherPriorityTaskWoken的含义是:是否有更高优先级的任务被唤醒了。如果为pdTRUE,则意味着后面要进行任务切换。
(2)以写队列为例,任务A调用 xQueueSendToBack() 写队列,有几种情况发生:
(3)可以看到,在任务中调用API函数可能导致任务阻塞、任务切换,这叫做"context switch",上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换。
(4)xQueueSendToBackFromISR() 函数也可能导致任务切换,但是不会在函数内部进行切换,而是返回一个参数:表示是否需要切换,函数原型与用法如下:
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/* 用法示例 */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
(5)pxHigherPriorityTaskWoken参数,就是用来保存函数的结果:是否需要切换任务:
(6)为什么不在"FromISR"函数内部进行任务切换,而只是标记一下而已呢?为了效率!示例代码如下:
void XXX_ISR()
{
int i;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(...); /* 被多次调用 */
}
}
(7)ISR中有可能多次调用"FromISR"函数,如果在"FromISR"内部进行任务切换,会浪费时间。解决方法是:
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */
}
/* 最后再决定是否进行任务切换 */
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
}
(8)上述的例子很常见,比如UART中断:在UART的ISR中读取多个字符,发现收到回车符时才进行任务切换。
(9)在ISR中调用API时不进行任务切换,而只是在"xHigherPriorityTaskWoken"中标记一下,除了效率,还有多种好处:
(10)使用"FromISR"函数时,如果不想使用xHigherPriorityTaskWoken参数,可以设置为NULL。
(1)FreeRTOS的ISR函数中,使用两个宏进行任务切换:
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
或
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
(2)这两个宏做的事情是完全一样的,在老版本的FreeRTOS中,
(3)新版本都统一使用 portYIELD_FROM_ISR 。
(4)使用示例如下:
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */
}
/* 最后再决定是否进行任务切换
* xHigherPriorityTaskWoken为pdTRUE时才切换
*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
(1)前面讲过,ISR要尽量快,否则:
(2)如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为2部分:
(3)这种处理方式叫"中断的延迟处理"(Deferring interrupt processing),处理流程如下图所示:
(1)队列、信号量、互斥量、事件组、任务通知等等方法,都可以进行中断与任务间的通信。
(2)要注意的是,在ISR中使用的函数要有"FromISR"后缀。
?