4. 定时器与计数器:SysTick定时器、通用定时器原理、PWM生成、输入捕获与输出比较
定时器这东西,说白了就是芯片里的「节拍器」。我刚开始做嵌入式那会儿,总觉得定时器不就是数数嘛,有啥好研究的?后来被坑过几次才明白——定时器玩不转,整个系统的时序就全乱套了。
这一章咱们把定时器掰开揉碎了讲。从最简单的SysTick,到通用定时器的各种模式,再到PWM、输入捕获、输出比较这些实战功能。嗯,内容不少,但都是硬通货。
核心认知:定时器 = 计数器 + 时钟源 + 控制逻辑。给它一个稳定的时钟,它就能精确地数时间。反过来,给它一个外部信号,它就能测量外部事件的频率或脉宽。
4.1 SysTick定时器——操作系统的「心跳」
SysTick是ARM Cortex-M内核自带的24位递减计数器。它不依赖外设总线,内核一上电就能用。我个人习惯在裸机开发中也开着SysTick,用它做精确延时和超时检测。
SysTick的工作原理很简单:
- 从重装载值开始递减计数
- 减到0时触发中断(可选),然后自动重装
- 24位计数器,最大计数值16,777,215
配置SysTick的代码其实就几行:
// SysTick配置示例(STM32)
// 系统时钟72MHz,产生1ms中断
void SysTick_Init(void)
{
// 重装载值 = 72000000 / 1000 = 72000
SysTick->LOAD = 72000 - 1;
SysTick->VAL = 0;
SysTick->CTRL = 0x07; // 使能 + 中断 + 使用系统时钟
}
// 中断服务函数
void SysTick_Handler(void)
{
static uint32_t tick = 0;
tick++;
// 每1ms进入一次
}
我的经验:SysTick的重装载值最好减1再写入。因为计数器从LOAD值减到0需要LOAD+1个时钟周期。我第一次用的时候没注意这个,结果延时总是多了一个周期。虽然一个周期影响不大,但强迫症受不了。
SysTick在RTOS里扮演什么角色?它就是任务调度的「节拍器」。比如FreeRTOS的configTICK_RATE_HZ设为1000,那SysTick就每1ms触发一次,OS在里面做任务切换和延时管理。
4.2 通用定时器——真正的「多面手」
通用定时器比SysTick复杂得多。它通常有16位或32位的计数器,支持向上计数、向下计数、中央对齐计数三种模式。我当年做电机控制项目时,一个通用定时器同时干了三件事:产生PWM、测量编码器脉冲、做定时中断。嗯,一个定时器当三个用。
通用定时器的核心寄存器就这几个:
| 寄存器 | 功能 | 说明 |
|---|---|---|
| CNT | 计数器当前值 | 可读可写,复位后为0 |
| ARR | 自动重装载寄存器 | 决定计数周期 |
| PSC | 预分频器 | 对时钟源分频,16位 |
| CCR | 捕获/比较寄存器 | 每个通道独立,用于PWM或捕获 |
定时时间的计算公式:
定时时间 = (PSC + 1) × (ARR + 1) / 定时器时钟频率
举个例子:时钟72MHz,想要1ms中断一次:
// 预分频 7200-1,重装载 10-1
// 定时器时钟 = 72MHz / 7200 = 10kHz
// 中断周期 = 10 / 10kHz = 1ms
TIM_TimeBaseInitTypeDef tim;
tim.TIM_Prescaler = 7200 - 1;
tim.TIM_Period = 10 - 1;
tim.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &tim);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);
注意:预分频器PSC是16位的,最大值65535。如果时钟频率太高、需要长时间定时,光靠PSC可能不够。这时候要么用32位定时器,要么在中断里做软件计数累加。我曾经在一个项目中需要定时1小时,就是用中断里累加的方式实现的。
4.3 PWM生成——让LED呼吸,让电机转动
PWM(脉冲宽度调制),说白了就是通过调节高低电平的占空比来模拟模拟量。LED调光、电机调速、舵机控制,都离不开它。
通用定时器生成PWM的原理:计数器从0递增到ARR,同时CCR里存一个比较值。当CNT小于CCR时输出高电平,大于等于CCR时输出低电平。改变CCR就能改变占空比。
// PWM配置示例(STM32 TIM2_CH1,PA0输出)
void PWM_Init(void)
{
GPIO_InitTypeDef gpio;
TIM_OCInitTypeDef oc;
TIM_TimeBaseInitTypeDef tim;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置GPIO为复用推挽输出
gpio.GPIO_Pin = GPIO_Pin_0;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio);
// 3. 配置定时器时基
tim.TIM_Prescaler = 72 - 1; // 72MHz/72 = 1MHz
tim.TIM_Period = 1000 - 1; // PWM频率 = 1MHz/1000 = 1kHz
tim.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &tim);
// 4. 配置PWM模式1
oc.TIM_OCMode = TIM_OCMode_PWM1;
oc.TIM_OutputState = TIM_OutputState_Enable;
oc.TIM_Pulse = 500; // 初始占空比50%
oc.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &oc);
TIM_Cmd(TIM2, ENABLE);
}
// 动态调整占空比
void PWM_SetDuty(uint16_t duty)
{
TIM_SetCompare1(TIM2, duty); // duty范围0~999
}
你想想看,改变占空比就能控制LED亮度或电机转速,这背后的原理其实就是定时器在不停地比较CNT和CCR的值。硬件自动完成的,不占CPU。
避坑指南:我曾经在调PWM驱动舵机时,发现舵机抖得厉害。查了半天,原来是PWM频率不对。舵机标准是50Hz(周期20ms),而我设成了1kHz。记住:不同负载对PWM频率要求不同——LED调光要高于人眼闪烁频率(通常100Hz以上),电机驱动要看MOS管开关损耗,舵机必须50Hz。
4.4 输入捕获——测量外部信号的「频率计」
输入捕获用来测量外部信号的频率、周期、脉宽。原理是:当检测到指定边沿(上升沿或下降沿)时,把当前CNT的值锁存到CCR里,同时触发中断。
测量脉宽的典型步骤:
- 配置捕获通道检测上升沿
- 捕获到上升沿时,记录CCR值T1
- 切换为下降沿捕获
- 捕获到下降沿时,记录CCR值T2
- 脉宽 = (T2 - T1) × 定时器时钟周期
// 输入捕获配置(测量高电平脉宽)
void InputCapture_Init(void)
{
TIM_ICInitTypeDef ic;
// 通道1:上升沿捕获
ic.TIM_Channel = TIM_Channel_1;
ic.TIM_ICPolarity = TIM_ICPolarity_Rising;
ic.TIM_ICSelection = TIM_ICSelection_DirectTI;
ic.TIM_ICPrescaler = TIM_ICPSC_DIV1;
ic.TIM_ICFilter = 0;
TIM_ICInit(TIM3, &ic);
// 使能捕获中断
TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);
TIM_Cmd(TIM3, ENABLE);
}
// 中断中切换边沿
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3, TIM_IT_CC1))
{
static uint8_t edge = 0;
uint16_t cap = TIM_GetCapture1(TIM3);
if(edge == 0) // 上升沿
{
// 记录上升沿时间
// 切换为下降沿
TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Falling);
edge = 1;
}
else // 下降沿
{
// 计算脉宽 = cap - last_cap
// 切换回上升沿
TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Rising);
edge = 0;
}
TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
}
}
注意:输入捕获的精度取决于定时器时钟频率。比如定时器时钟72MHz,一个计数周期约13.9ns。如果被测信号频率太高,可能一个周期内计数器就溢出了。这时候需要开启定时器的从模式——用外部信号复位计数器,或者用32位定时器。
4.5 输出比较——精确控制输出时刻
输出比较和PWM有点像,但思路不同。PWM是持续输出可调占空比的方波,而输出比较是在「指定时刻」改变输出电平。你可以把它理解成「定时开关」。
应用场景举例:
- 产生特定宽度的脉冲
- 步进电机的精确脉冲控制
- 红外遥控信号的编码发送
输出比较的工作模式:
| 模式 | 行为 | 典型用途 |
|---|---|---|
| 冻结 | CNT=CCR时无动作 | 仅用于软件查询 |
| 翻转 | CNT=CCR时电平翻转 | 产生对称方波 |
| 强制低/高 | 强制输出固定电平 | 调试用 |
| PWM模式 | CNT<CCR输出有效电平 | PWM生成 |
输出比较和输入捕获其实是同一套硬件——捕获/比较通道。区别在于:捕获是「外部信号来了,我记下时间」;比较是「时间到了,我改变输出」。一个往里收,一个往外发。
总结一下:
- SysTick:内核自带,24位递减,适合做OS心跳和精确延时
- 通用定时器:功能最全,支持多种计数模式,是PWM和捕获的基础
- PWM:通过比较CNT和CCR产生可调占空比方波,驱动LED、电机、舵机
- 输入捕获:锁存边沿时刻,测量频率、周期、脉宽
- 输出比较:在指定时刻改变输出电平,产生精确脉冲
这些功能说到底都是围绕「计数+比较」这两个核心操作展开的。理解了这个本质,不管换什么芯片,定时器部分你都能快速上手。