FreeRTOS教程3 中断管理
1、准备材料
STM32CubeMX软件(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
2、学习目标
本文主要学习 FreeRTOS 中断管理的相关知识,包括系统硬件中断、 FreeRTOS 可管理的中断、中断屏蔽和一些其他注意事项等知识
3、前提知识
3.1、STM32 的硬件中断
根据STM32CubeMX教程4 EXTI 按键外部中断实验 “3、中断系统概述表” 小节内容可知
- STM32F4 系列有 10 个系统中断和82个可屏蔽的外部中断
- 嵌套向量中断控制器(NVIC)采用 4 位二进制数表示中断优先级,这 4 位二进制数表示的中断优先级又分为了抢占优先级和次优先级
当启用FreeRTOS之后,NVIC中断分组策略采用 4 位抢占优先级且不可修改,对于 STM32 的硬件优先级来说,优先级数字越小表示优先级越高,最高优先级为0,如下所示为 STM32 的中断列表
3.2、FreeRTOS 可管理的中断
对于 STM32 处理器所有的硬件中断来说,其中有些可以被 FreeRTOS 软件管理,而有些特别重要的中断则不能够被 FreeRTOS 软件所管理,这很好理解,比如系统的硬件 Reset 中断,如果 Reset 中断可以被FreeRTOS所管理,那么在系统死机时用户需要硬件复位,但 FreeRTOS 不能响应最终导致无法复位从而卡死
那么哪些硬件中断可以被 FreeRTOS 所管理呢?
这由 configLIBRARY_LOWEST_INTERRUPT_PRIORITY (中断的最低优先级数值) 和 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY (FreeRTOS可管理的最高优先级) 两个参数决定,由于 NVIC 中断分组策略采用 4 位抢占优先级,因此中断最低优先级数值为 15 ,而 FreeRTOS 可管理的最高优先级默认设置为 5
当配置参数 configLIBRARY_LOWEST_INTERRUPT_PRIORITY = 15 , configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5 时,则表示在 STM32 的所有硬件中断中优先级为 0~4 的中断 FreeRTOS 不可管理,而对于中断优先级为 5~15 的中断 FreeRTOS 可以管理,具体如下图所示
3.3、何为上下文?
在操作系统和嵌入式系统中,上下文(Context)是指程序执行过程中的当前状态,包括所有的寄存器值、程序计数器(PC)值以及其他与执行环境相关的状态信息。上下文记录了程序执行的位置和状态,使得程序可以在中断、任务切换或函数调用等场景下进行正确的恢复和继续执行
在 FreeRTOS 中,上下文通常与任务(Task)或中断处理函数相关联。当任务切换发生时,当前任务的上下文会被保存,然后将控制权转移到下一个任务,该任务的上下文会被恢复以便继续执行。类似地,当中断发生时,处理器会保存当前执行任务的上下文,并在中断处理完毕后,恢复之前任务的上下文以继续执行。
3.4、在 ISR 中使用 FreeRTOS API 函数
3.4.1、中断安全版本 API
通常需要在中断服务例程 (ISR) 中使用 FreeRTOS API 函数提供的功能,但许多 FreeRTOS API 函数执行的操作在 ISR 内无效,比如能够让任务进入阻塞状态的 API 函数,如果从 ISR 调用这些 API 函数,因为它不是从任务调用,所以没有有效的调用任务使其进入阻塞状态
因此对于一些 API 函数,FreeRTOS 提供了两种不同版本,一种版本供任务使用,另一种版本供 ISR 使用,在 ISR 中使用的函数名称后面带有 “FromISR” 的后缀,关于这种设计的优缺点,感兴趣的读者可以自行阅读 “Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf” 6.2小节内容
3.4.2、xHigherPriorityTaskWoken 参数
xHigherPriorityTaskWoken 参数是中断安全版本 API 中常见的一个参数,该参数用于通知应用程序编写者在退出 ISR 时是否应该进行上下文切换,因为在执行某个中断期间,在进入中断时和退出中断后多个任务的状态可能发生了改变,也即可能存在中断某个任务,但返回另外一个任务的情况发生
如果通过 FreeRTOS API 函数解锁的任务的优先级高于运行状态任务的优先级,则根据 FreeRTOS 调度策略,应切换到更高优先级的任务,但究竟何时实际切换到更高优先级的任务则取决于调用 API 函数的上下文,有以下两种情况
- 如果 API 函数是从任务中调用的,那么在抢占式调度策略下,在 API 函数退出之前,API 函数内会自动切换到更高优先级的任务
- 如果 API 函数是从 ISR 中调用的,那么在中断中不会自动切换到更高优先级的任务,但是可以设置一个变量来通知应用程序编写者应该执行上下文切换,也就是 FreeRTOS 中断安全版本的 API 函数中经常见到的 xHigherPriorityTaskWoken 参数
如果应执行上下文切换,则中断安全 API 函数会将 pxHigherPriorityTaskWoken 设置为 pdTRUE ,而且只能将其设置为 pdTRUE ,所以 pxHigherPriorityTaskWoken 指向的变量必须在第一次使用之前初始化为 pdFALSE
如果不通过上述的方法在退出 ISR 前执行上下文切换,那么最坏的情况就是本来应该在退出 ISR 时切换到某个高优先级的任务进行执行,但现在只能将其转为就绪状态,直到下一个滴答定时器到来进行上下文切换其才会转为运行状态
3.4.3、portYIELD_FROM_ISR() 和 portEND_SWITCHING_ISR() 宏
在FreeRTOS教程2 任务管理 文章 "3.8、任务调度方法" 小节中,介绍了主动让位于另一项同等优先级任务的 API 函数 taskYIELD() ,它是一个可以在任务中调用来请求上下文切换的宏,portYIELD_FROM_ISR() 和 portEND_SWITCHING_ISR() 都是 taskYIELD() 的中断安全版本,他们两个的使用方式相同,并且执行相同的操作
3.4.4、简单总结
所以根据上面几个小节的叙述,如果我们希望在 ISR 中使用 FreeRTOS 提供的 API 函数,则应该使用这些 API 函数的中断安全版本,并且通过 xHigherPriorityTaskWoken 参数和 portYIELD_FROM_ISR() 宏在退出 ISR 之前进行可能的上下文切换,其可能的一种应用结构如下所示
/*一个可能的在中断中使用FreeRTOS API函数,然后进行上下文切换的例子*/
void An_Interrupt_Instance_Function(void)
{
//定义一个用于通知应用程序编程者是否应该进行上下文切换的变量,必须初始化为pdFALSE
BaseType_t highTaskWoken = pdFALSE;
//使用二值信号量API函数做演示
if(BinarySem_Handle != NULL)
{
//将中断安全版本API函数的pxHigherPriorityTaskWoken参数指向 highTaskWoken
xSemaphoreGiveFromISR(BinarySem_Handle, &highTaskWoken);
//根据highTaskWoken决定是否要进行上下文切换
portYIELD_FROM_ISR(highTaskWoken);
}
}
但是不是所有中断中都可以使用 FreeRTOS 提供的 API 函数,在 ISR 中使用 FreeRTOS API 函数总结如下所述
- 对于FreeRTOS可屏蔽的ISR中,如果要调用 FreeRTOS API 函数,则应该使用 FreeRTOS API 的中断安全版本函数(函数名末尾为FromISR或FROM_ISR),不可以使用任务级的API函数
- 对于FreeRTOS不可屏蔽的ISR中,不能够调用任何 FreeRTOS API函数
另外在 STM32CubeMX 软件 NVIC 配置界面中,如果在某个中断后面勾选了 “Uses FreeRTOS functions” 选项,根据上面的两点描述可知,只能在 FreeRTOS 可屏蔽的ISR中使用 FreeRTOS API 函数,所以该中断优先级可选范围会被强制到 15~5 之间,具体如下图所示
3.5、任务优先级和中断优先级
任务优先级为软件设置的一个属性,设置范围为1~(configMAX_PRIORITIES-1),数字越大优先级越高,在抢占式调度方式中高优先级的任务可以抢占低优先级的任务
中断优先级为硬件响应优先级,中断分组策略4位全用于抢占优先级的中断优先级数字设置范围为0-15,数字越小优先级越高
对于大多数的系统,其既会存在多个不同优先级的任务,同时也会存在多个不同优先级的中断,它们之间的执行顺序应该如下图所示
3.6、延迟中断处理
通常认为最佳实践是使 ISR 尽可能短,下面列出了可能的几条原因
- 即使任务被分配了非常高的优先级,它们也只有在硬件没有中断服务时才会运行
- ISR 会扰乱(添加“抖动”)任务的开始时间和执行时间
- 应用程序编写者需要考虑任务和 ISR 同时访问变量、外设和内存缓冲区等资源的后果,并防范这些资源
- 某些 FreeRTOS 端口允许中断嵌套,但中断嵌套会增加复杂性并降低可预测性,中断越短,嵌套的可能性就越小
什么时延迟中断处理?
中断服务程序必须记录中断原因,并清除中断。中断所需的任何其他处理通常可以在任务中执行,从而允许中断服务例程尽可能快地退出,这称为“延迟中断处理”,因为中断所需的处理从 ISR “延迟” 到任务。举个例子,比如在 ADC 周期采集中,当一轮采集完成之后,ADC 采集完成中断回调函数只负责将采集完成的值写入缓存区,然后由其他任务对缓存区中的数据进行更复杂处理
将中断处理推迟到任务还允许应用程序编写者相对于应用程序中的其他任务确定处理的优先级,并能够使用所有 FreeRTOS API 函数
如果中断处理被推迟的任务的优先级高于任何其他任务的优先级,则处理将立即执行,就像处理已在 ISR 本身中执行一样。这种场景如下图所示,其中任务 1 是普通应用程序任务,任务 2 是中断处理被推迟的任务
那么什么情况下需要进行延迟中断处理操作呢?
没有具体绝对的规则,在以下列出的几点情况下,将处理推迟到任务可能比较有用:
- 中断所需的处理并不简单。比如上面的举例,如果 ADC 仅仅需要采集值,那么在采集完成中断回调函数中将采集值写入缓存区即可,但是如果还需要对采集值进行复杂处理,那么最好推迟到任务中完成
- 中断处理是不确定的 - 这意味着事先不知道处理需要多长时间。
3.7、进行中断屏蔽
FreeRTOS 中为什么要屏蔽中断?
想象这样一个场景,当一个中等优先级的任务 TASK1 正在通过串口输出字符串 “Hello world!” 并且刚好输出到 ”Hello“ 时,另外一个高级优先级的任务 TASK2 突然抢占 TASK1 ,然后通过串口输出字符串 “lc_guo” ,当两个任务均执行完毕之后,你可能会在串口接受框中看到 ”Hellolc_guo world!“ 字符串
上述场景最终的结果与我们期望任务输出的字符串不符,在操作系统中称 TASK1 输出字符串的操作不是原子的,可能被打断的,因此在某些时候需要我们屏蔽掉中断以保证某些操作为原子的,可以连续执行完不被打断的,能够连续执行完且不被打断的程序段称其为临界段
那 FreeRTOS 中应该如何屏蔽中断?
在 FreeRTOS 中提供了三组宏函数方便用户在合适的位置屏蔽中断,在功能上屏蔽中断和定义临界代码段几乎是相同的,这几组函数通常成对使用
/**
* @brief 屏蔽FreeRTOS可管理的MCU中断
* @retval None
*/
void taskDISABLE_INTERRUPTS(void);
/**
* @brief 解除屏蔽FreeRTOS可管理的MCU中断
* @retval None
*/
void taskENABLE_INTERRUPTS(void);
/**
* @brief 开始临界代码段
* @retval None
*/
void taskENTER_CRITICAL(void);
/**
* @brief 退出临界代码段
* @retval None
*/
void taskEXIT_CRITICAL(void);
/**
* @brief 开始临界代码段的中断安全版本
* @retval 返回中断屏蔽状态uxSavedInterruptStatus,作为参数用于匹配的taskEXIT_CRITICAL_FROM_ISR()调用
*/
UBaseType_t taskENTER_CRITICAL_FROM_ISR(void);
/**
* @brief 退出临界代码段的中断安全版本
* @param uxSavedInterruptStatus:进入临界代码段时返回的中断屏蔽状态,taskENTER_CRITICAL_FROM_ISR()返回的值
* @retval None
*/
void taskEXIT_CRITICAL_FROM_ISR(UBaseType_t uxSavedInterruptStatus);
4、实验一:中断各种特性测试
4.1、实验目的
- 启动 RTC 周期唤醒中断,在周期唤醒中通过串口 USART1 不断输出当前 RTC 时间
- 创建任务 TASK_TEST ,在该任务中通过串口 USART1 输出提示信息即可
4.2、CubeMX相关配置
首先读者应按照 “FreeRTOS教程1 基础知识” 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求
本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读 “STM32CubeMX教程9 USART/UART 异步通信” ,如下图所示
本实验需要配置 RTC 周期唤醒中断,具体配置步骤和参数介绍读者可阅读 ”STM32CubeMX教程10 RTC 实时时钟 - 周期唤醒、闹钟A/B事件和备份寄存器“ 实验,此处不再赘述,这里参数、中断、时钟如下图所示
配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可
4.3、添加其他必要代码
按照 “STM32CubeMX教程9 USART/UART 异步通信” 实验 “6、串口printf重定向”小节增加串口 printf 重定向代码,具体不再赘述
然后在 rtc.c 文件下方重新实现 RTC 的周期唤醒回调函数,在该函数体内获取当前 RTC 时间并通过 USART1 将时间输出到串口助手,具体如下所述
/*周期唤醒回调函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
if(HAL_RTC_GetTime(hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
{
HAL_RTC_GetDate(hrtc, &sDate, RTC_FORMAT_BIN);
char str[22];
sprintf(str,"RTC Time= %2d:%2d:%2d\r\n",sTime.Hours,sTime.Minutes,sTime.Seconds);
printf("%s", str);
}
}
最后在 freertos.c 中添加任务函数体内代码即可,这里无需实现具体功能,仅通过 USART1 串口输出信息告知用户该任务已执行即可,具体如下所述
/*测试任务函数*/
void TASK_TEST(void *argument)
{
/* USER CODE BEGIN TASK_TEST */
/* Infinite loop */
for(;;)
{
printf("TASK_TEST\r\n");
osDelay(pdMS_TO_TICKS(500));
}
/* USER CODE END TASK_TEST */
}
4.4、烧录验证
烧录程序,打开串口助手,由于周期唤醒中断每隔 1s 执行依次,TASK_TEST 任务大概每隔 500ms 执行有一次,因此通过串口助手输出信息可以发现,每输出两次 ”TASK_TEST“ 就会输出一次当前 RTC 时间,和预期一致,具体如下图所示
上述任务流程应该如下图所示
4.5、各种特性测试
4.5.1、中断如果处理时间较长呢?
修改 RTC 周期唤醒中断函数体,在函数体末尾增加 1s 延时函数 HAL_Delay(1000); 模拟中断处理时间较长的情况,注意由于 RTC 周期唤醒中断优先级为 1 ,因此不能调用任何 FreeRTOS API 函数,包括延时函数,任务 TASK_TEST 不做任何改动,将修改后的程序重新编译烧录,观察串口助手的输出信息,具体如下图所示
可以发现,只有最开始测试任务 TASK_TEST 执行了两次,一旦 RTC 周期唤醒被执行那么之后测试任务便得不到执行,为什么会这样?RTC 周期唤醒每隔 1s 执行一次,执行一次之后延时 1s 占用处理器,不断循环,导致处理器没有任何机会去处理 TASK_TEST
上述任务流程应该如下图所示
4.5.2、任务如果处理时间较长呢?
修改测试任务 TASK_TEST 函数体,将其可以进入阻塞状态的延时函数 osDelay() 修改为 HAL_Delay() 函数,同时将延时时间从 500ms 增加至 2s ,用于模拟任务一直运行的情况,RTC 周期唤醒中断函数与 “4.3、添加其他必要代码” 小节一致,具体如下所示
void TASK_TEST(void *argument)
{
/* USER CODE BEGIN TASK_TEST */
/* Infinite loop */
for(;;)
{
printf("TASK_TEST\r\n");
HAL_Delay(2000);
}
/* USER CODE END TASK_TEST */
}
将修改后的程序重新编译烧录,观察串口助手的输出信息,具体如下图所示,从图中可知 RTC 运行正常,本来应该连续运行 2s 的 TASK_TEST 并没有影响到每隔 1s 输出 RTC 时间的周期唤醒中断,说明中断抢占了 TASK_TEST 得到了执行,也就是说虽然我们希望 TASK_TEST 测试任务连续运行 2s ,但是其并没有真正连续运行 2s,其在大概 1s 的时候被中断了
4.5.3、进行中断屏蔽
上述 ”4.5.2、任务如果处理时间较长呢?“ 小节阐述了一个问题,有时候我们希望我们的 TASK_TEST 任务是原子式执行的,不希望被中断打断,所以我们需要在任务函数体内屏蔽中断,修改 TASK_TEST 任务函数体如下所示
void TASK_TEST(void *argument)
{
/* USER CODE BEGIN TASK_TEST */
/* Infinite loop */
for(;;)
{
//进入临界段
//taskDISABLE_INTERRUPTS();
taskENTER_CRITICAL();
printf("TASK_TEST\r\n");
HAL_Delay(2000);
//退出临界段
//taskENABLE_INTERRUPTS();
taskEXIT_CRITICAL();
}
/* USER CODE END TASK_TEST */
}
同时别忘记,FreeRTOS能够屏蔽中断优先级为5~15,因此我们还需要在 STM32CubeMX 软件的 NVIC 中将 RTC 周期唤醒中断优先级设置到该范围内,这里笔者将其设置为了 7 ,具体如下图所示
将修改后的程序重新编译烧录,观察串口助手的输出信息,具体如下图所示,可以发现 RTC 周期唤醒函数每隔 2s 才会得到一次输出,这说明 TASK_TEST 任务整个函数体得到了连续运行,成功屏蔽掉了 RTC 周期唤醒中断
5、注释详解
注释1:图片来源于 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
参考资料
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf