STM32F103 RCC 第一层复习:时钟树、外设时钟与重复测试

STM32F103 RCC 第一层复习:时钟树、外设时钟与重复测试

芯片/板卡:STM32F103C8T6(中容量产品)

开发方式:Keil + STM32F10x 标准外设库 V3.5.0

对照工程:程序源码/STM32Project-有注释版/13-1 修改主频

主要资料:RM0008 中文参考手册、STM32F103x8/B 数据手册、固件函数库用户手册、6时钟及配置.pdf

整理日期:2026-09-09

本节边界:只完成第一层------会看时钟树、会算频率、会用库函数开启时钟、会读取并验证频率。寄存器逐位配置留到第二层。


1. 第一层到底要学到什么程度

RCC 是 Reset and Clock Control(复位和时钟控制)。第一层不要求背下 RCC_CRRCC_CFGR 的每一位,也不要求从零写寄存器时钟初始化;要求形成下面这条能力链:
#mermaid-svg-GpwLDUPNRoDZDYUW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GpwLDUPNRoDZDYUW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GpwLDUPNRoDZDYUW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GpwLDUPNRoDZDYUW .error-icon{fill:#552222;}#mermaid-svg-GpwLDUPNRoDZDYUW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GpwLDUPNRoDZDYUW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GpwLDUPNRoDZDYUW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GpwLDUPNRoDZDYUW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GpwLDUPNRoDZDYUW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GpwLDUPNRoDZDYUW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GpwLDUPNRoDZDYUW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GpwLDUPNRoDZDYUW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GpwLDUPNRoDZDYUW .marker.cross{stroke:#333333;}#mermaid-svg-GpwLDUPNRoDZDYUW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GpwLDUPNRoDZDYUW p{margin:0;}#mermaid-svg-GpwLDUPNRoDZDYUW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GpwLDUPNRoDZDYUW .cluster-label text{fill:#333;}#mermaid-svg-GpwLDUPNRoDZDYUW .cluster-label span{color:#333;}#mermaid-svg-GpwLDUPNRoDZDYUW .cluster-label span p{background-color:transparent;}#mermaid-svg-GpwLDUPNRoDZDYUW .label text,#mermaid-svg-GpwLDUPNRoDZDYUW span{fill:#333;color:#333;}#mermaid-svg-GpwLDUPNRoDZDYUW .node rect,#mermaid-svg-GpwLDUPNRoDZDYUW .node circle,#mermaid-svg-GpwLDUPNRoDZDYUW .node ellipse,#mermaid-svg-GpwLDUPNRoDZDYUW .node polygon,#mermaid-svg-GpwLDUPNRoDZDYUW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GpwLDUPNRoDZDYUW .rough-node .label text,#mermaid-svg-GpwLDUPNRoDZDYUW .node .label text,#mermaid-svg-GpwLDUPNRoDZDYUW .image-shape .label,#mermaid-svg-GpwLDUPNRoDZDYUW .icon-shape .label{text-anchor:middle;}#mermaid-svg-GpwLDUPNRoDZDYUW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GpwLDUPNRoDZDYUW .rough-node .label,#mermaid-svg-GpwLDUPNRoDZDYUW .node .label,#mermaid-svg-GpwLDUPNRoDZDYUW .image-shape .label,#mermaid-svg-GpwLDUPNRoDZDYUW .icon-shape .label{text-align:center;}#mermaid-svg-GpwLDUPNRoDZDYUW .node.clickable{cursor:pointer;}#mermaid-svg-GpwLDUPNRoDZDYUW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GpwLDUPNRoDZDYUW .arrowheadPath{fill:#333333;}#mermaid-svg-GpwLDUPNRoDZDYUW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GpwLDUPNRoDZDYUW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GpwLDUPNRoDZDYUW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GpwLDUPNRoDZDYUW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GpwLDUPNRoDZDYUW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GpwLDUPNRoDZDYUW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GpwLDUPNRoDZDYUW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GpwLDUPNRoDZDYUW .cluster text{fill:#333;}#mermaid-svg-GpwLDUPNRoDZDYUW .cluster span{color:#333;}#mermaid-svg-GpwLDUPNRoDZDYUW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GpwLDUPNRoDZDYUW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GpwLDUPNRoDZDYUW rect.text{fill:none;stroke-width:0;}#mermaid-svg-GpwLDUPNRoDZDYUW .icon-shape,#mermaid-svg-GpwLDUPNRoDZDYUW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GpwLDUPNRoDZDYUW .icon-shape p,#mermaid-svg-GpwLDUPNRoDZDYUW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GpwLDUPNRoDZDYUW .icon-shape .label rect,#mermaid-svg-GpwLDUPNRoDZDYUW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GpwLDUPNRoDZDYUW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GpwLDUPNRoDZDYUW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GpwLDUPNRoDZDYUW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

从具体外设需求出发
查该外设挂在哪条总线
找到对应的时钟使能库函数
算出 SYSCLK/HCLK/PCLK
初始化并运行外设
读取时钟或测量 MCO
结果正确?
PASS
检查晶振、宏、分频和时钟使能

第一层通过后,你应该能独立回答并完成:

  1. STM32F103 复位后默认使用什么时钟?
  2. 8 MHz HSE 怎样得到 72 MHz SYSCLK?
  3. SYSCLKHCLKPCLK1PCLK2 分别是什么?
  4. GPIO、USART1、SPI1、ADC1、I2C1、SPI2、CAN 分别挂在哪条总线?
  5. 为什么初始化任何外设之前通常都要先开启它的时钟?
  6. 怎样用 RCC_GetClocksFreq()SystemCoreClock 检查配置?
  7. 怎样把时钟从 PA8(MCO)输出,用示波器或逻辑分析仪做硬件验证?
  8. 把工程从 36 MHz 改到 72 MHz 后,为什么固定写死为 72 的延时程序会出错?

第一层学习比例:20% 看资料,80% 关掉原例程后自己写、改、测、排错。


2. 资料怎么查:先看这些页

2.1 官方参考手册 RM0008

当前芯片 STM32F103C8T6 属于中容量产品,应看 RM0008 第 6 章,不看第 7 章"互联型产品 RCC"。本地中文版的 PDF 页码与页面底部印刷页码一致。

PDF页/印刷页 内容 第一层怎么用
54 第6章入口;系统复位、电源复位概述 知道 RCC 同时管理复位和时钟
55 时钟源总览:HSI、HSE、PLL、LSI、LSE 先记住五个源各自用途
56 图8时钟树;AHB/APB分频;各总线最高频率;定时器倍频规则 本节最重要的一页,必须能顺着箭头读
57 HSE、HSI的来源、启停、稳定标志和精度差异 理解"外部更准,内部启动方便"
58 PLL、LSE、LSI 会说明 PLL 的输入、倍频及"先配置后使能"
59 SYSCLK选择、CSS、RTC/IWDG时钟、MCO 理解切换条件和时钟输出验证
60~62 RCC_CRRCC_CFGR 第一层只用于查含义,不要求逐位默写
63~68 时钟中断、APB外设复位寄存器 了解位置,第二层再深入
69~70 AHB、APB2外设时钟使能 查 DMA1、SPI1、USART1、ADC1 等时钟
71~73 APB2、APB1外设时钟使能 查 GPIO、I2C、SPI2、CAN、TIM2~4 等时钟
74~75 备份域和复位原因标志 RTC、低功耗和复位原因分析时再细学

图 1 RM0008 中文参考手册第 56 页(页面底部同为 56/754):小/中/大容量 STM32F10xxx 时钟树。复习时从左向右指读一次。

2.2 STM32F103x8/B 数据手册

PDF页/印刷页 内容 用途
12 图2时钟树和限制说明 与 RM0008 第56页交叉验证
34 外部用户时钟特性 查 HSE 外部时钟输入范围和电气条件
35 4~16 MHz HSE晶体/陶瓷谐振器特性 核对板上晶振及负载电容设计
36 32.768 kHz LSE晶体特性 RTC阶段再重点使用
37 HSI、LSI内部RC特性 理解内部RC存在温度、电压和制造偏差

2.3 标准外设库用户手册与实际源码

本地 STM32F103xx固件函数库用户手册.pdf 的 RCC 章节在 PDF 第 194~208 页:

  • 第194页:RCC函数总览;
  • 第195~199页:复位、HSE、HSI、PLL、SYSCLK和总线分频函数;
  • 第206页:RCC_GetClocksFreq()
  • 第207~208页:AHB、APB2、APB1外设时钟使能函数及参数。

注意:这份函数库手册版本比当前工程中的 V3.5.0 源码旧。函数概念可看手册,最终函数原型、参数宏和实际行为以当前工程的 stm32f10x_rcc.h/.c 为准。

2.4 课程资料 6时钟及配置.pdf

PDF页 内容 使用方式
1~6 时钟信号、AHB、APB1/APB2和常见分支 建立直观认识
7~10 HSI/HSE/LSE/LSI、PLL、USB、CSS、MCO 第一层概念复习
11~13 完整时钟树图片 对照官方图指读
14~17 RCC和复位、启动过程、配置字段 了解整体流程,具体结论以官方手册校正
18~20 SysTick及延时 用于理解时钟变化为什么影响延时
21~25 RTC 不属于本次 RCC 第一层核心,可后续复习

3. 五个时钟源:先背用途,不背寄存器位

名称 全称 STM32F103典型频率/范围 主要特点 常见用途
HSI High-Speed Internal 内部 RC,典型 8 MHz 无需外部器件、启动快,但精度较低 复位后的默认系统时钟、故障备用
HSE High-Speed External 晶体模式 4~16 MHz;外部时钟旁路最高可到 25 MHz 精度通常高于 HSI 精确系统时钟、PLL输入、USB相关应用
PLL Phase-Locked Loop 对 HSI/2 或 HSE(可选/2)倍频 用较低源频率得到较高系统频率 常用来生成 72 MHz SYSCLK
LSI Low-Speed Internal 内部 RC,约 40 kHz,误差较大 低功耗、独立于主时钟 IWDG、RTC可选源
LSE Low-Speed External 32.768 kHz 低功耗且更准确 RTC首选时钟源

必须背出的因果句:

复位后先由 HSI 保证芯片能运行;需要更高、更准确的主频时,程序启动 HSE,等待它稳定,再配置并启动 PLL,最后把 SYSCLK 切到 PLL。

特别注意:

  • HSI 作为 PLL 输入时先固定除以 2,即 8 MHz / 2 = 4 MHz;RM0008 第56页注明,此路径的系统时钟最高为 64 MHz。
  • PLL 的来源和倍频系数必须在 PLL 关闭时配置;PLL 使能后不能直接修改这些参数(RM0008 第58页)。
  • 目标源没有就绪时,系统不会完成 SYSCLK 切换(RM0008 第59页)。

4. 从左到右读懂时钟树

先记住主干:

text 复制代码
HSI / HSE
    ↓
PLL(可选倍频)
    ↓
SYSCLK(系统时钟源选择)
    ↓ AHB预分频
HCLK(CPU、存储器、DMA、AHB)
    ├─ APB1预分频 → PCLK1(低速外设总线,最高36MHz)
    └─ APB2预分频 → PCLK2(高速外设总线,最高72MHz)

4.1 四个最常见名称

名称 含义 给谁使用
SYSCLK 被选中的系统主时钟 进入 AHB 预分频器
HCLK SYSCLK 经 AHB 分频后的时钟 Cortex-M3内核、存储器、DMA、AHB总线;SystemCoreClock保存的就是它
PCLK1 HCLK 经 APB1 分频后的时钟 APB1外设,例如 I2C1/2、SPI2、CAN、USART2/3、TIM2~4
PCLK2 HCLK 经 APB2 分频后的时钟 APB2外设,例如 GPIO/AFIO、USART1、SPI1、ADC1、TIM1

不要把 SYSCLKHCLK 永远当成同一个量。课程工程通常把 AHB 设置为 1 分频,所以二者数值相等;一旦 AHB 不是 1 分频,它们就不同。

4.2 三组必须会算的例子

例1:标准 72 MHz 配置
text 复制代码
HSE = 8 MHz
PLLCLK = 8 MHz × 9 = 72 MHz
SYSCLK = PLLCLK = 72 MHz
HCLK = SYSCLK / 1 = 72 MHz
PCLK1 = HCLK / 2 = 36 MHz
PCLK2 = HCLK / 1 = 72 MHz

这是 STM32F103 最常见的满速配置。APB1 必须除以 2,因为它最高只能到 36 MHz。

例2:当前注释版工程的 36 MHz 配置

system_stm32f10x.c 当前启用 SYSCLK_FREQ_36MHz,实际路径是:

text 复制代码
HSE = 8 MHz
PLL输入 = HSE / 2 = 4 MHz
PLLCLK = 4 MHz × 9 = 36 MHz
SYSCLK = 36 MHz
HCLK = 36 MHz
PCLK1 = 36 MHz
PCLK2 = 36 MHz

36 MHz 没超过 APB1 上限,因此这个预设把 APB1 设为 1 分频。

例3:复位默认 HSI
text 复制代码
HSI = 8 MHz
SYSCLK = HSI = 8 MHz
HCLK = 8 MHz(若AHB为1分频)

这解释了为什么外部晶振不起振时,CPU仍可能继续运行,但实际频率不是你预期的 36/72 MHz。

4.3 定时器和 ADC 的两个特殊点

  • 若某 APB 预分频系数为 1,则对应定时器时钟 TIMxCLK = PCLKx;否则 TIMxCLK = 2 × PCLKx。所以 72 MHz配置下,虽然 PCLK1=36 MHz,TIM2~4通常仍得到 72 MHz。
  • ADC 时钟来自 PCLK2 的 2、4、6或8分频,且最大 14 MHz。72 MHz工程常用 72 / 6 = 12 MHz,对应课程代码中的 RCC_ADCCLKConfig(RCC_PCLK2_Div6)

5. 七个目标外设挂在哪条总线

外设 常用实例 总线 第一层常用时钟函数
GPIO/AFIO GPIOA/B/C、AFIO APB2 RCC_APB2PeriphClockCmd()
UART USART1 APB2 RCC_APB2PeriphClockCmd()
UART USART2、USART3 APB1 RCC_APB1PeriphClockCmd()
SPI SPI1 APB2 RCC_APB2PeriphClockCmd()
SPI SPI2 APB1 RCC_APB1PeriphClockCmd()
I2C I2C1、I2C2 APB1 RCC_APB1PeriphClockCmd()
ADC ADC1、ADC2 APB2 开接口时钟后,再用 RCC_ADCCLKConfig() 设置ADC分频
CAN CAN1 APB1 RCC_APB1PeriphClockCmd()

AHB常见例子:DMA1 使用 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE)

第一层最实用的记忆法不是背整张表,而是按项目链路记:

text 复制代码
GPIOA + AFIO + USART1 + SPI1 + ADC1 → APB2
I2C1 + I2C2 + SPI2 + CAN1 + USART2/3 → APB1
DMA1 → AHB

5.1 为什么不先开时钟,外设就不工作

RCC 用门控时钟决定一个外设是否获得时钟脉冲。没有时钟脉冲,外设内部状态机和寄存器接口不能正常工作。RM0008 第69、70、71页明确提示:外设时钟未启用时,读取相应外设寄存器会得到 0x0

典型初始化顺序:

c 复制代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  // 1. 先供时钟
GPIO_Init(GPIOA, &GPIO_InitStructure);                  // 2. 再配置外设

多个同总线时钟可以按位或一次开启:

c 复制代码
RCC_APB2PeriphClockCmd(
    RCC_APB2Periph_GPIOA |
    RCC_APB2Periph_AFIO  |
    RCC_APB2Periph_USART1,
    ENABLE
);

6. 上电后代码到底按什么顺序运行

#mermaid-svg-NHsigNkV2UVuUOBW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NHsigNkV2UVuUOBW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NHsigNkV2UVuUOBW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NHsigNkV2UVuUOBW .error-icon{fill:#552222;}#mermaid-svg-NHsigNkV2UVuUOBW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NHsigNkV2UVuUOBW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NHsigNkV2UVuUOBW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NHsigNkV2UVuUOBW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NHsigNkV2UVuUOBW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NHsigNkV2UVuUOBW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NHsigNkV2UVuUOBW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NHsigNkV2UVuUOBW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NHsigNkV2UVuUOBW .marker.cross{stroke:#333333;}#mermaid-svg-NHsigNkV2UVuUOBW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NHsigNkV2UVuUOBW p{margin:0;}#mermaid-svg-NHsigNkV2UVuUOBW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NHsigNkV2UVuUOBW .cluster-label text{fill:#333;}#mermaid-svg-NHsigNkV2UVuUOBW .cluster-label span{color:#333;}#mermaid-svg-NHsigNkV2UVuUOBW .cluster-label span p{background-color:transparent;}#mermaid-svg-NHsigNkV2UVuUOBW .label text,#mermaid-svg-NHsigNkV2UVuUOBW span{fill:#333;color:#333;}#mermaid-svg-NHsigNkV2UVuUOBW .node rect,#mermaid-svg-NHsigNkV2UVuUOBW .node circle,#mermaid-svg-NHsigNkV2UVuUOBW .node ellipse,#mermaid-svg-NHsigNkV2UVuUOBW .node polygon,#mermaid-svg-NHsigNkV2UVuUOBW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NHsigNkV2UVuUOBW .rough-node .label text,#mermaid-svg-NHsigNkV2UVuUOBW .node .label text,#mermaid-svg-NHsigNkV2UVuUOBW .image-shape .label,#mermaid-svg-NHsigNkV2UVuUOBW .icon-shape .label{text-anchor:middle;}#mermaid-svg-NHsigNkV2UVuUOBW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NHsigNkV2UVuUOBW .rough-node .label,#mermaid-svg-NHsigNkV2UVuUOBW .node .label,#mermaid-svg-NHsigNkV2UVuUOBW .image-shape .label,#mermaid-svg-NHsigNkV2UVuUOBW .icon-shape .label{text-align:center;}#mermaid-svg-NHsigNkV2UVuUOBW .node.clickable{cursor:pointer;}#mermaid-svg-NHsigNkV2UVuUOBW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NHsigNkV2UVuUOBW .arrowheadPath{fill:#333333;}#mermaid-svg-NHsigNkV2UVuUOBW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NHsigNkV2UVuUOBW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NHsigNkV2UVuUOBW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NHsigNkV2UVuUOBW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NHsigNkV2UVuUOBW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NHsigNkV2UVuUOBW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NHsigNkV2UVuUOBW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NHsigNkV2UVuUOBW .cluster text{fill:#333;}#mermaid-svg-NHsigNkV2UVuUOBW .cluster span{color:#333;}#mermaid-svg-NHsigNkV2UVuUOBW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NHsigNkV2UVuUOBW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NHsigNkV2UVuUOBW rect.text{fill:none;stroke-width:0;}#mermaid-svg-NHsigNkV2UVuUOBW .icon-shape,#mermaid-svg-NHsigNkV2UVuUOBW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NHsigNkV2UVuUOBW .icon-shape p,#mermaid-svg-NHsigNkV2UVuUOBW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NHsigNkV2UVuUOBW .icon-shape .label rect,#mermaid-svg-NHsigNkV2UVuUOBW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NHsigNkV2UVuUOBW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NHsigNkV2UVuUOBW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NHsigNkV2UVuUOBW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 芯片复位,HSI为默认SYSCLK
启动文件 Reset_Handler
调用 SystemInit
恢复RCC基础状态
SetSysClock选择编译时预设
启动并等待HSE
设置Flash等待周期和AHB/APB分频
配置、启动并等待PLL
切换SYSCLK并等待切换成功
进入C运行库
进入 main

关键结论:SystemInit()main() 之前由启动文件调用。因此进入 main() 时,系统通常已经按 system_stm32f10x.c 中选择的预设运行。

6.1 当前 13-1 修改主频 工程做了什么

文件和位置 作用
Start/system_stm32f10x.c:112 当前启用 SYSCLK_FREQ_36MHz
Start/system_stm32f10x.c:151~164 根据预设给 SystemCoreClock 初始值
Start/system_stm32f10x.c:212 SystemInit()入口
Start/system_stm32f10x.c:419 SetSysClock()根据宏选择具体配置函数
Start/system_stm32f10x.c:683 SetSysClockTo36()实现36 MHz配置
Start/system_stm32f10x.c:987 SetSysClockTo72()实现72 MHz配置
User/main.c:7~18 初始化OLED,显示 SystemCoreClock,用闪烁观察运行速度
Hardware/OLED.c:11 OLED模块先开启GPIOB时钟,再使用PB8/PB9

切换预设时应保证 24/36/48/56/72 MHz 宏中只启用一个。例如改为72 MHz:

c 复制代码
/* #define SYSCLK_FREQ_36MHz  36000000 */
#define SYSCLK_FREQ_72MHz  72000000

这里修改的是 CMSIS 系统文件中的编译时选择,适合第一层练习;不要同时再在 main() 中重复重配一次系统时钟。


7. 第一层需要掌握的 RCC 函数

7.1 必须会独立使用

函数/对象 作用 第一层要会什么
RCC_APB2PeriphClockCmd(mask, state) 开/关APB2外设时钟 会给GPIO、AFIO、USART1、SPI1、ADC1选参数
RCC_APB1PeriphClockCmd(mask, state) 开/关APB1外设时钟 会给I2C1/2、SPI2、CAN1、USART2/3选参数
RCC_AHBPeriphClockCmd(mask, state) 开/关AHB外设时钟 会给DMA1选参数
RCC_ADCCLKConfig(div) 设置ADC时钟相对PCLK2的分频 会根据"不超过14 MHz"计算2/4/6/8分频
RCC_GetClocksFreq(&clocks) 读取并计算SYSCLK/HCLK/PCLK1/PCLK2 会声明结构体、调用并显示结果
SystemCoreClock CMSIS全局变量,保存当前HCLK 知道它不是硬件测频结果,也不一定等于SYSCLK
SystemCoreClockUpdate() 按RCC配置重新计算 SystemCoreClock 运行中改变HCLK后必须调用
RCC_MCOConfig(source) 选择PA8输出的时钟 会输出PLL/2、HSI、HSE或SYSCLK做测量

RCC_ClocksTypeDef 有四个常用成员:

c 复制代码
typedef struct
{
    uint32_t SYSCLK_Frequency;
    uint32_t HCLK_Frequency;
    uint32_t PCLK1_Frequency;
    uint32_t PCLK2_Frequency;
} RCC_ClocksTypeDef;

7.2 第一层要知道用途,但不要求每天手写整套系统时钟

函数 作用 关键注意点
RCC_DeInit() 把RCC恢复到接近复位状态 不要在普通外设初始化中随意调用,会影响全系统
RCC_HSEConfig() 开/关HSE或设置旁路 开启后还要等待就绪
RCC_WaitForHSEStartUp() 等待HSE稳定并返回成功/失败 必须处理失败路径,不能无限假设晶振正常
RCC_HSICmd() 开/关HSI HSI作为当前SYSCLK时不能直接关闭
RCC_PLLConfig() 设置PLL来源及倍频 必须在PLL关闭时配置
RCC_PLLCmd() 开/关PLL 使能后要等待PLLRDY
RCC_HCLKConfig() 设置AHB分频 决定HCLK
RCC_PCLK1Config() 设置APB1分频 PCLK1不能超过36 MHz
RCC_PCLK2Config() 设置APB2分频 PCLK2不能超过72 MHz
RCC_SYSCLKConfig() 选择HSI/HSE/PLL作为SYSCLK 目标源就绪后才会完成切换
RCC_GetSYSCLKSource() 查询实际使用的SYSCLK源 返回的是状态编码,不是频率

第二层再做的事:单步进入上述函数,对照 stm32f10x_rcc.c 和 RM0008 第60~75页,研究它们具体改了哪些寄存器位。


8. 现有工程的两个重要勘误

8.1 Delay.c 把72 MHz写死了

当前 13-1 修改主频/System/Delay.c:10

c 复制代码
SysTick->LOAD = 72 * xus;

但同一工程当前配置为36 MHz。这会让所谓的 Delay_us(1) 实际等待约2 μs,Delay_ms(500)也大约变成1 s。它正好能演示"主频变化会影响定时",但不能作为可靠的通用延时实现。

正确的第一层认识是:SysTick选择HCLK作为时钟源后,每微秒所需计数应由当前 SystemCoreClock 计算,而不是永远写死72。

8.2 SystemCoreClock 更准确的含义是 HCLK

main.c 注释称它表示"系统主频"。在当前AHB 1分频配置下这样理解数值上没错,但CMSIS定义更准确:SystemCoreClock 保存的是内核时钟HCLK。若以后把AHB设置为2分频,SYSCLKSystemCoreClock就不相等。


9. 可重复复习的综合示例代码

用途:替换 13-1 修改主频/User/main.c 进行练习。它完成以下工作:

  • 重新根据硬件配置更新 SystemCoreClock
  • 读取 SYSCLK、HCLK、PCLK1、PCLK2;
  • 在OLED上显示四个频率和PASS/FAIL;
  • 用PA1低有效LED显示测试结果;
  • 把PLL/2输出到PA8(MCO),供示波器或逻辑分析仪实测;
  • 使用当前HCLK动态计算SysTick延时,避免写死72 MHz。

硬件沿用课程工程:OLED使用PB8/PB9,LED1使用PA1且低电平点亮。PA8为MCO输出。测量PA8前确认板卡已引出该引脚,并与仪器共地。72 MHz系统下选择PLL/2输出36 MHz,避免直接把72 MHz作为普通GPIO测量目标。

c 复制代码
#include "stm32f10x.h"
#include "OLED.h"

/* 与 system_stm32f10x.c 当前启用的预设保持一致。
 * 第一次练习填 36000000UL,第二次改时钟后填 72000000UL。
 */
#define EXPECTED_HCLK_HZ       36000000UL

static RCC_ClocksTypeDef g_clocks;

static void Demo_LED_MCO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* PA1用于测试结果LED;PA8用于MCO。GPIOA和AFIO均挂APB2。 */
    RCC_APB2PeriphClockCmd(
        RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO,
        ENABLE
    );

    /* PA1:课程板LED1,低电平点亮。 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_SetBits(GPIOA, GPIO_Pin_1);

    /* PA8:MCO复用推挽输出。 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 当前24/36/48/56/72 MHz预设都使用PLL,输出PLL/2便于测量。 */
    RCC_MCOConfig(RCC_MCO_PLLCLK_Div2);
}

static void Delay_us_ByCurrentHCLK(uint32_t xus)
{
    uint32_t ticks_per_us;
    uint32_t ticks;

    ticks_per_us = SystemCoreClock / 1000000UL;
    ticks = ticks_per_us * xus;

    /* SysTick LOAD只有24位;本实验每次只延时1000 us,不会越界。 */
    if ((ticks == 0UL) || (ticks > 0x01000000UL))
    {
        return;
    }

    SysTick->LOAD = ticks - 1UL;
    SysTick->VAL = 0UL;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_ENABLE_Msk;

    while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0UL)
    {
    }

    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk;
}

static void Delay_ms_ByCurrentHCLK(uint32_t xms)
{
    while (xms > 0UL)
    {
        Delay_us_ByCurrentHCLK(1000UL);
        xms--;
    }
}

static uint8_t Clock_Check(void)
{
    uint32_t expected_pclk1;

    /* STM32F103的APB1最高36 MHz;课程预设在HCLK>36 MHz时使用2分频。 */
    if (EXPECTED_HCLK_HZ > 36000000UL)
    {
        expected_pclk1 = EXPECTED_HCLK_HZ / 2UL;
    }
    else
    {
        expected_pclk1 = EXPECTED_HCLK_HZ;
    }

    if (SystemCoreClock != EXPECTED_HCLK_HZ)
    {
        return 0;
    }
    if (g_clocks.SYSCLK_Frequency != EXPECTED_HCLK_HZ)
    {
        return 0;
    }
    if (g_clocks.HCLK_Frequency != EXPECTED_HCLK_HZ)
    {
        return 0;
    }
    if (g_clocks.PCLK1_Frequency != expected_pclk1)
    {
        return 0;
    }
    if (g_clocks.PCLK2_Frequency != EXPECTED_HCLK_HZ)
    {
        return 0;
    }

    return 1;
}

static void Clock_Show(uint8_t pass)
{
    OLED_ShowString(1, 1, "SYS:");
    OLED_ShowNum(1, 5, g_clocks.SYSCLK_Frequency / 1000000UL, 2);
    OLED_ShowString(1, 8, "MHz");

    OLED_ShowString(2, 1, "HCLK:");
    OLED_ShowNum(2, 6, g_clocks.HCLK_Frequency / 1000000UL, 2);
    OLED_ShowString(2, 9, "MHz");

    OLED_ShowString(3, 1, "P1:");
    OLED_ShowNum(3, 4, g_clocks.PCLK1_Frequency / 1000000UL, 2);
    OLED_ShowString(3, 7, " P2:");
    OLED_ShowNum(3, 11, g_clocks.PCLK2_Frequency / 1000000UL, 2);

    if (pass != 0U)
    {
        OLED_ShowString(4, 1, "CLOCK PASS");
    }
    else
    {
        OLED_ShowString(4, 1, "CLOCK FAIL");
    }
}

int main(void)
{
    uint8_t pass;

    /* SystemInit已经在main之前执行。这里根据实际寄存器重新计算HCLK变量。 */
    SystemCoreClockUpdate();
    RCC_GetClocksFreq(&g_clocks);

    OLED_Init();
    Demo_LED_MCO_Init();

    pass = Clock_Check();
    Clock_Show(pass);

    while (1)
    {
        if (pass != 0U)
        {
            /* PASS:PA1低有效LED以500 ms节奏闪烁。 */
            GPIO_ResetBits(GPIOA, GPIO_Pin_1);
            Delay_ms_ByCurrentHCLK(500UL);
            GPIO_SetBits(GPIOA, GPIO_Pin_1);
            Delay_ms_ByCurrentHCLK(500UL);
        }
        else
        {
            /* FAIL:快速闪烁,提示检查预期值、晶振和时钟预设。 */
            GPIO_ResetBits(GPIOA, GPIO_Pin_1);
            Delay_ms_ByCurrentHCLK(100UL);
            GPIO_SetBits(GPIOA, GPIO_Pin_1);
            Delay_ms_ByCurrentHCLK(100UL);
        }
    }
}

9.1 代码执行总路线

先不要陷入每一行,先记住程序的执行顺序:
#mermaid-svg-PIVYq909jde1xHA1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PIVYq909jde1xHA1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PIVYq909jde1xHA1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PIVYq909jde1xHA1 .error-icon{fill:#552222;}#mermaid-svg-PIVYq909jde1xHA1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PIVYq909jde1xHA1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PIVYq909jde1xHA1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PIVYq909jde1xHA1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PIVYq909jde1xHA1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PIVYq909jde1xHA1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PIVYq909jde1xHA1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PIVYq909jde1xHA1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PIVYq909jde1xHA1 .marker.cross{stroke:#333333;}#mermaid-svg-PIVYq909jde1xHA1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PIVYq909jde1xHA1 p{margin:0;}#mermaid-svg-PIVYq909jde1xHA1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PIVYq909jde1xHA1 .cluster-label text{fill:#333;}#mermaid-svg-PIVYq909jde1xHA1 .cluster-label span{color:#333;}#mermaid-svg-PIVYq909jde1xHA1 .cluster-label span p{background-color:transparent;}#mermaid-svg-PIVYq909jde1xHA1 .label text,#mermaid-svg-PIVYq909jde1xHA1 span{fill:#333;color:#333;}#mermaid-svg-PIVYq909jde1xHA1 .node rect,#mermaid-svg-PIVYq909jde1xHA1 .node circle,#mermaid-svg-PIVYq909jde1xHA1 .node ellipse,#mermaid-svg-PIVYq909jde1xHA1 .node polygon,#mermaid-svg-PIVYq909jde1xHA1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PIVYq909jde1xHA1 .rough-node .label text,#mermaid-svg-PIVYq909jde1xHA1 .node .label text,#mermaid-svg-PIVYq909jde1xHA1 .image-shape .label,#mermaid-svg-PIVYq909jde1xHA1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PIVYq909jde1xHA1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PIVYq909jde1xHA1 .rough-node .label,#mermaid-svg-PIVYq909jde1xHA1 .node .label,#mermaid-svg-PIVYq909jde1xHA1 .image-shape .label,#mermaid-svg-PIVYq909jde1xHA1 .icon-shape .label{text-align:center;}#mermaid-svg-PIVYq909jde1xHA1 .node.clickable{cursor:pointer;}#mermaid-svg-PIVYq909jde1xHA1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PIVYq909jde1xHA1 .arrowheadPath{fill:#333333;}#mermaid-svg-PIVYq909jde1xHA1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PIVYq909jde1xHA1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PIVYq909jde1xHA1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PIVYq909jde1xHA1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PIVYq909jde1xHA1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PIVYq909jde1xHA1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PIVYq909jde1xHA1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PIVYq909jde1xHA1 .cluster text{fill:#333;}#mermaid-svg-PIVYq909jde1xHA1 .cluster span{color:#333;}#mermaid-svg-PIVYq909jde1xHA1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PIVYq909jde1xHA1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PIVYq909jde1xHA1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PIVYq909jde1xHA1 .icon-shape,#mermaid-svg-PIVYq909jde1xHA1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PIVYq909jde1xHA1 .icon-shape p,#mermaid-svg-PIVYq909jde1xHA1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PIVYq909jde1xHA1 .icon-shape .label rect,#mermaid-svg-PIVYq909jde1xHA1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PIVYq909jde1xHA1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PIVYq909jde1xHA1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PIVYq909jde1xHA1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

复位后启动文件调用SystemInit
进入main
SystemCoreClockUpdate更新HCLK变量
RCC_GetClocksFreq计算四个时钟
OLED_Init初始化显示
Demo_LED_MCO_Init初始化PA1和PA8
Clock_Check逐项比较频率
Clock_Show显示结果
pass是否为1
LED以500ms节奏闪烁
LED以100ms节奏快闪

9.2 逐行标注:头文件、宏和全局变量

下面按代码出现顺序解释每一条有效语句。单独的 {} 只是划分作用域,不再重复解释。

代码 逐行解释
#include "stm32f10x.h" 引入STM32F10x设备头文件。它继续包含CMSIS内核定义、芯片外设结构、位定义以及 system_stm32f10x.h,所以本例可以使用RCC、GPIO和SysTick相关类型及宏。
#include "OLED.h" 引入课程OLED模块的函数声明,使编译器认识 OLED_Init()OLED_ShowString()OLED_ShowNum()
#define EXPECTED_HCLK_HZ 36000000UL 定义本轮测试期望的HCLK。UL表示无符号长整型常量,适合和 uint32_t频率值比较。切换72 MHz预设后必须改成 72000000UL
static RCC_ClocksTypeDef g_clocks; 定义一个只在当前 main.c 内可见的全局结构体。库函数会把SYSCLK、HCLK、PCLK1和PCLK2写入它的四个成员。使用全局变量可让显示函数和检查函数共享结果。

记忆:头文件解决"函数和硬件名称从哪里来",宏解决"本轮预期是多少",结构体解决"四个实际计算值存在哪里"。

9.3 逐行标注:Demo_LED_MCO_Init()

c 复制代码
static void Demo_LED_MCO_Init(void)
  • static:函数只供本文件使用,不向其他 .c 文件导出。
  • 第一个 void:函数没有返回值。
  • 参数列表中的 void:函数不接收参数。
  • 函数任务:初始化PA1测试LED和PA8主时钟输出MCO。
代码 逐行解释 对硬件的影响
GPIO_InitTypeDef GPIO_InitStructure; 定义GPIO初始化结构体,后面把引脚、模式和速度填进去,再交给 GPIO_Init() 此时只是在RAM中分配变量,尚未修改GPIO。
RCC_APB2PeriphClockCmd( 开始调用APB2外设时钟控制函数。 准备修改APB2时钟门控状态。
`RCC_APB2Periph_GPIOA RCC_APB2Periph_AFIO,` 用按位或把GPIOA与AFIO两个掩码组合,表示一次同时选择两个外设。`
ENABLE 要求开启所选时钟;若传 DISABLE则关闭。 GPIOA/AFIO开始获得PCLK2派生的时钟。
); 结束函数调用。 从下一行起可以正常配置GPIOA。
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; 选择PA1。课程板的LED1接在PA1。 只修改RAM中的结构体字段。
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 把PA1目标模式设为通用推挽输出。 稍后可主动输出高、低电平。
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; 设置GPIO输出速度等级为2 MHz。LED翻转很慢,无需50 MHz,以降低边沿噪声。 只准备配置,并不是让LED以2 MHz闪烁。
GPIO_Init(GPIOA, &GPIO_InitStructure); 把结构体地址传给库函数,真正配置GPIOA的PA1。&表示取变量地址。 PA1成为2 MHz推挽输出。
GPIO_SetBits(GPIOA, GPIO_Pin_1); 将PA1置高。课程LED低电平点亮,因此高电平代表初始化后先熄灭。 PA1输出高电平,LED1熄灭。
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; 复用同一个结构体,仅把目标引脚改成PA8。其他字段后面也会按MCO需要更新。 仍只是修改RAM变量。
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; PA8设为复用推挽输出。MCO不是普通软件GPIO,而是RCC内部时钟信号经复用功能送到引脚。 为MCO接管PA8做好配置。
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; MCO是高速信号,因此选50 MHz输出速度等级。 允许更快的输出边沿。
GPIO_Init(GPIOA, &GPIO_InitStructure); 把新的PA8配置写入GPIO硬件。 PA8成为复用推挽输出。
RCC_MCOConfig(RCC_MCO_PLLCLK_Div2); 选择PLL输出二分频作为MCO源。36 MHz系统对应PA8约18 MHz,72 MHz系统对应约36 MHz。 RCC将PLL/2信号连接到PA8的MCO通路。

为什么必须先开GPIOA时钟再调用 GPIO_Init():GPIOA没有时钟时,其内部配置逻辑不能正常接收写操作。为什么MCO需要PA8复用模式:普通输出模式由GPIO输出数据寄存器控制,复用模式才允许RCC的MCO信号控制引脚。

9.4 逐行标注:Delay_us_ByCurrentHCLK()

c 复制代码
static void Delay_us_ByCurrentHCLK(uint32_t xus)

该函数接收要延时的微秒数 xus,用当前HCLK动态计算SysTick重装值。它只供本文件调用,无返回值。

代码 逐行解释 为什么这样写
uint32_t ticks_per_us; 定义"每微秒有多少个HCLK周期"。 72 MHz时为72,36 MHz时为36。
uint32_t ticks; 定义本次延时需要的总计数值。 例如36 MHz下延时1000 μs需要36000个周期。
ticks_per_us = SystemCoreClock / 1000000UL; 用HCLK的Hz数除以一百万,换算成每微秒周期数。 消除原工程固定写死72的问题。
ticks = ticks_per_us * xus; 计算本次延时所需的总周期数。 "每微秒周期数 × 微秒数 = 总周期数"。
`if ((ticks == 0UL) (ticks > 0x01000000UL))`
return; 参数非法时立即退出,不继续启动SysTick。 防止错误延时配置。
SysTick->LOAD = ticks - 1UL; 写重装载寄存器。计数从LOAD到0包含LOAD+1个周期,所以要减1。 决定本次倒计数长度。
SysTick->VAL = 0UL; 清当前计数值,同时清除旧的计数状态。 让本轮延时从新的LOAD开始。
`SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk ` 开始设置控制寄存器,首先选择处理器时钟HCLK作为SysTick时钟。
SysTick_CTRL_ENABLE_Msk; 与前一行按位或,同时置位ENABLE,使SysTick开始倒计数。本例不置TICKINT,因此不产生SysTick中断。 启动轮询式延时。
while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0UL) 循环读取COUNTFLAG;只要还没倒数到0,就继续等待。&用于从CTRL中取出目标标志位。 CPU阻塞等待延时结束。
空循环体 { } 循环中不做其他工作,只等待硬件置位COUNTFLAG。 这是阻塞式延时,不适合同时处理其他实时任务。
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk; 保留HCLK时钟源选择,但清掉ENABLE,停止SysTick。 结束本轮计时,避免它继续自动运行。

这一函数涉及SysTick寄存器,是为了修复主频变化后的验证误差,不是要求在RCC第一层背SysTick寄存器。第一层只需理解"延时计算必须跟着HCLK变化"。

9.5 逐行标注:Delay_ms_ByCurrentHCLK()

代码 逐行解释
static void Delay_ms_ByCurrentHCLK(uint32_t xms) 定义毫秒延时函数,参数 xms 表示毫秒数。
while (xms > 0UL) 只要还有剩余毫秒数,就继续循环。
Delay_us_ByCurrentHCLK(1000UL); 每轮调用一次1000 μs延时,也就是1 ms。这样重装值在36/72 MHz下都不会超过SysTick的24位范围。
xms--; 完成1 ms后把剩余毫秒数减1。减到0时退出。

如果直接调用 Delay_us_ByCurrentHCLK(500000),72 MHz下需要36000000次计数,超过24位上限,所以毫秒函数采用多次1000 μs的方式。

9.6 逐行标注:Clock_Check()

c 复制代码
static uint8_t Clock_Check(void)

返回 uint8_t:返回1代表通过,返回0代表失败。它不接收参数,而是读取全局的 SystemCoreClockg_clocks 和宏 EXPECTED_HCLK_HZ

代码 逐行解释 本轮36/72 MHz的结果
uint32_t expected_pclk1; 定义预期PCLK1。它不能简单地永远等于HCLK,因为APB1最高36 MHz。 等待后续分支赋值。
if (EXPECTED_HCLK_HZ > 36000000UL) 判断目标HCLK是否超过APB1允许的36 MHz。 36 MHz为假;72 MHz为真。
expected_pclk1 = EXPECTED_HCLK_HZ / 2UL; 超过36 MHz时,按课程系统预设使用APB1二分频。 72 MHz时得到36 MHz。
else 处理HCLK不超过36 MHz的情况。 当前36 MHz进入此分支。
expected_pclk1 = EXPECTED_HCLK_HZ; 课程24/36 MHz预设把APB1设为1分频。 36 MHz时得到36 MHz。
if (SystemCoreClock != EXPECTED_HCLK_HZ) 首先比较CMSIS记录的HCLK与本轮预期值。 不相等说明宏、实际预设或更新过程不一致。
return 0; 上一项失败就立即返回FAIL,不再做后续比较。 "尽早失败"便于定位第一处错误。
if (g_clocks.SYSCLK_Frequency != EXPECTED_HCLK_HZ) 检查库函数算出的SYSCLK。课程预设AHB为1分频,所以期望SYSCLK等于HCLK。 36/72 MHz预设都应相等。
return 0; SYSCLK不符合预期则失败。 提示检查时钟源或PLL。
if (g_clocks.HCLK_Frequency != EXPECTED_HCLK_HZ) 检查结构体中的HCLK。 应与 SystemCoreClock一致。
return 0; HCLK不符合预期则失败。 提示检查AHB分频。
if (g_clocks.PCLK1_Frequency != expected_pclk1) 检查APB1频率是否符合分频计算。 36和72 MHz两轮都期望36 MHz。
return 0; PCLK1不符合预期则失败。 提示检查APB1预分频。
if (g_clocks.PCLK2_Frequency != EXPECTED_HCLK_HZ) 课程预设把APB2设为1分频,因此检查PCLK2是否等于HCLK。 36 MHz轮为36;72 MHz轮为72。
return 0; PCLK2不符合预期则失败。 提示检查APB2预分频。
return 1; 所有比较都通过,返回PASS。 pass最终得到1。

注意:这里的预期关系是针对课程的24/36/48/56/72 MHz预设写的,不是任何自定义时钟配置都通用。如果你以后主动把AHB改成2分频或APB2改成2分频,就要按新设计修改测试条件。

9.7 逐行标注:Clock_Show()

c 复制代码
static void Clock_Show(uint8_t pass)

参数 pass 接收检查结果;函数负责显示,不再重新计算时钟。把"计算"和"显示"分开,便于定位错误和复用。

代码 逐行解释 OLED结果示例
OLED_ShowString(1, 1, "SYS:"); 在第1行第1列显示标签。OLED函数坐标从1开始。 SYS:
OLED_ShowNum(1, 5, g_clocks.SYSCLK_Frequency / 1000000UL, 2); Hz除以一百万换算为MHz,在第1行第5列显示2位无符号数。 3672
OLED_ShowString(1, 8, "MHz"); 显示单位。 SYS:36 MHz
OLED_ShowString(2, 1, "HCLK:"); 第2行显示HCLK标签。 HCLK:
OLED_ShowNum(2, 6, g_clocks.HCLK_Frequency / 1000000UL, 2); 显示HCLK的MHz整数值。 3672
OLED_ShowString(2, 9, "MHz"); 补上MHz单位。 HCLK:36 MHz
OLED_ShowString(3, 1, "P1:"); 第3行先显示PCLK1标签。 P1:
OLED_ShowNum(3, 4, g_clocks.PCLK1_Frequency / 1000000UL, 2); 在标签后显示PCLK1。 两轮通常都是36
OLED_ShowString(3, 7, " P2:"); 同一行第7列继续显示PCLK2标签,开头空格用于分隔。 P2:
OLED_ShowNum(3, 11, g_clocks.PCLK2_Frequency / 1000000UL, 2); 在第11列显示PCLK2,两位数刚好占到第12列。 3672
if (pass != 0U) 非0按真处理;本例中通过时 pass=1 决定第4行显示内容。
OLED_ShowString(4, 1, "CLOCK PASS"); 所有时钟比较正确时显示PASS。 CLOCK PASS
else 处理 pass=0 至少一项频率不匹配。
OLED_ShowString(4, 1, "CLOCK FAIL"); 显示FAIL。 CLOCK FAIL

这里使用整数MHz显示,足够验证课程的整MHz预设。如果以后使用非整数MHz时钟,应显示Hz值或增加小数换算,不能只看截断后的整数。

9.8 逐行标注:main()

代码 逐行解释 执行结果
int main(void) C程序主函数。启动文件和C运行库准备完成后进入这里。返回类型是 int,嵌入式主循环通常不会返回。 正式进入用户程序。
uint8_t pass; 定义1字节变量保存时钟检查结果。 尚未赋值。
SystemCoreClockUpdate(); 读取当前RCC配置,重新计算HCLK并更新全局变量 SystemCoreClock。虽然系统文件已经给它初值,主动更新可以避免运行中改时钟后变量过期。 获得配置推导出的当前HCLK。
RCC_GetClocksFreq(&g_clocks); 把全局结构体地址传给库函数,填入四种时钟频率。 g_clocks四个成员获得Hz值。
OLED_Init(); 初始化PB8/PB9和OLED。OLED模块内部会先开启GPIOB时钟。 屏幕可以显示。
Demo_LED_MCO_Init(); 初始化PA1 LED和PA8 MCO。 LED先熄灭,PA8开始输出PLL/2。
pass = Clock_Check(); 调用检查函数,把返回的1或0保存到 pass 得到本轮总测试结果。
Clock_Show(pass); 把四种频率和PASS/FAIL显示到OLED。 屏幕给出软件检查结果。
while (1) 进入无限循环。嵌入式主程序不会自然结束。 持续根据结果闪灯。
if (pass != 0U) 判断初始化阶段的测试是否通过。 PASS进入慢闪分支。
GPIO_ResetBits(GPIOA, GPIO_Pin_1); 把PA1拉低。课程LED低电平有效,因此点亮。 LED亮。
Delay_ms_ByCurrentHCLK(500UL); 按当前HCLK准确延时约500 ms。 LED保持亮约0.5 s。
GPIO_SetBits(GPIOA, GPIO_Pin_1); 把PA1置高,关闭低有效LED。 LED灭。
Delay_ms_ByCurrentHCLK(500UL); 再等待约500 ms。 PASS时形成约1 s完整闪烁周期。
else pass=0时进入故障提示分支。 使用更快闪烁节奏。
GPIO_ResetBits(GPIOA, GPIO_Pin_1); PA1拉低,LED点亮。 LED亮。
Delay_ms_ByCurrentHCLK(100UL); 保持约100 ms。 快闪的亮阶段。
GPIO_SetBits(GPIOA, GPIO_Pin_1); PA1拉高,LED熄灭。 LED灭。
Delay_ms_ByCurrentHCLK(100UL); 保持约100 ms。 FAIL时完整周期约200 ms。

9.9 把库函数翻译成硬件动作

第一层背函数时,不要只背函数名,要能说出它最终使硬件发生了什么:

函数 软件层动作 硬件层结果
SystemCoreClockUpdate() 读取RCC配置并做频率计算 不改变实际时钟,只更新RAM中的HCLK记录值
RCC_GetClocksFreq() 读取时钟配置并填结构体 不改变实际时钟,只返回软件计算结果
RCC_APB2PeriphClockCmd(..., ENABLE) 打开选中外设的时钟门控 GPIOA/AFIO开始得到时钟,可以响应配置访问
GPIO_Init() 根据结构体配置目标引脚 PA1或PA8的模式、速度被改变
RCC_MCOConfig() 选择MCO信号来源 内部时钟被连接到PA8复用输出通路
GPIO_ResetBits() 输出低电平 PA1低有效LED点亮
GPIO_SetBits() 输出高电平 PA1低有效LED熄灭
OLED_ShowNum() 把数字转换成字模并写给OLED 屏幕出现计算后的频率;它不是独立测频结果

OLED负责"软件观察",PA8 MCO负责"硬件测量"。软件显示正确但MCO测量不对时,应怀疑 HSE_VALUE、真实晶振、MCO配置或测量接线;二者一致才是更有说服力的验证。

9.10 预期结果

预设 SYSCLK HCLK PCLK1 PCLK2 PA8 MCO(PLL/2)
36 MHz 36 MHz 36 MHz 36 MHz 36 MHz 18 MHz
72 MHz 72 MHz 72 MHz 36 MHz 72 MHz 36 MHz

10. 20%看、80%调:四轮重复学习法

第一轮:开卷复现

  1. 看 RM0008 第55~59页。
  2. 指着第56页时钟树,从HSE说到PCLK1/PCLK2。
  3. 打开 13-1 修改主频,确认当前36 MHz宏。
  4. 输入上面的示例代码,编译、下载。
  5. 对照表检查OLED结果;有仪器则测PA8应为18 MHz。

通过标准:能运行并得到PASS,但这一轮允许看文档。

第二轮:闭卷重写

关掉原代码,只保留空工程和函数手册,自己完成:

text 复制代码
读取四个时钟 → OLED显示 → 自动比较 → PASS/FAIL → LED提示

可以查函数原型和宏名,但不能复制整段示例。

通过标准:30~45分钟内完成;若报错,能自己根据编译信息找到头文件、函数名或参数问题。

第三轮:改为72 MHz

  1. system_stm32f10x.c 注释36 MHz宏,启用72 MHz宏。
  2. EXPECTED_HCLK_HZ 改为 72000000UL
  3. 预测结果后再运行:72/72/36/72 MHz
  4. 测PA8,预期PLL/2约为36 MHz。
  5. 确认LED闪烁周期没有因为主频变化而变化。

通过标准:能先写出计算过程,再让软件显示和硬件测量支持计算结果。

第四轮:故障注入与定位

每次只制造一个错误:

故障 预期现象 应检查的位置
72 MHz工程仍把预期值写成36 MHz OLED显示实际72,但结果FAIL EXPECTED_HCLK_HZ
保留原来写死72的Delay 36 MHz时延时约变为2倍 Delay.cSystemCoreClock
不开GPIOA时钟 PA1/PA8不正常 RCC_APB2PeriphClockCmd()
PA8没设复用推挽 MCO测不到正确波形 GPIO模式和引脚
HSE_VALUE与板上晶振不一致 软件计算值可能"看起来对",实际波特率/延时/MCO不对 stm32f10x.h、板上晶振、PA8测量
同时启用两个SYSCLK预设宏 预处理选择与期望不一致或编译逻辑混乱 system_stm32f10x.c宏区

通过标准:不是"重新复制正确代码",而是能描述故障链:哪一级时钟错了 → 哪些模块会受影响 → 用什么现象验证。


11. 第一层独立测试题

11.1 口述题

不看笔记,每题用完整因果句回答:

  1. 为什么复位后选择HSI而不是直接使用HSE?
  2. HSI、HSE、PLL、LSI、LSE分别适合什么用途?
  3. 8 MHz HSE怎样得到72 MHz SYSCLK?
  4. 为什么72 MHz配置下PCLK1通常只有36 MHz?
  5. 为什么此时TIM2的时钟仍可能是72 MHz?
  6. SystemCoreClock表示SYSCLK还是HCLK?什么时候两者不同?
  7. SystemCoreClockUpdate()为什么不是物理测频?
  8. 为什么外设初始化的第一步通常是开时钟?
  9. SPI1和SPI2应该分别调用哪个总线时钟函数?
  10. ADC1接口时钟和ADCCLK分频为什么要分别配置?

11.2 计算题

  1. HSE=8 MHz,PLL×9,AHB÷1,APB1÷2,APB2÷1:算出四个时钟。
  2. HSE=8 MHz,先÷2,PLL×9,AHB÷1,APB1÷1,APB2÷1:算出四个时钟。
  3. HCLK=72 MHz,APB1÷4:PCLK1是多少?TIM2时钟是多少?
  4. PCLK2=72 MHz,ADC选择6分频:ADCCLK是多少?是否满足不超过14 MHz?
  5. 72 MHz预设下PA8输出PLL/2,仪器应测到多少?

答案:

  1. SYSCLK=72, HCLK=72, PCLK1=36, PCLK2=72 MHz
  2. SYSCLK=36, HCLK=36, PCLK1=36, PCLK2=36 MHz
  3. PCLK1=18 MHz;由于APB1预分频不为1,TIM2CLK=36 MHz
  4. ADCCLK=12 MHz,满足限制。
  5. 约36 MHz。

11.3 上机测试

在不参考 13-1main.c 和本节完整示例的情况下,可查手册和函数原型,完成:

text 复制代码
1. 读取并显示SYSCLK/HCLK/PCLK1/PCLK2
2. 自动核对预期结果并输出PASS/FAIL
3. 用PA8输出一个可测时钟
4. 把36 MHz改成72 MHz并再次通过
5. 保证500 ms LED闪烁节奏在两种主频下基本一致
6. 故意关闭GPIO时钟,定位问题后恢复

最终通过标准:

  • 不看原例程,可查资料;
  • 45分钟左右独立完成;
  • 能在编译错误、显示错误或MCO无波形时分层排查;
  • 能解释每个RCC库函数为什么出现在该位置;
  • 能写出36 MHz和72 MHz的完整计算链;
  • 不靠背固定模板,换成SPI1、SPI2、I2C1、ADC1或CAN1时也能选对总线和时钟函数。

12. 背诵卡片

卡片1:主时钟链

text 复制代码
时钟源 → PLL可选倍频 → SYSCLK → AHB分频 → HCLK
HCLK → APB1分频 → PCLK1
HCLK → APB2分频 → PCLK2

卡片2:最高频率

text 复制代码
SYSCLK/HCLK:最高72 MHz
APB2:最高72 MHz
APB1:最高36 MHz
ADC:最高14 MHz
HSI进入PLL时:系统最高64 MHz

卡片3:常用总线

text 复制代码
APB2:GPIO、AFIO、USART1、SPI1、ADC1、TIM1
APB1:I2C1/2、SPI2、CAN1、USART2/3、TIM2/3/4
AHB:DMA1

卡片4:系统启动

text 复制代码
复位默认HSI
→ 启动文件调用SystemInit
→ 等HSE就绪
→ 配Flash和总线分频
→ 配PLL并等待就绪
→ 切SYSCLK
→ 进入main

卡片5:第一层排错顺序

text 复制代码
晶振实际频率/HSE_VALUE
→ 选择了哪个SYSCLK预设
→ HSE/PLL是否就绪
→ AHB/APB分频是否超限
→ 外设挂在哪条总线
→ 对应外设时钟是否开启
→ 软件读取频率
→ MCO硬件测量

13. 课程资料与工程勘误

  1. 6时钟及配置.pdf 第3页把"HCLK的时钟源有HSI、HSE"作为简化说法。严格路径是:HSI/HSE/PLL先经系统时钟选择成为SYSCLK,SYSCLK再经过AHB预分频得到HCLK。
  2. 该资料第17页对 PLLXTPRE 的描述不够准确。STM32F103该字段是一个选择位:HSE不分频或HSE/2,不是一个任意"1或2分频器"。以RM0008第61~62页定义为准。
  3. "外部时钟启动失败时启用PLL中断并自动切换"容易误解。启动阶段,课程CMSIS代码等待HSE超时后保留HSI并留下错误处理入口;运行过程中HSE故障的自动切换属于启用CSS后的行为,见RM0008第59页。
  4. 13-1 修改主频 当前选36 MHz,但 Delay.c 按72 MHz固定计算,这是教学现象代码而不是通用延时。主频改变后必须同步修改或改为基于 SystemCoreClock 的实现。
  5. 本地函数库用户手册版本早于工程的标准库V3.5.0;具体宏和原型应以当前工程头文件为准。

14. 页码与源码索引

来源 定位
参考文档/STM32F10xxx参考手册(中文).pdf PDF/印刷第54~75页,重点55~60、69~73页
参考文档/STM32F103x8B数据手册(中文).pdf PDF/印刷第12、34~37页
参考文档/STM32F103xx固件函数库用户手册.pdf PDF第194~208页,重点206~208页
参考文档/Z620 004.STM32单片机 嵌入式系统原理/6时钟及配置.pdf PDF第1~20页,重点2~17页
13-1 修改主频/Start/system_stm32f10x.c 预设宏107~115;SystemInit 212;SystemCoreClockUpdate 306;36 MHz配置683;72 MHz配置987
13-1 修改主频/Library/stm32f10x_rcc.h 时钟结构体46~52;RCC函数声明647~708
13-1 修改主频/Library/stm32f10x_rcc.c RCC_GetClocksFreq 908;AHB/APB2/APB1使能1064/1095/1126;MCO 1282
13-1 修改主频/User/main.c OLED显示与原复习实验7~18
13-1 修改主频/System/Delay.c 写死72 MHz的SysTick延时8~22

15. 一句话总结

RCC第一层的核心不是背寄存器,而是能从时钟源沿时钟树算到各总线,给目标外设选对时钟使能函数,再用软件读取、运行现象和MCO测量证明配置正确。

相关推荐
Kuakewei8882 小时前
PD/QC快充协议诱骗芯片设计手册:Type-C受电端完整应用方案
嵌入式硬件
smartpi_ai3 小时前
原理图上的 1.2V 是输入还是输出?蜂鸟 M 裸芯供电架构、CORE 滤波设计与外围器件三规矩
单片机·嵌入式硬件·架构
扬扬的三格电力笔记3 小时前
编码器信号远距离传输总出错?聊聊 HTL/TTL 转光纤模块,解决编码器干扰与距离难题
网络·嵌入式硬件
一条破秋裤4 小时前
STM32 单片机两层学习规划
stm32·单片机·学习
2601_950790684 小时前
国产单片机厂家推荐:用统一回复协议筛选候选
单片机·嵌入式硬件
天空'之城5 小时前
单片机基础核心知识点汇总(三十二)
单片机·嵌入式硬件
雨田言炎5 小时前
2026/09/09 STM32F103复习(三)
stm32·单片机·嵌入式硬件
Jackson_GJH5 小时前
模拟 IIC 逻辑的深度解构
stm32·单片机·嵌入式硬件
苹果嘉尔121386 小时前
imx6ull芯片——设置流程
单片机·嵌入式硬件