前面的文章都提到了部分FreeRTOS的API有中断安全的版本,下面详解一下,为什么会这样。
参考资料:
《Mastering the FreeRTOS™ Real Time Kernel》-Chapter 6 Interrupt Management 6.2 Using the FreeRTOS API from an ISR/6.3 Deferred Interrupt Processing
FreeRTOS全解析-7.中断安全API和推迟中断处理
目录
1.使用FreeRTOS中断安全的API
1.1什么是中断安全API
1.2使用两套API的优点
1.3使用两套API的缺点
1.4xHigherPriorityTaskWoken参数
1.5中断中切换任务
2.中断的延迟处理
不了解中断的,可以看一下中断基础知识:嵌入式Linux入门-异常与中断(流程+寄存器全解析)
部分FreeRTOS的API中的部分行为,在中断服务例程(ISR)中执行是非法的,有可能破坏FreeRTOS的调度,或者出现其他不可预测的问题,就比如进入阻塞态,只有任务才能进入阻塞态,ISR又不是任务。
但是在写一个中断服务例程(ISR)时,我们也可能会需要用到一些FreeRTOS的API,这就导致了存在不安全的情况,为了解决这个问题,FreeRTOS提供了两套API,后缀名为FromISR的是中断安全API
这里就引发一个问题了,为什么要使用两套API,而不是使用同一个函数,在函数中判断一下是处于任务中还是处于ISR中,再分别进行不同操作?
归根结底,就是更加高效。
(1)使用同一套函数的话,需要增加额外的判断代码、增加额外的分支,会让函数更长、更复杂、难以测试
(2)在任务、ISR中调用时,需要的参数不一样。就像阻塞,通过参数指定阻塞时间,而ISR中根本就无法阻塞,不需要这个参数。
(3)使用同一套函数的话,移植FreeRTOS时必须提供检测是处于任务还是ISR的方法,有一些芯片架构不好检测,则需要更多的代码来判断。
使用两套API会引入一个问题,比如你要使用第三方库函数时,即需要在任务中调用它,也需要在ISR中调用它。这个第三方库函数用到了FreeRTOS的API函数。可以用下面的方法解决这个问题:
(1)把中断的处理推迟到任务中进行,在任务中调用库函数
(2)在库函数中使用"FromISR"函数:在任务中、在ISR中都可以调用"FromISR"函数,反过来就不行,非FromISR函数无法在ISR中使用。
(3)第三方库函数也许会提供OS抽象层,自行判断当前环境是在任务还是在ISR中,分别调用不同的函数。
xHigherPriorityTaskWoken字面上理解Higher更高的Priority优先级Task任务Woken被唤醒。含义就是:是否有更高优先级的任务被唤醒了。如果为pdTRUE,则意味着后面要进行任务切换,下面详解:
很多API会导致任务切换(或者说上下文切换context switch)
以写队列为例:不了解队列的可以看这篇
FreeRTOS全解析-5.队列(Queue)
任务A调用xQueueSendToBack()写队列,有几种情况发生:
(1)队列满了,任务A阻塞等待,另一个任务B运行
(2)队列没满,任务A成功写入队列,但是有另一个任务B(处于阻塞态,等待队列有数)任务B被唤醒,任务B的优先级更高:任务B先运行
(3)队列没满,任务A成功写入队列,即刻返回
情况1和2都导致了任务切换。并且这个切换是发生在任务A当中的,xQueueSendToBack()还没有返回就切换了。
让我们再来看看中断时调用类似API会发生什么情况
根据上文中的论述,我们在中断中要写队列,应该调用xQueueSendToBackFromISR()。
BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken);
可以看到xTicksToWait换成了pxHigherPriorityTaskWoken,因为中断是无法阻塞的。而xQueueSendToBackFromISR函数内部不会切换,只是用pxHigherPriorityTaskWoken参数来保存函数的结果:是否需要切换。
用法:pxHigherPriorityTaskWoken参数使用前需要初始化为pdFALSE。
当它为pdTRUE时表示有任务需要切换。
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
为什么不在API内部切换?
(1)避免没必要的切换
比如UART中断:在UART的ISR中读取字符,我们处理任务往往处理的是一个字符串,而不是单个字符,收到一个字符串后(发现收到回车符),才有必要切换到字符串处理任务。假如在API内部切换,则会变成,中断,处理,中断,处理....效率低下。
(2)让ISR更可控
中断产生的时机是不可预测的,在API中进行任务切换的话,会导致问题更复杂
(3)可移植性更强
(4)高效。部分芯片架构只允许在ISR最后进行切换,如果要修改这部分限制,需要更多的代码。
(5)FreeRTOS Tick中断中可以开启钩子函数,也就是可以调用自己的代码。这时候就需要上述的优点了。
如果你不需要切换,用不到这个参数,传入NULL就可以了。
在任务中主动切换,调用taskYIELD(),在ISR函数中,使用两个宏进行任务切换:
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
这两个宏做的事情是完全一样的,在老版本的FreeRTOS中,
portEND_SWITCHING_ISR使用汇编实现
portYIELD_FROM_ISR使用C语言实现
新版本都统一使用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);
}
ISR要尽可能得简短快速。原因包括:
(1)即使任务被分配了非常高的优先级,它们也只会在硬件没有服务中断的情况下运行。
(2)ISR会扰乱一个任务的开始时间和执行时间
(3)部分CPU架构在ISR时无法处理新的中断。
(4)写程序时需要考虑变量、外设和内存缓冲区等资源同时被任务和ISR访问的后果并加以防范。
(5)一些CPU架构允许中断嵌套,但是中断嵌套会增加复杂性并降低可预测性。中断越短,嵌套的可能性就越小。
在ISR中做一些必要的工作,比如记录和清除中断,然后把其他的工作放到一个任务中去运行,这就叫延迟中断处理。
只要把这个任务优先级设置得比其他任务高,那么发生中断后,这个任务就会立即运行,就像在中断中运行的效果一样。
如图Task2是中断处理任务,一开始是处于阻塞态。
t1时Task1运行,t2时发生中断,ISR执行,清除中断,解除Task2的阻塞态。
t3时ISR运行结束,Task2的优先级比Task1高,所以ISR结束后直接跳到Task2运行。
t4时Task2运行结束进入阻塞态,等待下一次中断,Task1运行。
什么时候在ISR中处理,什么时候推迟到任务中,没有一个必然的规定,下面几种情况最好在任务中处理:
(1)处理比较复杂。例如,如果中断只是存储模拟到数字转换的结果,那么这最好在ISR中执行,但如果转换的结果也必须通过软件过滤器,那么最好在任务中执行过滤器。
(2)存在ISR内部无法执行的操作,例如写入控制台或分配内存。
(3)处理时间是不确定的——不知道处理将花费多长时间。
当然还有别的办法推迟中断处理,将在后面的文章中提到。