[活动] 【FreeRTOS打卡第五站开启】中断与任务切换,关门时间8月26日

nmg   2020-8-24 10:14 楼主

活动总览:点此查看(含活动鼓励和活动学习总内容)

 

本站打卡开始和截止时间:8月24日-8月26日(3天)

打卡任务

1、阅读cruelfox干货笔记第五篇:FreeRTOS学习笔记 (5)中断与任务切换

2、跟本帖回复思考题: 请简述 portYIELD_FROM_ISR() 这个宏调用的作用。(阅读FreeRTOS官方文档,以及查看源代码中这个宏的实现都有助于理解)

 

 

FreeRTOS学习笔记 (5)中断与任务切换

为了方便大家阅读,将cruelfox“FreeRTOS学习笔记 (5)中断与任务切换”复制过来了~

 

在FreeRTOS具备了任务的内存资源——堆栈管理机制,能根据任务状态和优先级进行CPU执行的上下文切换,并提供了任务间通信渠道以实现必要的任务同步和互斥之后,多个任务可以协同起来工作了。不过,既然名称叫做 Real-Time (实时)的操作系统,还需要能对外部(硬件)事件作出快速的响应。尤其是对于单片机上的应用,在一个硬件中断(IRQ)产生以后,立即唤醒某个任务来处理这个事件是操作系统必须要支持的。从任务的角度来看,其实有很多任务是需要根据硬件上的事件(比如传输完成,设备就绪,接收到数据等)来被调度的,否则它将不停测试硬件设备状态寄存器标志位,浪费CPU时间。

1.jpg


  FreeRTOS 的时间片管理,其实背后就是借用了定时器中断。不然不可能一个任务执行时没有申请调度就被打断,去执行其它同一优先级的任务。类似的,任何硬件中断发生时都会执行相应的中断服务程序(Interrupt Service Routine, ISR, 又叫IRQ Handler),在ISR执行完之后是返回当前任务,还是调度执行其它任务?这完全由ISR来决定。

1. ISR 独立于所有任务
  尽管从效果上看,ISR,即中断服务程序是为了某个任务的功能在服务的,务必先强调一下:ISR 的代码不属于 FreeRTOS 任何一个任务代码的部分。每个 ISR 都是一个C语言函数,但它不是一个任务,也不会被任何一个任务所调用。

2.jpg
  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.  
  11. void vPortExitCritical( void )
  12. {
  13.     configASSERT( uxCriticalNesting );
  14.     uxCriticalNesting--;
  15.     if( uxCriticalNesting == 0 )
  16.     {
  17.         portENABLE_INTERRUPTS();
  18.     }
  19. }

复制代码


  比屏蔽中断多加了一点点操作:用到一个计数的变量。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.  
  5.     __asm volatile
  6.     (
  7.         "   mov %0, %1            undefined" \
  8.         "   msr basepri, %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 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对应 其它特性
xTaskGetTickCountFromISR xTaskGetTickCount  
xTaskNotifyFromISR xTaskNotify 附加参数
xTaskNotifyAndQueryFromISR xTaskNotifyAndQuery 附加参数
vTaskNotifyGiveFromISR xTaskNotifyGive 附加参数
xTaskResumeFromISR vTaskResume 返回值
xQueueIsQueueEmptyFromISR ---  
xQueueIsQueueFullFromISR ---  
uxQueueMessagesWaitingFromISR uxQueueMessagesWaiting  
xQueueOverwriteFromISR xQueueOverwrite 附加参数
xQueuePeekFromISR xQueuePeek 取消等待
xQueueReceiveFromISR xQueueReceive 附加参数,取消等待
xQueueSelectFromSetFromISR xQueueSelectFromSet 取消等待
xQueueSendFromISR xQueueSend 附加参数,取消等待
xQueueSendToBackFromISR xQueueSendToBack 附加参数
xQueueSendToFrontFromISR xQueueSendToFront 附加参数
xSemaphoreGiveFromISR xSemaphoreGive 附加参数
xSemaphoreTakeFromISR xSemaphoreTake 附加参数,取消等待
xTimerChangePeriodFromISR xTimerChangePeriod 附加参数,取消等待
xTimerPendFunctionCallFromISR xTimerPendFunctionCall 附加参数,取消等待
xTimerResetFromISR xTimerReset 附加参数,取消等待
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)任务状态及切换")

  用 ISR 触发任务调度,在逻辑上是将外部中断事件的一部分处理工作交给了某个(或某些)任务去做,只在 ISR 中做一些紧迫且耗时不多的处理(像读硬件设备的寄存器,清除标志位,将缓冲区数据进行转存之类)。而余下的由任务处理的工作,再根据任务优先级由 FreeRTOS 的调度器器去管理。在软件看来,就好象是任务在等待中断发生然后立即处理一样。

4. Daemon Task
  将硬件中断处理的较为复杂、耗时的工作交给一个单独的任务来做当然顺理成章,不过 FreeRTOS 还提供了一种机制,可以免去创建单独的任务。这就是借助系统的 Daemon Task.

  xTimerPendFunctionCallFromISR() 函数将一个普通函数作为参数“提交”给系统服务,让系统自带的 Daemon Task 执行这个函数。提交时一并指定两个参数传递给这个函数。Daemon Task 受调度器管理,它的任务优先级由 configTIMER_TASK_PRIORITY 指定。Daemon Task 何时执行提交的函数,就要看系统是否空闲了,当它获得执行机会时,就会从命令队列里面取出要执行的函数入口地址和参数去执行。借用手册上的一个图:

3.jpg

  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.  
  6.  
  7.     #if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
  8.     {
  9.         extern 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. BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend, void *pvParameter1, uint32_t ulParameter2, BaseType_t *pxHigherPriorityTaskWoken )
  2. {
  3. DaemonTaskMessage_t xMessage;
  4. BaseType_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.  
  13.     tracePEND_FUNC_CALL_FROM_ISR( xFunctionToPend, pvParameter1, ulParameter2, xReturn );
  14.  
  15.     return xReturn;
  16. }

复制代码


  容易理解,把要执行的函数地址和参数填写在 DaemonTaskMessage_t 数据结构里面,加到 xTimerQueue 队列。而在上面任务循环中的 prvProcessTimerOrBlockTask() 函数里面有这么一条(完整代码就不在此列出了)调用:
vQueueWaitForMessageRestricted(xTimerQueue, (xNextExpireTime - xTimeNow), xListWasEmpty);
也就是等待 xTimerQueue 队列中有消息,直到下一个软件定时器到期。于是,Daemon Task 收到 ISR 中发来的消息,就会转而执行消息指定的命令(函数调用)了。

  小结
  为了支持对硬件事件的实时响应,中断服务程序(ISR)必须要尽早得到执行。因为系统可能有多种中断发生,ISR 需要编写得尽可能短,执行完关键的操作后就返回,以允许其它中断处理。FreeRTOS 提供了一系列机制,让 ISR 将需要处理但又不是那么紧急的操作交给任务去完成,合理分配CPU资源。

 

 

欢迎跟本帖回复本站cruelfox留给大家的思考题: 请简述 portYIELD_FROM_ISR() 这个宏调用的作用。(阅读FreeRTOS官方文档,以及查看源代码中这个宏的实现都有助于理解)

回复评论 (10)

 portYIELD_FROM_ISR(pdTRUE),让调度器切换任务。对于 Cortex-m3 平台,portYIELD_FROM_ISR() 除了检查参数是否为真外,实现调度的方式和 portYIELD() 完全一样,就是让 NVIC (中断控制器)中的 PendSV 位置位。这样当所有的硬件中断请求 ISR 返回以后,PendSV 中断的 ISR 被执行,调度器进行任务切换。

http://shop34182318.taobao.com/ https://shop436095304.taobao.com/?spm=a230r.7195193.1997079397.37.69fe60dfT705yr
点赞  2020-8-24 19:53
ddllxxrr 发表于 2020-8-24 19:53  portYIELD_FROM_ISR(pdTRUE),让调度器切换任务。对于 Cortex-m3 平台,portYIELD_FROM_ISR() 除了检 ...

这个就是复制粘贴啊,标准答案?

点赞  2020-8-25 11:51

执行系统调用,比如普通任务可以使用taskYIELD()强制任务切换,中断服务程序中使用portYIELD_FROM_ISR()强制任务切换。

portYIELD_FROM_ISR函数的参数,如果为pdTRUE,退出中断就会切换到高优先级任务执行。那是因为这个portYIELD_FROM_ISR函数在参数为pdTRUE时,会调用portYIELD()函数强制上下文切换。如果为pdFALSE,不执行上下文切换。

点赞  2020-8-26 20:33

源码中宏定义

#define portNVIC_INT_CTRL_REG  ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT  ( 1UL << 28UL )
#define portYIELD()     vPortYield()
#define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired ) portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )

可见置位的是寄存器地址为0xe000ed04

查看寄存器表

中断控制状态寄存器

中断控制状态寄存器用于:

  • 设置一个挂起(pending)NMI
  • 设置或清除一个挂起 SVC 
  • 设置或清除一个挂起 SysTick
  • 查找挂起异常 
  • 查找最高优先级挂起异常的向量号
  • 查找激活异常的向量号

寄存器地址、访问类型和复位状态:

地址 0xE000ED04

访问类型 读/写或只读

复位状态 0x00000000

中断控制状态寄存器的位分配如图 所示。

image.png

 

由源码宏定义可知,该宏定义主要是挂起pendSV

image.png

 

PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。

 

 

操作系统,上下文切换 实例:

场景假设:一个系统(按时间片轮转调度的系统)中有两个就绪的任务(A任务、B任务),
上下文切换被触发的场合可以是:

  • 执行一个系统调用
  • 系统滴答定时器(SYSTICK)中断,(轮转调度中需要)

A、B两个就绪任务,通过SysTick 异常启动上下文切换。如图7.15 所示。
image.png
上图是两个任务轮转调度的示意图。 但若在产生SysTick 异常时正在响应一个中断,则SysTick 异常会抢占其ISR。
在这种情况下,操作系统 不可以执行上下文切换,否则将使中断请求被延迟, 而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。 因此,在CM3 中也是严禁没商量——如果操作系统 在某中断活跃时尝试切入线程模式,将触犯用法fault 异常。
image.png
为解决此问题,早期的操作系统 大多会在SysTick 异常中 检测当前是否有中断在活跃中,只有没有任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。
然而,这种方法的弊端在于, 它可能把任务切换动作拖延很久(因为如果抢占了IRQ,则本次SysTick 在执行后不得作上下文切换,只能等待下一次SysTick 异常),尤其是当某中断源的频率和SysTick 异常的频率比较接近时,会发生“共振”。 现在好了,PendSV 来完美解决这个问题了(产生SysTick 异常时正在响应一个中断,SysTick 异常会抢占其ISR。此时,操作系统 不可以执行上下文切换,否则将使中断请求被延迟):
把PendSV 编程为最低优先级的异常,PendSV 异常会自动延迟上下文切换的请求,直到其它的ISR 都完成了处理后才放行。 如果操作系统 检测到某IRQ 正在活动并且被SysTick 抢占,它将悬起一个PendSV 异常,以便缓期执行上下文切换。如图7.17 所示
image.png

 

流水账记录如下:
1. 任务 A 呼叫SVC 来请求任务切换(例如,等待某些工作完成)
2. OS 接收到请求,做好上下文切换的准备,并且pend 一个PendSV 异常。
3. 当 CPU 退出SVC 后,它立即进入PendSV,从而执行上下文切换。
4. 当 PendSV 执行完毕后,将返回到任务B,同时进入线程模式。
5. 发生了一个中断,并且中断服务程序开始执行
6. 在 ISR 执行过程中,发生SysTick 异常,并且抢占了该ISR。
7. OS 执行必要的操作,然后pend 起PendSV 异常以作好上下文切换的准备。
8. 当 SysTick 退出后,回到先前被抢占的ISR 中,ISR 继续执行
9. ISR 执行完毕并退出后,PendSV 服务例程开始执行,并且在里面执行上下文切换
10. 当 PendSV 执行完毕后,回到任务A,同时系统再次进入线程模式。

 

部分资料来源:网络

点赞  2020-8-26 21:39

让调度器切换任务

点赞  2020-8-26 21:48

根据官方教程

portYIELD_FROM_ISR()是用于ISR请求任务切换的宏,在较新的端口中还提供了portEND_SWITCHING_ISR()。

如果这个宏的参数位pdFALSE(zero),那么任务切换将不被请求,如果为pdTURE则请求任务切换,并且处于运行状态的任务可能切换,中断将总会返回到这个运行状态的任务,即使处于中断执行时运行状态的任务也会切换。

点赞  2020-8-26 22:09

portYIELD_FROM_ISR()在任务中请求上下文切换。用于在中断任务执行完后,进行一次任务切换。

点赞  2020-8-26 22:11

该宏函数主要用于上下文切换的申请,本质上是通过操作中断控制和状态寄存器(ICSR)的相关位置实现 PendSV 异常;然后再PendSV 异常中设置其他任务的上下文返回,这样在高优先级异常处理完毕后会立即进入PendSV 异常,该异常又将开启新的任务

 

君应有语,渺万里层云,千山暮雪,知向谁边?
点赞  2020-8-26 23:05
引用: abc9981 发表于 2020-8-26 21:39 源码中宏定义 #define portNVIC_INT_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000ed04 ) ) #defi ...

点赞  2020-8-27 08:18

作者cruelfox相关解读:https://bbs.eeworld.com.cn/thread-1141050-1-1.html

点赞  2020-10-13 11:19
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复