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_CR、RCC_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. SYSCLK、HCLK、PCLK1、PCLK2 分别是什么?
  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_CR、RCC_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

不要把 SYSCLK 和 HCLK 永远当成同一个量。课程工程通常把 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分频,SYSCLK与SystemCoreClock就不相等。


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代表失败。它不接收参数,而是读取全局的 SystemCoreClock、g_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位无符号数。 36或72
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整数值。 36或72
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列。 36或72
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.c与 SystemCoreClock
不开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-1 原 main.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测量证明配置正确。

相关推荐
HQB202612 小时前
EG1198|屹晶微电子 120V 内置 MOS 异步降压 DC‑DC(高性价比)
嵌入式硬件·硬件工程
果壳science14 小时前
张祥前实验如何获取正电荷
笔记·科技·嵌入式硬件·硬件工程·制造
南山电子nscn14 小时前
中科芯CETC汽车电池管理系统BMS进口芯片国产型号替代指南
单片机·汽车·bms
沉淀的.晴天14 小时前
can总线
stm32
桀人15 小时前
认识AT24C02
stm32·单片机·嵌入式硬件
国科安芯16 小时前
星间激光通信终端CANFD控制网络的时延确定性与辐射可靠性
网络·嵌入式硬件·信息与通信·canfd·低轨卫星·商业航天·抗辐射
一直C16 小时前
嵌入式 Linux 驱动开发学习笔记:内核定时器、platform 总线与 I²C 驱动
linux·驱动开发·嵌入式硬件·学习·imx6ull
传感器与混合集成电路16 小时前
150MHz主频、8路DAC与6路PWM:JLH221360伺服控制SIP解析
嵌入式硬件
comyccccc16 小时前
STM32PID控制板设计全解析学习笔记
单片机·嵌入式硬件
碧海银沙音频科技研究院17 小时前
实现炬芯ATS308X的usb cdc 串口IAP 固件升级功能实现
人工智能·嵌入式硬件·深度学习·算法