跳转到内容
新建笔记

Cortex-M3 FreeRTOS 中断优先级与 ISR 通信

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_PRIORITY15应用常用的未移位最低优先级记法
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY4应用常用的未移位内核调用阈值
configKERNEL_INTERRUPT_PRIORITY15 << 4,即 0xF0移植层使用的寄存器表示
configMAX_SYSCALL_INTERRUPT_PRIORITY4 << 4,即 0x40BASEPRI 屏蔽阈值,不能设为 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、随机损坏或 HardFaultISR 是否高于调用阈值,是否错误使用普通 API,分组是否改变
接收偶发丢失队列满统计、ORE、长屏蔽时间、消费者吞吐、是否存在两个驱动所有者
唤醒后延迟到后续 Tick是否正确传递唤醒标志并请求 ISR 退出后的切换;同时检查更高优先级负载
临界区内数据仍改变访问者是否为未被 BASEPRI 屏蔽的更紧迫 ISR
回调使用空句柄队列/任务对象是否在 IRQ 使能前创建成功,是否过早删除

使能调试期 configASSERT 有助于尽早暴露端口约束,但它不能代替工程级延迟、丢包和最坏执行时间测量。