1. 从"读到了什么"说起:编码器信号链的物理本质
很多人第一次用STM32接编码器,代码跑通了,定时器也能读出数来,但被问一句"你读到的这个数到底是什么"就卡住了。是脉冲数?是位置?是速度?还是某种中间量?这个问题不搞清楚,后面做速度闭环、做位置控制、做多圈计数,迟早要翻车。
编码器本质上是把机械运动转换成电信号的传感器。无论是光电式、磁式还是电容式,最终输出的都是两路(或三路)方波信号,通常标记为A相、B相,有的还有Z相(索引相)。A、B两相在相位上相差90度电角度,这就是所谓的"正交编码"(Quadrature Encoding)。Z相每转一圈输出一个脉冲,用来做零位校准。
那STM32定时器的编码器模式读到的到底是什么?答案很明确:它读到的是计数器的计数值(CNT),这个值是对A、B两相边沿的累计计数结果。在四倍频模式下,A相上升沿、A相下降沿、B相上升沿、B相下降沿都会触发计数,所以CNT的变化量等于编码器实际脉冲数的4倍。这个细节很多人忽略,导致算出来的速度总是差4倍。
举个例子,一个1000线的增量式编码器,转一圈A相输出1000个脉冲。如果定时器配置为四倍频(TI1和TI2都触发),那么CNT会变化4000。如果你按1000去算转速,结果就偏了。这个坑我在早期做电机控制的时候踩过,当时调了半天PID,以为是参数问题,最后发现是倍频系数搞错了。
从脉冲到速度,中间还隔着一层"时间"。速度的本质是位置对时间的导数,所以你必须知道两个信息:一段时间内CNT变化了多少,以及这段时间有多长。前者靠定时器读取,后者靠定时器溢出中断或者系统滴答定时器来提供时间基准。这两个信息缺一不可,而且时间基准的精度直接决定了速度计算的精度。
还有一个容易被忽视的点:编码器信号的质量。A、B两相的边沿如果抖动严重,或者存在毛刺,定时器的计数器就会误触发,CNT值就会跳变。这种情况下你读到的"脉冲"其实是噪声。硬件上通常需要加RC滤波或者施密特触发器整形,软件上可以在定时器输入捕获通道配置数字滤波(STM32的ICFilter位),这个后面会详细讲。
2. STM32定时器编码器模式的配置逻辑与倍频选择
2.1 为什么用定时器的编码器模式而不是外部中断
初学者最容易想到的方案是用外部中断(EXTI)去捕获A相的上升沿,然后在中断里判断B相电平来决定方向。这个方案能跑,但有几个致命问题:第一,高速旋转时中断频率极高,CPU会被中断淹没,根本没时间做其他事情;第二,中断响应有延迟,两路信号的判断可能出错;第三,无法利用硬件四倍频,分辨率白白浪费。
STM32的定时器编码器模式是硬件级别的解决方案。它把A、B两相信号直接接到定时器的TI1和TI2输入通道,由硬件自动完成方向判断和计数。CPU只需要在需要的时候去读CNT寄存器就行,完全不占用中断资源。这是STM32做电机控制的一大优势,也是为什么几乎所有STM32电机控制教程都会用这个模式。
配置的核心寄存器是TIMx_SMCR(从模式控制寄存器)。把SMS位设置为001就是编码器模式1(只在TI1边沿计数),设置为010是编码器模式2(只在TI2边沿计数),设置为011是编码器模式3(TI1和TI2都计数,即四倍频)。同时要把CC1S和CC2S配置为01,把通道映射到TI1和TI2。
2.2 倍频系数怎么选:一倍频、二倍频还是四倍频
倍频系数的选择直接决定了速度分辨率和低速性能。我用一个实际案例来说明。
假设你用的是一个500线的增量式编码器,电机额定转速3000RPM。如果只做一倍频(只在A相上升沿计数),每转得到500个计数。在1ms的采样周期内,3000RPM对应50转/秒,也就是每毫秒0.05转,对应25个计数。这个分辨率在高速时够用,但在低速时就很粗糙了。如果电机降到30RPM,每毫秒只有0.25个计数,四舍五入后要么是0要么是1,速度曲线会像台阶一样跳。
换成四倍频,每转2000个计数。同样30RPM下,每毫秒有1个计数,虽然还是不够细腻,但至少不会频繁出现0。如果采样周期拉长到10ms,四倍频下就有10个计数,分辨率提升了4倍。
但四倍频也不是没有代价。倍频越高,对信号质量的要求越高。如果编码器输出信号有抖动,四倍频会把抖动放大4倍。而且高速时CNT溢出更快,16位定时器在四倍频下,500线编码器转到3000RPM时,CNT每秒钟变化100000次,16位计数器(最大65535)大约0.65秒就溢出一圈。所以必须配合溢出中断来扩展计数范围。
我的经验是:低速高精度场景用四倍频,高速场景用二倍频或一倍频,信号质量差的场合降频使用。具体选哪个,拿示波器看一下编码器输出波形再决定。
2.3 定时器溢出与多圈计数的处理
16位定时器的CNT范围是0到65535,溢出是必然的。处理溢出有两种常见方案。
第一种是开启溢出中断,在中断里维护一个软件计数器。每次溢出中断触发,软件计数器加1或减1(取决于计数方向)。读取位置时,把软件计数器的值左移16位再加上当前CNT。这个方案简单可靠,但中断频率不能太高,否则会挤占CPU时间。
第二种是利用定时器的重复计数器(RCR,高级定时器才有)或者直接用32位定时器(如STM32F4系列的TIM2和TIM5)。32位定时器省去了溢出处理的麻烦,但并不是所有STM32型号都有。
我在实际项目中更倾向于第一种方案,因为它的可移植性好,换芯片不用改逻辑。但要注意一点:读取CNT和读取软件计数器必须是原子操作,否则可能在读取过程中发生溢出中断,导致数据不一致。解决办法是先关中断,读完之后再开中断,或者用定时器的UIF标志位来判断是否发生了溢出。
3. 从CNT到速度:M法、T法与M/T法的实战取舍
3.1 M法测速:固定时间读脉冲
M法是最直观的方法:每隔固定时间T读一次CNT,算出差值ΔCNT,然后速度 = ΔCNT / (T × 每转计数) × 60,单位是RPM。
这个方法的优点是实现简单,适合高速场景。缺点是低速时ΔCNT很小,量化误差大。比如T=1ms,每转4000个计数,电机转1RPM时,ΔCNT = 4000 × 1/60 × 0.001 ≈ 0.067,四舍五入后是0,根本测不出速度。
M法的分辨率取决于采样时间T和每转计数。要提高低速分辨率,只能加大T,但T太大会导致速度响应滞后,动态性能变差。这是一个矛盾。
3.2 T法测速:固定脉冲测时间
T法反过来:记录两个相邻脉冲之间的时间间隔,速度 = 1 / (时间间隔 × 每转计数) × 60。
T法在低速时精度很高,因为低速时脉冲间隔大,时间测量相对准确。但高速时脉冲间隔很小,定时器时钟频率不够高的话,时间测量误差会很大。比如定时器时钟84MHz,电机3000RPM,每转4000个计数,脉冲间隔 = 60/(3000×4000) = 5微秒,对应420个时钟周期。这个精度还可以,但如果转速再高,脉冲间隔进一步缩小,误差就不可忽略了。
3.3 M/T法:兼顾高低速的混合方案
M/T法的思路是:同时测量固定时间内的脉冲数和最后一个脉冲的剩余时间。具体做法是,在固定时间T内计数脉冲,同时用另一个定时器测量从最后一个脉冲到T结束的时间。这样在低速时,虽然脉冲数少,但剩余时间的测量补偿了精度;在高速时,脉冲数多,剩余时间占比小,精度也够。
M/T法的实现复杂度明显高于M法和T法,需要两个定时器配合,还要处理边界情况。我在做高性能伺服控制时用过M/T法,效果确实好,但调试周期也长。如果项目对低速精度要求不是特别苛刻,M法配合适当的采样时间通常就够了。
下面这张表是我在实际项目中总结的选型参考:
| 测速方法 | 适用转速范围 | 低速精度 | 高速精度 | 实现复杂度 | 资源占用 |
|---|---|---|---|---|---|
| M法 | 中高速 | 差 | 好 | 低 | 1个定时器 |
| T法 | 中低速 | 好 | 差 | 中 | 2个定时器 |
| M/T法 | 全范围 | 好 | 好 | 高 | 2-3个定时器 |
3.4 速度计算的滤波与平滑
不管用哪种方法,算出来的速度都会有噪声。原因很多:编码器信号抖动、定时器量化误差、机械振动等。直接拿原始速度去做PID,电机就会发出"滋滋"的声音,甚至震荡。
常用的滤波方法有三种。第一种是滑动平均滤波,把最近N次的速度值取平均。简单有效,但会引入相位滞后,N越大滞后越严重。第二种是一阶低通滤波,公式是 v_filtered = α × v_new + (1-α) × v_old,α越小滤波越强,但响应越慢。第三种是卡尔曼滤波,理论最优但计算量大,一般用在高端伺服上。
我的习惯是先用一阶低通滤波,α取0.3到0.5之间,根据实际效果调整。如果还不够平滑,再考虑滑动平均。卡尔曼滤波除非是特别高端的应用,否则没必要上。
注意:滤波会引入相位滞后,在做高动态响应控制时要权衡。滤波强度越大,系统响应越慢,可能引起震荡。建议先用较小的滤波系数,逐步加大直到速度曲线平滑为止。
4. 硬件信号链上的那些坑:从编码器输出到MCU引脚
4.1 编码器输出类型与电平匹配
增量式编码器的输出类型主要有三种:集电极开路(OC)、推挽输出和差分输出(RS422)。集电极开路输出需要外接上拉电阻,否则无法输出高电平。推挽输出可以直接接MCU引脚,但要注意电平匹配。差分输出抗干扰能力最强,但需要差分接收器转换成单端信号。
STM32的IO引脚通常是3.3V电平,而很多编码器是5V供电、5V输出。直接把5V信号接到3.3V的IO上,短时间可能没事,但长期运行会损坏IO口。正确的做法是用电平转换芯片,或者用电阻分压。电阻分压虽然简单,但会降低信号边沿的陡峭度,高速时可能导致计数错误。
我在一个项目里用过电阻分压方案,低速没问题,但电机转速超过2000RPM后就开始丢脉冲。后来换成专用的电平转换芯片(如TXS0108E),问题就解决了。所以高速场景一定要用专用电平转换芯片,不要图省事用电阻分压。
4.2 滤波电路的设计与参数计算
编码器信号在传输过程中容易受到电机PWM噪声的干扰。尤其是PWM频率和编码器信号频率接近时,干扰会更明显。硬件上通常需要加RC低通滤波。
RC滤波的截止频率计算公式是 f_c = 1/(2πRC)。截止频率要高于编码器最高输出频率,否则会衰减有效信号。假设编码器最高输出频率是100kHz,那么截止频率至少要是200kHz。取R=100Ω,C=10nF,截止频率约为159kHz,基本够用。
但RC滤波会引入延迟,延迟时间约为RC。100Ω×10nF=1微秒,这个延迟在大多数应用中可以接受。如果编码器频率更高,需要减小RC值,但滤波效果也会减弱。这是一个权衡。
除了RC滤波,还可以用施密特触发器(如74HC14)来整形信号。施密特触发器有滞回特性,能有效抑制噪声引起的误触发。我在一个干扰特别严重的项目里,RC滤波加施密特触发器双管齐下,才把信号搞干净。
4.3 布线注意事项与屏蔽
编码器信号线一定要远离电机动力线。如果必须交叉,尽量垂直交叉,不要平行走线。信号线最好用双绞线或屏蔽线,屏蔽层单端接地(接控制器端),不要两端都接,否则会形成地环路。
我在一个项目里遇到过编码器计数偶尔跳变的问题,排查了很久,最后发现是编码器线缆和电机线缆绑在一起走了一米多。分开走线后问题就消失了。这个坑很隐蔽,因为大部分时间信号是正常的,只有在电机特定工况下才会出现干扰。
提示:如果编码器线缆较长(超过1米),建议使用差分输出编码器加差分接收器,抗干扰能力比单端信号强很多。差分信号的共模抑制比高,能有效抵消传输过程中的共模噪声。
5. 速度闭环中的编码器数据处理实战
5.1 速度环采样周期与编码器读取的配合
速度环的采样周期通常和PWM频率相关。如果PWM频率是20kHz,速度环可以每1ms执行一次,也就是每20个PWM周期做一次速度计算和PID更新。这个1ms的时间基准可以用系统滴答定时器(SysTick)或者一个专门的定时器来提供。
读取CNT的时机很关键。最好在PWM周期的特定时刻读取,避免在PWM开关瞬间读取,因为那时候噪声最大。我通常会在PWM计数器归零的时候触发ADC采样和编码器读取,这样能保证每次读取的时机一致,减少噪声影响。
5.2 方向判断与符号处理
编码器模式下的CNT是向上还是向下计数,取决于A、B两相的相位关系。正转时CNT增加,反转时CNT减少。但CNT是无符号的16位寄存器,反转时从0变成65535,直接做差会得到一个巨大的正数。
正确的做法是把CNT强制转换为有符号的16位整数(int16_t),然后再做差。这样65535就被解释为-1,差值计算就正确了。这个技巧在C语言里用强制类型转换就能实现:int16_t delta = (int16_t)(cnt_now - cnt_last);。
但要注意,如果两次采样之间CNT变化超过32767,符号就会出错。所以采样周期不能太长,要保证CNT变化量在正负32767以内。对于500线四倍频编码器,32767对应约8转,1ms内转8转意味着480000RPM,显然不可能,所以1ms采样周期是安全的。
5.3 速度限幅与异常保护
编码器故障或者信号干扰可能导致速度计算值异常大。如果不做限幅,PID输出会瞬间饱和,电机可能猛冲。所以速度值一定要做限幅,超过额定转速的120%就认为是异常,触发保护。
还有一种情况是编码器断线。断线后CNT不再变化,速度计算值为0。这时候如果速度环还在运行,PID会以为电机停了,不断加大输出,导致电机飞车。所以需要检测编码器是否正常:如果速度指令不为0但速度反馈持续为0超过一定时间,就判定编码器故障,紧急停机。
我在实际项目中会加一个简单的检测逻辑:每次速度计算后,如果连续N次(比如50次)速度反馈都是0,而速度指令大于某个阈值,就报编码器故障。这个逻辑虽然简单,但能有效防止飞车事故。
6. 调试工具与实测方法:示波器、逻辑分析仪和串口波形
6.1 用示波器看编码器信号质量
调试编码器第一步永远是看波形。把示波器探头接到A相和B相上,观察波形是否干净、边沿是否陡峭、相位差是否接近90度。如果波形上有毛刺或者边沿缓慢,就要先解决硬件问题,软件再怎么调也没用。
看波形的时候要注意示波器的带宽和采样率。编码器信号频率可能达到几百kHz,示波器带宽至少要是信号频率的5倍以上。用低带宽示波器看高频信号,看到的波形是失真的,会误导判断。
6.2 逻辑分析仪抓取正交信号解码过程
逻辑分析仪可以同时抓取A、B、Z三路信号,并且自带正交解码功能。把抓到的数据和STM32读到的CNT值对比,就能验证解码逻辑是否正确。我常用这个方法来排查计数方向错误或者丢脉冲的问题。
比如有一次客户反映电机正转时速度显示为负,我用逻辑分析仪抓了一下,发现A、B两相接反了。交换两相接线后问题解决。这种问题用示波器不容易发现,因为单看一路信号都是正常的,只有对比两路信号的相位关系才能看出来。
6.3 串口输出波形辅助PID调参
调PID的时候,把速度指令、速度反馈、PID输出通过串口发送到上位机,用波形显示工具画出来,能直观地看到超调、震荡、响应时间等指标。我常用的是串口示波器类的工具,把数据按特定格式发送,上位机就能实时显示曲线。
这个方法的优势是能同时看多个变量,而且可以长时间记录。比如观察电机从启动到稳态的整个过程,看看有没有低频震荡或者积分饱和。这些现象用示波器很难捕捉,但用串口波形一目了然。
注意:串口发送数据会占用CPU时间,如果波特率不够高或者发送频率太快,可能影响控制环的实时性。建议用DMA发送,或者降低发送频率(比如每10ms发一次),避免干扰控制逻辑。
7. 几个容易混淆的概念澄清
7.1 编码器分辨率与精度的区别
分辨率是指编码器能分辨的最小角度变化,通常用每转脉冲数(PPR)或者每转计数(CPR)来表示。精度是指编码器输出值与实际角度的偏差。分辨率高不代表精度高。一个10000线的编码器,如果安装偏心或者信号质量差,实际精度可能还不如一个2500线的编码器。
选型的时候不要只看分辨率,还要看精度指标。增量式编码器的精度主要取决于码盘刻线的均匀性和安装同心度。绝对值编码器的精度还取决于内部ADC的分辨率。
7.2 电气角度与机械角度
在电机控制中,编码器输出的机械角度需要乘以极对数才能得到电气角度。比如一个4极对(8极)的电机,机械角度转一圈,电气角度转4圈。FOC控制中用的是电气角度,如果搞错了,电机就不会正常运转。
这个坑我在第一次做FOC的时候踩过。当时用的是2500线编码器,电机是4极对,我直接把机械角度当电气角度用了,结果电机抖动严重,根本转不起来。后来查了资料才明白要乘极对数。
7.3 增量式与绝对值编码器的数据含义差异
增量式编码器输出的是相对位置变化,断电后位置丢失,需要回零。绝对值编码器输出的是绝对位置,断电后位置不丢失。但绝对值编码器的"绝对"是相对于编码器自身的零位,不是相对于机械系统的零位。安装后仍然需要做一次零位校准,把编码器零位和机械零位对齐。
绝对值编码器的接口类型也更多:SSI、BiSS、EnDat、CANopen等。不同接口的协议不同,读取方式也不同。选型的时候要考虑MCU是否支持相应的接口,或者是否需要额外的接口芯片。
8. 从脉冲到速度的完整代码框架
8.1 定时器编码器模式初始化
以STM32F103的TIM3为例,配置为编码器模式四倍频。关键配置项包括:预分频器设为0(不分频),自动重装载值设为65535(最大范围),CC1S和CC2S设为01(映射到TI1和TI2),SMS设为011(编码器模式3),IC1Filter和IC2Filter设为适当值(比如6,对应约1.3微秒的滤波时间)。
滤波参数的选择要根据编码器信号频率来定。滤波时间太长会丢失高速脉冲,太短则起不到滤波效果。一般取信号最小脉宽的1/4到1/2比较合适。
8.2 速度计算函数的实现
速度计算函数的核心逻辑是:读取当前CNT,与上次CNT做差得到ΔCNT,除以采样时间得到脉冲频率,再换算成RPM。同时要做溢出处理、符号处理和限幅。
采样时间可以用SysTick的计数值来测量,这样比固定时间更准确。因为中断响应有延迟,实际采样间隔可能不是精确的1ms,用SysTick测量实际间隔能提高速度计算精度。
8.3 与PID控制器的对接
速度反馈值算出来后,送入PID控制器。PID的输出限幅要和PWM占空比范围匹配。比如PWM是0到1000对应0到100%占空比,PID输出就要限幅在0到1000之间。
积分项要做抗饱和处理。如果PID输出已经饱和,但误差还在累积,积分项会越来越大,导致退出饱和时响应迟钝。常用的抗饱和方法是:当输出饱和时,停止积分累积,或者只累积使输出退出饱和的方向。
9. 实际项目中的经验教训
做电机控制这些年,编码器相关的问题占了调试时间的一大半。总结下来,最容易出问题的环节往往不是算法本身,而是硬件和底层配置。
有一次客户反馈电机低速时速度波动大,高速正常。我查了代码,M法测速,1ms采样。低速时ΔCNT只有几个,量化误差导致速度波动。改成四倍频后,ΔCNT翻倍,波动明显减小。这个案例说明,倍频系数的选择对低速性能影响巨大。
还有一次是编码器计数偶尔跳变,一天出现几次,每次跳变导致电机瞬间抖动。用逻辑分析仪抓了很久才抓到,发现是编码器线缆经过一个接触器附近时,接触器吸合的瞬间产生干扰。把线缆移开后问题解决。这种偶发问题最难查,因为大部分时间都是正常的。
最后一个教训是关于初始化顺序的。编码器定时器必须在电机使能之前初始化完成,否则电机启动瞬间的编码器读数可能是随机的,导致位置环出错。我在一个项目里因为初始化顺序问题,电机每次上电都会轻微抖动一下,后来调整了初始化顺序就好了。
编码器这个东西,说简单也简单,说复杂也复杂。核心就是搞清楚你读到的数是什么、怎么来的、怎么用。把这三点搞明白,大部分问题都能自己排查。