STM32定时器实战:从基础配置到高级应用

张开发
2026/4/17 18:39:18 15 分钟阅读

分享文章

STM32定时器实战:从基础配置到高级应用
1. STM32定时器基础配置从零开始搭建计时系统第一次接触STM32定时器时我完全被那些专业术语搞懵了。什么预分频、自动重装载、计数模式听起来就像天书。但后来我发现定时器其实就是个电子秒表理解了这一点一切都变得简单了。最基本的定时器配置需要关注四个核心参数时钟源就像给秒表供电的电池预分频系数决定秒表走多快计数模式秒表是往上数还是往下数自动重装载值秒表数到多少就重新开始这里有个实际项目中的配置示例我们配置TIM2定时器每1ms产生一次中断// 时钟配置 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 定时器基础参数设置 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 1000 - 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); NVIC_EnableIRQ(TIM2_IRQn); // 启动定时器 TIM_Cmd(TIM2, ENABLE);新手最容易踩的坑就是忘记计算时钟树。STM32的时钟系统像个复杂的管道网络定时器的时钟可能来自APB1或APB2总线还要考虑是否经过倍频。我建议在CubeMX里先确认时钟配置或者用这个公式计算定时器时钟 APB总线时钟 × (APB预分频器为1 ? 1 : 2)比如APB1时钟是72MHz预分频器是2那么定时器实际时钟就是72×2144MHz。这个细节坑了我整整两天时间调试。2. PWM输出实战从呼吸灯到电机控制PWM脉冲宽度调制可能是定时器最酷的功能之一。它就像快速开关的电灯通过改变开和关的时间比例可以控制LED亮度、电机转速甚至产生音频信号。配置PWM输出需要关注三个关键参数周期一个完整PWM波形的时间占空比高电平持续时间占周期的百分比极性起始电平是高还是低这里有个配置TIM3通道1输出PWM的完整示例// GPIO配置 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate GPIO_AF2_TIM3; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PWM配置 TIM_HandleTypeDef htim3; htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 1000 - 1; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim3); TIM_OC_InitTypeDef sConfigOC; sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 500; // 初始占空比50% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); // 启动PWM HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);在实际项目中我常用PWM控制直流电机。有个重要技巧当需要动态调整PWM频率时不要直接修改Period值而是应该先停止定时器修改配置后再重新启动否则可能导致输出异常。3. 输入捕获精准测量脉冲宽度输入捕获功能就像个高精度的秒表可以测量外部信号的脉冲宽度。我在做红外遥控解码时这个功能帮了大忙。配置输入捕获需要注意触发边沿上升沿、下降沿或双边沿滤波设置防止噪声误触发分频系数对输入信号进行预分频下面是个测量高电平持续时间的典型配置// GPIO配置 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate GPIO_AF2_TIM5; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 输入捕获配置 TIM_HandleTypeDef htim5; htim5.Instance TIM5; htim5.Init.Prescaler 72 - 1; htim5.Init.CounterMode TIM_COUNTERMODE_UP; htim5.Init.Period 0xFFFFFFFF; // 32位定时器用最大周期 htim5.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(htim5); TIM_IC_InitTypeDef sConfigIC; sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0; HAL_TIM_IC_ConfigChannel(htim5, sConfigIC, TIM_CHANNEL_1); // 启动输入捕获 HAL_TIM_IC_Start_IT(htim5, TIM_CHANNEL_1);在中断处理函数中我们可以获取捕获值并计算脉冲宽度void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { static uint32_t firstCapture 0; if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { if (firstCapture 0) { firstCapture HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 切换为下降沿捕获 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); } else { uint32_t pulseWidth HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1) - firstCapture; firstCapture 0; // 切换回上升沿捕获 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); } } }4. 编码器模式精准读取旋转位置编码器模式让定时器变身成为专业的位置传感器读取器。我在做机器人项目时就是用这个功能读取电机编码器信号。编码器模式有几个关键点支持正交编码器A/B相和单相编码器可以自动识别旋转方向计数器在溢出时会自动处理配置编码器模式的示例代码// GPIO配置 - 以TIM4为例 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin GPIO_PIN_12 | GPIO_PIN_13; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate GPIO_AF2_TIM4; HAL_GPIO_Init(GPIOD, GPIO_InitStruct); // 编码器模式配置 TIM_HandleTypeDef htim4; htim4.Instance TIM4; htim4.Init.Prescaler 0; htim4.Init.CounterMode TIM_COUNTERMODE_UP; htim4.Init.Period 65535; // 16位定时器最大值 htim4.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim4.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; HAL_TIM_Encoder_Init(htim4, TIM_ENCODERMODE_TI12); // 启动编码器接口 HAL_TIM_Encoder_Start(htim4, TIM_CHANNEL_ALL);读取编码器位置和方向的技巧int16_t getEncoderPosition() { // 注意使用int16_t处理计数器溢出 return (int16_t)TIM4-CNT; } bool isEncoderCountingDown() { return __HAL_TIM_IS_TIM_COUNTING_DOWN(htim4); }有个常见问题为什么编码器读数会跳变这通常是因为信号抖动。解决方法是在GPIO配置中启用上拉电阻并适当设置输入滤波GPIO_InitStruct.Pull GPIO_PULLUP; // 在TIM_IC_InitTypeDef中设置 sConfigIC.ICFilter 6; // 适当增加滤波5. 高级技巧与疑难解答在实际项目中我积累了一些宝贵的经验教训。比如有一次定时器中断怎么都不触发最后发现是NVIC优先级配置冲突。多定时器协同工作 当需要多个定时器配合时可以使用主从模式。比如让TIM1作为主定时器触发TIM2// 配置TIM1为主模式 TIM_MasterConfigTypeDef sMasterConfig; sMasterConfig.MasterOutputTrigger TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim1, sMasterConfig); // 配置TIM2为从模式 TIM_SlaveConfigTypeDef sSlaveConfig; sSlaveConfig.SlaveMode TIM_SLAVEMODE_TRIGGER; sSlaveConfig.InputTrigger TIM_TS_ITR0; // TIM1作为触发源 HAL_TIM_SlaveConfigSynchronization(htim2, sSlaveConfig);低功耗定时器使用 在电池供电设备中可以使用LPTIM低功耗定时器。它的配置略有不同// LPTIM配置 LPTIM_HandleTypeDef hlptim1; hlptim1.Instance LPTIM1; hlptim1.Init.Clock.Source LPTIM_CLOCKSOURCE_APBCLOCK_LPOSC; hlptim1.Init.Clock.Prescaler LPTIM_PRESCALER_DIV128; hlptim1.Init.Trigger.Source LPTIM_TRIGSOURCE_SOFTWARE; hlptim1.Init.OutputPolarity LPTIM_OUTPUTPOLARITY_HIGH; hlptim1.Init.UpdateMode LPTIM_UPDATE_IMMEDIATE; hlptim1.Init.CounterSource LPTIM_COUNTERSOURCE_INTERNAL; HAL_LPTIM_Init(hlptim1);常见问题排查清单定时器不工作检查时钟是否使能GPIO是否配置正确PWM无输出确认通道配置正确GPIO复用功能选择正确输入捕获不触发检查边沿极性设置信号是否确实到达引脚编码器读数异常检查A/B相接线适当增加滤波中断不触发检查NVIC配置中断优先级设置记得在调试时充分利用STM32的调试寄存器。比如查看TIMx_CR1寄存器可以确认定时器是否真的启动了TIMx_SR寄存器可以查看中断标志状态。

更多文章