第一章 中断系统入门:ARM Cortex-A系列中断控制器(GIC)基础、U-Boot中的中断向量表

各位同学,咱们今天聊聊中断。说实话,中断这玩意儿,是嵌入式系统的灵魂之一。你想想看,没有中断,CPU就得轮询——那效率得多低啊。我早年做第一个ARM项目时,就因为中断没配好,系统跑起来跟老牛拉车似的。后来才明白,搞懂GIC和中断向量表,是U-Boot稳定性的基石。

1.1 ARM Cortex-A中断控制器:GIC是个啥?

GIC,全称Generic Interrupt Controller。说白了,它就是CPU和外设之间的一个“总机”。外设有事要报告,先打给GIC,GIC再决定要不要转给CPU。

ARM Cortex-A系列用的GIC,目前主流是GICv2和GICv3。咱们U-Boot里,绝大多数平台还在用GICv2。为什么?因为简单、够用。我建议你先把GICv2吃透,v3无非是多了一些虚拟化和安全扩展。

核心要点:GIC负责中断的优先级判断、分发和屏蔽。它把中断分为三类:

  • SPI(共享外设中断):所有核都能收到,比如网卡、定时器。
  • PPI(私有外设中断):每个核独享,比如每个核自己的本地定时器。
  • SGI(软件生成中断):一个核写给另一个核,用于核间通信。

我在项目中遇到过一个问题:两个核同时收到了同一个SPI中断,结果处理函数里没做互斥,直接导致数据错乱。嗯,这里要注意——SPI虽然所有核都能收,但你得在软件里指定哪个核来处理,否则就是给自己挖坑。

1.2 GICv2的寄存器模型

GICv2由两个主要模块组成:Distributor(分发器)和CPU Interface(CPU接口)。Distributor负责全局中断管理,CPU Interface负责和具体某个核打交道。

咱们直接看几个关键寄存器:

寄存器 偏移地址 作用
GICD_CTLR 0x0000 Distributor控制寄存器,使能/禁用整个GIC
GICD_ISENABLERn 0x0100 + 4*n 中断使能寄存器组,每个bit对应一个中断ID
GICD_ICPENDRn 0x0280 + 4*n 中断清除挂起寄存器,用于取消一个挂起的中断
GICC_IAR 0x000C CPU接口的中断确认寄存器,读它就知道是哪个中断来了
GICC_EOIR 0x0010 中断结束寄存器,处理完中断后写它,告诉GIC我搞定了

我个人习惯,初始化GIC时先关掉Distributor,配好所有中断的优先级和目标核,最后再打开。顺序错了,容易出一些莫名其妙的问题。

小技巧:调试GIC时,可以用GICD_ISPENDRn寄存器来软件触发一个中断。这样不用接外设,就能验证你的中断处理流程通不通。我曾经靠这招,半小时定位了一个中断丢失的bug。

1.3 U-Boot中的中断向量表

中断向量表,就是一张“地址表”。CPU收到中断后,硬件自动跳转到这张表里对应的地址去执行。ARM Cortex-A的向量表,从地址0x00000000开始,每个向量占4个字节。

标准的向量表布局是这样的:

0x00000000: 复位向量          (Reset)
0x00000004: 未定义指令向量     (Undefined Instruction)
0x00000008: 软件中断向量       (SWI)
0x0000000C: 预取中止向量       (Prefetch Abort)
0x00000010: 数据中止向量       (Data Abort)
0x00000014: 保留
0x00000018: IRQ中断向量        (IRQ)
0x0000001C: FIQ快速中断向量    (FIQ)

U-Boot里,中断向量表通常放在start.S里。你打开arch/arm/cpu/armv7/start.S,会看到类似这样的代码:

_start:
    b   reset
    ldr pc, _undefined_instruction
    ldr pc, _software_interrupt
    ldr pc, _prefetch_abort
    ldr pc, _data_abort
    ldr pc, _not_used
    ldr pc, _irq
    ldr pc, _fiq

注意看,复位向量用的是b跳转,其他向量用的是ldr pc。为什么?因为b跳转有范围限制(±32MB),而ldr pc可以从任意地址加载。U-Boot可能会被重定位到内存的高地址,用b跳复位向量没问题(因为复位后CPU还在低地址),但其他中断向量必须用ldr pc,否则一跳就飞了。

警告:千万不要在U-Boot里直接写中断处理函数时,把向量表放在DDR里。DDR在初始化之前是不可用的。我见过有人把向量表放在0x1000,结果DDR还没配好,一进中断直接死机。正确的做法是:向量表放在SRAM或ROM里,等DDR初始化完成后,再通过CP15协处理器把向量表基址重定位到DDR。

1.4 U-Boot中如何注册中断处理函数

U-Boot不像Linux那样有复杂的中断子系统。它很简单——你直接在向量表里放一个跳转指令,指向你的处理函数就行。

举个例子,假设你要注册一个IRQ处理函数:

void irq_handler(void)
{
    unsigned int irq_num;

    // 读取中断号
    irq_num = readl(GICC_BASE + GICC_IAR);

    // 处理中断
    printf("IRQ %d occurred!\n", irq_num);

    // 写EOIR,告诉GIC处理完毕
    writel(irq_num, GICC_BASE + GICC_EOIR);
}

然后在start.S里,把_irq指向这个函数:

_irq: .word irq_handler

就这么简单。但要注意,U-Boot默认是关中断的。你需要在board_init或arch_early_init_r里,手动打开GIC和CPU的IRQ使能位。

避坑指南:我曾经在U-Boot里注册了一个定时器中断,结果每次进入中断后,系统就卡死了。查了半天,发现是中断处理函数里调用了printf,而printf内部又用了UART,UART的发送完成中断优先级比定时器高,导致嵌套中断把栈搞崩了。解决方案:中断处理函数里尽量别用printf,用简单的寄存器操作代替。

1.5 中断使能与屏蔽的流程

在U-Boot里使能一个中断,大致分三步:

  1. 配置GIC:设置中断类型、优先级、目标核。
  2. 使能中断:写GICD_ISENABLERn对应bit。
  3. 打开CPU的IRQ:修改CPSR寄存器的I位,清除屏蔽。

代码示例:

void enable_irq(int irq_num)
{
    // 1. 设置中断为level触发,优先级0x80,目标核0
    writel(0x80800000, GICD_BASE + GICD_ICFGR(irq_num / 16));
    writel(0x80, GICD_BASE + GICD_IPRIORITYR(irq_num));
    writel(0x01, GICD_BASE + GICD_ITARGETSR(irq_num));

    // 2. 使能中断
    writel(1 << (irq_num % 32), GICD_BASE + GICD_ISENABLER(irq_num / 32));

    // 3. 打开CPU IRQ
    asm volatile("cpsie i" : : : "memory");
}

屏蔽中断就反过来:写GICD_ICENABLERn,然后执行"cpsid i"。

嗯,这里有个细节——GICD_ITARGETSR寄存器在GICv2里是每个中断占一个字节,但如果你用的是多核,这个字节的bit[7:4]表示目标核掩码。单核系统直接写0x01就行。

1.6 本章知识体系总览

下面这张图,是我画的中断系统核心逻辑。你看一眼,就能把GIC和向量表的关系串起来:

ARM Cortex-A 中断处理流程 外设(网卡/定时器) 产生中断信号 GIC Distributor 优先级判断/分发 使能/屏蔽/挂起管理 GIC CPU Interface IAR/EOIR寄存器 向CPU发送IRQ/FIQ CPU Core CPSR.I位检查 跳转向量表 中断向量表(位于SRAM/ROM) 0x00000000: b reset 0x00000018: ldr pc, _irq 0x0000001C: ldr pc, _fiq 中断处理函数 读IAR → 处理 → 写EOIR 硬件自动跳转

从这张图你能看到,外设产生中断后,信号经过GIC Distributor做优先级判断,再通过CPU Interface送给CPU。CPU检查CPSR的I位——如果中断没被屏蔽,就自动跳转到向量表里对应的地址,然后执行你的处理函数。处理完别忘了写EOIR,否则GIC以为你还在忙,不会再发下一个中断。

我的经验:刚开始学GIC时,最容易犯的错就是忘记写EOIR。你想想看,中断处理完了,你不告诉GIC,GIC就一直等着。结果就是——系统只响应一次中断,后面全丢了。我建议你在写中断处理函数时,第一件事就是读IAR保存中断号,最后一件事就是写EOIR。养成这个习惯,能省很多调试时间。

好了,这一章的内容就到这儿。GIC和向量表是中断系统的地基,地基打不牢,后面盖什么楼都得塌。下一章咱们会深入U-Boot的中断初始化流程,看看start.S里到底是怎么把GIC和向量表串起来的。

可可精品资料库