4、时钟与定时器:系统时钟树、PLL配置、定时器驱动、Tick中断服务
时钟,说白了就是嵌入式系统的「心跳」。没有时钟,CPU 没法跑,外设没法转,整个系统就是一摊死硅。我见过不少刚入行的工程师,花大把时间调外设驱动,结果系统跑不起来——最后发现是时钟树配错了。嗯,这章咱们就把时钟和定时器彻底讲透。
4.1 系统时钟树——你得先看懂这张图
每颗 SoC 的手册里都有一张时钟树图,看着像地铁线路图。我个人习惯,拿到一个新平台,第一件事不是看 GPIO,而是先找到时钟树。为什么?因为所有外设的时钟源、分频系数、使能开关,全在这张图上。
时钟树的核心逻辑其实不复杂:
- 时钟源:晶振(HSE/LSE)、内部 RC 振荡器(HSI/LSI)、PLL 输出
- 时钟路径:源 → 分频/倍频 → 开关 → 外设总线
- 时钟门控:每个外设模块前都有一个时钟使能位,关了就能省电
核心原则:CPU 核、总线、外设,各自有最高频率限制。配时钟树就是在「性能」和「稳定性」之间找平衡。
我在项目中遇到过一件事:某款工业控制器,量产 100 台后有 3 台上电后串口乱码。查了两周,最后发现是 HSI 校准值没写对,导致 PLL 输出频率漂了 2%。从那以后,我所有时钟配置代码里都加了频率测量和校验逻辑。
4.2 PLL 配置——倍频不是你想乘就能乘
PLL(锁相环)是时钟树里最关键的模块。它的作用是把低频时钟源(比如 8MHz 晶振)倍频到高频(比如 400MHz 给 CPU 核)。
典型的 PLL 配置流程:
- 选择输入源:HSE 还是 HSI?我建议优先用 HSE,精度高、温漂小。
- 设置分频系数(M):把输入频率降到 PLL 的参考输入范围内(通常是 1-2MHz)。
- 设置倍频系数(N):决定输出频率,N × 参考频率 = VCO 频率。
- 设置后分频(P/Q/R):把 VCO 频率分频到目标值。
- 等待 PLL 锁定:必须检查锁定标志位,否则输出频率不稳定。
/* STM32F4 PLL 配置示例:HSE 8MHz → 系统时钟 168MHz */
void SystemClock_Config(void)
{
RCC->CR |= RCC_CR_HSEON; /* 使能 HSE */
while(!(RCC->CR & RCC_CR_HSERDY)); /* 等待 HSE 就绪 */
RCC->PLLCFGR = (8 << 0) | /* PLL_M = 8 */
(336 << 6) | /* PLL_N = 336 */
(2 << 16) | /* PLL_P = 4 */
(7 << 24); /* PLL_Q = 7 */
RCC->CR |= RCC_CR_PLLON; /* 使能 PLL */
while(!(RCC->CR & RCC_CR_PLLRDY)); /* 等待 PLL 锁定 */
FLASH->ACR = FLASH_ACR_LATENCY_5WS; /* 配置 Flash 等待周期 */
RCC->CFGR |= RCC_CFGR_SW_PLL; /* 切换系统时钟为 PLL */
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
我曾经踩过的坑:PLL 配置完成后,一定要检查 VCO 频率是否在手册规定的范围内(通常是 100-432MHz)。超了,芯片会随机死机;低了,PLL 锁不住。别问我怎么知道的。
4.3 定时器驱动——不只是数数
嵌入式里的定时器,分三类:
| 类型 | 位数 | 典型用途 | 我常用的场景 |
|---|---|---|---|
| 基本定时器 | 16-bit | 定时中断、延时 | 按键消抖、轮询间隔 |
| 通用定时器 | 16/32-bit | PWM、输入捕获、输出比较 | 电机控制、脉冲计数 |
| 高级定时器 | 16/32-bit | 互补PWM、死区插入、刹车 | 三相逆变器、开关电源 |
定时器驱动的核心就三件事:时钟源选择、预分频器设置、自动重装载值配置。
你想想看,一个 16 位定时器,最大计数值是 65535。如果时钟频率是 72MHz,不分频的话,一个计数周期才 13.9ns,定时器只能跑 0.9ms 就溢出了。所以必须用预分频器把时钟降下来。
/* 通用定时器初始化:1ms 中断周期 */
void TIM_Init(TIM_TypeDef* TIMx, uint32_t period_ms)
{
uint32_t psc, arr;
uint32_t tim_clk = 72000000; /* 假设定时器时钟 72MHz */
/* 计算预分频系数:让计数器时钟为 1MHz(1us 一个 tick) */
psc = (tim_clk / 1000000) - 1;
/* 计算自动重装载值:1ms = 1000 个 tick */
arr = (1000 * period_ms) - 1;
TIMx->PSC = psc;
TIMx->ARR = arr;
TIMx->EGR = TIM_EGR_UG; /* 产生更新事件,重新加载寄存器 */
TIMx->DIER |= TIM_DIER_UIE; /* 使能更新中断 */
TIMx->CR1 |= TIM_CR1_CEN; /* 启动定时器 */
}
个人经验:预分频器和自动重装载值,尽量用整数除法算,别用浮点。嵌入式里浮点运算慢,而且容易引入精度误差。我习惯先算好分频系数,再反算验证一下实际频率。
4.4 Tick 中断服务——系统的脉搏
Tick 中断,就是系统定时器(SysTick)每 1ms 触发一次的中断。它是 RTOS 的心跳,也是裸机延时函数的基础。
SysTick 是 Cortex-M 内核自带的 24 位递减计数器。配置起来很简单:
/* SysTick 初始化:1ms 中断 */
void SysTick_Init(void)
{
uint32_t reload = SystemCoreClock / 1000; /* 1ms 的计数值 */
SysTick->LOAD = reload - 1;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
/* Tick 中断服务函数 */
void SysTick_Handler(void)
{
static uint32_t tick_count = 0;
tick_count++;
/* 裸机延时:每 1ms 减一次 */
if(delay_ms_counter > 0)
delay_ms_counter--;
/* RTOS 的 Tick 钩子 */
#ifdef USE_RTOS
xPortSysTickHandler();
#endif
}
这里有个细节:SysTick 中断里不要做耗时操作。我见过有人直接在中断里打印日志,结果系统卡死。Tick 中断应该只做三件事:
- 更新系统滴答计数
- 处理软件定时器
- 触发 RTOS 调度
我曾经犯过的错:在 Tick 中断里调用了 printf,结果串口还没发完,下一次 Tick 又来了。中断嵌套把栈撑爆了,系统直接 HardFault。从那以后,我所有中断服务函数都遵循「快进快出」原则。
4.5 时钟与定时器知识体系
下面这张图,是我自己总结的时钟与定时器知识结构。你把它记熟了,遇到任何 SoC 都能快速上手。
这张图把整个知识体系串起来了。你从上往下看:时钟源经过 PLL 倍频后,送入时钟树分配;时钟树给各个定时器提供时钟;定时器驱动配置好预分频和重装载值后,产生中断;Tick 中断服务负责系统心跳。每一步都环环相扣。
总结一下:时钟和定时器是 BSP 里最基础也最容易出错的模块。我的建议是——拿到新板子,先花半天把时钟树吃透,把 PLL 配稳,把 Tick 中断调准。后面所有驱动开发都会顺很多。