[ 附件影像 RECORD ]
FreeRTOS学习笔记 · 第九讲:3.1freertos任务与中断服务程序

FreeRTOS学习笔记 · 第九讲:3.1freertos任务与中断服务程序

[ SCAN_URL ]
[ 归档时间 ]:2026-03-24 11:24 [ 课题责任人 ]:文止墨 [ 档案分类 ]:嵌入式, 文章, 编程
*1774351496*

在使用 freertos 的同时我们也可以使用其他硬件中断,不过这些硬件中断程序的设计需要注意一些我们此前可能不会在意的内容1

在深入学习 FreeRTOS 如何管理中断之前,回顾这些 MCU 底层逻辑是非常关键的:

硬件中断基础回顾 (结合 STM32 与 FreeRTOS)

MCU 硬件中断基本特性

  • 硬件属性:中断是微控制器(MCU)自带的硬件特性,属于底层机制。
  • 中断服务例程 (ISR):系统中存在的每一个中断,都会对应一个专属的中断服务程序(ISR)来处理具体事务。

STM32 的 NVIC 优先级策略

  • 数字越小,级别越高:这是 STM32 中断配置的核心法则。优先级数字设得越低,它在硬件层面的优先级别就越高(例如,优先级为 5 的中断可以打断优先级为 15 的中断)。2
  • 4位分组机制:STM32 使用 4 个比特位(bits)来配置优先级的分配策略。系统默认被配置为 4 bits for pre-emption priority 0 bits for subpriority,即 4 个位全部用于抢占优先级,没有子优先级。

FreeRTOS 的“调度双擎”:SysTick 与 PendSV

启用 FreeRTOS 后,它会自动接管 NVIC 的配置,其中最核心的是这两个系统异常:

  • SysTick (系统滴答定时器):它的主要职责是“打卡”。FreeRTOS 会在 SysTick 中断里进行任务调度的申请(更新系统节拍,检查是否有任务延时到期或时间片耗尽等)。
  • PendSV (可悬起系统调用):它的主要职责是“干活”。FreeRTOS 真正的任务调度动作(上下文切换,即保存当前任务寄存器并恢复下一个任务寄存器),全部都是在 PendSV 中断里完成的。
  • 为什么设为最低优先级 (15)?:在 CubeMX 列表中,PendSV 和 SysTick 的优先级都被设定为 15(系统最低)。这种设计的精妙之处在于:保证硬件的绝对实时性。如果外部有紧急的硬件事件(如电机编码器脉冲、串口数据到达),CPU 会立刻去处理。只有当所有紧急的外部硬件中断都处理完毕,系统相对空闲时,才会执行最低优先级的 PendSV 去做纯软件层面的任务切换。

FreeRTOS 中断嵌套与系统调用边界机制

在复杂的嵌入式系统中,中断不可避免地会发生嵌套。为了既保证某些极高实时性硬件中断的响应速度,又维护 RTOS 内核数据结构的安全,FreeRTOS 划定了一道严格的“边界线”。

核心边界宏:configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY

这个宏定义代表了 FreeRTOS 所能管理的最高逻辑优先级(默认数值通常设为 5)。

  • 受控区(优先级数值 ≥ 5,即逻辑优先级 ≤ 5)
  • 在这个范围内的中断(例如优先级数值设置为 5, 6, 7… 15),都在 FreeRTOS 的管理权限之内。
  • 规则:只有在这些中断的 ISR (中断服务程序) 里,才可以安全地调用 FreeRTOS 提供的 API 函数。注意,必须是带有 FromISR 后缀的中断安全 API。
  • 化外之地(优先级数值 < 5,即逻辑优先级 > 5)
  • 在这个范围内的中断(例如优先级数值设置为 0, 1, 2, 3, 4),其执行级别比操作系统的核心还要高。FreeRTOS 调度器即使在执行最关键的临界区代码(关闭了受控区的中断)时,也无法阻挡这些高优先级中断的触发。
  • 警告绝对不要在大于此优先级(数值小于 5)的中断 ISR 里调用任何 FreeRTOS 的 API 函数,即使是带 FromISR 的也不行!违规调用会直接破坏 RTOS 内核的链表结构,导致系统彻底崩溃。
  • 警告该宏的数值本身绝对不允许被设置为 0。

在 CubeMX 我们可以看到 NVIC 一栏最右边有一列Uses FreeRTOS functions,勾选它表示我们将会在这些中断函数里调用 freertos 的函数,同时它的优先级可配置范围也变成了 5~15。但这个选项并不会更改生成代码的逻辑,只会改变配置范围。

同时我们能看到Pendable request for system serviceSystem 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,而是随便设置了一个较低的优先级 ,则可能会因为定时器中断被其它中断打断,二其它中断又依赖定时器中断,导致两个中断互相等待从而发生死锁:

假设配置了一个串口接收中断(USART1),优先级设为 5
  • 当串口收到数据,CPU 进入了 USART1 的中断服务程序 (ISR)。
  • 在这个 USART1 的 ISR 里面,可能调用了一个 HAL 库的函数,比如 HAL_UART_Transmit() 回复一段数据,并且设置了超时时间为 100ms。
  • HAL_UART_Transmit() 内部会不断去检查 uwTick 变量的值,看看有没有超过 100ms。
  • 灾难:因为 USART1 的优先级是 5,而负责让 uwTick 增加的 TIM6 优先级是 15(优先级比串口低)。这意味着,只要 CPU 还在串口中断里,TIM6 就永远无法打断它去更新时间!
  • 结果uwTick 的值永远卡死了。HAL_UART_Transmit() 认为时间一直没走,永远等不到超时。整个 CPU 就死死地卡在这个串口中断的死循环里。

taskDISABLE_INTERRUPTS()

为了保护一段数据不被中断打断(比如所谓的“临界区”),我们可能经常会调用这个宏。 但它的名字其实有误导性:它并没有关闭单片机的所有中断。它在底层操作了 Cortex-M 的 BASEPRI 寄存器,仅仅只屏蔽了优先级在 5 到 15 之间的“受管辖中断”

任务与 ISR 函数的关系

在 FreeRTOS 系统中,必须严格区分“硬件中断 (ISR)”和“软件任务 (Task)”这两个属于不同维度的概念。它们之间存在着绝对的层级关系和截然相反的优先级编号规则:

优先级的逻辑

  • 硬件中断 (ISR):属于 MCU 的底层硬件特性。
  • 规则:优先级数字越低,优先级别越高
  • 极值:最高优先级为 0
  • 软件任务 (Task):属于 FreeRTOS 的纯软件概念,由开发者在代码中赋予。
  • 规则:优先级数字越低,优先级别越低。(这与硬件中断恰好相反!)
  • 极值:最低优先级为 0(通常分配给系统的 Idle 空闲任务)。

绝对的执行层级压制

在 CPU 的执行权争夺上,硬件永远具有最高特权:

  • 任务只有在没有任何 ISR 运行的时候才能获得执行机会。
  • 即使是系统中优先级最低的硬件中断(比如优先级设为 15 的外部按键中断),也可以瞬间抢占系统中优先级最高的软件任务(比如优先级为 5 的电机控制核心任务)。
  • 软件任务绝对无法抢占任何 ISR 的运行。

可以理解为rtos的任务优先级是指在硬件优先级为15的情况下,通过软件的方式又给这些任务划分了一套优先级。

15 级是指 因为系统的调度器(PendSV)处于硬件的最低优先级 15,所以只有当 CPU 闲下来(没有外部硬件打扰)时,调度器才有机会介入,并在它自己用软件划分出的那一套“ 就绪链表 ”中,挑选一个最高级别的任务扔给 CPU 执行。

我们前面还说过 freertos 可以“屏蔽中断”,但是那仅限于 freertos 处于临界状态时,才会临时修改中断屏蔽寄存器来屏蔽中断,平时正常执行任务时不会屏蔽。关于临界状态,我们在下面会讲解。

时序图

  1. “健康”的中断抢占 (t1 ~ t4)
    • t1 – t2:此时系统正常运行,User Task(用户任务)占据 CPU。
    • t2时刻 (关键点):发生了一个硬件中断(中断1)。此时不管 User Task 的任务优先级有多高,硬件中断对应的处理函数 ISR1 都会瞬间抢占 CPU。User Task 被迫暂停(虚线状态)。
    • t2 – t3:ISR1 正在执行。这是一个“好”的 ISR,因为它执行得非常快(红色的短矩形)。
    • t3时刻:ISR1 执行完成,立刻把 CPU 使用权还给系统,User Task 从被强行打断的地方恢复,继续执行。
  2. “灾难性”的中断抢占 (t6 ~ t8)
    • t6时刻:系统再次触发了一个硬件中断(中断2)。与之前一样,ISR2 毫不留情地抢占了正在运行的 User Task。
    • t6 – t7 (问题所在):请注意这个漫长的绿色矩形ISR2 占用 CPU 的时间非常长。这可能是因为开发者在 ISR2 里面加了复杂的数学运算、死循环等待,甚至是很长的 Delay 延时。
    • 直接后果:因为 ISR 不退出,软件调度器就彻底瘫痪了。User Task(可能正在计算电机的姿态或者刷新关键 UI)被硬生生地卡在了一半,导致它的整体完成时间被极大地拉长。从用户的宏观视角来看,这就导致了软件运行响应变得迟钝(卡顿)

所以设计 ISR 函数时,要尽量精简操作,不要过多的占用 CPU 时间。一般情况下 ISR 只负责数据采集收发,数据处理的逻辑应放在用户的任务里执行。

中断屏蔽和临界代码段

在多任务和多中断交织的复杂系统中,一段代码随时可能被打断。但有些操作(例如修改一个全局链表、更新一个多字节的传感器数据结构)必须一气呵成。

临界段 (Critical Section) 的定义: 任务中某些非常关键、需要连续执行完、绝对不希望被其他高优先级任务或任何(受管辖的)ISR 函数打断的程序段

FreeRTOS 提供的两套保护 API

FreeRTOS 提供了两组不同级别的宏来实现这种保护:

1. 基础的全局中断开关

  • taskDISABLE_INTERRUPTS():屏蔽 MCU 的部分中断。(结合上一节知识:它实际上是修改了 BASEPRI 寄存器,屏蔽了优先级在 5~15 之间的中断)。
  • taskENABLE_INTERRUPTS():解除中断屏蔽。

2. 专业的临界区宏(推荐使用)

  • 在普通任务中使用:
  • taskENTER_CRITICAL():开始临界代码段。
  • taskEXIT_CRITICAL():结束临界代码段。
  • 核心特性:它可以嵌套定义
  • 在中断 (ISR) 中使用
  • taskENTER_CRITICAL_FROM_ISR():在中断里进入临界区。
  • taskEXIT_CRITICAL_FROM_ISR(x):在中断里退出临界区。

为什么要有两套屏蔽中断开关?

假设你有函数 A 和函数 B,它们都需要保护自己的代码。

C
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_CRITICALtaskEXIT_CRITICAL()虽然在内部也会调用taskDISABLE_INTERRUPTS,但它会额外实现一个计数器来处理嵌套,只有当计数器归零时进行退出中断屏蔽才会视为一个有效操作。

在 ISR 函数中使用 FreeRTOS API 函数

如上文所示,在 FreeRTOS 中,所有的 API 函数被严格划分为了两个版本,这在其他很多实时操作系统中是不多见的:

API 的双版本机制

  • “任务级” (Task-Level) API:也就是我们平时用的普通名称的 API,例如 xQueueSend()xSemaphoreGive()
  • “中断级” (Interrupt-Level) API:带有 FromISR 后缀的函数,或者带有 FROM_ISR 后缀的宏函数,例如 xQueueSendFromISR()xSemaphoreGiveFromISR()。它们也被称为“中断安全 API 函数”

为什么必须分两个版本

ISR 执行的时候是不能进行任务调度的

假设你在一个串口接收中断 (ISR) 里调用了普通的 xQueueReceive() 函数,并且这个队列目前是空的。 普通的 API 发现队列为空时,会理所当然地试图把当前任务挂起(进入阻塞状态),并请求调度器去运行别的任务。 但是现在 CPU 正在执行的是硬件中断,根本没有“当前任务”的上下文可以挂起。这种“试图在硬件中断里引发软件阻塞”的行为,会直接导致 CPU 指针错乱,引发 HardFault 死机。

带有 FromISR 的函数则去掉了所有可能引起阻塞的代码逻辑。如果队列满了写不进去,它会立刻返回一个错误码,而绝对不会死等。

注意事项

  1. 在 ISR 中绝对不能使用任务级 API 函数。
  2. 向下兼容:在普通的任务函数中,你是可以使用中断级 API 函数的。(虽然可以,但通常不推荐,因为中断级 API 往往缺少阻塞等待机制,用起来不如普通 API 方便)。
  3. 例外:(结合我们前面的知识点)即便你用的是带有 FromISR 的安全 API,也绝对不能在优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(例如优先级为 0~4)的中断 ISR 里调用它们。

总结

原则一:精准跨界(中断优先级的分野)

  • 原文:中断分为 FreeRTOS 不可屏蔽中断和可屏蔽中断,要根据中断的重要性和功能为其设置合适的中断优先级。
  • 实战回响:这就是我们讲过的 BASEPRI 机制(通常阈值设为 5)。
  • 像电机紧急刹车这种要命的动作,就设为 0~4(不可屏蔽)
  • 像普通的串口通信、按键响应,必须乖乖设为 5~14(可屏蔽),归 FreeRTOS 管辖。

原则二:快进快出(中断延迟处理机制)

  • 原文:ISR 函数的代码应该尽量简短,将处理功能延迟到任务里去实现。
  • 实战回响:还记得那张时序图里导致系统卡顿的“超长绿色矩形 ISR2”吗?在中断里绝对不能做死循环、复杂计算或延时。正确的做法是在 ISR 里仅仅接收数据或发送信号量,立刻退出,唤醒后台的高优先级软件任务去慢慢消化数据。这在 RTOS 中被称为“中断延迟处理 (Deferred Interrupt Processing)”。

原则三:泾渭分明(API 的调用区分)

  • 原文:在可屏蔽中断的 ISR 函数里能调用中断级的 FreeRTOS API 函数,绝对不能调用普通的 FreeRTOS API 函数。在不可屏蔽中断的 ISR 函数里,不能调用任何的 FreeRTOS API 函数。
  • 实战回响
  • 普通任务:用普通 API(如 xQueueSend)。
  • 优先级 5~14 的中断:必须用带 FromISR 后缀的 API,且记得要有变量接住并处理返回值。
  • 优先级 0~4 的中断:连 FromISR 都不能用,只能老老实实操作裸机寄存器。
  1. 涉及到多线程就是很麻烦啊。 ↩︎
  2. 这和 freertos 的优先级规则正好相反。 ↩︎

[ 发起通讯连接 / INITIATE COMM-LINK ]

[SYS]: 您的回传节点(邮箱)将被严格保密。带有 * 的字段为必填项。


> 终止读取并返回主控制台 <