3. 系统时钟与定时器基础:STM32时钟树详解
时钟,是单片机的心脏。没有时钟,芯片里所有逻辑单元都动不了。我刚开始学STM32时,总觉得时钟配置就是抄几个寄存器值,后来才发现——时钟树要是没理清楚,整个系统跑起来都是歪的。
这一章,咱们把STM32的时钟树掰开揉碎,从HSE、HSI到PLL,再到SysTick和基本定时器,一条路走通。
3.1 时钟树概览:谁给谁供时钟?
STM32的时钟源有好几个,它们不是随便接的,而是通过一个叫“时钟树”的结构,逐级分配。说白了,就是先选一个“根时钟”,然后经过分频、倍频,再分给各个外设。
核心原则:系统时钟(SYSCLK)是所有外设时钟的源头。SYSCLK最高72MHz,超过这个值,芯片会不稳定甚至锁死。
我画了一张时钟树的简化图,帮你快速建立整体认知:
3.2 时钟源详解:HSE、HSI、PLL
3.2.1 HSE(外部高速晶振)
HSE是外部晶振,通常接4~16MHz的晶体。我个人习惯用8MHz,因为后面PLL倍频到72MHz刚好是整数倍(×9)。
- 优点:精度高,温漂小,适合对时序敏感的场景(如串口通信、USB)。
- 缺点:需要外部电路,占PCB面积,成本略高。
- 我踩过的坑:曾经在画板子时,晶振离芯片太远,走线太长,结果起振不稳定。后来把晶振紧贴OSC_IN/OUT引脚,问题就解决了。
3.2.2 HSI(内部高速RC振荡器)
HSI是芯片内部自带的RC振荡器,频率8MHz。上电后默认就是HSI作为系统时钟。
- 优点:无需外部元件,上电即用,省成本。
- 缺点:精度差(约±1%),温漂大。做串口通信时,波特率容易偏。
我的建议:如果项目对成本敏感、对时序要求不高(比如只控制LED、读取按键),直接用HSI就够了。但如果你要用串口、CAN、USB,老老实实上HSE。
3.2.3 PLL(锁相环倍频)
PLL的作用是把HSE或HSI的频率“翻倍”。STM32F103的PLL最高输出72MHz。
配置PLL时,有几个参数要搞清楚:
| 参数 | 说明 | 典型值 |
|---|---|---|
| PLLSRC | PLL输入源选择(HSE或HSI/2) | HSE |
| PLLMUL | 倍频系数(2~16) | 9(8MHz×9=72MHz) |
| PLLXTPRE | HSE预分频(1或2) | 1 |
注意:PLL输出频率不能超过72MHz。我曾经见过有人把PLLMUL设成16,8MHz×16=128MHz,结果芯片直接死机,电流飙升。嗯,那次差点烧了开发板。
3.3 配置系统时钟(72MHz)
下面是一段标准的系统时钟配置代码,把SYSCLK设为72MHz。我习惯在工程初始化时直接调用这个函数。
void SystemClock_Config(void)
{
// 1. 使能HSE,等待就绪
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY));
// 2. 配置FLASH预取指、等待周期(72MHz需要2个等待周期)
FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
// 3. 配置AHB、APB1、APB2分频
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // AHB不分频 = 72MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // APB2不分频 = 72MHz
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1二分频 = 36MHz
// 4. 配置PLL:HSE作为源,倍频系数9
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
// 5. 使能PLL,等待就绪
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
// 6. 选择PLL作为系统时钟
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
关键点:配置顺序不能乱。先使能时钟源,再配置分频和PLL,最后切换系统时钟。跳步会导致时钟切换失败,系统跑在默认的HSI上。
3.4 SysTick定时器(系统滴答)
SysTick是Cortex-M3内核自带的24位递减计数器。它不依赖外设定时器,所以哪怕你换了芯片型号,SysTick的用法基本不变。
SysTick的作用有两个:
- 给操作系统(如FreeRTOS)提供心跳节拍。
- 实现精确的软件延时。
它的工作原理很简单:从重装载值开始递减,减到0时触发中断,然后自动重新装载。中断频率由你设定的重装载值决定。
计算公式:
SysTick中断频率 = 系统时钟频率 / (重装载值 + 1)
比如系统时钟72MHz,想要1ms中断一次:
重装载值 = 72000000 / 1000 - 1 = 71999
3.5 使用SysTick实现精确延时
我项目中常用的延时函数,就是基于SysTick的。下面给出一套完整的实现:
// 全局变量,记录SysTick计数
static volatile uint32_t uwTick;
// SysTick中断服务函数
void SysTick_Handler(void)
{
uwTick++;
}
// 初始化SysTick,1ms中断一次
void SysTick_Init(void)
{
// 重装载值 = 72MHz / 1000 - 1
SysTick->LOAD = 72000 - 1;
// 使能SysTick中断,使用处理器时钟
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
// 获取当前tick值
uint32_t GetTick(void)
{
return uwTick;
}
// 毫秒延时
void Delay_ms(uint32_t ms)
{
uint32_t start = GetTick();
while((GetTick() - start) < ms);
}
避坑指南:我曾经在中断里直接调用Delay_ms(),结果死锁了。因为SysTick中断优先级比当前中断低,导致uwTick一直不更新。解决办法:要么不用阻塞延时,要么把SysTick优先级设到最高。
3.6 基本定时器TIM6/TIM7
TIM6和TIM7是STM32里最“纯粹”的定时器。它们只有基本的计数功能,没有输入捕获、输出比较、PWM等花哨功能。但正因为简单,所以稳定可靠。
| 特性 | TIM6/TIM7 | 通用定时器(TIM2~TIM5) |
|---|---|---|
| 计数方向 | 仅向上 | 向上/向下/中心对齐 |
| 输入捕获 | 无 | 有 |
| 输出比较/PWM | 无 | 有 |
| DMA支持 | 有 | 有 |
| 主要用途 | 定时中断、触发DAC | PWM、脉冲测量、编码器 |
TIM6/TIM7挂在APB1总线上。如果APB1分频系数不是1,那么定时器的时钟频率会是APB1的两倍。这个细节容易忽略,我当初调试时发现定时器跑得比预期快一倍,查了半天才发现是APB1分频导致的。
配置TIM6产生1秒中断的示例:
void TIM6_Config(void)
{
// 使能TIM6时钟(APB1)
RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
// 预分频器:72MHz / 7200 = 10kHz
TIM6->PSC = 7200 - 1;
// 自动重装载值:10kHz / 10000 = 1Hz
TIM6->ARR = 10000 - 1;
// 更新中断使能
TIM6->DIER |= TIM_DIER_UIE;
// 使能计数器
TIM6->CR1 |= TIM_CR1_CEN;
// 配置NVIC
NVIC_EnableIRQ(TIM6_IRQn);
NVIC_SetPriority(TIM6_IRQn, 1);
}
// 中断服务函数
void TIM6_IRQHandler(void)
{
if(TIM6->SR & TIM_SR_UIF)
{
TIM6->SR = ~TIM_SR_UIF; // 清除标志
// 用户代码:每秒执行一次
}
}
重要提醒:定时器中断服务函数里,一定要先清除中断标志位,再执行用户代码。否则会反复进入中断,导致程序卡死。我刚开始写STM32时,就因为这个bug排查了一下午。
3.7 本章小结
时钟和定时器,是STM32开发的基石。你想想看,如果时钟配错了,后面所有外设的时序都是错的。SysTick和TIM6/TIM7虽然简单,但用好了,能解决大部分定时需求。
我个人建议:新项目上手,先把系统时钟配到72MHz,然后用SysTick做一个1ms的延时函数,再跑一个LED闪烁。这一步走通了,后面就顺了。