4. 定时器:从SysTick到输入捕获
定时器这东西,可以说是嵌入式开发的基石。我做了这么多年BSP,几乎每个项目都离不开它。从简单的延时,到复杂的电机控制,定时器无处不在。今天咱们就把这四种定时器玩法——SysTick、通用定时器、PWM输出、输入捕获——一次性讲透。
核心观点:定时器不只是用来计时的。它是CPU的“硬件协处理器”,能帮你完成精确的时间控制、波形生成和信号测量。用好定时器,你的系统实时性会上一个台阶。
4.1 SysTick定时器:系统的心跳
SysTick是Cortex-M内核自带的24位递减计数器。说白了,它就是给操作系统提供“心跳”的。就算你不用RTOS,它也是个非常精准的延时工具。
我个人习惯,在裸机开发时也把SysTick用起来。为什么?因为它不占用通用定时器资源,而且所有Cortex-M芯片都有,代码移植性极好。
4.1.1 SysTick的工作原理
SysTick从你设定的重装载值开始,每个时钟周期减1。减到0时,会触发一个中断(如果你使能了的话),然后自动重新装载,继续下一轮计数。
// SysTick配置示例:产生1ms中断
void SysTick_Init(uint32_t systemClockHz) {
// 重装载值 = 系统时钟频率 / 1000 - 1
// 假设系统时钟为72MHz
SysTick->LOAD = 72000 - 1; // 1ms中断
SysTick->VAL = 0; // 清空计数器
SysTick->CTRL = 0x07; // 使能、使能中断、使用系统时钟
}
小技巧:SysTick的LOAD寄存器是24位的,最大值是0xFFFFFF。如果你的系统时钟很高,又想产生较长的定时周期,记得先算一下会不会溢出。我曾经在168MHz的芯片上直接写了个1秒的装载值,结果发现计数器根本装不下——嗯,后来改用分频解决了。
4.1.2 基于SysTick的延时实现
有了1ms的中断,我们就可以实现一个毫秒级的延时函数。这里有个关键点:延时函数里要“忙等”,但中断服务函数里只做计数。
volatile uint32_t sysTickUptime = 0;
void SysTick_Handler(void) {
sysTickUptime++; // 每1ms加1
}
void delay_ms(uint32_t ms) {
uint32_t start = sysTickUptime;
while ((sysTickUptime - start) < ms) {
// 等待
}
}
你想想看,这个实现有什么好处?它不会阻塞中断。延时期间,其他中断照常响应。这就是“基于系统滴答的延时”比纯循环延时高明的地方。
4.2 通用定时器:多功能的时间管理
通用定时器比SysTick复杂得多。它通常有多个通道,支持向上计数、向下计数、中央对齐模式,还能产生PWM、捕获输入信号。每个MCU厂商的实现略有差异,但核心逻辑是相通的。
4.2.1 定时器的核心寄存器
| 寄存器 | 作用 | 我常用的配置 |
|---|---|---|
| PSC(预分频器) | 对系统时钟分频,降低计数频率 | 通常设为 71(72MHz→1MHz) |
| ARR(自动重装载) | 计数值达到此值后溢出/更新 | 根据需要的周期计算 |
| CNT(计数器) | 当前计数值,可读可写 | 初始化时清0 |
| SR(状态寄存器) | 标志位,如更新中断标志 | 中断服务函数中清标志 |
举个例子。你想产生一个1ms的定时中断,系统时钟72MHz。先设置PSC=71,这样计数频率变成1MHz(1微秒一个计数)。再设置ARR=999,这样从0数到999正好是1000微秒,也就是1ms。
// 通用定时器初始化(以TIM2为例)
void TIM2_Init(void) {
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // 使能时钟
TIM2->PSC = 71; // 72MHz / (71+1) = 1MHz
TIM2->ARR = 999; // 1MHz / (999+1) = 1kHz = 1ms
TIM2->DIER |= TIM_DIER_UIE; // 使能更新中断
NVIC_EnableIRQ(TIM2_IRQn); // 使能NVIC中断
TIM2->CR1 |= TIM_CR1_CEN; // 启动定时器
}
避坑指南:我曾经在配置定时器时,忘了使能RCC时钟。结果定时器寄存器怎么写都不动,查了整整半天。记住:外设时钟使能是第一步,没有时钟,一切都是空谈。
4.3 PWM输出:让硬件帮你干活
PWM(脉冲宽度调制)说白了就是输出一个频率固定、占空比可调的方波。你可能会问:这玩意儿有什么用?用处大了去了——调LED亮度、控制电机速度、驱动舵机角度,都靠它。
用通用定时器产生PWM,核心思路是:定时器工作在输出比较模式,当CNT的值小于某个阈值时,输出高电平;大于阈值时,输出低电平。这个阈值就是“比较值”,通常存在CCR寄存器里。
4.3.1 PWM配置步骤
- 配置GPIO为复用功能——把引脚交给定时器控制
- 设置定时器时基——PSC和ARR决定PWM频率
- 配置输出比较通道——选择PWM模式1或模式2
- 设置占空比——写CCR寄存器
- 使能输出和定时器——开始输出波形
// PWM输出配置(TIM2通道1,PA0)
void PWM_Init(void) {
// 1. GPIO配置
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER |= GPIO_MODER_MODER0_1; // 复用功能
GPIOA->AFR[0] |= 1 << 0; // AF1 = TIM2_CH1
// 2. 定时器时基
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 71; // 1MHz计数频率
TIM2->ARR = 999; // PWM频率 = 1MHz/1000 = 1kHz
// 3. 输出比较配置
TIM2->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; // PWM模式1
TIM2->CCMR1 |= TIM_CCMR1_OC1PE; // 预装载使能
// 4. 设置占空比(50%)
TIM2->CCR1 = 500;
// 5. 使能
TIM2->CCER |= TIM_CCER_CC1E; // 使能通道输出
TIM2->CR1 |= TIM_CR1_CEN; // 启动定时器
}
// 动态修改占空比
void PWM_SetDuty(uint16_t duty) {
TIM2->CCR1 = duty; // 范围:0~ARR
}
经验之谈:修改占空比时,最好在定时器更新事件之后写CCR,否则可能出现一个完整的错误周期。我习惯在PWM初始化时使能预装载功能(OCxPE位),这样CCR的值会在下一个更新事件才生效,避免波形毛刺。
4.4 输入捕获:测量外部信号
输入捕获和PWM正好相反。PWM是输出波形,输入捕获是测量外部波形的频率和占空比。原理很简单:当检测到指定的边沿(上升沿或下降沿)时,定时器会把当前CNT的值“捕获”到CCR寄存器里,同时触发中断。
通过两次捕获的差值,就能算出信号的周期和频率。如果再捕获一次另一个边沿,还能算出高电平宽度,进而得到占空比。
4.4.1 频率测量实现
volatile uint32_t cap1 = 0, cap2 = 0;
volatile uint8_t capDone = 0;
void TIM3_IRQHandler(void) {
if (TIM3->SR & TIM_SR_CC1IF) {
static uint8_t edgeCount = 0;
if (edgeCount == 0) {
cap1 = TIM3->CCR1; // 第一次上升沿
edgeCount = 1;
} else {
cap2 = TIM3->CCR1; // 第二次上升沿
capDone = 1;
edgeCount = 0;
}
TIM3->SR = ~TIM_SR_CC1IF; // 清标志
}
}
// 在主循环中读取结果
if (capDone) {
uint32_t period;
if (cap2 > cap1) {
period = cap2 - cap1;
} else {
period = (TIM3->ARR + 1) - cap1 + cap2; // 处理溢出
}
// 频率 = 计数频率 / period
capDone = 0;
}
注意:输入捕获一定要处理计数器溢出!如果信号周期很长,CNT可能已经翻转了好几次。我遇到过这种情况:测量一个低频信号,结果算出来的频率完全不对,就是因为没考虑溢出。解决方案是开启定时器的更新中断,在中断里记录溢出的次数。
4.5 本章知识体系
下面这张图总结了四种定时器玩法的关系和应用场景。你可以把它当作一个快速参考。
4.6 避坑总结
做定时器开发这些年,我踩过的坑不少。挑几个典型的分享给你:
- 时钟源搞错:APB1和APB2的定时器时钟可能不同。APB1上的定时器通常是系统时钟的一半(如果APB1分频系数不为1)。我吃过这个亏,算出来的时间常数全不对。
- 中断优先级:定时器中断优先级设置不当,会导致其他中断响应延迟。特别是SysTick,如果优先级设得太低,系统调度会出问题。
- PWM初始状态:刚使能PWM输出时,引脚电平可能不确定。我习惯在初始化时先设置一个安全的占空比(比如0%或50%),再启动定时器。
- 输入捕获的噪声:外部信号可能有毛刺。建议开启定时器的数字滤波器功能,或者加一个简单的RC滤波电路。
定时器这部分内容,说难不难,说简单也不简单。关键是要理解它的本质——一个独立于CPU运行的硬件计数器。你配置好参数,它自己就能干活,CPU可以去做别的事。这就是嵌入式系统里“硬件加速”的思想。
嗯,这一章就到这里。代码示例都是基于STM32的,但思路通用。你换成其他芯片,寄存器名字不一样,逻辑是一样的。