开篇
STM32 为什么能”有条不紊”地工作?为什么芯片内部的电路不会乱套?
在每一篇博客的代码里,都有这么一行永远存在的代码,之前没有深究:
1 | SystemClock_Config(); |
这行代码在配置什么?为什么 GPIO、I2C、串口能正常工作的前提,都是先执行它?答案就是本期的主角——时钟源与时钟树,单片机的”心脏与动脉”。
这一集是纯理论讲解,不写任何新代码。但它是理解后续所有外设(定时器、ADC、DMA……)的地基——因为每个外设的时钟频率,都是从这颗时钟树上”摘”下来的。
为什么单片机需要”心跳”
微观视角:门电路的”运算延时”
芯片内部是庞杂的逻辑电路组成的,我们假设 STM32 内部有这样一套电路:一个与门 + 一个异或门,异或门的输出连接到了某个寄存器的某一位:
1 | A ──┬─────┐ |
- 与门:A 和 B 都是高电平 1 时输出 1,其他情况输出 0
- 异或门:两个输入不一致时输出 1,一致时输出 0
寄存器是”程序与电路之间的桥梁”——程序读寄存器获取电路状态,写寄存器控制电路运行。
假设当前 A、B 都是高电平:与门输出 1,异或门两输入都是 1 → 输出 0,寄存器存储 0。
现在 B 突然从高电平变为低电平(比如某个 GPIO 输入的电平变了),理想情况下:与门输出变 0,异或门两输入都变 0 → 输出还是 0,寄存器安然无恙。
但实际情况有出入:
1 | B 电平变化瞬间到达异或门输入端 |
寄存器中出现了一段由门电路运算延时产生的错误状态——这段错误虽然短暂,但可能导致依赖此寄存器的程序、其他逻辑电路出现更严重的连锁错误。芯片内部电路比这个例子复杂成千上万倍,每个信号经过的门电路数量不一、错综复杂,如果放任不管,芯片内部电信号将乱成一锅粥。
解决办法:边沿触发器 + 方波
解决方法其实很简单——在寄存器前放一个边沿触发器:
1 | D端(数据输入) Q端(数据输出) |
边沿触发器的特性:当控制端出现由低到高的上升沿时,输出端才采样 D 端的值,其他时候输出保持不变。
于是,只要给控制端接入一个一定频率的方波信号:
- 方波一个周期内,门电路运算产生的任何”毛刺”都无法进入寄存器
- 等一切运算都稳定后,方波的上升沿到达,把最终稳定结果锁存进寄存器
这个好似脉搏的方波信号,就是时钟信号。它在单片机中由”心脏”——时钟源产生,通过”动脉”——时钟树传播到整个芯片。这也是”单片机离开了时钟源就无法存活”的原因。
💡 这就是数字电路里的经典概念:同步时序逻辑。时钟就是全芯片的”节拍器”,让所有电路在同一个节拍下协同工作,避免异步混乱。还记得 STM32 – I2C 中断与 DMA 里讲的”同步/异步”吗?硬件层面的同步,就是靠这根时钟线实现的。
宏观视角:外设也需要时钟
除了微观层面,宏观的外设层面同样需要精确的时钟脉冲:
- 串口需要固定的波特率进行通信
- 定时器需要计时,得知道时间流逝了多少
- ADC 需要采样时序
这些都需要时钟脉冲告诉外设”时间在流逝”。
数据流全链路:时钟从晶振走到外设
理解了”为什么需要时钟”,我们来看时钟是怎么从源头一步步传到每个外设的。这就是数据流全链路,也是本篇的核心:
1 | ┌─────────┐ |
一句话概括这条链路:8MHz 晶振 ÷1 → PLL ×9 → SYSCLK 72MHz → AHB ÷1 → HCLK 72MHz → APB1 ÷2 / APB2 ÷1 → PCLK1 36MHz / PCLK2 72MHz。
下面把每个节点拆开讲。
时钟树各节点详解
主干道:AHB 总线与 HCLK
STM32 芯片内部有一个 AHB 总线(Advanced High-performance Bus,先进高性能总线),它就像芯片的主干道。处理器、内存、DMA 以及各种外设都连接到这条总线上,通过它进行数据通信。
HCLK 就是这条 AHB 总线上的时钟线,直接连入处理器内核、内存和 DMA,为它们带来时钟信号。如果 HCLK 是 72MHz,那这些部件的时钟频率就都是 72MHz。
72MHz 是什么概念?赫兹是频率单位,1Hz = 每秒 1 次,72MHz = 每秒 7200 万次运算。
SysTick:系统滴答
在处理器内核中有一个 SysTick(系统滴答)定时器,为程序提供时间基准——之前学外部中断时提过,HAL_Delay() 的延时功能就是靠它实现的。
与 HCLK 的”直连”不同,SysTick 前多了一个分频器(÷1 或 ÷8):HCLK=72MHz 时设 ÷1 则 SysTick 也是 72MHz,设 ÷8 则降为 9MHz。
外设分支:APB1 / APB2 总线
像 GPIO、串口、I2C 这些外设,并没有直接挂在 AHB 总线上,而是分门别类连接到两个 APB 总线(Advanced Peripheral Bus,先进外设总线)上,再通过桥接器与 AHB 上的处理器、DMA 通信:
| 总线 | 挂载的外设 |
|---|---|
| APB1(低速) | TIM2~TIM5、SPI3、I2C、USB、CAN、通用/基本定时器等 |
| APB2(高速) | ADC、串口1、SPI1、高级定时器 TIM1/TIM8、所有 GPIO、中断等 |
(详细清单见 STM32 中文参考手册第 25 页的系统结构图。)
两个 APB 总线的时钟都来自 HCLK,经过各自可设置的预分频器分频后,分频后的时钟叫 PCLK,下面是一个例子:
- APB1 预分频 ÷2 → PCLK1 = 36MHz(APB1 上除定时器外的外设)
- APB2 预分频 ÷1 → PCLK2 = 72MHz

定时器的”特权”:倍频器
定时器比较特殊——它前面多了一个倍频器,用来把频率”再提上去”。这个倍频器有 ×1 和 ×2 两个值,由预分频器自动决定,我们无法手动设置:
| APB 预分频值 | 定时器倍频 | 定时器时钟 |
|---|---|---|
| ÷1 | ×1 | = PCLK |
| ÷2 / ÷4 / ÷8 / ÷16 | ×2 | = 2 × PCLK |
规则背后的意图:让定时器尽可能保持较高频率。比如 APB1 预分频 ÷2 时 PCLK1=36MHz,但定时器倍频 ×2 后仍是 72MHz——这正是我们代码里看到的配置,也让马上要学的定时器能有最高的 72MHz 时基。
ADC 的专属预分频器
ADC 还有一套专属的预分频器:ADC 采样需要采集一段时间才能更准确地获取电压值,所以它的时钟频率需要慢下来。比如 ADC 预分频 ÷6 → ADC 时钟 = 12MHz。
时钟源(树的根):HSI 与 HSE
前面的部分让时钟树有了枝叶,而根茎从哪来?HCLK 上的频率源头有两个时钟源:
| 时钟源 | 全称 | 频率 | 特点 |
|---|---|---|---|
| HSI | 高速内部时钟 | 8MHz | 内置在芯片内部,无需外接电路,默认使用,简单方便但精度稍差 |
| HSE | 高速外部时钟 | 8MHz | 需要从外部电路接入晶振,脉冲精度遥遥领先 |
HSE 依赖的晶振内部有石英晶体,可以发出一定频率的脉冲信号——大家常用的石英表就是相同原理(这就是为什么石英表走时准)。
HSI 或 HSE 二选一接入时钟树后,这个接入的数值叫做 SYSCLK 系统时钟。
⚠️ 注意:别把 SYSCLK(系统时钟) 和前面说的 SysTick(系统滴答) 搞混了!SysTick 是内核里的一个定时器,SYSCLK 是时钟树顶端的时钟源,只是名字像。
8MHz 经过分频器只能变小,怎么也不可能是 72MHz——所以 SYSCLK 还有第三个选项:PLLCLK。
PLL:锁相环(倍频器)
PLL 锁相环的具体原理很深,我们不去深究,只需要知道:它在此处起到倍频器的作用。
1 | PLL 输入前分频: HSI → 固定 ÷2 (4MHz) |
于是:HSE 8MHz ÷1 ×9 = 72MHz,只要 AHB 预分频 ÷1,HCLK 就是 72MHz——这就是我们代码中配置的完整路径。
剩余的分支
时钟树讲到这里,对照 CubeMX 的时钟树页面还差几样东西:
- FCLK(自由运行时钟):当 STM32 进入低功耗的停止模式时,AHB 总线停止运行、HCLK 停止传输脉冲,所有挂载外设全部停摆。但休眠只是暂时的省电手段,还得靠外部中断唤醒——此时 FCLK 继续运行,为中断采样提供时钟。在 CubeMX 上看它来自 HCLK,只是表示两者同步、频率一致,真正的来源是 AHB 预分频器。
- Flash 编程接口时钟:永远来自 HSI。
- USB 时钟:来自 PLL(48MHz)。
- CSS(时钟安全系统):当系统时钟源选择 HSE(或经过 PLL 倍频的 HSE)时,可以开启 CSS。它在 HSE 发生故障时立刻把时钟源切换回 HSI,并产生中断,让我们可以进行紧急制动等处理——相当于时钟的”安全气囊”。
- RTC 与看门狗:一个独立的小时钟树,时钟源是 LSI(低速内部时钟)/ LSE(低速外部时钟)/ HSE 的 128 分频。结构非常简单,等学到 RTC 和看门狗时再详细了解。
- MCO(时钟输出):在 CubeMX 勾选 Master Clock Output 后,PA8 引脚被设置为 MCO 功能,可以把某个时钟信号从引脚输出出来,方便外部测量/调试。一般用不上。
至此,时钟树全部讲解完成。
解读初始化代码:SystemClock_Config
说了这么多,回到每篇博客都有的 SystemClock_Config()——它就是把上面的时钟树用代码配置一遍。以温湿度计项目为例,逐行解析:
1 | void SystemClock_Config(void) |
各配置项与时钟树的对应关系
| 代码配置 | 对应时钟树节点 | 本项目的值 |
|---|---|---|
HSEState = RCC_HSE_ON |
打开 HSE 晶振(RCC_CR.HSEON) | 开启 |
HSEPredivValue = DIV1 |
HSE 预分频(RCC_CFGR.PLLXTPRE) | ÷1 → 8MHz |
PLL.PLLSource = PLLSOURCE_HSE |
PLL 输入源选择(RCC_CFGR.PLLSRC) | HSE |
PLL.PLLMUL = MUL9 |
PLL 倍频(RCC_CFGR.PLLMUL) | ×9 → 72MHz |
SYSCLKSource = PLLCLK |
SYSCLK 三选一(RCC_CFGR.SW) | PLLCLK |
AHBCLKDivider = DIV1 |
AHB 预分频(RCC_CFGR.HPRE) | ÷1 → HCLK 72MHz |
APB1CLKDivider = DIV2 |
APB1 预分频(RCC_CFGR.PPRE1) | ÷2 → PCLK1 36MHz |
APB2CLKDivider = DIV1 |
APB2 预分频(RCC_CFGR.PPRE2) | ÷1 → PCLK2 72MHz |
FLASH_LATENCY_2 |
Flash 等待周期(FLASH_ACR.LATENCY) | 2 个等待周期 |
两个值得注意的点:
HSI 为什么要保持开启(
RCC_HSI_ON)? 即使不用它做系统时钟,HSI 也是 Flash 编程接口时钟的唯一来源,并且是 CSS 时钟安全系统切换的备用时钟——所以不能关。FLASH_LATENCY_2是什么? Flash 读数据比 CPU 运算慢。CPU 频率越高,等待周期就越多:72MHz 需要 2 个等待周期(每读一次 Flash 等 2 个周期)。这也是 HAL_RCC_ClockConfig 的第二个参数,CubeMX 会根据 HCLK 自动算好。
HAL 层与寄存器的关系
整个 SystemClock_Config 是 HAL 封装层,最终写入的是 RCC 外设的寄存器:
HAL_RCC_OscConfig()→ 写 RCC_CR(HSEON/HSERDY、PLLON/PLLRDY)和 RCC_CFGR(PLLSRC、PLLMUL)HAL_RCC_ClockConfig()→ 写 RCC_CFGR(SW/SWS、HPRE、PPRE1、PPRE2)和 FLASH_ACR(LATENCY)
这也再次印证博客开头的观点:每个外设运行前,都得先让这颗时钟树”枝繁叶茂”。
实操步骤:CubeMX 配置 72MHz
说了这么多理论,实操其实非常简单——CubeMX 图形化界面就是替我们操作上面的时钟树。正确流程:
① 在 RCC 配置页打开 HSE
1 | System Core → RCC → High Speed Clock (HSE) → Crystal/Ceramic Resonator |
② 在 Clock Configuration 页把 HCLK 改为 72MHz
1 | Clock Configuration 页面 → HCLK 输入 72 → 回车 |
配置完成后,对照 Clock Configuration 页面应该能看到与我们代码一致的参数:
1 | HSE 8MHz ──÷1──► PLL ×9 ──► SYSCLK 72MHz ──÷1──► HCLK 72MHz |
💡 顺序提醒:务必先开 HSE,再改 HCLK。如果没开 HSE 就改 72MHz,CubeMX 会弹窗提示只能配到 64MHz(原因见下节踩坑记录)。
③ 生成代码,SystemClock_Config() 会自动出现在 main.c 里——就是上面解读的那段代码。
踩坑记录:72MHz 弹窗 “no solution found” 的真相
刚接触这块时很容易遇到这个弹窗:
改 HCLK=72MHz 时弹出 “no solution found using 72MHz, do you want to use 64MHz?”
原因:没开 HSE 时,系统只能用 HSI(8MHz)作为 PLL 输入。HSI 经过固定的 ÷2 变成 4MHz,PLL 最大 ×16 → 4 × 16 = 64MHz,这就是 HSI 路线的极限,再怎么配也到不了 72MHz。
解决:回到 RCC 配置页打开 HSE(Crystal/Ceramic Resonator),HSE 8MHz ÷1 ×9 = 72MHz,完美。这也是上一节”先开 HSE,再改 HCLK”的原因。
这个坑的通用价值在于:理解时钟树各节点的频率上下限,就能看懂 CubeMX 的报错。HSI 只能跑到 64MHz、PCLK1 最大 36MHz、APB2 定时器是 72MHz……这些限制全部写在这棵树上。
总结
| 概念 | 一句话 |
|---|---|
| 时钟信号 | 一定频率的方波,让门电路稳定后才锁存进寄存器,全芯片的”节拍器” |
| 时钟源 | 心脏:HSI(内部 8MHz)/ HSE(外部晶振 8MHz,精度高) |
| PLL | 倍频器:×2~×16,HSE ÷1 ×9 = 72MHz |
| SYSCLK | 系统时钟:HSI / HSE / PLLCLK 三选一 |
| AHB / HCLK | 主干道:CPU、内存、DMA 的时钟线 |
| APB1 / PCLK1 | 外设低速总线:÷2 = 36MHz(定时器自动倍频回 72MHz) |
| APB2 / PCLK2 | 外设高速总线:÷1 = 72MHz(GPIO、串口1、ADC、TIM1/TIM8) |
| SysTick | 内核定时器:HCLK 分频(÷1/÷8),HAL_Delay 的基准 |
| CSS | 时钟安全系统:HSE 故障自动切回 HSI + 中断 |
| FCLK | 自由运行时钟:停止模式时继续跑,供中断唤醒 |
这一集涉及的概念很多,有不少是后面才学到的(定时器、RTC、看门狗……)。但就像 keysking 说的”常看常新”——现在把时钟树这张地图装进脑子里,后面每学一个外设,都能在树上找到它的位置。
下一集就要进入定时器的学习了,有了时钟树的知识打底,定时器的分频/计数原理会好理解很多——因为定时器的 72MHz 时基,正是从 APB1/APB2 这条分支上摘下来的。
参考资源
- 视频:【STM32】超清晰STM32时钟树动画讲解(keysking)
- STM32 中文参考手册:第 25 页”系统结构图”(APB1/APB2 外设挂载清单)
- 波特律动文档站:https://docs.baud-dance.com/docs/stm32/intro
- 上一篇:STM32番外篇——oled温湿度计


