文章目录
一、Cortex-M 中断
1.1 中断简介
中断是微控制器一个很常见的特性,中断由硬件产生,当中断产生以后 CPU 就会中断当前的流程转而去处理中断服务,Cortex-M 内核的 MCU 提供了一个用于中断管理的嵌套向量中断控制器(NVIC)。
Cotex-M3 的NVIC 最多支持 240 个IRO(中断请求)1个不可屏蔽中断NMI1个 Svstick(滴答定时器)定时器中断和多个系统异常。
关于中断的更多介绍,可以见博主的STM32速成笔记专栏中断篇。
1.2 优先级分组定义
当多个中断来临的时候处理器应该响应哪一个中断是由中断的优先级来决定的,高优先级的中断(优先级编号小)肯定是首先得到响应,而且高优先级的中断可以抢占低优先级的中断,这个就是中断嵌套。Cortex-M 处理器的有些中断是具有固定的优先级的,比如复位、NMI、HardFault,这些中断的优先级都是负数,优先级也是最高的。
Cortex-M 处理器有三个固定优先级和 256 个可编程的优先级,最多有 128 个抢占等级,但是实际的优先级数量是由芯片厂商来决定的。但是,绝大多数的芯片都会精简设计的,以致实际上支持的优先级数会更少,如 8 级、16 级、32 级等,比如 STM32 就只有 16 级优先级。
移植FreeRTOS时,STM32的中断分组配置为4,也就是全部都是抢占优先级。有 0~15 共 16 个优先级。因为FreeRTOS 的中断配置没有处理亚优先级这种情况,所以只能配置为组 4,直接就 16 个优先级
。
二、用于中断屏蔽的特殊寄存器
在 STM32 上移植 FreeRTOS 的时候需要重点关注 PRIMASK、FAULTMASK 和 BASEPRI 这三个寄存器。
2.1 PRIMASK 寄存器
有时候需要暂时屏蔽所有的中断,执行一些对时序要求严格的任务,这个时候就可以使用 PRIMASK 寄存器。PRIMASK 用于禁止除 NMI 和 HardFalut 外的所有异常和中断。该寄存器可以通对MRS和MSR访问
- 关中断
MOVS R0, #1
MSR PRIMASK, R0 ;//将 1 写入 PRIMASK 禁止所有中断
- 开中断
MOVS R0, #0
MSR PRIMASK, R0 ;//将 0 写入 PRIMASK 以使能中断
或者可以使用 CPS(修改处理器状态)指令修改 PRIMASK 寄存器的数值实现中断的开关
CPSIE I; //清除 PRIMASK(使能中断)
CPSID I; //设置 PRIMASK(禁止中断)
在使用库函数进行开发时,在sys.c文件中定义了相关函数。
//关闭所有中断
void INTX_DISABLE(void)
{
__ASM volatile("cpsid i");
}
//开启所有中断
void INTX_ENABLE(void)
{
__ASM volatile("cpsie i");
}
2.2 FAULTMASK 寄存器
FAULTMASK 比PRIMASK 更狠,它可以连 HardFaut 都屏蔽掉,使用方法和 PRIMASK 类似,需要注意的是FAULTMASK 会在退出时自动清零。
2.3 BASEPRI寄存器
在FreeRTOS中,并不会使用上面的两个寄存器来开关中断,而是使用BASEPRI寄存器。
因为再更精巧的设计中,需要对中断的屏蔽进行更加细腻地控制。比如只屏蔽优先级低于某一个阈值的中断——优先级在数字上大于等于某一个数
。这个作为阈值的优先级值存储在BASEPRI寄存器。如果向 BASEPRI 写 0 的话就会停止屏蔽中断。
任务优先级数值越大,优先级越大。但是中断优先级,数值越大,优先级越低。
比如,我们要屏蔽优先级不高于 0X60 的中断
MOV R0, #0X60
MSR BASEPRI, R0
如果需要取消 BASEPRI 对中断的屏蔽,直接向BASEPRI寄存器写0
MOV R0, #0
MSR BASEPRI, R0
在FreeRTOS中,开启和关闭中断的函数为
portENABLE_INTERRUPTS(); // 打开中断
portDISABLE_INTERRUPTS(); // 关闭中断
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
关闭中断,方法是往BASEPRI寄存器写值,写的是ulNewBASEPRI,也就是configMAX_SYSCALL_INTERRUPT_PRIORITY。
/*-----------------------------------------------------------*/
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
section. */
msr basepri, ulNewBASEPRI
dsb
isb
}
}
/*-----------------------------------------------------------*/
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 //中断最低优先级
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 //系统可管理的最高中断优先级
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
这里设定的系统可管理的中断优先级是5,也就是说优先级低于5的(5~15)的中断会被屏蔽。
开启中断也就是直接给BASEPRI寄存器写0。
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
}
}
通过上面的介绍可知,0~4的中断优先级不属于FreeRTOS管理。在0~4优先级的中断服务函数中不能使用FreeRTOS的AP函数。比如调用在中断中解挂任务的函数。以“FromISR”结尾的API函数是在中断服务函数中使用的API函数,这些都不能在0~4的优先级的中断服务函数中使用。
三、临界段代码
临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段,比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断。FreeRTOS 系统本身就有很多的临界段代码这些代码都加了临界段代码保护,我们在写自己的用户程序的时候有些地方也需要添加临界段代码保护。
FreeRTOS 与临界段代码保护有关的函数有 4 个,taskENTER_CRITICAL(),taskEXIT_CRITICAL(),taskENTER_CRITICAL_FROM_ISR(),taskEXIT_CRITICAL_FROM ISR()。这四个函数其实是宏定义,在 task.h 文件中有定义。这四个函数的区别是前两人是任务级的临界段代码保护,后两个是中断级的临界段代码保护。
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
3.1 任务级临界段代码保护
taskENTER_CRITICAL()和 taskEXIT_CRITICAL()是任务级的临界代码保护。一个是进入临界段,一个是退出临界段,这两个函数是成对使用的。任务级临界代码保护使用方法如下
void taskcritical_test(void)
{
while(1)
{
taskENTER_CRITICAL(); // 进入临界区
// 临界区代码
taskEXIT_CRITICAL(); // 退出临界区
vTaskDelay(1000);
}
}
注意临界区代码一定要精简!因为进入临界区会关闭中断,这样会导致优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断得不到及时响应。
3.2 中断级临界段代码保护
函数 taskENTER_CRITICAL_FROM_ISR()和 askEXIT_CRITICAL_FROM_ISR()中断级别临界段代码保护,是用在中断服务程序中的,而且这个中断的优先级一定要低于configMAX_SYSCALL_INTERRUPT_PRIORITY。原因前面已经说了。这两个函数在文件 task.h中有如下定义。
中断级临界代码保护使用方法如下
// 定时器3中断服务函数
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) // 溢出中断
{
status_value=taskENTER_CRITICAL_FROM_ISR();
// 临界区代码
taskEXIT_CRITICAL_FROM_ISR(status_value);
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); // 清除中断标志位
}
四、中断测试
这里测试一下在第二部分介绍的,FreeRTOS可以屏蔽5~15优先级的中断,但是不能屏蔽0到4优先级的中断。我们使能两个中断,一个是定时器中断,中断优先级为6。在定时器的中断服务函数中,将LED1的状态取反。另一个是WK UP的外部中断,中断优先级为3。在外部中断的中断服务函数中,将LED2的状态翻转。利用按键KEY0和KEY1来控制中断的开关。测试开启中断屏蔽后,外部中断和定时器中断的状态。
4.1 中断测试程序设计
定时器配置程序和中断服务函数如下
/*
*==============================================================================
*函数名称:TIM2_Iint
*函数功能:初始化定时器2
*输入参数:per:自动重装载值;psc:预分频系数
*返回值:无
*备 注:无
*==============================================================================
*/
void TIM2_Iint (u16 per,u16 psc)
{
// 结构体定义
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); // 使能TIM2时钟
TIM_TimeBaseInitStructure.TIM_Period = per; // 自动装载值
TIM_TimeBaseInitStructure.TIM_Prescaler = psc; // 分频系数
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 不分频
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; // 设置向上计数模式
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); // 开启定时器中断
TIM_ClearITPendingBit(TIM2,TIM_IT_Update); // 使能更新中断
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 定时器中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=6; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =0; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // IRQ通道使能
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2,ENABLE); // 使能定时器
}
// TIM2中断服务函数
void TIM2_IRQHandler(void) // TIM2中断
{
// 产生更新中断
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
Med_Led_StateReverse(LED0); // LED状态翻转
}
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除TIM2更新中断标志
}
外部中断配置函数和中断服务函数如下
/*
*==============================================================================
*函数名称:Exit_Init
*函数功能:初始化外部中断
*输入参数:无
*返回值:无
*备 注:无
*==============================================================================
*/
void Exit_Init (void)
{
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO时钟
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); //选择GPIO管脚用作外部中断线路
//EXTI0 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //EXTI0中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =0; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
EXTI_InitStructure.EXTI_Line=EXTI_Line0; // EXIT0
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt; // 中断
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising; // 上升沿触发
EXTI_InitStructure.EXTI_LineCmd=ENABLE; // 使能
EXTI_Init(&EXTI_InitStructure);
}
/*
*==============================================================================
*函数名称:EXTI0_IRQHandler
*函数功能:外部中断0中断服务函数
*输入参数:无
*返回值:无
*备 注:无
*==============================================================================
*/
void EXTI0_IRQHandler(void)
{
// 如果EXIT0中断标志位被置1
if(EXTI_GetITStatus (EXTI_Line0)==1)
{
Med_Led_StateReverse(LED1); // LED状态翻转
}
EXTI_ClearITPendingBit (EXTI_Line0); // 清除中断标志位
}
main.c程序如下
//任务优先级
#define START_TASK_PRIO 1
//任务堆栈大小
#define START_STK_SIZE 120
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task (void *pxCreatedTask);
//任务优先级
#define KEY_TASK_PRIO 2
//任务堆栈大小
#define KEY_STK_SIZE 120
//任务句柄
TaskHandle_t KEYTask_Handler;
//任务函数
void key_task (void *pxCreatedTask);
int main(void)
{
Med_Mcu_Iint(); // 系统初始化
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
void start_task (void *pxCreatedTask)
{
taskENTER_CRITICAL(); // 进入临界区
//创建按键任务
xTaskCreate((TaskFunction_t )key_task, //任务函数
(const char* )"key_task", //任务名称
(uint16_t )KEY_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )KEY_TASK_PRIO, //任务优先级
(TaskHandle_t* )&KEYTask_Handler); //任务句柄
// 开始任务只需要执行一次,执行完成后删除即可
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); // 退出临界区
}
void key_task (void *pxCreatedTask)
{
u8 keyValue = 0; // 获取返回键值
while (1)
{
keyValue = Med_KeyScan(); // 按键扫描
// KEY0按下
if (keyValue == 2)
{
printf ("关闭中断!\r\n");
portDISABLE_INTERRUPTS(); // 关闭中断
}
// KEY1按下
else if (keyValue == 3)
{
printf ("开启中断!\r\n");
portENABLE_INTERRUPTS(); // 开启中断
}
}
}
4.2 中断测试结果
按下按键KEY0,关闭中断时,定时器中断被屏蔽,LED1停止闪烁。按下KEY1,开启中断时,定时器中断恢复,LED1开始闪烁。即使关闭了中断,外部中断依旧可以控制LED2状态翻转。但是出现了一个异常现象。在关闭中断后,定时器中断被屏蔽,LE1不再闪烁。但是,当按下WK UP,触发外部中断时,定时器中断又恢复了
。
修改程序,将外部中断的优先级设置为7。在一开始初始化系统后屏蔽中断。发现外部中断依旧可以触发。