中断管理
ISR 与内核的边界规则:Cortex-M 中断优先级如何影响 FreeRTOS、什么中断能调用内核 API、FromISR 与 portYIELD_FROM_ISR,以及中断延迟处理范式。
ISR 与内核的边界规则:Cortex-M 中断优先级如何影响 FreeRTOS、什么中断能调用内核 API、FromISR 与 portYIELD_FROM_ISR,以及中断延迟处理范式。
__NVIC_PRIO_BITS=4,16 级)。分组决定这几位里多少位用作"抢占优先级"、多少位用作"子优先级"。FreeRTOS 要求把全部位配成抢占优先级(即分组 4 / NVIC_PRIORITYGROUP_4),否则行为未定义。*FromISR 内核 API。数值更小(优先级更高)的中断完全不受内核屏蔽影响、也绝不能调用任何内核 API,用于对延迟极敏感的实时中断。BASEPRI 寄存器设为 configMAX_SYSCALL_INTERRUPT_PRIORITY,从而只屏蔽"能调内核 API 的中断",比它更高优先级的中断依然可以立即响应。这就是 FreeRTOS 能兼顾实时性与内核安全的机制。FromISR 后缀(xQueueSendFromISR、xSemaphoreGiveFromISR、vTaskNotifyGiveFromISR 等)。它们不阻塞、内部用适合中断的临界区实现,并通过 pxHigherPriorityTaskWoken 出参告知是否唤醒了更高优先级任务。非 FromISR 版本绝不能在中断里用。pdTRUE。中断退出时据此调用 portYIELD_FROM_ISR() 立即切换到该高优先级任务,而不必等到下一个 tick。忘了它会导致响应延迟。pxHigherPriorityTaskWoken。若为真,它请求一次 PendSV 上下文切换,使中断返回时不回到被打断的任务,而是直接运行刚被唤醒的更高优先级任务。这是让中断事件被"立即"处理的关键。如果一个中断的优先级数值 小于 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))
/* 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(); } }
中断里若需要读系统 tick,用 xTaskGetTickCountFromISR() 而非 xTaskGetTickCount()。同理,所有在 ISR 中使用的内核查询/操作函数,只要存在 FromISR 变体就必须用它。这些变体针对中断上下文做了临界区处理,混用普通版会破坏内核状态。
| 规则 | 说明 |
|---|---|
| 只用 FromISR API | 中断里绝不调用会阻塞的普通版本 |
| 优先级数值 ≥ 分界线 | 要调内核 API 的中断,抢占优先级 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY |
| ISR 尽量短 | 清标志 + 唤醒任务,重活延迟到任务上下文 |
| 处理 woken 标志 | 末尾 portYIELD_FROM_ISR(woken) |
| 用分组 4 | NVIC_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 帮助排查。