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 )
- 宏portNVIC_SHPR3_REG 被定义成了一个指向 0xE000ED20 地址的指针,而0xE000ED20 就是 SHPR3 寄存器地址的指针;
- 从此只需通过宏 portNVIC_SHPR3_REG 就能够访问 SHPR3 寄存器;
- 宏 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 进出临界区
-
临界区是指那些**必须完整运行的区域,在临界区中的代码必须完整运行,不能被打断。**例如一些使用软件模拟的通信协议,通信协议在通信时,必须严格按照通信协议的时序进行,不能被打断。
-
FreeRTOS 在进出临界区的时候,通过关闭和打开受 FreeRTOS 管理的中断,以保护临界区中的代码;
-
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 );
}
}
- 函数 vPortEnterCritical()进入临界区就是关闭中断,当然了,不受 FreeRTOS 管理的中断是不受影响的;
- 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;
}
- 可 以 看 到 函 数 ulPortRaiseBASEPRI() 同 样 是 将 BASEPRI 寄 存 器 设 置 为 宏configMAX_SYSCALL_INTERRUPT_PRIORITY 的值,以达到关闭中断的效果;
- 只不过函数 ulPortRaiseBASEPRI()在设置 BASEPRI 寄存器之前,先读取了 BASEPRI 的值,并在函数的最后返回这个值,这是为了在后续从中断中退出临界区时,恢复 BASEPRI 寄存器的值;
- 从中断中进入临界区时不支持嵌套
- taskEXIT_CRITICAL():从 非 中 断 中 退 出 临 界 区 , 此 宏 展 开 后 是 函 数 vPortExitCritical()
void vPortExitCritical( void )
{
/* 必须是进入过临界区才能退出 */
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
/* 打开中断 */
portENABLE_INTERRUPTS();
}
}
- 将用于临界区嵌套的计数器减 1,当计数器减到 0 ,说明临界区已经无嵌套,于是调用函数 portENABLE_INTERRUPT()打开中断;
- 在函数的一开始还有一个断言,这个断言用于判断用于临界区嵌套的计数器在进入此函数的不为 0,这样就保证了用户不会在还未进入临界区时,就错误地调用此函数退出临界区
- taskEXIT_CRITICAL_FROM_ISR(x)
- 用于从中断中退出临界区,此宏展开后是调用了函数 vPortSetBASEPRI(),将参数 x传入函数 vPortSetBASEPRI();
- 其中参数 x 就是宏 taskENTER_CRITICAL_FROM_ISR()的返回值,用于在从中断中对出临界区时,恢复 BASEPRI 寄存器
4.4 FreeRTOS 中断测试实验
见下一篇