3、硬件初始化优化:时钟树配置策略、PLL锁定时间优化、内存控制器初始化加速、外设时钟门控
硬件初始化,说白了就是给芯片「通电、喂时钟、配内存」这三板斧。很多工程师觉得这步没啥技术含量,照着参考代码抄一遍就行。但我做了这么多年底层优化,可以负责任地告诉你:启动时间的大头,往往就耗在这看似简单的初始化里。
我见过一个项目,Bootloader 跑了 800 多毫秒,其中硬件初始化占了 600 毫秒。优化之后,整个启动时间压到了 120 毫秒以内。怎么做到的?就是下面这几个关键点。
3.1 时钟树配置策略:别让时钟成为瓶颈
时钟树配置,是硬件初始化的第一步,也是最容易踩坑的地方。很多芯片上电后默认跑在内部低速振荡器上,比如 8MHz 甚至 32kHz。你要做的第一件事,就是把它切换到高速外部晶振,然后配置 PLL 倍频到目标频率。
这里有个常见的误区:有人喜欢一上来就把所有时钟都配好,包括各个外设的分频、门控等等。其实没必要。我个人习惯是:先配核心时钟,让 CPU 跑起来,其他的后面再说。
核心原则:先让 CPU 跑快,再管别的。
具体策略是这样的:
- 第一步:切换时钟源到外部晶振。这一步要快,别磨蹭。
- 第二步:配置 PLL,直接倍频到目标频率。别搞什么中间频率过渡。
- 第三步:等待 PLL 锁定。锁定之后立刻切换系统时钟。
- 第四步:其他外设时钟,用到的时候再开。
你想想看,如果你在 8MHz 下把所有的分频器、门控都配好了,再切到 400MHz,那前面配置的时间不就白白浪费在低速上了吗?
3.2 PLL锁定时间优化:别傻等,做点有用的事
PLL 锁定是需要时间的。不同芯片的 PLL 锁定时间不一样,从几十微秒到几毫秒都有。很多人的做法是:配置完 PLL,然后一个 while(!PLL_LOCKED); 死等。
嗯,这里要注意:死等是最浪费时间的做法。为什么不能利用这段时间做点别的?
我在项目中遇到过这样的情况:PLL 锁定需要 200 微秒,而这段时间足够我初始化一部分 GPIO、配置中断向量表、甚至把栈指针重新设一下。所以我的建议是:
- 先配置 PLL,启动锁定过程。
- 不等待锁定,立刻去做其他不依赖高频时钟的初始化。
- 等那些事情做完了,再回来检查 PLL 是否锁定。
代码示例:
// 启动 PLL,但不等待
PLL->CR |= PLL_CR_PLLON;
// 立刻去做别的事情
GPIO_Init();
NVIC_Setup();
Stack_Reinit();
// 回来检查 PLL 是否锁定
while(!(PLL->SR & PLL_SR_LOCK));
// 切换系统时钟
Switch_SysClk(PLL_OUT);
这样就把等待时间「藏」到其他初始化里了。说白了,就是让 CPU 别闲着。
小技巧:如果 PLL 锁定时间特别长(比如某些芯片需要几毫秒),你甚至可以在这段时间里初始化一部分内存控制器。不过要注意,有些内存控制器需要高频时钟才能工作,得先确认好。
3.3 内存控制器初始化加速:DDR 配置的「快」与「稳」
内存控制器初始化,尤其是 DDR 的初始化,是 Bootloader 里最复杂也最耗时的部分之一。一个典型的 DDR 初始化流程包括:
- 配置控制器寄存器
- 发送 ZQ 校准命令
- 等待 DLL 锁定
- 执行读写训练
- 设置刷新周期
每一步都有等待时间。我见过有人把 DDR 初始化写成 200 多行代码,里面全是 delay_ms()。这其实是不对的。
我曾经踩过一个坑:某款芯片的 DDR 控制器要求 ZQ 校准后必须等待 512 个时钟周期。我当时直接写了个 delay_us(10),结果在低频下没问题,一超频就挂。后来才发现,应该用时钟周期计数,而不是固定延时。
优化策略:
- 用循环计数代替延时函数。比如等待 512 个周期,就写个空循环 512 次,而不是调用
delay_us。 - 合并配置步骤。有些寄存器可以一次性写入,别拆成多个写操作。
- 跳过不必要的训练。如果芯片支持「快速训练」模式,就用它。完整的读写训练可以放到操作系统启动后再做。
表格对比:
| 初始化步骤 | 传统做法 | 优化做法 | 节省时间 |
|---|---|---|---|
| ZQ 校准等待 | delay_us(10) | 循环计数 512 周期 | 约 5-10μs |
| DLL 锁定等待 | delay_ms(1) | 查询状态寄存器 | 约 0.5-1ms |
| 读写训练 | 完整训练 | 快速训练 | 约 2-5ms |
| 寄存器配置 | 逐位写入 | 整字写入 | 约 10-20μs |
你看,光这几项加起来,就能省下好几毫秒。对于 Bootloader 来说,这几毫秒可能就是「能启动」和「启动太快看不到 Logo」的区别。
注意:跳过完整训练可能会导致内存稳定性下降。如果产品对可靠性要求高,建议在量产前做充分的压力测试。我个人的经验是:快速训练 + 高温老化测试,基本能覆盖 99% 的问题。
3.4 外设时钟门控:用多少开多少,别浪费
外设时钟门控,说白了就是「用不到的模块就别给它时钟」。这个道理大家都懂,但实际做的时候,很多人图省事,直接把所有外设时钟都打开了。
为什么不能这么做?有两个原因:
- 功耗问题:虽然 Bootloader 阶段不太在意功耗,但有些芯片的时钟门控会影响 PLL 的负载,导致锁定时间变长。
- 启动时间:开启一个外设时钟,往往需要等待几个时钟周期才能稳定。如果一口气开几十个外设,累积起来也是不小的开销。
我的做法是:只开 Bootloader 必需的外设时钟。比如:
- UART(用于打印调试信息)
- GPIO(用于控制 LED 或按键)
- 定时器(用于延时或超时判断)
- DMA(如果要用)
其他的,比如 USB、以太网、I2C、SPI,统统不开。等 Bootloader 要跳转到应用程序之前,再统一关闭所有外设时钟,只保留必要的。
代码示例:
// 只开启必要的外设时钟
RCC->AHB1ENR = GPIOA_EN | GPIOB_EN;
RCC->APB1ENR = UART2_EN | TIM3_EN;
// 其他外设时钟保持关闭
// RCC->AHB1ENR |= USB_EN | ETH_EN; // 不要开
你想想看,如果 Bootloader 里根本用不到 USB,你开它的时钟干嘛?不仅浪费那几微秒的开启时间,还可能在跳转前忘记关掉,导致应用程序初始化时出问题。
避坑指南:我曾经遇到过一个问题:Bootloader 里开了某个外设的时钟,但没做初始化。跳转到应用程序后,那个外设的中断标志位莫名其妙被置位了,导致应用程序一直进中断。排查了半天才发现是 Bootloader 的锅。所以,用多少开多少,用完记得关。
小结
硬件初始化优化,核心就四个字:别让 CPU 闲着。时钟树配置要快,PLL 等待时间要利用起来,内存控制器要加速,外设时钟要按需开启。每一步省下几十微秒到几毫秒,加起来就是几十毫秒的差距。
嗯,这些经验都是我一个个坑踩出来的。下一章我们会聊「Flash 读取加速与代码执行效率优化」,到时候再分享一些更深入的内容。