时钟系统(RCC)¶
时钟是 STM32 的"心跳"。没有时钟,CPU 不会运行,外设不会工作,一切都是静止的。理解时钟系统是理解 STM32 一切行为的基础——为什么使用外设前要开时钟?为什么串口波特率不对会乱码?答案都在时钟树里。
一、为什么需要时钟?¶
时钟的本质¶
数字电路的一切操作都靠时钟来驱动。每一个时钟周期,寄存器读一次输入、更新一次输出。
STM32F103 的主频 72MHz,意味着 CPU 每秒执行约 7200 万个时钟周期。
为什么不是所有外设都一直开着?¶
省电设计
STM32 默认关闭所有外设时钟。不用的外设不给时钟 = 不消耗功率。这就是为什么 CubeMX 在初始化代码中会自动调用 __HAL_RCC_GPIOx_CLK_ENABLE() 等宏来开启时钟。
二、时钟源¶
STM32F103 有 4 个时钟源:
| 时钟源 | 全称 | 频率 | 特点 |
|---|---|---|---|
| HSI | High Speed Internal | 8 MHz | 内部 RC 振荡器,精度较低(±1%),上电即可用 |
| HSE | High Speed External | 4~16 MHz(通常 8MHz) | 外接晶振,精度高,需要起振时间 |
| LSI | Low Speed Internal | ~40 kHz | 内部 RC,用于独立看门狗和 RTC |
| LSE | Low Speed External | 32.768 kHz | 外接晶振,专用于 RTC(精确计时) |
为什么 LSE 是 32.768kHz?
\(32768 = 2^{15}\),经过 15 次二分频后刚好得到 1Hz,非常适合做秒脉冲时钟。
三、时钟树¶
时钟树是 STM32 时钟系统的核心。它描述了时钟从源头到各个外设的传播路径:
graph LR
HSI[HSI 8MHz] --> SW{系统时钟<br>选择器 SW}
HSE[HSE 8MHz] --> SW
HSE --> PLL_MUL[PLL 倍频器<br>×2~×16]
HSI_DIV[HSI/2 = 4MHz] --> PLL_MUL
PLL_MUL --> |"通常 ×9 = 72MHz"| SW
SW --> |SYSCLK| AHB_PRE[AHB 预分频<br>/1,/2,...,/512]
AHB_PRE --> |"HCLK=72MHz"| CORE[CPU 内核]
AHB_PRE --> |"HCLK"| AHB_BUS[AHB 总线<br>Flash/SRAM/DMA]
AHB_PRE --> APB1_PRE[APB1 预分频<br>/1,/2,/4,/8,/16]
AHB_PRE --> APB2_PRE[APB2 预分频<br>/1,/2,/4,/8,/16]
APB1_PRE --> |"PCLK1=36MHz"| APB1[APB1 外设<br>TIM2/3/4, USART2/3<br>SPI2, I2C, DAC]
APB1_PRE --> |"×2=72MHz"| TIM_APB1[APB1 定时器时钟]
APB2_PRE --> |"PCLK2=72MHz"| APB2[APB2 外设<br>GPIO, USART1<br>SPI1, ADC]
APB2_PRE --> |"72MHz"| TIM_APB2[APB2 定时器时钟]
默认时钟配置(72MHz)¶
CubeMX 默认会配置 72MHz 的系统时钟。生成的 SystemClock_Config() 函数会在 main() 开头自动调用。默认配置路径:
各总线时钟:
| 时钟 | 频率 | 分频设置 | 服务对象 |
|---|---|---|---|
| SYSCLK | 72 MHz | — | 系统时钟 |
| HCLK | 72 MHz | AHB /1 | CPU、AHB 总线(Flash、SRAM、DMA) |
| PCLK1 | 36 MHz | APB1 /2 | APB1 外设(I2C、USART2/3、TIM2/3/4) |
| PCLK2 | 72 MHz | APB2 /1 | APB2 外设(GPIO、USART1、ADC、SPI1) |
| APB1 定时器 | 72 MHz | APB1 分频≠1 时×2 | TIM2, TIM3, TIM4 |
| APB2 定时器 | 72 MHz | APB2 分频=1,不倍频 | TIM1, TIM8 |
| ADC 时钟 | ≤14 MHz | PCLK2 /2,/4,/6,/8 | ADC1, ADC2, ADC3 |
APB1 定时器时钟的特殊规则
当 APB1 预分频系数 ≠ 1 时(默认是 /2),APB1 上的定时器时钟 = PCLK1 × 2 = 36 × 2 = 72MHz。
这个"倍频"规则经常让初学者困惑。简单记住:在默认配置下,所有定时器的时钟都是 72MHz。
四、PLL(锁相环)¶
PLL 的作用¶
PLL(Phase-Locked Loop)是一个倍频器,把低频时钟"乘"到高频。
STM32F103 的 PLL 输入可以是:
- HSI / 2 = 4 MHz
- HSE = 8 MHz(推荐)
倍频系数可选 2 ~ 16,常用配置:
| PLL 输入 | 倍频系数 | 输出频率 | 说明 |
|---|---|---|---|
| HSE 8MHz | ×9 | 72 MHz | 默认最高频率 |
| HSE 8MHz | ×6 | 48 MHz | USB 需要 48MHz |
| HSI/2 4MHz | ×16 | 64 MHz | 不接晶振时的最高频率 |
为什么主频是 72MHz 而不是更高?
STM32F103 系列的最高主频就是 72MHz,这是芯片设计限制。更高性能可以选择 F4 系列(168MHz)或 H7 系列(480MHz)。
五、HAL 库时钟使能¶
CubeMX 自动管理时钟¶
使用 CubeMX 时,时钟使能是自动处理的。当你在 CubeMX 中配置了某个外设,生成代码时会自动在初始化函数中开启时钟。
CubeMX 生成的时钟使能代码使用宏而非函数:
// GPIO 时钟使能(在 gpio.c 的 MX_GPIO_Init() 中自动生成)
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
// 外设时钟使能(在各外设的 HAL_xxx_MspInit() 中自动生成)
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_I2C1_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
HAL 库的 MSP 回调机制
HAL 库使用 HAL_xxx_MspInit() 函数来处理硬件相关的初始化(时钟、GPIO、DMA、NVIC)。CubeMX 在 stm32f1xx_hal_msp.c 中自动生成这些函数,你通常不需要手动管理时钟。
外设与总线对应速查¶
| 总线 | 时钟频率 | 外设 |
|---|---|---|
| AHB | 72 MHz | DMA1/2、SRAM、Flash、CRC |
| APB2 | 72 MHz | GPIOA/B/C/D/E、USART1、SPI1、ADC1/2/3、TIM1/8、AFIO |
| APB1 | 36 MHz | TIM2/3/4/5/6/7、USART2/3、SPI2/3、I2C1/2、DAC、CAN、USB |
记忆技巧
- APB2 = 高速外设:GPIO(引脚翻转要快)、ADC(采样要快)、SPI1(通信要快)
- APB1 = 低速外设:I2C(本身速度就慢)、CAN、DAC
- 使用 CubeMX 时不需要记忆这些,工具自动处理
六、时钟配置实战(CubeMX)¶
CubeMX 时钟树配置¶
CubeMX 提供了可视化的时钟树配置界面(Clock Configuration 标签页),这是 CubeMX 最强大的功能之一:
- 打开 CubeMX 工程 → 点击 Clock Configuration 标签页
- 在输入端选择时钟源(HSE / HSI)
- 设置 PLL 倍频系数
- 直接在目标框中输入想要的频率(如 HCLK 输入 72),CubeMX 自动计算所有分频和倍频参数
- 生成代码
CubeMX 会在 main.c 中生成 SystemClock_Config() 函数:
// CubeMX 自动生成的时钟配置(72MHz)
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/* 配置 HSE 和 PLL */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz × 9 = 72MHz
HAL_RCC_OscConfig(&RCC_OscInitStruct);
/* 配置各总线分频 */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // HCLK = 72MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // PCLK1 = 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // PCLK2 = 72MHz
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); // Flash 等待 2 周期
}
查看当前时钟频率¶
/* USER CODE BEGIN 2 */
printf("SYSCLK = %lu Hz\r\n", HAL_RCC_GetSysClockFreq());
printf("HCLK = %lu Hz\r\n", HAL_RCC_GetHCLKFreq());
printf("PCLK1 = %lu Hz\r\n", HAL_RCC_GetPCLK1Freq());
printf("PCLK2 = %lu Hz\r\n", HAL_RCC_GetPCLK2Freq());
/* USER CODE END 2 */
修改系统时钟频率¶
如需降频到 48MHz:
- 在 CubeMX 的 Clock Configuration 标签页中,将 HCLK 改为 48
- CubeMX 自动计算:PLL ×6 → 48MHz
- 重新生成代码,
SystemClock_Config()会自动更新
CubeMX 的优势
手动配置时钟需要注意 PLL 倍频、Flash 等待周期、APB 分频限制等细节,很容易出错。CubeMX 会自动检查所有约束条件,确保配置合法。
Flash 等待周期
Flash 的读取速度有限,主频越高需要插入的等待周期越多:
| SYSCLK 范围 | Flash 等待周期 |
|---|---|
| 0 ~ 24 MHz | 0(FLASH_LATENCY_0) |
| 24 ~ 48 MHz | 1(FLASH_LATENCY_1) |
| 48 ~ 72 MHz | 2(FLASH_LATENCY_2) |
CubeMX 生成的代码会自动设置正确的等待周期。
七、常见问题¶
不接外部晶振可以用吗?
可以。STM32 内部有 HSI(8MHz)振荡器,上电就能工作。但 HSI 精度较低(±1%),可能导致串口通信波特率偏差较大。对精度要求不高的场景可以使用。
如何确认系统时钟是否正确?
- 使用
HAL_RCC_GetSysClockFreq()等函数打印各总线频率 - 用定时器产生一个已知频率的方波,用示波器测量是否准确
- 用串口输出数据,看 PC 端能否正确解码(波特率依赖于时钟配置)
改了时钟配置后串口乱码?
串口波特率寄存器的值是根据 PCLK 频率计算的。如果在 CubeMX 中修改了时钟配置,只需重新生成代码即可——CubeMX 会自动根据新时钟重新计算所有外设参数。