4、硬件初始化阶段:时钟与PLL初始化、DDR内存控制器初始化、关键外设时钟使能

好,咱们接着往下聊。上一节我们把BootROM的启动流程捋了一遍,这一节要深入进去,看看硬件初始化阶段到底在干什么。

说白了,芯片上电后第一件事就是“活过来”。但怎么活?得有心跳,得有记忆,还得能跟外界打招呼。这个阶段,就是给芯片装上心脏、大脑和手脚。

4.1 时钟与PLL初始化:给芯片装上心脏

芯片上电后,内部振荡器会先起振,产生一个很慢的参考时钟。这个时钟通常只有几兆赫兹,比如24MHz或32.768kHz。你想想看,这么慢的速度,连个LED都点不利索,更别说跑操作系统了。

所以,我们需要PLL(锁相环)。PLL的作用,就是把慢时钟倍频上去。比如从24MHz倍频到1.2GHz,甚至更高。

我个人习惯,在调试PLL时,会先确认参考时钟是否稳定。我曾经遇到过一块板子,PLL死活锁不住,查了半天,结果是晶振的负载电容焊错了。嗯,这种坑,踩过一次就忘不了。

PLL初始化核心步骤:

  1. 等待参考时钟稳定(通常需要几百微秒)
  2. 配置PLL的倍频系数(M)、分频系数(N)和输出分频(O)
  3. 启动PLL,等待锁定信号(Lock信号)
  4. 切换系统时钟源到PLL输出

代码层面,大概长这样:

// 伪代码示例:PLL初始化
void pll_init(void) {
    // 1. 等待外部晶振稳定
    while(!(OSC_STATUS & OSC_READY));
    
    // 2. 配置PLL参数
    // Fout = Fin * M / (N * O)
    PLL_M = 50;   // 倍频系数
    PLL_N = 1;    // 分频系数
    PLL_O = 2;    // 输出分频
    
    // 3. 启动PLL
    PLL_CTRL |= PLL_ENABLE;
    
    // 4. 等待锁定
    while(!(PLL_STATUS & PLL_LOCKED));
    
    // 5. 切换时钟源
    CLK_SWITCH = CLK_SRC_PLL;
}

避坑指南:我曾经在调试一款国产芯片时,发现PLL锁定后直接切换时钟源,系统就挂了。后来查手册才发现,切换时钟源时需要先切换到中间时钟(比如内部RC振荡器),再切到PLL输出。这叫“无毛刺切换”,很多芯片都有这个要求。

4.2 DDR内存控制器初始化:给芯片装上大脑

时钟搞定后,接下来就是DDR了。DDR是芯片的“短期记忆”,没有它,代码跑不起来。

DDR初始化,是BootROM里最复杂、最敏感的部分。为什么?因为DDR的时序参数非常多,而且跟PCB布局、走线长度、颗粒型号都有关。一个参数设错了,系统可能直接死机。

我记得有一次,客户反馈说他们的板子偶尔启动失败。我远程调试了一整天,最后发现是DDR的ODT(片上端接)配置不对。因为他们的PCB走线比较长,需要更大的端接电阻来抑制信号反射。

DDR初始化流程,大致如下:

步骤 操作 说明
1 DDR控制器复位 将控制器置于已知状态
2 配置时序参数 如CL、tRCD、tRP、tRAS等
3 配置DDR颗粒模式寄存器 通过MR0~MR3设置工作模式
4 ZQ校准 校准输出驱动强度
5 执行DDR训练 读写训练,找到最佳采样点

其中,DDR训练是最关键的一步。说白了,就是让控制器和颗粒之间“对表”,找到数据采样窗口的中心点。

// 伪代码示例:DDR训练(简化版)
void ddr_training(void) {
    // 写DQS门控训练
    for(int delay = 0; delay < 256; delay++) {
        set_dqs_gate_delay(delay);
        if(write_dq_calibration() == PASS) {
            break;
        }
    }
    
    // 读DQS训练
    for(int delay = 0; delay < 256; delay++) {
        set_dqs_delay(delay);
        if(read_dq_calibration() == PASS) {
            break;
        }
    }
}

注意:DDR训练的结果,跟温度、电压都有关系。我建议在产品量产时,做一下全温范围的DDR训练验证。否则,冬天能启动,夏天可能就挂了。这不是开玩笑,我亲眼见过。

4.3 关键外设时钟使能:给芯片装上手脚

时钟有了,内存有了,接下来就是让外设“醒过来”。

外设时钟使能,听起来很简单,就是往寄存器里写个1。但这里有个坑:很多芯片的外设时钟使能,是有依赖关系的。比如,你要用UART,得先使能UART模块时钟,还得使能对应的GPIO时钟,甚至可能还需要使能总线桥时钟。

我建议,在写这部分代码时,先画一张时钟树图。把每个外设的时钟源、分频关系、使能条件都标清楚。这样写代码时,就不会漏掉某个环节。

常见的外设时钟使能顺序:

  • 调试接口:UART或JTAG,方便输出日志
  • 定时器:用于延时和超时判断
  • 中断控制器:使能全局中断
  • DMA控制器:用于大数据搬运
  • 存储控制器:如NAND、eMMC、SPI Flash
// 伪代码示例:外设时钟使能
void peripheral_clock_enable(void) {
    // 使能UART时钟
    CLK_ENABLE(UART0_CLK);
    CLK_ENABLE(GPIO_CLK);  // UART依赖GPIO
    
    // 使能定时器时钟
    CLK_ENABLE(TIMER0_CLK);
    
    // 使能中断控制器
    CLK_ENABLE(INTC_CLK);
    
    // 使能DMA
    CLK_ENABLE(DMA_CLK);
}

小技巧:我曾经在调试时,发现某个外设怎么都使能不了。后来用示波器量了一下时钟引脚,发现根本没波形。查了三天,最后发现是时钟树里有个“时钟门控”没打开。所以,我建议你在调试外设时钟时,先确认时钟树上每一级门控都打开了。

4.4 知识体系总览

为了让你更直观地理解这个阶段,我画了一张流程图:

硬件初始化阶段知识体系 时钟与PLL初始化 DDR内存控制器初始化 关键外设时钟使能 等待参考时钟稳定 配置倍频/分频系数 启动PLL并等待锁定 无毛刺切换时钟源 控制器复位与配置 配置时序参数 模式寄存器配置 ZQ校准与DDR训练 调试接口(UART/JTAG) 定时器与中断控制器 DMA控制器 存储控制器(NAND/eMMC) 核心目标:为操作系统和应用程序提供稳定的硬件运行环境

嗯,这个阶段讲完了。你可能会问,为什么要把时钟、DDR和外设分开讲?其实在实际的BootROM代码里,它们是交织在一起的。比如,DDR初始化需要用到定时器来做超时判断,而定时器又依赖时钟。所以,写代码时要注意依赖关系。

我个人习惯,是先让时钟跑起来,然后使能最基本的定时器和UART,最后再搞DDR。这样,万一DDR初始化失败了,还能通过UART打印点调试信息出来。否则,死都不知道怎么死的。

好了,这一节就到这里。记住,硬件初始化是BootROM的基石,这块搞不定,后面全是空中楼阁。

可可精品资料库