2. 硬件初始化:CPU模式设置、时钟系统初始化、内存控制器配置、堆栈指针设置
Bootloader 启动的第一步,说白了就是让芯片“活过来”。
芯片上电后,内部逻辑会从复位向量指向的地址取第一条指令。但这时候,CPU 处于什么模式?时钟跑多快?内存能不能用?堆栈在哪?——全是未知数。所以硬件初始化的任务,就是把一个“裸片”武装成能执行 C 代码的环境。
我个人习惯把这四个步骤看作一个递进链条:先定身份(CPU模式),再定心跳(时钟),然后搭舞台(内存控制器),最后摆好椅子(堆栈指针)。顺序不能乱,少了哪一步,后面都可能直接死机。
核心原则:硬件初始化必须在进入 main() 之前完成。Bootloader 的启动代码(startup.S)里,这些操作通常由汇编语言直接控制,因为此时连堆栈都还没设好,无法调用 C 函数。
2.1 CPU 模式设置:先让处理器知道“我是谁”
ARM Cortex-M 系列上电默认处于 Thread Mode + Privileged Level,但很多 SoC 的 BootROM 或一级 Bootloader 会先切换到 Supervisor Mode(SVC)或 System Mode。为什么?因为这些模式能访问所有寄存器,而且有独立的堆栈指针。
我记得第一次调一个基于 Cortex-A8 的板子,上电后直接跑飞。查了半天,发现是 CPU 还在 IRQ Mode,堆栈指针指向未初始化区域。后来我在启动代码开头强制设成 SVC Mode,问题就解决了。
典型的 CPU 模式设置步骤:
- 读取当前 CPSR(Current Program Status Register)
- 清除模式位,写入目标模式对应的位域
- 确保中断关闭(FIQ 和 IRQ 位都置 1)
- 如果是多核处理器,还需要确认当前核是主核还是从核
@ ARM Cortex-A 系列启动代码片段
reset_handler:
/* 切换到 SVC Mode,关闭中断 */
MRS r0, cpsr
BIC r0, r0, #0x1F @ 清除模式位
ORR r0, r0, #0x13 @ SVC Mode (0b10011)
ORR r0, r0, #0xC0 @ 关闭 IRQ + FIQ
MSR cpsr, r0
/* 此时 CPU 处于特权级、无中断的安全状态 */
小技巧:设置 CPU 模式时,顺手把中断关了。等所有初始化完成、堆栈稳定后再开中断。我曾经见过有人先开中断再配时钟,结果一个定时器中断进来,堆栈还没设好,直接 HardFault。
2.2 时钟系统初始化:给芯片一颗稳定的“心脏”
时钟是芯片的脉搏。上电后,芯片通常跑在一个低速内部振荡器上(比如 8MHz RC 振荡器)。但 Bootloader 要搬代码、初始化 DDR,这点频率根本不够用。所以我们需要切换到外部晶振 + PLL 倍频,把主频拉到几百 MHz 甚至 GHz。
时钟初始化的典型流程:
- 等待外部晶振稳定——晶振起振需要时间,通常几百微秒到几毫秒。不能一上电就切,否则时钟源不稳定,系统直接乱掉。
- 配置 PLL 倍频系数——根据目标频率计算分频比、倍频比、后分频。这里最容易踩坑:PLL 输出频率不能超过芯片规格,否则芯片会过热甚至烧毁。
- 切换系统时钟源——从内部振荡器切换到 PLL 输出。切换前要确认 PLL 已经锁定(Lock 标志位置 1)。
- 配置外设时钟分频——AHB、APB1、APB2 等总线时钟需要单独设置。外设跑太快会挂,跑太慢浪费性能。
| 时钟源 | 典型频率 | 用途 | 注意事项 |
|---|---|---|---|
| 内部 RC 振荡器 | 8~16 MHz | 上电默认、低功耗模式 | 精度差(±1%~±3%),不适合高速通信 |
| 外部晶振 | 4~50 MHz | PLL 参考源 | 需要匹配负载电容,起振时间较长 |
| PLL 输出 | 几百 MHz ~ GHz | CPU 核心、DDR 控制器 | 必须等待 Lock 标志,防止频率跳变 |
/* STM32F4 时钟初始化示例(简化) */
void SystemClock_Config(void) {
/* 1. 使能 HSE 外部晶振 */
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY)); /* 等待稳定 */
/* 2. 配置 PLL:HSE 8MHz -> PLL 168MHz */
RCC->PLLCFGR = (8 << 0) | /* PLLM = 8 分频 */
(336 << 6) | /* PLLN = 336 倍频 */
(2 << 16) | /* PLLP = 4 分频 -> 168MHz */
RCC_PLLCFGR_PLLSRC_HSE;
/* 3. 使能 PLL,等待锁定 */
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY));
/* 4. 切换系统时钟到 PLL */
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
避坑指南:我曾经在一个项目里,PLL 配置完直接切时钟,没等 Lock 标志。结果芯片跑在未锁定的 PLL 上,频率忽高忽低,UART 通信全是乱码。后来加了等待循环,问题消失。记住:PLL 锁定时间通常需要几百个参考时钟周期,别省这一步。
2.3 内存控制器配置:让 CPU 能访问“大仓库”
Bootloader 通常运行在内部 SRAM 里,但内部 SRAM 很小(几十 KB 到几 MB)。要加载更大的程序(比如 Linux 内核),必须初始化外部内存控制器——DDR Controller 或 SDRAM Controller。
内存控制器配置是硬件初始化里最复杂的一环。为什么?因为 DDR 颗粒的时序参数非常多:CAS 延迟、RAS 到 CAS 延迟、预充电时间、刷新周期……任何一个参数设错,内存读写就会出错,而且这种错误非常隐蔽——可能跑 99 次都对,第 100 次突然挂掉。
配置内存控制器的核心步骤:
- 设置时序参数——根据 DDR 颗粒的 datasheet,填入 tRCD、tCL、tRP、tRAS 等参数。这些参数通常以时钟周期为单位。
- 配置地址映射——行列地址宽度、Bank 数量、芯片使能信号。错了会导致内存地址错位。
- 校准 ZQ——DDR3/DDR4 需要阻抗校准,确保信号完整性。
- 执行初始化序列——NOP、预充电、自动刷新、设置模式寄存器。这个序列必须严格按 JEDEC 标准来。
- 验证内存可用性——写一个已知模式(如 0xAA55AA55),读回来对比。我习惯做三次不同地址的读写测试。
/* DDR 控制器初始化伪代码 */
void DDR_Init(void) {
/* 1. 配置时序寄存器 */
DDR_TIMING->tRCD = 5; /* RAS-to-CAS 延迟 */
DDR_TIMING->tCL = 7; /* CAS 延迟 */
DDR_TIMING->tRP = 5; /* 预充电时间 */
DDR_TIMING->tRAS = 20; /* 行激活时间 */
/* 2. 配置地址映射 */
DDR_MAP->ROW_BITS = 15; /* 行地址宽度 */
DDR_MAP->COL_BITS = 10; /* 列地址宽度 */
DDR_MAP->BANK_NUM = 8; /* Bank 数量 */
/* 3. 执行初始化序列 */
DDR_CMD->NOP(); /* 空操作 */
DDR_CMD->PRECHARGE(); /* 预充电所有 Bank */
DDR_CMD->AUTO_REFRESH();/* 自动刷新 */
DDR_CMD->SET_MR(); /* 设置模式寄存器 */
/* 4. 等待初始化完成 */
while (!(DDR_STS->INIT_DONE));
/* 5. 简单读写验证 */
*((volatile uint32_t*)0x80000000) = 0xDEADBEEF;
if (*((volatile uint32_t*)0x80000000) != 0xDEADBEEF) {
while(1); /* 内存故障,死循环 */
}
}
个人经验:DDR 初始化完成后,我习惯做一个“走步测试”——往整个 DDR 地址空间写递增模式,再读回来校验。这个测试能暴露地址线短路、时序不稳等问题。有一次我发现高 4MB 区域读写总出错,最后查出是 PCB 上地址线 A12 虚焊。如果没有这个测试,后面加载内核时可能随机崩溃,查都查不到。
2.4 堆栈指针设置:给 C 代码一个“落脚点”
堆栈指针(SP)是 C 语言运行的基础。局部变量、函数调用、中断现场保存,全依赖堆栈。在汇编启动代码里,我们必须手动设置 SP 寄存器,指向一块可读写的内存区域。
设置堆栈指针有两个关键点:
- 堆栈地址必须对齐——ARM 要求 8 字节对齐,x86 通常 4 字节对齐。不对齐会导致访问异常。
- 堆栈大小要预留足够——Bootloader 的堆栈通常不需要太大(几 KB 足够),但如果你要调用复杂的 C 函数(比如文件系统解析),建议至少 4KB。
对于多模式处理器(如 ARM Cortex-A),每个异常模式都有独立的堆栈指针:
| CPU 模式 | 堆栈指针寄存器 | 典型堆栈大小 | 用途 |
|---|---|---|---|
| SVC (Supervisor) | R13_svc | 1~4 KB | 主程序、系统调用 |
| IRQ | R13_irq | 256~512 B | 外部中断处理 |
| FIQ | R13_fiq | 128~256 B | 快速中断处理 |
| ABT (Abort) | R13_abt | 128 B | 数据/指令异常 |
| UND (Undefined) | R13_und | 128 B | 未定义指令异常 |
@ 设置各模式堆栈指针(ARM Cortex-A)
/* 先进入各模式,设置 SP,再切回 SVC */
MSR cpsr, #0xD3 @ SVC Mode, 关中断
LDR sp, =_svc_stack_top
MSR cpsr, #0xD2 @ IRQ Mode
LDR sp, =_irq_stack_top
MSR cpsr, #0xD1 @ FIQ Mode
LDR sp, =_fiq_stack_top
MSR cpsr, #0xD7 @ ABT Mode
LDR sp, =_abt_stack_top
MSR cpsr, #0xDB @ UND Mode
LDR sp, =_und_stack_top
/* 最后切回 SVC Mode,准备跳转到 main */
MSR cpsr, #0x13
注意:堆栈是向下生长的(满递减堆栈)。所以堆栈顶地址要设在内存区域的最高地址。比如你分配了 4KB 堆栈区,起始地址是 0x20000000,那么 SP 应该设为 0x20001000。设反了,堆栈会覆盖代码区,程序跑着跑着就飞了。
2.5 本章知识体系总览
下面这张图把四个初始化步骤的依赖关系串起来了。你可以看到,每一步都为下一步做准备,最终目标就是构建一个能运行 C 代码的稳定环境。
嗯,到这里,硬件初始化的四个核心步骤就讲完了。你可能会问:为什么顺序是固定的?因为每一步都依赖上一步的结果。CPU 模式不对,你连时钟寄存器都访问不了;时钟没配好,DDR 控制器根本跑不起来;内存没初始化,堆栈设在哪?——所以这个顺序是硬约束,不是习惯问题。
总结一句话:硬件初始化的本质,就是给芯片一个确定的初始状态。CPU 模式、时钟、内存、堆栈——这四个要素缺一不可。任何一个环节出问题,Bootloader 都走不到下一步。我个人建议,每次移植 Bootloader 到新板子时,先单独验证这四个步骤,再往下走。别问我怎么知道的……