2. STM32时钟系统与BSP底层初始化

STM32的时钟系统是整个芯片运行的“心脏”,它决定了CPU、总线、外设各自以什么频率工作。很多初学者在点亮第一颗LED或配置串口时遇到“代码没错但不工作”的情况,根源往往就在时钟:外设时钟没使能、PLL没锁、总线分频不对。本节围绕HSE/HSI/PLL时钟树、AHB/APB分频、SysTick初始化以及BSP时钟使能与延时封装,把这条底层链路讲清楚。

一、时钟树:从时钟源到外设

STM32的时钟源主要有四类:HSI(内部高速RC,典型8MHz)、HSE(外部晶振,常见8MHz)、LSI/LSE(低速,供RTC和看门狗)、以及PLL(锁相环,用于倍频)。以STM32F103为例,系统时钟SYSCLK可以来自HSI、HSE或PLL输出。PLL的输入可以选HSI/2或HSE,经过倍频后得到PLLCLK,再经AHB预分频器得到HCLK(即CPU内核与AHB总线时钟)。HCLK再经APB1、APB2预分频器分别得到PCLK1(低速外设,最高36MHz)和PCLK2(高速外设,最高72MHz)。

一个常见的配置目标是:HSE=8MHz,PLL倍频9倍得到72MHz,AHB不分频,APB1二分频得36MHz,APB2不分频得72MHz。这条路径就是“8MHz晶振 → PLL×9 → 72MHz SYSCLK → HCLK 72MHz → PCLK1 36MHz / PCLK2 72MHz”。理解这条链路,才能明白为什么串口波特率计算、定时器周期计算都要先确认对应总线的实际频率。

STM32F103 时钟树配置示意(HSE 8MHz → 72MHz) HSE 8 MHz 晶振 HSI 8 MHz RC PLL ×9 倍频 输入 8MHz SYSCLK 72 MHz AHB HCLK 72 MHz APB1 PCLK1 36MHz ÷2 APB2 PCLK2 72MHz PLLSRC HSI/2 PLLCLK SW PPRE1 PPRE2

二、用标准库配置时钟

在BSP工程中,时钟初始化通常放在 SystemInit() 或自定义的 BSP_ClockInit() 中。使用标准库时,核心是配置RCC寄存器。下面是一段可直接参考的配置代码,目标为HSE 8MHz、PLL×9、SYSCLK 72MHz:

void BSP_ClockInit(void)
{
    RCC_DeInit();                          // 复位RCC寄存器
    RCC_HSEConfig(RCC_HSE_ON);             // 使能HSE
    if (RCC_WaitForHSEStartUp() == SUCCESS) // 等待HSE稳定
    {
        RCC_HCLKConfig(RCC_SYSCLK_Div1);   // AHB = SYSCLK
        RCC_PCLK2Config(RCC_HCLK_Div1);    // APB2 = HCLK
        RCC_PCLK1Config(RCC_HCLK_Div2);    // APB1 = HCLK/2

        RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
        RCC_PLLCmd(ENABLE);                // 使能PLL
        while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);

        RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 切换SYSCLK到PLL
        while (RCC_GetSYSCLKSource() != 0x08);     // 等待切换完成
    }
}

这里有两个关键等待:RCC_WaitForHSEStartUp() 确认晶振起振,RCC_GetFlagStatus(RCC_FLAG_PLLRDY) 确认PLL锁定。缺少任何一个等待,后续切换时钟都可能失败。切换完成后,RCC_GetSYSCLKSource() 返回0x08表示当前系统时钟源为PLL。

三、AHB/APB分频与外设频率

分频配置直接影响外设的实际工作频率。APB1挂载的是低速外设,如USART2/3、TIM2~TIM7、I2C1/2、SPI2等,最高36MHz;APB2挂载USART1、SPI1、TIM1、GPIO等高速外设,最高72MHz。如果APB预分频系数不为1,定时器时钟还会自动倍频:当APB分频系数为2、4、8、16时,定时器时钟 = PCLK × 2。例如TIM2挂在APB1上,PCLK1=36MHz,则TIM2实际计数时钟为72MHz。这一点在计算定时器重装载值时必须注意。

四、SysTick定时器初始化

SysTick是Cortex-M内核自带的24位递减计数器,常用于产生毫秒级延时或作为RTOS的时基。它的时钟源可以选择HCLK或HCLK/8。在72MHz系统下,若选择HCLK,则每计数一次为1/72μs。要产生1ms中断,重装载值应设为72000-1。初始化代码如下:

void BSP_SysTickInit(void)
{
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);
    SysTick->LOAD = 72000 - 1;   // 72MHz下1ms
    SysTick->VAL  = 0;
    SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk  // 开中断
                   | SysTick_CTRL_ENABLE_Msk;  // 启动
}

在中断服务函数 SysTick_Handler() 中维护一个全局毫秒计数变量 g_msTick,每次中断自增。这样BSP层就拥有了一个稳定的时间基准。

五、BSP时钟使能与延时函数封装

BSP层的价值在于把底层寄存器操作封装成简洁接口。时钟使能方面,可以按总线分组封装:

#define BSP_RCC_APB2_ENABLE(periph)  RCC_APB2PeriphClockCmd(periph, ENABLE)
#define BSP_RCC_APB1_ENABLE(periph)  RCC_APB1PeriphClockCmd(periph, ENABLE)
#define BSP_RCC_AHB_ENABLE(periph)   RCC_AHBPeriphClockCmd(periph, ENABLE)

延时函数则基于SysTick实现。查询式延时不需要开中断,直接操作VAL和COUNTFLAG即可:

void BSP_DelayUs(uint32_t us)
{
    uint32_t load = us * 72;     // 72MHz下1us=72次
    SysTick->LOAD = load - 1;
    SysTick->VAL  = 0;
    SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; // 关中断
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
    while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk));
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}

void BSP_DelayMs(uint32_t ms)
{
    while (ms--) BSP_DelayUs(1000);
}
使用查询式延时时,注意不要与中断式SysTick混用。如果系统中已经用SysTick做1ms中断,BSP_DelayUs 会修改LOAD和VAL,破坏中断节拍。此时应改用DWT周期计数器或独立的定时器实现微秒延时。

六、验证与常见问题

时钟配置完成后,可以通过MCO引脚输出SYSCLK到PA8,用示波器测量是否为72MHz;也可以翻转GPIO,用示波器观察方波周期是否符合预期。常见问题包括:HSE起振失败导致程序卡在等待循环(可加超时退出并切换到HSI);APB1分频忘记设置导致外设超频;SysTick重装载值计算错误导致延时偏差。建议在BSP初始化完成后,用 RCC_GetClocksFreq() 读取各总线频率并打印到串口,作为上电自检的一部分。

把时钟树配置、总线分频、SysTick和延时封装统一收进BSP层,后续每个外设驱动只需调用 BSP_RCC_APBx_ENABLE() 和 BSP_DelayMs(),就能把注意力集中在业务逻辑上,这也是BSP工程化的基本要求。