目录
1.中断配置
1.1 中断概述
中断是处理器的一个常见特性,由硬件产生,中断产生后CPU就会中断当前流程转去处理中断服务。Cortex-M内核的MCU还挺过来一个用于管理中断的嵌套向量中断控制器 NVIC,它支持多种中断和异常管理。
Cortex-M处理器提供了多个管理中断和异常的可编程寄存器,重点需要关注的是3个中断屏蔽寄存器:PRIMASK、FAULTMASK和BASEPRI。
1.2 优先级分组
(1)优先级数量
Cortex-M内核支持中断优先级配置,中断优先级数值越小,优先级越高,与FreeRTOS任务优先级相反,任务优先级数值越小,优先级越低。
Cortex-M处理器有3个固定优先级和256个可编程的优先级:
3个固定优先级都是负数,即拥有最高优先级,为复位、NMI和HardFault
256个可编程优先级是指可以配置最多 256个优先级
但是芯片厂商在做基于 Cortex-M 内核的处理器时,不会配置出256个优先级,复杂度高还浪费钱,因此会通过优先级配置寄存器来缩减优先级数。比如,STM32就只使用高4位来实现0x00~0xF0共 16个优先级。
(2)优先级分组
以STM32
为例,在确定使用的优先级配置寄存器位数和优先级数之后,还要对优先级进行划分,分为:抢占优先级和子优先级。
- 具体划分方法就是确定抢占优先级和子优先级分别使用的位数
STM32
共使用了 4
位,就会有5
种分组情况,分别为
// 高位用来表示抢占优先级,低位用来表示子优先级
#define NVIC_PRIORITYGROUP_0 0x00000007U /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1 0x00000006U /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2 0x00000005U /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3 0x00000004U /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4 0x00000003U /*!< 4 bits for pre-emption priority
0 bits for subpriority */
STM32
使用的是分组4
,也就是4
位全用来表示抢占优先级,无子优先级。其实,FreeRTOS
的中断配置没有处理子优先级的这种情况,也必须配置为分组4
。
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // HAL_Init()调用
(3)优先级设置
简单提一点加深对前面内容的理解,每个中断的优先级都可以通过优先级寄存器配置优先级,比如前面任务调度时会配置SysTick
和PendSV
的中断优先级,就是通过这种方式。
1.3 中断屏蔽寄存器
(1)屏蔽范围
一开始便提到了需要重点关注的3
个寄存器:PRIMASK
、FAULTMASK
和BASEPRI
,都是用来不同程度地屏蔽中断的。
寄存器 | 屏蔽范围 |
---|---|
PRIMASK | 禁止除NMI和HardFault外的所有异常和中断 |
FAULTMASK | 禁止除NMI之外的所有异常和中断 |
BASEPRI | 只屏蔽优先级低于一定阈值的中断 |
- 注意:FreeRTOS开关中断就是操作 BASEPRI 寄存器实现的,它可以关闭低于某个阈值的中断,高于阈值的中断不会被关闭。
(2)开关中断
FreeRTOS
开关中断的函数都是宏定义,如下:
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
开中断就是向BASEPRI
寄存器中写入0
来开中断。
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI // ulBASEPRI为传入参数 0
}
}
关中断,向BASEPRI
寄存器中写入阈值,其实就是配置宏configMAX_SYSCALL_INTERRUPT_PRIORITY
。
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
msr basepri, ulNewBASEPRI
dsb
isb
}
}
1.4 中断配置宏
上一小节中提到了一个中断配置宏,这一小结来详细看看FreeRTOSConfig.h
中所有相关的宏。
- 注意:这里配置宏的参数还是以内核优先级大小设置的,即数值越小,优先级越高。
(1)configPRIO_BITS
MCU
使用的优先级位数,决定优先级数量,STM32使用的是4位。
#define configPRIO_BITS 4
(2)configLIBRARY_LOWEST_INTERRUPT_PRIORITY
可以设置的最低优先级,STM32
使用了4
位,支持16
个优先级,优先级最低的就是0xf,就是15
。
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0xf
(3)configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
FreeRTOS
系统可管理的最大优先级(stm32配置优先级分组的产生的优先级),即BASEPRI
相关的阈值优先级,但不是直接使用的这个宏!需要进行转换(将这个宏进行移位)。
STM32中的数字越小,优先级越高;数字越大,优先级越小。
FreeRTOS相反
//stm32配置uart,tim,gpio等中断要关注这个数值,优先级要小于它才能用FreeRTOS的API,也就是优先级数要大于等于5才能用FreeRTOS的API.
// 优先级数小于5的不归FreeRTOS管理,不能调用freeRTOS的API
//优先级高于它的不归FreeRTOS管理,不能调用freeRTOS的API
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
/* Set Interrupt Group Priority */ //优先级0——15
//STM32中的数字越小,优先级越高;数字越大,优先级越小。
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // HAL_Init()调用
STM32中的数字越小,优先级越高;数字越大,优先级越小。
FreeRTOS相反
(4)configKERNEL_INTERRUPT_PRIORITY
将可以设置的最低优先级数转换成寄存器值,即将 0xf
转换为 0xf0
,因为用了4
位,所以左移4
位。
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
(5)configMAX_SYSCALL_INTERRUPT_PRIORITY
将阈值优先级转换为寄存器值,即将数值 5
左移 4
位。这个宏才是真正写入BASEPRI
寄存器的值。
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
中断配置宏 | 功能 |
---|---|
configPRIO_BITS | 优先级使用位数 |
configLIBRARY_LOWEST_INTERRUPT_PRIORITY | 可以设置的最低优先级数值 |
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY | 中断屏蔽阈值优先级的数值 |
configKERNEL_INTERRUPT_PRIORITY | 可以设置的最低优先级寄存器值 |
configMAX_SYSCALL_INTERRUPT_PRIORITY | 中断屏蔽阈值优先级的寄存器值 |
2.临界区
(1)概述
临界区,就是必须完整运行、不能被打断的代码段。因此就用到了上面接上的开关中断,通过关闭低于阈值优先级的中断来实现。
进出临界区的函数也分为任务级和中断级:taskENTER_CRITICAL()
,taskEXIT_CRITICAL()
,taskENTER_CRITICAL_FROM_ISR()
,taskEXIT_CRITICAL_FROM_ISR( x )
。
(2)任务级临界区
任务级临界区进出函数都是宏定义,进入临界区的函数实际上调用的函数如下
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 ) // 只有计数值为0时才开启中断
{
portENABLE_INTERRUPTS();
}
}
上面代码中不仅是开关中断,还包含了一个进入临界区的计数值uxCriticalNesting
,只有计数值uxCriticalNesting
为0
时才开启中断,主要是为了保证在有多个临界区代码的时候不会因某一个临界区的退出而打乱其他临界区。
3.中断管理
这里是从一个应用角度,而不是源码角度来理解FreeRTOS
的中断管理,其实就是二值信号量和计数型信号量在中断与任务同步中的应用,主要参考延时中断和《FreeRTOS
中文使用教程》。
为了便于理解,将FreeRTOS
系统中的中断分为:内部中断和外部中断。外部中断就是应用开发时涉及的中断,采用延迟中断机制。
- 内部中断,即任务与内核之间进行的,如
SysTick
中断、SVC
中断和PendSV
中断 - 外部中断,
FreeRTOS
对外部事件的响应,如按键状态获取、传感器中断信号等
3.1 延迟中断机制
先来回顾下一般外部中断的执行流程:
- 首先系统配置好中断源、中断触发方式、中断服务函数、中断屏蔽位等,中断开始工作
- 当中断源满足中断触发条件(事件到达),
CPU
就会停下当前的工作,并保存当前工作状态 - 然后跳转到中断服务函数
ISR
,执行完毕后再恢复工作的状态,继续当前工作
一般对中断的响应都是在中断服务函数ISR
中进行,但当中断服务函数的工作量较大时,将占用较长时间,造成其他中断得不到及时响应,影响系统实时性和效率,因此ISR
的工作量应该尽可能的小。
若ISR
工作量较大,可以把工作从中断服务函数中移到任务中,中断和任务相互合作,任务可以阻塞等待中断的到来,ISR
只负责唤醒任务和少部分工作,而不做其它事情,于是延迟中断的概念就出现了。
可以认为,事件的到来触发了中断,但是ISR
并不进行实质性的处理,而是先唤醒负责这个事件的任务,让任务进行实质性的工作,于是认为中断被推迟执行。
3.2 延迟中断的实现
FreeRTOS的外部中断处理都是基于延迟中断机制,要实现延迟中断机制,需要使用二值信号量和计数型信号量,这两个信号值在之前介绍过。
3.2.1 二值信号量
二值信号量队列长度为1
,具有空和满两个状态,正好用来表征外部事件的到达与否,从而实现中断和任务之间的同步,并把事件处理工作转移到任务中执行。
一般的使用过程是:
- 创建二值信号量,事件的执行工作在任务的任务函数中,任务阻塞获得二值信号量(
take
操作) - 事件到达触发中断后,
ISR
中释放出二值信号量(give
操作) - 任务获得信号量被解除阻塞,然后执行,执行完毕后再次阻塞等待二值信号量
3.2.2 计数型信号量
使用二值信号量都是基于这样一种思想:中断到来给出了二值信号量,而任务能及时获得并处理。问题是能否真正地及时,答案是不一定,FreeRTOS是多任务并发执行,任务阻塞态的解除其实需要一段时间,并不是二值信号量一旦给出,任务就能马上获得,所以如果事件发生比较频繁,在这段时间内有多个相同的事件到达,那么中断就会发生多次,但是二值信号量只能记录一个状态,于是后来的中断就会覆盖前面的中断,发生了中断丢失,即使多个中断到达了,但是任务依然只看见一个。
如果某个中断比较频繁,为了能够记录多个同样中断的到达,需要使用计数信号量,可以认为是一个数组,数组内的每一个元素都是二值信号量,计数信号量能够记录多个状态,这样即使任务没有马上获得信号量,后来的中断也不会覆盖前面的中断,中断就不会丢失。
3.3 延迟中断的实时性
可能会认为延迟中断机制违背了FreeRTOS的实时操作系统的概念,其实不是,这种所谓的延迟就可以实现两种实时功能,实时分为硬实时和软实时,相同点是都必须要及时处理和必须完成,不同点是硬实时要求在某个时刻前一定要完成,而软实时要求尽可能快地完成,并没有给出绝对的时刻。
(1)硬实时
除了在中断服务函数中释放出信号量外,再把任务A的优先级置为最高,这样中断服务函数一旦结束,紧接着的就是任务A的执行,于是整体效果就是工作是在中断服务函数中执行的。
(2)软实时
实现软实时,也可以一样地改变任务A的优先级,但不需要改为最高优先级,可以低一点,也可以不改变,这样让调度器进行调度,虽然时间并不确定,但是这个中断确实被及时”处理“了,而且只要系统运行正常,任务A确实在尽可能快地完成工作,同时一定会执行完毕,满足软实时的要求。
4.总结
- 中断配置可以使
FreeRTOS
不管理高于阈值优先级的中断 FreeRTOS
的中断处理都使用延迟中断机制- 延迟中断机制的本质是把工作量移到任务里面
- 支撑延迟中断机制需要用到二值信号量和计数信号量
- 延迟中断机制可以实现硬实时和软实时