4. 定时器基础与应用:SysTick定时器、通用定时器配置、PWM输出、输入捕获、呼吸灯与频率测量
定时器,说白了就是单片机的“心跳”和“节拍器”。
我刚开始学STM32那会儿,总觉得定时器不就是个计数器嘛,有啥好学的?后来做项目才发现,定时器玩不转,很多功能根本做不出来。PWM调不了光、电机控不了速、频率测不准——全得回来补课。
这一章,咱们就把定时器彻底搞明白。从最基础的SysTick,到通用定时器的配置,再到PWM输出和输入捕获。最后用两个实战案例收尾:呼吸灯和频率测量。
本章核心脉络: 理解定时器本质 → 掌握SysTick作为系统心跳 → 学会通用定时器配置 → 实战PWM与输入捕获
4.1 定时器家族:STM32的“时间管理大师”
STM32的定时器分好几类,我习惯把它们分成三个梯队:
| 类型 | 定时器 | 位数 | 主要用途 |
|---|---|---|---|
| 基本定时器 | TIM6、TIM7 | 16位 | 基础定时、触发DAC |
| 通用定时器 | TIM2~TIM5 | 16/32位 | PWM、输入捕获、输出比较 |
| 高级定时器 | TIM1、TIM8 | 16位 | 电机控制、互补PWM、刹车功能 |
| SysTick | 系统定时器 | 24位 | 操作系统心跳、精确延时 |
你想想看,基本定时器就像个“闹钟”,到点就响。通用定时器就厉害了,它不仅能当闹钟,还能测量外部信号的脉宽、产生可调宽度的方波。高级定时器则是为电机控制量身定做的,带死区插入和刹车功能。
嗯,这里要注意:SysTick是内核自带的,不属于外设定时器。它挂在系统总线上,专门给操作系统提供心跳。咱们做FreeRTOS移植,第一步就是配好SysTick。
4.2 SysTick定时器:FreeRTOS的“心脏起搏器”
SysTick是个24位的递减计数器。它从你设定的重装载值开始,每个系统时钟周期减1,减到0就触发一次中断,然后自动重装。
我个人习惯把SysTick的周期设为1ms。为什么?因为FreeRTOS的时钟节拍默认就是1ms,这样最省事。
配置代码其实很简单:
// SysTick配置函数
// 参数:系统时钟频率(Hz),目标中断频率(Hz)
uint32_t SysTick_Config(uint32_t ticks)
{
// 判断重装载值是否超出24位范围
if (ticks > SysTick_LOAD_RELOAD_Msk)
return 1;
// 设置重装载值
SysTick->LOAD = ticks - 1;
// 设置当前值为0
SysTick->VAL = 0;
// 使能SysTick中断,使能定时器
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
return 0;
}
// 如果系统时钟是72MHz,要产生1ms中断
// ticks = 72000000 / 1000 = 72000
// SysTick_Config(72000);
我的经验: 在FreeRTOS中,不要直接在SysTick中断里做耗时操作。我曾经犯过这个错——在中断里打印日志,结果系统直接卡死。SysTick中断里只做两件事:递增系统时钟计数、通知FreeRTOS调度器。
4.3 通用定时器配置:从“跑起来”到“玩出花”
通用定时器的配置,我总结了一个“三步走”套路:
- 时钟使能——让定时器通电
- 参数配置——设置分频、重装载值、计数模式
- 中断/事件使能——决定定时器怎么通知你
咱们以TIM3为例,配置一个1秒中断一次:
void TIM3_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
// 第一步:使能TIM3时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 第二步:配置定时器参数
// 预分频器:72MHz / 7200 = 10KHz
TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1;
// 自动重装载值:10000 / 10KHz = 1秒
TIM_TimeBaseStructure.TIM_Period = 10000 - 1;
// 向上计数模式
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
// 时钟分频(用于滤波,这里不用)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// 第三步:使能更新中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
// 使能定时器
TIM_Cmd(TIM3, ENABLE);
// 配置NVIC(中断优先级)
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
// 中断服务函数
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
// 1秒到了,做你想做的事
// 注意:中断里别做耗时操作!
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
避坑指南: 我曾经在配置TIM2时,忘了查它挂在哪个总线上。TIM2挂在APB1上,如果APB1的时钟不是系统时钟的一半,实际频率会翻倍。记住:APB1的定时器时钟 = APB1时钟 × 2(如果APB1分频系数≠1)。这个坑我踩过,你别再踩了。
4.4 PWM输出:让LED“呼吸”起来
PWM,说白了就是“快速开关”。通过调节高电平占一个周期的时间比例(占空比),就能控制LED的亮度、电机的转速。
STM32的通用定时器都带PWM模式。配置起来也不复杂:
void TIM2_PWM_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置PA0为复用推挽输出(TIM2_CH1)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 定时器基础配置
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 72MHz/72 = 1MHz
TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 1MHz/1000 = 1KHz
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// PWM模式配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1
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_Cmd(TIM2, ENABLE);
}
// 设置占空比函数
void PWM_SetDuty(uint16_t duty)
{
TIM_SetCompare1(TIM2, duty);
}
你看,核心就是TIM_Pulse这个值。它和TIM_Period的比值就是占空比。比如Period=1000,Pulse=500,占空比就是50%。
4.5 输入捕获:测量外部信号的“脉搏”
输入捕获,就是测量外部信号的频率或脉宽。原理很简单:当检测到信号的上升沿或下降沿时,定时器会把当前的计数值“捕获”到寄存器里。
我举个例子:测量一个方波的频率。
volatile uint32_t IC_Value1 = 0;
volatile uint32_t IC_Value2 = 0;
volatile uint8_t IC_Flag = 0;
void TIM4_InputCapture_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置PB6为浮空输入(TIM4_CH1)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 定时器基础配置
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 1MHz计数
TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 最大计数值
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
// 输入捕获配置:捕获上升沿
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
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 = 0;
TIM_ICInit(TIM4, &TIM_ICInitStructure);
// 使能捕获中断
TIM_ITConfig(TIM4, TIM_IT_CC1, ENABLE);
TIM_Cmd(TIM4, ENABLE);
// NVIC配置
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void TIM4_IRQHandler(void)
{
if (TIM_GetITStatus(TIM4, TIM_IT_CC1) != RESET)
{
if (IC_Flag == 0)
{
// 第一次捕获:记录第一个上升沿
IC_Value1 = TIM_GetCapture1(TIM4);
IC_Flag = 1;
// 改为捕获下降沿
TIM_OC1PolarityConfig(TIM4, TIM_ICPolarity_Falling);
}
else if (IC_Flag == 1)
{
// 第二次捕获:记录下降沿(用于测脉宽)
IC_Value2 = TIM_GetCapture1(TIM4);
IC_Flag = 2;
// 改回捕获上升沿
TIM_OC1PolarityConfig(TIM4, TIM_ICPolarity_Rising);
}
else if (IC_Flag == 2)
{
// 第三次捕获:记录第二个上升沿(用于测周期)
uint32_t temp = TIM_GetCapture1(TIM4);
uint32_t period = temp - IC_Value1;
// 频率 = 1MHz / period
// 这里可以计算频率
IC_Value1 = temp;
IC_Flag = 1;
TIM_OC1PolarityConfig(TIM4, TIM_ICPolarity_Falling);
}
TIM_ClearITPendingBit(TIM4, TIM_IT_CC1);
}
}
我的经验: 输入捕获最怕信号抖动。我建议你开启滤波功能(TIM_ICFilter),设个4-8的滤波值。别设太大,否则会滤掉有效信号。这个值我一般根据信号频率来调,低频信号滤波值设小点,高频信号设大点。
4.6 实战一:呼吸灯——从“亮”到“灭”的优雅渐变
呼吸灯,就是让LED的亮度像呼吸一样缓慢变化。原理很简单:不断改变PWM的占空比,从0%逐渐增加到100%,再从100%逐渐减小到0%。
我习惯用定时器中断来更新占空比:
// 呼吸灯参数
#define BREATHE_PERIOD 20 // 每次更新间隔20ms
#define STEP_VALUE 5 // 每次变化步长
uint16_t breathe_duty = 0;
uint8_t breathe_dir = 1; // 1:增加,0:减小
// 在定时器中断中调用(假设定时器每20ms中断一次)
void Breathe_Update(void)
{
if (breathe_dir)
{
breathe_duty += STEP_VALUE;
if (breathe_duty >= 1000)
{
breathe_duty = 1000;
breathe_dir = 0;
}
}
else
{
if (breathe_duty < STEP_VALUE)
{
breathe_duty = 0;
breathe_dir = 1;
}
else
{
breathe_duty -= STEP_VALUE;
}
}
PWM_SetDuty(breathe_duty);
}
你想想看,这个渐变过程其实很“人性化”。LED不会突然亮起或熄灭,而是缓缓变化,看起来就很舒服。
4.7 实战二:频率测量——给信号“把脉”
频率测量,说白了就是数一数单位时间内信号跳了多少次。有两种方法:
| 方法 | 原理 | 适用场景 | 精度 |
|---|---|---|---|
| 测频法 | 固定闸门时间,计数脉冲个数 | 高频信号(>1KHz) | 闸门时间越长越准 |
| 测周法 | 测量一个周期的时间 | 低频信号(<1KHz) | 周期越长越准 |
我一般这样选:信号频率高就用测频法,频率低就用测周法。如果频率范围很宽,那就两种方法结合着用。
测频法的代码实现:
// 测频法:1秒闸门
volatile uint32_t pulse_count = 0;
volatile uint32_t frequency = 0;
// 外部中断服务函数(检测信号上升沿)
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
pulse_count++;
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
// 1秒定时器中断服务函数
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
frequency = pulse_count; // 读取1秒内的脉冲数
pulse_count = 0; // 清零,开始下一轮计数
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
避坑指南: 我曾经用测频法测一个50Hz的信号,闸门时间设了1秒,结果发现频率一直在49-51之间跳动。后来才意识到,闸门时间和信号周期不是整数倍关系,会有±1的计数误差。解决办法:闸门时间设长一点,比如10秒,或者用测周法。
4.8 本章知识体系
下面这张图,是我画的本章节知识结构,帮你理清思路:
这张图把本章的知识点串起来了。你从中心出发,沿着四个方向走,就能把定时器的全貌看清楚。
好了,定时器这块的内容就到这儿。代码我都跑过,你直接复制到工程里就能用。遇到问题别慌,先检查时钟配置,再看中断优先级——80%的问题都出在这两个地方。