3. 系统时钟与定时器:时钟树详解、SysTick定时器、模块化封装延时函数、实战:精准闪烁LED
时钟,是单片机的“心跳”。没有时钟,CPU 无法执行指令,外设也无法工作。很多初学者觉得时钟配置很神秘,其实说白了,就是给芯片的各个部分分配一个稳定的“节拍”。
我个人习惯,拿到一块新的 STM32 板子,第一件事就是先把时钟树理清楚。为什么?因为后面所有的定时、通信、PWM 都依赖它。时钟乱了,整个系统就乱了。
3.1 时钟树:STM32 的“心脏”与“血管”
STM32 的时钟源有好几个,它们之间的关系就像一棵树——根是振荡器,干是 PLL,枝是总线时钟。我画了一张图,帮你快速建立整体认知。
这张图里,你重点关注三个东西:时钟源、PLL 倍频、总线分频。我刚开始学的时候,总搞不清 HSI 和 HSE 到底用哪个。后来在项目里吃过亏——用 HSI 做 USART 通信,结果波特率偏差太大,数据全是乱码。从那以后,只要涉及通信,我必用 HSE 外部晶振。
3.2 SysTick 定时器:Cortex-M 内核自带的“节拍器”
SysTick 是 Cortex-M 内核内部的一个 24 位递减计数器。它不属于 STM32 外设,而是 ARM 内核自带的。这意味着,不管你是 STM32F1 还是 F4,SysTick 的用法完全一样。
它的工作原理很简单:
- 从重装载值开始,每个时钟周期减 1
- 减到 0 时,触发一次中断,并自动重新装载
- 24 位计数器,最大计数值 16,777,215
你想想看,如果我们把 SysTick 配置成 1ms 中断一次,那它就是一个精准的“心跳”。延时函数、操作系统时钟节拍,都靠它。
3.2.1 SysTick 寄存器一览
| 寄存器 | 地址偏移 | 功能描述 |
|---|---|---|
| CTRL | 0x00 | 控制寄存器:使能、时钟源选择、中断使能 |
| LOAD | 0x04 | 重装载值寄存器(24位有效) |
| VAL | 0x08 | 当前计数值寄存器(读清零) |
| CALIB | 0x0C | 校准寄存器(一般不用) |
3.3 模块化封装延时函数
裸机开发里,延时函数是最基础的工具。但很多人直接写死循环空转,既不精准,又浪费 CPU。我习惯把延时函数封装成一个独立模块,方便移植和复用。
3.3.1 头文件设计:systick.h
#ifndef __SYSTICK_H
#define __SYSTICK_H
#include "stm32f10x.h" // 根据芯片型号调整
// 初始化 SysTick,配置为 1ms 中断一次
void SysTick_Init(void);
// 毫秒级延时
void delay_ms(uint32_t ms);
// 微秒级延时(通过循环精细调整)
void delay_us(uint32_t us);
#endif
3.3.2 实现文件:systick.c
#include "systick.h"
// 全局变量,用于记录 SysTick 中断次数
static volatile uint32_t sysTickCounter = 0;
void SysTick_Init(void)
{
// 1. 关闭 SysTick
SysTick->CTRL = 0;
// 2. 设置重装载值:72MHz / 1000 = 72000
// 即每 1ms 产生一次中断
SysTick->LOAD = 72000 - 1;
// 3. 清空当前计数值
SysTick->VAL = 0;
// 4. 配置时钟源为 HCLK (72MHz)
// 使能中断,使能计数器
SysTick->CTRL = (1 << 2) | // CLKSOURCE: HCLK
(1 << 1) | // TICKINT: 使能中断
(1 << 0); // ENABLE: 启动
}
void delay_ms(uint32_t ms)
{
uint32_t start = sysTickCounter;
while ((sysTickCounter - start) < ms);
}
void delay_us(uint32_t us)
{
// 微秒级延时:用循环精细控制
// 72MHz 下,一个空循环约 6 个时钟周期
// 所以 1us ≈ 12 次循环
uint32_t count = us * 12;
while (count--);
}
// SysTick 中断处理函数
void SysTick_Handler(void)
{
sysTickCounter++;
}
3.4 实战:精准闪烁 LED
理论说完了,咱们来点实际的。用刚才封装的延时函数,让 LED 以精准的 500ms 间隔闪烁。
3.4.1 硬件连接
- LED 正极 → 通过 220Ω 电阻 → PB0
- LED 负极 → GND
3.4.2 主程序代码
#include "systick.h"
#include "stm32f10x_gpio.h"
void GPIO_Init(void)
{
// 使能 GPIOB 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置 PB0 为推挽输出,50MHz
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = GPIO_Pin_0;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio);
}
int main(void)
{
// 初始化 SysTick
SysTick_Init();
// 初始化 GPIO
GPIO_Init();
while (1)
{
GPIO_SetBits(GPIOB, GPIO_Pin_0); // LED 亮
delay_ms(500); // 延时 500ms
GPIO_ResetBits(GPIOB, GPIO_Pin_0); // LED 灭
delay_ms(500); // 延时 500ms
}
}
3.4.3 验证方法
- 用示波器测量 PB0 引脚,应该看到 1Hz 的方波(500ms 高,500ms 低)
- 用逻辑分析仪抓取,观察高低电平时间是否精准
- 肉眼观察 LED,应该以稳定的节奏闪烁
3.5 本章小结
时钟树是 STM32 的基石,SysTick 是裸机开发的瑞士军刀。把这两样东西封装好,后面的 PWM、输入捕获、通信协议都会轻松很多。
嗯,这里再啰嗦一句:不要跳过时钟配置直接写应用代码。我见过太多人拿着开发板,跑例程没问题,自己写就翻车。原因十有八九是时钟没配对。把时钟树吃透,你就算入门了。