FreeRTOS中断与临界区

1、ARM Cortex-M 中断

  1.1 、ARM Cortex-M 中断简介

  中断是微控制器一个很常见的特性,中断一般由硬件产生,当中断产生以后 CPU 就会中断当前的流程转而去处理中断服务, Cortex-M 内核的 MCU 提供了一个用于中断管理的嵌套向量中断控制器(NVIC)。
  Cotex-M3 的 NVIC 最多支持 240 个 IRQ(中断请求)、1 个不可屏蔽中断(NMI)、1 个 Systick(滴答定时器)定时器中断和多个系统异常。

  1.2 、中断优先级管理

  Cortex-M 处理器有多个用于管理中断和异常的可编程寄存器, 这些寄存器大多数都在NVIC 和系统控制块(SCB)中, CMSIS 将这些寄存器定义为结构体。以 STM32F103 为例,打开core_cm3.h,有两个结构体, NVIC_Type 和 SCB_Type,如下:

typedef struct
{
    __IO uint32_t ISER[8]; /*!< Offset: 0x000 Interrupt Set Enable Register */
    uint32_t RESERVED0[24];
    __IO uint32_t ICER[8]; /*!< Offset: 0x080 Interrupt Clear Enable Register */
    uint32_t RSERVED1[24];
    __IO uint32_t ISPR[8]; /*!< Offset: 0x100 Interrupt Set Pending Register */
    uint32_t RESERVED2[24];
    __IO uint32_t ICPR[8]; /*!< Offset: 0x180 Interrupt Clear Pending Register */
    uint32_t RESERVED3[24];
    __IO uint32_t IABR[8]; /*!< Offset: 0x200 Interrupt Active bit Register */
    uint32_t RESERVED4[56];
    __IO uint8_t IP[240]; /*!< Offset: 0x300 Interrupt Priority Register (8Bit wide) */
    uint32_t RESERVED5[644];
    __O uint32_t STIR; /*!< Offset: 0xE00 Software Trigger Interrupt Register */
} NVIC_Type;

typedef struct
{
    __I uint32_t CPUID; /*!< Offset: 0x00 CPU ID Base Register */
    __IO uint32_t ICSR /*!< Offset: 0x04 Interrupt Control State Register */
    __IO uint32_t VTOR; /*!< Offset: 0x08 Vector Table Offset Register */
    __IO uint32_t AIRCR; /*!< Offset: 0x0C Application Interrupt / Reset Control Register */
    __IO uint32_t SCR; /*!< Offset: 0x10 System Control Register */
    __IO uint32_t CCR; /*!< Offset: 0x14 Configuration Control Register */
    __IO uint8_t SHP[12]; /*!< Offset: 0x18 System Handlers Priority Registers (4-7, 8-11, 12-15)*/
    __IO uint32_t SHCSR; /*!< Offset: 0x24 System Handler Control and State Register */
    __IO uint32_t CFSR; /*!< Offset: 0x28 Configurable Fault Status Register */
    __IO uint32_t HFSR; /*!< Offset: 0x2C Hard Fault Status Register */
    __IO uint32_t DFSR; /*!< Offset: 0x30 Debug Fault Status Register */

    __IO uint32_t MMFAR; /*!< Offset: 0x34 Mem Manage Address Register */
    __IO uint32_t BFAR; /*!< Offset: 0x38 Bus Fault Address Register */
    __IO uint32_t AFSR; /*!< Offset: 0x3C Auxiliary Fault Status Register */
    __I uint32_t PFR[2]; /*!< Offset: 0x40 Processor Feature Register */
    __I uint32_t DFR; /*!< Offset: 0x48 Debug Feature Register */
    __I uint32_t ADR; /*!< Offset: 0x4C Auxiliary Feature Register */
    __I uint32_t MMFR[4]; /*!< Offset: 0x50 Memory Model Feature Register */
    __I uint32_t ISAR[5]; /*!< Offset: 0x60 ISA Feature Register */
} SCB_Type;

  NVIC 和 SCB 都位于系统控制空间(SCS)内, SCS 的地址从 0XE000E000 开始, SCB 和 NVIC的地址也在 core_cm3.h 中有定义,如下:

#define SCS_BASE (0xE000E000) /*!< System Control Space Base Address */
#define NVIC_BASE (SCS_BASE + 0x0100) /*!< NVIC Base Address */
#define SCB_BASE (SCS_BASE + 0x0D00) /*!< System Control Block Base Address */
#define SCB ((SCB_Type * ) SCB_BASE ) /*!< SCB configuration struct */
#define NVIC ((NVIC_Type* ) NVIC_BASE ) /*!< NVIC configuration struct *//

  1.3、优先级分组定义

  当多个中断来临的时候处理器应该响应哪一个中断是由中断的优先级来决定的,高优先级的中断(优先级编号小)肯定是首先得到响应,而且高优先级的中断可以抢占低优先级的中断,这个就是中断嵌套Cortex-M 处理器的有些中断是具有固定的优先级的,比如复位、 NMI、HardFault,这些中断的优先级都是负数,优先级也是最高的。

  Cortex-M 处理器有三个固定优先级和 256 个可编程的优先级。最多有 128 个抢占等级,但是实际的优先级数量是由芯片厂商来决定的。但是,绝大多数的芯片都会精简设计的,以致实际上支持的优先级数会更少,如 8 级、 16 级、 32 级等, 比如 STM32 就只有 16 级优先级。在设计芯片的时候会裁掉表达优先级的几个低端有效位,以减少优先级数,所以不管用多少位来表达优先级,都是 MSB 对齐的,如图 4.1.3.1 就是使用三位来表达优先级。

  在图中, Bit0~Bit4 没有实现,所以读它们总是返回零,写如它们的话则会忽略写入的值。因此,对于 3 个位的情况,可是使用的优先级就是 8 个: 0X00(最高优先级)、 0X20、 0X40、0X60、 0X80、 0XA0、 0XC0 和 0XE0。 注意,这个是芯片厂商来决定的!不是我们能决定的,比如 STM32 就选择了 4 位作为优先级

  在 NVIC 的相关结构体中, 成员变量 IP 用于配置外部中断的优先级, 成员变量 IP 的定义如下所示:

__IO uint8_t IP[240];        /* 中断优先级寄存器 ,设置每个中断的优先级*/

  可以看到成员变量 IP 是一个 uint8_t 类型的数组, 数组一共有 240 个元素, 数组中每一个8bit 的元素就用来配置对应的外部中断的优先级。

  综上可知, ARM Cortex-M 使用了 8 位宽的寄存器来配置中断的优先等级, 这个寄存器就是中断优先级配置寄存器, 因此最大中断的优先级配置范围位 0~255。 但是芯片厂商一般用不完这些资源, 对于 STM32, 只用到了中断优先级配置寄存器的高 4 位[7:4], 低四位[3:0]取零处理, 因此 STM32 提供了最大 2^4=16 级的中断优先等级,即4位配置的优先级最大只能为16, 如下图所示:

image

  中断优先级配置寄存器的值与对应的优先等级成反比, 即中断优先级配置寄存器的值越小,中断的优先等级越高。

  有读者可能就会问,优先级配置寄存器是 8 位宽的,为什么却只有 128 个抢占等级? 8 位不应该是 256 个抢占等级吗?为了使抢占机能变得更可控, Cortex-M 处理器还把 256 个优先级按位分为高低两段:抢占优先级(分组优先级)和亚优先级(子优先级)。
  抢占优先级和子优先级的区别如下:抢占优先级: 抢占优先级高的中断可以打断正在执行但抢占优先级低的中断, 即中断嵌套。子优先级: 抢占优先级相同时, 子优先级高的中断不能打断正在执行但子优先级低的中的中断, 即子优先级不支持中断嵌套。

   NVIC 中有一个寄存器是“应用程序中断及复位控制寄存器(AIRCR)”, AIRCR 寄存器里面有个位段名为“优先级组”,如下表所示:

   表 4.1.3.1 中 PRIGROUP 就是优先级分组, 它把优先级分为两个位段: MSB 所在的位段(左边的)对应抢占优先级, LSB 所在的位段(右边的)对应亚优先级,如表 4.1.3.2 所示。

  STM32 中每个中断的优先级就由抢占优先级和子优先级共同组成, 使用中断优先级配置寄存器的高 4 位来配置抢占优先级和子优先级, 那么中断优先级配置寄存器的高 4 位是如何分配设置抢占优先级和子优先级的呢? 一共由 5 种分配方式, 对应这中断优先级分组的 5 个组, 优先级分组的 5 种分组情况在 HAL 中进行了定义, 如下所示:

  在看一下 STM32 的优先级分组情况,我们前面说了 STM32 使用了 4 位,因此最多有 5 组优先级分组设置,这 5 个分组在 msic.h 中有定义,如下:

#define NVIC_PriorityGroup_0 ((uint32_t)0x700) /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PriorityGroup_1 ((uint32_t)0x600) /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PriorityGroup_2 ((uint32_t)0x500) /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) /*!< 4 bits for pre-emption priority
0 bits for subpriority */
  这4个寄存器位的分组方式如下:
第0组:所有4位用于指定响应优先级
第1组:最高1位用于指定抢占式优先级,最低3位用于指定响应优先级
第2组:最高2位用于指定抢占式优先级,最低2位用于指定响应优先级
第3组:最高3位用于指定抢占式优先级,最低1位用于指定响应优先级
第4组:所有4位用于指定抢占式优先级
  可以通过调用STM32的固件库中的函数NVIC_PriorityGroupConfig()选择使用哪种优先级分组方式,这个函数的参数有下列5种:
NVIC_PriorityGroup_0 => 选择第0组
NVIC_PriorityGroup_1 => 选择第1组
NVIC_PriorityGroup_2 => 选择第2组
NVIC_PriorityGroup_3 => 选择第3组
NVIC_PriorityGroup_4 => 选择第4组

  优先级分组对应的抢占优先级和子优先级分配方式如下表所示:

image

  FreeRTOS 的中断配置没有处理亚优先级这种情况,所以只能配置为组 4,直接就 16 个优先级,使用起来也简单!

  1.4、三个系统中断优先级配置寄存器

  每个外部中断都有一个对应的优先级寄存器,每个寄存器占 8 位,因此最大宽度是 8 位,但是最小为 3 位。 4 个相临的优先级寄存器拼成一个 32 位寄存器。如前所述,根据优先级组的设置,优先级又可以分为高、低两个位段,分别抢占优先级和亚优先级。 STM32 我们已经设置位组 4,所以就只有抢占优先级了。优先级寄存器都可以按字节访问,当然也可以按半字/字来访问,有意义的优先级寄存器数目由芯片厂商来实现,如表 4.1.4.1 和 4.1.4.2 所示:

   上面说了,4个相临的寄存器可以拼成一个32位的寄存器,因此地址0xE000_ED20~0xE000_ED23 这四个寄存器就可以拼接成一个地址为 0xE000_ED20 的 32 位寄存器。 这一点很重要! 因为 FreeRTOS 在设置 PendSV 和 SysTick 的中断优先级的时候都是直接操作的地址 0xE000_ED20。

  除了外部中断, 系统中断有独立的中断优先级配置寄存器, 分别为 SHPR1、 SHPR2、 SHPR3,
  下面就分别来看一下这三个寄存器的作用。
  (1)SHPR1
  SHPR1 寄存器的地址为 0xE000ED18, 用于配置 MemManage、 BusFault、 UsageFault 的中断优先级, 各比特位的功能描述如下表所示:

image

  (2)SHPR2
  SHPR2 寄存器的地址为 0xE000ED1C, 用于配置 SVCall 的中断优先级, 各比特位的功能描述如下表所示:

image

  (3)SHPR3
  SHPR3 寄存器的地址为 0xE000ED20, 用于配置 PendSV、 SysTick 的中断优先级, 各比特位的功能描述如下表所示:

image

   FreeRTOS 在配置 PendSV SysTick 中断优先级的时, 就使用到了 SHPR3 寄存器, 因此请读者多留意此寄存器。

  1.5、 三个用于中断屏蔽的特殊寄存器(PRIMASK、FAULTMASK 和 BASEPR)

  ARM Cortex-M 有三个用于屏蔽中断的寄存器, 分别为 PRIMASK、 FAULTMASK 和BASEPRI, 下面就分别来看一下这三个寄存器的作用。

  (1)、 PRIMASK 寄存器

  作用: PRIMASK 寄存器有 32bit, 但只有 bit0 有效, 是可读可写的, 将 PRIMASK 寄存器设置为 1 用于屏蔽除 NMI 和 HardFault 外的所有异常和中断, 将 PRIMASK 寄存器清 0 用于使能中断。

  在许多应用中,需要暂时屏蔽所有的中断一执行一些对时序要求严格的任务,这个时候就可以使用 PRIMASK 寄存器, PRIMASK 用于禁止除 NMI 和 HardFalut 外的所有异常和中断,汇编编程的时候可以使用 CPS(修改处理器状态)指令修改 PRIMASK 寄存器的数值:

//用法1
CPSIE I; //清除 PRIMASK(使能中断)
CPSID I; //设置 PRIMASK(禁止中断)

  PRIMASK 寄存器还可以通过 MRS 和 MSR 指令访问,如下:

//用法二:
MRS R0, PRIMASK /* 读取 PRIMASK 值 */

MOV R0, #0
MSR PRIMASK, R0 /* 清除 PRIMASK(使能中断) */

MOV R0, #1
MSR PRIMASK, R0 /* 设置 PRIMASK(屏蔽中断) */

  还可以调用函数:

//用法三:
__get_PRIMASK(); /* 读取 PRIMASK 值 */
__set_PRIMASK(0U); /* 清除 PRIMASK(使能中断) */
__set_PRIMASK(1U); /* 设置 PRIMASK(屏蔽中断) */

  UCOS 中的临界区代码代码保护就是通过开关中断实现的,而开关中断就是直接操作 PRIMASK寄存器的,所以在 UCOS 中关闭中断的时候时关闭了除复位、 NMI 和 HardFault 以外的所有中断!

  (2)、FAULTMASK寄存器

  作用: FAULTMASK 寄存器有 32bit, 但只有 bit0 有效, 也是可读可写的, 将 FAULTMASK寄存器设置为 1 用于屏蔽除 NMI 外的所有异常和中断, 将 FAULTMASK 寄存器清零用于使能中断。
  所以FAULTMASK 比 PRIMASK 更狠, 它可以连 HardFault 都屏蔽掉,使用方法和 PRIMASK 类似, FAULTMASK 会在退出时自动清零。
  汇编编程的时候可以利用 CPS 指令修改 FAULTMASK 的当前状态:

//用法一
CPSIE F /* 清除 FAULTMASK(使能中断) */
CPSID F /* 设置 FAULTMASK(屏蔽中断) */

  还可以利用 MRS 和 MSR 指令访问 FAULTMASK 寄存器:

//用法二
MRS R0, FAULTMASK /* 读取 FAULTMASK 值 */
MOV R0, #0 MSR FAULTMASK, R0 /* 清除 FAULTMASK(使能中断) */
MOV R0, #1 MSR FAULTMASK, R0 /* 设置 FAULTMASK(屏蔽中断) */

  还可以直接调用函数:

//用法三:
__get_FAULTMASK(); /* 读取 FAULTMASK 值 */
__set_FAULTMASK(0U); /* 清除 FAULTMASK(使能中断) */
__set_FAULTMASK(1U); /* 设置 FAULTMASK(屏蔽中断) */

 (3)、 BASEPRI 寄存器

  作用: BASEPRI 有 32bit, 但只有低 8 位[7:0]有效, 也是可读可写的。 BASEPRI 寄存器比起 PRIMASK 和 FAULTMASK 寄存器直接屏蔽掉大部分中断的方式, BASEPRI 寄存器的功能显得更加细腻, BASEPRI 用于设置一个中断屏蔽的阈值, 设置好 BASEPRI 后, 中断优先级低于 BASEPRI 的中断就都会被屏蔽掉, FreeRTOS 就是使用 BASEPRI 寄存器来管理受 FreeRTOS管理的中断的, 而不受 FreeRTOS 管理的中断, 则不受 FreeRTOS 的影响。

  所以PRIMASK 和 FAULTMASK 寄存器太粗暴了,直接关闭除复位、 NMI 和 HardFault 以外的其他所有中断,但是在有些场合需要对中断屏蔽进行更细腻的控制, 比如只屏蔽优先级低于某一个阈值的中断。那么这个作为阈值的优先级值存储在哪里呢?在 BASEPRI 寄存器中,不过如果向 BASEPRI 写 0 的话就会停止屏蔽中断。 比如,我们要屏蔽优先级不高于 0X60 的中断,则可以使用如下汇编编程:

//用法一
MRS R0, BASEPRI /* 读取 BASEPRI 值 */

MOV R0, #0
MSR BASEPRI, R0 /* 清除 BASEMASK(使能中断) */

MOV R0, #0x60 /* 举例 */
MSR BASEPRI, R0 /* 设置 BASEMASK(屏蔽优先级低于 0x60 的中断) */
//用法二:
__get_BASEPRI(); /* 读取 BASEPRI 值 */
__set_BASEPRI(0); /* 清除 BASEPRI(使能中断) */
__set_BASEPRI(0x60); /* 设置 BASEPRI(屏蔽优先级小于 0x60 的中断) */

  注意! FreeRTOS 的开关中断就是操作 BASEPRI 寄存器来实现的!它可以关闭低于某个阈值的中断,高于这个阈值的中断就不会被关闭!

  1.6、中断控制状态寄存器
  中断状态状态寄存器(ICSR) 的地址为 0xE000ED04, 用于设置和清除异常的挂起状态,以及获取当前系统正在执行的异常编号, 各比特位的功能描述如下表所示:

image

  这个寄存器主要关注 VECTACTIVE 段[8:0], 通过读取 VECTACTIVE 段就能够判断当前执行的代码是否在中断中。

2、 FreeRTOS 中断配置宏

  FreeRTOSConfig.h 文件中有 6 个与中断相关的 FreeRTOS 配置项, 如表 3.2.7.1 所示。 这里主要讲如何配置这 6 个中断相关的 FreeRTOS配置项。

image

  (1)configPRIO_BITS
  此宏是用于辅助配置的宏, 主要用于辅助配置宏 configKERNEL_INTERRUPT_PRIORITY和宏 configMAX_SYSCALL_INTERRUPT_PRIORITY 的, 此宏应定义为 MCU 的 8 位优先级配置寄存器实际使用的位数, 因为 STM32 只使用到了中断优先级配置寄存器的高 4 位, 因此,此宏应配置为 4。
  (2)configLIBRARY_LOWEST_INTERRUPT_PRIORITY
  此宏是用于辅助配置宏 configKERNEL_INTERRUPT_PRIORITY 的, 此宏用来设置MCU的最低优先等级, 前面说了,STM32 优先级使用了 4 位,而且 STM32 配置的使用组 4,也就是 4 位都是抢占优先级。因此优先级数就是 16 个,最低优先级那就是 15。 所以此宏就是 15,注意!不同的 MCU 此值不同,具体是多少要看所使用的 MCU 的架构。
  (3)configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
  此宏是用于辅助配置宏 configMAX_SYSCALL_INTERRUPT_PRIORITY 的, 此宏适用于配置 FreeRTOS 可管理的最高优先级的中断, 此功能就是操作 BASEPRI 寄存器来实现的。 此宏的值可以根据用户的实际使用场景来决定, 本教程的配套例程源码全部将此宏配置为 5, 即中断优先级高于 5 的中断不受 FreeRTOS 影响, 如下图所示:

image

  (4)configKERNEL_INTERRUPT_PRIORITY
  此宏用来配置 MCU 的最低优先级在中断优先级配置寄存器中的值, 在 FreeRTOS 的源码中, 使用此宏将 SysTick 和 PenSV 的中断优先级设置为最低优先级。 因为 STM32 只使用了中断优先级配置寄存器的高 4 位, 因此, 此宏应配置为最低中断优先级在中断优先级配置寄存器高 4 位的表示, 即(configLIBRARY_LOWEST_INTERRUPT_PRIORITY<<(8-configPRIO_BITS))。

  此宏用来设置内核中断优先级, 此宏定义如下:

#define configKERNEL_INTERRUPT_PRIORITY
( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

  宏 configKERNEL_INTERRUPT_PRIORITY为宏configLIBRARY_LOWEST_INTERRUPT_PRIORITY 左移 8-configPRIO_BITS 位,也就是左移 4位。为什么要左移 4 位呢?前面我们说了, STM32 使用了 4 位作为优先级,而这 4 位是高 4 位,因 此 要 左 移 4 位 才 是 真 正 的 优 先 级 。 当 然 了 也 可 以 不 用 移 位 , 直 接 将 宏configLIBRARY_LOWEST_INTERRUPT_PRIORITY 定义为 0XF0! 不过这样看起来不直观。
  宏 configKERNEL_INTERRUPT_PRIORITY 用来设置 PendSV 和滴答定时器的中断优先级,port.c 中有如下定义:

#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
#define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )

  可 以 看 出 , portNVIC_PENDSV_PRI 和 portNVIC_SYSTICK_PRI 都 是 使 用 了 宏configKERNEL_INTERRUPT_PRIORITY , 为 什 么 宏 portNVIC_PENDSV_PRI 是 宏configKERNEL_INTERRUPT_PRIORITY 左移 16 位呢?宏 portNVIC_SYSTICK_PRI 也同样是左移 24 位。 因为PendSV 和 SysTcik 的中断优先级设置是操作 0xE000_ED20 地址的, 这样一次写入的是个 32 位的数据, SysTick 和 PendSV 的优先级寄存器分别对应这个 32位数据的最高 8 位和次高 8 位,不就是一个左移 16 位,一个左移 24 位了。
  PendSV 和 SysTick 优先级是在哪里设置的呢?在函数 xPortStartScheduler()中设置,此函数在文件 port.c 中,函数如下:

BaseType_t xPortStartScheduler( void )
{
    configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
    configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
    configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
    #if( configASSERT_DEFINED == 1 )
    {
        volatile uint32_t ulOriginalPriority;
        volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * )
        ( portNVIC_IP_REGISTERS_OFFSET_16 +
        portFIRST_USER_INTERRUPT_NUMBER );
        volatile uint8_t ucMaxPriorityValue;
        ulOriginalPriority = *pucFirstUserPriorityRegister;
        *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
        ucMaxPriorityValue = *pucFirstUserPriorityRegister;
        configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY &
        ucMaxPriorityValue ) );
        ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY &
        ucMaxPriorityValue;
        ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
        while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
        {
            ulMaxPRIGROUPValue--;
            ucMaxPriorityValue <<= ( uint8_t ) 0x01;
        }
        ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
        ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
        *pucFirstUserPriorityRegister = ulOriginalPriority;
    }
    #endif /* conifgASSERT_DEFINED */
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; //设置 PendSV 中断优先级
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; //设置 SysTick 中断优先级
    vPortSetupTimerInterrupt();
    uxCriticalNesting = 0;
    prvStartFirstTask();
    return 0;
}

  上述代码中红色部分就是设置 PendSV 和 SysTick 优先级的,它们是直接向地址portNVIC_SYSPRI2_REG 写入优先级数据, portNVIC_SYSPRI2_REG 是个宏,在文件 port.c 中由定义,如下:

#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )

  可以看到宏 portNVIC_SYSPRI2_REG 就是地址 0XE000ED20!同时也可以看出在 FreeRTOS中 PendSV 和 SysTick 的中断优先级都是最低的!

  (5)、configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
  此宏用来设置 FreeRTOS 系统可管理的最大优先级,也就是我们在 4.1.5 小节中讲解BASEPRI 寄存器说的那个阈值优先级,这个大家可以自由设置,这里我设置为了 5。也就是高于 5 的优先级(优先级数小于 5)不归 FreeRTOS 管理!
  (6)、configMAX_SYSCALL_INTERRUPT_PRIORITY

  此宏是 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 左移 4 位而来的,原因和宏 configKERNEL_INTERRUPT_PRIORITY 一样。此宏设置好以后,低于此优先级的中断可以安全的调用 FreeRTOS 的 API 函数,高于此优先级的中断 FreeRTOS 是不能禁止的,中断服务函数也不能调用 FreeRTOS 的 API 函数
  以 STM32 为例,有 16 个优先级, 0 为最高优先级, 15 为最低优先级,配置如下:

configMAX_SYSCALL_INTERRUPT_PRIORITY==5
configKERNEL_INTERRUPT_PRIORITY==15

  结果如下图所示:

  由于高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的优先级不会被 FreeRTOS 内核屏蔽,因此那些对实时性要求严格的任务就可以使用这些优先级,比如四轴飞行器中的壁障检测。

  (7) configMAX_API_CALL_INTERRUPT_PRIORITY
  此宏为宏 configMAX_SYSCALL_INTERRUPT_PRIORITY 的新名称, 只被用在 FreeRTOS官方一些新的移植当中, 此宏于宏 configMAX_SYSCALL_INTERRUPT_PRIORITY 是等价的。

3、 FreeRTOS 中断管理详解

  3.1、PendSV 和 SysTick 中断优先级
  前面提到, FreeRTOS 使用 SHPR3 寄存器配置 PendSV 和 SysTick 的中断优先级,那么 FreeRTOS 是如何配置的呢? 在 FreeRTOS 的源码中有如下定义:

#define portNVIC_SHPR3_REG \
( *( ( volatile uint32_t * ) 0xe000ed20 ) )

#define portNVIC_PENDSV_PRI \
( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )

#define portNVIC_SYSTICK_PRI \
( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )

  可以看到宏 portNVIC_SHPR3_REG 被定义成了一个指向 0xE000ED20 地址的指针, 而0xE000ED20 就是 SHPR3 寄存器地址的指针, 因此只需通过宏 portNVIC_SHPR3_REG 就能够访问 SHPR3 寄存器了。
  接 着 是 宏 portNVIC_PENDSV_PRI 和 宏 portNVIC_SYSTICK_PRI 分 别 定 义 成 了 宏configKERNEL_INTERRUPT_PRIORITY 左 移 16 位 和 24 位 , 其 中 宏configKERNEL_INTERRUPT_PRIORITY 在 FreeRTOSConfig.h 文件中被定义成了系统的最低优先等级, 而左移的 16 位和 24 位, 正好是 PendSV 和 SysTick 中断优先级配置在 SHPR3 寄存器中的位置, 因此只需将宏 portNVIC_PENDSV_PRI 和宏 portNVIC_SYSTICK_PRI 对应地写入SHPR3 寄存器, 就能将 PendSV 和 SysTick 的中断优先级设置为最低优先级。
  接着 FreeRTOS 在启动任务调度器的函数中设置了 PendSV 和 SysTick 的中断优先级, 代码如下所示:

BaseType_t xPortStartScheduler( void )
{
    /* 忽略其他代码 */
    /* 设置 PendSV 和 SysTick 的中断优先级为最低中断优先级 */
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
    /* 忽略其他代码 */
}

  为什么要将PendSV 和 SysTick 的中断优先级设置为最低优先级?

  简单来说,这样做的目的是:确保实时中断(如 GPIO、通信外设)能够立即得到响应,而任务调度等“不那么紧急”的操作则在这些紧急处理完成之后再进行,从而保证系统的实时性。
  下面我们从几个层面来深入剖析这个设计哲学。
  (1)两个中断的角色分工
  SysTick(系统节拍定时器):
    作用:作为操作系统的“心跳”,定期产生中断。在中断服务程序中,它会更新系统时间戳、检查任务延时是否到期、以及判断是否需要进行一次任务调度(例如,一个高优先级任务延时结束了)。
    关键点:SysTick 只负责“触发”调度请求,它并不亲自执行完整的上下文切换。
  PendSV(可挂起的系统调用):
  作用:这是一个专为操作系统设计的异常。它的特性是可以被挂起,直到没有其他更高优先级的中断需要处理时才会执行。FreeRTOS 利用这个特性来执行实际的、耗时的上下文切换(保存当前任务状态,恢复下一个任务状态)。
  (2)为什么要把它们的优先级设为最低?
  核心思想是:“触发调度”和“执行调度”不应该打断正在处理的关键硬件事务。
  让我们通过一个经典的场景来说明:
    场景:没有优先级设置为最低时的问题
      假设 SysTick 的优先级被设置为一个很高的级别。
      一个高优先级的硬件中断正在运行(例如,一个非常重要的 UART 数据接收中断)。
      就在此时,SysTick 中断发生了。因为它的优先级更高,它会立即抢占正在处理的 UART 中断。
      SysTick 检查后发现需要调度(比如一个更高优先级的任务就绪了),于是它请求 PendSV 执行上下文切换。
      如果 PendSV 的优先级也很高,它可能会在 SysTick 退出后、UART 中断恢复前立刻执行。
      结果:PendSV 进行了上下文切换,系统切换到了另一个任务。当 PendSV 完成后,处理器返回的却不是被 SysTick 打断的 UART 中断,而是另一个任务。UART 中断服务程序被无限期推迟执行,直到很久以后才有可能被继续执行完。这严重破坏了系统的实时性,可能导致数据丢失。
    场景:优先级设置为最低时(正确的设计)
      同样,一个高优先级的 UART 中断正在运行。
      SysTick 中断发生,但因为它的优先级最低,它无法打断 UART 中断,只能等待。
      UART 中断服务程序完整地、不受干扰地执行完毕。
      在 UART 中断退出后,处理器会发现有两个挂起的中断等待响应:一个是刚才被阻塞的、最低优先级的 SysTick,另一个可能也是最低优先级的 PendSV(如果之前被挂起过)。
      由于它们优先级相同,处理器会先执行 SysTick。
      SysTick 执行它的工作:更新时间,发现需要调度,于是挂起 PendSV。
      SysTick 执行完毕退出后,因为 PendSV 已经被挂起,且没有其他更高优先级的中断,处理器开始执行 PendSV。
      PendSV 执行耗时的上下文切换。
      切换完成后,PendSV 退出,系统可能就运行在了新的任务上。
      这个过程的精髓在于:UART 中断的实时性得到了绝对保证。任务调度(由 SysTick 触发,由 PendSV 执行)被推迟到了一个“安全”的时刻——即没有其他紧急硬件中断需要处理的时候。
    总结:设计优势
      最大化实时性:确保了硬件的实时中断能够以最小的延迟得到响应,不会被操作系统内核中断所抢占。这对于嵌入式实时系统至关重要。
      将中断处理与任务调度解耦:高优先级的硬件中断只处理与硬件相关的紧急事务。与任务管理相关的、不那么紧急的操作被推迟到 PendSV 中处理。
      避免不必要的多次上下文切换:如果在高优先级中断中立即进行上下文切换,可能会导致中断嵌套非常深时产生多次无意义的切换,而将所有切换请求集中到 PendSV 中执行,可以合并处理,更加高效。
      利用 PendSV 的特性:PendSV 的“可挂起”特性天生就是为这种“推迟执行”的操作系统行为而设计的。
    一个形象的比喻
      你可以把整个系统想象成一个繁忙的医院:
        高优先级硬件中断:像是 急诊病人 或 正在做手术的医生。他们需要立即、不间断的 attention。
        SysTick:像是医院的 排班系统闹钟。它定时提醒:“该检查一下有没有新医生该上班了,或者有没有医生该下班了。”
        PendSV:像是 负责交接班的护士长。她的工作是安排医生们换班,这个工作虽然重要,但不能打断正在进行的手术。
        如果把排班闹钟(SysTick)和护士长(PendSV)的权限设为最高,那么手术做到一半,闹钟一响,手术就得停下,先处理排班,这无疑是灾难性的。正确的做法是,让手术(高优先级中断)安心做完。手术结束后,再响闹钟、再让护士长去安排换班(任务调度)。
        因此,将 PendSV 和 SysTick 的优先级设置为最低,不是贬低它们,而是为了赋予整个系统最强的实时响应能力,是 FreeRTOS 乃至所有 Cortex-M 内核 RTOS 的一个精妙设计。

  3.2、FreeRTOS开关中断

  前面说过, FreeRTOS 使用 BASEPRI 寄存器来管理受 FreeRTOS 管理的中断, 而不受FreeRTOS 管理的中断不受 FreeRTOS 开关中断的影响, 那么 FreeRTOS 开关中断是如何操作的呢? 首先来看一下 FreeRTOS 开关中断的宏定义, 代码如下所示:

#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() 
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )

#define taskDISABLE_INTERRUPTS() portDISABLE_INTERRUPTS()  //关中断
#define taskENABLE_INTERRUPTS() portENABLE_INTERRUPTS()  //开中断

  可以看出开关中断实际上是通过函数 vPortSetBASEPRI(0)和 vPortRaiseBASEPRI()来实现的,这两个函数如下:

static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
  __asm
  {
    /* 设置 BasePRI 寄存器 */ 
    msr basepri, ulBASEPRI
  }
}
/*-----------------------------------------------------------*/
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
  uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
  __asm
  {
    /* 设置 BasePRI 寄存器 */ 
    msr basepri, ulNewBASEPRI
    dsb
    isb
  }
}

  函数 vPortSetBASEPRI()是向寄存器 BASEPRI 写入一个值,此值作为参数 ulBASEPRI 传递进来, portENABLE_INTERRUPTS()是开中断,它传递了个 0 给 vPortSetBASEPRI(),根据我们前面讲解 BASEPRI 寄存器可知,结果就是开中断。
  函 数 vPortRaiseBASEPRI() 是 向 寄 存 器 BASEPRI 写 入 宏configMAX_SYSCALL_INTERRUPT_PRIORITY , 那 么 优 先 级 低 于configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断就会被屏蔽!

  下面再来看看 FreeRTOS 中开关中断的两个宏定义:
  (1)宏 portDISABLE_INTERRUPTS()

  #define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()

  从上面的宏定义可以看出, FreeRTOS 关闭中断的操作就是将 BASEPRI 寄存器设置为宏configMAX_SYSCALL_INTERRUPT_PRIORITY 的值, 以此来达到屏蔽受 FreeRTOS 管理的中断, 而不影响到哪些不受 FreeRTOS 管理的中断。
  (3) 宏 portENABLE_INTERRUPTS()

#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )

  从上面的宏定义可以看出, FreeRTOS 开启中断的操作就是将 BASEPRI 寄存器的值清零,以此来取消屏蔽中断。

4、FreeRTOS 进出临界区
  临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段,比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。 FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断。 FreeRTOS 系统本身就有很多的临界段代码,这些代码都加了临界段代码保护,我们在写自己的用户程序的时候有些地方也需要添加临界段代码保护。
  FreeRTOS 与 临 界 段 代 码 保 护 有 关 的 函 数 有 4 个 : taskENTER_CRITICAL() 、taskEXIT_CRITICAL() 、 taskENTER_CRITICAL_FROM_ISR() 和taskEXIT_CRITICAL_FROM_ISR(),这四个函数其实是宏定义,在 task.h 文件中有定义。 这四个函数的区别是前两个是任务级的临界段代码保护,后两个是中断级的临界段代码保护。

  定义代码如下所示:

/* 进入临界区 */
#define portENTER_CRITICAL() vPortEnterCritical()
#define taskENTER_CRITICAL() portENTER_CRITICAL()

/* 中断中进入临界区 */
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()

/* 退出临界区 */
#define portEXIT_CRITICAL() vPortExitCritical()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()

/* 中断中退出临界区 */
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
#define taskEXIT_CRITICAL_FROM_ISR(x) portCLEAR_INTERRUPT_MASK_FROM_ISR(x)

  进入临界区会关闭中断,这样会导致优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断得不到及时的响应。

  函数 taskENTER_CRITICAL_FROM_ISR()和 taskEXIT_CRITICAL_FROM_ISR()中断级别临 界 段 代 码 保 护 , 是 用 在 中 断 服 务 程 序 中 的 , 而 且 这 个 中 断 的 优 先 级 一 定 要 低 于configMAX_SYSCALL_INTERRUPT_PRIORITY!

  下面分别来看一下这四个进出临界区的宏定义。
  (1)宏 taskENTER_CRITICAL()
  此 宏 用 于 在 非 中 断 中 进 入 临 界 区 , 此 宏 展 开 后 是 函 数 vPortEnterCritical() , 函 数vPortEnterCritical()的代码如下所示:

void vPortEnterCritical( void )
{
    /* 关闭受 FreeRTOS 管理的中断 */
    portDISABLE_INTERRUPTS();
    /* 临界区支持嵌套 */
    uxCriticalNesting++;
    if( uxCriticalNesting == 1 )
    {
        /* 这个函数不能在中断中调用 */
        configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
    }
}

  从上面的代码中可以看出, 函数 vPortEnterCritical()进入临界区就是关闭中断, 当然了, 不受 FreeRTOS 管理的中断是不受影响的。 还可以看出, FreeRTOS 的临界区是可以嵌套的, 意思就是说, 在程序中可以重复地进入临界区, 只要后续重复退出相同次数的临界区即可。
  在上面的代码中还有一个断言, 代码如下所示:

if( uxCriticalNesting == 1 )
{
    /* 这个函数不能在中断中调用 */
    configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}

  断言中使用到的两个宏定义在 FreeRTOS 的源码中都有定义, 定义如下所示:

#define portNVIC_INT_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portVECTACTIVE_MASK ( 0xFFUL )

  可以看出, 宏 portNVIC_INT_CTRL_REG 就是指向中断控制状态寄存器(ICSR) 的指针,而宏 portVECTACTIVE_MASK 就是 ICSR 寄存器中 VECTACTIVE 段对应的位置, 因此这个断言就是用来判断当第一次进入临界区的时候, 是否是从中断服务函数中进入的, 因为函数vportEnterCritical()是用于从非中断中进入临界区, 如果用户错误地在中断服务函数中调用函数vportEnterCritical(), 那么就会通过断言报错。
  (2)宏 taskENTER_CRITICAL_FROM_ISR()
  此 宏 用 于 从 中 断 中 进 入 临 界 区 , 此 宏 展 开 后 是 函 数 ulPortRaiseBASEPRI() , 函 数ulPortRaiseBASEPRI()的代码如下所示:

static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
    uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
    __asm
    {
        /* 读取 BASEPRI 寄存器 */
        mrs ulReturn, basepri
        /* 设置 BASEPRI 寄存器 */
        msr basepri, ulNewBASEPRI
        dsb
        isb
    }
    return ulReturn;
}   

  可 以 看 到 函 数 ulPortRaiseBASEPRI() 同 样 是 将 BASEPRI 寄 存 器 设 置 为 宏configMAX_SYSCALL_INTERRUPT_PRIORITY 的值, 以达到关闭中断的效果, 当然了, 不受FreeRTOS 管理的中断是不受影响的。 只不过函数 ulPortRaiseBASEPRI()在设置 BASEPRI 寄存器之前, 先读取了 BASEPRI 的值, 并在函数的最后返回这个值, 这是为了在后续从中断中退出临界区时, 恢复 BASEPRI 寄存器的值。
  从上面的代码中也可以看出, 从中断中进入临界区时不支持嵌套的。
  (3)宏 taskEXIT_CRITICAL()
  此 宏 用 于 从 非 中 断 中 退 出 临 界 区 , 此 宏 展 开 后 是 函 数 vPortExitCritical() , 函 数vPortExitCritical()的代码如下所示:

void vPortExitCritical( void )
{
    /* 必须是进入过临界区才能退出 */
    configASSERT( uxCriticalNesting );
    uxCriticalNesting--;
    if( uxCriticalNesting == 0 )
    {
        /* 打开中断 */
        portENABLE_INTERRUPTS();
    }
}

  这个函数就很好理解了, 就是将用于临界区嵌套的计数器减 1, 当计数器减到 0 的时候,说明临界区已经没有嵌套了, 于是调用函数 portENABLE_INTERRUPT()打开中断。 在函数的一开始还有一个断言, 这个断言用于判断用于临界区嵌套的计数器在进入此函数的不为 0, 这样就保证了用户不会在还未进入临界区时, 就错误地调用此函数退出临界区。
  (4)taskEXIT_CRITICAL_FROM_ISR(x)
  此宏用于从中断中退出临界区, 此宏展开后是调用了函数 vPortSetBASEPRI(), 并将参数 x传入函数 vPortSetBASEPRI()。 其中参数 x 就是宏 taskENTER_CRITICAL_FROM_ISR()的返回值, 用于在从中断中对出临界区时, 恢复 BASEPRI 寄存器。
  读者在使用 FreeRTOS 进行开发的时候, 应适当并合理地使用临界区, 以让设计的程序更加可靠。

static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
    __asm
    {
        /* Barrier instructions are not used as this function is only used to
        lower the BASEPRI value. */
        msr basepri, ulBASEPRI
    }
}

 




posted @ 2024-04-25 11:25  孤情剑客  阅读(529)  评论(0)    收藏  举报