4、定时器基础与应用:SysTick定时器、通用定时器配置、PWM输出、输入捕获
定时器这东西,说白了就是单片机的「心跳」。没有它,很多功能都转不起来。我刚开始学嵌入式那会儿,总觉得定时器不就是个计数器嘛,有啥好学的?后来做项目才发现,这里面的门道可真不少。今天咱们就把定时器这块彻底捋清楚。
4.1 SysTick定时器:系统的心跳
SysTick,全称叫系统滴答定时器。它是 Cortex-M 内核自带的,不需要额外配置外设时钟。我个人习惯,拿到一个新板子,第一件事就是把 SysTick 配好。
为什么?因为操作系统需要它来调度任务,裸机程序也需要它来做精准延时。你想想看,如果没有一个稳定的时间基准,你的程序就像没戴手表的上班族,全凭感觉走。
核心要点:SysTick 是一个 24 位的递减计数器,计数到 0 时自动重装载。它的时钟源可以是系统时钟,也可以是系统时钟的 1/8。
配置 SysTick 其实很简单,就几个寄存器的事。我给大家看一段我常用的初始化代码:
// SysTick 配置函数
// 参数:ticks - 重装载值
void SysTick_Init(uint32_t ticks)
{
// 先关掉定时器,防止配置过程中产生中断
SysTick->CTRL = 0;
// 设置重装载值
SysTick->LOAD = ticks - 1;
// 清空当前计数值
SysTick->VAL = 0;
// 配置时钟源为系统时钟,使能中断,启动定时器
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
// 1ms 延时函数
void delay_ms(uint32_t ms)
{
for(uint32_t i = 0; i < ms; i++)
{
// 等待 SysTick 计数到 0
while(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk));
}
}
嗯,这里要注意:SysTick 的中断优先级默认是最低的。如果你在中断里做延时,记得先把优先级调高一点。我曾经在一个项目中,因为没注意这个细节,导致中断嵌套出了问题,排查了整整一个下午。
个人经验:SysTick 的重装载值最好用 (系统时钟频率 / 1000) - 1,这样刚好 1ms 中断一次。比如 72MHz 的 STM32,重装载值就是 71999。这样算出来的延时精度最高。
4.2 通用定时器配置:不只是计数
通用定时器比 SysTick 复杂多了。它有自己的时钟源,可以向上计数、向下计数,还能中心对齐计数。我刚开始学的时候,被 TIMx 的寄存器搞得头大。后来发现,其实只要抓住几个关键点就行。
通用定时器的核心配置包括:
- 时钟源选择:内部时钟、外部触发、编码器模式等
- 预分频器:决定计数频率,PSC 寄存器
- 自动重装载:决定计数周期,ARR 寄存器
- 计数模式:向上、向下、中心对齐
为什么需要预分频器?说白了,就是降频。系统时钟太快了,直接计数的话,1秒钟能计几千万次,根本没法用。预分频器就像减速齿轮,把时钟频率降到你需要的范围。
举个例子,假设系统时钟是 72MHz,你想让定时器 1 秒中断一次:
// 定时器配置示例
void TIM_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
// 使能定时器时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 预分频器:7200 - 1,得到 10KHz 的计数频率
TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1;
// 自动重装载值:10000 - 1,得到 1Hz 的中断频率
TIM_TimeBaseStructure.TIM_Period = 10000 - 1;
// 向上计数模式
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
// 时钟分频:不分频
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 使能中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
你看,72MHz / 7200 = 10KHz,也就是每 0.1ms 计一次数。计 10000 次就是 1 秒。这个逻辑其实很简单,就是分频再分频。
避坑指南:我曾经在配置定时器时,忘记使能 APB 总线时钟,结果定时器死活不工作。查了半天才发现是 RCC 时钟没开。记住:任何外设使用前,第一件事就是开时钟!
4.3 PWM输出:让硬件自己干活
PWM,脉冲宽度调制。说白了就是输出一串方波,通过调节高电平的宽度来控制功率。比如调灯光亮度、电机转速、舵机角度,都是靠 PWM。
为什么不用软件延时来做 PWM?你想想看,如果软件里用 delay 来产生 PWM,那 CPU 啥也别干了,全程在那数数。用定时器的硬件 PWM,CPU 只需要设置好参数,剩下的活硬件自己干,多省心。
配置 PWM 的关键是捕获/比较寄存器(CCR)。它决定了占空比:
// PWM 输出配置
void PWM_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 配置 GPIO 为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // TIM2_CH1
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置 PWM 模式
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 500; // 初始占空比 50%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
// 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
// 设置占空比
void PWM_SetDuty(uint16_t duty)
{
TIM_SetCompare1(TIM2, duty);
}
这里有个细节:PWM 模式 1 和模式 2 的区别。模式 1 是计数值小于 CCR 时输出有效电平,模式 2 则相反。我个人习惯用模式 1,因为更直观——CCR 越大,占空比越大。
实用技巧:如果你需要控制多个通道的 PWM,可以用同一个定时器的不同通道。比如 TIM2 有 4 个通道,可以同时控制 4 个 LED 或 4 个电机。但要注意,它们的频率必须相同,只有占空比可以独立调节。
4.4 输入捕获:测量外部信号
输入捕获,和 PWM 正好相反。PWM 是输出,输入捕获是输入。它用来测量外部信号的频率、脉宽、占空比等参数。
工作原理很简单:当检测到信号的上升沿或下降沿时,定时器会把当前的计数值「捕获」到 CCR 寄存器里。通过两次捕获的差值,就能算出信号的周期或脉宽。
我给大家看一个测量信号频率的例子:
// 输入捕获配置
void InputCapture_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 配置 GPIO 为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; // TIM2_CH2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置输入捕获
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿捕获
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 不分频
TIM_ICInitStructure.TIM_ICFilter = 0x0; // 不滤波
TIM_ICInit(TIM2, &TIM_ICInitStructure);
// 使能捕获中断
TIM_ITConfig(TIM2, TIM_IT_CC2, ENABLE);
// 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
// 中断服务函数中计算频率
uint32_t capture_value = 0;
uint32_t frequency = 0;
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET)
{
uint32_t current = TIM_GetCapture2(TIM2);
uint32_t diff = current - capture_value;
// 计算频率:计数频率 / 差值
frequency = 10000 / diff; // 假设计数频率是 10KHz
capture_value = current;
TIM_ClearITPendingBit(TIM2, TIM_IT_CC2);
}
}
嗯,这里有个坑:如果信号频率很低,差值可能会超过 16 位定时器的最大值。这时候要么用 32 位定时器,要么在中断里做溢出处理。我曾经在测量 1Hz 信号时,就遇到了这个问题,后来改用 TIM2 的 32 位模式才解决。
核心总结:定时器的四个核心功能——SysTick 做系统心跳、通用定时器做精准延时、PWM 输出控制功率、输入捕获测量信号。掌握了这些,大部分嵌入式项目的时间控制问题都能搞定。
最后说一句:定时器的学习,光看理论是不够的。我建议你拿一块开发板,把今天讲的每个功能都亲手实现一遍。遇到问题不要怕,调试的过程才是真正学到东西的时候。