4. 中断处理:中断上下文中的阻塞操作、中断优先级配置不当导致系统卡死

中断处理,是嵌入式开发的深水区。

我见过太多开发者,包括我自己早期,都在这里栽过跟头。说白了,中断里一个不起眼的阻塞操作,或者优先级配错一个数字,就能让整个系统直接“死给你看”。

今天咱们就来聊聊,PX4 驱动里中断处理的两个核心坑点:上下文阻塞优先级配置

4.1 中断上下文中的阻塞操作:别在“快车道”上踩刹车

先问一个问题:中断服务函数(ISR)里,能不能调用 printf?能不能用 malloc?能不能加锁?

答案是:绝对不能

为什么?因为中断上下文是系统的“快车道”。它要求极速响应,任何阻塞操作都会让这条车道堵死。

⚠️ 核心原则: 中断服务函数中,禁止任何可能导致阻塞或睡眠的操作。

我举个例子。有一次我在调试一个 GPS 驱动,ISR 里为了打印调试信息,顺手加了个 printf。结果系统跑起来后,GPS 数据一更新,整个飞控就卡死了。查了半天才发现,printf 底层用到了互斥锁,而锁被主循环占着不放。ISR 等锁,主循环等 ISR 完成——死锁了。

常见的“禁区”操作包括:

  • 动态内存分配mallocnew 等,可能触发内存碎片整理或系统调用。
  • 锁操作:互斥锁、信号量(带阻塞语义的)。
  • I/O 操作:文件读写、串口打印(如 printf)。
  • 延时函数delay_msusleep 等。
  • 系统调用:涉及上下文切换的函数。

那正确的做法是什么?

我个人习惯是: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 中断处理的“黄金法则”

总结一下,我这些年积累的几个原则:

  1. ISR 要短:能放一个标志位解决的,绝不放一个函数调用。
  2. 优先级要分层:时间关键的中断(定时器、DMA)优先级最高,数据流中断(SPI、I2C)次之,事件中断(GPIO、UART)最低。
  3. 避免依赖链:不要在 ISR 里等待另一个中断完成。
  4. 用工具验证:逻辑分析仪、示波器、RTOS 的追踪工具(如 Tracealyzer)都能帮你看到中断抢占的真实情况。

中断处理,说白了就是“快进快出”。你想想看,系统里那么多任务在跑,中断就是那个插队的 VIP。VIP 处理不好,整个系统都得乱套。

💡 最后一个小技巧:

如果你不确定某个函数能不能在 ISR 里用,查一下它的文档。RTOS 或 HAL 库通常会在函数说明里标注“不可在中断上下文中调用”。或者,你可以在 ISR 里加一个断言,检查当前是否在中断上下文:

// 在 ISR 入口处断言
ASSERT(!in_interrupt_context());

这样,一旦误用,系统会立刻崩溃并告诉你问题在哪。调试效率高很多。


专注资料整理