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”。理解这条链路,才能明白为什么串口波特率计算、定时器周期计算都要先确认对应总线的实际频率。
二、用标准库配置时钟
在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);
}
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工程化的基本要求。