目录
参考材料
中断简介
中断管理简介
优先级分组定义(正点原子freertos手册)
优先级设置
用于中断屏蔽的特殊寄存器
primask暂时屏蔽中断寄存器(RT-THREAD使用)
faultmask寄存器
basepri优先级屏蔽中断寄存器(freeRTOS使用)
FreeRTOS中断配置宏
configPRIO_BITS 几位优先级
configLIBRARY_LOWEST_INTERRUPT_PRIORITY
configKERNEL_INTERRUPT_PRIORITY
PendSV 和 SysTick 优先级设置
xPortStartScheduler代码的讲解
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
configMAX_SYSCALL_INTERRUPT_PRIORITY
FreeRTOS 开关中断
临界段代码
临界段的好处和坏处
临界区的副作用
临界区内代码的设计原则
带有FROM_ISR和不带有FROM_ISR中断API函数区别
任务级临界段代码保护
任务级临界代码保护使用方法
中断级临界段代码保护
中断级临界代码保护使用方法
常用中断API
(正点原子freertos手册)
(CM3权威指南)
(STM32F4XX中文参考手册)
CM4内核支持256个中断,其中包含了16个内核中断和240个外部中断,并且具有256级的可编程中断设置
Cotex-M3 和 M4 的 NVIC 最多支持 240 个 IRQ(中断请求)、 1 个不可屏蔽中断(NMI)、 1 个Systick(滴答定时器)定时器中断和多个系统异常。
system_stm32f4xx.c --> "stm32f4xx.h" IRQn中断向量--> "core_cm4.h"/* Cortex-M4 processor and core peripherals */ -->
Cortex-M 处理器有多个用于管理中断和异常的可编程寄存器, 这些寄存器大多数都在
NVIC 和系统控制块(SCB)中,以 STM32F407 为例,打开core_cm4.h,有两个结构体, NVIC_Type 和 SCB_Type,如下:
Structure type to access the Nested Vectored Interrupt Controller (NVIC) typedef struct { __IO uint32_t ISER[8]; /*!< Offset: 0x000 (R/W) Interrupt Set Enable Register */ uint32_t RESERVED0[24]; __IO uint32_t ICER[8]; /*!< Offset: 0x080 (R/W) Interrupt Clear Enable Register */ uint32_t RSERVED1[24]; __IO uint32_t ISPR[8]; /*!< Offset: 0x100 (R/W) Interrupt Set Pending Register */ uint32_t RESERVED2[24]; __IO uint32_t ICPR[8]; /*!< Offset: 0x180 (R/W) Interrupt Clear Pending Register */ uint32_t RESERVED3[24]; __IO uint32_t IABR[8]; /*!< Offset: 0x200 (R/W) Interrupt Active bit Register */ uint32_t RESERVED4[56]; __IO uint8_t IP[240]; /*!< Offset: 0x300 (R/W) Interrupt Priority Register (8Bit wide) */ uint32_t RESERVED5[644]; __O uint32_t STIR; /*!< Offset: 0xE00 ( /W) Software Trigger Interrupt Register */ } NVIC_Type; |
__IO uint8_t IP[240]; //中断优先级控制的寄存器组
__IO uint32_t ISER[8]; //中断使能寄存器组
__IO uint32_t ICER[8]; //中断失能寄存器组
__IO uint32_t ISPR[8]; //中断挂起寄存器组
__IO uint32_t ICPR[8]; //中断解挂寄存器组
__IO uint32_t IABR[8]; //中断激活标志位寄存器组
/** \ingroup CMSIS_core_register \defgroup CMSIS_SCB System Control Block (SCB) \brief Type definitions for the System Control Block Registers @{ */ /** \brief Structure type to access the System Control Block (SCB). */ typedef struct { __I uint32_t CPUID; /*!< Offset: 0x000 (R/ ) CPUID Base Register */ __IO uint32_t ICSR; /*!< Offset: 0x004 (R/W) Interrupt Control and State Register */ __IO uint32_t VTOR; /*!< Offset: 0x008 (R/W) Vector Table Offset Register */ __IO uint32_t AIRCR; /*!< Offset: 0x00C (R/W) Application Interrupt and Reset Control Register */ __IO uint32_t SCR; /*!< Offset: 0x010 (R/W) System Control Register */ __IO uint32_t CCR; /*!< Offset: 0x014 (R/W) Configuration Control Register */ __IO uint8_t SHP[12]; /*!< Offset: 0x018 (R/W) System Handlers Priority Registers (4-7, 8-11, 12-15) */ __IO uint32_t SHCSR; /*!< Offset: 0x024 (R/W) System Handler Control and State Register */ __IO uint32_t CFSR; /*!< Offset: 0x028 (R/W) Configurable Fault Status Register */ __IO uint32_t HFSR; /*!< Offset: 0x02C (R/W) HardFault Status Register */ __IO uint32_t DFSR; /*!< Offset: 0x030 (R/W) Debug Fault Status Register */ __IO uint32_t MMFAR; /*!< Offset: 0x034 (R/W) MemManage Fault Address Register */ __IO uint32_t BFAR; /*!< Offset: 0x038 (R/W) BusFault Address Register */ __IO uint32_t AFSR; /*!< Offset: 0x03C (R/W) Auxiliary Fault Status Register */ __I uint32_t PFR[2]; /*!< Offset: 0x040 (R/ ) Processor Feature Register */ __I uint32_t DFR; /*!< Offset: 0x048 (R/ ) Debug Feature Register */ __I uint32_t ADR; /*!< Offset: 0x04C (R/ ) Auxiliary Feature Register */ __I uint32_t MMFR[4]; /*!< Offset: 0x050 (R/ ) Memory Model Feature Register */ __I uint32_t ISAR[5]; /*!< Offset: 0x060 (R/ ) Instruction Set Attributes Register */ uint32_t RESERVED0[5]; __IO uint32_t CPACR; /*!< Offset: 0x088 (R/W) Coprocessor Access Control Register */ } SCB_Type; |
NVIC 和 SCB 都位于系统控制空间(SCS)内, SCS 的地址从 0XE000E000 开始, SCB 和 NVIC的地址也在 core_cm4.h 中有定义,如下:
/* Memory mapping of Cortex-M4 Hardware */ #define SCS_BASE (0xE000E000UL) /*!< System Control Space Base Address */ #define ITM_BASE (0xE0000000UL) /*!< ITM Base Address */ #define DWT_BASE (0xE0001000UL) /*!< DWT Base Address */ #define TPI_BASE (0xE0040000UL) /*!< TPI Base Address */ #define CoreDebug_BASE (0xE000EDF0UL) /*!< Core Debug Base Address */ #define SysTick_BASE (SCS_BASE + 0x0010UL) /*!< SysTick Base Address */ #define NVIC_BASE (SCS_BASE + 0x0100UL) /*!< NVIC Base Address */ #define SCB_BASE (SCS_BASE + 0x0D00UL) /*!< System Control Block Base Address */ #define SCnSCB ((SCnSCB_Type *) SCS_BASE ) /*!< System control Register not in SCB */ #define SCB ((SCB_Type *) SCB_BASE ) /*!< SCB configuration struct */ #define SysTick ((SysTick_Type *) SysTick_BASE ) /*!< SysTick configuration struct */ #define NVIC ((NVIC_Type *) NVIC_BASE ) /*!< NVIC configuration struct */ #define ITM ((ITM_Type *) ITM_BASE ) /*!< ITM configuration struct */ #define DWT ((DWT_Type *) DWT_BASE ) /*!< DWT configuration struct */ #define TPI ((TPI_Type *) TPI_BASE ) /*!< TPI configuration struct */ #define CoreDebug ((CoreDebug_Type *) CoreDebug_BASE) /*!< Core Debug configuration struct */ |
我们可以在标准库的misc.c文件中,看到这个寄存器的使用。指针操作即可。
NVIC->IP[NVIC_InitStruct->NVIC_IRQChannel] = tmppriority;
但是
我们重点关心的是是三个中断屏蔽寄存器: PRIMASK、 FAULTMASK 和 BASEPRI,这三个寄存器后面会详细的讲解
复位、 NMI、HardFault,这些中断的优先级都是负数,优先级也是最高的。
Cortex-M 处理器有三个固定优先级和 256 个可编程的优先级,最多有 128 个抢占等级,比如 STM32 就只有 16 级优先级
如图 4.1.3.1 就是使用三位来表达优先级
__IO uint8_t IP[240]; /*!< Offset: 0x300 (R/W) Interrupt Priority Register (8Bit wide) */
优先级配置寄存器是 8 位宽的,为什么却只有 128 个抢占等级? 8 位不应该是 256 个抢占等级吗?为了使抢占机能变得更可控, Cortex-M 处理器还把 256 个优先级按位分为高低两段:抢占优先级(分组优先级)和亚优先级(子优先级), NVIC 中有一个寄存器是“应用程序中断及复位控制寄存器(AIRCR)”, AIRCR 寄存器里面有个位段名为“优先级组”,如表 4.1.3.1 所示
[10:8] PRIGROUP R/W 0 优先级分组
分组位置 |
表达抢占优先级的位段 |
表达亚优先级的位段 |
0(默认) |
[7:1] |
[0:0] |
1 |
[7:2] |
[1:0] |
2 |
[7:3] |
[2:0] |
3 |
[7:4] |
[3:0] |
4 |
[7:5] |
[4:0] |
5 |
[7:6] |
[5:0] |
6 |
[7:7] |
[6:0] |
7 |
无 |
[7:0] |
表 4.1.3.2 抢占优先级和亚优先级的表达,位数与分组位置的关系,红色部分就是stm32使用的
在看一下 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 */全部是抢占 |
主要是 FreeRTOS 的中断配置没有处理亚优先级这种情况,所以只能配置为组 4,直接就 16 个优先级,使用起来也简单!
每个外部中断都有一个对应的优先级寄存器,每个寄存器占 8 位,因此最大宽度是 8 位,但是最小为 3 位。 4 个相临的优先级寄存器拼成一个 32 位寄存器。如前所述,根据优先级组的设置,优先级又可以分为高、低两个位段,分别抢占优先级和亚优先级。 STM32 我们已经设置位组 4,所以就只有抢占优先级了。优先级寄存器都可以按字节访问,当然也可以按半字/字来访问,有意义的优先级寄存器数目由芯片厂商来实现,如表 4.1.4.1 和 4.1.4.2 所示:
名称 |
类型 |
地址 |
复位值 |
描述 |
PRI_0 |
R/W |
0xE000_E400 |
0(8 位) |
外中断#0 的优先级 |
PRI_1 |
R/W |
0xE000_E401 |
0(8 位) |
外中断#1 的优先级 |
... |
... |
... |
... |
... |
PRI_239 |
R/W |
0xE000_E4EF |
0(8 位) |
外中断#239 的优先级 |
表 4.1.4.1 中断优先级寄存器阵列(地址: 0xE000_E400~0xE000_E4EF)
名称 |
类型 |
地址 |
复位值 |
描述 |
PRI_4 |
0xE000_ED18 |
存储管理 fault 的优先级 |
||
PRI_5 |
0xE000_ED19 |
总线 fault 的优先级 |
||
PRI_6 |
0xE000_ED1A |
用法 fault 的优先级 |
...
PRI_12 |
- |
0xE000_ED20 |
(8 位) |
调试监视器的优先级 |
- |
- |
0xE000_ED21 |
(8 位) |
|
PRI_14 |
- |
0xE000_ED22 |
(8 位) |
PendSV 的优先级 |
PRI_15 |
- |
0xE000_ED23 |
(8 位) |
SysTick 的优先级 |
系统异常优先级阵列(地址: 0XE000_ED18~0xE000_ED23)
0xE000_ED20~0xE000_ED23 这四个寄存器就可以拼接成一个地址为 0xE000_ED20 的 32 位寄存器。 这一点很重要! 因为 FreeRTOS 在设置 PendSV 和 SysTick 的中断优先级的时候都是直接操作的地址 0xE000_ED20。
STM32 上移植 FreeRTOS 的时候需要重点关注 PRIMASK、FAULTMASK 和 BASEPRI 这三个寄存器
在许多应用中,需要暂时屏蔽所有的中断一执行一些对时序要求严格的任务,这个时候就 可以使用 PRIMASK 寄存器,PRIMASK 用于禁止除 复位、NMI(不可屏蔽中断) 和 HardFalut(硬故障寄存器) 外的所有异常和中断.
FAULTMASK比PRIMASK更狠,它可以连HardFault都屏蔽掉,使用方法和PRIMASK类似,FAULTMASK会在退出时自动清零。
BASEPRI它屏蔽操作很细腻,它能屏蔽优先级低于BASEPRI的中断,优先级阈值存在BASEPRI寄存器中,如果向BASEPRI写入0会停止屏蔽中断功能。
注意!freeos开关中断就是控制basepri寄存器,优先级低于阈值关闭,高于阈值不关闭。
在 BASEPRI 寄存器中,不过如果向 BASEPRI 写 0 的话就会停止屏蔽中断。 比如,我们要屏蔽优先级不高于 0X60 的中断,
则可以使用如下汇编编程:
MOV R0, #0X60 MSR BASEPRI, R0 |
如果需要取消 BASEPRI 对中断的屏蔽,可以使用如下代码:
MOV R0, #0 MSR BASEPRI, R0 |
这里可以参考正点原子的
设置MCU使用几位优先级,STM32使用的是4位,前面有讲到《中断分组管理》
设置最低优先级,STM32配置使用组4,都是抢占优先级,最低优先级为15
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0xf
设置freeRTOS系统内核中断优先级,通常设置为中断优先级最低,就是15。但是赋值寄存器时,就需要15<<4,也就是0xf0。
如上宏在freeRTOS中主要用来设置PENDSV、SYSTICK优先级等级。通常设置这两个最低。也就是15,也就是寄存器值为0xf0
因为RTOS内核中断不允许抢占用户使用的中断,因此这个宏一般定义为硬件最低优先级
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) ) 我自己板子打印的结果:F407打印出来的结果 configKERNEL_INTERRUPT_PRIORITY:240 0xf0 |
这里虽然是240,但是并不是优先等级为240,由于cm4中我们只使用了高4位用于表达优先等级,5个优先级分组,由于我们这里使用了高4位,如果这里设置为如上,就是freeRTOS系统内核中断优先等级最低的,具体的解释如下:
为什么要左移 4 位呢?前面我们说了,cm4内核中配置中断优先级的8位中,从高位开始有效,STM32使用了高4位。
直 接 将 宏configLIBRARY_LOWEST_INTERRUPT_PRIORITY 定义为 0XF0! 不过这样看起来不直观。
在我的工程中搜索哪里用到了configKERNEL_INTERRUPT_PRIORITY
Searching for 'configKERNEL_INTERRUPT_PRIORITY'... main.c(97) : xprintf("configKERNEL_INTERRUPT_PRIORITY:%d\r\n",configKERNEL_INTERRUPT_PRIORITY); //我自己打印 FreeRTOSConfig.h(165) : #define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) ) //就是0xf0 port.c(124) : #define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL ) port.c(125) : #define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL ) port.c(372) : configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) ); Lines matched: 5 Files matched: 3 Total files searched: 196 |
宏 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 ) |
问题:configKERNEL_INTERRUPT_PRIORITY 左移 16 位,宏 portNVIC_SYSTICK_PRI 也同样是左移 24 位呢?
回答:在优先级设置有如下:
PRI_12 |
- |
0xE000_ED20 |
(8 位) |
调试监视器的优先级 |
- |
- |
0xE000_ED21 |
(8 位) |
|
PRI_14 |
- |
0xE000_ED22 |
(8 位) |
PendSV 的优先级 |
PRI_15 |
- |
0xE000_ED23 |
(8 位) |
SysTick 的优先级 |
系统异常优先级阵列(地址: 0XE000_ED18~0xE000_ED23)
PendSV 和 SysTcik 的中断优先级设置是操作 0xE000_ED20 地址的, 这样一次写入的是个 32 位的数据, SysTick 和 PendSV 的优先级寄存器分别对应这个 32位数据的最高 8 位和次高 8 位,不就是一个左移 16 位,一个左移 24 位了。
PendSV 和 SysTick 优先级是在哪里设置的呢?在函数 xPortStartScheduler()中设置,此函数在文件 port.c 中,函数如下:
BaseType_t xPortStartScheduler( void ) { /* configMAX_SYSCALL_INTERRUPT_PRIORITY不能设置为0。 See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */ configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY ); /* This port can be used on all revisions of the Cortex-M7 core other than the r0p1 parts. r0p1 parts should use the port from the /source/portable/GCC/ARM_CM7/r0p1 directory. */ 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 = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER ); volatile uint8_t ucMaxPriorityValue; /*确定可以调用ISR安全FreeRTOS API函数的最大优先级。ISR安全函数是以“FromISR”结尾的函数。FreeRTOS维护单独的线程和ISR API函数,以确保中断输入尽可能快速和简单。 保存即将中断的中断优先级值. */ ulOriginalPriority = *pucFirstUserPriorityRegister; /*确定可用的优先级位数。首先写入所有可能的位. */ *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE; /* Read the value back to see how many bits stuck. */ ucMaxPriorityValue = *pucFirstUserPriorityRegister; /*在最大系统调用优先级上使用相同的掩码。 */ ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue; /* Calculate the maximum acceptable priority group value for the number of bits read back. */ ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS; while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE ) { ulMaxPRIGROUPValue--; ucMaxPriorityValue <<= ( uint8_t ) 0x01; } /* Shift the priority group value back to its position within the AIRCR register. */ ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT; ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK; /* Restore the clobbered interrupt priority register to its original value. */ *pucFirstUserPriorityRegister = ulOriginalPriority; } #endif /* conifgASSERT_DEFINED */ /* Make PendSV and SysTick the lowest priority interrupts. */ portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; //设置 PendSV 中断优先级 portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; //设置 SysTick 中断优先级 /* Start the timer that generates the tick ISR. Interrupts are disabled here already. */ vPortSetupTimerInterrupt(); /* Initialise the critical nesting count ready for the first task. */ uxCriticalNesting = 0; /* Ensure the VFP is enabled - it should be anyway. */ vPortEnableVFP(); /* Lazy save always. */ *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS; /* Start the first task. */ vPortStartFirstTask(); /* Should not get here! */ return 0; } |
它们是直接向地址portNVIC_SYSPRI2_REG 写入优先级数据, portNVIC_SYSPRI2_REG 是个宏,在文件 port.c 中由定义,如下
#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) ) |
可以看到宏portNVIC_SYSPRI2_REG 就是地址0XE000ED20!同时也可以看出在 FreeRTOS中 PendSV 和 SysTick 的中断优先级都是最低的!
系统可管理的最大优先级,就是设置中断屏蔽寄存器BASEPRI 寄存器的值。(优先级数小于这个值)不归 FreeRTOS 管理。不会被freeRTOS屏蔽。
优先级数低于5不归FreeRTOS管理
/*任何调用中断安全FreeRTOS API函数的中断服务例程都可以使用的最高中断优先级。不要从任何优先级高于此的中断调用中断安全FREERTOS API函数!(优先级越高,数值越低。*/ #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 |
此宏用来设置 FreeRTOS 系统可管理的最大优先级,也就是我们在 4.1.5 小节中讲解BASEPRI 寄存器说的那个阈值优先级,这个大家可以自由设置,这里我设置为了 5。也就是高于 5 的优先级(优先级数小于 5)不归 FreeRTOS 管理!
设置注意:
1、configMAX_SYSCALL_INTERRUPT_PRIORITY不能设置为0
2、“FromISR”带有这样的API都会调用portASSERT_IF_INTERRUPT_PRIORITY_INVALID()函数进行检查当前优先等级是否有效。
3、因为freeRTOS系统规定,中断中使用的API函数使用带有“FromISR”后缀的。
此宏是 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 内核屏蔽,因此那些对实时性要求严格的任务就可以使用这些优先级,比如四轴飞行器中的壁障检测
如果中断设置为4,并使用RTOS的api的话
如果如果我们将
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=4;//**抢占优先级6 ,这里的优先级必须小于系统时钟的优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority =0; //子优先级 |
如上我们设置串口的中断等级为4,此时如果发送外部中断,调用API的话会出现断言。
位于port.c最下面
void vPortValidateInterruptPriority( void ) { uint32_t ulCurrentInterrupt; uint8_t ucCurrentPriority; /*获取当前正在执行的中断的编号。 */ __asm volatile( "mrs %0, ipsr" : "=r"( ulCurrentInterrupt ) ); /*中断号是用户定义的中断吗? */ if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER ) { /*查找中断的优先级。 */ ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ]; /* T如果已分配优先级高于configMAX_SYSCALL_interrupt_priority的中断的服务例程(ISR)调用ISR安全的FreeRTOS API函数,则以下断言将失败。ISR安全的FreeRTOS API函数必须*仅*从已分配优先级等于或低于configMAX_SYSCALL_INTERRUPT_priority的中断调用。 数字低的中断优先级数字表示逻辑高的中断优先级,因此中断优先级必须设置为等于或数字*高于configMAX_SYSCALL_interrupt_priority的值。 使用FreeRTOS API的中断不能保持其默认优先级为零,因为这是可能的最高优先级,保证高于configMAX_SYSCALL_INTERRUPT_priority,因此也保证无效。 FreeRTOS维护单独的线程和ISR API函数,以确保中断输入尽可能快速和简单。 以下链接提供了详细信息: http://www.freertos.org/RTOS-Cortex-M3-M4.html http://www.freertos.org/FAQHelp.html */ configASSERT( ucCurrentPriority >= ucMaxSysCallPriority ); } /* Priority grouping: The interrupt controller (NVIC) allows the bits that define each interrupt's priority to be split between bits that define the interrupt's pre-emption priority bits and bits that define the interrupt's sub-priority. For simplicity all bits must be defined to be pre-emption priority bits. The following assertion will fail if this is not the case (if some bits represent a sub-priority). If the application only uses CMSIS libraries for interrupt configuration then the correct setting can be achieved on all Cortex-M devices by calling NVIC_SetPriorityGrouping( 0 ); before starting the scheduler. Note however that some vendor specific peripheral libraries assume a non-zero priority group setting, in which cases using a value of zero will result in unpredicable behaviour. */ configASSERT( ( portAIRCR_REG & portPRIORITY_GROUP_MASK ) <= ulMaxPRIGROUPValue ); } |
#define portASSERT_IF_INTERRUPT_PRIORITY_INVALID() vPortValidateInterruptPriority() |
而portASSERT_IF_INTERRUPT_PRIORITY_INVALID会在queue.c和task.c中都使用到,并且信号量和互斥量等等,都会使用到queue。所以基本上所有的freeRTOS的API都会进行检测当前中断的优先级。
“FromISR”带有这样的API都会调用portASSERT_IF_INTERRUPT_PRIORITY_INVALID()函数进行检查当前优先等级是否有效。
FreeRTOS 开关中断函数为 portENABLE_INTERRUPTS ()和 portDISABLE_INTERRUPTS(),这两个函数其实是宏定义,在 portmacro.h 中有定义,如下:
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() //去能中断 #define portENABLE_INTERRUPTS() vPortSetBASEPRI(0) //使能中断 |
可以看出开关中断实际上是通过函数 vPortSetBASEPRI(0)和 vPortRaiseBASEPRI()来实现的,这两个函数如下:
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) { __asm { msr basepri, ulBASEPRI //设置basepri为0,就是使能中断 } } /*-----------------------------------------------------------*/ static portFORCE_INLINE void vPortRaiseBASEPRI( void ) { uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; __asm { msr basepri, ulNewBASEPRI //设置basepri为configMAX_SYSCALL_INTERRUPT_PRIORITY,就是屏蔽优先级低于5的中断。 dsb isb } } |
函数 vPortSetBASEPRI()是向寄存器 BASEPRI 写入一个值,此值作为参数 ulBASEPRI 传递进来, portENABLE_INTERRUPTS()是开中断,它传递了个 0 给 vPortSetBASEPRI(),根据我们前面讲解 BASEPRI 寄存器可知,结果就是开中断。
函 数 vPortRaiseBASEPRI() 是 向 寄 存 器 BASEPRI 写 入 宏configMAX_SYSCALL_INTERRUPT_PRIORITY , 那 么 优 先 级 低 于configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断就会被屏蔽!
keil的版本,IAR的不同
临界区为什么能实现共享资源的保护
在 RTOS 中的任务调度与三种任务模型 章节中,我们介绍了任务的调度是通过 SysTick 中断完成的,即每个 SysTick 中断到来时都会触发任务调度器工作,检查是否需要切换任务,即更换 CPU 的使用权。
如前所述,临界区是通过关闭全局中断来实现的(部分系统不一样,RTT系统是关闭可编程中断,freeRTOS关闭一定阈值的中断),这其中包括 SysTick 中断。即调用进入临界区的 API后,设备的中断停止了,这导致了两个方面的变化:
1)SysTick 中断被禁用,无法触发任务切换,因此临界区内的代码,不会被其他任务打断执行。
2)设备中断被禁用,无法响应新的中断,因此临界区内的代码,不会被其他中断打断执行。
所以,临界区内的代码在每次使用时都是独占的,完全不会被干扰。
1)对任务而言,SysTick 中断被禁用,无法触发任务切换,因此同优先级任务、高优先级任务均无法被执行,对任务的实时调度产生影响。
2)对中断而言,设备中断被禁用,无法响应新的中断,因此设备的中断响应实时性遭到破坏。
总结:得于斯者毁于斯,因此临界区在发挥作用时,也有对应得副作用。
1)临界区应尽可能短。如果可能,将尽可能多的处理和/或事件处理推迟到临界区之外。
2)临界段持续的时间越长,挂起的中断延迟的时间就越长,因此对于读操作,最好只是拷贝原时值,对于写操作,最好只是更改原始值的简单操作。
3)典型的临界区应该只访问几个数据结构和/或硬件寄存器。
4)FreeRTOS API不应在临界区内调用。
5)用户不应在临界区内调用任何阻塞(block)或会停止代码执行直到它完成才返回的函数(这类函数学名叫 yielding functions)。
1、taskENTER_CRITICAL() —> portENTER_CRITICAL()宏 —> vPortEnterCritical()函数 { portDISABLE_INTERRUPTS();uxCriticalNesting++; } —> 调用portDISABLE_INTERRUPTS()宏 —> vPortRaiseBASEPRI()函数
2、taskENTER_CRITICAL_FROM_ISR() —> portSET_INTERRUPT_MASK_FROM_ISR()宏 —> ulPortRaiseBASEPRI()函数
3、taskEXIT_CRITICAL() --> portEXIT_CRITICAL()宏 --> vPortExitCritical()函数{ uxCriticalNesting--; portENABLE_INTERRUPTS()} --> portENABLE_INTERRUPTS() --> vPortSetBASEPRI( 0 )函数
4、taskEXIT_CRITICAL_FROM_ISR( x ) --> portCLEAR_INTERRUPT_MASK_FROM_ISR( x )宏 --> vPortSetBASEPRI(x) 函数
taskEXIT_CRITICAL_FROM_ISR( 0 )
前面我在分析 FreeRTOS 实现细节的时候,多次遇到 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 这两个调用。从名称来理解就是说,这时任务中要做一个很要紧的操作,不允许被打断,比如要对任务状态列表进行访问。如果不这样处理的话,有可能中途要访问的数据被改写了,或者是数据改动未完成被其它任务或者 FreeRTOS 内核访问,都会造成错误的结果。于是,定义一段代码为 critical section, 前后用taskENTER_CRITICAL()和taskEXIT_CRITICAL() 保护起来,禁止任务调度,以及禁止其它中断 ISR 访问 FreeRTOS 核心数据。
在 task.h 头文件中有这两个宏定义: #define taskENTER_CRITICAL() portENTER_CRITICAL() #define taskEXIT_CRITICAL() portEXIT_CRITICAL() 在portmacro.h 文件中又定义为 #define portENTER_CRITICAL() vPortEnterCritical() #define portEXIT_CRITICAL() vPortExitCritical() |
临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段,比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。 FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断。 FreeRTOS 系统本身就有很多的临界段代码,这些代码都加了临界段代码保护,我们在写自己的用户程序的时候有些地方也需要添加临界段代码保护。
FreeRTOS 与临界段代码保护有关的函数有4 个 : taskENTER_CRITICAL() 、taskEXIT_CRITICAL() 、 taskENTER_CRITICAL_FROM_ISR() 和taskEXIT_CRITICAL_FROM_ISR(),这四个函数其实是宏定义,在 task.h 文件中有定义。 这四个函数的区别是前两个是任务级的临界段代码保护,后两个是中断级的临界段代码保护。
函数 vPortEnterCritical()和 vPortExitCritical()在文件 port.c 中,函数如下:
void vPortEnterCritical( void ) { portDISABLE_INTERRUPTS(); uxCriticalNesting++; if( uxCriticalNesting == 1 ) { configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 ); } } void vPortExitCritical( void ) { configASSERT( uxCriticalNesting ); uxCriticalNesting--; if( uxCriticalNesting == 0 ) { portENABLE_INTERRUPTS(); } } |
可以看出在进入函数 vPortEnterCritical()以后会首先关闭中断,然后给变量 uxCriticalNesting加一, uxCriticalNesting 是个全局变量,用来记录临界段嵌套次数的。函数 vPortExitCritical()是退出临界段调用的,函数每次将 uxCriticalNesting 减一,只有当 uxCriticalNesting 为 0 的时候才会调用函数 portENABLE_INTERRUPTS()使能中断。这样保证了在有多个临界段代码的时候不会因为某一个临界段代码的退出而打乱其他临界段的保护,只有所有的临界段代码都退出以后才会使能中断!
任务级临界代码保护使用方法如下:
void taskcritical_test(void) { while(1) { taskENTER_CRITICAL(); (1) total_num+=0.01f; printf("total_num 的值为: %.4f\r\n",total_num); taskEXIT_CRITICAL(); (2) vTaskDelay(1000); } } |
(1)、进入临界区。
(2)、退出临界区。
(1)和(2)中间的代码就是临界区代码,注意临界区代码一定要精简!因为进入临界区会关闭中断,这样会导致优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断得不到及时的响应!
函数 taskENTER_CRITICAL_FROM_ISR()和 taskEXIT_CRITICAL_FROM_ISR()中断级别临 界 段 代 码 保 护 , 是 用 在 中 断 服 务 程 序 中 的 , 而 且 这 个 中 断 的 优 先 级 一 定 要 低 于configMAX_SYSCALL_INTERRUPT_PRIORITY!原因前面已经说了。这两个函数在文件 task.h中有如下定义:
task.h #define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR() #define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x ) portmacro.h #define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI() #define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x) |
函数 ulPortRaiseBASEPRI()在文件 portmacro.h 中定义的,如下:
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void ) { uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; __asm { mrs ulReturn, basepri (1) 先读出 BASEPRI 的值,保存在 ulReturn 中。 msr basepri, ulNewBASEPRI (2) 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 写入到寄存器 BASEPRI 中。 dsb isb } return ulReturn; (3) 返回 ulReturn,退出临界区代码保护的时候要使用到此值! } |
//定时器 3 中断服务函数 void TIM3_IRQHandler(void) { if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断 { status_value=taskENTER_CRITICAL_FROM_ISR(); (1) total_num+=1; printf("float_num 的值为: %d\r\n",total_num); taskEXIT_CRITICAL_FROM_ISR(status_value); (2) } TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位 } |
taskENTER_CRITICAL()
taskEXIT_CRITICAL()
taskENTER_CRITICAL_FROM_ISR()
taskEXIT_CRITICAL_FROM_ISR()
portDISABLE_INTERRUPTS()
portENABLE_INTERRUPTS()
portASSERT_IF_INTERRUPT_PRIORITY_INVALID() 检查当前中断的优先等级是否有效,通常使用freeRTOS的API不允许中断高于
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY