跳转至

时钟系统(RCC)

时钟是 STM32 的"心跳"。没有时钟,CPU 不会运行,外设不会工作,一切都是静止的。理解时钟系统是理解 STM32 一切行为的基础——为什么使用外设前要开时钟?为什么串口波特率不对会乱码?答案都在时钟树里。


一、为什么需要时钟?

时钟的本质

数字电路的一切操作都靠时钟来驱动。每一个时钟周期,寄存器读一次输入、更新一次输出。

\[ f = 72\text{MHz} \Rightarrow T = \frac{1}{72 \times 10^6} \approx 13.9\text{ns} \]

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() 开头自动调用。默认配置路径:

\[ \text{HSE}(8\text{MHz}) \xrightarrow{\text{PLL} \times 9} \text{SYSCLK}(72\text{MHz}) \xrightarrow{\text{AHB}/1} \text{HCLK}(72\text{MHz}) \]

各总线时钟:

时钟 频率 分频设置 服务对象
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)是一个倍频器,把低频时钟"乘"到高频。

\[ f_{\text{PLL\_OUT}} = f_{\text{PLL\_IN}} \times \text{倍频系数} \]

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 最强大的功能之一:

  1. 打开 CubeMX 工程 → 点击 Clock Configuration 标签页
  2. 在输入端选择时钟源(HSE / HSI)
  3. 设置 PLL 倍频系数
  4. 直接在目标框中输入想要的频率(如 HCLK 输入 72),CubeMX 自动计算所有分频和倍频参数
  5. 生成代码

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:

  1. 在 CubeMX 的 Clock Configuration 标签页中,将 HCLK 改为 48
  2. CubeMX 自动计算:PLL ×6 → 48MHz
  3. 重新生成代码,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%),可能导致串口通信波特率偏差较大。对精度要求不高的场景可以使用。

如何确认系统时钟是否正确?

  1. 使用 HAL_RCC_GetSysClockFreq() 等函数打印各总线频率
  2. 用定时器产生一个已知频率的方波,用示波器测量是否准确
  3. 用串口输出数据,看 PC 端能否正确解码(波特率依赖于时钟配置)

改了时钟配置后串口乱码?

串口波特率寄存器的值是根据 PCLK 频率计算的。如果在 CubeMX 中修改了时钟配置,只需重新生成代码即可——CubeMX 会自动根据新时钟重新计算所有外设参数。