第一章:中断触发到响应的完整路径

大家好,我是老李。做嵌入式这么多年,中断处理这块我踩过的坑,估计能写个小册子了。今天咱们就来聊聊U-Boot里的中断流程——从硬件触发到软件响应的完整路径。

说实话,很多人觉得U-Boot里的中断很简单,不就是注册个函数嘛。但实际项目中,中断处理出问题,往往是最难排查的。我曾在某个ARM平台上,因为中断优先级配置不当,导致系统在启动阶段频繁死机,查了整整两天才找到原因。

1.1 中断触发的硬件路径

先看硬件层面。一个中断信号从外设产生,到CPU响应,中间要经过哪些环节?

  1. 外设产生中断请求:比如UART收到一个字节,或者GPIO电平变化
  2. 中断控制器仲裁:GIC(Generic Interrupt Controller)负责收集各路中断,按优先级排队
  3. CPU接收中断信号:IRQ或FIQ引脚被拉低
  4. CPU保存上下文:自动保存PC、CPSR等关键寄存器到栈中
  5. 跳转到中断向量表:根据异常类型,跳转到对应的向量地址

核心要点:中断向量表是硬件和软件的桥梁。CPU一旦检测到中断,就会强制跳转到向量表指定的地址。这个地址在U-Boot中通常是链接脚本里定义的_start或irq_handler。

嗯,这里要注意:不同架构的向量表位置不一样。ARM的向量表通常在0x00000000或0xFFFF0000,而RISC-V的mtvec寄存器指向的地址才是入口。我当年从ARM转到RISC-V时,就因为这个差异栽过跟头。

1.2 U-Boot中的中断向量表

U-Boot的向量表定义在arch/arm/lib/vectors.S中。咱们直接看代码:

/*
 * 中断向量表 - 每个入口4字节
 * 注意:这里用的是ARM模式,不是Thumb
 */
_start:
    b   reset            @ 0x00: Reset
    b   undefined_instruction @ 0x04: Undefined Instruction
    b   software_interrupt    @ 0x08: Software Interrupt (SWI)
    b   prefetch_abort        @ 0x0C: Prefetch Abort
    b   data_abort            @ 0x10: Data Abort
    b   not_used              @ 0x14: Reserved
    b   irq_handler           @ 0x18: IRQ
    b   fiq_handler           @ 0x1C: FIQ

看到没?每个入口就是一个跳转指令。为什么用b指令而不是ldr?因为b指令是位置无关的,不管代码加载到哪个地址都能正确跳转。这个细节,我在做安全启动时深有体会——如果向量表用了绝对地址,一旦代码重定位就全乱了。

1.3 中断服务程序的注册流程

U-Boot里注册ISR,说白了就是告诉中断控制器:这个中断来了,你帮我调用这个函数。具体流程如下:

步骤 操作 关键函数/宏
1 初始化中断控制器 irq_init()
2 注册ISR到中断表 irq_install_handler()
3 使能具体中断源 enable_interrupts()
4 全局中断使能 local_irq_enable()

我建议你重点关注第二步。irq_install_handler这个函数,它把ISR函数指针存到一个全局数组里。这个数组的大小通常是NR_IRQS,也就是系统支持的最大中断数。我曾经遇到一个bug,就是数组越界——注册的中断号超过了NR_IRQS,结果把别的数据给覆盖了。

/*
 * 注册中断处理函数
 * @irq: 中断号
 * @handler: 处理函数指针
 * @data: 传递给处理函数的私有数据
 */
int irq_install_handler(int irq, interrupt_handler_t *handler, void *data)
{
    if (irq < 0 || irq >= NR_IRQS)
        return -EINVAL;

    /* 保存到中断表 */
    irq_handler_list[irq].handler = handler;
    irq_handler_list[irq].data = data;

    return 0;
}

小技巧:注册ISR时,最好把data参数用起来。比如传递一个设备结构体指针,这样在ISR里就能直接访问设备寄存器,省得再去全局变量里找。

1.4 中断响应的完整路径

好了,现在我们把整个路径串起来。假设UART收到一个字节,触发中断:

  1. UART控制器拉高中断请求线
  2. GIC检测到中断,查优先级表,决定是否转发给CPU
  3. CPU进入IRQ模式,自动保存CPSR和返回地址
  4. CPU跳转到0x18处的irq_handler
  5. irq_handler保存所有寄存器到栈
  6. 调用do_irq()函数,读取GIC的IAR寄存器获取中断号
  7. 根据中断号查表,调用对应的ISR
  8. ISR处理完,写EOI寄存器通知GIC
  9. 恢复寄存器,执行返回指令

你想想看,这9步里哪一步最容易出问题?我个人经验是第6步——读取中断号。如果GIC配置不对,读到的中断号可能是0xFF(伪中断),这时候如果没做有效性检查,直接去查表就会访问非法地址。

避坑指南:我曾经在一个项目中,因为GIC的优先级掩码设置得太高,导致低优先级中断一直被阻塞。表面上看系统没反应,实际上中断一直在pending状态。排查方法很简单——读GIC的PENDING寄存器,看看有没有中断在排队。

1.5 中断处理的SVG流程图

下面这张图,是我根据实际调试经验画的。它展示了从外设中断到ISR执行的完整路径:

U-Boot中断处理完整路径 外设(UART/GPIO) 中断请求 GIC中断控制器 IRQ信号 CPU(IRQ模式) 保存上下文 中断向量表 跳转irq_handler irq_handler 调用do_irq() do_irq() 查中断号 ISR表 执行ISR 关键寄存器说明: IAR(Interrupt Acknowledge Register):读取获取中断号 EOI(End of Interrupt Register):写入通知中断结束 CPSR(Current Program Status Register):保存CPU状态

1.6 实际项目中的注意事项

最后,我分享几个实战经验:

  • 中断嵌套要谨慎:U-Boot里默认不开启中断嵌套。如果你非要开,记得把栈空间算好,别嵌套几层就把栈撑爆了。
  • 临界区保护:在ISR里访问共享数据时,一定要关中断。我习惯用local_irq_save/local_irq_restore这对宏,比直接调用enable/disable更安全。
  • ISR要短小精悍:U-Boot的ISR里别做复杂操作。我见过有人在ISR里调用printf,结果因为printf本身用了UART,导致递归中断——直接死机。
  • 调试手段:如果中断不触发,先查GIC的使能位,再查CPU的CPSR里的I位。这两个地方最容易忽略。

我的习惯:每次写ISR之前,我都会先画一个时序图,把中断触发、处理、清除的流程理清楚。这样写出来的代码,基本不会出大问题。

好了,中断处理的完整路径就讲到这里。记住一句话:中断处理的核心不是写ISR,而是理解从硬件到软件的整个链路。链路通了,问题就好解决了。

可可精品资料库