4、硬件初始化阶段:时钟与PLL初始化、DDR内存控制器初始化、关键外设时钟使能
好,咱们接着往下聊。上一节我们把BootROM的启动流程捋了一遍,这一节要深入进去,看看硬件初始化阶段到底在干什么。
说白了,芯片上电后第一件事就是“活过来”。但怎么活?得有心跳,得有记忆,还得能跟外界打招呼。这个阶段,就是给芯片装上心脏、大脑和手脚。
4.1 时钟与PLL初始化:给芯片装上心脏
芯片上电后,内部振荡器会先起振,产生一个很慢的参考时钟。这个时钟通常只有几兆赫兹,比如24MHz或32.768kHz。你想想看,这么慢的速度,连个LED都点不利索,更别说跑操作系统了。
所以,我们需要PLL(锁相环)。PLL的作用,就是把慢时钟倍频上去。比如从24MHz倍频到1.2GHz,甚至更高。
我个人习惯,在调试PLL时,会先确认参考时钟是否稳定。我曾经遇到过一块板子,PLL死活锁不住,查了半天,结果是晶振的负载电容焊错了。嗯,这种坑,踩过一次就忘不了。
PLL初始化核心步骤:
- 等待参考时钟稳定(通常需要几百微秒)
- 配置PLL的倍频系数(M)、分频系数(N)和输出分频(O)
- 启动PLL,等待锁定信号(Lock信号)
- 切换系统时钟源到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 知识体系总览
为了让你更直观地理解这个阶段,我画了一张流程图:
嗯,这个阶段讲完了。你可能会问,为什么要把时钟、DDR和外设分开讲?其实在实际的BootROM代码里,它们是交织在一起的。比如,DDR初始化需要用到定时器来做超时判断,而定时器又依赖时钟。所以,写代码时要注意依赖关系。
我个人习惯,是先让时钟跑起来,然后使能最基本的定时器和UART,最后再搞DDR。这样,万一DDR初始化失败了,还能通过UART打印点调试信息出来。否则,死都不知道怎么死的。
好了,这一节就到这里。记住,硬件初始化是BootROM的基石,这块搞不定,后面全是空中楼阁。