FreeRTOS中断配置和临界段
中断简介
STM32的中断很重要,它不同于if、switch等条件检测语句,它是由硬件产生的。在程序中,代码默认按顺序执行,当遇到中断触发时,就会打断当前执行代码并且调到中断服务函数中去执行中断任务。
这种情况在汇编语言中的表现形式一般是:
- 保存用到的寄存器(保护现场)
- 处理终端
- 回复用到的寄存器(恢复现场)
- 中断返回
Cortex-M处理器内核提供了一个用于中断管理的嵌套向量中断控制器(NVIC),最多支持240个中断请求(IRQ)、一个不可屏蔽中断(NMI)、一个滴答定时器中断(Systick)和多个系统异常(硬件错误、总线错误、使用错误、PendSV等等)。Cortex-M 处理器有多个用于管理中断和异常的可编程寄存器, 这些寄存器大多数都在 NVIC 和系统控制块(SCB)中。
中断优先级
Cortex-M处理器有三个固定优先级和256个可编程优先级,最多有128个抢占优先级,但具体数目需要由芯片厂商自行设定。绝大多数芯片都会进行剪裁,一般优先级个数是8、16和32个。Cortex-M处理器最多有256个可编程优先级,说明控制优先级的寄存器共有8位,这个优先级是MSB(最高位)对齐的,所以一般都是前3或4位是用来表达优先级的。
例如:
Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 |
---|---|---|---|---|---|---|---|
置位 | 置位 | 置位 | 置位 | 返回0 | 返回0 | 返回0 | 返回0 |
STM32就使用了前四位作为优先级表达位,因此STM32就有16个可编程优先级:0x00(最高优先级)、0x10-0xF0。此外中断可以被屏蔽,这时需要用到3个特殊寄存器:PRIMASK、FAULTMASK、BASEPRI。
PRIMASK 和 FAULTMASK 寄存器
在需要进行对时序严格要求的任务时,我们需要暂时屏蔽所有中断来保证任务的精确完成,这时可以使用 PRIMASK 寄存器,PRIMASK 用于禁止除复位 、NMI 和 HardFalut 外的所有异常和中断,可以通过汇编语言CPS指令来控制PRIMASK。
CPSIE I; //清除 PRIMASK(使能中断)
CPSID I; //设置 PRIMASK(禁止中断)
以上代码等同于
MOVS R0, #0
MSR PRIMASK, R0 ;//将 0 写入 PRIMASK 以使能中断
MOVS R0, #1
MSR PRIMASK, R0 ;//将 1 写入 PRIMASK 禁止所有中断
BASEPRI 寄存器
当需要精准控制低于哪些优先级的中断需要关闭时,这时就可以使用BASEPRI寄存器,这个寄存器可以进行更加准确的屏蔽。例如屏蔽优先级0x80及以下的中断:
MOV R0, #0X60
MSR BASEPRI, R0
解除屏蔽只需要将0写入BASEPRI寄存器即可
MOV R0, #0
MSR BASEPRI, R0
FreeRTOS中断配置宏
configPRIO_BITS
优先级表达位,STM32为前4位,所以此宏为4
configLIBRARY_LOWEST_INTERRUPT_PRIORITY
此宏表示最低优先级,STM32最低优先级为0~15,所以此宏为15,当处理器为其他芯片时,需要按手册修改。
configKERNEL_INTERRUPT_PRIORITY
该宏定义如下:
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
该宏是设置内核中断优先级,内容是将 configLIBRARY_LOWEST_INTERRUPT_PRIORITY左移四位,因为中断表达需要用高4位来表达,所以需要左移4位。除此外,也可以直接写为:
#define configKERNEL_INTERRUPT_PRIORITY 0xF0
宏 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 )
PendSV和SysTcik的中断优先级设置是操作0xE000-0xED20地址的,这样一次写入的是个32位的数据,SysTick和PendSV的优先级寄存器分别对应这个32位数据的最高8位和次高8位,因此是一个左移16位,一个左移24位。
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
此宏定义了FreeRTOS可管理的最大优先级,程序生成后默认设置的是5,所以优先级高于5(优先级数小于5)的都不归FreeRTOS管。
configMAX_SYSCALL_INTERRUPT_PRIORITY
此宏是由configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY左移4位而来的,原因和宏configKERNEL_INTERRUPT_PRIORITY一样。此宏设置好以后,低于此优先级的中断可以安全的调用 FreeRTOS的API函数,高于此优先级的中断FreeRTOS是不能禁止的,中断服务函数也不能调用FreeRTOS的API函数!
FreeRTOS开关中断
FreeRTOS开关中断函数为portENABLE_INTERRUPTS()和portDISABLE_INTERRUPTS(),这两个函数其实是宏定义,在portmacro.h中有定义,如下:
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)
函数vPortBASEPRI()传递了一个0,这就对应了上面说到的,开启中断是将0写入BASEPRI寄存器。
临界段代码
临界段代码也称为临界区,是指哪些必须完成运行,且不能中途打断的代码,比如某些外设的初始化过程需要遵守严格的时序,因此初始化过程不能被打断,所以我们在进行外设初始化时可以添加临界段代码。
FreeRTOS与临界段代码保护有关的函数有4个:taskENTER_CRITICAL()、
和taskEXIT_CRITICAL()、taskENTER_CRITICAL_FROM_ISR()、taskEXIT_CRITICAL_FROM_ISR(),这四个函数其实是宏定义,在task.h文件中有定义。这四个函数的区别是前两个是任务级的临界段代码保护,后两个是中断级的临界段代码保护。
任务级临界段代码保护
taskENTER_CRITICAL()和taskEXIT_CRITICAL()是任务级的临界代码保护,一个是进入临界段,一个是退出临界段,这两个函数是成对使用的,这函数的定义如下:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
而portENTER_CRITICAL()和portEXIT_CRITICAL()也是宏定义,在文件 portmacro.h中有定义。
任务级临界代码保护使用方法如下:
void CriticalTask_TEST(void const * argv)
{
taskENTER_CRITICAL(); //进入临界区
total_num += 0.01f;
printf("total_num 的值为: %.4f\r\n",total_num);
taskEXIT_CRITICAL(); //退出临界区
vTaskDelay(1000);
}
当进入临界区时,中断被屏蔽,临界区代码无法被打断,只有当所有的临界段代码都退出以后才会使能中断!
注:当进入临界区时,优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断得不到及响应,所以临界区代码一定要精简。
中断级临界段代码保护
函数taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()中断级别临界段代码保护,是用在中断服务程序中的,而且这个中断的优先级一定要低于configMAX_SYSCALL_INTERRUPT_PRIORITY。这两个函数在文件task.h中有如下定义:
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL_FROM_ISR(x) portCLEAR_INTERRUPT_MASK_FROM_ISR(x)
中断级临界代码保护使用方法如下:
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
status_value=taskENTER_CRITICAL_FROM_ISR(); //进入临界区
total_num += 1;
printf("float_num 的值为: %d\r\n",total_num);
taskEXIT_CRITICAL_FROM_ISR(status_value); //退出临界区
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
FreeRTOS中断测试实验
创建工程
打开串口1,参数默认。
配置是时钟树
配置定时器3和定时器5
配置定时器中断优先级
配置FreeRTOS并创建任务
创建工程
查看代码
打开工程可以在stm32f1xx_hal_cortex.c文件中找到定制器中断优先级配置代码。
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{
if(tim_baseHandle->Instance==TIM3)
{
/* USER CODE BEGIN TIM3_MspInit 0 */
/* USER CODE END TIM3_MspInit 0 */
/* TIM3 clock enable */
__HAL_RCC_TIM3_CLK_ENABLE();
/* TIM3 interrupt Init */
HAL_NVIC_SetPriority(TIM3_IRQn, 4, 0);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
/* USER CODE BEGIN TIM3_MspInit 1 */
/* USER CODE END TIM3_MspInit 1 */
}
else if(tim_baseHandle->Instance==TIM5)
{
/* USER CODE BEGIN TIM5_MspInit 0 */
/* USER CODE END TIM5_MspInit 0 */
/* TIM5 clock enable */
__HAL_RCC_TIM5_CLK_ENABLE();
/* TIM5 interrupt Init */
HAL_NVIC_SetPriority(TIM5_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(TIM5_IRQn);
/* USER CODE BEGIN TIM5_MspInit 1 */
/* USER CODE END TIM5_MspInit 1 */
}
}
可以从中看出TIM3优先级为4,大于FreeRTOS所能管理的优先级,TIM优先级为5,恰好在FreeRTOS所能管理的优先级之中。
先在main.c中将定时器回调函数代码补全:
定时器回调函数代码补全
打开main.c文件,补全HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)函数中缺少的定时器3和定时器5的代码。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* USER CODE BEGIN Callback 0 */
if(htim->Instance == TIM3)
{
printf("TIM3 输出.......\r\n");
}
/* USER CODE END Callback 0 */
if (htim->Instance == TIM1) {
HAL_IncTick();
}
/* USER CODE BEGIN Callback 1 */
if(htim->Instance == TIM5)
{
printf("TIM5 输出.......\r\n");
}
/* USER CODE END Callback 1 */
}
编写测试代码
打开freertos.c文件,找到void extitask_test(void const * argument)函数,添加自己的代码:
void extitask_test(void const * argument)
{
static uint32_t total_num = 0;
/* USER CODE BEGIN extitask_test */
/* Infinite loop */
for(;;)
{
total_num += 1;
if(total_num == 5)
{
printf("关闭中断.............\r\n");
portDISABLE_INTERRUPTS();
HAL_Delay(5000);
printf("打开中断.............\r\n");
portENABLE_INTERRUPTS();
}
}
/* USER CODE END extitask_test */
}
查看运行结果
打开串口助手可以看到当中断关闭时只有定时器3在工作,而定时器5则失效,当中断重新打开时,定时器3和定时器5又重新一起计时了。
小结
当我们使用FreeRTOS时,要注意有些外设的驱动代码需要严格连续执行,不能被打断,这时我们可以使用临界段代码来保护现场。但是只需要屏蔽某些优先级的中断时,就可以使用中断屏蔽代码。
本节工程创建的部分步骤有所省略,详细步骤可参考STM32CubeMX+FreeRTOS学习笔记(一)。