1. 定时器外设基础认知
在嵌入式开发领域,定时器(TIMER)堪称最常用的外设之一。我从业十年来接触过上百个嵌入式项目,几乎没有一个项目能完全避开定时器的使用。基本定时器就像电子系统中的心跳,而通用定时器则是实现复杂时序控制的瑞士军刀。
以STM32系列为例,其定时器分为基本定时器(TIM6/TIM7)和通用定时器(TIMx)两大类。基本定时器结构简单,通常只具备最基本的计数功能,适合做时基生成;而通用定时器则集成了输入捕获、输出比较、PWM生成等丰富功能。在实际项目中,我经常用基本定时器做系统时钟节拍,而用通用定时器实现电机控制、LED调光等需要精确波形输出的场景。
2. 基本定时器工作原理
2.1 时钟树与预分频机制
所有定时器的核心都是计数器,而计数器的动力来源于时钟源。以STM32F4为例,其定时器时钟通常来自APB总线。这里有个关键细节:当APB预分频系数不为1时,定时器时钟会倍频。例如APB1时钟配置为42MHz且分频系数为2时,定时器实际时钟是84MHz。这个特性我在早期项目中曾忽略过,导致定时计算出现2倍误差。
预分频器(PSC)将时钟源进一步分频供给计数器。计算公式为:
code复制定时器时钟 = 输入时钟 / (PSC + 1)
例如输入时钟84MHz,PSC设为83,则计数器每1us加1。这里+1的操作容易被新手忽略,我见过不少工程师直接填分频值导致计时错误。
2.2 自动重装载与更新事件
基本定时器的核心工作流程:
- 计数器(CNT)从0开始递增
- 当CNT值等于自动重装载值(ARR)时产生更新事件
- CNT复位到0重新计数
这个简单的循环构成了所有定时功能的基础。ARR的值决定了定时周期:
code复制定时周期 = (ARR + 1) * (PSC + 1) / 输入时钟频率
这里两个+1经常成为计算错误的根源。我的经验是先用示波器测量实际波形,反推出有效参数。
3. 通用定时器的PWM模式
3.1 PWM生成原理
通用定时器通过比较寄存器(CCRx)和输出比较单元实现PWM。当CNT < CCRx时输出有效电平,反之输出无效电平。通过调整CCRx的值即可改变占空比。
STM32的通用定时器支持多种PWM模式:
- PWM模式1:CNT < CCRx时有效
- PWM模式2:CNT ≥ CCRx时有效
- 边沿对齐模式(常用)
- 中心对齐模式(适合电机控制)
在电机控制项目中,我通常使用中心对齐模式,因为它能产生对称的PWM波形,减少电机谐波损耗。
3.2 关键寄存器配置
以STM32F407的TIM3_CH1为例,配置步骤:
- 使能TIM3时钟:
c复制RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
- 配置时基单元:
c复制TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 999; // ARR值
TIM_TimeBaseStructure.TIM_Prescaler = 83; // PSC值
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
- 配置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; // CCR初始值
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
- 使能预装载和启动定时器:
c复制TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_Cmd(TIM3, ENABLE);
注意:一定要配置GPIO为复用功能并映射到对应定时器通道,这是最容易遗漏的步骤。
4. 高级PWM技巧与问题排查
4.1 动态调整占空比
在实际应用中经常需要实时调整PWM占空比。正确做法是:
c复制TIM_SetCompare1(TIM3, newCCRValue); // 修改CCR值
错误做法是直接操作寄存器,这可能导致毛刺。我在早期项目中曾直接写TIM3->CCR1导致PWM输出异常。
4.2 死区时间配置
驱动H桥电路时必须配置死区时间,防止上下管直通。STM32的高级定时器(TIM1/TIM8)内置死区发生器:
c复制TIM_BDTRInitTypeDef TIM_BDTRInitStructure;
TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable;
TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable;
TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1;
TIM_BDTRInitStructure.TIM_DeadTime = 0x54; // 具体值根据需求计算
TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable;
TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_Low;
TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure);
死区时间计算公式:
code复制DeadTime = (DTG[7:0] + 1) * Tdtg
其中Tdtg取决于时钟分频
4.3 常见问题排查
-
无PWM输出:
- 检查GPIO是否配置为复用功能
- 验证定时器时钟是否使能
- 确认TIM_Cmd()已调用
-
PWM频率不对:
- 重新计算PSC和ARR值
- 检查APB总线时钟配置
- 确认没有其他代码修改定时器配置
-
PWM抖动严重:
- 降低系统中断频率
- 检查电源稳定性
- 尝试增加滤波电容
我在调试无刷电机控制器时,曾遇到PWM输出偶尔丢失的问题。最终发现是中断服务程序中操作了定时器寄存器,导致时序冲突。解决方案是将所有定时器操作放在主循环中。
5. 工程实践建议
5.1 定时器资源规划
在复杂系统中,合理分配定时器资源至关重要。我的常用策略:
- TIM6/TIM7:系统时基、任务调度
- TIM2/TIM5:高精度测量(32位计数器)
- TIM1/TIM8:电机控制(带死区)
- TIM3/TIM4:通用PWM生成
- TIM9-TIM14:简单定时任务
5.2 使用HAL库的注意事项
虽然HAL库简化了开发,但需要注意:
- 调用HAL_TIM_PWM_Start()前必须先初始化GPIO
- 修改ARR值时要调用__HAL_TIM_SET_AUTORELOAD()
- 使用HAL_TIM_GenerateEvent()可强制产生更新事件
5.3 低功耗设计
在电池供电设备中:
- 选择适合的低功耗定时器(如LPTIM)
- 在PWM不使用时关闭定时器时钟
- 考虑使用DMA自动更新PWM参数,减少CPU唤醒
在智能家居项目中,我通过合理配置TIM2的触发输出模式,实现了在CPU睡眠时仍能维持PWM输出,使整体功耗降低60%。
6. 测量与调试技巧
6.1 使用输入捕获测量PWM
通用定时器的输入捕获功能可以测量外部PWM参数:
c复制// 配置为上升沿和下降沿都触发
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_BothEdge;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
通过捕获两次边沿的时间差即可计算周期和占空比。
6.2 使用示波器调试
推荐使用示波器的这些高级功能:
- 频率统计功能验证PWM稳定性
- 占空比测量功能快速验证参数
- 触发模式捕捉异常波形
- XY模式观察电机驱动时序
我在调试伺服系统时,发现用普通边沿触发难以捕捉偶发的PWM异常。改用示波器的序列触发功能后,成功捕获到由于中断延迟导致的脉冲宽度异常。