在使用 freertos 的同时我们也可以使用其他硬件中断,不过这些硬件中断程序的设计需要注意一些我们此前可能不会在意的内容1。
在深入学习 FreeRTOS 如何管理中断之前,回顾这些 MCU 底层逻辑是非常关键的:
4 bits for pre-emption priority 0 bits for subpriority,即 4 个位全部用于抢占优先级,没有子优先级。启用 FreeRTOS 后,它会自动接管 NVIC 的配置,其中最核心的是这两个系统异常:
在复杂的嵌入式系统中,中断不可避免地会发生嵌套。为了既保证某些极高实时性硬件中断的响应速度,又维护 RTOS 内核数据结构的安全,FreeRTOS 划定了一道严格的“边界线”。
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY这个宏定义代表了 FreeRTOS 所能管理的最高逻辑优先级(默认数值通常设为 5)。
FromISR 后缀的中断安全 API。FromISR 的也不行!违规调用会直接破坏 RTOS 内核的链表结构,导致系统彻底崩溃。
在 CubeMX 我们可以看到 NVIC 一栏最右边有一列Uses FreeRTOS functions,勾选它表示我们将会在这些中断函数里调用 freertos 的函数,同时它的优先级可配置范围也变成了 5~15。但这个选项并不会更改生成代码的逻辑,只会改变配置范围。
同时我们能看到Pendable request for system service和System tick timer中断都是最低的 15,这表示只有在没有其他 ISR 运行的时候 freertos 才会进行上下文切换和响应调度请求。
你可能会看到有人将Time base: TIM6 global interrupt, DAC1 and DAC2 underrun error interrupts 的中断优先级从默认的 15 设置成 0,这是为了 是为了防止 HAL 库的底层逻辑发生死锁 (Deadlock)。
为了不让 HAL 库和 FreeRTOS 互相影响,STM32CubeMX 会强烈建议给 HAL 库重新找一个定时器当“备用手表”。通常我们会选择 TIM6(一个只具备基础定时功能的纯粹定时器)来专门负责给 HAL 库提供 1ms 的中断,也就是
Time base: TIM6 global interrupt。
假设我们没有把 TIM6 设置为 0,而是随便设置了一个较低的优先级 ,则可能会因为定时器中断被其它中断打断,二其它中断又依赖定时器中断,导致两个中断互相等待从而发生死锁:
HAL_UART_Transmit() 回复一段数据,并且设置了超时时间为 100ms。HAL_UART_Transmit() 内部会不断去检查 uwTick 变量的值,看看有没有超过 100ms。uwTick 增加的 TIM6 优先级是 15(优先级比串口低)。这意味着,只要 CPU 还在串口中断里,TIM6 就永远无法打断它去更新时间!uwTick 的值永远卡死了。HAL_UART_Transmit() 认为时间一直没走,永远等不到超时。整个 CPU 就死死地卡在这个串口中断的死循环里。taskDISABLE_INTERRUPTS()为了保护一段数据不被中断打断(比如所谓的“临界区”),我们可能经常会调用这个宏。 但它的名字其实有误导性:它并没有关闭单片机的所有中断。它在底层操作了 Cortex-M 的 BASEPRI 寄存器,仅仅只屏蔽了优先级在 5 到 15 之间的“受管辖中断”。
在 FreeRTOS 系统中,必须严格区分“硬件中断 (ISR)”和“软件任务 (Task)”这两个属于不同维度的概念。它们之间存在着绝对的层级关系和截然相反的优先级编号规则:
在 CPU 的执行权争夺上,硬件永远具有最高特权:
可以理解为rtos的任务优先级是指在硬件优先级为15的情况下,通过软件的方式又给这些任务划分了一套优先级。
15 级是指 因为系统的调度器(PendSV)处于硬件的最低优先级 15,所以只有当 CPU 闲下来(没有外部硬件打扰)时,调度器才有机会介入,并在它自己用软件划分出的那一套“ 就绪链表 ”中,挑选一个最高级别的任务扔给 CPU 执行。
我们前面还说过 freertos 可以“屏蔽中断”,但是那仅限于 freertos 处于临界状态时,才会临时修改中断屏蔽寄存器来屏蔽中断,平时正常执行任务时不会屏蔽。关于临界状态,我们在下面会讲解。

Delay 延时。所以设计 ISR 函数时,要尽量精简操作,不要过多的占用 CPU 时间。一般情况下 ISR 只负责数据采集收发,数据处理的逻辑应放在用户的任务里执行。
在多任务和多中断交织的复杂系统中,一段代码随时可能被打断。但有些操作(例如修改一个全局链表、更新一个多字节的传感器数据结构)必须一气呵成。
临界段 (Critical Section) 的定义: 任务中某些非常关键、需要连续执行完、绝对不希望被其他高优先级任务或任何(受管辖的)ISR 函数打断的程序段。
FreeRTOS 提供了两组不同级别的宏来实现这种保护:
1. 基础的全局中断开关
taskDISABLE_INTERRUPTS():屏蔽 MCU 的部分中断。(结合上一节知识:它实际上是修改了 BASEPRI 寄存器,屏蔽了优先级在 5~15 之间的中断)。taskENABLE_INTERRUPTS():解除中断屏蔽。2. 专业的临界区宏(推荐使用)
taskENTER_CRITICAL():开始临界代码段。taskEXIT_CRITICAL():结束临界代码段。taskENTER_CRITICAL_FROM_ISR():在中断里进入临界区。taskEXIT_CRITICAL_FROM_ISR(x):在中断里退出临界区。假设你有函数 A 和函数 B,它们都需要保护自己的代码。
void Function_B(void) {
taskDISABLE_INTERRUPTS(); // 关中断
// ... B 的核心操作 ...
taskENABLE_INTERRUPTS(); // 开中断
}
void Function_A(void) {
taskDISABLE_INTERRUPTS(); // 关中断保护 A
Function_B(); // A 调用了 B
// ... A 剩下的操作 (此时中断已经被 B 提前打开了!保护失效!)
taskENABLE_INTERRUPTS();
}当你有两个都需要开关中断屏蔽的函数互相嵌套时,其中一个函数的屏蔽会因为另一个函数的结束屏蔽操作而提前失效。
而taskENTER_CRITICAL和taskEXIT_CRITICAL()虽然在内部也会调用taskDISABLE_INTERRUPTS,但它会额外实现一个计数器来处理嵌套,只有当计数器归零时进行退出中断屏蔽才会视为一个有效操作。
如上文所示,在 FreeRTOS 中,所有的 API 函数被严格划分为了两个版本,这在其他很多实时操作系统中是不多见的:
xQueueSend()、xSemaphoreGive()。FromISR 后缀的函数,或者带有 FROM_ISR 后缀的宏函数,例如 xQueueSendFromISR()、xSemaphoreGiveFromISR()。它们也被称为“中断安全 API 函数”。ISR 执行的时候是不能进行任务调度的。
假设你在一个串口接收中断 (ISR) 里调用了普通的 xQueueReceive() 函数,并且这个队列目前是空的。 普通的 API 发现队列为空时,会理所当然地试图把当前任务挂起(进入阻塞状态),并请求调度器去运行别的任务。 但是现在 CPU 正在执行的是硬件中断,根本没有“当前任务”的上下文可以挂起。这种“试图在硬件中断里引发软件阻塞”的行为,会直接导致 CPU 指针错乱,引发 HardFault 死机。
带有 FromISR 的函数则去掉了所有可能引起阻塞的代码逻辑。如果队列满了写不进去,它会立刻返回一个错误码,而绝对不会死等。
FromISR 的安全 API,也绝对不能在优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(例如优先级为 0~4)的中断 ISR 里调用它们。BASEPRI 机制(通常阈值设为 5)。xQueueSend)。FromISR 后缀的 API,且记得要有变量接住并处理返回值。FromISR 都不能用,只能老老实实操作裸机寄存器。