第四章 BootROM第一阶段:基础硬件初始化

芯片上电后,第一行代码从哪里开始?

这个问题我当年刚入行时也琢磨了很久。说白了,BootROM就是固化在芯片内部的一小块只读存储器,CPU复位后第一条指令就从这里取。它要做的事情很纯粹——把最基本的硬件环境搭起来,然后把接力棒交给下一棒。

4.1 复位向量与BootROM入口

ARM Cortex-A系列处理器复位后,从0x00000000或0xFFFF0000取第一条指令。具体地址由硬件配置引脚决定。我见过不少新手工程师,调试时发现程序跑飞了,最后查出来是复位向量地址配错了。

关键事实:BootROM是芯片出厂时烧录的,用户无法修改。它的大小通常只有几十KB到几百KB。以i.MX6为例,BootROM只有96KB,却要完成时钟、DDR、存储设备检测等一系列工作。

BootROM的执行流程大致如下:

  1. 设置异常向量表
  2. 关闭中断(CPU默认中断使能位不确定)
  3. 初始化堆栈指针(SP)
  4. 跳转到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为例):

  1. 复位DDR颗粒:拉低复位引脚,保持至少200μs
  2. 配置控制器寄存器:设置时序参数(CL、CWL、tRCD等)
  3. 发送ZQ校准命令:DDR3/DDR4需要ZQ校准
  4. 等待DDR就绪:轮询控制器状态寄存器
  5. 执行写电平校准:调整DQS与时钟的相位关系
  6. 执行读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第一阶段的核心逻辑串起来了:

BootROM第一阶段知识体系 BootROM第一阶段:基础硬件初始化 CPU复位入口 设置异常向量 & 堆栈 时钟源选择 PLL配置与锁定 时钟树分频 DDR控制器初始化 写电平校准 + 读DQS门控校准 BootROM完成,跳转U-Boot DDR校准是BootROM最关键的步骤 参数错误会导致系统完全无法启动

4.5 常见问题与避坑指南

做BootROM开发这些年,我踩过不少坑。挑几个典型的说说:

坑1:PLL锁定超时处理不当

我曾经在一个项目里,PLL锁定超时后直接执行了硬件复位。结果晶振起振慢,PLL反复锁定失败,系统陷入了复位循环。正确的做法是:超时后先检查时钟源是否稳定,再尝试重新配置PLL。

坑2:DDR时序参数过于激进

芯片数据手册给的时序参数通常是理想值。实际PCB上,信号完整性、串扰、温度漂移都会影响。我建议至少留10%-20%的时序余量。特别是tRFC和tREFI这两个参数,高温下影响很大。

调试技巧:如果DDR初始化失败,别急着改代码。先用示波器抓一下时钟、DQS、命令线的波形。很多时候,问题出在硬件上——比如时钟抖动过大、DQS信号质量差。软件调参只能掩盖问题,不能根治。

BootROM第一阶段的工作,说白了就是给芯片搭建一个能跑C语言的基础环境。时钟要稳,DDR要快,校准要准。这三件事做好了,后面的U-Boot才能顺利接手。