
做电机控制这些年测速一直是绕不开的基础活。不管你是做小车、云台还是工业伺服速度反馈不准后面的PID调得再好也是白搭。我刚接触STM32的时候也试过用霍尔传感器、红外对管去测速后来换了正交编码器才发现这东西在低速下的表现和抗干扰能力确实靠谱。这篇就把我用编码器配合STM32做电机测速的完整过程写出来从原理到代码直接给到能用的程度尽量少讲废话。1. 项目概述与整体设计思路1.1 为什么选择编码器测速测速方案其实有很多常见的就那么几种直流电机尾部带霍尔码盘、光电对管加光栅码盘还有增量式正交编码器。前两种成本便宜但有个通病——低速时脉冲间隔拉得太长单片机的定时器捕捉容易丢信号测出来的转速一跳一跳的根本没法用来做闭环控制。编码器测速就不太一样。正交编码器输出的是两路相位相差90度的方波信号STM32的定时器外设直接支持编码器接口模式硬件上就能完成鉴相和倍频。也就是说你不用自己在中断里判断方向也基本不用担心漏脉冲定时器计数器的值就代表当前的角度位移速度计算只是简单做个差分。这个方案的低速性能、实时性和代码复杂度对中小型电机项目来说几乎是首选。顺手提一个很多人踩过的坑EC11旋钮编码器也能用来测速但拆开看就知道它内部是机械触点一顿一顿的适合做人机交互的旋钮输入不适合电机测速。电机测速要的是光电或磁编码器比如常见的欧姆龙E6A2、国产的ABZ增量编码器、以及各种带编码器的小型直流减速电机脉冲数从13ppr到500ppr不等。1.2 项目技术选型与预期目标这次我的硬件环境如下部件型号/参数说明主控STM32F103RCT672MHz主频板载定时器资源丰富电机25GA370直流减速电机带13线霍尔码盘减速比30:1驱动L298N模块经典H桥驱动够用编码器接口定时器TIM4的CH1/CH2引脚复用编码器模式电源12V适配器5V稳压模块电机和逻辑分压供电选择STM32F103RCT6的原因很简单芯片便宜、资料多、CubeMX一键生成代码定时器数量也够。定时器这块TIM1和TIM8是高级定时器可以输出互补PWM适合做电机驱动TIM2、TIM3、TIM4是通用定时器编码器模式基本都能用。这里我把TIM4留给编码器TIM1用CH1输出PWM控制电机转速。目标很明确编码器数据实时读取电机的实际转速误差控制在正负2%以内整个测速逻辑不额外占用CPU中断资源留着给后面的PID控制算力。2. 编码器测速原理拆解2.1 正交编码器与AB相信号从最底层的东西说起。增量式编码器内部码盘转动时会输出A、B两路方波信号。码盘上有一圈均匀的刻线每过一个刻线就产生一个脉冲所以脉冲个数和转角成正比。A和B的差别在于它们错开了90度相位这个相位差就是判断电机转向的关键。当电机正转时A相上升沿对应的B相是高电平反转时同样的A相上升沿对应B相是低电平。就靠这么一前一后的时序关系单片机就能知道转轴在往哪边跑。有人问那为什么不用单路脉冲测速因为单路只能测速度测不了方向而且抗干扰能力差一旦有个毛刺脉冲计数就全乱了。AB两路交叉校验错误的逻辑组合很容易被识别出来系统的鲁棒性就上来了。还有一个隐含的知识点叫倍频。一个完整的A、B周期内A和B一共会产生4个有效边沿上升沿和下降沿各两个如果定时器把这4个边沿全部计数分辨率就等于原码盘线数的4倍。这就是常说的“4倍频”或者“4X模式”。用软件模拟这个功能虽然也行但让单片机用定时器硬件去干这件事不占CPU也不用开中断几乎零成本拿到4倍分辨率这才是STM32编码器接口模式最大的价值。2.2 STM32定时器编码器模式的工作机制STM32的通用定时器内置一个编码器接口本质上它把定时器的外部时钟源切换成了CH1和CH2引脚上的输入信号然后根据两个信号的边沿组合来判断计数方向。向上计数电机正转计数器值CNT朝上累加。向下计数电机反转计数器值CNT向下递减。计数范围通常是0到ARRARR就是自动重装载值32位定时器的话可以配到65535或者更大。和普通外部计数不一样的是编码器模式下计数器的方向是硬件自动判断的你不需要在代码里读引脚电平再去判断正反转。软件端只要周期性地把CNT读出来和前一次读数做个差就能得到“单位时间内的脉冲增量”正负号天然带有方向信息。再深挖一点编码器模式内部有三种计数方式只在TI1边沿计数只在TI2边沿计数在TI1和TI2的每个边沿都计数。第三种就是前面说的4倍频模式。因为每次计数都对应一个确定的微小角度变化所以4倍频模式下分辨率最高、速度测量量化误差最小。实际项目我建议直接用4X模式反正硬件免费送不要白不要。2.3 测速算法的选型逻辑M法、T法、M/T法硬件拿到脉冲数之后怎么换算成转速这是决定测速精度的关键一步。常用的算法有三类方法原理优点缺点适用场景M法测频法固定时间内统计脉冲个数乘以系数算转速高速精度高代码简单低速时脉冲少量化误差大中高速电机T法测周期法测量单个脉冲的时间间隔通过定时器捕捉换算低速精度好高速时时间间隔短误差变大低速电机M/T法综合统计一段时间内脉冲数和标准时钟脉冲数宽范围内精度都高逻辑复杂实时性略差高精度伺服我这次使用的直流减速电机带减速箱输出轴转速最高也就几百转每分加上13线的码盘4倍频后每圈53个脉冲高速时可能到每秒几千个脉冲低速时每秒可能只有几个脉冲。在这种跨度下想兼顾高低速如果你用的是裸机简单逻辑最稳妥的是M法加一个定时器中断的固定采样周期比如10ms采一次。M法公式转速 (脉冲增量 x 60) / (编码器线数 x 倍频系数 x 采样周期秒数) 单位转/分举个例子编码器线数134倍频后每圈53个脉冲采样周期10ms0.01秒一次采样脉冲增量是53个那转速 53 × 60 / (13 × 4 × 0.01) 3180 / 0.52 ≈ 6115这个单位不对我重新理一下。每圈脉冲数即13×452转速 (53 / 52) / 0.01 × 60 约61.15转/分。简单说采样到的脉冲数除以每圈脉冲总数乘以60秒再除以采样秒数就得到了每分钟的转数。低速场景如果脉冲增量只有1到2个M法的波动会非常明显。这时候可以考虑在低转速范围自动切换成T法测周期或者干脆用STM32的输入捕获中断去测单个脉冲的脉宽。不过实测下来普通项目里只要采样周期拉长一点、做一下滑动平均M法的抖动大部分能被过滤掉不是非要上M/T法。3. 硬件连接与CubeMX配置3.1 接线图与注意事项先看一下最基础的接线。STM32F103RCT6的TIM4引脚映射是PB6对应CH1PB7对应CH2。编码器的A相接PB6B相接PB7。如果编码器是集电极开路输出记得接上拉电阻一般直接用单片机内部上拉就行。VCC和GND按编码器要求接5V供电的编码器注意别直接怼到3.3V引脚上。电机驱动部分L298N的IN1、IN2接STM32的两个普通GPIO我用的PB0、PB1ENA接PWM输出。PWM从TIM1的CH1输出引脚是PA8。电源方面L298N的逻辑电源和电机电源可以分开如果只有一路12V就把它接到L298N的电机电源端再从它的5V输出口取5V给单片机供电。但注意L298N的5V是线性稳压出来的电流不大带编码器加单片机还行带舵机或者多个传感器就得加独立的稳压模块。注意编码器的信号线和电机电源线不要捆在一起走电机启动瞬间的电流扰动会在信号线上感应出毛刺导致计数器多计或漏计。信号线尽量短必要时用屏蔽线或者双绞线。3.2 STM32CubeMX关键配置项这一步直接上CubeMX的操作路径照着点就行。打开STM32CubeMX选好芯片型号后按下面流程配置RCCHSE选择Crystal/Ceramic Resonator使用外部晶振。时钟树把SYSCLK拉到72MHzAPB1定时器时钟改成72MHz。这个很关键定时器的编码器模式计数频率直接跟这个挂钩。TIM4在左侧列表里选中TIM4MODE选择Encoder Mode也就是Encoder Mode下的TI1 and TI2对应4倍频。Configuration里的Parameter SettingsCounter Period填65535也就是自动重装载值编码器模式下CNT会在这个范围内来回数。Encoder ModeTI1 and TI2Input Filter看情况配电机干扰大就填高一点比如0x0F。GPIOPB6、PB7自动变成输入模式不用额外改。TIM1CH1选PWM Generation CH1Prescaler填71Counter Period填999这样PWM频率就是72MHz / (711) / (9991) 1kHz占空比调节范围0到999。NVIC使能TIM4全局中断吗这个看你代码怎么写。如果用DMA或裸读CNT其实不需要中断。为了省CPU我这次用DMA定时读取但更简单的入门写法是定时器中断里读CNT两者都行代码部分我会给出中断版本。项目生成设置里Toolchain选MDK-ARM然后点GENERATE CODE生成就行。3.3 编码器模式参数背后的原理官方手册里关于Encoder Mode的解释比较多这里挑重点讲清楚。Counter PeriodARR为什么设置65535而不是别的值因为在4倍频模式下如果电机连续同向旋转CNT会在0到ARR之间循环计数超过ARR就归零重来。ARR设置越大溢出频率越低但缺点是当你要算增量时要处理“跨过零点的回绕”问题。如果ARR设置成65535正转的时候从65000涨到1000这是正常的回绕这时候你必须用int16_t类型来读取寄存器无符号数这么一转换负增量自动变成负数加加减减刚好正确这是很多人的盲区。Input Filter这个参数是用来对输入信号做数字滤波的。简单说信号边沿如果存在抖动定时器会要求信号保持稳定N个时钟周期后才认定这是有效边沿N越大抗抖能力越强但信号延迟也越明显。对于电机电刷带来的干扰滤波值配到0x0F基本能挡掉常见毛刺。编码器线数高、转得快的场合滤波值太大会丢脉冲这需要根据实际波形来调整。1kHz的PWM频率对普通直流电机控制来说是够的。有人问为什么不用20kHz避开音频噪声那是因为L298N这种模块的开关损耗在20kHz下比较大而且1kHz控制直流电机的响应带宽完全够实时性没有问题。4. 完整代码实现与关键讲解4.1 测速主逻辑代码框架代码风格我尽量用标准库CubeMX生成的HAL库混搭因为HAL库读CNT太方便了一行搞定。整个代码分三块初始化、周期采样、脉冲增量换算。// main.c 关键片段 __IO int16_t g_motor_pulse 0; // 10ms内的脉冲增量带方向 __IO uint16_t g_pwm_duty 0; // PWM占空比 // TIM1输出PWM初始化代码由CubeMX生成这里略 // 定时器中断回调TIM3每10ms触发一次读取编码器CNT void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static int16_t last_cnt 0; int16_t cur_cnt; if (htim-Instance TIM3) { cur_cnt (int16_t)__HAL_TIM_GET_COUNTER(htim4); g_motor_pulse cur_cnt - last_cnt; last_cnt cur_cnt; // 清零TIM4计数器方便下一周期直接读 __HAL_TIM_SET_COUNTER(htim4, 0); } }这里有个细节CNT被我用(int16_t)强制转换就是为了自动处理回绕问题。如果电机正转导致CNT从65000变成200转换后是负数用int16_t算差值结果是正数500多正确。如果编码器反向CNT从200变成65000差值显示负的500多也对。这是定时器测速里最微妙的地方搞懂之后就能举一反三。另外注意我把读取和清零放在同一个临界区内但因为没有进中断函数所以不存在竞争问题。如果开了TIM4中断两个中断之间就要做保护这里先按下不表。4.2 转速换算与滤波处理上面拿到了10ms的脉冲增量接下来就是把它换算成直观的转速值。#define PULSE_PER_REV (13 * 4) // 13线码盘4倍频后每圈52个脉冲 #define SAMPLE_PERIOD_S 0.01f // 采样周期10ms float calc_speed_rpm(int16_t pulse_delta) { float rpm; rpm (float)pulse_delta * 60.0f / PULSE_PER_REV / SAMPLE_PERIOD_S; return rpm; }这个公式其实不需要死记你只要理解一个事情脉冲增量除以每圈脉冲数得到的是“多少圈”再除以采样时间得到“每秒多少圈”乘以60得到“每分钟多少圈”。展开就是上面代码的计算过程。但裸算出来的rpm抖动较大尤其是低转速下步进感强。所以我一般在显示或反馈之前加一个一阶低通滤波float g_speed_filtered 0.0f; void update_speed_filtered(float rpm_raw) { float alpha 0.3f; g_speed_filtered alpha * rpm_raw (1.0f - alpha) * g_speed_filtered; }alpha取值0到1越大对新值越敏感越小越平滑。我用0.3滤波后波形稳定又不至于滞后太多够用。实操心得滤波系数不是一个固定死的东西它取决于你的采样周期和应用场景。10ms采样周期配0.3的alpha实际效果滞后大约30ms左右用在电流环就能感觉到拖沓但用在速度环甚至位置环这个滞后完全不影响。4.3 基于DMA的测速版本进阶如果不想在中断里频繁读取编码器还有个更省资源的方式用定时器DMA把CNT值周期性地搬运到内存数组里。这样CPU基本零开销适合后续要做FOC或者多路电机控制的场景。// 伪代码框架 #define DMA_BUF_SIZE 64 uint16_t cnt_buf[DMA_BUF_SIZE]; HAL_TIM_Base_Start_DMA(htim3, (uint32_t *)cnt_buf, DMA_BUF_SIZE); // DMA传输完成回调 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { // cnt_buf数组里就有过去64个周期的CNT快照 // 对相邻元素做差即可推算速度曲线 } }DMA方案的思路是TIM3触发更新事件时DMA自动把CNT搬到内存等搬满64个值再通知CPU一次性处理。这样能拿到一段历史速度轨迹做加速度估算或者异常检测非常有用。不过这个方案的缺点是数据是异步的调试时不太直观新手建议先把中断版本跑通了再玩这个。5. 实测数据与调试经验5.1 逻辑分析仪验证编码器信号接线无误后先用逻辑分析仪看看A、B两相波形这一步能省掉后面无数排查的力气。正常波形应该是两路同频率的方波彼此错开90度。如果发现A、B完全同步或者无相位差要么编码器坏了要么接线错位。如果波形有大量毛刺优先怀疑电源和地线质量问题。这时候调节TIM4的Input Filter参数观察毛刺是否被消除。实际调试时我还遇到过一种情况电机转动时波形正常但一停下来波形噪声变大。这是电刷火花干扰解决方法是给电机两端并一个0.1uF瓷片电容和10uF电解电容信号线再串个1k电阻基本能压住。5.2 测速数据对照与精度分析手动给电机不同占空比记录下测量速度和用光电测速枪测的转速得到的数据大致如下占空比编码器测量转速(rpm)测速枪参考(rpm)误差20%63.5622.4%40%132.61302.0%60%199.31971.2%80%268.92700.4%从这个表能看到一个规律转速越高误差越小这正是M法的特性。到低转速时10ms采样间隔内可能只有两三个脉冲量化误差会拉到4个百分点以上。如果你想在100rpm以下还有稳定精度建议把采样周期加到50ms或者换更高线数的编码器。另外一个常见问题是计数方向反了。如果电机正转时脉冲增量为负不需要改接线直接把TIM4的极性配置反向或者在代码里把g_motor_pulse取反就行。注意不能随意颠倒A、B两相的配置否则倍频逻辑会乱掉。6. 常见问题与排查技巧6.1 踩坑典型案例现象一转速读数特别大明显不合理。新手最容易犯的错就是采样周期和PULSE_PER_REV配得不一致。比如代码里采样周期还是50ms但公式里的SAMPLE_PERIOD_S填成了0.01这一下就差了5倍。排查时把变量全部打印出来一项项核对。现象二转速跳动厉害一会正一会负。这是个经典问题。如果你不用int16_t读CNT而用uint16_t当CNT在0附近回绕时差值会被算成65535级别的巨大数字。读CNT务必用有符号类型。另外编码器接线接触不良也会导致偶发方向翻转优先检查接插件。现象三电机不转时转速读数缓慢漂移。考虑是不是存在外部干扰比如PWM驱动信号耦合过来的毛刺。输入滤波值加大到0x1F或者在软件里设置一个死区比如|g_motor_pulse|小于3个脉冲就置0。这个死区值取决于编码器线数和采样周期慢慢调。6.2 测速模块调试速查表问题现象可能原因排查顺序读数始终为0编码器供电没接、A/B接错先量VCC再量A/B电压最后看波形读数正常但方向反A/B相序接反在CubeMX里交换极性即可不用改硬件低速跳变严重采样周期太短、干扰增大滤波、增加采样周期高速丢步定时器时钟配置错误、输入滤波过大核对CubeMX时钟树降低滤波值很多人调试编码器习惯用串口把转速数据传到上位机画曲线我推荐一个更直观的办法直接驱动一个数码管或者屏幕显示转速肉眼能看到的转速变化比看串口数据快得多。稳了之后再换回上位机细调。7. 项目扩展与后续方向测速只是闭环控制的第一步。拿到精准的速度反馈后后面能加的东西就多了速度闭环PID用编码器算出来的转速当作反馈量和期望转速做PID控制就可以让电机在负载波动时保持恒定速度。这不是复杂的事重点是把PID的积分限幅和输出限幅算清楚不然一上电就飞车。位置控制在速度环外面再套一层位置环记录CNT的绝对值控制转轴转到指定角度。这种方案用在舵机、转台、机械臂关节上都很常见。多电机同步两块板子之间通过CAN或者485通信把各自的编码器数据同步交换做双轮差速小车或者四驱底盘能明显改善直线跑偏的问题。编码器接口模式只是STM32定时器功能里的一小块但这块用熟了你会发现剩下的正交解码、PWM捕获、PWM输出、输入捕获其实都是同一套外设的变体。把这个学会了再去上手FOC控制、步进电机闭环这些项目底子就有了。实话说测速这种“小功能”往往决定了整套系统的性能上限。你在编码器这块多花点时间把原理吃透后面调PID时会感谢自己。