1. 从脉冲到速度:一个被低估的认知断层
很多人第一次用STM32接编码器,代码跑通了,串口也能打印出数,但盯着那个不断跳动的数字,心里其实是虚的——这个数到底代表什么?它和电机转了多少圈、现在跑多快,中间隔着什么?我见过太多项目卡在这一步:硬件没问题,程序也能跑,但速度环调不稳,或者位置算出来总是差那么一点。根子往往不在PID参数上,而在于对“编码器读到的到底是什么”这件事没有真正想透。
编码器输出的本质是脉冲,而我们在控制里需要的是速度和位置。从脉冲到速度,中间要经过方向判别、计数、单位换算、时间基准、滤波这一整套链路。任何一个环节理解偏了,后面全是歪的。这篇文章就围绕这条链路展开,把增量式编码器、AB相正交解码、STM32定时器编码器模式、M法/T法测速、PWM调速之间的关系一层层拆开。适合正在做电机控制、云台、机器人底盘、CNC进给的朋友,也适合刚接触STM32定时器、对编码器模式一知半解的初学者。看完你应该能自己回答:我的编码器每转一圈到底产生多少脉冲,我的速度值是怎么算出来的,为什么我的速度读数会抖。
2. 编码器输出的物理本质与信号形态
2.1 增量式编码器的三码道结构
市面上最常见的增量式编码器,内部核心是一个带狭缝的码盘加光电对管,或者磁编用霍尔/磁阻元件对着多极磁环。所谓“三码道”,指的是A相、B相、Z相三路输出。A和B是两路方波,它们的频率相同,但相位差90度电角度,这就是“正交”的由来。Z相每转一圈输出一个脉冲,用来做机械零位校准。
为什么是90度而不是180度?因为只有正交才能同时判方向和倍频。如果A、B同相,你只能数脉冲个数,无法知道是正转还是反转。90度相位差让两路信号组合出四种状态:00、01、11、10,正转时按这个顺序循环,反转时逆序。STM32的编码器接口就是靠这个状态机来硬件判向和计数的,不需要你在中断里手动判断A和B谁先谁后。
注意:有些低成本编码器标称“AB相”,但实际输出的是两路同频同相或相位差不是严格90度的信号,这种接上STM32编码器模式后计数会乱。选型时务必确认是标准正交输出。
2.2 分辨率、线数与倍频的关系
编码器参数里常看到“500线”“1000线”“2500线”,这个“线”指的是码盘一圈的物理狭缝数,也就是每转A相(或B相)输出的脉冲数。但STM32编码器模式默认工作在四倍频,因为它对A、B的每个边沿都计数。所以实际计数分辨率是线数的4倍。
举个例子:一个500线的编码器,A相每转输出500个脉冲,B相同样500个,但相位差90度。四倍频后,定时器每转会计数 500 × 4 = 2000 个计数。如果电机轴和编码器轴之间还有减速比,比如减速箱是30:1,那输出轴转一圈,编码器转30圈,定时器计数就是 2000 × 30 = 60000。这个数字直接决定了你的位置精度和速度计算的最小分辨单位。
很多人算速度时直接用“脉冲数/时间”,但忘了乘4、忘了减速比、忘了轮径,最后速度值差几倍甚至几十倍。我建议在代码里把这些系数单独定义成宏,别散落在各处。
2.3 集电极开路、推挽与差分输出
编码器输出级常见三种:集电极开路(OC)、推挽、差分(RS422)。OC输出需要外接上拉电阻才能形成高电平,上拉电阻值影响边沿陡峭程度和功耗。推挽输出驱动能力强,适合短距离直接接MCU。差分输出抗干扰最好,长线传输时用,但需要接收芯片转成单端再进STM32。
如果你用OC输出的编码器直接接STM32,没加上拉,读到的可能是常低或者波形严重变形,计数自然不对。上拉电阻一般选1k到4.7k,太小功耗大,太大边沿变缓,高速时丢脉冲。这个细节在调试时经常被忽略,尤其是从别人那里拿来的板子,编码器接口没焊上拉,查半天以为是程序问题。
3. STM32定时器编码器模式的工作机制
3.1 为什么用硬件编码器接口而不是外部中断
最原始的做法是把A相或B相接外部中断,在中断里读另一相电平判方向,然后手动加减计数。这种方法在低速时能用,但脉冲频率一高,中断开销就爆炸。假设编码器1000线、四倍频、电机3000转/分,脉冲频率是 1000×4×3000/60 = 200kHz。每5微秒一次中断,STM32基本不用干别的了。
STM32的定时器编码器模式把判向和计数全部交给硬件,CPU只需要定期读计数器值。这是它最大的价值。你配置好TIM的编码器模式后,A、B两相接到定时器的两个输入通道上,硬件自动根据边沿和电平关系增减CNT。你读CNT就是位置,读两次CNT做差就是位移。
3.2 编码器模式的配置要点
以STM32F1/F4常见的TIM2、TIM3、TIM4为例,配置步骤大致是:使能定时器时钟和GPIO时钟,把对应引脚配成浮空输入或上拉输入,配置TIM_EncoderInterfaceConfig选择编码器模式(TI1、TI2或both edge),设置ARR为最大计数值,使能定时器。
关键点有几个。第一,ARR决定计数范围。16位定时器ARR最大65535,如果你正反转来回转,CNT会在0和65535之间回绕。读位置时要做溢出处理,否则从65535跳到0会算出一个巨大的负速度。第二,输入滤波。TIM的ICFilter可以设置滤波带宽,编码器信号有毛刺时能滤掉,但设太大高速时会丢脉冲。第三,极性。如果发现正转计数减小,把其中一个通道的极性反过来就行,不用改硬件接线。
// 以TIM3为例的编码器模式初始化片段 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA6->TIM3_CH1, PA7->TIM3_CH2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 65535; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_ICStructInit(&TIM_ICInitStructure); TIM_ICInitStructure.TIM_ICFilter = 6; TIM_ICInit(TIM3, &TIM_ICInitStructure); TIM_Cmd(TIM3, ENABLE);这段代码里TIM_EncoderMode_TI12表示A、B两相都计数,也就是四倍频。TIM_ICFilter=6是中等滤波,具体值要根据你的脉冲频率和干扰情况调。
3.3 计数方向与溢出处理
读位置时,我习惯用int16_t或int32_t来接收CNT,然后做差分。如果两次读取之间CNT从65500跳到100,说明正向溢出了一次,实际位移是 (65536 - 65500) + 100 = 136。反过来从100跳到65500就是负溢出。这个逻辑必须写对,否则高速时位置会突然跳变。
更稳妥的做法是用一个累计位置变量,每次读CNT后计算差值,把差值累加进去,同时更新上一次的CNT。这样即使溢出也能正确处理。注意差值要用有符号数计算,并且考虑16位回绕。
4. 从脉冲计数到速度值的换算方法
4.1 M法测速:固定时间读脉冲数
M法是最直观的:每隔固定时间T读一次编码器计数,得到Δcount,速度 = Δcount / T。这个方法的优点是低速时也能测,缺点是T固定时低速分辨率差。比如T=10ms,编码器四倍频后每转2000计数,电机1转/分时,10ms内只走了 2000/60×0.01 ≈ 0.33 个计数,四舍五入就是0或1,速度读数会跳。
M法适合中高速场合。T的选择要权衡:T太小,低速分辨率差;T太大,速度响应滞后。一般T取5ms到50ms,根据你的控制周期来定。如果速度环周期是1ms,你可以每10个周期算一次速度,或者用滑动平均。
4.2 T法测速:固定脉冲数测时间
T法反过来:记录编码器走过固定脉冲数(比如1个脉冲)所用的时间,速度 = 固定脉冲数 / 时间。低速时一个脉冲的时间长,定时器计数值大,分辨率高;高速时时间短,分辨率差。所以T法适合低速。
实际工程里常用M/T法结合:高速用M法,低速用T法,中间过渡。但实现复杂一些。对于大多数STM32电机控制项目,M法加滑动滤波已经够用。
4.3 单位换算:从count到rpm和m/s
假设编码器500线,四倍频,减速比30,轮径65mm。每输出轴转一圈的计数是 500×4×30 = 60000。如果10ms内读到Δcount=600,那转速 = 600/60000 转/10ms = 0.01转/10ms = 1转/秒 = 60rpm。线速度 = 60rpm × π × 0.065m / 60 = 0.204 m/s。
把这些系数写成宏:
#define ENCODER_LINES 500 #define GEAR_RATIO 30 #define WHEEL_DIAMETER_M 0.065 #define COUNTS_PER_REV (ENCODER_LINES * 4 * GEAR_RATIO) #define SPEED_SAMPLE_TIME 0.01f // 10ms float rpm = (delta_count / (float)COUNTS_PER_REV) / SPEED_SAMPLE_TIME * 60.0f; float mps = rpm * 3.1415926f * WHEEL_DIAMETER_M / 60.0f;这样改参数时只改宏,不用满代码找系数。
4.4 速度滤波:为什么你的速度读数在抖
原始M法速度一定有抖动,因为编码器脉冲和采样时钟不同步,Δcount本身有±1的量化误差。低速时这个误差占比大,速度读数可能在一半范围内跳。解决办法有几种:滑动平均、一阶低通、卡尔曼。滑动平均最简单,取最近N次速度的平均值,N取5到10。一阶低通是 y = α×x + (1-α)×y_prev,α取0.1到0.3。卡尔曼效果好但调参麻烦。
我一般先用滑动平均,如果还抖再上低通。注意滤波会引入相位滞后,速度环带宽高时不能滤太狠,否则系统会振荡。
5. PWM调速与编码器闭环的配合
5.1 PWM频率与编码器采样的关系
PWM调速的本质是改变电机平均电压,从而改变转速。PWM频率一般选10kHz到20kHz,低于20kHz可能听到啸叫,高于20kHz开关损耗大。编码器采样周期和PWM周期是独立的,但速度环周期通常是PWM周期的整数倍,比如PWM 10kHz,速度环1kHz,每10个PWM周期调一次速度。
这里有个坑:如果PWM频率和编码器脉冲频率产生某种整数关系,速度读数可能出现周期性波动。我遇到过PWM 8kHz、编码器脉冲频率接近8kHz时,速度值有规律地跳。后来把PWM改成12kHz就好了。所以调试时如果发现速度有规律抖动,不妨换个PWM频率试试。
5.2 死区、限幅与积分饱和
PWM输出到H桥驱动时,上下桥臂之间要留死区,防止直通。死区时间一般几百纳秒到1微秒,具体看MOS管和驱动芯片。死区会损失一点有效电压,低速时影响明显,可能需要死区补偿。
速度环PID输出要限幅到PWM的占空比范围,比如0到100%。积分项要抗饱和,否则电机堵转时积分会累积到很大,松开后猛冲。常用做法是积分分离或者输出限幅时停止积分。
5.3 编码器反馈极性对速度环的影响
如果编码器反馈的速度符号和PWM输出的方向相反,速度环会变成正反馈,一给指令就飞车。判断方法:手动给一个正PWM,看编码器速度读数是否为正。如果为负,要么把编码器计数方向反过来,要么把PWM输出极性反过来。这个必须在闭环前确认,否则上电就失控。
6. 常见问题与排查技巧实录
6.1 编码器计数不动或只增不减
先查硬件:A、B相有没有接反、有没有上拉、供电是否正常。用示波器看A、B波形,应该是两路方波,相位差90度,幅值干净。如果波形有毛刺,加滤波电容或调TIM的ICFilter。如果波形正常但计数不动,检查TIM编码器模式配置,特别是TIM_EncoderInterfaceConfig的参数和GPIO复用功能是否使能。
只增不减通常是方向判别失效,可能是A、B接在了同一个通道,或者其中一个通道没配成编码器输入。也有可能是编码器本身AB相相位差不是90度,这种只能换编码器。
6.2 速度值跳变或溢出
速度跳变最常见的原因是CNT溢出没处理。16位定时器在正反转频繁时很容易溢出。解决办法是用32位累计位置,或者换32位定时器。另一个原因是采样周期不稳定,比如在中断里算速度但中断被高优先级任务延迟了。用定时器硬件触发采样,或者记录实际时间间隔来补偿。
如果速度值偶尔出现极大值,检查是否有丢脉冲或额外脉冲。丢脉冲可能是滤波太狠或信号边沿太缓,额外脉冲可能是干扰。示波器抓波形最直接。
6.3 低速时速度读数为零或抖动
这是M法的固有缺陷。低速时Δcount太小,量化误差大。解决办法:增大采样时间T,或者低速切换到T法,或者用位置差分加低通。如果项目对低速性能要求高,建议用T法或M/T法。另外,编码器分辨率越高,低速性能越好,但成本也高。
6.4 PWM干扰编码器信号
PWM开关时dv/dt很大,容易通过空间或电源耦合到编码器信号线上。表现是电机一转速度读数就乱,不转就正常。解决办法:编码器线用屏蔽线并单端接地,PWM线远离编码器线,电源加去耦电容,必要时用差分编码器。我遇到过PWM背光干扰显示的情况,原理类似,都是开关噪声耦合。
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 计数不动 | 接线错误、无上拉、模式未使能 | 示波器看波形、查GPIO配置 | 修正接线、加上拉、使能TIM |
| 只增不减 | AB相接错、相位差不对 | 交换AB相测试 | 换编码器或改接线 |
| 速度跳变 | CNT溢出、采样不稳 | 打印CNT原始值 | 32位累计、硬件触发采样 |
| 低速为零 | M法分辨率不足 | 增大T或换T法 | 提高编码器线数、M/T法 |
| PWM干扰 | 开关噪声耦合 | 电机转时看波形 | 屏蔽线、远离、去耦 |
6.5 实操心得:先开环验证再闭环
我的习惯是先把编码器读数调通,手动转电机轴,看计数是否正比于转角,方向是否正确。然后开环给PWM,看速度读数是否随占空比变化。确认这两步都没问题,再上闭环。很多人跳过开环验证直接调PID,结果方向反了或者计数不对,PID怎么调都不对,浪费大量时间。
另一个心得是:编码器线不要和电机线捆在一起走,尤其是大电流电机线。我见过一个底盘项目,编码器线和电机线在同一线槽里,电机一加速速度就乱跳,分开走线后立刻正常。
7. 几个容易混淆的概念澄清
7.1 编码器分辨率与精度不是一回事
分辨率是能分辨的最小角度,由线数和倍频决定。精度是读数与真实角度的偏差,受码盘刻线误差、安装偏心、信号质量影响。高分辨率不等于高精度。一个500线编码器如果安装偏心,精度可能还不如一个安装良好的250线编码器。选型时不要只看线数。
7.2 绝对式与增量式的选择
增量式编码器便宜、接口简单,但断电后位置丢失,需要回零。绝对式编码器断电后位置保留,但贵、接口复杂(SSI、BiSS、I2C等)。如果项目不需要断电记忆位置,增量式足够。如果需要,可以考虑带Z相回零或者加电池备份的绝对值编码器。磁编码器AS5047P这类芯片是绝对值输出,SPI或ABI接口,适合空间受限的场合。
7.3 编码器模式与PWM模式的资源冲突
STM32的同一个定时器不能同时做编码器模式和PWM输出。如果你用TIM3做编码器,那TIM3的PWM输出就不能用了。选定时器时要规划好:一般TIM1、TIM8做PWM,TIM2、TIM3、TIM4做编码器。如果编码器路数多,可以考虑用外部计数芯片或者软件解码,但软件解码高速时吃力。
8. 从脉冲到速度的完整代码框架
把前面这些串起来,一个典型的速度采集框架是这样的:TIM编码器模式初始化,定时器中断或主循环定时采样,读CNT算差分,累计位置,算速度,滤波,输出给速度环。下面是一个简化的框架示意。
volatile int32_t g_position = 0; volatile int16_t g_last_cnt = 0; volatile float g_speed_rpm = 0.0f; void Encoder_Update(void) // 每10ms调用一次 { int16_t now_cnt = (int16_t)TIM3->CNT; int16_t diff = now_cnt - g_last_cnt; g_last_cnt = now_cnt; g_position += diff; float delta_count = (float)diff; float rpm = (delta_count / (float)COUNTS_PER_REV) / SPEED_SAMPLE_TIME * 60.0f; // 一阶低通 static float rpm_filtered = 0.0f; rpm_filtered = 0.2f * rpm + 0.8f * rpm_filtered; g_speed_rpm = rpm_filtered; }这个框架里,g_position是累计位置,g_speed_rpm是滤波后速度。注意diff用int16_t接收,因为CNT是16位,差分结果在-32768到32767之间,正好用int16_t。如果采样周期内位移超过这个范围,说明速度太快或者采样太慢,需要缩短周期或换32位定时器。
9. 调试工具与观测手段
调试编码器问题,示波器是最有用的。四通道示波器同时看A、B、PWM和电源,能快速定位是信号问题还是电源问题。如果没有示波器,可以用逻辑分析仪,便宜的那种也能看波形和时序。STM32的定时器还可以配置成输入捕获模式,测量脉冲宽度和周期,用来验证编码器信号频率。
串口打印也是常用手段,但要注意打印本身会占用时间,高速时可能影响采样。建议用DMA串口或者把数据存到数组里批量打印。另外,用SWD调试器实时看变量比串口更方便,不占用CPU时间。
我个人习惯在调试初期用示波器确认波形,中期用SWD看变量,后期用串口记录数据做分析。不同阶段用不同工具,效率最高。
10. 写在最后的一点个人体会
编码器这个东西,原理不复杂,但细节特别多。从线数、倍频、减速比、轮径,到CNT溢出、滤波、PWM干扰,每一个环节都可能让你调半天。我的经验是:先把物理量和计数的关系在白纸上算清楚,写进代码宏定义里;然后开环验证编码器读数,确认方向和比例都对;最后再上闭环调PID。跳过任何一步,后面都要还债。
还有一点,别迷信高线数。线数越高,同样转速下脉冲频率越高,对MCU和信号质量要求越高。够用就行,留点余量。我见过用2500线编码器做低速云台,脉冲频率不高,但信号处理没做好,照样丢脉冲。反过来,500线编码器配合好的滤波和M/T法,低速也能做得很稳。
最后分享一个小技巧:如果速度读数在某个特定转速下特别抖,试试改变速度采样周期,让它和PWM周期错开。很多时候抖动是采样和开关频率拍频造成的,错开就好了。这个在电机控制里很常见,但文档里很少写。