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 是垃圾值,一压栈就崩了。
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 字节。但有些芯片只实现了部分中断,复制多了浪费,复制少了漏了。我建议直接读芯片参考手册,看它实现了多少个异常,然后按那个大小复制。
3.2.2 RISC-V 的异常向量表
RISC-V 的异常向量表通过 mtvec(机器模式陷阱向量基址)寄存器设置:
# RISC-V:设置异常向量表基址
la t0, _vector_table # 获取向量表地址(在 RAM 中)
csrw mtvec, t0 # 写入 mtvec 寄存器
RISC-V 的 mtvec 寄存器还有一个 MODE 字段,决定向量表是直接模式(所有异常跳到同一个入口)还是向量模式(每个异常有独立入口)。Bootloader 通常用向量模式,这样每个异常都能快速定位。
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; // 喂狗
3.3.2 关闭所有中断
关闭中断,不只是关 CPU 的中断总开关,还要关掉各个外设的中断使能。为什么?因为外设可能在复位后处于不确定状态,它的中断标志位可能是置位的。如果你只关了 CPU 的中断,外设的中断请求还在那挂着。等你一开中断,它立马就进来了。
关闭中断的步骤:
- 关 CPU 中断总开关: ARM 用 CPSID 指令,RISC-V 清 MIE 位。
- 关外设中断使能: 遍历所有外设,清除其中断使能寄存器。
- 清除 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 还是会响应。虽然概率很低,但我在项目中确实遇到过——外设的中断使能寄存器是分组的,关了一组,另一组的中断进来了。
3.4 本章知识体系
下面这张图,把本章的三个核心步骤串起来了。你可以看到,它们之间是有依赖关系的:
这张图里,我特意把“关闭看门狗”放在了“关闭中断”前面。为什么?因为看门狗是硬件复位,它的优先级比中断高。如果看门狗先复位了,你关中断的动作就白做了。所以,先关狗,再关中断,这个顺序不能乱。
好了,这一章的内容就到这。CPU 模式、向量表、看门狗和中断,这三件事做完,Bootloader 的“地基”就算打好了。下一阶段,我们就要开始初始化内存和时钟了——那又是另一番天地。