第4章 NuttX中断与异常处理

中断系统,说白了就是嵌入式系统的神经反射弧。没有它,CPU就得像个傻子一样轮询等待,效率低得吓人。NuttX作为一个为资源受限系统设计的RTOS,其中断管理机制设计得相当精巧。今天我就带大家把这块硬骨头啃下来。

4.1 中断向量表配置

中断向量表,就是一张“出事了找谁”的通讯录。每个中断源对应一个入口地址,CPU一检测到中断信号,就自动跳转到对应地址去执行。

在NuttX里,中断向量表的配置通常分两步走。第一步,你得告诉CPU向量表放在哪。第二步,把具体的处理函数地址填进去。

我习惯在芯片启动文件里就把向量表基地址设好。以ARM Cortex-M系列为例,有个叫VTOR的寄存器专门干这事:

/* 设置向量表偏移寄存器 */
putreg32((uint32_t)&_vectors, NVIC_VECTORS);

这里的_vectors就是我们定义的中断向量数组。NuttX在chip/stm32_vectors.S这类汇编文件里,会预先定义好一个标准的向量表模板:

_vectors:
    .word   _estack            /* 栈顶指针 */
    .word   _reset             /* 复位向量 */
    .word   _nmi               /* NMI中断 */
    .word   _hard_fault        /* 硬错误 */
    .word   _mem_fault         /* 内存管理错误 */
    .word   _bus_fault         /* 总线错误 */
    .word   _usage_fault       /* 用法错误 */
    /* ... 后续中断向量 ... */

小提示:我个人建议把向量表放在RAM里,而不是Flash里。为什么?因为运行时可以动态修改。我在做OTA升级时,就靠这个特性实现了中断服务程序的在线更新。

4.2 中断服务函数编写

中断服务函数(ISR)怎么写?记住三个字:短、快、稳。

短——代码量要少。快——执行时间要短。稳——不能有死循环,不能调用可能引起阻塞的函数。

NuttX里注册一个中断服务函数,用的是irq_attach()这个API:

int irq_attach(int irq, xcpt_t isr, FAR void *arg);

参数很简单:中断号、函数指针、传给函数的参数。我给大家看个实际例子,这是我在一个车载CAN通信模块里写的:

static int can_isr(int irq, void *context, FAR void *arg)
{
    struct can_dev_s *dev = (struct can_dev_s *)arg;
    uint32_t status;

    /* 读取中断状态寄存器 */
    status = getreg32(dev->base + CAN_IRQ_STATUS);

    /* 处理接收中断 */
    if (status & CAN_IRQ_RX) {
        can_receive(dev);
    }

    /* 处理发送完成中断 */
    if (status & CAN_IRQ_TX) {
        can_transmit_done(dev);
    }

    /* 清除中断标志 */
    putreg32(status, dev->base + CAN_IRQ_CLEAR);

    return OK;
}

/* 注册中断 */
irq_attach(CAN_IRQ_NUMBER, can_isr, &g_can_dev);
up_enable_irq(CAN_IRQ_NUMBER);

关键点:ISR里不要做复杂的数据处理,只做最必要的硬件操作。把耗时的工作通过消息队列或信号量交给任务线程去做。我曾经见过一个同事在ISR里调用了printf(),结果系统直接卡死——因为printf内部用了互斥锁,而中断里不能等锁。

4.3 中断嵌套与优先级管理

中断嵌套,就是高优先级中断打断低优先级中断的处理。NuttX支持这个特性,但用起来要小心。

NuttX的中断优先级管理,底层依赖芯片的NVIC(嵌套向量中断控制器)。每个中断都可以设置抢占优先级和子优先级。抢占优先级决定能否打断,子优先级决定同优先级下的响应顺序。

配置方法很简单,调用up_prioritize_irq()

int up_prioritize_irq(int irq, int priority);

优先级数值越小,优先级越高。嗯,这里要注意,不同芯片的优先级位数不一样,STM32是4位,也就是0-15级。

我给大家整理了一个优先级分配的建议方案:

优先级 中断类型 说明
0-1 系统时钟、看门狗 最高优先级,保证系统心跳
2-3 实时性要求高的外设(CAN、SPI) 通信不能丢数据
4-7 普通外设(UART、I2C) 可以容忍少量延迟
8-15 GPIO、定时器 低优先级,可被其他中断打断

警告:中断嵌套层级不要超过3层。嵌套太深,栈空间容易爆掉。我在一个项目里就吃过这个亏——嵌套了5层,结果栈溢出,系统莫名其妙复位。查了两天才找到原因。

4.4 系统异常处理

系统异常,说白了就是CPU遇到了它处理不了的情况。比如访问了非法地址、执行了未定义指令、除零操作等等。

NuttX对异常的处理策略是:能恢复就恢复,不能恢复就记录信息然后复位。你看,这跟汽车电子的安全理念是一致的——出了问题,先保证系统不崩溃,再想办法恢复。

异常处理函数通常长这样:

static int fault_handler(int irq, void *context, FAR void *arg)
{
    struct fault_info_s *info = (struct fault_info_s *)context;

    /* 打印异常信息到控制台 */
    _err("Fault! IRQ: %d, PC: 0x%08x, LR: 0x%08x\n",
         irq, info->pc, info->lr);

    /* 保存异常上下文到非易失存储 */
    fault_save_to_nvram(info);

    /* 尝试恢复 */
    if (fault_can_recover(info)) {
        return fault_recover(info);
    }

    /* 无法恢复,触发系统复位 */
    up_systemreset();

    return OK; /* 不会执行到这里 */
}

这里有个细节——context参数里保存了异常发生时的CPU寄存器状态。通过分析PC指针和堆栈,就能定位到是哪行代码出了问题。

我在实际项目中,会把异常信息保存到备份寄存器或专用Flash区域。下次启动时读取出来,通过诊断接口上报。这样即使系统复位了,也能知道之前发生了什么故障。

避坑指南:我曾经遇到一个诡异的问题——系统偶尔会进入HardFault,但没有任何规律。后来发现是中断服务函数里用了一个未初始化的指针。从那以后,我养成了一个习惯:所有ISR里用到的全局变量,都在系统初始化时显式赋初值。

4.5 实战经验总结

说了这么多,我给大家总结几条实战经验:

  • 中断优先级要统一规划——别想到哪配到哪,画个表格,一目了然。
  • ISR里别调printf——这个坑我已经替你们踩过了。
  • 异常处理要留痕迹——哪怕只是闪个LED,也比默默复位强。
  • 中断嵌套要克制——不是所有中断都需要高优先级,够用就行。

中断和异常处理,是嵌入式系统里最考验功力的地方。你想想看,一个系统里几十个中断源,优先级、嵌套、共享资源、延迟要求……这些因素交织在一起,稍有不慎就会出问题。但反过来,如果你能把这块玩明白了,那其他RTOS的机制对你来说就是小菜一碟。

好了,这一章的内容就到这里。下一章我们聊聊NuttX的任务调度——说白了就是CPU怎么在多个任务之间来回切换,让每个任务都觉得“CPU是我一个人的”。