Bootstrap

【FreeRTOS】 中断管理

FreeRTOS 中断管理

  • FreeRTOS 使用SHPR3 寄存器配置 PendSV 和 SysTick 的中断优先级;
  • FreeRTOS 使用 BASEPRI 寄存器来管理受 FreeRTOS 管理的中断
4.1 ARM Cortex-M 中断

4.1.1 ARM Cortex-M中断简介

  • 中断是 CPU 的一种常见特性,中断一般由硬件产生,当中断发生后,会中断 CPU 当前正在执行的程序而跳转到中断对应的服务程序种去执行;

  • ARM Cortex-M 内核的 MCU 具有一个用于中断管理的嵌套向量中断控制器(NVIC,全称:Nested vectored interrupt controller);

  • ARM Cortex-M 的 NVIC 最大可支持 256 个中断源,其中包括 16 个系统中断和 240 个外部中断;

  • 芯片厂商一般情况下都用不完这些资源,以正点原子的战舰开发板为例,所使用的STM32F103ZET6 芯片就只用到了 10 个系统中断和 60 个外部中断。


4.1.2中断优先级管理(NVIC)


  • ARM Cortex-M 使用 NVIC 对不同优先级的中断进行管理,首先看一下 NVIC 在 CMSIS 中的结构体定义,如下所示:
typedef struct
{
 __IOM uint32_t ISER[8U]; /* 中断使能寄存器 */
 uint32_t RESERVED0[24U];
 __IOM uint32_t ICER[8U]; /* 中断除能寄存器 */
 uint32_t RSERVED1[24U];
 __IOM uint32_t ISPR[8U]; /* 中断使能挂起寄存器 */
 uint32_t RESERVED2[24U];
 __IOM uint32_t ICPR[8U]; /* 中断除能挂起寄存器 */
 uint32_t RESERVED3[24U];
 __IOM uint32_t IABR[8U]; /* 中断有效位寄存器 */
 uint32_t RESERVED4[56U];
 __IOM uint8_t IP[240U];  /* 中断优先级寄存器 */
 uint32_t RESERVED5[644U];
 __OM uint32_t STIR; 	  /* 软件触发中断寄存器 */
} NVIC_Type;
  • 在 NVIC 的相关结构体中,成员变量 IP 用于配置外部中断的优先级,成员变量 IP 的定义如下所示:
  __IOM uint8_t IP[240U]; /* 中断优先级寄存器 */

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

​ 综上可知,ARM Cortex-M 使用了 8 位宽的寄存器来配置中断的优先等级,这个寄存器就是中断优先级配置寄存器,因此最大中断的 优先级配置范围位 0~255;

​ 但是芯片厂商一般用不完这些资源,对于 STM32,只用到了中断优先级配置寄存器的高 4 位[7:4],低四位[3:0]取零处理,因此 STM32 提供了最大 2^4=16 级的中断优先等级,如下图所示:

在这里插入图片描述

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

  • STM32 的中断优先级可以分为抢占优先级和子优先级,抢占优先级和子优先级的区别如下:

​ **抢占优先级:**抢占优先级高的中断可以打断正在执行但抢占优先级低的中断,即中断嵌套;

​ **子优先级:**抢占优先级相同时,子优先级高的中断不能打断正在执行但子优先级低的中的中断,即子优先级不支持中断嵌套。

  • STM32 中每个中断的优先级就由抢占优先级和子优先级共同组成,使用中断优先级配置寄存器的高 4 位来配置抢占优先级和子优先级;
  • 中断优先级配置寄存器的高 4 位设置抢占优先级和子优先级,一共由 5 种分配方式,对应这中断优先级分组的 5 个组:
#define NVIC_PRIORITYGROUP_0 0x00000007U /* 优先级分组 0 */
#define NVIC_PRIORITYGROUP_1 0x00000006U /* 优先级分组 1 */
#define NVIC_PRIORITYGROUP_2 0x00000005U /* 优先级分组 2 */
#define NVIC_PRIORITYGROUP_3 0x00000004U /* 优先级分组 3 */
#define NVIC_PRIORITYGROUP_4 0x00000003U /* 优先级分组 4 */

在这里插入图片描述

  • FreeRTOS 的官方强烈建议 STM32 在使用 FreeRTOS 的时候,使用中断优先级分组 4(NVIC_PriorityGroup_4)即优先级配置寄存器的高 4 位全部用于抢占优先级,不使用子优先级,这么一来用户就只需要设置抢占优先级即可:
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

4.1.3 三个系统中断优先级配置寄存器(★SHPR3 寄存器 :0xE000ED20)


  • 除了外部中断,系统中断有独立的中断优先级配置寄存器,分别为 SHPR1、SHPR2、SHPR3

SHPR1寄存器:地址为 0xE000ED18,用于配置 MemManage、BusFault、UsageFault 的中断优先级,各比特位的功能描述如下表所示:
在这里插入图片描述

SHPR2 寄存器:地址为 0xE000ED1C,用于配置 SVCall 的中断优先级,各比特位的功能描述如下表所示:
在这里插入图片描述

SHPR3 寄存器:地址为 0xE000ED20,用于配置 PendSV、SysTick 的中断优先级,各比特位的功能描述如下表所示:
在这里插入图片描述

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

4.1.4 三个中断屏蔽寄存器(★BASEPRI寄存器:0x50)


  • ARM Cortex-M 有三个用于屏蔽中断的寄存器,分别为 PRIMASK、FAULTMASK 和BASEPRI
  • 功能分别为:屏蔽除 NMI 和 HardFault 外的所有异常和中断、屏蔽除 NMI 外的所有异常和中断、中断优先级低于 BASEPRI 的中断就都会被屏蔽掉

PRIMASK寄存器:屏蔽除 NMI 和 HardFault 外的所有异常和中断

  • PRIMASK 寄存器有 32bit,但只有 bit0 有效,是可读可写的;
  • 将 PRIMASK 寄存器设置为 1 则屏蔽除 NMI 和 HardFault 外的所有异常和中断,将 PRIMASK 寄存器清 0 则使能中断
用法一:
CPSIE I 			/* 清除 PRIMASK(使能中断) */
CPSID I 			/* 设置 PRIMASK(屏蔽中断) */
    
用法二:
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(屏蔽中断) */

FAULTMASK 寄存器:屏蔽除 NMI 外的所有异常和中断

  • FAULTMASK 寄存器有 32bit,但只有 bit0 有效,也是可读可写的;
  • 将 FAULTMASK寄存器设置为 1 则屏蔽除 NMI 外的所有异常和中断,将 FAULTMASK 寄存器清零则使能中断
用法一:
CPSIE F 				/* 清除 FAULTMASK(使能中断) */
CPSID F 				/* 设置 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(屏蔽中断) */

BASEPRI 寄存器:中断优先级低于 BASEPRI 的中断就都会被屏蔽掉

  • BASEPRI 有 32bit,但只有低 8 位[7:0]有效,也是可读可写的;
  • BASEPRI 用于设置一个中断屏蔽的阈值,设置好 BASEPRI 后,中断优先级低于 BASEPRI 的中断就都会被屏蔽掉
  • FreeRTOS 就是使用 BASEPRI 寄存器来管理受 FreeRTOS管理的中断的,而不受 FreeRTOS 管理的中断,则不受 FreeRTOS 的影响
用法一:
MRS R0, BASEPRI 		/* 读取 BASEPRI 值 */
MOV R0, #0
MSR BASEPRI, R0 		/* 清除 BASEMASK(使能中断) */
MOV R0, #0x50 			/* 举例 */
MSR BASEPRI, R0 		/* 设置 BASEMASK(屏蔽优先级低于 0x50 的中断) */
    
用法二:
__get_BASEPRI(); 		/* 读取 BASEPRI 值 */
__set_BASEPRI(0); 		/* 清除 BASEPRI(使能中断) */
__set_BASEPRI(0x50); 	/* 设置 BASEPRI(屏蔽优先级小于 0x50 的中断) */

4.1.5 中断控制状态寄存器(ICSR :0xE000ED04)


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

在这里插入图片描述

  • 主要关注 VECTACTIVE 段[8:0],通过读取 VECTACTIVE 段就能够判断当前执行的代码是否在中断中
4.2 FreeRTOS 中断配置项

4.2.1 configPRIO_BITS(4):用于辅助配置的宏

  • 用于辅助配置的宏,辅助配置宏 configKERNEL__INTERRUPT_PRIORITY和宏 configMAX_SYSCALL__INTERRUPT_PRIORITY;
  • 此宏应定义为 MCU 的 8 位优先级配置寄存器实际使用的位数,因为 STM32 只使用到了中断优先级配置寄存器的高 4 位,因此,此宏应配置为 4

4.2.2 configLIBRARY_LOWEST_INTERRUPT_PRIORITY(15):辅助配置宏 configKERNEL_INTERRUPT_PRIORITY

  • 辅助配置宏 configKERNEL_INTERRUPT_PRIORITY 的,此宏应设置为 MCU的最低优先等级
  • 因为 STM32 只使用了中断优先级配置寄存器的高 4 位,因此 MCU 的最低优先等级就是 2^4-1=15,因此,此宏应配置为 15

4.2.3 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:辅助配置宏 configMAX_SYSCALL_INTERRUPT_PRIORITY

  • 辅助配置宏 configMAX_SYSCALL_INTERRUPT_PRIORITY 的,此宏适用于配置 FreeRTOS 可管理的最高优先级的中断,此功能就是操作 BASEPRI 寄存器来实现的;
  • 此宏的值可以根据用户的实际使用场景来决定,本教程的配套例程源码全部将此宏配置为 5,即中断优先级高于 5 的中断不受 FreeRTOS 影响。如下图所示:

在这里插入图片描述


4.2.4 configKERNEL_INTERRUPT_PRIORITY:配置最低优先级在中断优先级配置寄存器中的值

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

4.2.5 configMAX_SYSCALL_INTERRUPT_PRIORITY:打开和关闭中断

  • 此宏用于配置 FreeRTOS 可管理的最高优先级的中断,在 FreeRTOS 的源码中,使用此宏来打开和关闭中断。
  • 因为 STM32 只使用了中断优先级配置寄存器的高 4 位,因此,此宏应配置(configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY<<(8-configPRIO_BITS))

4.2.6 configMAX_API_CALL_INTERRUPT_PRIORITY:宏 configMAX_SYSCALL_INTERRUPT_PRIORITY 的新名称

  • 只被用在 FreeRTOS官方一些新的移植当中,此宏于宏 configMAX_SYSCALL_INTERRUPT_PRIORITY 是等价的
4.3 FreeRTOS 中断管理详解

ARM Cortex-M 中断和 FreeRTOS 中断配置项了解后,待会将通过分析 FreeRTOS 源码的方式来讲解 FreeRTOS 是如何管理中断的


4.3.1 PendSV SysTick 中断优先级


  • FreeRTOS 使用 SHPR3 寄存器配置 PendSV 和 SysTick 的中断优先级:
#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 )
  1. 宏portNVIC_SHPR3_REG 被定义成了一个指向 0xE000ED20 地址的指针,而0xE000ED20 就是 SHPR3 寄存器地址的指针;
  2. 从此只需通过宏 portNVIC_SHPR3_REG 就能够访问 SHPR3 寄存器;
  3. portNVIC_PENDSV_PRI 和portNVIC_SYSTICK_PRI分别定义成宏configKERNEL_INTERRUPT_PRIORITY 左 移 16 位和 24
  • FreeRTOS 在启动任务调度器的函数中设置了 PendSV 和 SysTick 的中断优先级:
BaseType_t xPortStartScheduler( void )
{
 /* 忽略其他代码 */
    
 /* 设置 PendSV 和 SysTick 的中断优先级为最低中断优先级 */
 portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
 portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
    
 /* 忽略其他代码 */
}

4.3.2 FreeRTOS 开关中断


  • FreeRTOS开关中断具体原理结合如下函数解释

FreeRTOS 使用 BASEPRI 寄存器来管理受 FreeRTOS 管理的中断,而不受FreeRTOS 管理的中断不受 FreeRTOS 开关中断的影响;

那么 FreeRTOS 开关中断是如何操作?首先来看一下 FreeRTOS 开关中断的宏定义,代码如下所示:

// FreeRTOS开关中断
#define portDISABLE_INTERRUPTS() 	vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() 	vPortSetBASEPRI( 0 )
#define taskDISABLE_INTERRUPTS() 	portDISABLE_INTERRUPTS()
#define taskENABLE_INTERRUPTS() 	portENABLE_INTERRUPTS()
  • **vPortRaiseBASEPRI() 😗*将 BASEPRI 寄 存 器 设 置 为 宏configMAX_SYSCALL_INTERRUPT_PRIORITY 配置的值
//函数 vPortRaiseBASEPRI()
{
 uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
 
 __asm
 {
 /* 设置 BasePRI 寄存器 */
 msr basepri, ulNewBASEPRI
 dsb
 isb
 }
}
//简单介绍一下 DSB 和 ISB 指令,DSB 和 ISB 指令分别为数据同步隔离和指令同步隔离
  • vPortSetBASEPRI():将 BASEPRI 寄存器设置为指定的值
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
 __asm
 {
 /* 设置 BasePRI 寄存器 */
 msr basepri, ulBASEPRI
 }
}

参考上面两个函数结合宏定义,理解 FreeRTOS 中开关中断(task差不多):

#define portDISABLE_INTERRUPTS() 	vPortRaiseBASEPRI()
/*FreeRTOS 关闭中断的操作就是将 BASEPRI 寄存器设置为宏configMAX_SYSCALL_INTERRUPT_PRIORITY 的值;
  以此来达到屏蔽受 FreeRTOS 管理的中断,而不影响到哪些不受 FreeRTOS 管理的中断。*/

#define portENABLE_INTERRUPTS() 	vPortSetBASEPRI( 0 )
/*FreeRTOS 开启中断的操作就是将 BASEPRI 寄存器的值清零,以此来取消屏蔽中断。*/

4.3.3 FreeRTOS 进出临界区


  1. 临界区是指那些**必须完整运行的区域,在临界区中的代码必须完整运行,不能被打断。**例如一些使用软件模拟的通信协议,通信协议在通信时,必须严格按照通信协议的时序进行,不能被打断。

  2. FreeRTOS 在进出临界区的时候,通过关闭和打开受 FreeRTOS 管理的中断,以保护临界区中的代码;

  3. FreeRTOS 的源码中有四个相关的宏定义 ,分 别 为taskENTER_CRITICAL() 、 taskENTER_CRITICAL_FROM_ISR() 、taskEXIT_CRITICAL() 、taskEXIT_CRITICAL_FROM_ISR(x),这四个宏定义分别用于在中断和非中断中进出临界区

/* 进入临界区 */
#define taskENTER_CRITICAL() 				portENTER_CRITICAL()
#define portENTER_CRITICAL() 				vPortEnterCritical()
/* 中断中进入临界区 */
#define taskENTER_CRITICAL_FROM_ISR() 		portSET_INTERRUPT_MASK_FROM_ISR()
#define portSET_INTERRUPT_MASK_FROM_ISR() 	ulPortRaiseBASEPRI()
/* 退出临界区 */
#define taskEXIT_CRITICAL() 				portEXIT_CRITICAL()
#define portEXIT_CRITICAL() 				vPortExitCritical()
/* 中断中退出临界区 */
#define taskEXIT_CRITICAL_FROM_ISR(x) 			portCLEAR_INTERRUPT_MASK_FROM_ISR(x)
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) 	vPortSetBASEPRI(x)
  • taskENTER_CRITICAL():用于在非中断中进入临界区,此宏展开后是函数 vPortEnterCritical()
void vPortEnterCritical( void )
{
 /* 关闭受 FreeRTOS 管理的中断 */
 portDISABLE_INTERRUPTS();
 /* 临界区支持嵌套 */
 uxCriticalNesting++;
 
 if( uxCriticalNesting == 1 )
 {
 /* 这个函数不能在中断中调用 */
 configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
 }
}
  1. 函数 vPortEnterCritical()进入临界区就是关闭中断,当然了,不受 FreeRTOS 管理的中断是不受影响的;
  2. FreeRTOS 的临界区是可以嵌套的,意思就是说,在程序中可以重复地进入临界区,只要后续重复退出相同次数的临界区即可。
  • taskENTER_CRITICAL_FROM_ISR():从中断中进入临界区,此宏展开后是函数 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;
}
  1. 可 以 看 到 函 数 ulPortRaiseBASEPRI() 同 样 是 将 BASEPRI 寄 存 器 设 置 为 宏configMAX_SYSCALL_INTERRUPT_PRIORITY 的值,以达到关闭中断的效果;
  2. 只不过函数 ulPortRaiseBASEPRI()在设置 BASEPRI 寄存器之前,先读取了 BASEPRI 的值,并在函数的最后返回这个值,这是为了在后续从中断中退出临界区时,恢复 BASEPRI 寄存器的值;
  3. 从中断中进入临界区时不支持嵌套
  • taskEXIT_CRITICAL():从 非 中 断 中 退 出 临 界 区 , 此 宏 展 开 后 是 函 数 vPortExitCritical()
void vPortExitCritical( void )
{
 /* 必须是进入过临界区才能退出 */
 configASSERT( uxCriticalNesting );
 uxCriticalNesting--;
 
 if( uxCriticalNesting == 0 )
 {
 /* 打开中断 */
 portENABLE_INTERRUPTS();
 }
}
  1. 将用于临界区嵌套的计数器减 1,当计数器减到 0 ,说明临界区已经无嵌套,于是调用函数 portENABLE_INTERRUPT()打开中断;
  2. 在函数的一开始还有一个断言,这个断言用于判断用于临界区嵌套的计数器在进入此函数的不为 0,这样就保证了用户不会在还未进入临界区时,就错误地调用此函数退出临界区
  • taskEXIT_CRITICAL_FROM_ISR(x)
  1. 用于从中断中退出临界区,此宏展开后是调用了函数 vPortSetBASEPRI(),将参数 x传入函数 vPortSetBASEPRI();
  2. 其中参数 x 就是宏 taskENTER_CRITICAL_FROM_ISR()的返回值,用于在从中断中对出临界区时,恢复 BASEPRI 寄存器
4.4 FreeRTOS 中断测试实验

见下一篇

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;