[原创] FreeRTOS学习笔记 (5)中断与任务切换

cruelfox   2018-9-9 22:58 楼主
  在FreeRTOS具备了任务的内存资源——堆栈管理机制,能根据任务状态和优先级进行CPU执行的上下文切换,并提供了任务间通信渠道以实现必要的任务同步和互斥之后,多个任务可以协同起来工作了。不过,既然名称叫做 Real-Time (实时)的操作系统,还需要能对外部(硬件)事件作出快速的响应。尤其是对于单片机上的应用,在一个硬件中断(IRQ)产生以后,立即唤醒某个任务来处理这个事件是操作系统必须要支持的。从任务的角度来看,其实有很多任务是需要根据硬件上的事件(比如传输完成,设备就绪,接收到数据等)来被调度的,否则它将不停测试硬件设备状态寄存器标志位,浪费CPU时间。 irq_schedule.PNG   FreeRTOS 的时间片管理,其实背后就是借用了定时器中断。不然不可能一个任务执行时没有申请调度就被打断,去执行其它同一优先级的任务。类似的,任何硬件中断发生时都会执行相应的中断服务程序(Interrupt Service Routine, ISR, 又叫IRQ Handler),在ISR执行完之后是返回当前任务,还是调度执行其它任务?这完全由ISR来决定。 1. ISR 独立于所有任务   尽管从效果上看,ISR,即中断服务程序是为了某个任务的功能在服务的,务必先强调一下:ISR 的代码不属于 FreeRTOS 任何一个任务代码的部分。每个 ISR 都是一个C语言函数,但它不是一个任务,也不会被任何一个任务所调用。 code.png   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 section, 前后用 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 保护起来,禁止任务调度,以及禁止其它中断 ISR 访问 FreeRTOS 核心数据。   这样处理后,这段代码临时被赋予了很高的优先级,不论当前任务的优先级如何。猜想一下,先把中断屏蔽,执行过后再允许,不就可以了么?实际上也不是这么简单,来看看 FreeRTOS 怎么定义这两个操作的。   在 task.h 头文件中有这两个宏定义: #define taskENTER_CRITICAL() portENTER_CRITICAL() #define taskEXIT_CRITICAL() portEXIT_CRITICAL()   接着找,在(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. {
  7. configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
  8. }
  9. }
  10. void vPortExitCritical( void )
  11. {
  12. configASSERT( uxCriticalNesting );
  13. uxCriticalNesting--;
  14. if( uxCriticalNesting == 0 )
  15. {
  16. portENABLE_INTERRUPTS();
  17. }
  18. }
  比屏蔽中断多加了一点点操作:用到一个计数的变量。configASSERT() 代码是可以移除的,不用管。那么为何要计数?答案是为了嵌套调用,经历了多少次 vPortEnterCrititcal() 之后就需要同样次数的 vPortExitCritical() 才可以允许中断。   再看 Cortex-m3 平台下屏蔽中断的操作是怎样: #define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() #define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)   仔细看汇编代码实现的函数
  1. portFORCE_INLINE static void vPortRaiseBASEPRI( void )
  2. {
  3. uint32_t ulNewBASEPRI;
  4. __asm volatile
  5. (
  6. " mov %0, %1 undefined" \
  7. " msr basepri, %0 undefined" \
  8. " isb undefined" \
  9. " dsb undefined" \
  10. :"=r" (ulNewBASEPRI) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
  11. );
  12. }
  这个操作修改了 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 section, 但是需要调用 taskENTER_CRITICAL_FROM_ISR() 和 taskEXIT_CRITICAL_FROM_ISR(), 其参数和返回值有所不同,需要保存和恢复当前中断级别的状态。在 Cortex-m3 平台,对应的是保存和恢复 BASEPRI 寄存器。   configMAX_SYSCALL_INTERRUPT_PRIORITY 这个值的意义是只允许不高于这个优先级的 ISR 调用 FreeRTOS 的 API, 也就是正因为它们有机会调用 API, 就必须在进入 critical section 时将它们屏蔽。至于中断优先级越高,数值是越大还是越小,取决于硬件平台。务必不要将中断优先级(硬件上的概念)和 FreeRTOS 的任务优先级混淆了。 3. ISR 中可以使用的 FreeRTOS API 函数   FreeRTOS 文档里面,一直强调在 ISR 中必须调用名称以 FromISR 结尾的 API 函数, 而不能调用常规的 API. 是因为,ISR 的执行环境和任务不同,除了实现效率的考虑之外,有的API还不得不作出区分。   ISR 中调用的 API 要求迅速返回,不允许等待。系统是不允许中断处理占用过多时间的,更不能等待其它中断发生。有的 API 因为具有阻塞功能,就不能在 ISR 中使用了,要么就改变功能,包括参数传递要求。   任务调度在 ISR 中是可选项。比如通信对象的操作,可能唤醒比当前任务优先级更高的其它任务;如果是在任务中进行,将立即引起任务切换。但是在 ISR 里面也许并不需要那么频繁地切换任务,把它作为可以自由选择的操作有利于运行效率。这种 xxxxFromISR() 的API会有一个 BaseType_t *pxHigherPriorityTaskWoken 参数,用来判断是否有更高优先级任务被唤醒,再由 ISR 自己决定是否要作任务切换。   我从手册摘录了 ISR 专用的API函数,以及它们对应的普通API版本,列在下表。有的API普通版本是有一个参数指定等待时间,在ISR版本中就取消了该参数。
ISR专用函数名常规API对应其它特性
xTaskGetTickCountFromISRxTaskGetTickCount
xTaskNotifyFromISRxTaskNotify附加参数
xTaskNotifyAndQueryFromISRxTaskNotifyAndQuery附加参数
vTaskNotifyGiveFromISRxTaskNotifyGive附加参数
xTaskResumeFromISRvTaskResume返回值
xQueueIsQueueEmptyFromISR---
xQueueIsQueueFullFromISR---
uxQueueMessagesWaitingFromISRuxQueueMessagesWaiting
xQueueOverwriteFromISRxQueueOverwrite附加参数
xQueuePeekFromISRxQueuePeek取消等待
xQueueReceiveFromISRxQueueReceive附加参数,取消等待
xQueueSelectFromSetFromISRxQueueSelectFromSet取消等待
xQueueSendFromISRxQueueSend附加参数,取消等待
xQueueSendToBackFromISRxQueueSendToBack附加参数
xQueueSendToFrontFromISRxQueueSendToFront附加参数
xSemaphoreGiveFromISRxSemaphoreGive附加参数
xSemaphoreTakeFromISRxSemaphoreTake附加参数,取消等待
xTimerChangePeriodFromISRxTimerChangePeriod附加参数,取消等待
xTimerPendFunctionCallFromISRxTimerPendFunctionCall附加参数,取消等待
xTimerResetFromISRxTimerReset附加参数,取消等待
xTimerStartFromISRxTimerStart附加参数,取消等待
xTimerStopFromISRxTimerStop附加参数,取消等待
xEventGroupClearBitsFromISRxEventGroupClearBitsDaemon Task中执行
xEventGroupGetBitsFromISRxEventGroupGetBitsDaemon Task中执行
xEventGroupSetBitsFromISRxEventGroupSetBits附加参数,Daemon Task中执行
  当 ISR 需要任务调度的时候(例如遇到某个API返回 *pxHigherPriorityTaskWoken 等于 pdTRUE),应当在 ISR 返回之前执行 portYIELD_FROM_ISR(pdTRUE),让调度器切换任务。对于 Cortex-m3 平台,portYIELD_FROM_ISR() 除了检查参数是否为真外,实现调度的方式和 portYIELD() 完全一样,就是让 NVIC (中断控制器)中的 PendSV 位置位。这样当所有的硬件中断请求 ISR 返回以后,PendSV 中断的 ISR 被执行,调度器进行任务切换。(参看我以前写的帖子"FreeRTOS学习笔记 (3)任务状态及切换")   用 ISR 触发任务调度,在逻辑上是将外部中断事件的一部分处理工作交给了某个(或某些)任务去做,只在 ISR 中做一些紧迫且耗时不多的处理(像读硬件设备的寄存器,清除标志位,将缓冲区数据进行转存之类)。而余下的由任务处理的工作,再根据任务优先级由 FreeRTOS 的调度器器去管理。在软件看来,就好象是任务在等待中断发生然后立即处理一样。 4. Daemon Task   将硬件中断处理的较为复杂、耗时的工作交给一个单独的任务来做当然顺理成章,不过 FreeRTOS 还提供了一种机制,可以免去创建单独的任务。这就是借助系统的 Daemon Task.   xTimerPendFunctionCallFromISR() 函数将一个普通函数作为参数“提交”给系统服务,让系统自带的 Daemon Task 执行这个函数。提交时一并指定两个参数传递给这个函数。Daemon Task 受调度器管理,它的任务优先级由 configTIMER_TASK_PRIORITY 指定。Daemon Task 何时执行提交的函数,就要看系统是否空闲了,当它获得执行机会时,就会从命令队列里面取出要执行的函数入口地址和参数去执行。借用手册上的一个图: daemon.PNG   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. BaseType_t xListWasEmpty;
  5. #if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
  6. {
  7. extern void vApplicationDaemonTaskStartupHook( void );
  8. }
  9. #endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */
  10. for( ;; )
  11. {
  12. xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
  13. prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );
  14. prvProcessReceivedCommands();
  15. }
  16. }
  其中的循环是在处理软件定时器事件,按照到期时间排序一个一个处理(执行对应的函数)。这里涉及到软件定时器——FreeRTOS的功能,后面再研究吧。为了理清从 ISR 提交的函数怎么被执行,先看看 xTimerPendFunctionCallFromISR() 做了些什么:
  1. BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend, void *pvParameter1, uint32_t ulParameter2, BaseType_t *pxHigherPriorityTaskWoken )
  2. {
  3. DaemonTaskMessage_t xMessage;
  4. BaseType_t xReturn;
  5. xMessage.xMessageID = tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR;
  6. xMessage.u.xCallbackParameters.pxCallbackFunction = xFunctionToPend;
  7. xMessage.u.xCallbackParameters.pvParameter1 = pvParameter1;
  8. xMessage.u.xCallbackParameters.ulParameter2 = ulParameter2;
  9. xReturn = xQueueSendFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );
  10. tracePEND_FUNC_CALL_FROM_ISR( xFunctionToPend, pvParameter1, ulParameter2, xReturn );
  11. return xReturn;
  12. }
  容易理解,把要执行的函数地址和参数填写在 DaemonTaskMessage_t 数据结构里面,加到 xTimerQueue 队列。而在上面任务循环中的 prvProcessTimerOrBlockTask() 函数里面有这么一条(完整代码就不在此列出了)调用: vQueueWaitForMessageRestricted(xTimerQueue, (xNextExpireTime - xTimeNow), xListWasEmpty); 也就是等待 xTimerQueue 队列中有消息,直到下一个软件定时器到期。于是,Daemon Task 收到 ISR 中发来的消息,就会转而执行消息指定的命令(函数调用)了。   小结   为了支持对硬件事件的实时响应,中断服务程序(ISR)必须要尽早得到执行。因为系统可能有多种中断发生,ISR 需要编写得尽可能短,执行完关键的操作后就返回,以允许其它中断处理。FreeRTOS 提供了一系列机制,让 ISR 将需要处理但又不是那么紧急的操作交给任务去完成,合理分配CPU资源。 本帖最后由 cruelfox 于 2018-9-10 07:54 编辑

回复评论 (1)

好东东,谢谢分享,正在使用freertos
专注智能产品的研究与开发,专注于电子电路的生产与制造……QQ:2912615383,电子爱好者群: void
点赞  2018-9-10 08:36
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复