硬件初始化裁剪:关闭不必要的外设时钟,减少启动阶段的功耗与时间

各位做嵌入式固件的朋友,咱们接着聊启动优化。

上一节我们讲了启动流程的宏观把控。今天要聊的,是启动阶段里一个特别实在的优化点——硬件初始化裁剪。说白了,就是把你用不到的外设时钟,在启动时统统关掉。

为什么要这么做?我直接说结论:每开一个外设时钟,就多一份功耗,多一段初始化时间。你想想看,按摩仪这种电池供电的设备,启动时如果把所有外设都点亮一遍,那电池扛得住吗?用户按一下开关,等半天才启动,体验能好吗?

时钟树:你系统的命脉

先简单回顾一下。大多数MCU的时钟树是这样的:

  • 主时钟源(HSE、HSI、PLL)
  • 总线时钟(AHB、APB1、APB2)
  • 外设时钟(每个外设独立使能位)

启动时,默认行为往往是「能开的全开」。这就像你进家门,把客厅、卧室、厨房、厕所的灯全打开——没必要嘛。

核心原则:只使能启动阶段必须的外设时钟,其余一律关闭。等用到时再动态开启。

实战:我踩过的坑

我记得有一次做一款便携式按摩仪,用的是STM32G0系列。启动时间要求小于200ms,功耗要求启动阶段平均电流不超过10mA。

第一次测试结果:启动耗时380ms,峰值电流冲到25mA。完全不合格。

我打开代码一看,好家伙,初始化函数里把TIM、SPI、I2C、USART、ADC、DAC……所有外设的时钟全开了。有些外设根本没用上,比如DAC,我们产品压根不需要模拟输出。

这就是典型的「初始化偷懒」——图省事,把所有外设都初始化一遍。结果就是功耗和时间双双超标。

具体怎么做?三步走

第一步:梳理外设清单

打开你的原理图和代码,列出所有外设。然后问自己三个问题:

  1. 这个外设启动阶段必须用吗?
  2. 这个外设后续会用到吗?
  3. 如果后续用到,能不能延迟初始化?

我习惯用表格来梳理:

外设 启动阶段必需? 后续使用? 处理方式
GPIO 保留
TIM1(PWM输出) 保留
USART1(调试串口) 延迟初始化
SPI1(Flash) 延迟初始化
I2C1(触摸芯片) 延迟初始化
ADC1(电池检测) 延迟初始化
DAC 彻底关闭

第二步:修改时钟使能代码

这是最直接的优化。以STM32为例,原来可能是这样的:

// 原来的代码:一股脑全开
void HAL_MspInit(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_TIM1_CLK_ENABLE();
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_SPI1_CLK_ENABLE();
    __HAL_RCC_I2C1_CLK_ENABLE();
    __HAL_RCC_ADC1_CLK_ENABLE();
    __HAL_RCC_DAC_CLK_ENABLE();  // 这个根本没用!
}

优化后:

// 优化后的代码:只开必要的
void HAL_MspInit(void)
{
    // 启动阶段必需的外设
    __HAL_RCC_GPIOA_CLK_ENABLE();   // 按键检测
    __HAL_RCC_GPIOB_CLK_ENABLE();   // LED指示
    __HAL_RCC_TIM1_CLK_ENABLE();    // PWM输出(按摩电机)
    
    // 其他外设延迟到使用时再初始化
    // USART1、SPI1、I2C1、ADC1 在各自模块初始化时使能时钟
    // DAC 彻底关闭,因为硬件上就没用
}

小技巧:我习惯在初始化函数里加一个宏开关,方便调试时快速切换。比如:

#define ENABLE_ALL_CLOCKS_FOR_DEBUG  0  // 调试时改为1

#if ENABLE_ALL_CLOCKS_FOR_DEBUG
    // 全开
#else
    // 裁剪版本
#endif

第三步:动态时钟管理

有些外设虽然启动阶段不用,但后续会用到。这时候不要一股脑在启动时初始化,而是按需开启

举个例子,我们的按摩仪有个蓝牙模块,通过USART通信。但蓝牙模块本身启动需要时间(大概100ms),而且用户不一定马上用蓝牙。所以我的做法是:

  1. 启动时:不使能USART时钟,不初始化蓝牙
  2. 用户点击「连接蓝牙」按钮时:使能USART时钟,初始化蓝牙
  3. 蓝牙断开或用户关闭时:关闭USART时钟,进入低功耗

这样,启动阶段就省下了USART初始化的时间和功耗。

效果对比:数据说话

还是回到我那个项目。经过裁剪后,重新测试:

指标 优化前 优化后 提升
启动时间 380ms 120ms 68%
启动峰值电流 25mA 8mA 68%
启动平均电流 18mA 5mA 72%

效果很明显吧?就改了几行代码,时间和功耗都降下来了。

注意:关闭外设时钟时,一定要确认该外设当前没有正在执行的操作。比如,如果你在DMA传输过程中关闭了时钟,那DMA会立即停止,数据就丢了。我曾经就犯过这个错——在ADC采样过程中关了ADC时钟,结果采样值全是0,排查了半天。

避坑指南

我总结几个常见的坑,你写代码时注意一下:

  • 坑1:GPIO复用功能。有些GPIO的复用功能需要对应外设时钟。比如你用PA9做USART1的TX,如果关了USART1时钟,那PA9就输出不了数据。所以,关闭时钟前,确认一下GPIO的复用关系。
  • 坑2:中断和DMA。如果某个外设的中断或DMA正在等待响应,你关了时钟,那中断就永远来不了了。我建议在关闭时钟前,先禁用该外设的中断和DMA。
  • 坑3:调试接口。调试时别关SWD/JTAG的时钟,否则调试器连不上,你就只能按复位键了。嗯,这个我吃过亏。

总结

硬件初始化裁剪,说白了就是「按需分配,用完即走」。启动阶段只开必要的,后续用到再开,不用就关。这不仅是优化启动时间,更是降低功耗的关键手段。

我个人习惯是在项目初期就做好外设清单,标注每个外设的「启动阶段必需性」。这样后期优化时,就不用再翻代码重新梳理了。

下一节,我们会聊到内存初始化优化——如何减少BSS段清零和data段拷贝的时间。这也是启动阶段的一个大头。