第一次接触STM32定时器时,我被那些密密麻麻的寄存器搞得头晕眼花。直到后来做智能小车项目才发现,TIMx定时器简直就是嵌入式系统的"心脏起搏器"。想象一下:你的小车需要每100ms采集一次超声波测距数据(更新中断),在特定距离触发舵机转向(比较中断),同时还要实时调整电机转速(PWM输出)——这些全靠TIMx定时器在幕后精准协调。
STM32的定时器家族确实有点复杂,光F1系列就有8个16位定时器。我习惯把它们分成三类:
在智能小车项目中,我推荐这样分配资源:
c复制TIM2:更新中断(传感器采样时钟)
TIM3:比较中断(舵机控制时序)
TIM4:PWM输出(电机驱动)
提示:APB1总线上的定时器时钟需要特别注意,默认情况下系统时钟72MHz会先经过2分频再给定时器,所以实际时钟是36MHz。记得在RCC配置中开启定时器时钟倍频!
让定时器像闹钟一样准时叫醒MCU干活,这是更新中断最典型的用法。去年我给实验室的智能小车升级时,就用TIM2实现了这样的多任务调度:
c复制// 定时器2初始化示例(100ms周期)
void TIM2_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseStructure.TIM_Period = 10000 - 1; // 自动重装载值
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 预分频器
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);
}
这里有个坑我踩过:定时周期计算公式是(ARR+1)*(PSC+1)/时钟频率。第一次配置时忘了"+1",结果定时差了整整一倍时间。现在我的工程里永远留着这个注释:
c复制/* 定时周期T = (ARR+1)*(PSC+1)/72MHz
例如:ARR=9999, PSC=71 → T=10ms */
中断服务函数里我通常会做三件事:
c复制void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update))
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
ADC_StartConversion(); // 触发ADC采样
task_flag |= 0x01; // 设置任务标志
}
}
去年调试小车转向舵机时,发现普通延时函数根本满足不了精度要求。后来改用比较中断,终于把转向控制精度控制在±0.5ms以内——这对舵机控制至关重要。
比较中断的配置比更新中断稍复杂些,需要设置捕获/比较寄存器(CCR)。以TIM3通道1为例:
c复制// 比较中断初始化(500ms触发)
void TIM3_Compare_Init(void)
{
TIM_OCInitTypeDef TIM_OCInitStructure;
// 先配置时基(略)...
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Timing;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Disable;
TIM_OCInitStructure.TIM_Pulse = 36000; // 比较值CCR
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);
}
比较中断最妙的地方是可以多个通道独立工作。我在小车项目里这样分配TIM3:
c复制TIM3_CH1:左舵机控制(CCR1=2000 → 1ms脉冲)
TIM3_CH2:右舵机控制(CCR2=3000 → 1.5ms脉冲)
TIM3_CH3:超声波触发(CCR3=40000 → 200ms间隔)
中断服务函数要特别注意判断是哪个通道触发的中断:
c复制void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3, TIM_IT_CC1))
{
TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
GPIO_SetBits(GPIOA, GPIO_Pin_6); // 舵机信号置高
}
else if(TIM_GetITStatus(TIM3, TIM_IT_CC2))
{
// 其他通道处理...
}
}
说到PWM电机控制,最让我头疼的就是计算频率和占空比。后来总结出万能公式:
code复制PWM频率 = 定时器时钟 / [(ARR+1)*(PSC+1)]
占空比 = (CCR+1)/(ARR+1)
配置TIM4输出10kHz PWM的代码长这样:
c复制void TIM4_PWM_Init(void)
{
TIM_OCInitTypeDef TIM_OCInitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
// 使能时钟和GPIO(略)...
// 时基配置
TIM_TimeBaseStructure.TIM_Period = 720 - 1; // ARR
TIM_TimeBaseStructure.TIM_Prescaler = 0; // PSC
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
// PWM模式配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 360; // 初始占空比50%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC3Init(TIM4, &TIM_OCInitStructure);
TIM_Cmd(TIM4, ENABLE);
}
实际项目中我还会加上按键调节占空比的功能:
c复制// 按键处理函数片段
if(KEY1_Pressed()) // 加速
{
if(CCR_Value < 720)
CCR_Value += 72; // 步进10%
TIM_SetCompare3(TIM4, CCR_Value);
}
电机控制有个小技巧:在启动时要缓慢增加占空比,避免电流冲击。我通常会在初始化后这样启动电机:
c复制for(int i=0; i<360; i+=10) {
TIM_SetCompare3(TIM4, i);
Delay_ms(10);
}
当三个功能同时工作时,最怕遇到定时器冲突。去年调车时就遇到过PWM输出影响更新中断的问题,后来发现是NVIC优先级没配置好。现在我的优先级设置原则是:
NVIC配置示例:
c复制NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// TIM2更新中断(传感器)
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStructure);
// TIM3比较中断(舵机)
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_Init(&NVIC_InitStructure);
调试时我必用的几个工具:
c复制GPIO_SetBits(DEBUG_PORT, DEBUG_PIN);
// 要测试的代码段
GPIO_ResetBits(DEBUG_PORT, DEBUG_PIN);
最后分享一个血泪教训:定时器初始化后别忘了启动计数器!有次调试两小时才发现漏写了TIM_Cmd(TIMx, ENABLE),这个坑我现在每次新建工程都会特别注意。