Chapter 09 · FreeRTOS 实时操作系统

中断管理

ISR 与内核的边界规则:Cortex-M 中断优先级如何影响 FreeRTOS、什么中断能调用内核 API、FromISR 与 portYIELD_FROM_ISR,以及中断延迟处理范式。

课程进度75%

关键概念

Cortex-M 中断优先级(数值反直觉)
与任务优先级相反,Cortex-M 的中断优先级数值越小,优先级越高。优先级 0 是最高(最紧急),数值越大越不紧急。这个"反直觉"是 FreeRTOS 中断配置最易出错之处,务必牢记。
优先级分组 (Priority Grouping)
Cortex-M 的中断优先级寄存器只有高几位有效(STM32 为高 4 位,即 __NVIC_PRIO_BITS=4,16 级)。分组决定这几位里多少位用作"抢占优先级"、多少位用作"子优先级"。FreeRTOS 要求把全部位配成抢占优先级(即分组 4 / NVIC_PRIORITYGROUP_4),否则行为未定义。
configMAX_SYSCALL_INTERRUPT_PRIORITY
FreeRTOS 的关键分界线:只有优先级数值 ≥ 此值(即优先级不高于此)的中断,才被允许调用 *FromISR 内核 API。数值更小(优先级更高)的中断完全不受内核屏蔽影响、也绝不能调用任何内核 API,用于对延迟极敏感的实时中断。
BASEPRI 与临界区
FreeRTOS 进入临界区时并非关闭所有中断,而是把 Cortex-M 的 BASEPRI 寄存器设为 configMAX_SYSCALL_INTERRUPT_PRIORITY,从而只屏蔽"能调内核 API 的中断",比它更高优先级的中断依然可以立即响应。这就是 FreeRTOS 能兼顾实时性与内核安全的机制。
FromISR 系列 API
所有可在中断里安全调用的内核函数都带 FromISR 后缀(xQueueSendFromISRxSemaphoreGiveFromISRvTaskNotifyGiveFromISR 等)。它们不阻塞、内部用适合中断的临界区实现,并通过 pxHigherPriorityTaskWoken 出参告知是否唤醒了更高优先级任务。非 FromISR 版本绝不能在中断里用。
pxHigherPriorityTaskWoken
FromISR API 的出参指针。若这次操作解除了某个任务的阻塞,且它的优先级高于被中断打断的任务,则被置 pdTRUE。中断退出时据此调用 portYIELD_FROM_ISR() 立即切换到该高优先级任务,而不必等到下一个 tick。忘了它会导致响应延迟。
portYIELD_FROM_ISR
在 ISR 末尾调用的宏,传入 pxHigherPriorityTaskWoken。若为真,它请求一次 PendSV 上下文切换,使中断返回时不回到被打断的任务,而是直接运行刚被唤醒的更高优先级任务。这是让中断事件被"立即"处理的关键。
中断延迟处理 (Deferred Interrupt Processing)
最佳实践范式:ISR 里只做最少的紧急工作(清标志、读寄存器),然后用信号量/任务通知/队列唤醒一个专门的处理任务,把耗时逻辑"延迟"到任务上下文完成。好处是缩短中断关闭时间、逻辑可用完整内核服务、便于调试。第 3、5 章的例子都是它的实例。

中断优先级分界线全景

FreeRTOS Cortex-M 中断优先级模型(数值越小越高) ═══════════════════════════════════════════════════════════ 优先级数值 0 ┐ 最高优先级 1 │ 实时中断区:延迟极敏感(电机、安全) 2 │ ▶ 绝不能调用任何内核 API 3 │ ▶ 不受 FreeRTOS 临界区屏蔽 4 ┘ ───────────────────────────────────────────── configMAX_SYSCALL_INTERRUPT_PRIORITY = 5 ◄── 分界线 ───────────────────────────────────────────── 5 ┐ 内核可管理中断区 6 │ ▶ 可以调用 *FromISR API ... │ ▶ 进入临界区时会被 BASEPRI 屏蔽 15 ┘ 最低优先级(PendSV/SysTick 常在此) 规则:要用 xQueueSendFromISR 等 → 中断优先级数值必须 ≥ 5 比 5 更高(0~4)的中断里调用内核 API = 灾难
头号崩溃原因:中断优先级设太高又调了内核 API

如果一个中断的优先级数值 小于 configMAX_SYSCALL_INTERRUPT_PRIORITY(即比分界线更紧急),却在里面调用了 xQueueSendFromISR 之类的 API,会破坏内核临界区保护,导致偶发、难以复现的崩溃或死机。用 CubeMX 配 NVIC 时,凡是要调内核 API 的外设中断,其抢占优先级数值必须 ≥ 5(在 16 级、分组 4 下)。configASSERT 开启后 FreeRTOS 会在调试时帮你抓到这个错误。

正确的优先级分组配置

/* main.c — 必须用分组 4:全部 4 位都作抢占优先级 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

/* 配置一个要调用内核 API 的外设中断:
   抢占优先级 6 ≥ 5(分界线),子优先级 0 */
HAL_NVIC_SetPriority(USART2_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(USART2_IRQn);

/* 一个对延迟极敏感、不调内核 API 的紧急中断:
   抢占优先级 2(比分界线 5 更高,绝不调内核 API) */
HAL_NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 2, 0);
/* FreeRTOSConfig.h — 中断优先级相关配置 */
#define configPRIO_BITS   4   /* STM32 用 4 位优先级 */

/* 内核可管理的最高优先级(数值)——分界线 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY  5
/* 内核自身用的最低优先级(PendSV/SysTick) */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY       15

/* 换算成寄存器实际值(左移到高位),CubeMX 自动生成 */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY \
    (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))

中断延迟处理:标准范式

Deferred Interrupt Processing 数据流 外设事件 │ 触发 ▼ ┌─────────────────────────┐ 最少工作:清标志、读一个字节 │ ISR(中断上下文) │ ────────────────────────┐ │ · 清中断标志 │ │ │ · 读硬件寄存器 │ xSemaphoreGiveFromISR / │ │ · Give/Send FromISR │ ──vTaskNotifyGiveFromISR──► │ │ · portYIELD_FROM_ISR │ ▼ └─────────────────────────┘ 处理任务(任务上下文) ┌────────────────────┐ │ · 阻塞等待信号量 │ │ · 被唤醒后做重活 │ │ · 可用完整内核服务 │ └────────────────────┘ 关中断时间极短,实时性好,逻辑可调试
/* ISR:只做最少的事,把重活交给任务 */
void DMA1_Stream5_IRQHandler(void) {
  BaseType_t woken = pdFALSE;
  if (__HAL_DMA_GET_FLAG(&hdma_adc, DMA_FLAG_TCIF5)) {
    __HAL_DMA_CLEAR_FLAG(&hdma_adc, DMA_FLAG_TCIF5);
    /* 通知处理任务:一整块 ADC 数据就绪 */
    vTaskNotifyGiveFromISR(g_adcProcTask, &woken);
  }
  portYIELD_FROM_ISR(woken);   /* 若唤醒了更高优任务,立即切换 */
}

/* 处理任务:被唤醒后做滤波、换算等耗时逻辑 */
void AdcProcTask(void *arg) {
  for (;;) {
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
    filter_and_scale(g_adcBuffer, ADC_LEN);   /* 从容处理 */
    publish_result();
  }
}
ISR 里取时间戳用 FromISR 版本

中断里若需要读系统 tick,用 xTaskGetTickCountFromISR() 而非 xTaskGetTickCount()。同理,所有在 ISR 中使用的内核查询/操作函数,只要存在 FromISR 变体就必须用它。这些变体针对中断上下文做了临界区处理,混用普通版会破坏内核状态。

ISR 编写规则清单

规则说明
只用 FromISR API中断里绝不调用会阻塞的普通版本
优先级数值 ≥ 分界线要调内核 API 的中断,抢占优先级 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY
ISR 尽量短清标志 + 唤醒任务,重活延迟到任务上下文
处理 woken 标志末尾 portYIELD_FROM_ISR(woken)
用分组 4NVIC_PRIORITYGROUP_4,全位作抢占优先级
不在 ISR 里 malloc/printf可能阻塞或不可重入,交给任务
本章小结

Cortex-M 中断优先级数值越小越高(与任务相反)。FreeRTOS 要求用优先级分组 4(NVIC_PRIORITYGROUP_4),把全部优先级位都作抢占优先级。configMAX_SYSCALL_INTERRUPT_PRIORITY 是关键分界线:只有优先级数值 它(不比它紧急)的中断才能调用 *FromISR API;更紧急的中断绝不能碰任何内核 API,但换来零内核屏蔽的最低延迟。FreeRTOS 临界区通过设置 BASEPRI 只屏蔽"可管理中断",因此不影响更高优先级的实时中断。所有 ISR 内的内核调用都用 FromISR 版本(不阻塞、带 pxHigherPriorityTaskWoken 出参),并在末尾 portYIELD_FROM_ISR(woken) 触发即时切换。最佳范式是中断延迟处理:ISR 只清标志、读寄存器、唤醒处理任务,把耗时逻辑放到任务上下文。头号崩溃原因是"中断优先级太高又调了内核 API"——务必让要调 API 的外设中断优先级数值 ≥ 分界线,并开启 configASSERT 帮助排查。