目录
[一、先理清 ARM Cortex-M 中断优先级基础](#一、先理清 ARM Cortex-M 中断优先级基础)
[1. 两个关键阈值(Cortex-M 端口)](#1. 两个关键阈值(Cortex-M 端口))
[2. BASEPRI 寄存器作用](#2. BASEPRI 寄存器作用)
[二、中断服务函数 ISR 的两条路线](#二、中断服务函数 ISR 的两条路线)
[1. 高优先级快中断(禁止调用 RTOS API)](#1. 高优先级快中断(禁止调用 RTOS API))
[2. 可交互中断(允许调用 xxxFromISR)](#2. 可交互中断(允许调用 xxxFromISR))
[三、延迟中断处理(DPC,Deferred Interrupt Processing)------ 工程最推荐方案](#三、延迟中断处理(DPC,Deferred Interrupt Processing)—— 工程最推荐方案)
[1. 核心思想](#1. 核心思想)
[2. 标准模板(信号量版本)](#2. 标准模板(信号量版本))
[四、中断中可用与禁用 API 清单](#四、中断中可用与禁用 API 清单)
[六、临界区保护:taskENTER_CRITICAL 和 taskEXIT_CRITICAL](#六、临界区保护:taskENTER_CRITICAL 和 taskEXIT_CRITICAL)
前言
在嵌入式系统里,中断是硬件和内核交互的入口,按键、串口、传感器、DMA、定时器全部依赖中断。很多新手写 RTOS 项目时,裸机中断代码直接移植进来,出现莫名其妙的死机、任务卡死、数据丢包、临界区异常、低功耗唤醒失败。
根源在于:RTOS 环境下的中断不再是裸机里简单的 "来了就执行",中断和任务调度存在优先级博弈、资源共享规则、API 调用限制。
本篇围绕 FreeRTOS 中断整套体系,从中断优先级分组、BASEPRI 屏蔽、中断服务函数写法、中断里能用 / 不能用的 API、延迟中断处理、中断嵌套、临界区保护、量产踩坑点完整梳理,是稳定性项目必须吃透的内容。
一、先理清 ARM Cortex-M 中断优先级基础
Cortex-M 内核的中断优先级分为抢占优先级(Preempt Priority) 和子优先级(Sub Priority),由优先级分组决定。
重点:FreeRTOS 不关心子优先级,只认抢占优先级!子优先级只在同抢占优先级中断同时到来时决定先后,不能触发嵌套。
1. 两个关键阈值(Cortex-M 端口)
- configMAX_SYSCALL_INTERRUPT_PRIORITY 可以调用 FreeRTOS FromISR 系列 API 的中断最高抢占优先级。优先级数字越小,优先级越高。 高于这个阈值(数值更小)的中断,禁止调用任何 FreeRTOS API ,这类中断称为零延迟中断 / 内核外中断,不会被内核屏蔽,不参与调度。
- configKERNEL_INTERRUPT_PRIORITY SysTick、PendSV 这两个内核专用中断的优先级,一般设置为最低。
口诀:高优先级中断只管硬件快响应,不要发信号量、队列;低优先级中断才允许和 RTOS 内核交互。
举个常用配置示例:
// 优先级分组4,4位抢占优先级,0位子优先级
#define configPRIO_BITS __NVIC_PRIO_BITS
#define configKERNEL_INTERRUPT_PRIORITY (15)
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (5)
**含义:**抢占优先级 0~4:极高优先级中断,禁止调用 xxxFromISR;抢占优先级 5~15:可以安全调用中断版本 API。
2. BASEPRI 寄存器作用
FreeRTOS 关闭中断不是直接清全局中断掩码(PRIMASK),而是写 BASEPRI。 BASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY: 所有抢占优先级≥该值的中断被屏蔽;优先级更高的中断依旧可以正常进入,实现选择性关中断,这也是 RTOS 保证实时性的核心设计。
二、中断服务函数 ISR 的两条路线
1. 高优先级快中断(禁止调用 RTOS API)
**适用场景:**高频、严苛实时需求,比如高速编码器、PWM 故障保护、紧急停机。
特点:
- 抢占优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY
- ISR 代码必须极短,只做硬件寄存器清标志、简单 IO 翻转
- 绝对不能调用 xSemaphoreGiveFromISR、xQueueSendFromISR 等 API
- 不能触发任务切换,和 RTOS 内核完全隔离 缺点:无法通知任务,复杂业务逻辑不能放这里。
2. 可交互中断(允许调用 xxxFromISR)
**适用场景:**串口接收、按键触发、传感器数据就绪、DMA 传输完成。
特点:
- 抢占优先级 ≤ configMAX_SYSCALL_INTERRUPT_PRIORITY
- ISR 内只做最精简硬件操作,通过信号量 / 队列通知任务,把复杂逻辑丢到任务里执行(延迟中断处理)
- API 必须带后缀 FromISR,普通 API(xSemaphoreGive)严禁在中断中调用
- 携带参数
pxHigherPriorityTaskWoken,用来判断是否需要中断退出后触发任务切换
三、延迟中断处理(DPC,Deferred Interrupt Processing)------ 工程最推荐方案
1. 核心思想
中断服务函数只做三件小事:
- 清除中断标志位
- 拷贝必要的少量硬件数据到缓冲区
- 释放信号量 / 投递消息到队列,唤醒专门的处理任务 耗时解析、校验、协议解析、打印日志、数据存储全部放到任务中执行。
好处:
- ISR 执行时间极短,降低中断阻塞、丢失其他中断的风险
- 代码分层清晰,中断只管硬件通知,业务逻辑在任务,方便调试
- 可以正常使用所有任务 API、延时、队列、事件组
- 避免长中断占用 CPU,打乱调度、破坏 Tickless 低功耗
2. 标准模板(信号量版本)
// 全局二值信号量
SemaphoreHandle_t xSensorSem = NULL;
// 中断服务函数
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line0);
// 释放信号量,唤醒任务
xSemaphoreGiveFromISR(xSensorSem, &xHigherPriorityTaskWoken);
// 如果唤醒了更高优先级任务,退出中断时切换上下文
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
// 处理任务
void vSensorTask(void *pvParam)
{
while(1)
{
xSemaphoreTake(xSensorSem, portMAX_DELAY);
// 这里放耗时逻辑:读取数据、校验、滤波、上报
}
}
portYIELD_FROM_ISR:中断上下文的任务切换,只能在 FromISR API 之后使用,不要随便调用。
四、中断中可用与禁用 API 清单
✅ 中断(xxxFromISR)允许调用:
- xSemaphoreGiveFromISR /xSemaphoreTakeFromISR(二值、计数信号量)
- xQueueSendFromISR / xQueueReceiveFromISR
- xEventGroupSetBitsFromISR
- vTaskNotifyGiveFromISR(任务通知,轻量化首选)
❌ 中断里面绝对禁止:
- 不带 FromISR 后缀全部 API:xSemaphoreGive、xQueueSend、vTaskDelay
- 动态内存分配:pvPortMalloc
- vTaskSuspend、vTaskDelete
- printf、串口阻塞发送(底层可能带锁、延时)
- 任何会阻塞的函数
补充:任务通知(vTaskNotifyGiveFromISR)开销比信号量更小,不需要单独创建信号量,资源紧张项目优先选用。
五、中断嵌套规则
Cortex-M 本身支持中断嵌套:抢占优先级高的中断可以打断正在执行的低抢占优先级中断。 结合 FreeRTOS,嵌套约束:
- 高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的高优先级中断,可以随时嵌套打断低优先级中断,不受 BASEPRI 屏蔽
- 同抢占优先级,不会嵌套,按先后排队
- 内核关闭中断(BASEPRI 生效期间),只屏蔽≤阈值的中断,高优先级中断依旧可以进来
- 不要滥用嵌套:嵌套层级越深,栈溢出风险越高,调试难度指数上升
⚠️ 坑:很多人把所有中断优先级设成一样,结果并发中断丢事件、响应卡顿。
六、临界区保护:taskENTER_CRITICAL 和 taskEXIT_CRITICAL
临界区用来保护全局共享变量,防止任务和中断同时访问造成数据错乱。
taskENTER_CRITICAL();
// 访问共享全局变量,简短操作
g_u32Count++;
taskEXIT_CRITICAL();
- taskENTER_CRITICAL 底层写 BASEPRI,屏蔽可调用 RTOS API 的中断;高优先级硬中断依旧能进入
- 临界区代码必须极短,禁止延时、打印、循环、调用任何阻塞 API
- 如果是中断内部 的临界保护,要用
portSET_INTERRUPT_MASK_FROM_ISR()/portCLEAR_INTERRUPT_MASK_FROM_ISR(),不能用 taskENTER_CRITICAL
七、中断栈配置(极易忽略,量产死机元凶)
两个栈要区分:
- 主栈 MSP:中断服务函数默认使用 MSP 栈,在启动文件中配置 Stack_Size
- 任务栈 PSP:每个任务独立栈,任务运行时使用
重点:中断里面函数调用深度大、局部变量多,消耗的是 MSP!很多项目任务栈配很大,但 MSP 栈很小,中断一复杂直接栈溢出,随机 HardFault。
排查建议:
- 串口、DMA、协议解析这类中断相关逻辑尽量丢到任务,减少 ISR 栈消耗
- 调试时开启栈水位统计,监控 MSP 剩余空间
八、高频量产坑点汇总
- 裸机中断直接复制,ISR 里调用普通 xSemaphoreGive 直接触发异常、HardFault,必须替换成 FromISR 版本,带上 pxHigherPriorityTaskWoken。
- 优先级配置颠倒,数字大小理解错误 很多人误以为数值大优先级高,和 Cortex-M 规则相反,导致高实时中断被内核屏蔽。
- 中断里做大量耗时处理 ISR 长时间占用 CPU,其他中断被阻塞、丢数据,同时破坏 Tickless 休眠,功耗异常。
- pxHigherPriorityTaskWoken 传 NULL 高优先级任务唤醒后无法及时切换,调度延迟,时序异常。
- 临界区里面写延时、打印 临界区长时间关中断,系统 Tick 停止、调度卡死。
- 高优先级中断里面使用 FromISR API 高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断调用内核 API,直接崩溃。
- 多个中断同时操作同一个队列,缺少保护 队列本身 FromISR 接口是线程安全的,但如果中断 + 任务同时操作,遵循 API 规范即可;跨多个中断并发写入队列一般没问题,但要注意数据帧边界。
- 中断标志忘记清除、清除时机错误 中断反复进,系统卡死在 ISR 循环。
- MSP 栈空间不足,随机死机 现象:偶发 HardFault,复现概率低,抓栈很难定位。
- portYIELD_FROM_ISR 放错位置 必须在 FromISR 调用之后,中断退出前;不要放在 if 判断之外随意调用。
九、工程最佳实践总结
- 分层原则:ISR 只做硬件标记 + 通知,业务逻辑全部延迟到任务处理
- 优先级划分:故障保护、紧急信号放在高优先级(不碰 RTOS API);通信、传感器放在低优先级(允许 FromISR)
- 中断代码瘦身:禁止在 ISR 写循环、协议解析、日志输出
- 优先使用任务通知替代信号量,减少内核资源开销
- 合理设置 MSP 栈大小,定期监控栈水位
- 共享资源区分保护:任务之间用互斥量;中断与任务共享用临界区
- 低功耗项目注意:中断是唤醒源,所有唤醒中断优先级要合理配置,避免唤醒后调度异常
- 上线前长时间压力测试:高频触发中断,测试丢包、死机、时序异常
小结
FreeRTOS 中断管理的核心一句话:区分中断优先级等级,划分 "和内核交互" 与 "纯硬件快中断" 两类中断;中断只负责通知,业务交给任务。
中断是稳定性的重灾区,很多偶发死机、数据异常,根源都出自中断写法、优先级、栈配置这几块。吃透这部分,项目稳定性会上一个台阶。