3. 硬件初始化基础:CPU模式设置、异常向量表重定位、关看门狗与中断

各位同学,咱们今天聊点硬核的。Bootloader 启动的第一步,说白了就是让 CPU 先「清醒过来」,知道自己该在什么状态下干活。

我刚开始做嵌入式底层时,总觉得硬件初始化不就是写几个寄存器吗?后来吃过亏才明白——CPU 模式没设对,后面跑飞了都不知道怎么死的。嗯,这节课咱们就把这三件事掰开揉碎了讲清楚。

3.1 CPU 模式设置:SVC / IRQ / FIQ

ARM 处理器有 7 种工作模式,但 Bootloader 启动阶段我们只关心三种:SVC(超级用户模式)、IRQ(普通中断模式)、FIQ(快速中断模式)。

为什么?因为复位后 CPU 默认进入 SVC 模式,这是最高权限模式,可以访问所有系统资源。你想想看,如果 Bootloader 一开始跑在用户模式,连个 CP15 协处理器都碰不了,那还怎么初始化内存?

模式 CPSR 位域 用途 启动阶段是否使用
SVC 10011 操作系统保护模式,可访问全部资源 ✅ 必须
IRQ 10010 普通中断处理 ❌ 关闭
FIQ 10001 快速中断处理,有独立寄存器组 ❌ 关闭
UND 11011 未定义指令异常 ⚠️ 需设置向量
ABT 10111 数据/预取中止 ⚠️ 需设置向量
SYS 11111 系统模式,与 SVC 共享寄存器 ❌ 暂不使用
USR 10000 用户模式,受限访问 ❌ 不可用

我个人习惯在复位向量第一条指令就显式设置 CPSR,而不是依赖默认值。为什么?因为有些芯片的复位默认模式可能被 BootROM 改过,你直接往下跑可能就进了陷阱。

核心原则:Bootloader 启动阶段,CPU 必须保持在 SVC 模式,并关闭所有中断。等内核启动后再按需打开。

来看一段典型的汇编代码:

@ 设置 CPU 为 SVC 模式,关闭 IRQ 和 FIQ
MRS     r0, cpsr          @ 读取当前 CPSR
BIC     r0, r0, #0x1F     @ 清除模式位 [4:0]
ORR     r0, r0, #0xD3     @ 设置 SVC 模式 (10011) + 关 IRQ(bit7) + 关 FIQ(bit6)
MSR     cpsr_c, r0        @ 写入 CPSR

这里 0xD3 怎么来的?0xD3 = 1101 0011,bit7=1 关 IRQ,bit6=1 关 FIQ,低 5 位 10011 就是 SVC 模式。我在项目中遇到过有人只设了模式位没关中断,结果调试时莫名其妙进中断,查了两天才发现是这里漏了。

小技巧:设置 CPSR 时建议用 cpsr_c 只写控制位,不要动条件标志位。否则可能把之前的状态位搞乱,调试时很难复现。

3.2 异常向量表重定位

异常向量表,就是 CPU 发生异常时跳转的入口地址表。ARM 默认把向量表放在 0x00000000 地址,但问题来了——

你的 Bootloader 可能跑在 ROM 里,也可能跑在 RAM 里,甚至可能从 Nor Flash 启动。向量表的位置必须跟着代码走。

我记得有一次做项目,芯片上电默认从内部 SRAM 启动,向量表在 0x00000000。但后来我把 Bootloader 搬到了 DDR 里运行,忘了重定位向量表,结果一触发中断 CPU 直接跳回 SRAM 的旧向量表……那场面,真是「死都不知道怎么死的」。

ARM 提供了两种重定位方式:

  • 方式一:使用 CP15 协处理器的 VBAR 寄存器(Cortex-A 系列)
  • 方式二:使用 MCR p15, 0, r0, c12, c0, 0 指令设置向量基址

代码示例:

@ 异常向量表重定位到当前代码地址
LDR     r0, =_start        @ _start 是向量表起始标签
MCR     p15, 0, r0, c12, c0, 0   @ 写 VBAR 寄存器
ISB                             @ 指令同步屏障,确保生效

这里有个坑——ISB 指令不能省。我见过有人写完 VBAR 直接往下跑,结果下一条指令就触发了异常,CPU 还是跳到旧向量表。为什么?因为 VBAR 写入后需要等流水线刷新,ISB 就是干这个的。

注意:如果芯片不支持 VBAR(比如一些老 ARM7/ARM9),只能用向量表复制 + 内存映射的方式。这种情况下,你必须确保目标地址是 32 字节对齐的,且映射为可执行区域。

异常向量表的结构是这样的:

_start:
    b   reset_handler      @ 0x00: 复位
    b   undef_handler      @ 0x04: 未定义指令
    b   swi_handler        @ 0x08: 软件中断 (SVC)
    b   pabt_handler       @ 0x0C: 预取中止
    b   dabt_handler       @ 0x10: 数据中止
    nop                     @ 0x14: 保留
    b   irq_handler        @ 0x18: IRQ
    b   fiq_handler        @ 0x1C: FIQ

每个向量占 4 字节,一共 8 个向量,32 字节。你想想看,如果向量表没对齐到 32 字节边界,CPU 取指令时就会错位——这又是一个隐蔽的坑。

3.3 关看门狗与中断

看门狗,说白了就是一个「倒计时炸弹」。芯片上电后看门狗默认是开的,如果你不在规定时间内喂狗,它就帮你复位。但 Bootloader 启动阶段,你还在初始化时钟、配置内存,哪有空去喂狗?

所以,第一件事就是关掉看门狗。

不同芯片的看门狗寄存器位置不一样,但套路都一样:找到对应的寄存器,写一个解锁序列,然后禁用它。

以 S3C2440 为例:

@ 关看门狗
LDR     r0, =0x53000000    @ WTCON 寄存器地址
MOV     r1, #0x0000        @ 写 0 禁用看门狗
STR     r1, [r0]

就这么简单?嗯,但有些芯片的看门狗有写保护,需要先写解锁密钥。比如 i.MX6 的看门狗,你得先往 WDOG_WSR 写 0x5555,再写 0xAAAA,才能修改控制寄存器。

避坑指南:我曾经在某个项目里只关了主看门狗,没关内部独立看门狗(IWDG),结果 Bootloader 跑到一半被 IWDG 复位了。查了两天,最后发现是数据手册第 1200 页的小字写的……所以,关看门狗一定要关干净。

关中断就更直接了——在 CPSR 里把 IRQ 和 FIQ 的屏蔽位拉高就行。但要注意,有些外设的中断使能寄存器是独立于 CPU 中断屏蔽的。你光关了 CPSR 的 IRQ 位,但定时器、DMA 这些外设的中断标志还在积累,一旦你后面开了中断,它们会一股脑涌进来。

我建议的做法是:

  1. 设置 CPSR 关 IRQ/FIQ(前面已经做了)
  2. 遍历所有外设的中断使能寄存器,全部清零
  3. 清除所有挂起的中断标志位

代码示意:

@ 关所有外设中断(以某款芯片为例)
LDR     r0, =INT_BASE_ADDR    @ 中断控制器基址
MOV     r1, #0x0
STR     r1, [r0, #INTMSK]    @ 屏蔽所有中断
STR     r1, [r0, #INTCLEAR]  @ 清除所有挂起中断

3.4 知识结构总览

这三件事做完,CPU 才算有了一个「干净」的启动环境。我把它们的关系画成一张图,方便你理解:

硬件初始化核心流程 硬件初始化三大核心步骤 CPU 模式设置 SVC / IRQ / FIQ 异常向量表重定位 VBAR / 向量复制 关看门狗与中断 WDT 禁用 + 中断屏蔽 读取 CPSR → 修改模式位 屏蔽 IRQ/FIQ 位 写回 CPSR_c 获取向量表基址 写入 VBAR 寄存器 执行 ISB 同步 解锁看门狗(如需) 写 0 禁用 WDT 屏蔽所有外设中断 ⚠️ 启动阶段核心原则 SVC 模式 + 关中断 + 关看门狗 → 干净的启动环境 任何一步遗漏,都可能导致 Bootloader 启动异常

这张图把三个步骤的依赖关系画清楚了。注意看,三个步骤之间没有严格的先后顺序,但我个人建议按「CPU 模式 → 关看门狗 → 向量表重定位」的顺序来。为什么?因为设置 CPU 模式时你可能需要关中断,而向量表重定位之前最好确保看门狗已经关了,免得写到一半被复位。

3.5 常见问题与避坑

坑一:关看门狗后没有喂狗延时。有些芯片的看门狗禁用需要等待几个时钟周期才能生效,如果你紧接着就去操作其他寄存器,可能触发复位。解决方案:关 WDT 后加 3-5 个 NOP 空操作。

坑二:向量表重定位后没有重新映射异常入口。如果你把向量表搬到了 DDR,但 DDR 还没初始化完成,那 CPU 一跳转就挂了。所以向量表重定位必须在内存初始化之后做。

经验之谈:我习惯在向量表重定位后,主动触发一个软件中断(SWI)来验证新向量表是否生效。如果 CPU 能正确跳转到 SWI 处理函数,说明重定位成功了。这个小测试能帮你省下大量调试时间。