2.3 时钟树与低功耗设计 前两节解决了"地址"和"落位",这一节补上寄存器编程的最后一块地基:节拍。STM32 标称 72 MHz,这数字从哪来、怎么分给每个外设、为什么写寄存器前要先开时钟——答案全在时钟树里。顺着时钟这根藤,我们还顺手解决电池设备的命门:低功耗。学完这节,第 3 章点灯代码里的每一行你都能说出物理含义。 时钟源:三口泉水 STM32F103 有三个时钟来源。HSI 是内部 8 MHz 的 RC 振荡器,上电即用、免外围器件,但精度差(±1%),串口这类对波特率敏感的场合不敢依赖它;HSE 是外部晶振,蓝药丸板上焊着 8 MHz 无源晶振,精度高,是正经产品的标配;
前两节解决了"地址"和"落位",这一节补上寄存器编程的最后一块地基:节拍。STM32 标称 72 MHz,这数字从哪来、怎么分给每个外设、为什么写寄存器前要先开时钟——答案全在时钟树里。顺着时钟这根藤,我们还顺手解决电池设备的命门:低功耗。学完这节,第 3 章点灯代码里的每一行你都能说出物理含义。
STM32F103 有三个时钟来源。HSI 是内部 8 MHz 的 RC 振荡器,上电即用、免外围器件,但精度差(±1%),串口这类对波特率敏感的场合不敢依赖它;HSE 是外部晶振,蓝药丸板上焊着 8 MHz 无源晶振,精度高,是正经产品的标配;PLL(锁相环)本身不产生频率,它把 HSI 或 HSE 倍频后作为系统时钟,72 MHz 正是 8 MHz 晶振九倍频的产物。
上电瞬间系统跑在 HSI 上(复位后 RCC->CFGR 的 SW 位为 00),这是硬件保证的:不管晶振好坏,芯片总能活。等软件确认 HSE 稳定(等 HSERDY 标志位)、配好 PLL 并锁定(等 PLLRDY),再切换系统时钟源到 PLL。这套"先跑内置、再切高速"的流程几乎所有 ARM 芯片通用,ESP32 上电先跑内部 RTC 定时器再切主晶振,同款思路。
系统时钟 SYSCLK 确定后像树干一样逐级分发:到内核和 DMA 的 AHB 总线,可 1 到 512 分频;AHB 再分出 APB1(最高 36 MHz)与 APB2(最高 72 MHz)两条外设总线。定时器还有一层"双倍"规则——APB 分频若不为 1,挂在其上的定时器时钟自动乘回两倍,所以 APB1 分频到 36 MHz 时,TIM2 仍然拿到 72 MHz。初学者算定时器参数算错,十有八九栽在这条隐性规则上。
每个外设的时钟通路都带一个独立的使能开关,这就是第 1 章预告的时钟门控:开关断开,时钟进不来,外设完全静止,功耗几乎为零,寄存器也写不进去(写了等于白写,读回恒为 0)。这就是"写外设寄存器前必须先开时钟"的物理原因。下面的代码把 SYSCLK 从 8 MHz 晶振倍频到 72 MHz,并顺手打开 GPIOB 的时钟:
#include "stm32f10x.h" void clock_init_72mhz(void) { RCC->CR |= RCC_CR_HSEON; /* 打开外部晶振 */ while (!(RCC->CR & RCC_CR_HSERDY)) { } /* 等晶振起振稳定 */ RCC->CFGR |= RCC_CFGR_PLLMULL9 /* PLL 九倍频:8 MHz x 9 = 72 MHz */ | RCC_CFGR_PLLSRC; /* PLL 时钟源选 HSE */ FLASH->ACR |= FLASH_ACR_PRFTBE /* 开预取缓冲,匹配 Flash 速度 */ | FLASH_ACR_LATENCY_2; /* 72 MHz 下 Flash 等待周期设 2 */ RCC->CR |= RCC_CR_PLLON; /* 开 PLL */ while (!(RCC->CR & RCC_CR_PLLRDY)) { } /* 等 PLL 锁定 */ RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL; /* SYSCLK 切到 PLL */ while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) { } /* 确认真的切过去了 */ RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; /* 给 GPIOB 通上时钟 */ }
注意等待标志的写法:每一步都轮询就绪位再走下一步,跳步的代码在部分批次芯片上会卡死。工程上更稳妥的做法是直接用 ST 官方启动文件里的 SystemInit,它已按芯片型号把这些参数配好——但配参数的原理你要会手算,换主频、换芯片时才不慌。
时钟树天然引出省电哲学:不工作的单元不给时钟。STM32 提供三种递进的低功耗模式:
| 模式 | 关掉什么 | 唤醒方式 | 唤醒后 | 典型电流 |
|---|---|---|---|---|
| 睡眠 Sleep | 内核时钟 | 任意中断 | 接着跑 | 约几 mA |
| 停机 Stop | 全部时钟(外设冻结) | EXTI 中断 | 从停机处继续 | 约几十 uA |
| 待机 Standby | 连 1.8V 域一起断电 | WKUP 引脚、RTC | 相当于复位重启 | 约几 uA |
选型的分水岭是"醒来后要不要接着原来的地方跑"。周期采集类设备(每分钟量一次温度)最适合停机模式:醒来、采样、发送、再睡,中断现场完好;若一天只醒几次且无所谓重启,待机模式的几微安更香,代价是醒来等于重新上电,RAM 不保。
把"采样-发送-休眠"的循环写成代码,停机模式的进出配合 RTC 闹钟即可实现定时唤醒:
#include "stm32f10x.h" #include <stdint.h> void enter_stop_until_rtc_alarm(void) { PWR->CR |= PWR_CR_CWUF; /* 清唤醒标志 */ PWR->CR |= PWR_CR_LPDS; /* 调压器进低功耗模式 */ SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; /* SLEEPDEEP 置位:下条 WFI 进停机而非睡眠 */ /* 使能并等待 RTC 闹钟中断(RTC 配置略,原理同本节时钟等待套路)*/ EXTI->IMR |= EXTI_IMR_MR17; /* RTC 闹钟接到 EXTI 线 17 */ EXTI->FTSR |= EXTI_FTSR_TR17; __asm volatile ("wfi"); /* 等中断:CPU 在这里"睡死",直到闹钟响 */ SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk; /* 醒来第一件事:清深睡眠标志 */ SystemCoreUpdate(); /* 停机后 HSI 重启,主时钟需重新切回 PLL */ } void sample_loop(void) { for (;;) { read_sensor_and_send(); /* 醒着干活:读传感器、发送 */ enter_stop_until_rtc_alarm(); /* 干完就睡,等下一班闹钟 */ } }
停机醒来有个经典暗坑:停机把 PLL 也停了,唤醒后系统时钟回落到 HSI 8 MHz,若不重新走一遍 PLL 切换流程,串口波特率、定时器周期全部失准——表现为"睡一觉起来串口乱码"。看到这种症状,先查唤醒后的时钟状态。
ESP32 把这套思想做得更细:Modem Sleep(只关射频)、Light Sleep(CPU 暂停、RAM 保持、微秒级唤醒)、Deep Sleep(只留 RTC 域,约 10 uA,醒来重新走启动流程)和 Hibernation(只留一个 RTC 定时器,约 5 uA)。接口也现代化了,一行 esp_deep_sleep_start() 配上唤醒源即可,不用手摸寄存器。但判断"用哪档"的逻辑与 STM32 完全一致:醒来要不要现场?多久醒一次?醒了要干什么?这套问题清单是平台无关的。
正经产品还要回答"晶振坏了怎么办"。STM32 提供时钟安全系统(CSS):HSE 使能时打开 CSS 位,硬件持续监测晶振,一旦失效立即关断 HSE、触发 NMI 中断、系统自动切回 HSI 跑——频率从 72 MHz 掉到 8 MHz,但设备不死机。中断里该做什么?立刻降级运行:停掉对时序敏感的外设(PWM、通信),把状态通过尚能工作的通道报告出去。量产固件不开 CSS 等于把整批设备押注在一颗两毛钱的晶振上——温湿度循环、焊接应力、运输震动都可能让它停振,这种失效在售后统计里并不罕见。
至此三块地基——地址、落位、节拍——都打好了。下一章进入全书的主场:GPIO。你会亲手把本章的一切串起来:从存储映像找到 GPIOB 的基地址(2.1),往它的模式寄存器写入配置(2.1 的指针写法),先开 APB2 上的时钟门控(本节),然后看着那颗 LED 亮起来。那一刻你会发现,"寄存器点亮第一颗 LED"不是一句口号,而是把这三节的知识按顺序执行一遍。