STM32F103学习笔记-16-RCC(第3节)-使用HSE配置系统时钟并使用MCO输出监控系统时钟
在前两节中,我们学习了时钟树的理论和系统时钟配置函数。本节我们将亲手编写代码,对于调试和验证时钟配置非常重要,通过使用HSE(外部高速时钟)配置系统时钟,并利用MCO(微控制器时钟输出)功能,来监控时钟信号。
一、创建驱动文件与工程配置
在STM32开发中,我们经常需要根据项目需求自定义时钟配置。STM32标准库提供了丰富的函数来简化这个过程。今天,我们将编写一个名为 HSE_SetSysClk的函数,它使用HSE晶振作为时钟源,并通过PLL倍频到所需频率(包括超频)。我们还将通过MCO引脚输出时钟信号,用示波器测量验证。
步骤:
1.新建驱动文件:
-
在工程中创建两个文件:
bsp_rccclkconfig.c:包含函数实现。bsp_rcc_clock_config.c(源文件)和bsp_rcc_clock_config.h(头文件)。文件名中的bsp表示“板级支持包”,这是一种常见的驱动命名约定。bsp_rccclkconfig.h:包含函数声明和必要的头文件。在bsp_rcc_clock_config.c中,我们将实现时钟配置函数;在头文件中声明这些函数,以便其他文件调用。 -
在
bsp_rcc_clock_config.c中,我们将实现时钟配置函数;在头文件中声明这些函数,以便其他文件调用。
2. 工程配置:
-
将这两个文件添加到你的MDK(Keil)或IDE工程中。
-
在需要使用时钟配置的文件(如
main.c)中包含头文件:#include "bsp_rcc_clock_config.h" -
确保编译器能找到头文件:在IDE中设置头文件路径(如Keil中在Options for Target -> C/C++ -> Include Paths中添加路径)。
3. 防止头文件重复包含:
-
在头文件中使用条件编译指令,避免多次包含导致的错误。这是编程的良好习惯:
#ifndef __BSP_RCC_CLOCK_CONFIG_H #define __BSP_RCC_CLOCK_CONFIG_H/* 头文件内容 */#endif -
头文件中需要包含STM32的标准库头文件,以便使用固件库函数和寄存器定义:
#include "stm32f10x.h"
4. 编译验证:
-
完成以上步骤后,先编译工程,确保没有语法错误和路径问题。如果编译成功,说明工程配置正确。
原理说明:创建独立的驱动文件有助于代码模块化,提高可重用性。条件编译防止头文件被多次包含,避免重复定义错误。包含 stm32f10x.h是必须的,因为它提供了STM32固件库的核心函数和寄存器映射。
二、编写 bsp_rcclkconfig.c 时要参考 system_stm32f10x.c 里的 static void SetSysClockTo72(void)?
1. 背景:system_stm32f10x.c 是官方的“时钟初始化模板”
这个文件是 STM32 官方提供的系统初始化文件,在程序启动时自动执行,用于配置:
-
系统时钟源(HSI/HSE/PLL)
-
各总线分频(AHB、APB1、APB2)
-
Flash等待周期
-
启动时钟稳定检测等。
换句话说,它是整个STM32上电后第一份初始化时钟的“参考蓝图”。
例如,在该文件中定义的:
static void SetSysClockTo72(void)
就是 ST 官方配置 系统时钟为 72MHz(HSE × 9) 的标准做法。
它完整展示了:
-
HSE 启动检测;
-
PLL 倍频计算;
-
各总线分频;
-
Flash 延时配置;
-
切换系统时钟源的正确顺序。
2. bsp_rcclkconfig.c 是在“重写”这个功能
写的函数:
void HSE_SetSysClk(uint32_t RCC_PLLMul_x)
本质上与 SetSysClockTo72(void) 作用相同:配置系统时钟为某个频率(可选倍频)。
区别在于:
| 对比项 | system_stm32f10x.c | bsp_rcclkconfig.c |
|---|---|---|
| 函数名 | SetSysClockTo72() | HSE_SetSysClk(uint32_t RCC_PLLMul_x) |
| 调用方式 | 系统启动时由 SystemInit() 自动调用 | 用户在 main() 中手动调用 |
| 倍频系数 | 固定为 9(72MHz) | 可通过参数灵活指定(如 ×9, ×10, ×12) |
| 编写方式 | 直接操作寄存器 | 使用固件库函数(更直观、安全) |
因此,你需要参考 SetSysClockTo72() 的逻辑顺序与配置要点,因为:
-
它保证了正确的时钟切换流程;
-
它包含了所有必须的寄存器设置;
-
任何少配置或顺序错误都可能导致系统跑飞或时钟错误。
简言之:
参考
SetSysClockTo72()是为了在“自主编写固件库版本的时钟初始化函数”时,确保流程正确、配置完整、时序安全。
3. 为什么不是直接用 SetSysClockTo72()
因为:
-
它是
static函数,作用域仅限system_stm32f10x.c文件,外部无法直接调用; -
它只支持固定频率(72MHz),无法动态调整倍频;
-
要做“裸机编程实验与超频实验”,因此需要自己写一个可传参的版本,比如:
三、void HSE_SetSysClk(uint32_t RCC_PLLMul_x) 的函数参数语法作用
1. 参数类型说明
uint32_t RCC_PLLMul_x
-
uint32_t表示一个 32位无符号整数; -
RCC_PLLMul_x是一个 枚举常量,它来自固件库定义:
在 stm32f10x_rcc.h 文件中,可以找到类似的定义:
#define RCC_PLLMul_2 ((uint32_t)0x00000000)
#define RCC_PLLMul_3 ((uint32_t)0x00040000)
#define RCC_PLLMul_4 ((uint32_t)0x00080000)
...
#define RCC_PLLMul_9 ((uint32_t)0x001C0000)
#define RCC_PLLMul_16 ((uint32_t)0x003C0000)
这些宏值代表 PLL 的倍频系数,用于函数:
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_x);
这个函数会根据你传入的倍频参数,自动配置寄存器 RCC->CFGR 中的 PLLMULL 位域。
2. 参数的语法功能(重点理解)
当你调用:
HSE_SetSysClk(RCC_PLLMul_9);
实质上就是告诉 MCU:
“以 HSE 为输入时钟源(8MHz),倍频9倍,配置系统主频为 8MHz × 9 = 72MHz。”
如果改成:
HSE_SetSysClk(RCC_PLLMul_10);
则为:
“以 HSE 为输入时钟源,倍频10倍,系统主频 = 8MHz × 10 = 80MHz。”
所以:
-
RCC_PLLMul_x就是倍频选择参数; -
函数根据它来动态生成不同主频的系统时钟;
-
可用于测试超频、验证时钟输出。
3. 举例说明参数的使用流程
在 main.c 中:
int main(void)
{HSE_SetSysClk(RCC_PLLMul_10); // 设置系统时钟 = 8MHz × 10 = 80MHzLED_GPIO_Config();while(1){LED_G(OFF);Delay(0xFFFFF);LED_G(ON);Delay(0xFFFFF);}
}
你会发现:
-
如果改成
RCC_PLLMul_9(72MHz),LED闪烁频率为标准; -
如果改成
RCC_PLLMul_10(80MHz),LED闪烁变快; -
如果改成
RCC_PLLMul_16(128MHz),超频效果更明显。
这就是参数的灵活性与教学意义所在。
四、HSE系统时钟配置函数详解
现在,我们来编写 HSE_SetSysClk函数。这个函数接受一个参数 RCC_PLLMul_x,用于设置PLL的倍频因子,从而实现不同频率的输出(包括超频)。以下是函数的步骤分解:
步骤0: 函数框架与复位操作
void HSE_SetSysClk(uint32_t RCC_PLLMul_x)
{ErrorStatus HSEStatus; // 定义状态变量// 复位RCC寄存器到默认值RCC_DeInit();
原理说明:RCC_DeInit()将RCC寄存器恢复为复位值,确保从已知状态开始配置,避免之前的配置干扰。
步骤1: 使能HSE并等待就绪
HSE是外部晶振,通常为8MHz。我们需要启动它并等待稳定。
RCC_HSEConfig(RCC_HSE_ON); // 使能HSE
HSEStatus = RCC_WaitForHSEStartUp(); // 等待HSE启动
if (HSEStatus == SUCCESS) {// HSE启动成功,继续后续操作(步骤2~5)
} else {// HSE启动失败,可以在这里处理错误(例如,切换到HSI或报错)
}
原理说明:
-
RCC_HSEConfig()是库函数,用于启用HSE。 -
RCC_WaitForHSEStartUp()等待HSE稳定,返回SUCCESS或ERROR。HSE起振需要时间(通常几毫秒),所以必须等待,否则后续操作可能失败。 -
如果HSE失败,系统可能无法达到预期频率,因此错误处理很重要(例如,使用默认HSI)。
步骤2: 配置Flash预取指和等待周期
当系统时钟频率提高时,CPU访问Flash需要更长时间,否则可能读取出错。我们必须配置Flash的等待周期。
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); // 启用预取指缓冲区
FLASH_SetLatency(FLASH_Latency_2); // 设置2个等待周期
原理说明:
-
预取指缓冲区是Flash的缓存机制,能提前加载指令,提高效率。
-
等待周期设置依据SYSCLK频率:≤24MHz时无需等待,24-48MHz时设1周期,>48MHz时设2周期。由于我们目标是72MHz或更高,所以设2周期。
-
如果不设置,系统在高速下可能运行不稳定或崩溃。
步骤3: 配置总线分频因子
系统时钟SYSCLK通过分频分配给AHB、APB1和APB2总线,确保外设时钟不超限。
RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB分频 = 1, HCLK = SYSCLK
RCC_PCLK1Config(RCC_HCLK_Div2); // APB1分频 = 2, PCLK1 = HCLK/2 (最高36MHz)
RCC_PCLK2Config(RCC_HCLK_Div1); // APB2分频 = 1, PCLK2 = HCLK (最高72MHz)
原理说明:
-
AHB总线连接内核和高速外设(如DMA),1分频让HCLK跑在SYSCLK的全速。
-
APB1用于低速外设(如USART2、I2C),最高36MHz,所以2分频。
-
APB2用于高速外设(如GPIO、ADC),支持72MHz,1分频即可。
-
重要:如果APB1设成1分频,可能超频导致外设异常!务必遵循手册限值。
步骤4: 配置并使能PLL
PLL将HSE倍频到更高频率。我们使用HSE作为PLL输入,并设置倍频因子。
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_x); // PLL源 = HSE不分频, 倍频因子由参数指定
RCC_PLLCmd(ENABLE); // 使能PLL
while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); // 等待PLL锁定
原理说明:
-
RCC_PLLConfig()设置PLL的输入源和倍频因子。RCC_PLLSource_HSE_Div1表示HSE不分频(直接8MHz输入),RCC_PLLMul_x是参数,范围2-16(例如,9倍频得72MHz)。 -
PLL需要时间锁定频率,等待
RCC_FLAG_PLLRDY标志位确保输出稳定。 -
超频提示:倍频因子可设为16,得到128MHz(但超频可能不稳定,需测试)。
步骤5: 选择系统时钟源并等待切换完成
最后,将系统时钟源切换到PLL输出。
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 选择PLL作为SYSCLK源
while (RCC_GetSYSCLKSource() != 0x08); // 等待切换完成(0x08表示PLL已作为源)
原理说明:
-
RCC_SYSCLKConfig()选择时钟源(HSI、HSE或PLL)。 -
RCC_GetSYSCLKSource()返回当前源状态:0x00表示HSI,0x04表示HSE,0x08表示PLL。等待直到返回0x08,确认切换成功。 -
切换后,SYSCLK = PLL输出频率(如72MHz),系统正式运行在高速模式。
完整函数代码
结合以上步骤,HSE_SetSysClk函数的完整代码如下(在 bsp_rccclkconfig.c中):
#include "bsp_rcclkconfig.h"void HSE_SetSysClk(uint32_t RCC_PLLMul_x)
{ErrorStatus HSEStatus;//把RCC 寄存器复位成复位值RCC_DeInit;//使能HSERCC_HSEConfig(RCC_HSE_ON);HSEStatus = RCC_WaitForHSEStartUp();if(HSEStatus == SUCCESS){//使能预取指FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);FLASH_SetLatency(FLASH_Latency_2);//设置分频因子RCC_HCLKConfig(RCC_SYSCLK_Div1);RCC_PCLK1Config(RCC_HCLK_Div2);RCC_PCLK2Config(RCC_HCLK_Div1);//配置锁相环//配置PLLCLK = HSE * RCC_PLLMul_xRCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_x);//使能PLLRCC_PLLCmd(ENABLE);//等待PLL稳定while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);//选择系统时钟RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);while(RCC_GetSYSCLKSource() != 0x08);}else{ /*如果HSE启动失败,应用程序将会有错误的时钟配置,用户可以在这里添加代码来处理这个错误*/}
}
五、bsp_rcclkconfig.c 调用固件库的函数映射关系
1. 逻辑结构概览:三层关系图
| 层次 | 文件 | 作用 | 说明 |
|---|---|---|---|
| 应用层(用户代码) | bsp_rcclkconfig.c / .h | 调用固件库API封装特定功能(如配置HSE、PLL) | 这是你编写的外层接口函数,比如 HSE_SetSysClk() |
| 固件库接口层 | stm32f10x_rcc.h | 声明函数、定义宏、参数枚举 | 提供开发者调用API的“词典”和参数选项 |
| 底层驱动实现层 | stm32f10x_rcc.c | 实现具体函数操作RCC寄存器 | 直接读写 RCC->CR, RCC->CFGR 等寄存器,改变时钟设置 |
2. 图示理解:
main.c↓ 调用
bsp_rcclkconfig.c ← 用户自定义逻辑↓ 使用固件库函数
stm32f10x_rcc.h/.c ← 固件库层(标准外设驱动)↓ 操作寄存器
RCC 寄存器组(硬件层)
3. 对照表
| 层次 | 文件 | 内容类型 | 示例 |
|---|---|---|---|
| 用户层 | bsp_rcclkconfig.c | 应用逻辑(调用固件库) | 调用 RCC_PLLConfig() |
| 接口层 | stm32f10x_rcc.h | 函数声明 + 宏定义 | #define RCC_PLLMul_9 0x001C0000 |
| 底层层 | stm32f10x_rcc.c | 函数实现(操作寄存器) | `RCC->CFGR |
三者之间的关系可以理解为:
-
bsp_rcclkconfig.c像是一个“工程师调度中心”, -
stm32f10x_rcc.h是它的“功能目录”, -
stm32f10x_rcc.c是它的“实际执行员”, -
最终通过修改 RCC 寄存器来驱动 MCU 的时钟系统运行。
实操编程时,“光标”对准“函数名”、“鼠标单击右键”选择“Go To Definition of 'XXX' ”。
六、超频实验 与 MCO 输出监控系统时钟
1. 目标(本节只做这些)
-
在 STM32F103 上用 HSE+PLL 实现可参数化的超频(通过传入不同 PLL 倍频)。
-
用 MCO(PA8)把所选时钟源输出到探头,用示波器测频率与波形来验证实际系统时钟。
-
设计并执行一套稳健的验收 / 稳定性测试流程(避免盲目超频导致器件损伤)。
本节不重复 RCC、PLL 的基本概念和
RCC_*API 的说明(这些已在前面讲过)。下面直接给出实践要点和操作细则。
2. 实验前准备(硬件与软件)
-
开发板:STM32F103(确保晶振为 8 MHz HSE)。
-
示波器、探头(注意带宽须大于测量频率;例如要测 128 MHz,应使用 ≥200 MHz 带宽探头/示波器)。
-
若开发板 PA8 连有蜂鸣器或其他外设:先断开该外设(拔跳帽、断线),否则 MCO 输出会驱动外设,导致测量失真或噪声。
-
USB 或稳压电源:供电稳定,最好带有电流限制/监控。
-
将代码仓库/工程准备好:包含
bsp_rcclkconfig.c/.h、main.c、LED 例程等。
软件检查项(必须):
-
头文件原型声明采用 ANSI 风格(
void MCO_GPIO_Config(void);)。 -
在
bsp_rcclkconfig.c中RCC_DeInit();必须带括号(实际调用)。 -
在
main.c中调用MCO_GPIO_Config();时不要写void或错放空格在 include 名称。 -
在成功切换 SYSCLK 后调用
SystemCoreClockUpdate();,以便SystemCoreClock变量与实际频率一致(影响延时与外设时序)。
3. 实验步骤(简洁、可复制)
3.1 代码讲解
在 main() 里先设置系统主频(HSE_SetSysClk(...)),然后配置 PA8 为 MCO 输出(MCO_GPIO_Config()),接着用 RCC_MCOConfig(...) 选择要输出的时钟源(通常选 RCC_MCO_SYSCLK),用示波器测 PA8 波形并判定超频是否成功。同时注意更新 SystemCoreClock、设置 Flash 延迟和总线分频以保证系统稳定。
/* main.c (节选:关键部分) */
//......
int main(void)
{/* 假设上电启动时为默认 SystemInit 状态(HSI 或 boot 设置),现在我们想把系统时钟设为 HSE * PLLMUL */HSE_SetSysClk(RCC_PLLMul_10); /* 尝试 8MHz * 10 = 80MHz(示例) *//* 切换系统时钟后,必须更新库中的 SystemCoreClock 变量 */SystemCoreClockUpdate();/* 配置 PA8 引脚为 MCO 输出(复用推挽) */MCO_GPIO_Config(); /* 注意:这里是函数调用,不是函数声明 *//* 选择 MCO 输出源:输出当前系统时钟(SYSCLK)到 PA8 */RCC_MCOConfig(RCC_MCO_SYSCLK);/* 初始化 LED 引脚(用于直观观察) */LED_GPIO_Config();while (1){LED_G(OFF);Delay(0xFFFFF);LED_G(ON);Delay(0xFFFFF);}
}
注意事项(代码层面):
-
MCO_GPIO_Config();是函数调用,不要写成void MCO_GPIO_Config();(那是函数声明,会被当成声明语句)。 -
在
HSE_SetSysClk()成功后要调用SystemCoreClockUpdate(),否则基于SystemCoreClock的延时/串口波特率计算会不正确。 -
RCC_MCOConfig(RCC_MCO_SYSCLK)是选择输出 SYSCLK 到 PA8;如果你想输出 PLL 的直接输出而不是 SYSCLK,可使用RCC_MCO_PLLCLK_Div2(注意 PLL 输出一般是 PLLCLK/2)。
/* bsp_rcclkconfig.c 中的 PA8 配置函数(节选:关键部分) */
//......
void MCO_GPIO_Config(void)
{GPIO_InitTypeDef GPIO_InitStruct;/* 使能 GPIOA 时钟 */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);/* 配置 PA8 为 AF 推挽输出(MCO) */GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStruct);
}
说明:
-
这段代码把 PA8 置为复用推挽(AF_PP),这样内部 MCO 信号才能驱动到引脚。
-
必须先使能 GPIOA 时钟,否则
GPIO_Init()无效。
/* bsp_rcclkconfig.h 中 增加函数声明*/
#ifndef __BSP_RCCLKCONFIG_H
#define __BSP_RCCLKCONFIG_H#include "stm32f10x.h"void HSE_SetSysClk(uint32_t RCC_PLLMul_x);
void MCO_GPIO_Config(void);#endif /* __BSP_RCCLKCONFIG_H*/
举个生活类比:
-
.h文件 = 告诉别人“我有这个功能”。 -
.c文件 = 真正实现这个功能的细节。 -
main.c= 使用这个功能的人。
就像你告诉别人「我会修电脑(声明)」,而你真正修电脑的过程(实现)在别处完成。
别人听说你会修电脑(包含头文件)之后,就敢放心地让你来干活(调用函数)。
3.2 编译与基础校验
-
编译工程,确保无警告/错误(特别注意函数声明警告、
RCC_DeInit未调用警告等)。 -
在未超频前(例如使用 RCC_PLLMul_9 得到 72 MHz)加载并验证 LED、串口等外设是否正常。
3.3 配置 MCO 引脚
-
在代码里调用:
MCO_GPIO_Config(); // 配置 PA8 为 AF Push-Pull
RCC_MCOConfig(RCC_MCO_SYSCLK); // 选择输出 SYSCLK 到 MCO(PA8)
-
硬件上:断开 PA8 上的蜂鸣器跳帽(若有),接好示波器探头并确认共地。

3.4 切换不同 PLL 倍频(逐步上升)
-
在
main()或调试 shell 中,分别尝试传入RCC_PLLMul_9、RCC_PLLMul_10、……、RCC_PLLMul_16(或你板子/芯片允许的值)。 -
每次变更后,观察:
-
示波器测到的频率是否与预期(
HSE * PLLMUL)相符; -
波形的占空比 /上升沿陡峭程度(太差说明驱动/布线/负载问题);
-
板上 LED 闪烁是否与预期延时对应(注意:若使用延时函数依赖
SystemCoreClock,必须SystemCoreClockUpdate())。
-
3.5 验证并记录(必做)
-
用示波器读取频率并拍照或保存波形(记录实际值)。
-
记录每个倍频下的:
-
测得频率(Hz)
-
波形形状(方波、失真)
-
供电电流(观察是否显著上升)
-
板上温升(高频运行后检测芯片温度)
-
功能异常(外设是否失效、系统是否重启)
-
4. 观测要点与判读指南
4.1 频率是否正确
-
预期频率 = HSE(通常 8 MHz) × PLL 倍频。测量值须在小偏差范围内(示波器与探头本身有误差)。
-
若误差显著(例如 128 MHz 读成 115 MHz),先检查:示波器采样设定、探头补偿/带宽、是否有分频/探头 10× 模式设置错误。
4.2 波形质量
-
理想:近似对称方波,边沿清晰。
-
波形差的常见原因:PA8 上外设未断开、探头接地回路过大、PCB 路径阻抗或驱动能力限制、示波器带宽不足。
-
HSE 直出(8 MHz)时波形往往比经 PLL 后差(PLL 输出驱动能力、内部时钟网络有差异),但通常可接受。
-
过度失真或噪声说明该频率下 PCB/走线/驱动受限,不建议继续上调。
4.3 系统稳定性
-
仅凭 LED 变快不能判定稳定:必须运行 CPU/外设负载测试(UART/ADC/TIMER)并长时间(例如 10~60 分钟)验证。
-
超频对 Flash 等等待周期有严格要求:若未设置足够的 Flash Latency,会出现指令错误或系统崩溃。
-
APB1 上外设(例如 I2C、UART)最大时钟不能超过 36 MHz,若超出必须改分频。
