⼲货FreeRTOS学习笔记——中断与任务切换
EEWorld
在FreeRTOS具备了任务的内存资源——堆栈管理机制,能根据任务状态和优先级进⾏CPU执⾏的上下⽂切换,并提供了任务间通信渠道以实现必要的任务同步和互斥之后,多个任务可以协同起来⼯作了。不过,既然名称叫做 Real-Time (实时)的操作系统,还需要能对外部(硬件)事件作出快速的响应。尤其是对于单⽚机上的应⽤,在⼀个硬件中断(IRQ)产⽣以后,⽴即唤醒某个任务来处理这个事件是操作系统必须要⽀持的。从任务的⾓度来看,其实有很多任务是需要根据硬件上的事件(⽐如传输完成,设备就绪,接收到数据等)来被调度的,否则它将不停测试硬件设备状态寄存器标志位,浪费CPU时间。
FreeRTOS 的时间⽚管理,其实背后就是借⽤了定时器中断。不然不可能⼀个任务执⾏时没有申请调度就被打断,去执⾏其它同⼀优先级的任务。类似的,任何硬件中断发⽣时都会执⾏相应的中断服务程序(Interrupt Service Routine, ISR,⼜叫IRQ Handler),在ISR执⾏完之后是返回当前任务,还是调度执⾏其它任务?这完全由ISR来决定。
1. ISR 独⽴于所有任务
尽管从效果上看,ISR,即中断服务程序是为了某个任务的功能在服务的,务必先强调⼀下:ISR 的代码
不属于FreeRTOS 任何⼀个任务代码的部分。每个 ISR 都是⼀个C语⾔函数,但它不是⼀个任务,也不会被任何⼀个任务所调⽤。
ISR 对堆栈的使⽤与任务不同。前⾯的连载中已经介绍过,FreeRTOS 对每个任务分配了独⽴的堆栈空间,⽤于保存函数的局部变量等等。在发⽣中断时,CPU的某些寄存器会被保存到当前的堆栈⾥(⽽不是指定某任务的堆栈),然后开始执⾏ISR程序。如果当前是某个任务的代码正被执⾏,则会占⽤该任务的堆栈;如果当前是另外⼀个 ISR 的代码正在执⾏即发⽣中断嵌套,那么可能继续⽤更早被中断的任务的堆栈(注:这与平台有关。对于 ARM Cortex-m 系列平台上的实现,FreeRTOS 让任务运⾏在 thread mode, 使⽤PSP作堆栈指针,⽽ ISR 会切换到 handler mode, 使⽤MSP作为堆栈指针,于是所有 ISR 会共享⼀个堆栈)。
ISR 的执⾏可以与FreeRTOS 内核⽆关。只要在 ISR 中不使⽤ FreeRTOS 的API,那么 FreeRTOS 不会知道这个中断的发⽣,因为它不论当前堆栈在哪⾥,都能保存现场并在执⾏后恢复现场。同样,ISR 的执⾏本⾝也不会引起任何的任务切换。在将 FreeRTOS 代码引⼊到现有的⼯程时,原有的 ISR 不需要经过修改仍然可以运作。
ISR 不改变当前任务的状态。尽管 IRQ 发⽣以后,当前运⾏着的任务执⾏被暂停,CPU转⽽执⾏ ISR 的代码,但当前任务的状态仍然是 Running,并不是变成其它状态——这与任务被抢占明显不同。哪
怕是在 ISR ⾥⾯调⽤了FreeRTOS 的 API, 使其它具有⽐当前任务更⾼优先级的任务被唤醒(变为Ready状态),在 ISR 返回之后才会经过任务切换操作,重新选择运⾏的任务。其实,ISR 也不知道当前运⾏的任务是什么,去主动改变当前任务状态没有意义。
2. Critical Section 概念
前⾯我在分析 FreeRTOS 实现细节的时候,多次遇到 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 这两个调⽤。从名称来理解就是说,这时要做⼀个很要紧的操作,不允许被打断,⽐如要对任务状态列表进⾏访问。如果不这样处理的话,有可能中途要访问的数据被改写了,或者是数据改动未完成被其它任务或者 FreeRTOS 内核访问,都会造成错误的结果。于是,定义⼀段代码为 critical ction, 前后⽤taskENTER_CRITICAL()和taskEXIT_CRITICAL() 保护起来,禁⽌任务调度,以及禁⽌其它中断 ISR 访问 FreeRTOS 核⼼数据。
这样处理后,这段代码临时被赋予了很⾼的优先级,不论当前任务的优先级如何。猜想⼀下,先把中断屏蔽,执⾏过后再允许,不就可以了么?实际上也不是这么简单,来看看 FreeRTOS 怎么定义这两个操作的。
在 task.h 头⽂件中有这两个宏定义:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
接着找,在(CM3平台的) portmacro.h ⽂件中⼜定义为
接着找,在(CM3平台的) portmacro.h ⽂件中⼜定义为
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
在 port.c ⽂件中找到 vPortEnterCritical() 和 vPortExitCritical() 函数的实现:
1.void vPortEnterCritical( void )
2.{
3.portDISABLE_INTERRUPTS();
4.uxCriticalNesting++;
5.if( uxCriticalNesting == 1 )
6.{
8.}
9.}
10.
11.void vPortExitCritical( void )
12.{
14.uxCriticalNesting--;
15.if( uxCriticalNesting == 0 )
16.{
17.portENABLE_INTERRUPTS();
18.}
19.}
⽐屏蔽中断多加了⼀点点操作:⽤到⼀个计数的变量。configASSERT() 代码是可以移除的,不⽤管。那么为何要计数?答案是为了嵌套调⽤,经历了多少次 vPortEnterCrititcal() 之后就需要同样次数的 vPortExitCritical() 才可以允许中断。
再看 Cortex-m3 平台下屏蔽中断的操作是怎样:
#define portDISABLE_INTERRUPTS() vPortRaiBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)
仔细看汇编代码实现的函数
1.portFORCE_INLINE static void vPortRaiBASEPRI( void )
2.{
3.uint32_t ulNewBASEPRI;
4.
5.__asm volatile
6.(
7." mov %0, %1 undefined"
8." msr bapri, %0 undefined"
9." isb undefined"
10." dsb undefined"
11.:"=r" (ulNewBASEPRI) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )头晕吃什么
12.);
13.}
这个操作修改了 BASEPRI 寄存器,屏蔽⼀部分的硬件中断: 优先级等于或低于
柳树的介绍configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断。为什么是只屏蔽了部分呢?因为如果某个中断 ISR 不会访问FreeRTOS 的核⼼数据,也不会调⽤任何 FreeRTOS API,那么它中断了也是⽆害的。不过部分屏蔽中断需要硬件⽀持,⽐如在 ARM Cortex-m0 平台下没有 BASEPRI 寄存器,对应的实现代码就简单了:
#define portDISABLE_INTERRUPTS() __asm volatile ( " cpsid i " )
#define portENABLE_INTERRUPTS() __asm volatile ( " cpsie i " )
在 ISR ⾥⾯也可以有 critical ction, 但是需要调⽤ taskENTER_CRITICAL_FROM_ISR() 和
taskEXIT_CRITICAL_FROM_ISR(), 其参数和返回值有所不同,需要保存和恢复当前中断级别的状态。在 Cortex-m3 平台,对应的是保存和恢复 BASEPRI 寄存器。
configMAX_SYSCALL_INTERRUPT_PRIORITY 这个值的意义是只允许不⾼于这个优先级的 ISR 调⽤ FreeRTOS 的API, 也就是正因为它们有机会调⽤ API, 就必须在进⼊ critical ction 时将它们屏蔽。⾄于中断优先级越⾼,数值是越⼤还是越⼩,取决于硬件平台。务必不要将中断优先级(硬件上的概念)和 FreeRTOS 的任务优先级混淆了。
3. ISR 中可以使⽤的 FreeRTOS API 函数
FreeRTOS ⽂档⾥⾯,⼀直强调在ISR 中必须调⽤名称以FromISR结尾的 API函数, ⽽不能调⽤常规的 API. 是因
为,ISR 的执⾏环境和任务不同,除了实现效率的考虑之外,有的API还不得不作出区分。
ISR 中调⽤的 API 要求迅速返回,不允许等待。系统是不允许中断处理占⽤过多时间的,更不能等待其它中断发⽣。有的 API 因为具有阻塞功能,就不能在 ISR 中使⽤了,要么就改变功能,包括参数传递要求。
任务调度在 ISR 中是可选项。⽐如通信对象的操作,可能唤醒⽐当前任务优先级更⾼的其它任务;如果是在任务中进⾏,将⽴即引起任务切换。但是在 ISR ⾥⾯也许并不需要那么频繁地切换任务,把它作为可以⾃由选择的操作有利于运⾏效率。这种 xxxxFromISR() 的API会有⼀个 BaType_t *pxHigherPriorityTaskWoken 参数,⽤来判断是否有更⾼优先级任务被唤醒,再由 ISR ⾃⼰决定是否要作任务切换。
三星手机应用商店
我从⼿册摘录了 ISR 专⽤的API函数,以及它们对应的普通API版本,列在下表。有的API普通版本是有⼀个参数指定等待时间,在ISR版本中就取消了该参数。
ISR专⽤函数名常规API对应其它特性
xTaskGetTickCountFromISR xTaskGetTickCount
xTaskNotifyFromISR xTaskNotify附加参数
xTaskNotifyAndQueryFromISR xTaskNotifyAndQuery附加参数
vTaskNotifyGiveFromISR xTaskNotifyGive附加参数
xTaskResumeFromISR vTaskResume返回值
xQueueIsQueueEmptyFromISR---
xQueueIsQueueFullFromISR---uxQueueMessagesWaitingFromISRuxQueueMessagesWaiting
xQueueOverwriteFromISR xQueueOverwrite附加参数
xQueuePeekFromISR xQueuePeek取消等待
xQueueReceiveFromISR xQueueReceive附加参数,取消等待xQueueSelectFromSetFromISR xQueueSelectFromSet取消等待
xQueueSendFromISR xQueueSend附加参数,取消等待xQueueSendToBackFromISR xQueueSendToBack附加参数
xQueueSendToFrontFromISR xQueueSendToFront附加参数
xSemaphoreGiveFromISR xSemaphoreGive附加参数
xSemaphoreTakeFromISR xSemaphoreTake附加参数,取消等待xTimerChangePeriodFromISR xTimerChangePeriod附加参数,取消等待xTimerPendFunctionCallFromISR xTimerPendFunctionCall附加参数,取消等待
xTimerRetFromISR xTimerRet附加参数,取消等待
xTimerStartFromISR xTimerStart附加参数,取消等待
xTimerStopFromISR xTimerStop附加参数,取消等待xEventGroupClearBitsFromISR xEventGroupClearBits Daemon Task中执⾏xEventGroupGetBitsFromISR xEventGroupGetBits Daemon Task中执⾏xEventGroupSetBitsFromISR xEventGroupSetBits附加参数,Daemon Task中执⾏
当 ISR 需要任务调度的时候(例如遇到某个API返回 *pxHigherPriorityTaskWoken 等于 pdTRUE),应当在 ISR 返回之前执⾏ portYIELD_FROM_ISR(pdTRUE),让调度器切换任务。对于 Cortex-m3 平台,portYIELD_FROM_ISR() 除了检查参数是否为真外,实现调度的⽅式和 portYIELD() 完全⼀样,就是让 NVIC (中断控制器)中的 PendSV 位置位。这样当所有的硬件中断请求 ISR 返回以后,PendSV 中断的 ISR 被执⾏,调度器进⾏任务切换。(参看我以前写的帖⼦"FreeRTOS学习笔记 (3)任务状态及切换")
⼦"FreeRTOS学习笔记 (3)任务状态及切换")
⽤ ISR 触发任务调度,在逻辑上是将外部中断事件的⼀部分处理⼯作交给了某个(或某些)任务去做,只在 ISR 中做⼀些紧迫且耗时不多的处理(像读硬件设备的寄存器,清除标志位,将缓冲区数据进⾏转存之类)。⽽余下的由任务处理的⼯作,再根据任务优先级由 FreeRTOS 的调度器器去管理。在软件看来,就好象是任务在等待中断发⽣然后⽴即处理⼀样。
4. Daemon Task
将硬件中断处理的较为复杂、耗时的⼯作交给⼀个单独的任务来做当然顺理成章,不过 FreeRTOS 还提供了⼀种机制,可以免去创建单独的任务。这就是借助系统的 Daemon Task.
疣状痣xTimerPendFunctionCallFromISR() 函数将⼀个普通函数作为参数“提交”给系统服务,让系统⾃带的 Daemon Task 执⾏这个函数。提交时⼀并指定两个参数传递给这个函数。Daemon Task 受调度器管理,它的任务优先级由configTIMER_TASK_PRIORITY 指定。Daemon Task 何时执⾏提交的函数,就要看系统是否空闲了,当它获得执⾏机会时,就会从命令队列⾥⾯取出要执⾏的函数⼊⼝地址和参数去执⾏。借⽤⼿册上的⼀个图:
FreeRTOS 的 Event Group 实现就借⽤了 Daemon Task 来处理 ISR 中的操作,例如上⾯表中列出的xEventGroupSetBitsFromISR() 调⽤。⼿册叙述的原因是这不是⼀个 "deterministic operation"(耗时可能过长)。在event_groups.h 中定义了
#define xEventGroupClearBitsFromISR(xEventGroup, uxBitsToClear)
xTimerPendFunctionCallFromISR(vEventGroupClearBitsCallback,
(void *) xEventGroup, (uint32_t)uxBitsToClear, NULL)
就这样把⼀个 FromISR 的调⽤延迟到 Daemon Task 中去执⾏普通版本调⽤了。
Daemon Task 的主体是这样:
1.static void prvTimerTask( void *pvParameters )
2.{
3.TickType_t xNextExpireTime;
4.BaType_t xListWasEmpty;
5.
6.
7.#if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
8.{
西兰花沙拉
< void vApplicationDaemonTaskStartupHook( void );
10.}
11.#endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */
12.
展示台
13.for( ;; )
14.{
15.xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
16.prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );
17.prvProcessReceivedCommands();
18.}
19.}
其中的循环是在处理软件定时器事件,按照到期时间排序⼀个⼀个处理(执⾏对应的函数)。这⾥涉及到软件定时器——FreeRTOS的功能,后⾯再研究吧。为了理清从 ISR 提交的函数怎么被执⾏,先看看xTimerPendFunctionCallFromISR() 做了些什么:
1.BaType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend, void *pvParameter1,
uint32_t ulParameter2, BaType_t *pxHigherPriorityTaskWoken )
2.{
2.{
3.DaemonTaskMessage_t xMessage;
4.BaType_t xReturn;
5.
6.xMessage.xMessageID = tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR;
7.xMessage.u.xCallbackParameters.pxCallbackFunction = xFunctionToPend;
8.xMessage.u.xCallbackParameters.pvParameter1 = pvParameter1;
安全责任状9.xMessage.u.xCallbackParameters.ulParameter2 = ulParameter2;
10.
11.xReturn = xQueueSendFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );
12.
14.
16.}
容易理解,把要执⾏的函数地址和参数填写在 DaemonTaskMessage_t 数据结构⾥⾯,加到 xTimerQueue 队列。⽽在上⾯任务循环中的 prvProcessTimerOrBlockTask() 函数⾥⾯有这么⼀条(完整代码就不在此列出了)调⽤:
花样滑冰表演vQueueWaitForMessageRestricted(xTimerQueue, (xNextExpireTime - xTimeNow), xListWasEmpty);
也就是等待 xTimerQueue 队列中有消息,直到下⼀个软件定时器到期。于是,Daemon Task 收到 ISR 中发来的消息,就会转⽽执⾏消息指定的命令(函数调⽤)了。
⼩结
为了⽀持对硬件事件的实时响应,中断服务程序(ISR)必须要尽早得到执⾏。因为系统可能有多种中断发⽣,ISR 需要编写得尽可能短,执⾏完关键的操作后就返回,以允许其它中断处理。FreeRTOS 提供了⼀系列机制,让 ISR 将需要处理但⼜不是那么紧急的操作交给任务去完成,合理分配CPU资源。