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、配置中断向量表、甚至把栈指针重新设一下。所以我的建议是:

  1. 先配置 PLL,启动锁定过程。
  2. 不等待锁定,立刻去做其他不依赖高频时钟的初始化。
  3. 等那些事情做完了,再回来检查 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),结果在低频下没问题,一超频就挂。后来才发现,应该用时钟周期计数,而不是固定延时

优化策略:

  1. 用循环计数代替延时函数。比如等待 512 个周期,就写个空循环 512 次,而不是调用 delay_us
  2. 合并配置步骤。有些寄存器可以一次性写入,别拆成多个写操作。
  3. 跳过不必要的训练。如果芯片支持「快速训练」模式,就用它。完整的读写训练可以放到操作系统启动后再做。

表格对比:

初始化步骤 传统做法 优化做法 节省时间
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 读取加速与代码执行效率优化」,到时候再分享一些更深入的内容。