4、硬件初始化(二):时钟系统初始化(PLL、分频器)、内存控制器初始化(SDRAM/DDR)
上一节我们把中断向量表、栈指针和关键外设模块的时钟门控都搞定了。这时候芯片其实还处在一种「低速安全模式」里——内部振荡器或者外部低速晶振在工作,但主频可能只有几十兆赫兹。
嗯,这显然不够用。你要知道,Bootloader 的核心任务之一就是把 DDR 初始化好,然后把应用程序从 Flash 搬到内存里跑。而 DDR 对时钟的稳定性要求极高,频率不够或者抖动太大,内存控制器根本没法正常工作。
所以这一节,我们得把两件大事办了:时钟系统初始化和内存控制器初始化。这两步是 Bootloader 里最依赖硬件手册的环节,也是我当年踩坑最多的部分。
4.1 时钟系统初始化
现代 SoC 的时钟树非常复杂。你想想看,CPU 核心、总线、外设、内存控制器,各自需要不同的频率。而且这些频率之间还有相位关系,不是随便设几个寄存器就能跑的。
我个人的习惯是:先看时钟树框图,再动手写代码。别一上来就翻寄存器手册,那样容易迷失。
4.1.1 时钟源的选择
大多数嵌入式 SoC 提供至少两种时钟源:
- 内部振荡器(IRC):上电默认启用,精度一般,温漂大,适合安全启动阶段。
- 外部晶振(OSC):精度高,稳定性好,但需要起振时间。
Bootloader 里,我通常会在第一阶段使用内部振荡器跑低速代码,等外部晶振稳定后再切换过去。为什么?因为外部晶振起振需要几百微秒到几毫秒,如果一上电就等它,那启动速度就慢了。
4.1.2 PLL 配置
PLL(锁相环)是时钟系统的核心。它把低频输入倍频到高频输出。公式很简单:
Fout = Fin × (N / M) / K
其中:
- Fin:输入频率(外部晶振或内部振荡器)
- M:预分频系数(分频器)
- N:倍频系数(反馈分频器)
- K:后分频系数(输出分频器)
配置 PLL 时,有几个关键约束你必须遵守:
| 参数 | 典型范围 | 注意事项 |
|---|---|---|
| Fin / M | 1 MHz ~ 10 MHz | 鉴相器输入频率,太高或太低都会导致 PLL 失锁 |
| VCO 频率 | 100 MHz ~ 2 GHz | 由 Fin / M × N 决定,超出范围会损坏 PLL |
| Fout | 取决于芯片规格 | 不能超过芯片最高工作频率 |
配置步骤一般是:
- 将 PLL 置于旁路模式(Bypass),让时钟直接走输入路径
- 写 M、N、K 分频系数
- 等待 PLL 锁定(Lock)——通常轮询状态寄存器或等待固定延时
- 切换到 PLL 输出
4.1.3 分频器与时钟树
PLL 输出之后,还要经过一系列分频器,才能送到各个模块。典型的时钟树结构如下:
配置分频器时,我建议你注意两点:
- 分频系数必须是整数,有些芯片支持小数分频,但会引入抖动,DDR 时钟不建议用。
- 总线频率不能超过外设频率,否则外设会丢数据。这个我在调试 SD 卡驱动时遇到过,总线跑 150 MHz,SDIO 外设只能跑 50 MHz,忘了配分频,结果数据全是错的。
4.2 内存控制器初始化
时钟搞定之后,下一步就是初始化内存控制器。这一步是 Bootloader 里最「硬」的部分——配置错了,板子直接黑屏,连调试信息都打印不出来。
我个人的经验是:先把内存芯片的数据手册和 SoC 的内存控制器手册通读一遍,把时序参数列成表格,再动手写代码。
4.2.1 SDRAM 与 DDR 的核心差异
| 特性 | SDRAM | DDR |
|---|---|---|
| 数据采样 | 时钟上升沿 | 上升沿和下降沿(双倍速率) |
| 参考电压 | 不需要 | 需要 Vref(通常为 VDDQ/2) |
| DQS 信号 | 无 | 有,用于数据选通 |
| ODT(片上端接) | 无 | 有,用于信号完整性 |
| 初始化流程 | 简单 | 复杂(需 ZQ 校准、DLL 锁定等) |
说白了,DDR 比 SDRAM 多了很多「校准」步骤。这些步骤是为了补偿 PCB 走线延迟和芯片工艺偏差。你想想看,DDR3 的数据速率动辄 800 MHz 以上,信号在 PCB 上走 1 英寸就有约 150 ps 的延迟,不校准根本没法用。
4.2.2 DDR 初始化流程
以 DDR3 为例,标准初始化流程如下:
- 上电等待:VDD 和 VDDQ 稳定后,等待至少 200 μs
- CKE 拉低:时钟使能信号保持低电平
- 时钟稳定:提供稳定的差分时钟
- CKE 拉高:等待至少 500 ns
- 发送 NOP 命令:保持 400 ns
- 发送 MRS 命令:配置模式寄存器(突发长度、CAS 延迟等)
- 发送 ZQCL 命令:ZQ 校准,等待约 512 个时钟周期
- DLL 锁定:等待 DLL 锁定完成
- 发送 OCD 校准(可选):输出驱动校准
- 进入正常工作模式
4.2.3 内存控制器寄存器配置
SoC 的内存控制器通常包含以下几类寄存器:
- 时序寄存器:tRCD、tRP、tRAS、tWR 等,这些值来自内存芯片的数据手册
- 配置寄存器:数据位宽(8/16/32 bit)、Bank 数量、行/列地址宽度
- 校准寄存器:DQS 延迟、ODT 阻抗、驱动强度
- 控制寄存器:启动初始化序列、软件复位
配置示例(伪代码):
// 1. 配置时序参数
DDR_TIMING->tRCD = 3; // 行地址到列地址延迟
DDR_TIMING->tRP = 3; // 行预充电时间
DDR_TIMING->tRAS = 8; // 行激活时间
DDR_TIMING->tWR = 4; // 写恢复时间
// 2. 配置内存几何参数
DDR_CFG->DATA_WIDTH = 2; // 32-bit (0=8, 1=16, 2=32)
DDR_CFG->BANK_CNT = 3; // 8 banks (0=2, 1=4, 2=8)
DDR_CFG->ROW_ADDR = 15;// 15位行地址
DDR_CFG->COL_ADDR = 10;// 10位列地址
// 3. 启动初始化序列
DDR_CTRL->INIT_EN = 1;
while (!(DDR_CTRL->STATUS & INIT_DONE)); // 等待完成
// 4. 校准
DDR_CALIB->DQS_SLAVE_DELAY = 0x20; // 调整DQS延迟
DDR_CALIB->ODT_IMPEDANCE = 0x2; // 40Ω ODT
4.2.4 内存测试
初始化完成后,一定要做内存测试。我常用的测试方法:
- 走马灯测试:写 0xAA、0x55、0x00、0xFF 等模式,验证每个 bit 都能正确翻转
- 地址线测试:写地址到数据,读回来验证,检查地址线是否有短路或断路
- 压力测试:全地址范围写随机数,再读回来比对
测试代码片段:
// 地址线测试:检查地址线是否连通
uint32_t *mem_base = (uint32_t *)DDR_BASE;
uint32_t test_pattern = 0xDEADBEEF;
for (int i = 0; i < 32; i++) {
uint32_t *addr = mem_base + (1 << i);
*addr = test_pattern;
}
for (int i = 0; i < 32; i++) {
uint32_t *addr = mem_base + (1 << i);
if (*addr != test_pattern) {
// 地址线 i 有问题
error_handler(i);
}
}
嗯,这一步不能省。我见过太多「DDR 初始化成功但跑应用就挂」的案例,最后发现是某根地址线虚焊。内存测试能帮你把这类问题提前暴露出来。
4.3 小结
时钟系统和内存控制器是 Bootloader 硬件初始化的两大基石。时钟配错了,整个系统跑不起来;内存配错了,代码根本没法运行。我个人建议你在写代码之前,先把时钟树和内存时序图画出来,把每个参数的含义和取值范围搞清楚。
另外,调试这两个模块时,示波器和逻辑分析仪是你的好朋友。我曾经用示波器抓 PLL 锁定波形,发现锁定时间比预期长了 50%,最后查出是电源纹波太大。这种问题,光看代码是看不出来的。