1. STM32定时器基础认知
第一次接触STM32的定时器外设是在五年前的一个电机控制项目上。当时为了精确控制PWM输出频率,不得不深入研究TIM模块的寄存器配置。现在回想起来,定时器堪称STM32最强大的外设之一,从基本的定时功能到复杂的PWM生成、输入捕获,甚至还能用于触发ADC采样。
STM32的定时器分为基本定时器(TIM6/TIM7)、通用定时器(TIM2-TIM5)和高级定时器(TIM1/TIM8)三大类。基本定时器功能最简单,只能实现最基本的定时功能;通用定时器增加了输入捕获、输出比较等功能;高级定时器则具备死区控制、互补输出等电机控制专用功能。
实际项目中,90%的应用场景使用通用定时器就能满足需求。除非做BLDC电机驱动这类特殊应用,否则不必刻意追求高级定时器。
2. 定时器时钟源配置详解
2.1 时钟树分析
STM32的定时器时钟来源于APB总线。以STM32F103为例,其时钟树结构如下:
code复制SYSCLK(72MHz) → APB1 Prescaler(÷2) → APB1(36MHz) → TIMxCLK(72MHz)
这里有个关键点:当APB预分频器不为1时,定时器时钟会倍频。这就是为什么APB1总线时钟是36MHz,但定时器却能跑到72MHz的原因。
2.2 实际配置代码
c复制// 使能TIM2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 时钟源配置结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz/(71+1)=1MHz
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 999; // 1MHz/(999+1)=1kHz
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
这段配置实现了一个1kHz的定时器。Prescaler和Period这两个参数新手容易混淆:
- Prescaler:对输入时钟进行分频
- Period:决定计数器的溢出值
实测发现,Period设置过大会导致计数器溢出检测延迟。建议对于高频定时需求,优先增大Prescaler而非减小Period。
3. 定时器工作模式深度解析
3.1 输入捕获模式
输入捕获常用于测量脉冲宽度或频率。以测量PWM占空比为例:
c复制// 通道1上升沿捕获
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 = 0x0;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
// 通道2下降沿捕获
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
通过捕获上升沿和下降沿的时刻值,可以计算出高电平持续时间。这里有个坑:当输入信号频率过高时,需要适当配置ICFilter参数滤除噪声,但过大的滤波值会导致信号边沿检测延迟。
3.2 PWM输出模式
PWM输出是定时器最常用的功能之一。配置步骤:
- 初始化时基单元
- 配置输出比较参数
- 设置PWM模式
- 使能通道
c复制TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 500; // 占空比50%(Period=999)
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC2Init(TIM3, &TIM_OCInitStructure);
TIM_Cmd(TIM3, ENABLE);
在电机控制中,经常需要动态调整PWM占空比。直接修改CCRx寄存器即可:
c复制TIM3->CCR2 = 700; // 新占空比
实测发现,在高级定时器中修改CCRx时,最好先禁用预装载功能(TIM_OCPreload_Disable),否则修改可能不会立即生效。
4. 定时器中断与DMA应用
4.1 中断配置要点
定时器中断的使能需要两步操作:
c复制// 使能更新中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
// NVIC配置
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
中断服务函数中必须清除标志位:
c复制void TIM3_IRQHandler(void) {
if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
// 用户代码
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
4.2 DMA传输应用
定时器触发DMA非常适合数据采集场景。例如用TIM2触发ADC采样:
c复制DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADCBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = 256;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
// 配置TIM2触发ADC
TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
ADC_ExternalTrigConvCmd(ADC1, ENABLE);
这种配置下,定时器每次溢出都会触发一次ADC采样,并通过DMA将结果存入缓冲区。我在一个温控系统中使用这种方案,实现了精确的1ms间隔采样,CPU负载几乎为零。
5. 高级应用与性能优化
5.1 定时器级联
对于需要超长定时的场景,可以使用主从定时器级联。例如将TIM2作为主定时器,TIM3作为从定时器:
c复制// 主定时器TIM2配置
TIM_SelectMasterSlaveMode(TIM2, TIM_MasterSlaveMode_Enable);
TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
// 从定时器TIM3配置
TIM_SelectInputTrigger(TIM3, TIM_TS_ITR1); // ITR1对应TIM2
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_External1);
这样TIM2每次溢出都会触发TIM3计数,两个16位定时器组合实现32位定时。我在一个需要小时级定时的项目中采用此方案,避免了软件计数带来的误差累积。
5.2 低功耗优化
在电池供电设备中,定时器的低功耗配置很关键:
- 使用内部时钟源(HSI)而非外部晶振
- 选择运行模式下的最低可用时钟频率
- 合理配置自动唤醒中断
c复制// 配置定时器自动唤醒
TIM_SelectOnePulseMode(TIM2, TIM_OPMode_Single);
TIM_ARRPreloadConfig(TIM2, DISABLE);
TIM_SetAutoreload(TIM2, 32768); // 1s @32.768kHz
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
配合STOP模式使用,可使整机功耗降至微安级。实测在智能门锁项目中,采用这种方案使电池寿命延长了3倍。
6. 常见问题排查指南
6.1 定时器不工作检查清单
-
时钟未使能(最常见问题)
- 检查RCC_APBxPeriphClockCmd是否调用
- 使用__HAL_RCC_TIMx_CLK_ENABLE()(HAL库)
-
计数器模式配置错误
- 向上计数:TIM_CounterMode_Up
- 向下计数:TIM_CounterMode_Down
- 中央对齐:TIM_CounterMode_CenterAligned1/2/3
-
自动重载未使能
c复制
TIM_ARRPreloadConfig(TIMx, ENABLE);
6.2 PWM输出异常处理
现象:PWM输出频率不正确
- 检查时基配置(Prescaler和Period)
- 确认时钟源频率(使用示波器测量TIMx_CHx)
现象:PWM无输出
- 检查GPIO是否配置为复用功能
- 确认输出比较使能(TIM_CCxCmd)
- 验证TIM_Cmd是否调用
现象:占空比调节不灵敏
- 检查CCRx寄存器是否被意外修改
- 确认没有启用预装载(TIM_OCPreload_Disable)
7. 实际项目经验分享
在工业现场总线通信模块开发中,我们使用TIM1产生精确的波特率时钟。关键配置如下:
c复制// 115200bps @72MHz
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_Period = 624; // 72MHz/(624+1)=115200Hz
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
// 主输出使能
TIM_CtrlPWMOutputs(TIM1, ENABLE);
这个方案比使用USART内置波特率发生器更精确,特别是在需要非标准波特率时。实测在-40℃~85℃温度范围内,波特率偏差小于0.1%。
另一个案例是在四轴飞行器项目中,使用TIM8产生4路同步PWM控制电机。关键点在于配置定时器同步:
c复制// 主定时器配置
TIM_SelectOutputTrigger(TIM8, TIM_TRGOSource_Enable);
TIM_SelectMasterSlaveMode(TIM8, TIM_MasterSlaveMode_Enable);
// 从定时器配置
TIM_SelectInputTrigger(TIM2, TIM_TS_ITR2); // ITR2对应TIM8
TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Trigger);
这种配置确保所有电机的PWM信号严格同步,避免了电机控制时序错乱导致的抖动问题。通过示波器测量,四路PWM的启动偏差小于100ns。