2、硬件基础:CPU架构(ARM/RISC-V)、内存映射、外设寄存器访问方式、启动模式与启动介质概述
Bootloader 的本质是硬件与操作系统之间的桥梁,其首要任务是在上电后的极短时间内完成对 CPU、内存、外设和启动介质的初始化,为内核运行准备好一个稳定、可控的硬件环境。因此,理解 CPU 架构、内存映射、寄存器访问方式以及启动介质的工作原理,是编写可靠 Bootloader 的前提。本章将围绕这四个核心维度展开,帮助读者建立从芯片手册到实际代码的映射能力。
2.1 CPU 架构:ARM 与 RISC-V 的启动关键差异
Bootloader 编写者首先需要回答一个问题:CPU 上电后处于什么状态?对于 ARM 架构,Cortex-A 系列处理器上电后通常处于 EL3(若存在 TrustZone)或 SVC 模式,MMU 关闭、Cache 关闭、异常向量表地址未知(或指向 ROM 中的固定地址)。而 RISC-V 架构则从上电时的 Machine Mode(M 模式)开始执行,所有中断默认关闭,PC 从芯片厂商定义的 Reset Vector 地址取值。这两种架构在启动路径上的差异,直接决定了 Bootloader 第一行汇编代码的写法。
在实际开发中,判断当前 CPU 架构和实现版本是第一步。ARM 通过读取 MIDR 寄存器获取厂商 ID 和产品 ID;RISC-V 则通过 misa 寄存器(Machine ISA)识别支持的扩展指令集。例如,若 misa 的 bit2 为 1,则支持 C 压缩指令扩展,这会影响 Bootloader 代码的编译选项和链接地址对齐要求。
| 对比维度 | ARM Cortex-A(典型) | RISC-V(RV64GC) |
|---|---|---|
| 上电模式 | EL3 / SVC | Machine Mode(M) |
| 异常向量表 | VBAR / VBAR_ELx | mtvec(Direct/Vectored) |
| MMU 初始状态 | 关闭 | 关闭 |
| 通用寄存器位宽 | 32/64 可选 | 32/64 可选 |
| 启动时中断状态 | 默认屏蔽(DAIF 置位) | 默认屏蔽(mstatus.MIE=0) |
| 特权指令访问 | CPSR/SPSR 控制 | mstatus/mepc/mcause 控制 |
一个常见的误区是认为 Bootloader 只需要设置栈指针和跳转到 C 语言入口即可。实际上,在跳转之前必须完成至少三项工作:配置异常向量表、设置 CPU 工作模式(关闭中断)、初始化栈指针(SP)。对于 ARM,还需要考虑大小端模式(通常通过 SCTLR 寄存器配置);对于 RISC-V,则需要确认 misa 扩展位是否与编译时使用的 -march 选项匹配,否则执行非法指令会触发异常。
2.2 内存映射:链接脚本与地址窗口的工程实践
内存映射是 Bootloader 开发中最容易出错、也最需要严谨对待的部分。每个 SoC 的数据手册都会给出完整的地址映射表,例如:0x0000_0000 可能映射到片内 ROM,0x2000_0000 映射到 SRAM,0x4000_0000 映射到外设寄存器,0x8000_0000 映射到 DDR。Bootloader 的链接脚本(Linker Script)必须严格遵循这个映射关系,否则代码跳转后立即崩溃。
以典型的 ARM 嵌入式 SoC 为例,Bootloader 通常分为两个阶段:Stage 1 在片内 SRAM 中运行(因为 DDR 尚未初始化),Stage 2 则被复制到 DDR 中运行。因此,链接脚本需要定义两个不同的内存区域(MEMORY 命令),并为每个阶段设置独立的加载地址(LMA)和虚拟地址(VMA)。在 MMU 开启之前,LMA 和 VMA 相同;开启 MMU 后,VMA 可能被映射到更高的地址空间。
/* 典型 ARM Bootloader 链接脚本片段 */
MEMORY
{
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 256K
DDR (rwx) : ORIGIN = 0x80000000, LENGTH = 512M
}
SECTIONS
{
.text_stage1 :
{
*(.text.stage1)
} > SRAM
.text_stage2 :
{
*(.text*)
} > DDR
.data :
{
*(.data*)
} > DDR AT> SRAM /* 数据从 SRAM 加载,运行在 DDR */
}
判断内存映射是否正确的方法并不复杂:在 Bootloader 启动后,通过打印关键符号的地址(例如 &_start、&__bss_start)与链接脚本中的预期值进行比对。如果地址落在错误的区域,说明链接脚本或启动代码中的地址计算有误。另一个常见问题是内存属性配置——在开启 MMU 后,外设区域必须标记为 Device 或 Strongly-ordered 内存类型,禁止使用 Cache 和写缓冲,否则外设寄存器读写可能被乱序执行。
2.3 外设寄存器访问方式:MMIO 与位操作技巧
Bootloader 对外设的控制本质上是对寄存器的读写。绝大多数 SoC 采用内存映射 I/O(MMIO)方式,即外设寄存器被映射到 CPU 的地址空间中,通过普通的 load/store 指令访问。例如,UART 的数据寄存器地址可能是 0x40001000,状态寄存器地址是 0x40001014。在 C 语言中,通常使用指针强制类型转换来访问:
#define UART_BASE 0x40001000UL
#define UART_DR (*(volatile unsigned int *)(UART_BASE + 0x00))
#define UART_FR (*(volatile unsigned int *)(UART_BASE + 0x18))
void uart_putc(char c)
{
/* 等待发送 FIFO 为空 */
while (UART_FR & (1 << 5));
UART_DR = c;
}
这里的关键是 volatile 关键字。它告诉编译器该变量的值可能随时被硬件改变,禁止编译器将其优化到寄存器中缓存。如果没有 volatile,编译器可能将 UART_FR 的读取操作优化为只读一次,导致死循环。此外,访问宽度必须与寄存器定义一致——32 位寄存器不能使用 8 位指针访问,否则在 ARM 上会触发 Unaligned Access 异常,在 RISC-V 上则可能产生非对齐访问陷阱。
对于需要修改寄存器中某几位而保持其他位不变的场景,必须使用读-改-写(Read-Modify-Write)操作。例如,要设置 GPIO 方向寄存器的 bit3 为输出,同时不影响其他引脚:
volatile uint32_t *gpio_dir = (volatile uint32_t *)0x50000000;
uint32_t tmp = *gpio_dir; /* 读 */
tmp |= (1 << 3); /* 改 */
*gpio_dir = tmp; /* 写 */
在 ARM 和 RISC-V 中,这种操作不是原子性的,如果中断在读写之间发生,可能导致数据丢失。因此,在 Bootloader 中操作关键寄存器时,应暂时屏蔽中断,或使用位带(Bit-band)操作(Cortex-M 系列支持)来保证原子性。对于 RISC-V,没有位带机制,但可以通过原子指令(AMO)实现类似功能,不过 Bootloader 阶段通常直接屏蔽中断更简单。
| 操作类型 | ARM 实现 | RISC-V 实现 | 注意事项 |
|---|---|---|---|
| 读寄存器 | LDR | LW/LD | 必须使用 volatile 指针 |
| 写寄存器 | STR | SW/SD | 注意访问宽度匹配 |
| 读-改-写 | LDR + ORR + STR | LW + OR + SW | 非原子,需关中断 |
| 内存屏障 | DSB/DMB/ISB | fence | 外设操作后必须加 DSB |
| 位操作 | BIC/ORR 指令 | ANDI/ORI 指令 | RISC-V 的 ANDI 仅支持 12 位立即数 |
2.4 启动模式与启动介质:从 ROM 到 DDR 的完整链路
启动模式(Boot Mode)由 SoC 的特定引脚或 eFuse 决定。常见的启动介质包括:NOR Flash(支持 XIP,可直接执行代码)、NAND Flash(需要软件读取并复制到 RAM)、eMMC/SD 卡(通过 SD/MMC 控制器访问)、SPI NOR/NAND(通过 SPI 控制器访问)、UART/USB(用于调试和工厂烧录)。Bootloader 的第一阶段(ROM Code)由芯片厂商固化,其作用是根据启动模式从对应介质中读取 Bootloader 的第一段代码到 SRAM,然后跳转执行。
以典型的 i.MX 系列处理器为例,其启动流程如下:
- ROM Code 读取 Boot Mode 引脚(BOOT_MODE[1:0])和 Boot Device 配置引脚。
- 根据配置初始化对应的启动介质控制器(如 SDHC、QSPI、NAND Controller)。
- 从介质中读取 Bootloader 镜像头部(包含 DCD 数据,用于初始化 DDR)。
- 将 Bootloader 主体加载到 SRAM 或 DDR 中,并跳转执行。
对于 Bootloader 开发者来说,理解启动介质的关键在于:不同介质的读取方式差异巨大。NOR Flash 支持 XIP(Execute in Place),CPU 可以直接从 Flash 地址取指执行,无需复制到 RAM,但写入速度慢、容量小。NAND Flash 容量大、价格低,但坏块管理和 ECC 校验是必须处理的难题。eMMC 内置控制器,支持块读写,但需要初始化 MMC 协议栈,代码复杂度较高。
在启动介质的选择上,还需要考虑可靠性。NAND Flash 存在坏块问题,Bootloader 必须实现坏块跳过机制,并在写入时进行 ECC 校验。eMMC 虽然内置 ECC,但 Bootloader 需要处理分区表和启动分区概念。对于量产产品,建议使用 eMMC 或 SPI NOR,避免 NAND 的坏块管理复杂度。对于开发调试阶段,UART 或 USB 启动模式非常有用,可以快速下载代码到 SRAM 中运行,无需反复烧写 Flash。
int storage_read(unsigned int offset, void *buf, unsigned int size) 函数,底层分别实现 NOR、NAND、eMMC 的读取逻辑。这样上层代码无需关心具体介质类型,便于移植和调试。同时,在启动介质读取函数中,务必加入超时机制,防止控制器挂死导致 Bootloader 卡死。