当前位置: 首页 > news >正文

FreeRTOS - 中断管理

1. 临界段保护

临界段:一段在执行的时候不能被中断的代码段。
什么情况下临界段会被打断?一个是系统调度,一个是外部中断。
在FreeRTOS中,系统调度,最终产生 PendSV中断,在PendSV Handler里面实现任务的切换,所以还是可以归结为中断。

FreeRTOS对临界段的保护最终还是回到对中断的开和关的控制。

1.2 特殊寄存器

特殊寄存器未经过存储器映射,可以使用MSR和MRS等特殊寄存器访问指令来进行访问。

MRS <reg>,<special_reg> ; 将特殊寄存器读入寄存器
MSR <special_reg>,<reg> ; 写入特殊寄存器

  1. Program Status Register (xPSR)程序状态寄存器,用于保存结果
    • APSR:Application PSR,应用PSR
    • EPSR:Exectution PSR,执行PSR
    • IPSR:Interrupt PSR,中断PSR

  1. 中断 / 异常屏蔽寄存器
  • PRIMASK、FAULTMASK、BASEPRI都用于异常或中断屏蔽;
  • 每个异常(包含中断)都具有一个优先等级,数值小的优先级高,数值大的则优先级低。
  • PRIMASK、FAULTMASK、BASEPRI可基于优先等级屏蔽异常,只有在特权访问等级才可以对它们进行操作

a. PRIMASK:

  • 用于禁止除 NMI 和 HardFault外的所有异常,实际上是将当前优先级改为 0 (最高的可编程等级)
  • 当 PRIMASK 置位时, 所有的错误事件都会触发 HardFault异常,而不论相应的可配置错误异常(如 MemManage、总线错误和使用错误)是否使能。
  • 汇编编程中,可利用 CPS (修改处理器状态)指令修改 PRIMASK寄存器的数值。
CPSIE I ; 清除 PRIMASK (开中断)
CPSID I ; 设置 PRIMASK (关中断)
  • 也可以通过 MRS 和 MSR指令访问
MOVS R0, #1
MSR PRIMASK, R0 ;1 写入 PRIMASK 禁止所有中断MOVS R0, #0
MSR PRIMASK, R0 ;0 写入 PRIMASK 使能中断

b. FAULTMASK

  • 将当前优先级修改为 -1,将 hardFault也会被屏蔽。
  • 当 FAULTMASK置位时,只有 NMI异常处理才能执行。
  • 将配置的错误处理(如 MemManage、总线错误和使用错误)的优先级提升到 -1,这样这些处理就可以使用 HardFault的一些特殊特性。
  • 汇编编程中,可利用 CPS (修改处理器状态)指令修改 PRIMASK寄存器的数值。
CPSIE F ; 清除 FAULTMASK (开异常)
CPSID F ; 设置 FAULTMASK (关异常)
  • 也可以通过 MRS 和 MSR指令访问
MOVS R0, #1
MSR FAULTMASK, R0 ;1 写入 FAULTMASK 禁止所有中断MOVS R0, #0
MSR FAULTMASK, R0 ;0 写入 FAULTMASK 使能中断
  • 若在低优先级的异常处理中触发一个高优先级的异常(NMI除外),但想在低优先级处理完成后再进行高优先级的处理:
    • 设置 FAULTMASK 禁止所有中断和异常(NMI异常除外)
    • 设置高优先级中断或异常的挂起状态;
    • 退出处理。

在FAULTMASK置位时,挂起的高优先级异常处理无法执行,高优先级的异常就会在FAULTMASK被清除前继续保持挂起状态,低优先级处理完成后才会将其清除。因此,可以强制让高优先级处理在低优先级处理结束后开始执行。

c. BASEPRI
- 用于禁止低于某个特定等级的中断。
- 也可以通过 MRS 和 MSR指令访问

MOVS R0, #0x60
MSR BSEPRI, R0 ; 禁止所有中断优先级在 0x60~0xFF间的中断MOVS R0, #0x0
MSR BSEPRI, R0 ; 取消 BSEPRI屏蔽
  1. 处理器控制 CONTROL

定义了:

  • 栈指针的选择(主栈指针 / 进程栈指针)
  • 线程模式的访问等级(特权 / 非特权)
1.2 Cortex-M内核快速关中断指令

在 FreeRTOS 中,对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽,不受FreeRTOS 管理。

1.3 关中断

FreeRTOS 关中断的函数在 portmacro.h 中定义,分不带返回值和带返回值两种 。

#define portDISABLE_INTERRUPTS()				vPortRaiseBASEPRI()#define portSET_INTERRUPT_MASK_FROM_ISR()		ulPortRaiseBASEPRI()

1.4 开中断
#define portENABLE_INTERRUPTS()					vPortSetBASEPRI( 0 )#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x)	vPortSetBASEPRI(x)

1.5 进入临界段
1.5.1 不带中断保护版本,不能嵌套

1.5.2 带中断保护版本,可以嵌套

1.6 退出临界段
1.6.1 不带中断保护版本,不能嵌套

1.6.2 带中断保护版本,可以嵌套

1.7 进入临界段 / 退出临界段总结

在 taskENTER_CRITICA() / taskEXIT_CRITICAL()之间:

  • 低优先级的中断被屏蔽了:优先级 >= configMAX_SYSCALL_INTERRUPT_PRIORITY
  • 高优先级的中断可以产生:优先级 < configMAX_SYSCALL_INTERRUPT_PRIORITY
    • 但是,这些中断 ISR 里,不允许使用 FreeRTOS 的 API 函数
  • 任务调度依赖于中断、依赖于 API 函数,所以:这两段代码之间,不会有任务调度产生

taskENTER_CRITICA()/taskEXIT_CRITICAL()宏,是可以递归使用的,它的内部会记录嵌套的深度,只有嵌套深度变为0时,调用 taskEXIT_CRITICAL() 才会重新使能中断

使用taskENTER_CRITICA()/taskEXIT_CRITICAL()来访问临界资源是很粗鲁的方法:

  • 中断无法正常运行
  • 任务调度无法进行
  • 所以,之间的代码要尽可能快速地执行

2. 暂停调度器

如果有别的任务来跟你竞争临界资源,你可以把中断关掉:这当然可以禁止别的任务运行,但是这代价太大了。它会影响到中断的处理。

如果只是禁止别的任务来跟你竞争,不需要关中断,暂停调度器就可以了:在这期间, 中断还是可以发生、处理。

/* 暂停调度器 */
void vTaskSuspendAll( void );/* 恢复调度器
* 返回值: pdTRUE 表示在暂定期间有更高优先级的任务就绪了
* 可以不理会这个返回值
*/
BaseType_t xTaskResumeAll( void );

3. 中断

3.1 系统异常

  • Cortex-M处理器具有多个用于中断和异常管理的可编程寄存器,这些寄存器多数位于NVIC和系统控制块 (SCB)中。
  • 处理器内核中还有用于中断屏蔽的寄存器 PRIMASK、FAULTMASK 和 BASEPRI。
  • NVIC 和SCB 位于系统控制空间 SCS,地址从 0xE000E000开始,大小为 4KB。
  • SCS 中还有 SysTick定时器、存储器保护单元 MPU以及用于调试的寄存器。
  • 该地址区域中基本上所有的寄存器都只能由运行在特权访问等级的代码访问。

3.2 向量表和向量表重定位

向量表重定位特性提供了一个名为向量表偏移寄存器 (VTOR)的 可编程寄存器。该寄存器将正在使用的存储器的起始地址定义为向量表。

  1. 具有Bootloader的设备

有些微控制器具有多个程序存储器:启动ROM和用户Flash存储器。微控制器生产商一般会将Bootloader预先写到启动ROM中,这样在微控制器启动时,启动ROM中的Bootloader就会首先执行,而且在跳转到用户Flash的应用程序前,VTOR会被设置为指向用户Flash存储器的开始处,因此会使用用户Flash中的向量表。

  1. 应用程序加载到RAM

有些情况下,应用程序可能会被从外部设备加载到RAM中执行,它可能会位于SD卡中,或者甚至需要通过网络传输。在这种情况下,存储在片上存储器中用于启动的程序需要初始化一些硬件、复制位于外部设备中的应用程序到RAM、更新VTOR后执行存储在外部的程序。

3.3 中断的具体行为
3.3.1 中断/异常的响应序列

当CM3开始响应一个中断时,会在它看不见的体内奔涌起三股暗流:

  • 入栈: 把8个寄存器的值压入栈
  • 取向量:从向量表中找出对应的服务程序入口地址
  • 选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC
3.3.1.1 入栈:

响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR,PC,LR,R12以及 R3‐R0由硬件自动压入适当的堆栈中:如果当响应异常时,当前的代码正在使用PSP,则压入 PSP,即使用线程堆栈;否则压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用 主堆栈。

假设入栈开始时,SP的值为N,则在入栈后,堆栈内部的变化如表9.1表示。又因为AHB 接口上的流水线操作本质,地址和数据都在经过一个流水线周期之后才进入。另外,这种入栈在机器的内部,并不是严格按堆栈操作的顺序的——但是机器会保证:正确的寄存器将被保存到正确的位置,如图9.1和表9.1的第3列所示。

  • 先把PC与xPSR的值保存,就可以更早地启动服务例程指令的预取——因为这需要修改PC;同时,也做到了在早期就可以更新xPSR中IPSR位段的值。
  • R0‐R3、 R12是最后被压进去的:为的是可以更容易地使用SP基址来索引寻址,(以及为了LDM等多重加载指令,因为LDM必 须加载地址连续的一串数据)。参数的传递也是受益者:使之可以方便地通过压入栈的R0‐R3 取出(主要为系统软件所利用,多见于SVC与PendSV中的参数传递)。
3.3.1.2 取向量:
  • 当数据总线(系统总线)正在为入栈操作而忙得团团转时,
  • 指令总线(I‐Code总线)——它正在为响应中断紧张有序地执行另一项重要的任务:从向量表中找出正确的异常向量,然后在服务程序的入口处预取指。
  • 由此可以看到各自都有专用总线的好处:入栈与取指这两个工作能同时进行。
3.3.1.3 更新寄存器

在入栈和取向量的工作都完毕之后,执行服务例程之前,还要更新一系列的寄存器:

  • SP:在入栈中会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程后, 将由MSP负责对堆栈的访问。
  • PSR:IPSR位段(地处PSR的最低部分)会被更新为新响应的异常编号。
  • PC:在向量取出完毕后,PC将指向服务例程的入口地址,
  • LR:LR的用法将被重新解释,其值也被更新成一种特殊的值,称为“EXC_RETURN”, 并且在异常返回时使用。EXC_RETURN的二进制值除了最低4位外全为1,而其最低4 位则有另外的含义(后面讲到,见表9.3和表9.4)。

在启动了中断返回序列后,下述的处理就将进行:

  1. 出栈:先前压入栈中的寄存器在这里恢复。内部的出栈顺序与入栈时的相对应,堆栈指 针的值也改回去。
  2. 更新NVIC寄存器:伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若 中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始。
3.3.2 异常返回值

在进入异常服务程序后,LR的值被自动更新为特殊的EXC_RETURN,这是一个高28位全为1的值,只有[3:0]的值有特殊含义,如表9.3所示。当异常服务例程把这个值送往PC时,就会启动处理器的中断返回序列。因为LR的值是由CM3自动设置的,所以只要没有特殊需求,就不要改动它。

4. 中断管理

4.1 和中断相关的名词解释
  • 中断号:每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。
  • 中断请求:“紧急事件”需向 CPU 提出申请,要求 CPU 暂停当前执行的任务,转而处理该“紧急事件”,这一申请过程称为中断请求。
  • 中断优先级:为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级。
  • 中断处理程序:当外设产生中断请求后,CPU 暂停当前的任务,转而响应中断申请,即执行中断处理程序。
  • 中断触发:中断源发出并送给 CPU 控制信号,将中断触发器置“1”,表明该中断源产生了中断,要求 CPU 去响应该中断,CPU 暂停当前任务,执行相应的中断处理程序。
  • 中断触发类型:外部中断申请通过一个物理信号发送到 NVIC,可以是电平触发或边沿触发。
  • 中断向量:中断服务程序的入口地址。
  • 中断向量表:存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量 表中按照中断号顺序存储。
  • 临界段:代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。
4.2 中断管理的运作机制

当中断产生时,处理机将按如下的顺序执行:

  • 保存当前处理机状态信息
  • 载入异常或中断处理函数到 PC寄存器
  • 把控制权转交给处理函数并开始执行
  • 当处理函数执行完成时,恢复处理器状态信息
  • 从异常或中断中返回到前一个程序执行点

中断使得 CPU 可以在事件发生时才给予处理,而不必让 CPU 连续不断地查询是否有相应的事件发生。通过两条特殊指令:关中断和开中断可以让处理器不响应或响应中断, 在关闭中断期间,通常处理器会把新产生的中断挂起,当中断打开时立刻进行响应,所以 会有适当的延时响应中断,故用户在进入临界区的时候应快进快出。

中断发生的环境有两种情况:在任务的上下文中,在中断服务函数处理上下文中 。

  • 任务在工作的时候,如果此时发生了一个中断,无论中断的优先级是多大,都会 打断当前任务的执行,从而转到对应的中断服务函数中执行,其过程具体见图 24-1。

  • 在执行中断服务例程的过程中,如果有更高优先级别的中断源触发中断,由于当 前处于中断处理上下文环境中,根据不同的处理器构架可能有不同的处理方式, 比如新的中断等待挂起直到当前中断处理离开后再行响应;或新的高优先级中断 打断当前中断处理过程,而去直接响应这个更高优先级的新中断源。后面这种情 况,称之为中断嵌套。在硬实时环境中,前一种情况是不允许发生的,不能使响 应中断的时间尽量的短。而在软件处理(软实时环境)上,FreeRTOS 允许中断嵌 套,即在一个中断服务例程期间,处理器可以响应另外一个优先级更高的中断, 过程如图 24-2 所示。

4.3 中断延迟

中断延迟:

  • 是指从硬件中断发生到开始执行中断处理程序第一条指令之间的这段时间。
  • 也就是:系统接收到中断信号到操作系统作出响应,并完成换到转入中断服务程序的时间。也可以简单地理解为:(外部)硬件(设备)发生中断,到系统执行中断服务子程序(ISR)的第一条指令的时间。

中断的处理过程是:

  • 识别中断时间:外界硬件发生了中断后,CPU 到中断处理器读取中断向量,并且查找中断向量表,找到对应的中断服务子程序(ISR)的首地址,然后跳转到对应的 ISR 去做相应处理。这部分时间为:识别中断时间
  • 等待中断打开时间:在允许中断嵌套的实时操作系统中,中断也是基于优先级的,允许高优先级中断抢断正在处理的低优先级中断,所以,如果当前正在处理更高优先级的中断,即使此时有低优先级的中断,也系统不会立刻响应,而是等到高优先级的中断处理完之后,才会响应。而即使在不支持中断嵌套,即中断是没有优先级的,中断是不允许被中断的,所以,如果当前系统正在处理一个中断,而此时另一个中断到来了,系统也是不会立即响应的,而只是等处理完当前的中断之后,才会处理后来的中断。此部分时间为:等待中断打开时间
  • 关闭中断时间:在操作系统中,很多时候我们会主动进入临界段,系统不允许当前状态被中断打断, 故而在临界区发生的中断会被挂起,直到退出临界段时候打开中断。此部分时间,我称其为:关闭中断时间

中断延迟可以定义为:

  • 从中断开始的时刻到中断服务例程开始执行的时刻之间的时间段。
  • 中断延迟 = 识别中断时间 + [等待中断打开时间] + [关闭中断时间]。
  • 注意:“[ ]”的时间是不一定都存在的,此处为最大可能的中断延迟时间。
4.4 中断和任务

假设当前系统正在运行Task1时,用户按下了按键,触发了按键中断。这个中断的处理流程如下:

  • CPU跳到固定地址去执行代码,这个固定地址通常被称为中断向量,这个跳转是硬件实现
  • 执行代码做什么?
    • 保存现场:Task1被打断,需要先保存Task1的运行环境,比如各类寄存器的值
    • 分辨中断、调用处理函数(这个函数就被称为ISR,interrupt service routine)
    • 恢复现场:继续运行Task1,或者运行其他优先级更高的任务

你要注意到,ISR是在内核中被调用的,ISR执行过程中,用户的任务无法执行。ISR要尽量快,否则:

  • 其他低优先级的中断无法被处理:实时性无法保证
  • 用户任务无法被执行:系统显得很卡顿

如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为2部分:

  • ISR:尽快做些清理、记录工作,然后触发某个任务
  • 任务:更复杂的事情放在任务中处理
  • 所以:需要ISR和任务之间进行通信

要在FreeRTOS中熟练使用中断,有几个原则要先说明:

  • FreeRTOS把任务认为是硬件无关的,任务的优先级由程序员决定,任务何时运行由调度器决定
  • ISR虽然也是使用软件实现的,但是它被认为是硬件特性的一部分,因为它跟硬件密切相关
    • 何时执行?由硬件决定
    • 哪个ISR被执行?由硬件决定
  • ISR的优先级高于任务:即使是优先级最低的中断,它的优先级也高于任务。任务只有在没有中断的情况下,才能执行。
4.5 两类中断

用 户 可 以 自 定 义 配 置 系 统 可 管 理 的 最 高 中 断 优 先 级 的 宏 定 义 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY, 它是用于配置内核中的basepri 寄存器的,当 basepri 设置为某个值的时候,NVIC 不会响应比该优先级低的中断, 而优先级比之更高的中断则不受影响。

  • 就是说当这个宏定义配置为 5 的时候,中断优先级数值在 0、1、2、3、4 的这些中断是不受 FreeRTOS 屏蔽的,也就是说即使在系统进入临界段的时候,这些中断也能被触发而不是等到退出临界段的时候才被触发,当然,这些中断服务函数中也不能调用 FreeRTOS 提供的 API 函数接口。
  • 而中断优先级在 5 到 15 的这些中断是可以被屏蔽的,也能安全调用 FreeRTOS 提供的 API 函数接口。

4.6 中断优先级

ARM Cortex-M NVIC 支持中断嵌套功能:当一个中断触发并且系统进行响应时,处理 器硬件会将当前运行的部分上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR, R0,R1,R2,R3 以及 R12 寄存器。当系统正在服务一个中断时,如果有一个更高优先级的中断触发,那么处理器同样的会打断当前运行的中断服务例程,然后把老的中断服务例程上下文的 PSR,R0,R1,R2,R3 和 R12 寄存器自动保存到中断栈中。这些部分上下文 寄存器保存到中断栈的行为完全是硬件行为,这一点是与其他 ARM 处理器最大的区别 (以往都需要依赖于软件保存上下文)。

4.7 两套API函数
4.7.1 为什么需要两套API

FreeRTOS中很多API函数都有两套:一套在任务中使用,另一套在ISR中使用。后者的函数名含有"FromISR"后缀。

为什么要引入两套API函数?

  • 很多API函数会导致任务计入阻塞状态
    • 运行这个函数的 任务 进入阻塞状态
    • 比如写队列时,如果队列已满,可以进入阻塞状态等待一会
  • ISR调用API函数时,ISR不是"任务",ISR不能进入阻塞状态
  • 所以,在任务中、在ISR中,这些函数的功能是有差别的

FreeRTOS使用两套函数,而不是使用一套函数,是因为有如下好处:

  • 使用同一套函数的话,需要增加额外的判断代码、增加额外的分支,使得函数更长、更复杂、难以测试
  • 在任务、ISR中调用时,需要的参数不一样,比如:
    • 在任务中调用:需要指定超时时间,表示如果不成功就阻塞一会
    • 在ISR中调用:不需要指定超时时间,无论是否成功都要即刻返回
    • 如果强行把两套函数揉在一起,会导致参数臃肿、无效
  • 移植FreeRTOS时,还需要提供监测上下文的函数,比如 is_in_isr()
  • 有些处理器架构没有办法轻易分辨当前是处于任务中,还是处于ISR中,就需要额外添加更多、更复杂的代码

使用两套函数可以让程序更高效,但是也有一些缺点,比如你要使用第三方库函数时,即会在任务中调用它,也会在ISR总调用它。这个第三方库函数用到了FreeRTOS的API函数,你无法修改库函数。这个问题可以解决:

  • 把中断的处理推迟到任务中进行(Defer interrupt processing),在任务中调用库函数
  • 尝试在库函数中使用"FromISR"函数:
    • 在任务中、在ISR中都可以调用"FromISR"函数
    • 反过来就不行,非FromISR函数无法在ISR中使用
  • 第三方库函数也许会提供OS抽象层,自行判断当前环境是在任务还是在ISR中,分别调用不同的函数
4.7.2 两套API函数列表
类型在任务中在ISR中
队列(queue)xQueueSendToBackxQueueSendToBackFromISR
xQueueSendToFrontxQueueSendToFrontFromISR
xQueueReceivexQueueReceiveFromISR
xQueueOverwritexQueueOverwriteFromISR
xQueuePeekxQueuePeekFromISR
信号量(semaphore)xSemaphoreGivexSemaphoreGiveFromISR
xSemaphoreTakexSemaphoreTakeFromISR
事件组(event group)xEventGroupSetBitsxEventGroupSetBitsFromISR
xEventGroupGetBitsxEventGroupGetBitsFromISR
任务通知(task notification)xTaskNotifyGivevTaskNotifyGiveFromISR
xTaskNotifyxTaskNotifyFromISR
软件定时器(software timer)xTimerStartxTimerStartFromISR
xTimerStopxTimerStopFromISR
xTimerResetxTimerResetFromISR
xTimerChangePeriodxTimerChangePeriodFromISR
4.7.3 xHigherPriorityTaskWoken参数

xHigherPriorityTaskWoken的含义是:是否有更高优先级的任务被唤醒了。如果为pdTRUE,则意味着后面要进行任务切换。

还是以写队列为例。

xQueueSendToBackxQueueSendToBackFromISR
参数不同xTicksToWait: 队列满的话阻塞多久没有xTicksToWait
唤醒等待的任务写队列后,会唤醒等待数据的任务写队列后,会唤醒等待数据的任务
调度如果被唤醒的任务优先级更高,即刻调度如果被唤醒的任务优先级更高,不会调度 只是记录下来表示:需要调度
阻塞如果队列满,可以阻塞+如果队列满,不能阻塞

任务A调用 xQueueSendToBack() 写队列,有几种情况发生:

  • 队列满了,任务A阻塞等待,另一个任务B运行
  • 队列没满,任务A成功写入队列,但是它导致另一个任务B被唤醒,任务B的优先级更高:任务B先运行
  • 队列没满,任务A成功写入队列,即刻返回

可以看到,在任务中调用API函数可能导致任务阻塞、任务切换,这叫做"context switch",上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换。

xQueueSendToBackFromISR() 函数也可能导致任务切换,但是不会在函数内部进行切换,而是返回一个参数:表示是否需要切换,函数原型与用法如下:

/* * 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞*/
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,const void *pvItemToQueue,BaseType_t *pxHigherPriorityTaskWoken);/* 用法示例 */BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);if (xHigherPriorityTaskWoken == pdTRUE)
{/* 任务切换 */    
}

pxHigherPriorityTaskWoken参数,就是用来保存函数的结果:是否需要切换

  • *pxHigherPriorityTaskWoken等于pdTRUE:函数的操作导致更高优先级的任务就绪了,ISR应该进行任务切换
  • *pxHigherPriorityTaskWoken等于pdFALSE:没有进行任务切换的必要

为什么不在"FromISR"函数内部进行任务切换,而只是标记一下而已呢?为了效率!示例代码如下:

void XXX_ISR()
{int i;for (i = 0; i < N; i++){xQueueSendToBackFromISR(...); /* 被多次调用 */}
}

ISR中有可能多次调用"FromISR"函数,如果在"FromISR"内部进行任务切换,会浪费时间。解决方法是:

  • 在"FromISR"中标记是否需要切换
  • 在ISR返回之前再进行任务切换
  • 示例代码如下
void XXX_ISR()
{int i;BaseType_t xHigherPriorityTaskWoken = pdFALSE;for (i = 0; i < N; i++){xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */}/* 最后再决定是否进行任务切换 */if (xHigherPriorityTaskWoken == pdTRUE){/* 任务切换 */    }
}

上述的例子很常见,比如UART中断:在UART的ISR中读取多个字符,发现收到回车符时才进行任务切换。

在ISR中调用API时不进行任务切换,而只是在"xHigherPriorityTaskWoken"中标记一下,除了效率,还有多种好处:

  • 效率高:避免不必要的任务切换
  • 让ISR更可控:中断随机产生,在API中进行任务切换的话,可能导致问题更复杂
  • 可移植性
  • 在Tick中断中,调用 vApplicationTickHook() :它运行与ISR,只能使用"FromISR"的函数

使用"FromISR"函数时,如果不想使用xHigherPriorityTaskWoken参数,可以设置为NULL。

4.7.4 怎么切换任务

FreeRTOS的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);
}
4.8 中断的延迟处理

前面讲过,ISR要尽量快,否则:

  • 其他低优先级的中断无法被处理:实时性无法保证
  • 用户任务无法被执行:系统显得很卡顿
  • 如果运行中断嵌套,这会更复杂,ISR越快执行约有助于中断嵌套

如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为2部分:

  • ISR:尽快做些清理、记录工作,然后触发某个任务
  • 任务:更复杂的事情放在任务中处理

这种处理方式叫"中断的延迟处理"(Deferring interrupt processing),处理流程如下图所示:

  • t1:任务1运行,任务2阻塞
  • t2:发生中断,
    • 该中断的ISR函数被执行,任务1被打断
    • ISR函数要尽快能快速地运行,它做一些必要的操作(比如清除中断),然后唤醒任务2
  • t3:在创建任务时设置任务2的优先级比任务1高(这取决于设计者),所以ISR返回后,运行的是任务2,它要完成中断的处理。任务2就被称为"deferred processing task",中断的延迟处理任务。
  • t4:任务2处理完中断后,进入阻塞态以等待下一个中断,任务1重新运行

4.9 中断与任务间的通信

前面讲解过的队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。

要注意的是,在ISR中使用的函数要有"FromISR"后缀。

4.10 代码-优化实时性

本节代码为:29_fromisr_game,主要看DshanMCU-F103\driver_ir_receiver.c。

以前,在中断函数里写队列时,代码如下:

假设当前运行的是任务A,它的优先级比较低,在它运行过程中发生了中断,中断函数调用了DispatchKey函数写了队列,使得任务B被唤醒了。任务B的优先级比较高,它应该在中断执行完后马上就能运行。但是上述代码无法实现这个目标,xQueueSendFromISR函数会把任务B调整为就绪态,但是不会发起一次调度。

需要如下修改代码:

150 static void DispatchKey(struct ir_data *pidata)151 {152 #if 0153   extern QueueHandle_t g_xQueueCar1;154   extern QueueHandle_t g_xQueueCar2;155   extern QueueHandle_t g_xQueueCar3;156   xQueueSendFromISR(g_xQueueCar1, pidata, NULL);157    xQueueSendFromISR(g_xQueueCar2, pidata, NULL);158    xQueueSendFromISR(g_xQueueCar3, pidata, NULL);159 #else160    int i;161   BaseType_t xHigherPriorityTaskWoken = pdFALSE;162    for (i = 0; i < g_queue_cnt; i++)163    {164        xQueueSendFromISR(g_xQueues[i], pidata, &xHigherPriorityTaskWoken);165    }166   portYIELD_FROM_ISR(xHigherPriorityTaskWoken);167 #endif168 }

在第164行传入一个变量的地址:&xHigherPriorityTaskWoken,它的初始值是pdFALSE,表示无需发起调度。如果xQueueSendFromISR函数发现唤醒了更高优先级的任务,那么就会把这个变量设置为pdTRUE。

第166行,如果xHigherPriorityTaskWoken为pdTRUE,它就会发起一次调度。

/*  在driver_mpu6050.c  */
void MPU6050_Callback(void)
{BaseType_t xHigherPriorityTaskWoken = pdFALSE;/* 在中断中设置事件组来通过MPU6050控制挡球板的运动 * 在MPU6050任务中,只有当中断写了事件之后,得到事件后,才会来读IIC。*//* 根据原理图 : 中断是通过PB5端口设置的 *//* 设置事件组,bit0*/xEventGroupSetBitsFromISR(g_xEventMPU6050,(1<<0), &xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

本程序上机时,我们感觉不到有什么不同。


http://www.mrgr.cn/news/52281.html

相关文章:

  • Creo7.0软件安装教程+Creo7.0三维建模中文安装包下载
  • Okhttp3中设置超时的方法
  • 如何使用内网穿透工具配置Termux SFTP公网地址实现WinSCP远程连接
  • 纯干货:C语言中函数栈帧的介绍
  • FFmpeg源码:avformat_new_stream函数分析
  • 1.项目初始化
  • Spring Boot 的执行器是什么?
  • Linux Debian12基于ImageMagick图像处理工具编写shell脚本用于常见图片png、jpg、jpeg、tiff格式批量转webp格式
  • 42. 将数值保留两位小数
  • springboot 对接Telegram发送消息
  • 数据结构--链表
  • web APIs
  • 视频格式转换软件哪个好用?7款可靠的视频转换软件测评
  • HubSpot客户平台那些超好用的工具,你get了吗?
  • c高级day4
  • Python干货:良心整理出来Python15个超级库,学习python的小伙伴千万不要错过
  • element-ui 的el-calendar日历组件样式修改
  • 【升华】人工智能10大常用算法与及代码实现(汇总)
  • QTableView 接口详情
  • C语言小游戏--猜数字