FreeRTOS 的中断规则来自具体处理器移植。本文限定 STM32F103、Cortex-M3、4 个实现的优先级位、单核 FreeRTOS-Kernel V11.1.0 GCC ARM_CM3。Cortex-M0/M0+、带 MPU 的端口以及其他架构不能直接套用 BASEPRI 结论。
分清任务优先级与中断优先级
跳转到“分清任务优先级与中断优先级”| 对象 | 数值方向 | 本文范围 |
|---|---|---|
| FreeRTOS 任务 | 数值越大越优先 | 0 .. configMAX_PRIORITIES - 1 |
| Cortex-M NVIC 逻辑优先级 | 数值越小越紧迫 | 0 .. 15 |
| NVIC 优先级寄存器字节 | 有效位放在高位 | 逻辑值左移 4 位,例如 5 对应 0x50 |
本文使用 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4),四位均用于抢占优先级,不再划分子优先级。应在全局初始化阶段设置并保持一致。CMSIS NVIC_SetPriority() 和 HAL 的优先级参数接受逻辑值,不能给它们传已移位的 0x50。
系统调用阈值的含义
跳转到“系统调用阈值的含义”沿用原文阈值 4 的例子:
| 配置概念 | 值 | 作用 |
|---|---|---|
| 实现的优先级位数 | 4 | 通常由 __NVIC_PRIO_BITS 得到 |
configLIBRARY_LOWEST_INTERRUPT_PRIORITY | 15 | 应用常用的未移位最低优先级记法 |
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY | 4 | 应用常用的未移位内核调用阈值 |
configKERNEL_INTERRUPT_PRIORITY | 15 << 4,即 0xF0 | 移植层使用的寄存器表示 |
configMAX_SYSCALL_INTERRUPT_PRIORITY | 4 << 4,即 0x40 | BASEPRI 屏蔽阈值,不能设为 0 |
configLIBRARY_* 是许多 Cortex-M 工程采用的便于 HAL/CMSIS 使用的配置约定;内核实际使用的宏仍应按端口要求定义。阈值为 4 时:
- 逻辑优先级 0–3 的 ISR 比阈值更紧迫,不被这种 RTOS 临界区屏蔽,不能调用 FreeRTOS 内核 API。
- 逻辑优先级 4–15 的 ISR 可以在符合各 API 约束时使用中断安全接口,如
xQueueSendFromISR();会被内核临界区暂时屏蔽。
另一种常见工程配置把阈值设为 5,相应边界变成 0–4 与 5–15,寄存器阈值为 0x50。这是另一套完整配置,不应把阈值 4 的图、阈值 5 的宏和未经调整的 ISR 混在一起。没有普适的“0–2 不能用”规定;关键是中断的紧迫性、内核调用及系统时序预算。
临界区仍允许哪些中断
跳转到“临界区仍允许哪些中断”任务调用 taskENTER_CRITICAL() 后,本文端口提高 BASEPRI 并增加嵌套计数;taskEXIT_CRITICAL() 递减计数,只在最外层退出时恢复端口允许的中断状态。不要自己定义同名函数,把它简化成每次退出都直接写 BASEPRI 为 0。
阈值 = 4任务进入临界区 ─────────── 退出最外层临界区 ↑ IRQ 2 可以抢占 ↑ IRQ 6 暂存为 pending,解除屏蔽后才有机会执行“可抢占”不等于零延迟:PRIMASK、已经运行的更紧迫异常、存储访问和其他硬件条件仍可能延迟中断。紧迫 ISR 与任务共享的数据也不能只靠 FreeRTOS 的这种临界区保护,因为该 ISR 正是允许穿过它执行的。
任务上下文使用任务临界区接口;需要在允许的 ISR 中做短小保护时,使用对应端口支持的 taskENTER_CRITICAL_FROM_ISR() / taskEXIT_CRITICAL_FROM_ISR(saved_mask),保存并恢复原屏蔽状态。ISR 接口不能拿去规避中断优先级限制,任务临界区也不是任意 PRIMASK 状态的通用保存器。
三种工作放在哪个上下文
跳转到“三种工作放在哪个上下文”| 工作 | 适合的位置 | 约束 |
|---|---|---|
| 紧迫的硬件关闭请求 | 比内核阈值更紧迫的 ISR,可直接操作约定寄存器 | 不调用内核;GPIO 变化也不等于机械系统立即停止 |
| UART 接收等数据搬运 | 可调用内核的 ISR | 读取/确认硬件事件,使用 FromISR 接口,检查失败,尽快退出 |
| 解析、日志、协议与计算 | 消费者任务 | 可阻塞等待数据,并按任务 API 管理资源 |
不是每个普通 API 都存在同名 FromISR 版本。ISR 不能执行阻塞等待,也不能拿普通互斥量;必须逐项查接口文档。
UART 队列示例:先创建对象,再允许中断
跳转到“UART 队列示例:先创建对象,再允许中断”以下是独占 USART1 接收路径的示例模块,不是完整板级工程。假定时钟、引脚、波特率和 8 位无校验接收已配置,未启用 DMA、发送中断或其他 USART 中断;不再让 HAL_UART_IRQHandler() 同时处理这些接收事件。uart_rx_start_in_task() 在调度器运行后的初始化任务中调用一次。
#include "stm32f1xx_hal.h"#include "FreeRTOS.h"#include "task.h"#include "queue.h"
static QueueHandle_t rx_queue;static volatile uint32_t rx_dropped;static volatile uint32_t rx_errors;extern void process_uart_byte(uint8_t value);
static void uart_consumer_task(void *argument){ (void)argument; uint8_t value; for (;;) { if (xQueueReceive(rx_queue, &value, portMAX_DELAY) == pdPASS) { process_uart_byte(value); } }}
BaseType_t uart_rx_start_in_task(void){ NVIC_DisableIRQ(USART1_IRQn); USART1->CR1 &= ~USART_CR1_RXNEIE; rx_queue = xQueueCreate(32, sizeof(uint8_t)); if (rx_queue == NULL) { return pdFAIL; } if (xTaskCreate(uart_consumer_task, "UartRx", 256, NULL, 2, NULL) != pdPASS) { vQueueDelete(rx_queue); rx_queue = NULL; return pdFAIL; } /* 初始化阶段丢弃旧数据/错误状态。先读 SR 再读 DR。 */ (void)USART1->SR; (void)USART1->DR; NVIC_ClearPendingIRQ(USART1_IRQn); HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); USART1->CR1 |= USART_CR1_RXNEIE; NVIC_EnableIRQ(USART1_IRQn); return pdPASS;}
void USART1_IRQHandler(void){ BaseType_t higher_priority_task_woken = pdFALSE; const uint32_t status = USART1->SR; const uint32_t errors = USART_SR_ORE | USART_SR_FE | USART_SR_NE | USART_SR_PE; if ((USART1->CR1 & USART_CR1_RXNEIE) != 0U && (status & (USART_SR_RXNE | errors)) != 0U) { /* F1 的 SR 后 DR 读取序列确认相应接收/错误标志。 */ const uint8_t value = (uint8_t)USART1->DR; if ((status & errors) != 0U) { ++rx_errors; /* 本例遇到错误丢弃该次数据,由协议层恢复。 */ } else if ((status & USART_SR_RXNE) != 0U) { if (xQueueSendFromISR(rx_queue, &value, &higher_priority_task_woken) != pdPASS) { ++rx_dropped; } } } portYIELD_FROM_ISR(higher_priority_task_woken);}队列在消费者任务创建和 IRQ 使能前就已有效,不再让消费者任务运行后才创建 ISR 所用队列。该句柄在 IRQ 活跃期间保持有效;销毁前必须先关闭并同步中断和消费者。这里只展示字节接收,实际高吞吐场景可能更适合环形缓冲区或 DMA,但同样需要溢出和错误恢复策略。
读 DR 已确认 F1 的 RXNE 等状态,不应随后再用一个含糊的“清 RXNE”操作,误清新到的数据。示例明确统计 ORE、FE、NE、PE 和队列满,而不把队列发送成功当作必然。前台读取统计量应采用与 ISR 访问匹配的短快照方案;volatile 只保证可见访问,不提供多字段一致性。
ISR 中的 higher_priority_task_woken 每次入口先设为 pdFALSE,把它传给相应 FromISR API,末尾执行 portYIELD_FROM_ISR()。这会请求 PendSV,在异常退出和优先级条件允许后进行上下文切换,不是在 ISR 函数中间直接运行消费者任务。
故障定位
跳转到“故障定位”| 现象 | 先检查 |
|---|---|
configASSERT、随机损坏或 HardFault | ISR 是否高于调用阈值,是否错误使用普通 API,分组是否改变 |
| 接收偶发丢失 | 队列满统计、ORE、长屏蔽时间、消费者吞吐、是否存在两个驱动所有者 |
| 唤醒后延迟到后续 Tick | 是否正确传递唤醒标志并请求 ISR 退出后的切换;同时检查更高优先级负载 |
| 临界区内数据仍改变 | 访问者是否为未被 BASEPRI 屏蔽的更紧迫 ISR |
| 回调使用空句柄 | 队列/任务对象是否在 IRQ 使能前创建成功,是否过早删除 |
使能调试期 configASSERT 有助于尽早暴露端口约束,但它不能代替工程级延迟、丢包和最坏执行时间测量。