4、硬件初始化实战:时钟系统初始化、DDR内存控制器配置、存储控制器初始化、中断控制器初始化
好,咱们进入正题。BootROM 启动回滚保护,硬件初始化是第一步,也是最容易翻车的一步。我见过不少团队,软件写得飞起,结果硬件初始化没做好,板子直接变砖。说白了,这四块——时钟、DDR、存储、中断——任何一个没伺候好,后面的代码根本跑不起来。
4.1 时钟系统初始化
时钟是芯片的心脏。你想想看,心脏跳得不准,全身都乱套。我个人习惯,上来先看芯片的参考手册,找到时钟树那一章。别嫌麻烦,这一步省不了。
时钟初始化的核心逻辑其实就三件事:
- 选源:用内部振荡器还是外部晶振?
- 分频/倍频:PLL 怎么配?输出频率对不对?
- 使能:给各个外设的时钟门控打开。
我在项目中遇到过最坑的事,就是 PLL 锁定时间没给够。代码里配完寄存器,立马就去读锁定标志,结果读回来是 0。嗯,后来我学乖了,配完 PLL 至少等 200 微秒,或者直接轮询超时处理。
避坑指南:我曾经因为时钟源切换顺序搞反,导致芯片直接死锁。正确的做法是:先配好目标时钟源,再切换,最后关掉旧时钟源。千万别先关旧的,再开新的——中间那一段空白期,芯片就“断气”了。
下面给个伪代码示例,展示时钟初始化的骨架:
void clock_init(void) {
// 1. 等待外部晶振稳定
while(!(OSC_STATUS & OSC_READY));
// 2. 配置 PLL
PLL_CFG = (MUL_VAL << 8) | (DIV_VAL << 0);
PLL_EN = 1;
// 3. 等待 PLL 锁定
uint32_t timeout = 10000;
while(!(PLL_STATUS & PLL_LOCKED)) {
if(--timeout == 0) {
// 超时处理,回退到内部时钟
fallback_to_internal_osc();
return;
}
}
// 4. 切换系统时钟到 PLL 输出
CLK_SEL = CLK_SRC_PLL;
// 5. 使能外设时钟
PERIPH_CLK_EN |= (TIMER_CLK | UART_CLK | DDR_CLK);
}
小技巧:调试时钟时,可以先把某个 GPIO 脚配成时钟输出模式,用示波器看实际频率。别光信寄存器里的值,硬件有时候会跟你开玩笑。
4.2 DDR 内存控制器配置
DDR 初始化是 BootROM 里最复杂的一块。为什么?因为 DDR 的时序参数跟 PCB 布线、颗粒型号、温度都有关。你想想看,一个参数配错了,系统可能跑几分钟才崩,排查起来非常痛苦。
我建议把 DDR 初始化分成三步走:
- 控制器复位与时钟使能:先把 DDR 控制器从复位状态释放,给上时钟。
- 时序参数配置:根据 DDR 颗粒的 datasheet,填好 tRCD、tRP、tRAS 这些参数。
- ZQ 校准与训练:这一步很多人忽略,但实际很重要。尤其是高速 DDR,不做训练的话,信号质量可能很差。
我记得有一次,板子跑 Linux 总是随机死机。查了三天,最后发现是 DDR 的 ODT(片上端接)配置跟 PCB 走线阻抗不匹配。改了一个寄存器值,问题就解决了。嗯,从那以后,我每次配 DDR 都会仔细核对 PCB 的阻抗设计。
警告:DDR 初始化过程中,千万不要访问 DDR 地址空间!在控制器完成初始化之前,DDR 还处于“未就绪”状态,任何读写操作都会导致总线挂死。我见过有人把代码段放在 DDR 里,结果 BootROM 一跑就死——这就是典型的“先有鸡还是先有蛋”的问题。
下面是一个简化的 DDR 配置流程:
void ddr_init(void) {
// 1. 复位 DDR 控制器
DDR_RESET = 0;
delay_us(100);
DDR_RESET = 1;
// 2. 配置时序参数(以 DDR3-1600 为例)
DDR_TIMING->tRCD = 11; // 11 cycles
DDR_TIMING->tRP = 11; // 11 cycles
DDR_TIMING->tRAS = 28; // 28 cycles
DDR_TIMING->tRFC = 160; // 160 cycles
// 3. 配置内存大小与行列地址
DDR_CFG->ROW_BITS = 15; // 32K rows
DDR_CFG->COL_BITS = 10; // 1K columns
DDR_CFG->BANK_BITS = 3; // 8 banks
// 4. 启动 ZQ 校准
DDR_ZQCTRL = ZQ_CALIB_START;
while(!(DDR_STATUS & ZQ_DONE));
// 5. 等待 DDR 就绪
DDR_CMD = CMD_PRECHARGE_ALL;
DDR_CMD = CMD_REFRESH;
DDR_CMD = CMD_MRS | (BURST_LEN_8 << 0);
}
4.3 存储控制器初始化
存储控制器,说白了就是 BootROM 用来读代码的接口。常见的有 SPI、NAND、eMMC。不同的存储介质,初始化流程差别很大。
我个人习惯,先看 BootROM 支持哪几种启动介质,然后根据硬件设计选一种。如果支持回滚保护,通常需要从两个不同的物理区域读备份镜像。
以 SPI NOR Flash 为例,初始化流程如下:
- 配置 SPI 控制器的工作模式(模式 0/模式 3)
- 设置时钟分频,确保 SPI 时钟不超过 Flash 支持的最大频率
- 发送读 ID 命令,验证 Flash 是否在线
- 如果是 QSPI 模式,还需要切换成四线模式
避坑指南:我曾经在 SPI 初始化时,忘了等 Flash 退出“上电等待”状态。结果读 ID 总是返回 0xFF。后来加了 10ms 延时,问题就解决了。嗯,Flash 上电后需要一点时间才能响应命令,这个在 datasheet 里叫 tVSL。
NAND Flash 的初始化更麻烦,因为坏块管理是绕不开的。BootROM 里通常只做最基本的初始化,坏块检查留给下一级 bootloader。但如果你要做回滚保护,至少得保证能正确读取两个备份区域的起始块。
4.4 中断控制器初始化
中断控制器,在 BootROM 阶段其实用得不多。但为什么还要初始化?因为 BootROM 里可能会用到定时器中断来做超时处理,或者用外部中断来检测按键进入恢复模式。
中断初始化的要点:
- 关总中断:初始化期间,CPU 的中断使能位必须清零。防止意外中断打乱初始化流程。
- 配置中断向量表:把中断向量表放到 SRAM 里,并告诉 CPU 它的基地址。
- 使能具体中断源:比如定时器中断、UART 中断。
- 设置优先级:如果支持嵌套中断,优先级要配好。
我记得有一次,BootROM 里开了定时器中断,但忘了清中断标志位。结果一使能中断,CPU 立刻跳进中断服务函数,然后发现中断源是假的——死循环了。嗯,从那以后,我每次使能中断前,都会先清一次 pending 位。
void irq_init(void) {
// 1. 关总中断
__disable_irq();
// 2. 设置向量表基地址
SCB->VTOR = (uint32_t)&_vectors_start;
// 3. 清所有 pending 中断
for(int i = 0; i < NUM_IRQS; i++) {
NVIC_ClearPendingIRQ(i);
}
// 4. 使能特定中断
NVIC_EnableIRQ(TIMER0_IRQn);
NVIC_EnableIRQ(UART0_IRQn);
// 5. 开总中断
__enable_irq();
}
小技巧:在 BootROM 里,中断服务函数尽量短。最好只设一个标志位,主循环里轮询处理。为什么?因为 BootROM 的栈空间通常很小,中断嵌套容易栈溢出。
4.5 本章小结
硬件初始化,说白了就是给芯片“铺路”。时钟铺好了,CPU 才能跑;DDR 铺好了,代码才有地方放;存储铺好了,才能读镜像;中断铺好了,才能处理异常。每一步都环环相扣。
我个人经验是,写初始化代码时,多留一些调试手段。比如在关键步骤后打印一个字符,或者点亮一个 LED。这样出了问题,你能快速定位到是哪一步卡住了。别等到板子变砖了才后悔——嗯,我吃过这个亏。