第四章 BootROM第一阶段:基础硬件初始化
芯片上电后,第一行代码从哪里开始?
这个问题我当年刚入行时也琢磨了很久。说白了,BootROM就是固化在芯片内部的一小块只读存储器,CPU复位后第一条指令就从这里取。它要做的事情很纯粹——把最基本的硬件环境搭起来,然后把接力棒交给下一棒。
4.1 复位向量与BootROM入口
ARM Cortex-A系列处理器复位后,从0x00000000或0xFFFF0000取第一条指令。具体地址由硬件配置引脚决定。我见过不少新手工程师,调试时发现程序跑飞了,最后查出来是复位向量地址配错了。
关键事实:BootROM是芯片出厂时烧录的,用户无法修改。它的大小通常只有几十KB到几百KB。以i.MX6为例,BootROM只有96KB,却要完成时钟、DDR、存储设备检测等一系列工作。
BootROM的执行流程大致如下:
- 设置异常向量表
- 关闭中断(CPU默认中断使能位不确定)
- 初始化堆栈指针(SP)
- 跳转到C语言入口函数
; 典型的BootROM启动汇编片段
reset:
mrs r0, cpsr
bic r0, r0, #0x1F ; 清除模式位
orr r0, r0, #0xD3 ; 进入SVC模式,关IRQ/FIQ
msr cpsr, r0
ldr sp, =_stack_end ; 设置SVC模式堆栈
bl main ; 跳转到C代码
嗯,这里要注意——堆栈指针必须在进入C函数前设置好,否则局部变量和函数调用都会出问题。我曾经在一个项目中,因为堆栈地址算错了8个字节,导致DDR参数校验总是不通过,查了两天才找到原因。
4.2 时钟与PLL初始化
芯片刚上电时,系统时钟通常来自一个低速的内部振荡器,频率可能只有12MHz甚至更低。BootROM要做的第一件事,就是把PLL(锁相环)配置好,把CPU频率拉到正常工作范围。
为什么不能直接用低速时钟?你想想看,如果CPU跑在12MHz,初始化几百MB的DDR要等到什么时候?
4.2.1 PLL配置流程
以典型的ARM SoC为例,PLL初始化分三步:
- 选择参考时钟源:外部晶振还是内部RC振荡器
- 配置倍频系数:通过寄存器设置M、N、OD参数
- 等待PLL锁定:轮询锁定标志位,确认输出稳定
// PLL配置伪代码
void pll_init(void) {
// 1. 选择参考时钟
writel(CLK_SRC_SEL, OSC_24M, &clk->src);
// 2. 设置倍频参数:Fout = Fin * M / (N * OD)
writel(PLL_MDIV_100 | PLL_NDIV_3 | PLL_ODIV_2, &pll->con);
// 3. 使能PLL
set_bit(PLL_ENABLE, &pll->con);
// 4. 等待锁定,超时则报错
while (!(readl(&pll->stat) & PLL_LOCKED)) {
if (timeout++ > TIMEOUT_MAX) {
panic("PLL lock failed!");
}
}
}
注意:PLL锁定时间因芯片而异,一般在几十到几百微秒。不要用死等,一定要加超时机制。我遇到过一块开发板,晶振虚焊导致PLL永远锁不定,没有超时的话系统就卡死了。
4.2.2 时钟树与分频配置
PLL输出的是高频时钟,但芯片内部不同模块需要不同频率。CPU核可能需要1GHz,而外设总线只能跑100MHz。这就需要时钟树来分频。
| 模块 | 典型频率 | 时钟源 |
|---|---|---|
| CPU核心 | 800MHz - 2GHz | PLL0直出或分频 |
| DDR控制器 | 400MHz - 1600MHz | PLL1分频 |
| AXI总线 | 200MHz - 500MHz | PLL0二分频 |
| APB外设总线 | 50MHz - 100MHz | PLL2多级分频 |
我个人习惯是先配PLL,再配分频器,最后切换时钟源。顺序搞反了的话,芯片可能会因为时钟瞬态跳变而崩溃。
4.3 DDR控制器初始化
DDR初始化是BootROM里最复杂、最容易出错的环节。为什么?因为DDR参数和硬件布线、颗粒型号、PCB层叠都有关,BootROM必须用一套通用的流程去适配各种硬件。
4.3.1 DDR初始化流程
标准的DDR初始化步骤(以DDR3/DDR4为例):
- 复位DDR颗粒:拉低复位引脚,保持至少200μs
- 配置控制器寄存器:设置时序参数(CL、CWL、tRCD等)
- 发送ZQ校准命令:DDR3/DDR4需要ZQ校准
- 等待DDR就绪:轮询控制器状态寄存器
- 执行写电平校准:调整DQS与时钟的相位关系
- 执行读DQS门控校准:找到有效的读数据窗口
// DDR初始化核心代码片段
void ddr_init(void) {
// 1. 复位DDR颗粒
writel(DDR_RSTN_ASSERT, &ddr->reset);
udelay(500); // 保持复位至少500μs
// 2. 释放复位,进入CKE低电平状态
writel(DDR_RSTN_DEASSERT, &ddr->reset);
udelay(10);
// 3. 配置时序参数
writel(TCL_11 | TCWL_8 | TRCD_13 | TRP_13, &ddr->timing);
writel(TREFI_7800 | TRFC_350, &ddr->refresh);
// 4. 使能DDR控制器
set_bit(DDR_ENABLE, &ddr->config);
// 5. 等待初始化完成
while (!(readl(&ddr->status) & DDR_READY)) {
if (timeout++ > TIMEOUT_MAX) {
panic("DDR init failed!");
}
}
}
经验之谈:DDR时序参数可以从芯片厂商提供的Excel计算工具里获取。但别完全相信默认值——我曾在某项目里发现,工具算出的tRFC值偏小,导致高温下DDR频繁报错。最后手动加了20%的余量才稳定。
4.3.2 DDR校准的重要性
DDR校准说白了就是找到信号传输的最佳时机。因为PCB走线长度不同、温度变化、电压波动,DQS和时钟之间的相位关系会漂移。不做校准的话,高速DDR根本没法正常工作。
校准主要分两类:
- 写电平校准:调整DQS相对于时钟的延迟,确保写入数据时DQS边沿在时钟正中间
- 读DQS门控校准:找到读DQS的有效窗口,避免采到无效数据
我记得有一次调试一块8层板的DDR,读数据总是不稳定。用示波器抓了DQS信号才发现,门控窗口偏移了将近1/4个时钟周期。重新校准后,问题就解决了。
4.4 本章知识体系
下面这张图把BootROM第一阶段的核心逻辑串起来了:
4.5 常见问题与避坑指南
做BootROM开发这些年,我踩过不少坑。挑几个典型的说说:
坑1:PLL锁定超时处理不当
我曾经在一个项目里,PLL锁定超时后直接执行了硬件复位。结果晶振起振慢,PLL反复锁定失败,系统陷入了复位循环。正确的做法是:超时后先检查时钟源是否稳定,再尝试重新配置PLL。
坑2:DDR时序参数过于激进
芯片数据手册给的时序参数通常是理想值。实际PCB上,信号完整性、串扰、温度漂移都会影响。我建议至少留10%-20%的时序余量。特别是tRFC和tREFI这两个参数,高温下影响很大。
调试技巧:如果DDR初始化失败,别急着改代码。先用示波器抓一下时钟、DQS、命令线的波形。很多时候,问题出在硬件上——比如时钟抖动过大、DQS信号质量差。软件调参只能掩盖问题,不能根治。
BootROM第一阶段的工作,说白了就是给芯片搭建一个能跑C语言的基础环境。时钟要稳,DDR要快,校准要准。这三件事做好了,后面的U-Boot才能顺利接手。