1. STM32通用定时器基础概念回顾
在嵌入式开发领域,定时器是微控制器最核心的外设之一。STM32系列MCU的通用定时器(GP Timer)以其灵活性和强大功能著称,能够实现从简单延时到复杂PWM输出的各种功能。我们常用的TIM2、TIM3、TIM4等都属于通用定时器范畴。
通用定时器本质上是一个16位或32位的向上/向下计数器,其核心工作原理是通过时钟源驱动计数器累加,当计数值达到预设值时触发中断或事件。与基本定时器相比,通用定时器增加了输入捕获、输出比较、PWM生成等高级功能,使其在电机控制、信号测量等场景中表现突出。
注意:不同STM32系列的定时器资源分配存在差异,例如F1系列TIM2-TIM5是通用定时器,而F4系列可能包含更多定时器资源。开发前务必查阅对应型号的参考手册。
2. HAL库定时器开发环境搭建
2.1 STM32CubeMX工程配置
使用STM32CubeMX配置定时器可以大幅减少底层寄存器操作的工作量。以下是典型配置流程:
-
时钟树配置:确保定时器时钟源已使能。对于APB1总线上的定时器(如TIM2-TIM5),时钟频率通常为系统时钟的一半。
-
定时器模式选择:
- 基本定时模式:用于简单计时和中断
- PWM生成模式:用于驱动电机或LED调光
- 输入捕获模式:用于测量脉冲宽度
- 输出比较模式:用于精确时间控制
-
参数设置界面关键字段解析:
- Prescaler (PSC):分频系数,决定计数时钟频率
- Counter Mode:计数方向(向上/向下/中央对齐)
- Period (ARR):自动重装载值,决定定时周期
- Auto-reload preload:是否启用ARR缓冲
c复制// CubeMX生成的定时器初始化代码示例(HAL库)
htim2.Instance = TIM2;
htim2.Init.Prescaler = 8399; // 84MHz/(8399+1) = 10kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 9999; // 10kHz下计满10000次为1秒
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
2.2 HAL库关键函数解析
HAL库为定时器操作提供了完善的API接口,主要包含以下几类函数:
-
初始化与反初始化:
- HAL_TIM_Base_Init()
- HAL_TIM_Base_DeInit()
-
启停控制:
- HAL_TIM_Base_Start()
- HAL_TIM_Base_Stop()
- HAL_TIM_Base_Start_IT() // 带中断启动
- HAL_TIM_Base_Start_DMA() // DMA模式启动
-
中断回调函数:
- HAL_TIM_PeriodElapsedCallback() // 周期中断回调
- HAL_TIM_OC_DelayElapsedCallback() // 输出比较回调
实操技巧:使用CubeMX生成代码后,用户代码应写在/* USER CODE BEGIN /和/ USER CODE END */注释对之间,避免重新生成代码时被覆盖。
3. 通用定时器四种工作模式详解
3.1 定时器基本模式
基本定时模式是最简单的使用方式,常用于产生周期性中断。配置要点:
- 时钟源选择:通常使用内部时钟(CK_INT)
- 时基计算:
- 定时周期 = (PSC+1)*(ARR+1)/TIMx_CLK
- 例如:84MHz时钟,PSC=8399,ARR=9999 → 1秒周期
- 中断配置:
- 在CubeMX中使能定时器中断
- 实现HAL_TIM_PeriodElapsedCallback()回调函数
c复制void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2) {
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
3.2 PWM输出模式
PWM模式是通用定时器最常用的功能之一,配置步骤:
-
通道配置:
- 选择PWM Generation CHx模式
- 设置Pulse值(初始占空比)
- 极性选择(高电平有效/低电平有效)
-
关键参数关系:
- PWM频率 = TIMx_CLK / [(PSC+1)*(ARR+1)]
- 占空比 = (Pulse+1)/(ARR+1)
-
动态调整占空比:
c复制__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, newPulseValue); // 或使用HAL库函数 HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
3.3 输入捕获模式
输入捕获用于测量脉冲宽度或频率,典型应用包括:
- 旋转编码器信号测量
- 超声波测距回波检测
- 红外遥控信号解码
配置要点:
- 选择输入捕获通道和触发边沿(上升沿/下降沿/双边沿)
- 配置输入滤波器减少噪声干扰
- 实现捕获中断回调函数处理测量数据
c复制// 输入捕获中断处理示例
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
static uint32_t firstValue = 0;
if(captureState == 0) {
firstValue = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
captureState = 1;
// 切换为下降沿捕获
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
} else {
uint32_t pulseWidth = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1) - firstValue;
// 处理测量到的脉冲宽度
captureState = 0;
// 恢复上升沿捕获
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
}
}
}
3.4 输出比较模式
输出比较模式可用于:
- 精确的单脉冲输出
- 可变频率方波生成
- 延迟触发外部事件
关键配置参数:
- 比较模式:冻结/激活/翻转/强制等
- 输出极性
- 比较值(CCR)
4. 高级应用与性能优化
4.1 定时器级联技术
对于需要超长定时周期的应用,可以通过主从定时器级联实现:
- 配置主定时器为定时模式,启用更新事件输出
- 配置从定时器为外部时钟模式,时钟源选择ITRx
- 主定时器的更新事件作为从定时器的时钟
c复制// 主定时器TIM2配置
htim2.Init.Period = 1000 - 1; // 主定时器周期
htim2.Instance->CR2 |= TIM_CR2_MMS_1; // 主模式选择更新事件输出
// 从定时器TIM3配置
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 0xFFFF; // 最大计数周期
htim3.Instance->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1 | TIM_SMCR_SMS_2; // 外部时钟模式
htim3.Instance->SMCR |= TIM_SMCR_TS_0; // 触发选择ITR1(TIM2)
4.2 DMA与定时器配合
定时器触发DMA可以高效处理数据搬运,常见应用场景:
- PWM波形数据流输出
- ADC采样定时触发
- 定时数据批量传输
配置示例(使用TIM2触发DMA传输):
c复制// CubeMX中配置:
// 1. 启用TIM2的更新事件DMA请求
// 2. 配置DMA通道,模式为循环模式
// 3. 设置外设到存储器传输
// 启动DMA传输
HAL_TIM_Base_Start_DMA(&htim2, (uint32_t*)pwmData, BUFFER_SIZE);
4.3 低功耗定时器应用
在低功耗设计中,定时器可用于唤醒MCU:
- 配置RTC或低功耗定时器(LPTIM)
- 进入STOP模式前启动定时器
- 定时器中断唤醒MCU
c复制// 配置LPTIM1在停止模式下唤醒
hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_APBCLOCK_LPOSC;
hlptim1.Init.Trigger.Source = LPTIM_TRIGSOURCE_SOFTWARE;
hlptim1.Init.Period = 0xFFFF;
HAL_LPTIM_Init(&hlptim1);
// 启动定时器
HAL_LPTIM_Counter_Start_IT(&hlptim1, 0x1000);
// 进入停止模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
5. 常见问题与调试技巧
5.1 定时器不工作的排查步骤
-
检查时钟树配置:
- 确认定时器所在总线时钟已使能
- 使用__HAL_RCC_TIMx_CLK_ENABLE()手动使能时钟
-
验证GPIO配置:
- PWM/捕获功能需要正确配置GPIO复用功能
- 使用CubeMX的Pinout视图检查冲突
-
中断优先级配置:
- 确保NVIC中已使能定时器中断
- 合理设置抢占优先级和子优先级
-
调试技巧:
- 在定时器初始化后添加断点,检查寄存器值
- 使用逻辑分析仪观察PWM输出波形
5.2 精度优化技巧
-
时钟源选择:
- 对于高精度应用,使用外部晶振而非内部RC振荡器
- 考虑使用TIMx_ETR外部时钟输入
-
分频系数优化:
- 尽量使ARR值接近最大值以获得更高分辨率
- 计算公式:PSC = (TIMx_CLK / (ARR * 目标频率)) - 1
-
中断延迟补偿:
- 测量实际中断响应时间
- 在ARR中补偿延迟周期数
5.3 多定时器协同工作
当系统需要多个定时器协同工作时,需注意:
-
优先级管理:
- 高精度定时任务设为更高优先级
- 使用TIMx_CR1中的URS位选择仅溢出事件触发中断
-
同步触发:
- 使用主从模式同步多个定时器
- 通过TIMx_SMCR配置触发同步
-
资源冲突避免:
- 检查不同定时器是否共享DMA通道
- 避免中断服务程序执行时间过长
c复制// 定时器同步配置示例(TIM2主,TIM3从)
// TIM2配置
TIM2->CR2 |= TIM_CR2_MMS_1; // 主模式选择更新事件输出
// TIM3配置
TIM3->SMCR |= TIM_SMCR_SMS_2; // 从模式选择外部时钟模式1
TIM3->SMCR |= TIM_SMCR_TS_2 | TIM_SMCR_TS_0; // 触发选择ITR1(TIM2)
6. 实际项目案例分享
6.1 步进电机控制实现
使用TIM1和TIM8的高级控制定时器实现步进电机细分驱动:
-
PWM通道配置:
- 两路互补PWM输出(带死区控制)
- 动态调整Pulse值实现微步控制
-
速度曲线生成:
- 使用TIM2定时器中断计算速度曲线
- 通过DMA更新TIM1的CCR寄存器
-
关键代码片段:
c复制// 电机速度曲线更新
void updateMotorProfile(void)
{
static uint32_t step = 0;
if(step < PROFILE_STEPS) {
uint32_t newCCR = speedProfile[step++];
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, newCCR);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, newCCR);
}
}
// TIM2中断回调中调用
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2) {
updateMotorProfile();
}
}
6.2 多通道ADC定时采样
利用TIM4触发ADC多通道扫描采样:
- 配置TIM4为特定采样频率
- 配置ADC为外部触发模式,触发源选择TIM4_TRGO
- 使用DMA传输采样数据
c复制// CubeMX配置要点:
// 1. ADC1设置:
// - 外部触发源:Timer 4 Trigger Out event
// - 启用扫描模式和多通道DMA
// 2. TIM4设置:
// - 触发输出选择:更新事件
// - 配置合适的采样频率
// 启动顺序
HAL_TIM_Base_Start(&htim4);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer, ADC_CHANNELS);
6.3 红外遥控解码器
使用TIM5输入捕获实现NEC协议解码:
- 配置TIM5通道1为输入捕获模式
- 设置合适的分辨率(通常1-2μs)
- 实现状态机解析脉冲序列
c复制// NEC协议解码状态机
typedef enum {
NEC_IDLE,
NEC_LEADER_PULSE,
NEC_LEADER_SPACE,
NEC_DATA_PULSE,
NEC_DATA_SPACE
} NEC_State;
void processNEC(uint32_t pulseWidth)
{
static NEC_State state = NEC_IDLE;
static uint8_t bitCount = 0;
static uint32_t necCode = 0;
switch(state) {
case NEC_IDLE:
if(pulseWidth > NEC_LEADER_PULSE_MIN) {
state = NEC_LEADER_PULSE;
}
break;
// 其他状态处理...
}
}