在STM32嵌入式开发中,时钟系统就像人的心跳一样至关重要。理解时钟树的结构和编程方法,是掌握STM32性能调优的基础。本文将结合标准库,深入剖析STM32时钟树的原理与配置,帮助你从零开始掌握时钟编程。

一、时钟树基础:概念与核心组件

时钟信号是一种高低变化的方波信号,它决定了片上外设的工作速度。STM32的时钟系统以树形结构组织,称为时钟树。时钟树的核心组件包括:

  • 分频器:对频率做除法,降低时钟频率。
  • 锁相环:对频率做乘法,提高时钟频率。
  • 复用器:选择不同的时钟源。

时钟源分类:STM32拥有两套时钟树——高速树(几十MHz)和低速树(几十kHz),每套树有两个时钟源(内部和外部)。内部时钟源(HSI/LSI)集成在芯片内部,精度有限;外部时钟源(HSE/LSE)通过引脚外接晶振,精度更高。例如,通过PC14/PC15引脚外接LSE晶振,通过PD0/PD1引脚外接HSE晶振。

LSI(Low Speed Internal)

LSE(Low Speed External)
HSI(High Speed Internal)HSE(High Speed External)

二、树干与树枝:系统时钟的分发

时钟树的树干是系统时钟SYSCLK,它决定了CPU和总线的运行速度。SYSCLK有三个来源:

  • 来自HSI:精度低,固定8MHz。
  • 来自HSE:精度高,等于外部晶振频率。
  • 来自锁相环:灵活可调,可配置为8/12/16/20/36/72MHz等。

树枝部分包括三条总线:

  • AHB(高级高速总线):连接CPU、Flash、DMA等高速设备。
  • APB1(高级外设总线1):连接低速外设,如USART2、I2C1。
  • APB2(高级外设总线2):连接高速外设,如GPIO、SPI1。

⚠️ 配置示例:假设SYSCLK=72MHz,若要求HCLK=36MHz、PCLK1=9MHz、PCLK2=36MHz,则需设置AHB分频系数为2、APB1分频系数为4、APB2分频系数为1。这种配置在需要平衡性能和功耗时非常常见。

三、时钟树的初始状态与启动代码

STM32复位后,时钟树处于默认状态:SYSCLK直接来自HSI(8MHz),AHB、APB1、APB2分频系数均为1。这意味着CPU以8MHz运行。例如,以下代码使用for循环实现500ms延迟:

#include "stm32f10x.h"
int main(void)
{
	/*启动GPIOC时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
	/*声明GPIO结构变量*/
	GPIO_InitTypeDef GPIO_InitStruct;
	/*选择PC13引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
	/*输出开漏模式*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
	/*最大输出速率为2MHz*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	/*初始化PA0引脚*/
	GPIO_Init(GPIOC,&GPIO_InitStruct);
	while(1)
	{
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
		for(uint32_t i = 0;i < 400000;i++);
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
		for(uint32_t i = 0;i < 400000;i++);
	}
}

然而,实际项目中我们通常需要更高性能。标准库的启动文件startup_stm32f10x_hd.s中,复位处理程序Reset_Handler会调用SystemInit()函数,自动将时钟树配置为最高频率(通常是72MHz)。你可以通过注释掉相关代码来保留默认频率。

#include "stm32f10x.h"
int main(void)
{
	/*启动GPIOC时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
	/*声明GPIO结构变量*/
	GPIO_InitTypeDef GPIO_InitStruct;
	/*选择PC13引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
	/*输出开漏模式*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
	/*最大输出速率为2MHz*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	/*初始化PA0引脚*/
	GPIO_Init(GPIOC,&GPIO_InitStruct);
	while(1)
	{
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
		for(uint32_t i = 0;i < 666666;i++);
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
		for(uint32_t i = 0;i < 666666;i++);
	}
}

RCC接口:RCC(Reset And Clock Controller)是时钟编程的核心外设。标准库提供了丰富的函数接口,如RCC_HSEConfig()RCC_PLLConfig()等。

void RCC_HSEConfig(uint32_t RCC_HSE);//HSE开关
void RCC_HSICmd(FounctionalState NewState);//HSI开关
void RCC_PLLConfig(uint32_t RCC_PLLSource,uint32_t RCC_PLLMul);//锁相环配置
void RCC_PLLCmd(FunctionalState NewState);//锁相环开关
void RCC_SYSCLKConfig(uint32_t RCC_SYSCLKSource);//SYSCLK配置
void RCC_HCLKConfig(uint32_T RCC_SYSCLK);//HCLK配置
void RCC_PCLK1Config(uint32_T RCC_HCLK);//PCLK1配置
void RCC_PCLK2Config(uint32_T RCC_HCLK);//PCLK2配置
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);//获取RCC状态
uint8_t RCC_GetSTSCLKSource(void);//获取SYSCLK来源

四、时钟树编程实战:一步步配置

配置时钟树通常遵循以下步骤:

  1. 开启HSE时钟并等待就绪。
  2. 配置锁相环参数并启动。
  3. 配置AHB、APB1、APB2分频系数。
  4. 选择SYSCLK来源并验证。

4.1 启动HSE

使用RCC_HSEConfig(RCC_HSE_ON)开启外部高速晶振,然后调用RCC_WaitForHSEStartUp()等待就绪。

//HSE开关
void RCC_HSEConfig(uint32_t RCC_HSE);
//获取RCC的状态
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);
//#1:开启HSE
RCC_HSEConfig(RCC_HSE_ON);
while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET);

4.2 配置锁相环

使用RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9)设置PLL输入源和倍频系数。例如,HSE=8MHz,倍频9倍,得到72MHz。

//配置锁相环的参数
void RCC_PLLConfig(uint32_t RCC_PLLSource,uint32_t RCC_PLLMul);
//控制锁相环开关
void RCC_PLLCmd(FunctionalStatus NewState);
//获取RCC的状态
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);
//#2:配置并启动锁相环
RCC_PLLConfig(RCC_PLLSource_HSE_Div1,RCC_PLLMul_9);
RCC_PLLCmd(ENABLE);
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);

4.3 配置分频器

设置AHB、APB1、APB2分频系数,例如RCC_HCLKConfig(RCC_SYSCLK_Div1)

//配置AHB分频器的分频系数
void RCC_HCLKConfig(uint32_t RCC_SYSCLK);
//配置APB1分频器的分频系数
void RCC_PCLK1Config(uint32_t RCC_HCLK);
//配置APB2分频器的分频系数
void RCC_PCLK2Config(uint32_t RCC_HCLK);
//#3:配置AHB APB1 APB2的分频系数
/*配置AHB分频器的分频系数*/
RCC_HCLKConfig(RCC_SYSCLK_Div1);
/*配置APB1分频器的分频系数*/
RCC_PCLK1Config(RCC_HCLK_Div2);
/*配置APB2分频器的分频系数*/
RCC_PCLK2Config(RCC_HCLK_Div2);

4.4 选择SYSCLK来源

使用RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK)切换SYSCLK来源,并通过RCC_GetSYSCLKSource()验证。

//设置SYSCLK的来源
void RCC_SYSCLKConfig(uint32_t RCC_SYSCLKSource);
//获取SYSCLK的来源
uint8_t RCC_GetSYSCLKSource(void);
//#4:切换SYSCLK的来源
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
while(RCC_GetSYSCLKSource() != 0x08);

4.5 配置Flash指令预取

当SYSCLK较高时,CPU从Flash读取指令的速度可能跟不上。通过配置Flash预取缓冲区,可以提升性能。使用FLASH_PrefetchBufferCmd(ENABLE)启用预取,FLASH_SetLatency(FLASH_Latency_2)设置等待周期。

//开启Flash指令预取
FLASH_PrefetchBufferCmd(ENABLE);
//设置Flash访问延迟
FLASH_SetLatency(FLASH_Latency_2);

完整代码示例:以下是配置72MHz时钟的完整代码。

#include "stm32f10x.h"
void App_SystemClock_Init(void);
int main(void)
{
	App_SystemClock_Init();
	/*启动GPIOC时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
	/*声明GPIO结构变量*/
	GPIO_InitTypeDef GPIO_InitStruct;
	/*选择PC13引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
	/*输出开漏模式*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
	/*最大输出速率为2MHz*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	/*初始化PA0引脚*/
	GPIO_Init(GPIOC,&GPIO_InitStruct);
	while(1)
	{
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
		for(uint32_t i = 0;i < 666666;i++);
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
		for(uint32_t i = 0;i < 666666;i++);
	}
}
void App_SystemClock_Init(void)
{
	//开启Flash指令预取
	FLASH_PrefetchBufferCmd(ENABLE);
	//设置Flash访问延迟
	FLASH_SetLatency(FLASH_Latency_2);
	//#1:开启HSE
	RCC_HSEConfig(RCC_HSE_ON);
	while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET);
	//#2:配置并启动锁相环
	RCC_PLLConfig(RCC_PLLSource_HSE_Div1,RCC_PLLMul_9);
	RCC_PLLCmd(ENABLE);
	while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
	//#3:配置AHB APB1 APB2的分频系数
	/*配置AHB分频器的分频系数*/
	RCC_HCLKConfig(RCC_SYSCLK_Div1);
	/*配置APB1分频器的分频系数*/
	RCC_PCLK1Config(RCC_HCLK_Div2);
	/*配置APB2分频器的分频系数*/
	RCC_PCLK2Config(RCC_HCLK_Div2);
	//#4:切换SYSCLK的来源
	RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
	while(RCC_GetSYSCLKSource() != 0x08);
}

五、外设时钟管理与复位

除了系统时钟,每个外设的时钟需要单独开启。使用RCC_APB1PeriphClockCmd()RCC_APB2PeriphClockCmd()RCC_AHBPeriphClockCmd()来开启对应总线上的外设时钟。例如:

  • 开启GPIOA时钟:RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)
  • 开启USART2时钟:RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE)
  • 开启DMA1时钟:RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE)

复位外设使用RCC_APB1PeriphResetCmd()RCC_APB2PeriphResetCmd(),例如复位I2C1:RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE)

//开关AHB总线上的片上外设的时钟
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph,FunctionalStatus NewState);
//开关APB2总线上的片上外设的时钟
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph,FunctionalStatus NewState);
//开关APB1总线上的片上外设的时钟
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph,FunctionalStatus NewState);
//复位APB2总线上的片上外设
void RCC_APB2PeriphResetCmd(uint32_t RCC_APB2Periph,FunctionalStatus NewState);
//复位APB1总线上的片上外设
void RCC_APB1PeriphResetCmd(uint32_t RCC_APB1Periph,FunctionalStatus NewState);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,ENABLE);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,DISABLE);

实践建议:在调试阶段,建议使用HSI或HSE直接作为SYSCLK,避免锁相环引入的抖动。在性能敏感的应用中,优先使用PLL输出72MHz。此外,注意外设时钟的开启顺序,避免在时钟未就绪时访问外设寄存器。

延伸思考:时钟配置不仅影响性能,还影响功耗。在低功耗项目中,可以动态切换SYSCLK来源或降低总线频率。类似地,在Java、Python或Go等高级语言开发的物联网项目中,底层时钟配置决定了通信时序和采样精度。

[AFFILIATE_SLOT_1]

⚠️ 常见错误:忘记等待HSE或PLL就绪就切换SYSCLK,会导致系统死机。在配置PLL时,务必先切换SYSCLK到其他源(如HSI),再修改PLL参数。

[AFFILIATE_SLOT_2]

总结

STM32时钟系统看似复杂,但掌握时钟树的结构和标准库编程接口后,配置起来并不困难。关键步骤包括:选择时钟源、配置锁相环、设置分频系数、切换SYSCLK来源,以及开启外设时钟。通过本文的实战代码,你可以快速上手时钟编程,为后续外设驱动开发打下坚实基础。