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

Bootloader 启动,说白了就是给硬件“梳洗打扮”,让它准备好迎接操作系统。这第一步,就是硬件初始化。我做了这么多年底层,见过太多因为初始化没做好,后面跑飞了都不知道怎么死的案例。

这一章,咱们先啃三块硬骨头:CPU 模式设置、异常向量表重定位、关闭看门狗与中断。这三件事,是 Bootloader 的“入场券”,顺序错了或者漏了,后面全是坑。

3.1 CPU 模式设置:从“混沌”到“有序”

芯片上电那一刻,CPU 处于什么状态?嗯,通常是特权模式,但具体是哪种特权模式,不同架构不一样。ARM 一般是 SVC(超级用户)模式,RISC-V 可能是 M 模式。但不管哪种,我们都要主动设置,不能依赖默认值。

为什么?因为默认值可能不对。我记得有一次调试一块新板子,Bootloader 死活跑不起来,单步跟踪发现 CPU 竟然在未定义指令模式。查了半天,原来是复位后默认模式被板级逻辑拉偏了。从那以后,我养成了一个习惯:任何 Bootloader 的第一条有效指令,必须是设置 CPU 模式。

3.1.1 ARM 架构下的模式设置

ARM 有 7 种工作模式,Bootloader 最常用的是 SVC 模式。为什么?因为它有独立的堆栈指针(SP),而且权限高,适合做初始化。

代码很简单,但细节很要命:

@ ARM 汇编:设置 CPU 进入 SVC 模式,并关闭 IRQ/FIQ
MRS     r0, cpsr        @ 读取当前程序状态寄存器
BIC     r0, r0, #0x1F   @ 清除模式位(低5位)
ORR     r0, r0, #0xD3   @ 设置模式位:SVC(0x13) + 禁止IRQ(0x80) + 禁止FIQ(0x40)
MSR     cpsr_c, r0      @ 写回 CPSR

这里有个关键点:0xD3 这个值。它不只是设了 SVC 模式,还顺带把 IRQ 和 FIQ 都关了。我习惯在模式切换的同时就把中断关了,省得后面再单独操作。你想想看,如果模式切了一半,一个中断飞进来,CPU 跑到异常向量表,而向量表还没准备好……那画面太美我不敢看。

3.1.2 RISC-V 架构下的模式设置

RISC-V 的 Bootloader 通常在 M 模式(机器模式)下运行。设置方式是通过 mstatus 寄存器:

# RISC-V 汇编:设置 M 模式,关闭全局中断
csrr    t0, mstatus        # 读取机器状态寄存器
li      t1, ~0x8           # 清除 MIE 位(机器模式中断使能)
and     t0, t0, t1
csrw    mstatus, t0        # 写回

我个人建议,在设置 CPU 模式后,立即初始化各模式的堆栈指针。ARM 有 7 个模式,每个模式都有自己的 SP。虽然 Bootloader 只用 SVC 模式,但万一发生异常,CPU 会切换到其他模式。如果那个模式的 SP 是垃圾值,一压栈就崩了。

我的习惯: 在设置完 CPU 模式后,紧接着就给所有可能用到的模式分配栈空间。哪怕只是 64 字节,也比没有强。我曾经见过一个 Bootloader,因为 FIQ 模式的 SP 没设,一个 FIQ 进来直接写爆了内存,把关键数据给覆盖了。

3.2 异常向量表重定位:把“门牌号”挂对地方

异常向量表,就是 CPU 发生异常时的“门牌号”。CPU 根据异常类型,去固定地址找处理函数。问题是,这个固定地址在哪?

很多芯片上电后,异常向量表默认在 0x00000000(或者 ROM 里)。但我们的 Bootloader 通常跑在 RAM 里,或者需要把向量表搬到 RAM 里。为什么?因为 ROM 里的向量表是固定的,我们没法改。而 Bootloader 需要注册自己的异常处理函数,比如系统定时器中断、软件中断等。

3.2.1 重定位的两种方式

方式 原理 适用场景 注意事项
复制 + 修改基址 把 ROM 中的向量表复制到 RAM,然后修改 CPU 的向量表基址寄存器 ARM Cortex-M 系列(VTOR 寄存器) 复制后要确保 RAM 地址对齐(通常 256 字节或 512 字节)
直接映射 通过 MMU 或内存映射,把 RAM 地址映射到 0x00000000 ARM Cortex-A 系列、RISC-V 需要 MMU 初始化,Bootloader 早期可能还没开 MMU

ARM Cortex-M 系列最简单,有 VTOR(向量表偏移寄存器):

// ARM Cortex-M:设置向量表基址为 RAM 中的地址
#define VECTOR_TABLE_RAM_BASE  0x20000000

// 复制向量表(假设 ROM 中的向量表在 0x08000000)
memcpy((void*)VECTOR_TABLE_RAM_BASE, (void*)0x08000000, 256);

// 设置 VTOR 寄存器
SCB->VTOR = VECTOR_TABLE_RAM_BASE;

这里有个坑:复制的大小。向量表的大小取决于芯片支持多少个异常。Cortex-M3/M4 通常有 16 个系统异常 + 最多 240 个外部中断,总共 256 个入口,每个入口 4 字节,一共 1024 字节。但有些芯片只实现了部分中断,复制多了浪费,复制少了漏了。我建议直接读芯片参考手册,看它实现了多少个异常,然后按那个大小复制。

我曾经踩过的坑: 有一次我复制向量表时,只复制了前 16 个系统异常,没复制外部中断。结果一开外设中断,CPU 直接跑飞。查了两天才发现,向量表里外部中断的入口还是 ROM 里的地址,而 ROM 里的处理函数根本没初始化。从那以后,我复制向量表都是按最大值来,宁可多复制,不能少复制。

3.2.2 RISC-V 的异常向量表

RISC-V 的异常向量表通过 mtvec(机器模式陷阱向量基址)寄存器设置:

# RISC-V:设置异常向量表基址
la      t0, _vector_table    # 获取向量表地址(在 RAM 中)
csrw    mtvec, t0            # 写入 mtvec 寄存器

RISC-V 的 mtvec 寄存器还有一个 MODE 字段,决定向量表是直接模式(所有异常跳到同一个入口)还是向量模式(每个异常有独立入口)。Bootloader 通常用向量模式,这样每个异常都能快速定位。

关键点: 异常向量表重定位必须在使能任何中断之前完成。如果中断使能了,但向量表还没搬好,CPU 一收到中断就不知道往哪跳。这就像你开了门,但门牌号还没挂好,快递员只能瞎转悠。

3.3 关闭看门狗与中断:先“静默”,再“干活”

看门狗和中断,是 Bootloader 初期最烦人的两个东西。看门狗会定时复位芯片,中断会打断执行流。在初始化完成之前,我们得先把它们都关了。

3.3.1 关闭看门狗

看门狗(Watchdog)的关闭方式,不同芯片差别很大。有些芯片有专门的寄存器,有些需要写“解锁序列”。我见过最奇葩的,需要连续写三个不同的值才能关掉。

常见的关闭方式:

  • 写控制寄存器: 直接写 0 到看门狗使能位。比如 STM32 的 IWDG,写 IWDG->KR = 0x0000 可以禁止。
  • 写解锁序列: 先写一个“魔法数”到解锁寄存器,再写配置。比如某些 NXP 芯片,需要先写 0x5A5A 到 WDOG_UNLOCK 寄存器。
  • 硬件拉高/拉低引脚: 有些芯片的看门狗使能由外部引脚控制,Bootloader 需要配置 GPIO 来关闭。

我个人建议,在关闭看门狗之后,再喂一次狗。为什么?因为有些看门狗在关闭过程中会触发一次复位,喂狗可以清除这个 pending 的复位请求。这听起来有点玄学,但我确实遇到过这种情况。

// 示例:关闭 STM32 的独立看门狗(IWDG)
// 注意:IWDG 一旦使能,只能通过复位来关闭。所以这里其实是“禁止使能”
// 真正的做法是在启动配置中就不使能 IWDG,或者在进入 Bootloader 前就关掉

// 对于窗口看门狗(WWDG),可以这样关:
WWDG->CR &= ~WWDG_CR_WDGA;  // 清除激活位

// 喂一次狗,清除可能的复位标志
IWDG->KR = 0xAAAA;  // 喂狗
避坑指南: 有些芯片的看门狗在复位后默认就是使能的。比如某些 TI 的 DSP,上电后看门狗自动开始跑,如果你不在前几百微秒内关掉它,芯片就会不断复位。所以,关闭看门狗的操作,要放在 Bootloader 的最前面,甚至比设置 CPU 模式还早。

3.3.2 关闭所有中断

关闭中断,不只是关 CPU 的中断总开关,还要关掉各个外设的中断使能。为什么?因为外设可能在复位后处于不确定状态,它的中断标志位可能是置位的。如果你只关了 CPU 的中断,外设的中断请求还在那挂着。等你一开中断,它立马就进来了。

关闭中断的步骤:

  1. 关 CPU 中断总开关: ARM 用 CPSID 指令,RISC-V 清 MIE 位。
  2. 关外设中断使能: 遍历所有外设,清除其中断使能寄存器。
  3. 清除 pending 的中断标志: 写 1 到中断标志位清除(通常写 1 清 0)。
// ARM:关 CPU 中断
__asm volatile("cpsid i");   // 关 IRQ
__asm volatile("cpsid f");   // 关 FIQ

// 关外设中断(以 GPIO 为例)
for (int i = 0; i < GPIO_PORT_MAX; i++) {
    GPIO->IMR[i] = 0;        // 关中断使能
    GPIO->ICR[i] = 0xFFFFFFFF; // 清所有 pending 中断
}

这里要注意,关中断的顺序。一定要先关 CPU 总开关,再关外设。如果顺序反了,你关外设的过程中,一个中断飞进来,CPU 还是会响应。虽然概率很低,但我在项目中确实遇到过——外设的中断使能寄存器是分组的,关了一组,另一组的中断进来了。

我曾经犯过的错: 有一次我关中断时,只关了 CPU 的 IRQ,没关 FIQ。结果 FIQ 对应的外设(高速定时器)在复位后自动使能了,Bootloader 跑着跑着,FIQ 进来,把关键的寄存器给改了。从那以后,我关中断都是 IRQ 和 FIQ 一起关,一个都不放过。

3.4 本章知识体系

下面这张图,把本章的三个核心步骤串起来了。你可以看到,它们之间是有依赖关系的:

硬件初始化第一阶段流程 硬件初始化第一阶段:CPU模式 → 向量表 → 看门狗/中断 CPU 模式设置 ARM: SVC 模式 / RISC-V: M 模式 异常向量表重定位 复制到 RAM + 设置基址寄存器 关闭看门狗 写解锁序列 / 清使能位 / 喂狗 关闭所有中断 CPU总开关 + 外设使能 + 清pending ⚠️ 顺序不可颠倒:必须先设 CPU 模式,再搬向量表,最后关看门狗和中断 如果先关中断再搬向量表,搬的过程中中断来了,CPU 找不到处理函数

这张图里,我特意把“关闭看门狗”放在了“关闭中断”前面。为什么?因为看门狗是硬件复位,它的优先级比中断高。如果看门狗先复位了,你关中断的动作就白做了。所以,先关狗,再关中断,这个顺序不能乱。

好了,这一章的内容就到这。CPU 模式、向量表、看门狗和中断,这三件事做完,Bootloader 的“地基”就算打好了。下一阶段,我们就要开始初始化内存和时钟了——那又是另一番天地。