0%

STM32——浅析时钟源与时钟树理论知识

开篇

STM32 为什么能”有条不紊”地工作?为什么芯片内部的电路不会乱套?

在每一篇博客的代码里,都有这么一行永远存在的代码,之前没有深究:

1
SystemClock_Config();

这行代码在配置什么?为什么 GPIO、I2C、串口能正常工作的前提,都是先执行它?答案就是本期的主角——时钟源与时钟树,单片机的”心脏与动脉”。

这一集是纯理论讲解,不写任何新代码。但它是理解后续所有外设(定时器、ADC、DMA……)的地基——因为每个外设的时钟频率,都是从这颗时钟树上”摘”下来的。

为什么单片机需要”心跳”

微观视角:门电路的”运算延时”

芯片内部是庞杂的逻辑电路组成的,我们假设 STM32 内部有这样一套电路:一个与门 + 一个异或门,异或门的输出连接到了某个寄存器的某一位:

1
2
3
4
A ──┬─────┐
│ 与门 ├── 与门输出 ──┐
B ──┴─────┘ ├─► 异或门 ──► 寄存器某一位 ──► 程序可读到
B ───────────────────────┘
  • 与门:A 和 B 都是高电平 1 时输出 1,其他情况输出 0
  • 异或门:两个输入不一致时输出 1,一致时输出 0

寄存器是”程序与电路之间的桥梁”——程序读寄存器获取电路状态,写寄存器控制电路运行。

假设当前 A、B 都是高电平:与门输出 1,异或门两输入都是 1 → 输出 0,寄存器存储 0。

现在 B 突然从高电平变为低电平(比如某个 GPIO 输入的电平变了),理想情况下:与门输出变 0,异或门两输入都变 0 → 输出还是 0,寄存器安然无恙。

但实际情况有出入:

1
2
3
4
5
6
7
8
9
10
11
12
13
B 电平变化瞬间到达异或门输入端
│
▼
与门还在"运算"中!(MOS 管充放电需要时间,虽然对人类是瞬间,但相对光速是"龟速")
│
▼
异或门看到的是:A=1(与门还没算完,输出还是 1)、B=0
│
▼
异或门两输入不一致 → 输出 1 → 寄存器短暂读到错误值 1!
│
▼
与门运算完成,输出 0,异或门恢复输出 0,寄存器恢复正确值

寄存器中出现了一段由门电路运算延时产生的错误状态——这段错误虽然短暂,但可能导致依赖此寄存器的程序、其他逻辑电路出现更严重的连锁错误。芯片内部电路比这个例子复杂成千上万倍,每个信号经过的门电路数量不一、错综复杂,如果放任不管,芯片内部电信号将乱成一锅粥。

解决办法:边沿触发器 + 方波

解决方法其实很简单——在寄存器前放一个边沿触发器:

1
2
3
4
5
D端(数据输入)                    Q端(数据输出)
│ │
└────► 边沿触发器 ◄──────┘
│
控制端 ◄──── 一定频率的方波(时钟信号)

边沿触发器的特性:当控制端出现由低到高的上升沿时,输出端才采样 D 端的值,其他时候输出保持不变。

于是,只要给控制端接入一个一定频率的方波信号:

  1. 方波一个周期内,门电路运算产生的任何”毛刺”都无法进入寄存器
  2. 等一切运算都稳定后,方波的上升沿到达,把最终稳定结果锁存进寄存器

这个好似脉搏的方波信号,就是时钟信号。它在单片机中由”心脏”——时钟源产生,通过”动脉”——时钟树传播到整个芯片。这也是”单片机离开了时钟源就无法存活”的原因。

💡 这就是数字电路里的经典概念:同步时序逻辑。时钟就是全芯片的”节拍器”,让所有电路在同一个节拍下协同工作,避免异步混乱。还记得 STM32 – I2C 中断与 DMA 里讲的”同步/异步”吗?硬件层面的同步,就是靠这根时钟线实现的。

宏观视角:外设也需要时钟

除了微观层面,宏观的外设层面同样需要精确的时钟脉冲:

  • 串口需要固定的波特率进行通信
  • 定时器需要计时,得知道时间流逝了多少
  • ADC 需要采样时序

这些都需要时钟脉冲告诉外设”时间在流逝”。

数据流全链路:时钟从晶振走到外设

理解了”为什么需要时钟”,我们来看时钟是怎么从源头一步步传到每个外设的。这就是数据流全链路,也是本篇的核心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
                 ┌─────────┐
HSE 晶振 8MHz ──►│ 预分频器 │──┐ 8MHz
│ ÷1 / ÷2 │ │
└─────────┘ │ ┌─────────┐ ×9 = 72MHz
┌─────────┐ └──────►│ PLL锁相环 │──────────┐
HSI 内部 8MHz ──►│ 固定 ÷2 │────────►│ ×2~×16 │ │
└─────────┘ └─────────┘ ▼
┌─────────────── SYSCLK 系统时钟 72MHz
│ (三选一: HSI / HSE / PLLCLK)
▼
┌──────────────────────┐
│ AHB 预分频器 ÷1 │ ← AHB = 先进高性能总线
└──────────────────────┘
▼
┌────────────────────────┐
│ HCLK = 72MHz │
│ 连接: CPU / 内存 / DMA │
└───┬──────────┬──────┬───┘
│ │ │
APB1 预分频 ÷2 APB2 预分频 ÷1
│ │ │
▼ │ └──────► SysTick 系统滴答
PCLK1 = 36MHz │ (自带分频器 ÷1 / ÷8)
(除定时器外APB1外设) │ HAL_Delay 的时间基准!
│ │
定时器倍频器 ×2 │
│ │
▼ ▼
APB1 定时器 = 72MHz PCLK2 = 72MHz
(TIM2~5等) (GPIO/串口1/ADC等)
│
定时器倍频器 ×1
│
▼
APB2 定时器 = 72MHz
(高级定时器 TIM1/TIM8)

一句话概括这条链路: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 万次运算。

image-20260807164251762

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

image-20260807164447487

定时器的”特权”:倍频器

定时器比较特殊——它前面多了一个倍频器,用来把频率”再提上去”。这个倍频器有 ×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 系统时钟。image-20260807164701768

⚠️ 注意:别把 SYSCLK(系统时钟) 和前面说的 SysTick(系统滴答) 搞混了!SysTick 是内核里的一个定时器,SYSCLK 是时钟树顶端的时钟源,只是名字像。

8MHz 经过分频器只能变小,怎么也不可能是 72MHz——所以 SYSCLK 还有第三个选项:PLLCLK。

PLL:锁相环(倍频器)

PLL 锁相环的具体原理很深,我们不去深究,只需要知道:它在此处起到倍频器的作用。image-20260807164859963

1
2
3
PLL 输入前分频:  HSI → 固定 ÷2 (4MHz)
HSE → 可选择 ÷1 或 ÷2
PLL 倍频: ×2 ~ ×16 (可选)

于是: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0}; // 振荡器配置结构体
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 总线时钟配置结构体

/* ① 配置振荡器:选哪个时钟源、开不开 PLL */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; // 要配置的振荡器类型:HSE
RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 打开 HSE(外部晶振)
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; // HSE 预分频 ÷1 → 8MHz
RCC_OscInitStruct.HSIState = RCC_HSI_ON; // HSI 保持开启(备用时钟源)
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开 PLL
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // PLL 的输入源 = HSE
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // PLL 倍频 ×9 → 8×9=72MHz
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) // 写入 RCC 寄存器
{
Error_Handler();
}

/* ② 配置总线时钟:SYSCLK 选谁、各总线分频多少 */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 要配置的时钟种类
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // SYSCLK 来源 = PLLCLK
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB 预分频 ÷1 → HCLK = 72MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1 ÷2 → PCLK1 = 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2 ÷1 → PCLK2 = 72MHz

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) // 写入寄存器
{
Error_Handler();
}
}

各配置项与时钟树的对应关系

代码配置 对应时钟树节点 本项目的值
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 个等待周期

两个值得注意的点:

  1. HSI 为什么要保持开启(RCC_HSI_ON)? 即使不用它做系统时钟,HSI 也是 Flash 编程接口时钟的唯一来源,并且是 CSS 时钟安全系统切换的备用时钟——所以不能关。

  2. 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

image-20260806105659838

② 在 Clock Configuration 页把 HCLK 改为 72MHz

1
2
Clock Configuration 页面 → HCLK 输入 72 → 回车
CubeMX 自动帮我们算好所有预分频和倍频参数(弹出对话框直接点 OK)

image-20260806105812668

配置完成后,对照 Clock Configuration 页面应该能看到与我们代码一致的参数:

1
2
3
4
HSE 8MHz ──÷1──► PLL ×9 ──► SYSCLK 72MHz ──÷1──► HCLK 72MHz
│ ├──÷2──► PCLK1 36MHz(定时器自动 ×2 → 72MHz)
│ └──÷1──► PCLK2 72MHz(定时器 ×1)
└──► APB2 定时器 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 这条分支上摘下来的。

参考资源