2. STM32L4 时钟系统与低功耗模式

时钟,是 MCU 的心脏。这话一点不夸张。STM32L4 的时钟树比 F 系列复杂不少,但好处是——你可以按需裁剪功耗。我刚开始用 L4 时,总觉得时钟配置太绕,后来发现,只要抓住几个关键振荡器,剩下的就是连线题。

2.1 时钟树详解:四大振荡器

STM32L4 有四个主要时钟源。我习惯把它们分成两组:一组是“干活用的”,一组是“省电用的”。

时钟源 频率范围 典型功耗 主要用途
HSI16 16 MHz ~100 µA 系统启动、快速唤醒
MSI 100 kHz – 48 MHz ~5 µA (低频段) 低功耗运行、自动调频
HSE 4 – 48 MHz 取决于外部晶振 高精度、USB、以太网
LSE 32.768 kHz ~0.6 µA RTC、LPTIM、待机唤醒

核心原则: 系统时钟 (SYSCLK) 只能选一个主源。但外设时钟可以独立选择,甚至可以在运行中切换。

2.2 MSI:L4 的低功耗王牌

MSI 是 STM32L4 独有的。它内部有一个 RC 振荡器,但通过自动校准,精度能做到 ±0.25% 以内。我在一个电池供电的传感器项目里,全程用 MSI 跑 2 MHz,功耗比 HSI 低了将近 40%。

MSI 还有一个绝活:PLL 输入自动切换。当系统从 Stop 模式唤醒时,MSI 可以在 3 µs 内稳定输出,而 HSI 需要 5 µs 以上。你想想看,对于频繁休眠唤醒的场景,这 2 µs 的差距就是功耗的分水岭。

我的习惯: 只要应用不需要 USB 或高精度定时,我就用 MSI 做主时钟。省电,还省一颗外部晶振。

2.3 时钟配置实战:从复位到运行

系统上电后,默认使用 MSI 跑 2 MHz。这个频率足够做初始化。我一般会在 main 函数开头做三件事:

  1. 切换时钟源:根据需求切换到 HSI 或 HSE。
  2. 配置 PLL:如果需要更高频率,用 PLL 倍频。
  3. 设置 AHB/APB 分频:确保外设总线不超频。

下面是一个典型的配置代码,切换到 HSE 并倍频到 80 MHz:

void SystemClock_Config(void)
{
    // 1. 使能 HSE
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLM = 1;
    RCC_OscInitStruct.PLL.PLLN = 20;
    RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;
    RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
    RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
    HAL_RCC_OscConfig(&RCC_OscInitStruct);

    // 2. 配置 SYSCLK、AHB、APB 分频
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
    HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4);
}

注意: 切换时钟源时,一定要先确认新时钟已经稳定。HAL 库的 HAL_RCC_OscConfig 内部会等待就绪,但如果你自己操作寄存器,别忘了轮询 HSERDY 或 MSIRDY 标志位。

2.4 低功耗模式:三种模式,三种哲学

STM32L4 有三种低功耗模式。我经常跟学员说:选哪种模式,取决于你“睡多深”以及“醒多快”。

模式 CPU 状态 SRAM 保持 唤醒时间 典型功耗
Sleep 停止 全部 ~5 µs ~1 mA
Stop 掉电 全部 ~5 µs ~1 µA
Standby 掉电 仅备份区 ~50 µs ~0.1 µA

2.5 Sleep Mode:浅睡,随时待命

Sleep 模式只停了 CPU 内核,外设和 SRAM 都还在跑。说白了,就是“闭眼不关机”。

我常用 Sleep 模式配合 RTOS 的空闲任务。当所有任务都挂起时,让 CPU 睡下去,中断一来就醒。这样既不丢数据,又能省电。

// 进入 Sleep 模式
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);

// 任意中断唤醒后,继续执行下一行代码

避坑: 进入 Sleep 前,记得关掉不用的外设时钟。否则外设还在跑,功耗降不下来。我曾经在调试时发现 Sleep 模式功耗有 5 mA,查了半天,原来是 USART 没关。

2.6 Stop Mode:深度睡眠,保留现场

Stop 模式把 CPU 和大部分外设的时钟都停了,但 SRAM 和寄存器内容保留。唤醒后,从断点继续执行。

这里有个关键点:电压调节器。Stop 模式有两种调节器状态:

  • MR (Main Regulator):唤醒快,功耗稍高。
  • LPR (Low Power Regulator):唤醒慢,功耗更低。

我一般用 LPR,除非应用对唤醒时间有硬性要求。比如一个温湿度采集器,每 10 秒醒一次,LPR 完全够用。

// 进入 Stop 模式,使用低功耗调节器
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);

// 唤醒后,需要重新配置系统时钟
SystemClock_Config();

重要: 从 Stop 模式唤醒后,系统时钟会回到 MSI 2 MHz。你必须手动重新配置时钟源。我见过有人忘了这一步,结果外设跑在 2 MHz 下,通信全部超时。

2.7 Standby Mode:最深睡眠,几乎关机

Standby 模式是最省电的。CPU、SRAM、大部分外设全部掉电。只有备份域(RTC、LSE、备份寄存器)还活着。

唤醒方式只有几种:WKUP 引脚、RTC 闹钟、复位。唤醒后,系统相当于重新启动,但你可以通过检查 PWR_FLAG_SB 来判断是从 Standby 唤醒的。

// 进入 Standby 模式
HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);
HAL_PWR_EnterSTANDBYMode();

// 唤醒后,从复位向量开始执行
if (__HAL_PWR_GET_FLAG(PWR_FLAG_SB) != RESET) {
    // 清除标志
    __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB);
    // 执行唤醒后的初始化
}

经验: Standby 模式适合“心跳式”应用。比如电池供电的烟雾报警器,每分钟醒一次,检测完就睡。功耗可以做到 0.1 µA 以下,一节 CR2032 电池用两年没问题。

2.8 LPTIM:低功耗定时器,省电利器

LPTIM 是 STM32L4 的“省电定时器”。它可以在 Stop 模式下运行,时钟源来自 LSE 或 LSI。这意味着,CPU 睡了,LPTIM 还在走。

我做过一个项目,需要每 100 ms 唤醒一次采集数据。如果用普通定时器,CPU 必须保持运行。用 LPTIM,CPU 可以睡在 Stop 模式,LPTIM 计时到了再唤醒。功耗从 5 mA 降到了 2 µA。

配置 LPTIM 很简单:

// 使能 LPTIM 时钟
__HAL_RCC_LPTIM1_CLK_ENABLE();

// 配置 LPTIM
LPTIM_HandleTypeDef hlptim1;
hlptim1.Instance = LPTIM1;
hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_APBCLOCK_LPOSC;
hlptim1.Init.Clock.Prescaler = LPTIM_PRESCALER_DIV128;
hlptim1.Init.Trigger.Source = LPTIM_TRIGSOURCE_SOFTWARE;
hlptim1.Init.OutputPolarity = LPTIM_OUTPUTPOLARITY_HIGH;
hlptim1.Init.UpdateMode = LPTIM_UPDATE_IMMEDIATE;
hlptim1.Init.CounterSource = LPTIM_COUNTERSOURCE_INTERNAL;
HAL_LPTIM_Init(&hlptim1);

// 设置自动重载值,100 ms 唤醒一次
HAL_LPTIM_TimeOut_Start_IT(&hlptim1, 100, 0);

我的建议: LPTIM 的时钟源优先选 LSE。LSI 精度太差,温度变化时漂移严重。LSE 是晶振,稳定得多。

2.9 本章知识结构

下面这张图,把时钟源、低功耗模式、LPTIM 的关系串起来了。你可以把它当作本章的“思维导图”。

STM32L4 低功耗时钟与模式结构图 HSI16 (16 MHz) MSI (100k-48M) HSE (外部晶振) LSE (32.768k) SYSCLK (系统时钟) Sleep (浅睡) Stop (深度睡眠) Standby (待机) LPTIM (低功耗定时器) 说明:LSE 可为 LPTIM 提供时钟,使其在 Stop 模式下继续计时 LPTIM 超时后可通过中断唤醒 CPU,实现低功耗定时唤醒

2.10 避坑总结

最后,我把这些年踩过的坑列出来,你遇到了可以直接翻回来查:

  • 时钟切换后外设死掉:切换时钟源后,外设时钟可能丢失。记得重新配置外设时钟分频。
  • Stop 模式唤醒后系统跑飞:唤醒后没重新配置系统时钟,外设跑在 2 MHz 下。进 Stop 前保存时钟配置,唤醒后恢复。
  • LPTIM 不工作:检查 LPTIM 的时钟源是否使能。LSE 没起振,LPTIM 就停摆。
  • Standby 模式唤醒后数据丢失:SRAM 在 Standby 模式下掉电。需要保存的数据,放到备份寄存器或 RTC 备份域里。

一句话总结: 时钟是功耗的开关,模式是功耗的档位。选对时钟源,用对低功耗模式,L4 的功耗可以做到让你惊讶。