4. Boot ROM初始化(下):时钟与PLL初始化、内存控制器初始化(DDR/SRAM)、看门狗与定时器配置
上一节我们把CPU核心和异常向量表安排好了。芯片上电后,第一条指令执行了,栈也搭起来了。但这时候,芯片内部还是一片混沌——时钟没稳定,内存没法用,看门狗可能还在乱跳。
说白了,Boot ROM接下来的任务就是三件事:让芯片“按时”工作、让代码“有地方”运行、让系统“别乱复位”。这三件事做不好,后面的信任根建立就是空中楼阁。
核心逻辑:时钟是芯片的心跳,内存是代码的舞台,看门狗是系统的保安。Boot ROM必须在进入主程序前,把这三位“安排明白”。
4.1 时钟与PLL初始化:让芯片“按时”工作
芯片刚上电时,用的通常是内部低速振荡器(比如HSI)。这个时钟很慢,可能只有几兆赫兹,但胜在稳定、不需要外部晶振。Boot ROM的第一步,就是在这个“慢但可靠”的时钟下,把PLL(锁相环)配置好。
我个人习惯把PLL初始化分成三步走:
- 选择参考时钟源——用内部RC振荡器还是外部晶振?我建议优先用外部晶振,精度高。但有些低成本方案只能用内部RC,这时候要注意温漂。
- 配置倍频系数——比如参考时钟是8MHz,目标频率是400MHz,那倍频系数就是50。但别贪心,倍频太高PLL容易失锁。
- 等待PLL锁定——PLL需要一段时间来稳定输出。通常硬件会提供一个锁定标志位,轮询它就行。但一定要加超时处理。
经验之谈:我在一个IoT项目里遇到过,PLL锁定时间比数据手册长了整整3倍。后来发现是电源纹波太大。所以,PLL初始化前,我习惯先做一次短暂的延时(比如等待电源稳定),再开始配置PLL。这个“小动作”帮我避免了很多莫名其妙的启动失败。
代码实现上,大致是这样:
// 伪代码:PLL初始化流程
void pll_init(void) {
// 1. 选择时钟源:外部8MHz晶振
RCC->CFGR &= ~RCC_CFGR_SW_Msk;
RCC->CFGR |= RCC_CFGR_SW_HSE;
// 2. 等待HSE就绪
while (!(RCC->CR & RCC_CR_HSERDY_Msk)) {
if (timeout_expired()) {
// 降级到内部HSI
fallback_to_hsi();
return;
}
}
// 3. 配置PLL:8MHz * 50 = 400MHz
RCC->PLLCFGR = (50 << RCC_PLLCFGR_PLLN_Pos) |
(1 << RCC_PLLCFGR_PLLSRC_HSE_Pos);
// 4. 使能PLL并等待锁定
RCC->CR |= RCC_CR_PLLON_Msk;
while (!(RCC->CR & RCC_CR_PLLRDY_Msk)) {
if (timeout_expired()) {
// PLL锁定失败,使用HSI
fallback_to_hsi();
return;
}
}
// 5. 切换系统时钟到PLL输出
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL);
}
注意:系统时钟切换时,要确保目标时钟已经稳定。如果直接从低速时钟切到未锁定的PLL,芯片会直接“死机”。我见过一个团队因为没检查PLL锁定标志,导致芯片在低温下批量启动失败——PLL在低温下锁定时间变长,代码却以为它已经准备好了。
4.2 内存控制器初始化:让代码“有地方”运行
时钟搞定后,下一步就是让内存可用。Boot ROM通常运行在内部SRAM或Flash里,但真正的应用程序需要更大的运行空间——DDR或者外部SRAM。
内存控制器初始化,说白了就是告诉芯片:你的内存长什么样、多大、时序参数是多少。这一步做不好,程序跑起来就会随机崩溃。
我把它拆成四个关键步骤:
| 步骤 | 操作 | 常见坑点 |
|---|---|---|
| 1. 管脚复用配置 | 将GPIO管脚切换到内存控制器功能 | 管脚复用表看错,导致信号错位 |
| 2. 时序参数写入 | 配置CAS延迟、刷新周期、驱动强度等 | 时序太紧导致高温下不稳定 |
| 3. 内存校准 | 执行ZQ校准、读写训练 | 跳过校准会导致数据窗口偏移 |
| 4. 验证测试 | 写入特定模式并回读校验 | 只测一个地址,漏掉地址线问题 |
这里我想重点说说时序参数。DDR的时序参数非常多——CL、tRCD、tRP、tRAS……每个参数都对应着内存颗粒内部的具体行为。我建议直接从内存颗粒的数据手册里抄参数,别自己“优化”。
核心原则:内存初始化时,宁慢勿快。时序参数留10%~20%的余量,等系统稳定运行后再通过调优工具收紧。Boot ROM阶段的首要目标是“能跑起来”,不是“跑得最快”。
我曾经在一个项目里,为了追求启动速度,把DDR的刷新周期设到了极限值。结果在85℃老化测试时,内存频繁出现bit翻转。后来把刷新周期放宽了15%,问题就消失了。嗯,有些坑踩过一次就记住了。
对于内部SRAM,初始化相对简单——通常只需要使能时钟、配置等待状态即可。但要注意:有些芯片的SRAM在低功耗模式下会掉电,Boot ROM需要检查SRAM的内容是否有效。
// 伪代码:DDR初始化(简化版)
void ddr_init(void) {
// 1. 使能DDR控制器时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DDRCEN_Msk;
// 2. 配置管脚复用
gpio_config_ddr_pins();
// 3. 写入时序参数(来自内存颗粒数据手册)
DDRC->TIMING = (CL_VAL << DDRC_TIMING_CL_Pos) |
(tRCD_VAL << DDRC_TIMING_tRCD_Pos) |
(tRP_VAL << DDRC_TIMING_tRP_Pos);
// 4. 启动控制器
DDRC->CTRL |= DDRC_CTRL_EN_Msk;
// 5. 等待初始化完成
while (!(DDRC->STATUS & DDRC_STATUS_RDY_Msk));
// 6. 执行内存校验(写0xAA、0x55、0x00、0xFF)
if (!memory_test_pass()) {
// 内存初始化失败,进入安全模式
enter_safe_mode();
}
}
小技巧:内存校验时,我习惯用“走步0”和“走步1”模式——就是写一个0,其他全是1,然后逐个地址移动这个0。这种方法能有效检测地址线短路和信号串扰。比单纯写0xAA/0x55靠谱得多。
4.3 看门狗与定时器配置:让系统“别乱复位”
时钟和内存都准备好了,但还有一个“定时炸弹”需要处理——看门狗。
很多芯片上电后,独立看门狗(IWDG)是默认使能的。如果不及时喂狗,芯片会在几毫秒到几百毫秒内复位。你想想看,Boot ROM还在初始化内存呢,突然被看门狗复位了——这不就成死循环了吗?
所以,Boot ROM的早期阶段,我通常会做两件事:
- 先喂一次狗——在进入任何耗时操作前,确保看门狗计数器被重置。
- 重新配置看门狗超时时间——把默认的超时时间改长,给后续初始化留足时间。
但这里有个矛盾:看门狗的作用是防止程序跑飞。如果Boot ROM把看门狗超时设得太长,万一代码真的跑飞了,系统要很久才能恢复。我个人建议的做法是:
- 在Boot ROM早期,使用较短的超时时间(比如100ms)。
- 在关键操作(如PLL锁定、DDR训练)前,临时延长超时。
- 操作完成后,恢复较短的超时。
- 跳转到主程序前,把看门狗配置交给主程序管理。
避坑指南:我曾经在一个项目里,Boot ROM把看门狗完全禁用了,想着主程序会重新配置。结果主程序因为一个bug没使能看门狗,整个系统在无人值守的环境下跑了三个月,最后死机了都没人知道。所以,我的原则是:Boot ROM可以调整看门狗参数,但绝不能完全禁用看门狗功能。
至于定时器,Boot ROM主要用它来做两件事:
- 提供精确延时——比如等待PLL锁定、等待DDR训练完成。用定时器比用空循环靠谱得多。
- 作为超时计数器——防止某个初始化步骤卡死。
配置定时器时,我习惯用系统时钟直接驱动一个基本定时器,不经过任何预分频。这样定时精度最高,而且计算超时时间也简单——直接数时钟周期就行。
// 伪代码:使用定时器实现超时检测
void wait_with_timeout(uint32_t timeout_us) {
// 假设定时器时钟 = 系统时钟 = 400MHz
// 1us = 400个时钟周期
uint32_t target_count = timeout_us * 400;
TIM->CNT = 0;
TIM->CR1 |= TIM_CR1_CEN_Msk; // 启动定时器
while (TIM->CNT < target_count) {
// 等待,或者检查其他条件
if (some_condition_met()) {
break;
}
}
TIM->CR1 &= ~TIM_CR1_CEN_Msk; // 停止定时器
}
经验分享:定时器的计数值是32位的,在400MHz下,最大能计时约10.7秒。如果某个初始化步骤超过10秒还没完成,那大概率是出问题了。我一般会把超时时间设在实际预期时间的2~3倍,既不会误触发,又能及时发现问题。
4.4 本章小结
时钟、内存、看门狗——这三样东西是Boot ROM的“三驾马车”。时钟让芯片有节奏地工作,内存让代码有地方安家,看门狗确保系统不会在初始化过程中“暴毙”。
我个人觉得,这一阶段的代码质量直接决定了整个系统的可靠性。时钟配置错了,芯片可能跑在错误频率上;内存初始化漏了,程序可能随时崩溃;看门狗没处理好,系统可能反复复位。每一个细节都值得认真对待。