STM32 HAL库编码器测速全解析:从硬件连接到速度计算与调试

发布时间:2026/7/29 4:33:05

STM32 HAL库编码器测速全解析:从硬件连接到速度计算与调试 1. 项目缘起为什么编码器测速是嵌入式开发的必修课在电机控制、机器人底盘、智能小车这些嵌入式项目里实时、准确地获取电机转速是决定系统性能的基石。你可能会说用霍尔传感器或者红外对管不也能测速吗确实可以但那通常只适用于低速、精度要求不高的场合。一旦涉及到需要精确位置控制、快速响应或者需要知道旋转方向的场景比如平衡车的轮子、机械臂的关节光电或磁电编码器就成了不二之选。编码器输出的是一系列正交脉冲信号A相和B相。电机转得越快脉冲频率就越高电机的转向则通过A、B两相的相位关系来判定。STM32的定时器其编码器接口模式就是为高效处理这类信号而生的硬件外设。它能自动根据A、B相的边沿和电平关系在后台更新计数器的值方向正就加方向反就减完全不需要CPU频繁中断去数脉冲。这个“后台自动处理”的能力是软件模拟解码无法比拟的它解放了CPU让系统有更多资源去处理控制算法等核心任务。然而很多初学者甚至一些有经验的开发者在从标准库转向HAL库时会在这里卡壳。HAL库的封装层次更高API更抽象如果只照着CubeMX生成的代码跑很可能遇到计数器值不对、速度计算为0或者出现巨大跳变的问题。网上很多教程只给步骤却不讲清楚HAL库底层配置寄存器时到底做了什么以及速度计算中那几个关键参数定时器周期、采样频率、编码器线数到底怎么影响最终结果。这篇文章我就结合自己多次在平衡车、云台等项目上的踩坑经验把STM32 HAL库下定时器编码器模式测速的里里外外讲透让你不仅能配置出来更能理解每一个参数背后的意义遇到问题能自己排查。2. 硬件连接与定时器选型你的编码器接对了吗在写代码之前硬件连接是第一步也是最容易出错的一步。编码器通常有A、B、Z三相输出我们测速主要用A和B。Z相是零位信号一圈一个脉冲用于绝对位置校准在简单测速中可以不用。2.1 信号类型与接口匹配常见的编码器输出信号有集电极开路OC和推挽输出两种。对于OC输出的编码器很多是NPN型STM32的IO口需要配置为上拉输入模式或者外部接上拉电阻到VCC否则无法读到高电平。推挽输出的编码器则可以直接连接。我强烈建议无论编码器本身是什么输出都在STM32的输入引脚外部加上一个1k-10k的上拉电阻这是一个很好的防干扰习惯。编码器信号线最好使用双绞线并远离电机电源线以减少电磁干扰。2.2 STM32定时器的编码器模式能力不是所有STM32定时器都支持编码器模式。通常通用定时器TIM2, TIM3, TIM4, TIM5和高级定时器TIM1, TIM8都支持。以最常用的STM32F103C8T6为例它的TIM1、TIM2、TIM3、TIM4都支持编码器接口。在CubeMX中当你为一个定时器选择“Encoder Mode”时其对应的两个通道通常是CH1和CH2会自动被配置为输入模式。这里有一个关键点编码器模式会占用定时器的全部功能。一旦启用编码器模式这个定时器就不能再用于PWM输出、输入捕获等其他功能了。所以规划定时器资源时要提前考虑好。2.3 接线与CubeMX配置实例假设我们使用TIM3的通道1PA6和通道2PA7来接编码器的A、B相。打开CubeMX选择你的STM32型号。找到TIM3在“Combined Channels”中选择“Encoder Mode”。此时PA6和PA7的引脚模式会自动变为“Input Pullup”如果你之前没配置的话。我建议这里保持默认的上拉同时在硬件上也加上拉电阻双重保险。关键参数配置在下面的“Parameter Settings”里Counter Mode: 选择“Up”或“Down”其实在编码器模式下意义不大因为计数方向由硬件自动控制。通常选“Up”即可。Counter Period (ARR): 这是定时器的自动重装载值也是计数器的最大值。这个值决定了计数器的“量程”。设置太小高速时容易溢出设置太大影响速度计算的分辨率。对于16位定时器最大值是65535。一个常见的初始值是65535先保证不溢出后续再优化。Encoder Mode: 这是核心它有四个选项Encoder Mode TI1: 仅在TI1CH1的边沿计数。Encoder Mode TI2: 仅在TI2CH2的边沿计数。Encoder Mode TI1 and TI2(上升沿): 在TI1和TI2的上升沿都计数。这是最常用的模式分辨率最高4倍频。电机转一圈编码器输出N个脉冲在此模式下计数器会变化4*N次。Encoder Mode TI1 and TI2(下降沿): 在TI1和TI2的下降沿都计数。同样也是4倍频。 绝大多数情况下我们选择“Encoder Mode TI1 and TI2”以获得最高的精度。预分频器PSC在编码器模式下这个参数通常设置为0即不分频。因为编码器模式下的时钟源是外部引脚信号PSC是对内部时钟的分频不作用于外部信号。保持为0即可。最后别忘了在NVIC Settings中开启TIM3的全局中断如果需要在计数器溢出时处理的话。对于测速我们通常用另一个定时器定时比如1ms去读取计数器的值所以TIM3本身的中断不一定需要。配置完成后生成代码。HAL库会帮我们初始化好定时器和GPIO。3. HAL库编码器模式驱动原理解析生成了代码我们得知道HAL库在背后做了什么。打开stm32f1xx_hal_tim.c找到HAL_TIM_Encoder_Init函数。它主要做了以下几件事配置定时器的基础时基单元TIMx-PSC, TIMx-ARR。根据我们选择的EncoderMode配置输入捕获通道的极性、滤波器和预分频。最关键的一步设置从模式控制器SMCR的SMS位。编码器模式对应SMS3。同时它会根据EncoderMode设置TI1S位以及通道的输入选择映射CCMR1寄存器。使能定时器的计数器TIMx-CR1 | TIM_CR1_CEN。简单来说硬件层面的逻辑是编码器信号直接驱动了定时器的计数逻辑。当A、B相有边沿变化时硬件会自动判断方向并递增或递减计数器CNT。整个过程完全由硬件完成不消耗CPU时间。3.1 如何读取速度和方向测速的本质是“单位时间内的脉冲数”。方向直接读取定时器状态寄存器中的方向位。对于STM32F1可以检查TIMx-CR1寄存器的DIR位0向上计数1向下计数。HAL库提供了__HAL_TIM_GET_DIRECTION(htimx)宏来获取。更简单直接的方法是读取当前的计数器值CNT与上一次的值比较。如果CNT变大说明正向转动变小则反向转动。速度脉冲数我们需要在固定的时间间隔T比如10ms内读取两次计数器的值。速度Speed (CNT_current-CNT_last) /T。这里的Speed单位是“脉冲数/秒”。注意这里的脉冲数已经是经过编码器模式倍频后的计数值。3.2 计数器溢出处理这是第一个大坑。我们的计数器CNT是16位的范围是0~ARR通常设为65535。当电机正转CNT加到65535后再来一个脉冲它会变成0溢出。如果我们直接用CNT_current - CNT_last计算当发生溢出时比如CNT_last65000CNT_current100计算结果会是100-65000 -64900一个巨大的负数导致速度计算错误。处理溢出有两种方法使用定时器溢出中断开启定时器的更新中断溢出中断。在中断服务函数中维护一个软件计数器如tim_overflow_cnt。当CNT从ARR溢出到0时tim_overflow_cnt当CNT从0下溢到ARR时tim_overflow_cnt--。那么总的计数值 tim_overflow_cnt * (ARR1) CNT_current。这种方法精度高但增加了中断负担。限制采样时间内的最大脉冲数更常用的方法是合理选择采样周期T和编码器分辨率确保在T时间内最大的脉冲变化量不会超过ARR的一半即32767。这样即使发生了一次溢出我们也能通过判断差值的符号来修正。计算公式是delta CNT_current - CNT_last; if(delta 32767) delta - 65536; else if(delta -32767) delta 65536;。这种方法在软件上处理简单对大多数中低速应用足够可靠。我个人的项目里只要规划得当基本都用这种方法。4. 从脉冲到实际转速参数计算与代码实现得到了单位时间内的脉冲数delta_pulse这还不是我们最终想要的转速。我们需要将其转换为更有物理意义的单位例如转/分钟RPM。4.1 转换公式推导假设delta_pulse: 采样周期T内读取到的脉冲变化量已处理溢出。T: 采样周期单位是秒s。例如10ms就是0.01s。PPR: 编码器本身的线数即电机旋转一圈编码器输出的物理脉冲数。注意不是计数器变化的次数。倍频数: 我们在TIM配置中选择的编码器模式对应的倍频数。如果选择“TI1 and TI2”模式倍频数为4。那么采样周期内的转速转/秒 (delta_pulse / 倍频数) / PPR / T转速转/分钟RPM (delta_pulse / 倍频数) / PPR / T * 60简化一下RPM (delta_pulse * 60) / (PPR * 倍频数 * T)4.2 一个完整的测速模块代码示例我们使用TIM3作为编码器接口TIM2产生一个10ms的定时中断来定期计算速度。首先在main.c或你的驱动文件里定义一些全局变量// 编码器相关 extern TIM_HandleTypeDef htim3; // CubeMX生成的TIM3句柄 int32_t encoder_total_pulse 0; // 用于累积计数值处理溢出后 int16_t encoder_last_cnt 0; // 上一次的CNT值 float motor_rpm 0.0; // 计算得到的转速 // 编码器参数 #define ENCODER_PPR 13 // 我的电机编码器是13线 #define ENCODER_MULTIPLY 4 // 4倍频 #define SPEED_SAMPLE_TIME 0.01f // 采样周期10ms然后在TIM2的10ms中断服务函数中HAL_TIM_PeriodElapsedCallback进行速度计算void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { // 10ms定时器 int16_t encoder_cur_cnt __HAL_TIM_GET_COUNTER(htim3); // 读取当前CNT值 int16_t encoder_diff encoder_cur_cnt - encoder_last_cnt; // 处理计数器溢出假设ARR65535 if (encoder_diff 32767) { encoder_diff - 65536; } else if (encoder_diff -32767) { encoder_diff 65536; } encoder_total_pulse encoder_diff; // 累积总脉冲可用于计算位置 encoder_last_cnt encoder_cur_cnt; // 更新上一次值 // 计算转速 RPM (delta_pulse * 60) / (PPR * 倍频数 * T) motor_rpm (encoder_diff * 60.0f) / (ENCODER_PPR * ENCODER_MULTIPLY * SPEED_SAMPLE_TIME); // 可选低通滤波让速度值更平滑 // motor_rpm_filtered 0.8 * motor_rpm_filtered 0.2 * motor_rpm; } }4.3 启动与停止在main函数的初始化部分需要启动编码器定时器和采样定时器// 启动编码器定时器 HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL); // 启动10ms定时器中断 HAL_TIM_Base_Start_IT(htim2);5. 调试技巧与常见问题排查即使代码写好了你可能还是会遇到速度值为0、跳动大或者方向反的问题。别急按照以下步骤排查。5.1 基础信号检查首先确保硬件信号是好的。用示波器或者逻辑分析仪查看连接到PA6和PA7的编码器A、B相信号。电机缓慢转动时你应该能看到两路相位差90度的方波。如果没有信号检查编码器供电、接地以及上拉电阻。5.2 计数器值检查在调试器中或者通过串口打印实时查看__HAL_TIM_GET_COUNTER(htim3)的值。手动缓慢转动电机观察这个值是否随着转动均匀地增加或减少。如果值不变可能的原因有GPIO配置错误虽然CubeMX自动配置了但检查一下生成的代码确认PA6和PA7的模式确实是GPIO_MODE_INPUT并且上拉。定时器未启动确认调用了HAL_TIM_Encoder_Start。编码器模式选择错误确认是“TI1 and TI2”模式。如果选择成只在TI1边沿计数那么只有A相能触发计数在某些接线方式下可能导致计数不灵敏。如果计数器值变化但方向与预期相反最简单的解决办法是调换A、B相的接线。或者你也可以通过修改CubeMX中两个通道的“Polarity”来反转计数方向但调换线更直观。5.3 速度计算异常排查如果计数器值变化正常但计算出的motor_rpm不对检查参数反复核对ENCODER_PPR电机铭牌或手册上找、ENCODER_MULTIPLY一定是4、SPEED_SAMPLE_TIME单位是秒这三个参数。我见过太多人在这里栽跟头把PPR写成倍频后的值或者采样时间单位弄错。检查数据类型encoder_diff是int16_t但在计算公式(encoder_diff * 60.0f) / (ENCODER_PPR * ENCODER_MULTIPLY * SPEED_SAMPLE_TIME)中60.0f和SPEED_SAMPLE_TIME使得整个表达式是浮点数运算。如果写成整数除法结果永远是0。确保计算过程或接收结果的变量是float或double类型。采样时间过短如果采样时间T太短比如1ms而电机转速又很低那么encoder_diff可能经常为0导致速度计算为0或跳变很大。适当加长采样时间如10ms, 20ms或者采用M法测速固定时间测脉冲数结合T法测速测量固定脉冲数的时间的混合策略来提高低速下的精度。5.4 抗干扰与软件滤波实际项目中电机启停、PWM噪声都可能导致编码器信号有毛刺引起计数器偶尔跳变。除了硬件上做好滤波在CubeMX中可以配置输入滤波器的参数ICFilter软件上的滤波也必不可少。一阶低通滤波如上文代码注释所示对计算出的motor_rpm进行滤波可以平滑曲线。motor_rpm_filtered a * motor_rpm_filtered (1-a) * motor_rpm其中a是滤波系数0~1越接近1滤波效果越强但延迟也越大。通常取0.7~0.9。滑动平均滤波维护一个速度值的数组每次计算的速度存入数组输出时取平均值。这种方法也能有效平滑噪声。6. 进阶话题高精度与多电机协同当你的项目要求更高比如需要更高精度的位置信息或者要同时控制多个带编码器的电机时需要考虑更多。6.1 使用32位计数器或软件扩展对于高速电机或高分辨率编码器16位计数器65535可能很快溢出。一些高性能的STM32型号如F4、H7系列的定时器支持32位计数器。如果只能用16位定时器就必须使用前面提到的“溢出中断软件扩展”法将计数器扩展到32位甚至64位确保长时间运行不会丢失脉冲。6.2 利用DMA自动搬运计数器值为了进一步提高实时性和减少CPU干预可以配置DMA来定期由另一个定时器触发将编码器定时器的CNT寄存器值搬运到内存数组中。CPU只需要在需要时去处理这个数组中的数据即可。这在多电机控制或需要极高同步性的场景下非常有用。6.3 多电机编码器资源规划一个定时器只能接一个编码器。如果需要控制多个电机就需要多个支持编码器模式的定时器。在芯片选型和原理图设计时就要算好。例如控制一个双轮差速小车至少需要两个通用定时器如TIM3和TIM4。如果还有云台电机可能需要TIM1或TIM8。6.4 位置闭环控制的初步思路得到了准确的速度和方向结合总脉冲数encoder_total_pulse它代表了累积的位置信息你已经具备了实现位置闭环和速度闭环控制的基础。你可以设定一个目标速度RPM然后用PID控制器根据当前速度motor_rpm与目标速度的差值来计算并调整电机的PWM占空比这就是速度环。更进一步你可以设定一个目标位置总脉冲数用PID控制电机使encoder_total_pulse趋向目标值这就是位置环。编码器提供的反馈是这一切闭环控制的前提。从我个人的经验来看把编码器测速调稳定是整个运动控制项目成功的一半。它提供的反馈是否准确、及时直接决定了你的PID参数能不能调好系统是否稳定。开始时多在硬件连接、参数核对和基础调试上下功夫后面做高级控制时会事半功倍。最后一个小建议把所有关键的参数如PPR、倍频数、采样时间、滤波系数都定义为宏或变量放在头文件里并加上清晰的注释这样调试时修改起来非常方便也不容易遗忘。

相关新闻