4. 中断处理:中断上下文中的阻塞操作、中断优先级配置不当导致系统卡死
中断处理,是嵌入式开发的深水区。
我见过太多开发者,包括我自己早期,都在这里栽过跟头。说白了,中断里一个不起眼的阻塞操作,或者优先级配错一个数字,就能让整个系统直接“死给你看”。
今天咱们就来聊聊,PX4 驱动里中断处理的两个核心坑点:上下文阻塞和优先级配置。
4.1 中断上下文中的阻塞操作:别在“快车道”上踩刹车
先问一个问题:中断服务函数(ISR)里,能不能调用 printf?能不能用 malloc?能不能加锁?
答案是:绝对不能。
为什么?因为中断上下文是系统的“快车道”。它要求极速响应,任何阻塞操作都会让这条车道堵死。
我举个例子。有一次我在调试一个 GPS 驱动,ISR 里为了打印调试信息,顺手加了个 printf。结果系统跑起来后,GPS 数据一更新,整个飞控就卡死了。查了半天才发现,printf 底层用到了互斥锁,而锁被主循环占着不放。ISR 等锁,主循环等 ISR 完成——死锁了。
常见的“禁区”操作包括:
- 动态内存分配:
malloc、new等,可能触发内存碎片整理或系统调用。 - 锁操作:互斥锁、信号量(带阻塞语义的)。
- I/O 操作:文件读写、串口打印(如
printf)。 - 延时函数:
delay_ms、usleep等。 - 系统调用:涉及上下文切换的函数。
那正确的做法是什么?
我个人习惯是:ISR 里只做最轻量的事。比如,读一个寄存器、置一个标志位、或者通过一个无锁队列把数据扔出去。真正的处理逻辑,交给底半部(Bottom Half)或者任务去完成。
✅ 推荐模式:
// 中断上半部(ISR)
void my_isr(void) {
uint32_t data = REG_READ(MY_REG);
// 将数据放入无锁环形缓冲区
ringbuf_push(&my_buf, data);
// 触发一个工作队列或任务
work_queue_signal(&my_work);
}
// 中断下半部(任务/工作队列)
void my_work_callback(void *arg) {
uint32_t data;
while (ringbuf_pop(&my_buf, &data) == OK) {
// 在这里做真正的处理
process_data(data);
}
}
在 PX4 里,我们常用 work_queue 来实现这个机制。ISR 里调用 work_queue_schedule,把耗时操作扔到工作线程里。这样既保证了中断的快速响应,又避免了阻塞。
4.2 中断优先级配置不当:谁抢了谁的“话语权”?
中断优先级,说白了就是决定“谁先说话”。
配置不当,轻则数据丢失,重则系统卡死。我见过最典型的一个案例:一个开发者把 SPI 中断优先级设得比定时器中断还低。结果定时器频繁触发,SPI 中断一直被抢占,导致传感器数据读取超时,飞控直接失控。
在 PX4 驱动的上下文中,中断优先级配置有几个关键点:
4.2.1 优先级分组与抢占规则
ARM Cortex-M 系列内核(STM32、NXP 等)支持中断优先级分组。你需要理解:
- 抢占优先级:数值越小,优先级越高。高优先级可以打断低优先级。
- 子优先级:当抢占优先级相同时,子优先级决定谁先执行,但不能互相打断。
我建议在系统初始化时,明确设置优先级分组。比如:
// 设置优先级分组:4位抢占优先级,0位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
这样配置后,每个中断的优先级就只由抢占优先级决定,逻辑更清晰。
4.2.2 PX4 驱动的典型优先级分配
嗯,这里要注意。PX4 驱动里,不同外设的中断优先级是有“潜规则”的:
| 中断类型 | 推荐优先级 | 说明 |
|---|---|---|
| 定时器(时间基准) | 最高(0-1) | 系统心跳,丢失会导致时间错乱 |
| DMA 传输完成 | 高(1-2) | 数据流关键,延迟会导致缓冲区溢出 |
| SPI/I2C 外设 | 中(2-3) | 传感器数据读取,可容忍少量延迟 |
| GPIO 外部中断 | 低(3-4) | 非关键事件,如按键、状态变化 |
| UART 接收 | 中低(3-4) | 遥控器信号等,有硬件 FIFO 缓冲 |
我曾经在调试一个光流传感器驱动时,把 SPI 中断优先级设成了 5,而定时器中断是 3。结果每次定时器触发,SPI 传输就被打断。传感器数据包总是校验失败。后来我把 SPI 优先级提到 2,问题立刻解决。
💡 避坑指南:
我曾经犯过一个低级错误:把两个互相依赖的中断设成了相同优先级。比如,SPI 传输完成中断和 DMA 中断。如果它们优先级相同,且同时触发,系统会按硬件向量号顺序执行。但如果你在 SPI 中断里等待 DMA 完成,而 DMA 中断还没执行——死锁了。
所以,有依赖关系的中断,一定要设置不同的抢占优先级。
4.3 实战:一个中断优先级导致的“假死”案例
说一个我印象深刻的真实案例。
当时我在移植一个 PX4 的 IMU 驱动到新硬件上。驱动跑起来后,飞控能正常解锁,但一推油门就卡死。串口输出停在最后一条日志上,没有任何报错。
我一开始怀疑是电源问题,查了半天供电。后来用逻辑分析仪抓 SPI 总线,发现一个诡异现象:每次推油门后,SPI 时钟就停了。
再深入查,发现是电机控制用的 PWM 定时器中断优先级太高。PWM 中断频繁触发,把 SPI 中断完全“饿死”了。IMU 数据读不到,飞控的 EKF 滤波器直接挂起,系统进入保护状态。
解决方案很简单:把 PWM 定时器中断优先级降到 3,SPI 中断提到 1。问题解决。
✅ 配置示例(基于 STM32 HAL):
// SPI 中断 - 高优先级
HAL_NVIC_SetPriority(SPI1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(SPI1_IRQn);
// PWM 定时器中断 - 中优先级
HAL_NVIC_SetPriority(TIM1_UP_IRQn, 3, 0);
HAL_NVIC_EnableIRQ(TIM1_UP_IRQn);
// UART 接收中断 - 低优先级
HAL_NVIC_SetPriority(USART1_IRQn, 4, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
4.4 中断处理的“黄金法则”
总结一下,我这些年积累的几个原则:
- ISR 要短:能放一个标志位解决的,绝不放一个函数调用。
- 优先级要分层:时间关键的中断(定时器、DMA)优先级最高,数据流中断(SPI、I2C)次之,事件中断(GPIO、UART)最低。
- 避免依赖链:不要在 ISR 里等待另一个中断完成。
- 用工具验证:逻辑分析仪、示波器、RTOS 的追踪工具(如 Tracealyzer)都能帮你看到中断抢占的真实情况。
中断处理,说白了就是“快进快出”。你想想看,系统里那么多任务在跑,中断就是那个插队的 VIP。VIP 处理不好,整个系统都得乱套。
💡 最后一个小技巧:
如果你不确定某个函数能不能在 ISR 里用,查一下它的文档。RTOS 或 HAL 库通常会在函数说明里标注“不可在中断上下文中调用”。或者,你可以在 ISR 里加一个断言,检查当前是否在中断上下文:
// 在 ISR 入口处断言
ASSERT(!in_interrupt_context());
这样,一旦误用,系统会立刻崩溃并告诉你问题在哪。调试效率高很多。