**摘要:**本文从"为什么用外设前要先开时钟"这一疑问出发,系统讲解 STM32F103 的时钟体系。先厘清时钟的本质------它是催促 CPU 和所有片上外设工作的"节拍器";再拆解时钟树的四个原始时钟源(HSI/HSE/LSI/LSE)与三类基本元件(时钟源、倍频/分频器、选择器/使能开关);随后梳理主干六级时钟(PLLCLK、SYSCLK、FCLK、HCLK、PCLK1、PCLK2)的作用与频率,并介绍 CSS 时钟安全系统、低速时钟(RTC、独立看门狗)与 MCO 时钟输出。最后结合 RCC 的十个寄存器,逐行拆解 SystemClock_Config() 源码,讲清"配置时钟源 = 写寄存器、配置分频 = 改 CFGR"的底层逻辑,帮助读者真正打通时钟树、总线与外设之间的关系。
开发板:STM32F103ZET6
工具链:Keil MDK + VSCode + STM32CubeMX + HAL 库
上一篇:嵌入式入门(二)GPIO 深度解析
上一篇点灯时,我们"照抄"了这么一句:
cpp
__HAL_RCC_GPIOF_CLK_ENABLE(); // 使能 GPIOF 时钟
当时只知道"用外设前要先开时钟",但为什么必须开?时钟到底是什么?72MHz 到底是怎么来的? 这些问题悬在那里,导致后面的 USART、I2C、SPI 全都会"知其然不知其所以然"。
这一篇把整层地基挖开:从"时钟为什么存在"一路讲到时钟树结构,最后落到 SystemClock_Config() 的源码逐行拆解。
1:时钟:所有设备都是"被驱动"的
1.1 CPU也要被催促
你有没有想过:CPU 能从 Flash 读指令并执行,可谁让它去读的?又是谁让它一直读下去的?
警察能抓坏人(CPU 能执行指令),但是谁让警察去抓坏人的?
CPU 很聪明------能取指令、分析指令、执行指令;CPU 也很傻------它必须在别人的催促下 才能完成这些工作。这个"催促"的硬件机制,就叫时钟。
时钟像一台节拍器,以固定频率发出电信号,催促目标设备工作。
1.2 片上外设也是如此
MCU 内部除了 CPU,还有各种片上外设(GPIO、USART、I2C......),它们自己能主动工作吗?不能。
核心结论:硬件层面上,所有设备(CPU 和片上外设)都要在时钟推动下,统一步调地工作。
1.3 时钟信号与时钟源
产生高低电平时序的源头硬件叫时钟源 ;这串高低电平的时序就叫时钟信号 ,画出来就是方波 。有的设备在上升沿 工作,有的在下降沿工作。
易混概念:时钟源 是"产生或提供时钟的硬件实体 ";时钟信号 是"这个实体输出到电路中的电信号波形"。一个是东西,一个是波形。
1.4 时钟周期与频率
时钟周期 :一次完整波动所需的时间(起始状态 → 变化 → 回到起始状态)。注意------周期是时间单位。
一个周期必然有且只有:一个上升沿 + 一个下降沿。
时钟频率 :每秒输出的完整周期数,单位赫兹(Hz)。
一个周期耗时 1ms → 1 秒输出 1s/1ms = 1000 个周期 → 频率 = 1000Hz
1.5 使能开关的由来:一段设计者的心理活动
我是单片机的设计者。我知道芯片上有很多设备,但它们不一定被同时使用。如果开发者用不到的设备还在时钟催促下做无效功,不就是白白耗电?功耗高 → 使用成本高 → 我的芯片就没竞争力了。可我又要满足"用户能自由选择用哪些外设"------既要又要,怎么办?
答案:给每个设备受理时钟的能力加一个使能开关。于是:
- 使用任何外设前,先使能它的时钟------本质是开启该外设对时钟的"受理";
- CPU 例外,它要一直执行代码,所以一般不设使能。
核心结论:这就是为什么用任何一个片上外设之前,都要先开启它的时钟。
1.6 倍频和分频:一个源头驱动不同的频率
不同外设的工作频率并不相同(受硬件工艺、功耗散热、通信协议、精度要求影响)。为什么不能"给每个外设配一个时钟源"?------不现实,功耗高,设备间同步还是大问题。
于是几乎所有单片机都用同一个做法------倍频 + 分频:
| 操作 | 定义 | 典型电路 |
|---|---|---|
| 倍频 | 输入 1 个周期,输出 n 个周期(升频) | PLL 锁相环(Phase-Locked Loop) |
| 分频 | 输入 n 个周期,输出 1 个周期(降频) | 分频器(Frequency Divider) |
时钟的本质 :时钟是决策"片上外设什么时候工作"的设备,是发起者 ;其他外设是执行者。CPU 在"要被时钟驱动"这一点上,和片上外设并无本质区别。
扩展:PLL 锁相环为什么能倍频?
PLL 的核心是一个压控振荡器(VCO) ------它的输出频率能被输入电压控制。PLL 把 VCO 的输出用分频器降频后,与输入参考时钟做相位比较,根据误差反馈去调整 VCO 频率,最终锁定在"输入频率 × 倍频系数"上。就像一个闭环的"自动追频"系统:你让它输出 9 倍频率,它就自己调节直到稳定在 9 倍。这也是为什么 PLL 上电后需要"等待就绪"------锁定是需要时间的。
1.7 时钟源是真实存在的硬件
不是抽象概念,开发板上就焊着:
| 类型 | 实物 | 典型型号 |
|---|---|---|
| 8MHz 晶振 | 外部高速晶振 | X50328MSB2GI |
| 32.768kHz 晶振 | 外部低速晶振 | Q13FC13500004 |
2:时钟树:把时钟功能独立成一套硬件
2.1 像硬件工程师一样思考
继续站在设计者角度推理:
- 问题一 :如果只有一个时钟源,万一它因极端情况(高温、线路老化)停摆,整个系统不就瘫痪了?→ 所以不能只有一个,要冗余备份。
- 问题二 :既然可以有多个时钟源,那给每个外设都配一个 行不行?→ 不行。每个时钟都要分频、倍频、使能,MCU 内部会出现大量重复电路设计,重复意味着成本增加。MCU 是成本敏感的设备,此路不通。
所以答案:把时钟功能单独设计成一套硬件电路 ,黑盒式地对外提供各种频率的时钟------这就是时钟树 。把扇出图逆置过来,正好是一棵树,时钟源就是树根。
扩展:为什么世界的组织结构主流是树?
人要协作,就得先把人组织起来,而主流组织结构就是树形 。因为人做事只有两件事------决策 和执行 :决策要集中(只有一个根),执行要分散(叶子多)。树形结构通过调整分支个数和层高,让"从根到每个叶子的通知路径"最短。时钟树"长成树"也是同一个道理:一个根(时钟源),靠分频倍频不断分叉,把不同频率的时钟高效分发到每一个外设。
2.2 四个原始时钟来源
时钟树的树根不是一个,而是四个 ,命名是一套二维矩阵------高速/低速 × 内部/外部:
| 简称 | 全称 | 来源 | 典型频率 |
|---|---|---|---|
| HSI | High-Speed Internal | 内部 RC 振荡器 | 8 MHz |
| HSE | High-Speed External | 外部晶振 | 4~16 MHz(板子用 8MHz) |
| LSI | Low-Speed Internal | 内部 RC 振荡器 | 40 kHz |
| LSE | Low-Speed External | 外部 32.768kHz 晶振 | 32.768 kHz |
| 内部 | 外部 | |
|---|---|---|
| 高速 | HSI | HSE |
| 低速 | LSI | LSE |
记住这个二维表,四个时钟源就永远不会记混。
扩展:RC 振荡器 vs 晶振,精度差在哪?
HSI/LSI 是片内 RC 振荡器 ,靠电阻电容充放电定时,受温度、工艺、电压影响大,精度只有 1% 左右 (所以 HSI 还带一个
HSICalibrationValue校准值可微调);而 HSE/LSE 是石英晶振 ,靠晶体的压电效应谐振,精度可到 百万分之几十(ppm 级) 。这就是为什么"最佳实践"是拿 HSE 当系统主时钟源------精度高,通信波特率、定时才准。

2.3 时钟树只有三类基本元件
那张极其复杂的时钟树图,拆开后其实只有三种东西:
- 时钟源------树根,四个原始来源;
- 倍频/分频器 ------改频率。有的系数固定(如
/2),有的支持软件动态配置(如/1,2,4,5,16);倍频器主要是 PLL; - 选择器(复用器)+ 使能开关------圆圈画的是选择器(从多个输入信号里选一个),方框画的是使能开关。
整棵树的核心主干就是这样一条链路:
HSE (8MHz) ┐ ┌──÷1──→ HCLK 72MHz → AHB:内核/内存/DMA
├→[PLL ×9]→ SYSCLK ──┤
HSI (8MHz) ┘ 72MHz 72MHz ├──÷1──→ PCLK2 72MHz → APB2:GPIO/USART1/TIM1
└──÷2──→ PCLK1 36MHz → APB1:I2C/USART2/TIM2
3:主干解析:六级时钟
3.1 先解决时钟源的称呼问题
这个词在软硬件两个视角下含义不同:
- 狭义(硬件工程师) :只有 HSE、HSI、LSE、LSI 这四个硬件实体才叫时钟源;
- 广义(软件工程师) :由它们分频倍频形成的 PLLCLK、SYSCLK、FCLK、HCLK、PCLK1、PCLK2 也常被叫"时钟源"------因为从下游设备看,它们就是"时钟从哪里来"。
结合语境理解即可。
3.2 六级时钟的作用与频率
| 时钟 | 全称 | 核心作用 | 最高频率 |
|---|---|---|---|
| PLLCLK | Phase-Locked Loop Clock | 倍频输出,让系统可达最高速 | 72 MHz |
| SYSCLK | System Clock | 芯片根时钟,所有总线的源头 | 72 MHz |
| FCLK | Free-running Clock | CPU 内核自由运行时钟 | 72 MHz |
| HCLK | AHB Clock | AHB 总线、内存、DMA 时钟 | 72 MHz |
| PCLK1 | APB1 Clock | APB1 外设(USART2/3、I²C、SPI2、TIM2~4) | 36 MHz |
| PCLK2 | APB2 Clock | APB2 外设(GPIO、USART1、SPI1、TIM1、ADC) | 72 MHz |

最容易记错的一点:APB1 最高只能 36MHz,APB2 才能跑满 72MHz。 以后发现某个外设"最高频率"算不对,十有八九是把 APB1 当成 72MHz 了。
3.3 CSS时钟安全系统
回到"冗余"的推测:主干部分的原始时钟来源其实有两个------HSI 和 HSE,一内一外,互为备份。
- STM32F103 默认用 HSI 起步(不依赖外部器件,上电就能跑);
- 最佳实践用 HSE(精度高);
- HSE 异常时,**CSS(Clock Security System,时钟安全系统)**接管。
CSS 的两条行为,一条重要、一条常被误解:
| 情形 | 硬件行为 |
|---|---|
| HSE 故障 | 立即关闭 HSE 和 PLL,把 SYSCLK 强制切到 HSI(8MHz) ------全过程硬件完成,软件来不及干预,但产生 NMI 中断供补救 |
| HSI 故障 | 不会自动切到 HSE。HSI 是内部 RC,规格书没有"HSI 失效检测 + 自动切 HSE"的机制,一旦停振只能死机或复位 |
核心结论:只有 HSE 故障会自动降级到 HSI,反向不行;想反向保护得自己写软件监控。 在工业、汽车等高可靠场景,HSE 停振会让 PLL/SYSCLK 立刻丢失、CPU/外设/通信全停,CSS 能在毫秒级切到内部 HSI,至少让 MCU 有机会上报故障或安全停机。
4:低速时钟与时钟输出
4.1 RTC与VBAT
RTC(Real-Time Clock,实时时钟) :核心功能是在系统断电、重启或休眠时,仍持续准确记录年月日时分秒。典型场景是高速公路 ETC 门架计费------过车瞬间要记录毫秒级时间戳,断电丢了时间就会漏扣、对账失败。
它靠 VBAT 引脚 (Voltage Battery,电池电压引脚)外接一颗纽扣电池实现:正常供电时电池不工作,一旦 VDD 掉电,电池顶上维持计时。RTC 的本质就是一个递增计数器(记录 MCU 启动到现在过了多少秒)。
4.2 独立看门狗与"喂狗"
独立看门狗(IWDG) :MCU 里的"自动重启按钮"。程序跑飞死机时,自动把板子重启。本质是一个递减计数器 ,减到 0 就重启;你在循环里不断重置它,这个动作叫**"喂狗"**------一直喂就永远减不到 0,一旦跑飞不再喂,它就减到 0 替你重启。
两者都是秒/毫秒级更新的计数器,kHz 级时钟就够,所以走 LSE/LSI 低速线。最佳实践:
- RTC :首选 LSE(32.768kHz),掉电也能走;备选 LSI(≈40kHz)或 HSE/128;
- IWDG :只能用 LSI,独立运行,不受外部晶振影响。
扩展:还有一个"窗口看门狗(WWDG)",和 IWDG 的区别是------它不仅有"下限"(不能减到 0),还有"上限"(喂狗不能太早),喂太早或太晚都会复位。IWDG 管"程序彻底卡死",WWDG 管"程序时序跑偏"。这些留到看门狗专题再展开。
4.3 为什么是32.768kHz
因为 32768 = 2¹⁵。RTC 内部有默认分频系数 32768,一除正好得到 1Hz(稳定的 1 秒)。这就是电子世界里这个"奇怪数字"的由来。
如果你硬要用 8MHz 的 HSE 当 RTC 时钟源,就得自己算预分频值:
8MHz = 8,000,000 Hz
分频系数总数 = 8,000,000 ÷ 128 ÷ 1 = 62,500
4.4 MCO时钟输出
MCO(Microcontroller Clock Output) :把 MCU 内部选中的时钟信号(HSI/HSE/PLLCLK 等)引到某个 GPIO 引脚对外输出。两个用途:
- 给外围器件直接供时钟(同步设备、省一颗晶振的成本);
- 当测试接口用------外接示波器看波形,验证配置是否正确。
5:时钟树+总线+外设如何打通
5.1 总线不是一根线
总线是一簇线的集合,内部一般分三类:
| 总线 | 回答什么问题 |
|---|---|
| 地址总线 | 去哪里?(读:从哪读;写:写到哪) |
| 数据总线 | 具体数据是什么? |
| 控制总线 | 什么时候读写、方向、是否完毕等控制逻辑 |
注意:时钟线(CLK)和读写方向线,都属于控制总线内部。不管 ICode、DCode、System、AHB、APB,硬件本质都一样,只是挂的设备不同。
5.2 外设也有寄存器
寄存器不只 CPU 有------外设里也有 (上一章 GPIO 的 CRL/CRH/IDR/ODR 就是)。大体分三类:数据寄存器、控制/配置寄存器、状态寄存器。
5.3 时钟线怎么接入外设
把总线结构图和时钟树合并起来看:每个片上外设前面都有一个开关 ,这个开关就来自时钟树的"使能开关"。你未来用代码使能的,就是这个开关。
5.4 寄存器如何存住1个bit
一个存储单元要成立,必须有三种功能:
- 写入;
- 读取(读完数据还在,相当于拷贝);
- 保持(下次写入到来之前,值不变,即锁存)。
"保持"靠一个内部开关:写完就断开,直到下次闭合才接受新数据。开关由电平/边沿控制(上升沿触发、下降沿触发、高电平触发、低电平触发)。
所以写入成立的两个条件 :① 写端有数据;② 使能端来一个上升沿(闭合开关)。
把 8 个存储单元的开关接到同一个使能位上------恭喜,你亲手做出了一个 8 位寄存器(32 位同理)。
5.5 时钟与数据读写的同步
CPU 操作片上外设的完整过程:
一个时钟到来 → CPU 把数据放到数据总线 ,同时把目标地址 、读写方向 放到对应总线位置 → 与此同时(不考虑分频时),外设寄存器的开关也被使能打开 → 数据从 CPU → 总线 → 外设寄存器。
核心结论:时钟线上的时钟,让"把数据放到总线"和"开启目标设备寄存器"这两件事同步进行。 而这里的"同步"不一定是"同时"------因为有分频,这里的同步是**"步调有规律"**。
6:RCC时钟树的配置面板
6.1 RCC是什么
前面讲的全是时钟树本身 ,可它的选择器、倍频/分频系数、使能开关都需要被配置 。配置在哪做?RCC。
RCC = Reset and Clock Controller (复位与时钟控制器)。配置的本质,就是配置寄存器。
6.2 与门:使能开关的硬件本质
关键在与门:
| 输入 A | 输入 B | 输出 |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 0 | 0 | 0 |
看出来了吗------A=1 时,输出等于 B;A=0 时,B 完全无效。这不就是开关吗!
再把一个存储单元接到 A 端:
- 存 1 → 持续输出 1 → 与门开关闭合;
- 存 0 → 持续输出 0 → 与门开关断开。
寄存器的输出连线接到各外设的 enable 输入。所以配置寄存器 = 使能外设,本质就这么简单。
6.3 RCC的十个寄存器
| 序号 | 寄存器 | 一句话记忆 |
|---|---|---|
| 1 | RCC_CR | 开/关全部时钟源(HSI/HSE/PLL/LSI/LSE)及就绪标志 |
| 2 | RCC_CFGR | 选时钟源、设分频倍频、MCO 输出 |
| 3 | RCC_CIR | 时钟就绪/失钟中断使能与标志 |
| 4 | RCC_APB2RSTR | 一键复位挂在 APB2 上的高速外设 |
| 5 | RCC_APB1RSTR | 一键复位挂在 APB1 上的低速外设 |
| 6 | RCC_AHBENR | 开/关 DMA、SRAM、CRC、FSMC、SDIO 等 AHB 外设时钟 |
| 7 | RCC_APB2ENR | 开/关 GPIO、TIM1、ADC1/2、USART1、SPI1 等 APB2 外设时钟 |
| 8 | RCC_APB1ENR | 开/关 TIM2~7、USART2/3、I²C1/2、USB、CAN 等 APB1 外设时钟 |
| 9 | RCC_BDCR | 控制 RTC、LSE 及备份域复位 |
| 10 | RCC_CSR | 读"谁复位了我"、清标志、控制 LSI 开关 |
现在回头看就通了:
__HAL_RCC_GPIOF_CLK_ENABLE()就是往RCC->APB2ENR对应位写 1------GPIO 挂在 APB2 上,所以用 APB2ENR。这也解释了为什么 GPIO 的时钟是 PCLK2(72MHz)。
7:实战:时钟配置与源码解析
7.1测试方案:用CPU死循环"称重"时钟
怎么直观感受时钟频率的影响?这次不用 HAL_Delay(),改用 CPU 空转:
cpp
void CPU_Delay(volatile uint32_t delay)
{
while(delay--); // 让 CPU 疯狂死循环
}
while (1)
{
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8);
CPU_Delay(XXXXX); // 空转次数留给你自己算
}
原理:CPU 执行速度由 FCLK 决定。时钟调高 → 同样的空转代码跑完更快 → 灯闪得更快;调低 → 更慢。用肉眼看到时钟配置对系统的影响。
在 72MHz 下,估算
while(delay--);一行需要多少个时钟周期(把从 Flash 取指令也算进去),以及要让 CPU 空转约 200ms 需要执行多少次。这个留给你推------提示:注意 Flash 等待周期(见 7.4 的 FLASH_LATENCY)会拖慢取指速度。
7.2 RCC的物理地址和结构体
RCC 本质上也是一个片上外设,同样遵循统一编址。它的地址空间是 0x4002 1000 ~ 0x4002 13FF,大小 1KB:
cpp
#define RCC ((RCC_TypeDef *)RCC_BASE)
#define RCC_BASE (AHBPERIPH_BASE + 0x00001000UL)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000UL)
#define PERIPH_BASE 0x40000000UL /*!< 外设基地址 */
typedef struct
{
__IO uint32_t CR;
__IO uint32_t CFGR;
__IO uint32_t CIR;
__IO uint32_t APB2RSTR;
__IO uint32_t APB1RSTR;
__IO uint32_t AHBENR;
__IO uint32_t APB2ENR;
__IO uint32_t APB1ENR;
__IO uint32_t BDCR;
__IO uint32_t CSR;
} RCC_TypeDef;
结构体成员的排布和寄存器偏移一一对应------这和上一章 GPIO_TypeDef 是同一个套路。
7.3 __HAL_RCC_GPIOF_CLK_ENABLE 拆解
cpp
#define __HAL_RCC_GPIOF_CLK_ENABLE() do { \
__IO uint32_t tmpreg; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPFEN); \
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPFEN); \
UNUSED(tmpreg); \
} while(0U)
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPFEN):给 APB2ENR 的 GPIOF 位写 1,开启 GPIOF 的时钟;- 后面那句
READ_BIT(...)+UNUSED(tmpreg)是一次哑读------给外设一点时间让时钟稳定,是 HAL 库的惯例写法。
SET_BIT 的本质:
cpp
#define SET_BIT(REG, BIT) ((REG) |= (BIT))
而 RCC_APB2ENR_IOPFEN 最终指向某个具体的 bit 位(对应 APB2ENR 里的 GPIOF 使能位)。
7.4 SystemColck_Config:选择时钟源
CubeMX 里点几下时钟树,最终都会翻译成 SystemClock_Config()。它分两段------先配振荡器,再配分频:
cpp
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0}; // 振荡器配置结构体
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 时钟配置结构体
/* ---------- 第一段:配置振荡器(时钟源) ---------- */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; // 配置 HSE
RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 使能 HSE
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; // HSE 不分频
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; // 9 倍频:8MHz × 9 = 72MHz
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) // 写寄存器并等待就绪
{
Error_Handler();
}
/* ---------- 第二段:配置总线分频 ---------- */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 系统时钟 = PLL 输出
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB 不分频,HCLK = 72MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1 2 分频,PCLK1 = 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2 不分频,PCLK2 = 72MHz
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
扩展:FLASH_LATENCY_2 是什么?
当 SYSCLK 超过一定频率(STM32F103 约 48MHz 以上),Flash 的读取速度跟不上 CPU 取指速度,需要在访问 Flash 时插入等待周期(wait state) 。72MHz 时通常需要 2 个等待周期 (
FLASH_LATENCY_2)。这就是 7.1 思考题里"取指令要考虑 Flash 等待"的来源------它会实实在在拖慢 CPU 的执行速度。
调整时钟,让灯变慢------把 AHB 分频系数改大即可:
cpp
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV8; // AHB 8 分频:HCLK = 72/8 = 9MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // PCLK1 = 9/2 = 4.5MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // PCLK2 = 9MHz
HCLK 从 72MHz 降到 9MHz,CPU 慢下来,同样的 CPU_Delay 跑完花的时间变长,灯自然就变慢了。
7.5 HAL 库底层:配振荡器 = 写寄存器
HAL_RCC_OscConfig 底层就是一堆"写寄存器"的宏。以 HSE 为例:
cpp
#define __HAL_RCC_HSE_CONFIG(__STATE__) \
do { \
if ((__STATE__) == RCC_HSE_ON) \
{ \
SET_BIT(RCC->CR, RCC_CR_HSEON); \
} \
else if ((__STATE__) == RCC_HSE_OFF) \
{ \
CLEAR_BIT(RCC->CR, RCC_CR_HSEON); \
CLEAR_BIT(RCC->CR, RCC_CR_HSEBYP); \
} \
/* ...旁路模式等其他分支略 */ \
} while(0U)
#define RCC_HSE_ON RCC_CR_HSEON
#define RCC_CR_HSEON_Pos (16U)
#define RCC_CR_HSEON_Msk (0x1UL << RCC_CR_HSEON_Pos) /*!< 0x00010000 */
#define RCC_CR_HSEON RCC_CR_HSEON_Msk
注意 RCC_CR_HSEON 是 bit16 (0x00010000)------HSE 的使能开关就在 RCC_CR 的第 16 位。SET_BIT(RCC->CR, RCC_CR_HSEON) 就是把这一位置 1,HSE 开始起振。
LSE 同理,只是操作的是 RCC_BDCR:
cpp
#define __HAL_RCC_LSE_CONFIG(__STATE__) \
do { \
if ((__STATE__) == RCC_LSE_ON) \
{ \
SET_BIT(RCC->BDCR, RCC_BDCR_LSEON); \
} \
/* ... */ \
} while(0U)
#define RCC_BDCR_LSEON_Pos (0U)
#define RCC_BDCR_LSEON_Msk (0x1UL << RCC_BDCR_LSEON_Pos) /*!< 0x00000001 */
底层逻辑完全一致:配置时钟源 = 往对应寄存器的对应位写 1 或写 0。HSE 归 CR 管,LSE 归 BDCR 管------这和 6.3 节"RCC_CR 开/关全部时钟源、RCC_BDCR 管 LSE"完全对上了。
7.6 配置分频:修改CFGR
分频系数也落在 RCC_CFGR。以 APB1 分频为例:
cpp
#define RCC_HCLK_DIV2 RCC_CFGR_PPRE1_DIV2
#define RCC_CFGR_PPRE1_DIV2 0x00000400U /*!< HCLK divided by 2 */
HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency)
{
/* PCLK1 配置 */
if (((RCC_ClkInitStruct->ClockType) & RCC_CLOCKTYPE_PCLK1) == RCC_CLOCKTYPE_PCLK1)
{
MODIFY_REG(RCC->CFGR, RCC_CFGR_PPRE1, RCC_ClkInitStruct->APB1CLKDivider);
}
/* PCLK2 配置 */
if (((RCC_ClkInitStruct->ClockType) & RCC_CLOCKTYPE_PCLK2) == RCC_CLOCKTYPE_PCLK2)
{
MODIFY_REG(RCC->CFGR, RCC_CFGR_PPRE2, ((RCC_ClkInitStruct->APB2CLKDivider) << 3));
}
}
几个关键宏:
cpp
#define RCC_CFGR_PPRE1_Pos (8U)
#define RCC_CFGR_PPRE1_Msk (0x7UL << RCC_CFGR_PPRE1_Pos) /*!< 0x00000700 */
#define RCC_CFGR_PPRE1 RCC_CFGR_PPRE1_Msk /*!< PRE1[2:0] APB1 预分频 */
RCC_CFGR_PPRE1是 CFGR 的 bit8~10 三位(0x7 << 8),管 APB1 分频;MODIFY_REG是"读-改-写":先把目标三位清零,再写入新值,不影响其他位------和上一章 GPIO 的MODIFY_REG是同一个宏;- APB2 的分频位在 PPRE2(bit11~13),所以代码里
<< 3把它挪到正确位置。
7.7 关键结构体与宏(附录整理)
SystemClock_Config 用到的两个结构体:
cpp
typedef struct
{
uint32_t OscillatorType; // 要配置哪些振荡器(HSE/HSI/LSE/LSI)
uint32_t HSEState; // HSE 状态(ON/OFF/BYPASS)
uint32_t HSEPredivValue; // HSE 预分频(PREDIV1 / PLLXTPRE)
uint32_t LSEState; // LSE 状态
uint32_t HSIState; // HSI 状态
uint32_t HSICalibrationValue; // HSI 校准值(0x00~0x1F)
uint32_t LSIState; // LSI 状态
RCC_PLLInitTypeDef PLL; // PLL 参数
} RCC_OscInitTypeDef;
typedef struct
{
uint32_t PLLState; // PLL 开关
uint32_t PLLSource; // PLL 输入源(HSI/2 或 HSE)
uint32_t PLLMUL; // 倍频系数(2~16)
} RCC_PLLInitTypeDef;
typedef struct
{
uint32_t ClockType; // 配哪些时钟(SYSCLK/HCLK/PCLK1/PCLK2)
uint32_t SYSCLKSource; // 系统时钟源(HSI/HSE/PLLCLK)
uint32_t AHBCLKDivider; // AHB 分频
uint32_t APB1CLKDivider; // APB1 分频
uint32_t APB2CLKDivider; // APB2 分频
} RCC_ClkInitTypeDef;
PLL 倍频系数的可选值(2~16):
cpp
#define RCC_PLL_MUL2 RCC_CFGR_PLLMULL2
#define RCC_PLL_MUL3 RCC_CFGR_PLLMULL3
/* ... 一直到 ... */
#define RCC_PLL_MUL16 RCC_CFGR_PLLMULL16
再补一个附录里的完整示例(48MHz + MCO 输出),它和标准配置的区别就两处------PLLMUL 改成 6 倍频(8×6=48MHz),并加了 MCO 输出:
cpp
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL6; // 8 × 6 = 48MHz
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // 不分频,48MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
/* ... */
HAL_RCC_MCOConfig(RCC_MCO, RCC_MCO1SOURCE_SYSCLK, RCC_MCODIV_1); // MCO 输出 SYSCLK
8:总结
8.1 知识体系回顾
时钟
├── 本质:节拍器,催促 CPU 和所有片上外设工作
├── 周期 = 一个上升沿 + 一个下降沿(时间单位);频率 = 每秒周期数(Hz)
├── 使能开关:设计者为降功耗给每个外设加"时钟受理开关"(用外设先开时钟)
└── 倍频/分频:一个源头驱动不同频率设备(PLL 锁相环 / 分频器)
时钟树
├── 为什么要树:多时钟源冗余 + 避免重复电路
├── 四个原始来源:HSI / HSE(高速)、LSI / LSE(低速),内外 × 高低二维矩阵
├── 三类元件:时钟源 + 倍频分频器 + 选择器/使能开关
└── 三部分:主时钟树(重点)/ 低速时钟(RTC·看门狗)/ 时钟输出(MCO)
主干六级时钟
├── PLLCLK(72) → SYSCLK(72) → FCLK(72) / HCLK(72) → PCLK1(36) / PCLK2(72)
├── 广义 vs 狭义"时钟源"
└── CSS:HSE 故障自动切 HSI(单向,产生 NMI)
时钟树 × 总线 × 外设
├── 总线三类:地址 / 数据 / 控制(时钟线在控制总线内)
├── 寄存器存 bit:写入 + 读取 + 保持(锁存,靠使能上升沿)
└── 同步:时钟让"放数据到总线"和"开外设寄存器"步调一致(有规律,不一定是同时)
RCC
├── Reset and Clock Controller:配置 = 写寄存器
├── 使能开关本质 = 与门(A 端接寄存器输出)
└── 十个寄存器:CR / CFGR / CIR / APB2RSTR / APB1RSTR / AHBENR / APB2ENR / APB1ENR / BDCR / CSR
源码
├── RCC 地址:0x4002 1000(1KB),结构体映射
├── 开 GPIOF 时钟 = SET_BIT(RCC->APB2ENR, IOPFEN)
├── SystemClock_Config:先配振荡器(CR/BDCR),再配分频(CFGR)
└── FLASH_LATENCY_2:72MHz 需 2 个 Flash 等待周期
8.2 核心要点
- 时钟是"发起者":CPU 和外设都不会主动干活,全靠时钟节拍驱动------这就是"用外设前先开时钟"的根本原因。
- 四个时钟源是二维矩阵:高速/低速 × 内部/外部 → HSI/HSE/LSI/LSE,一次记住。
- 时钟树只有三类元件:时钟源、倍频/分频器、选择器/使能开关,再复杂的图都是这三样的组合。
- 六级时钟要背频率 :PLLCLK/SYSCLK/FCLK/HCLK 最高 72MHz,PCLK1 最高 36MHz、PCLK2 才 72MHz。
- CSS 是单向保护:HSE 挂了自动切 HSI(毫秒级 + NMI),反向不行。
- 使能开关 = 与门:寄存器存 1 输出 1 闭合开关,存 0 断开------"配置寄存器就能使能外设"的硬件真相。
- 同步是"步调有规律",不是"同时":分频会让不同设备错开节拍工作。
- 配置本质是写寄存器:开时钟写 APB2ENR/APB1ENR/AHBENR,选时钟源写 CR/BDCR,配分频写 CFGR。
- Flash 等待周期不可忽视:72MHz 要 FLASH_LATENCY_2,否则 Flash 读取跟不上 CPU 取指。
- HSE 精度远高于 HSI:晶振 ppm 级 vs 内部 RC 约 1%,所以系统主时钟选 HSE。
8.3 后续展望
- USART 串口(第 7 章):第一次和电脑"聊天",printf 重定向调试大法
- 中断与 EXTI:让 CPU 从"轮询"走向"事件驱动",理解中断优先级
- 定时器(TIM):基于 PCLK 时钟的精确定时、PWM 输出
- I2C / SPI(第 8、9 章):外设时钟的使能与复用功能实战
- RTC / 看门狗专题:把本篇一笔带过的第二部分时钟落地