嵌入式入门(三)RCC与时钟树深度解析

**摘要:**本文从"为什么用外设前要先开时钟"这一疑问出发,系统讲解 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 时钟树只有三类基本元件

那张极其复杂的时钟树图,拆开后其实只有三种东西:

  1. 时钟源------树根,四个原始来源;
  2. 倍频/分频器 ------改频率。有的系数固定(如 /2),有的支持软件动态配置(如 /1,2,4,5,16);倍频器主要是 PLL;
  3. 选择器(复用器)+ 使能开关------圆圈画的是选择器(从多个输入信号里选一个),方框画的是使能开关。

整棵树的核心主干就是这样一条链路:

复制代码
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 引脚对外输出。两个用途:

  1. 给外围器件直接供时钟(同步设备、省一颗晶振的成本);
  2. 当测试接口用------外接示波器看波形,验证配置是否正确。

5:时钟树+总线+外设如何打通

5.1 总线不是一根线

总线是一簇线的集合,内部一般分三类:

总线 回答什么问题
地址总线 去哪里?(读:从哪读;写:写到哪)
数据总线 具体数据是什么?
控制总线 什么时候读写、方向、是否完毕等控制逻辑

注意:时钟线(CLK)和读写方向线,都属于控制总线内部。不管 ICode、DCode、System、AHB、APB,硬件本质都一样,只是挂的设备不同。

5.2 外设也有寄存器

寄存器不只 CPU 有------外设里也有 (上一章 GPIO 的 CRL/CRH/IDR/ODR 就是)。大体分三类:数据寄存器、控制/配置寄存器、状态寄存器。

5.3 时钟线怎么接入外设

把总线结构图和时钟树合并起来看:每个片上外设前面都有一个开关 ,这个开关就来自时钟树的"使能开关"。你未来用代码使能的,就是这个开关。

5.4 寄存器如何存住1个bit

一个存储单元要成立,必须有三种功能:

  1. 写入;
  2. 读取(读完数据还在,相当于拷贝);
  3. 保持(下次写入到来之前,值不变,即锁存)。

"保持"靠一个内部开关:写完就断开,直到下次闭合才接受新数据。开关由电平/边沿控制(上升沿触发、下降沿触发、高电平触发、低电平触发)。

所以写入成立的两个条件 :① 写端有数据;② 使能端来一个上升沿(闭合开关)。

把 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 核心要点

  1. 时钟是"发起者":CPU 和外设都不会主动干活,全靠时钟节拍驱动------这就是"用外设前先开时钟"的根本原因。
  2. 四个时钟源是二维矩阵:高速/低速 × 内部/外部 → HSI/HSE/LSI/LSE,一次记住。
  3. 时钟树只有三类元件:时钟源、倍频/分频器、选择器/使能开关,再复杂的图都是这三样的组合。
  4. 六级时钟要背频率 :PLLCLK/SYSCLK/FCLK/HCLK 最高 72MHz,PCLK1 最高 36MHz、PCLK2 才 72MHz。
  5. CSS 是单向保护:HSE 挂了自动切 HSI(毫秒级 + NMI),反向不行。
  6. 使能开关 = 与门:寄存器存 1 输出 1 闭合开关,存 0 断开------"配置寄存器就能使能外设"的硬件真相。
  7. 同步是"步调有规律",不是"同时":分频会让不同设备错开节拍工作。
  8. 配置本质是写寄存器:开时钟写 APB2ENR/APB1ENR/AHBENR,选时钟源写 CR/BDCR,配分频写 CFGR。
  9. Flash 等待周期不可忽视:72MHz 要 FLASH_LATENCY_2,否则 Flash 读取跟不上 CPU 取指。
  10. HSE 精度远高于 HSI:晶振 ppm 级 vs 内部 RC 约 1%,所以系统主时钟选 HSE。

8.3 后续展望

  • USART 串口(第 7 章):第一次和电脑"聊天",printf 重定向调试大法
  • 中断与 EXTI:让 CPU 从"轮询"走向"事件驱动",理解中断优先级
  • 定时器(TIM):基于 PCLK 时钟的精确定时、PWM 输出
  • I2C / SPI(第 8、9 章):外设时钟的使能与复用功能实战
  • RTC / 看门狗专题:把本篇一笔带过的第二部分时钟落地
相关推荐
暖焰核心1 小时前
RCC 概念篇——单片机的心跳从哪来
单片机·嵌入式硬件
Naisu Xu2 小时前
AGM SOC AG32VF407(MCU+CPLD)点灯测试
单片机·嵌入式硬件
送外卖的工程师14 小时前
STM32智能家庭安防系统
stm32·单片机·嵌入式硬件
送外卖的工程师14 小时前
STM32智能消毒柜
stm32·单片机·嵌入式硬件
CodeFragment15 小时前
裸机开发速记:启动流程|GIC 中断|时钟系统|UART 通信
arm开发·单片机·嵌入式硬件
ebiobiz15 小时前
基于 GD32 Embedded Builder (GEB) 与 Nimmake 的 MCU 工程搭建指南
c++·python·单片机·嵌入式硬件·mcu
额额额对了16 小时前
SPI通信
linux·c语言·汇编·单片机·嵌入式硬件·arm
创界工坊工作室16 小时前
14-基于stm32的水肥一体化灌溉系统设计
stm32·单片机·嵌入式硬件
斑马13917 小时前
ARM学习笔记(八)——PWM与ADC
单片机·嵌入式硬件