第二章 时钟系统优化:MSI与HSI16选择策略、PLL关闭与旁路技巧、RTC与LSE时钟配置、时钟门控技术

时钟系统,说白了就是MCU的心脏。心跳多快、跳不跳、什么时候跳,直接决定了功耗的高低。很多工程师一上来就盯着休眠模式、停止模式折腾,结果发现功耗死活降不下去——回头一看,时钟树根本没优化。

这一章,我就带你把这颗“心脏”彻底调明白。

2.1 MSI vs HSI16:选谁更省电?

STM32L4内部有两个低频振荡器:MSI(多速内部振荡器)和HSI16(16 MHz高速内部振荡器)。很多人习惯性用HSI16,觉得它精度高、稳定。但低功耗场景下,这个选择不一定对。

核心结论:MSI在低频率下功耗远低于HSI16,且支持硬件自动调频,是低功耗运行的首选。

我在一个电池供电的温湿度采集项目中,最初用的HSI16分频到2 MHz,整机电流一直在80 µA左右徘徊。后来换成MSI的2 MHz模式,电流直接掉到35 µA。为什么?因为MSI内部是低功耗振荡器架构,HSI16是高速振荡器降频使用,底噪和功耗都高。

特性 MSI HSI16
频率范围 100 kHz ~ 48 MHz 16 MHz(固定)
典型功耗(2 MHz) ~15 µA ~80 µA
启动时间 ~5 µs ~1 µs
精度 ±1%(校准后) ±1%
硬件自动调频 支持(配合PLL) 不支持

我的选择策略:

  • 系统主频 ≤ 8 MHz:无脑选MSI,功耗优势明显
  • 系统主频 8~48 MHz:MSI + PLL升频,比HSI16 + PLL省电约20%
  • 需要快速启动(< 2 µs):选HSI16,但只在启动瞬间用,之后切到MSI

小技巧:MSI有一个“硬件自动调频”模式,配合PLL使用时,系统可以在低负载时自动降频到MSI的基频,高负载时通过PLL升频。这个功能在FreeRTOS空闲任务里特别好用——空闲时跑100 kHz,有任务时瞬间切到48 MHz。

2.2 PLL关闭与旁路:别让它偷你的电

PLL(锁相环)是个好东西,能把低频时钟倍频到高频。但它也是个电老虎。我见过太多人,系统明明只跑2 MHz,PLL却一直开着——白白多耗30~50 µA。

什么时候必须关PLL?

  • 进入Stop模式前:必须关闭PLL,否则无法进入最低功耗状态
  • 系统主频 ≤ 8 MHz且使用MSI:直接旁路PLL,用MSI直出
  • 外设时钟需求低于PLL输出:比如只跑UART 115200,用MSI 2 MHz就够了

我曾经踩过的坑:在Stop模式下,PLL没关干净,导致电流比预期高了40 µA。后来查手册才发现,PLL关闭后需要等待PLLRDY位清零,才算真正关掉。代码里加了个while循环等标志位,问题解决。

PLL旁路配置示例:

// 关闭PLL并旁路
RCC->CR &= ~RCC_CR_PLLON;          // 关闭PLL
while(RCC->CR & RCC_CR_PLLRDY);    // 等待PLL完全关闭

// 选择MSI作为系统时钟
RCC->CFGR &= ~RCC_CFGR_SW;        // 清除时钟源选择位
RCC->CFGR |= RCC_CFGR_SW_MSI;     // 选择MSI
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_MSI); // 等待切换完成

省电口诀:不用PLL就关,关了要等确认,确认完再切时钟源。三步缺一不可。

2.3 RTC与LSE时钟配置:时间不能停,但电要省

RTC(实时时钟)在低功耗系统里几乎是标配——定时唤醒、记录时间、维护日历。但RTC的时钟源选择,直接影响休眠时的功耗。

STM32L4的RTC有三个时钟源可选:

  • LSE(32.768 kHz外部晶振):精度最高(±5 ppm),功耗最低(~0.5 µA),我90%的项目都用它
  • LSI(32 kHz内部振荡器):免外部晶振,但精度差(±5%),温漂大,不适合长时间计时
  • HSE(外部高速时钟分频):精度高但功耗大,低功耗场景基本不用
时钟源 精度 功耗 启动时间 推荐场景
LSE ±5 ppm ~0.5 µA ~1 s 电池供电、长时间计时
LSI ±5% ~0.3 µA ~100 µs 短时间定时、唤醒
HSE ±10 ppm ~5 µA ~1 ms 高精度、不关心功耗

我的配置习惯:

  • 需要日历功能:必须用LSE,否则一天能差几十分钟
  • 只需要定时唤醒(比如每10秒醒一次):用LSI,省掉外部晶振和两个电容,BOM成本降0.2元
  • 混合方案:平时用LSI做短定时,每隔1小时用LSE校准一次LSI的漂移

关键配置:RTC的异步预分频和同步预分频要算好。LSE 32.768 kHz,异步预分频设为128,同步预分频设为256,得到1 Hz的秒脉冲。这个组合功耗最低,因为异步分频器工作在低频,同步分频器工作在高频但分频比小。

// RTC时钟配置示例(LSE + 1 Hz输出)
RCC->BDCR |= RCC_BDCR_LSEON;          // 开启LSE
while(!(RCC->BDCR & RCC_BDCR_LSERDY)); // 等待LSE稳定(约1秒)

RCC->BDCR &= ~RCC_BDCR_RTCSEL;       // 清除RTC时钟源
RCC->BDCR |= RCC_BDCR_RTCSEL_LSE;    // 选择LSE

RTC->PRER = (127 << 16) | 255;       // 异步128分频,同步256分频
RTC->CR |= RTC_CR_BYPSHAD;           // 旁路影子寄存器,减少访问延迟

注意:LSE启动时间很长,最长能到2秒。如果系统上电后立刻需要RTC,建议先切到LSI,等LSE稳定后再切换。我有个产品因为这个原因,上电后前2秒RTC不走,导致日志时间戳全乱了。

2.4 时钟门控技术:不给外设发粮

时钟门控,说白了就是“用谁开谁,不用就关”。STM32L4的每个外设都有独立的时钟使能位,在RCC寄存器里控制。但很多人只会在初始化时开时钟,之后再也不关。

时钟门控的核心原则:

  • 外设不用时,关掉它的时钟(包括APB1、APB2、AHB总线时钟)
  • 进入低功耗模式前,检查所有外设时钟是否已关闭
  • DMA和中断触发的外设,用完后立即关时钟

我常用的做法:写一个外设时钟管理函数,每次使用外设前开时钟,使用后关时钟。配合RTOS的信号量,确保不会在中断里误关。

// 外设时钟管理示例
void USART_SendByte(USART_TypeDef* usart, uint8_t data) {
    // 开时钟
    if(usart == USART1) {
        RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    }
    
    // 等待发送完成
    while(!(usart->ISR & USART_ISR_TXE));
    usart->TDR = data;
    while(!(usart->ISR & USART_ISR_TC));
    
    // 关时钟
    if(usart == USART1) {
        RCC->APB2ENR &= ~RCC_APB2ENR_USART1EN;
    }
}

哪些外设的时钟必须一直开着?

  • RTC(如果要用)
  • LPTIM(低功耗定时器,用于唤醒)
  • IWDG(独立看门狗,如果使能了)
  • DMA(如果数据还在传输中)

其他外设,包括GPIO、SPI、I2C、ADC、DAC,用完后一律关时钟。GPIO的时钟尤其容易被忽略——很多人初始化完GPIO就不管了,结果GPIO时钟一直开着,多耗几µA。

终极技巧:在进入Stop模式前,遍历所有外设时钟寄存器,打印出还在使能的外设列表。我写了一个debug函数,每次调试时跑一遍,能发现很多“漏网之鱼”。

2.5 本章知识体系

下面这张图,把时钟系统优化的核心逻辑串起来了。你可以把它贴在工位上,每次做低功耗设计时对照着检查。

时钟系统优化知识体系 时钟源选择 MSI vs HSI16 PLL关闭与旁路 RTC与LSE配置 MSI策略 ≤8MHz用MSI直出 8~48MHz用MSI+PLL PLL策略 Stop模式前必须关 低主频时旁路直出 RTC策略 日历用LSE,唤醒用LSI 混合方案:LSI+LSE校准 时钟门控:用谁开谁,不用就关 目标:系统功耗降至最低