4. 时钟系统与定时器:芯片的“心跳”与“秒表”
时钟,是嵌入式系统的绝对命脉。没有时钟,CPU 就是一块死寂的硅片。我刚开始做 BSP 时,总觉得时钟配置就是填几个寄存器,没什么技术含量。直到有一次,我调了三天三夜,板子就是跑不起来,最后发现是 PLL 的锁定时间没给够。嗯,从那以后,我再也不敢小看时钟树了。
这一章,我们就把时钟系统和定时器彻底讲透。从时钟树的源头,到 SysTick 的精准延时,再到通用定时器的 PWM 和输入捕获,一步步带你掌握。
4.1 系统时钟树解析
说白了,时钟树就是芯片内部所有时钟信号的“供水管网”。源头是几个振荡器,中间经过 PLL 倍频、分频器分频,最后送到各个外设。你想想看,如果水管压力不稳,后面的设备肯定出问题。
4.1.1 时钟源:HSI、HSE、LSI、LSE
主流 MCU 通常有四个时钟源,我整理了一个表格,方便你对比:
| 时钟源 | 全称 | 典型频率 | 精度 | 典型用途 |
|---|---|---|---|---|
| HSI | 高速内部振荡器 | 8 MHz / 16 MHz | ±1% ~ ±2% | 上电默认时钟,低成本方案 |
| HSE | 高速外部振荡器 | 4 ~ 26 MHz | ±50 ppm(晶振) | PLL 输入,高精度系统时钟 |
| LSI | 低速内部振荡器 | 32 kHz / 40 kHz | ±5% ~ ±10% | 独立看门狗,RTC 备用 |
| LSE | 低速外部振荡器 | 32.768 kHz | ±20 ppm | 高精度 RTC 时钟 |
这里有个关键点:HSI 虽然启动快,但精度差。我在一个需要 USB 通信的项目里,偷懒用了 HSI,结果 USB 枚举死活过不去。后来换成 HSE + PLL,一次通过。所以,涉及通信的场景,老老实实用外部晶振。
4.1.2 PLL 倍频与系统时钟路径
PLL(锁相环)是时钟树的核心。它能把低频输入(比如 8 MHz HSE)倍频到几百 MHz。但 PLL 不是万能的,它有输入频率范围、倍频系数限制、VCO 频率范围等约束。
我画了一张典型的时钟树路径图,你看一眼就明白了:
从图里你能看到,HSE 或 HSI 经过时钟选择器后,进入 PLL 倍频,然后成为 SYSCLK。SYSCLK 再经过 AHB 分频器,分给 APB1 和 APB2 总线。APB1 通常跑低速外设(如 UART、I2C),APB2 跑高速外设(如 SPI、TIM)。
4.2 时钟配置驱动开发
时钟配置的代码,说白了就是操作 RCC(Reset and Clock Control)寄存器。我个人习惯把时钟配置封装成一个函数,放在 bsp_clk.c 里。
4.2.1 标准配置流程
以 STM32F103 为例,配置 72 MHz 系统时钟的步骤:
- 使能 HSE,等待 HSE 就绪。
- 配置 Flash 预取缓冲,设置等待周期(因为主频高了,Flash 跟不上)。
- 配置 PLL:设置 PLL 源为 HSE,设置倍频系数。
- 使能 PLL,等待 PLL 锁定。
- 切换系统时钟为 PLL 输出。
- 配置 AHB、APB1、APB2 分频器。
代码示例:
void SystemClock_Config(void)
{
// 1. 使能 HSE
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY));
// 2. 配置 Flash 等待周期
FLASH->ACR = FLASH_ACR_LATENCY_2 | FLASH_ACR_PRFTBE;
// 3. 配置 PLL: HSE 8 MHz × 9 = 72 MHz
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
// 4. 使能 PLL
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY));
// 5. 切换系统时钟
RCC->CFGR &= ~RCC_CFGR_SW;
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
// 6. 配置总线分频
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // AHB = 72 MHz
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = 36 MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // APB2 = 72 MHz
}
4.3 SysTick 定时器与延时实现
SysTick 是 Cortex-M 内核自带的 24 位递减计数器。它不依赖外设,所以移植性极好。你想想看,不管换什么 MCU,只要内核是 Cortex-M,SysTick 的代码基本不用改。
4.3.1 工作原理
SysTick 从重装载值开始递减,减到 0 时触发中断,然后自动重装。它的时钟源通常是 HCLK/8 或 HCLK。我个人习惯用 HCLK/8,这样计数范围更大。
实现微秒级延时,核心思路是:
- 先计算 SysTick 每减一次需要多少微秒。
- 然后设置重装载值,启动计数。
- 等待计数到 0。
代码示例:
static volatile uint32_t us_tick;
void SysTick_Init(void)
{
// 配置 SysTick 时钟源为 HCLK/8
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_0;
}
void delay_us(uint32_t us)
{
uint32_t reload = SystemCoreClock / 8000000; // 每微秒的计数次数
SysTick->LOAD = us * reload - 1;
SysTick->VAL = 0;
SysTick->CTRL |= SysTick_CTRL_ENABLE;
while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG));
SysTick->CTRL &= ~SysTick_CTRL_ENABLE;
}
4.4 通用定时器:PWM 输出与输入捕获
通用定时器(TIM)是 MCU 里最灵活的外设之一。它既能输出 PWM,又能测量外部信号的频率和脉宽。说白了,它就是一块多功能“秒表”。
4.4.1 PWM 输出
PWM 输出的核心是:自动重装载寄存器(ARR)决定周期,捕获/比较寄存器(CCR)决定占空比。
配置步骤:
- 使能 TIM 时钟,配置 GPIO 为复用推挽输出。
- 设置 ARR 和 PSC(预分频器),确定 PWM 频率。
- 设置 CCR,确定占空比。
- 配置输出比较模式为 PWM 模式 1 或 2。
- 使能 TIM 的捕获/比较输出,使能 TIM。
代码示例(TIM2,通道 1,PA0):
void PWM_Init(void)
{
// 1. 使能时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// 配置 PA0 为复用推挽输出
GPIOA->CRL &= ~GPIO_CRL_CNF0;
GPIOA->CRL |= GPIO_CRL_CNF0_1; // 复用推挽
GPIOA->CRL |= GPIO_CRL_MODE0; // 50 MHz 输出
// 2. 设置频率: 72 MHz / (71+1) / (999+1) = 1 kHz
TIM2->PSC = 71;
TIM2->ARR = 999;
// 3. 设置占空比 50%
TIM2->CCR1 = 500;
// 4. 配置 PWM 模式 1
TIM2->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;
TIM2->CCMR1 |= TIM_CCMR1_OC1PE; // 预装载使能
// 5. 使能输出和定时器
TIM2->CCER |= TIM_CCER_CC1E;
TIM2->CR1 |= TIM_CR1_CEN;
}
void PWM_SetDuty(uint16_t duty)
{
TIM2->CCR1 = duty;
}
4.4.2 输入捕获
输入捕获用来测量外部信号的频率或脉宽。原理是:检测到指定边沿时,把当前计数器的值锁存到 CCR 寄存器。
测量脉宽的步骤:
- 配置通道为输入模式,设置触发边沿(上升沿或下降沿)。
- 使能捕获中断。
- 在中断里,第一次捕获到上升沿时,记录 CCR 值。
- 第二次捕获到下降沿时,计算差值,得到高电平脉宽。
代码片段:
volatile uint32_t cap_start, cap_end, pulse_width;
void TIM_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_CC1IF)
{
if (TIM2->CCER & TIM_CCER_CC1P) // 下降沿
{
cap_end = TIM2->CCR1;
pulse_width = cap_end - cap_start;
TIM2->CCER |= TIM_CCER_CC1P; // 切换回上升沿
}
else // 上升沿
{
cap_start = TIM2->CCR1;
TIM2->CCER &= ~TIM_CCER_CC1P; // 切换为下降沿
}
TIM2->SR &= ~TIM_SR_CC1IF;
}
}
4.5 本章小结
时钟系统和定时器,是 BSP 开发的基础。你掌握了时钟树,就能理解芯片的功耗和性能边界;你掌握了 SysTick,就能实现精准延时;你掌握了通用定时器,就能驱动电机、测量传感器信号。
嗯,这一章的内容就到这里。记住,时钟配置是“一票否决”项——配错了,整个系统都跑不起来。所以,每次改时钟配置,我都会用示波器确认一下。这个习惯,救了我很多次。