1. 从一个反直觉的现象说起:为什么脉冲数不等于速度
很多人第一次接触编码器测速,脑子里会有一个非常自然的想法:编码器每转一圈输出固定数量的脉冲,那我只要数一秒内收到多少个脉冲,再除以每圈的脉冲数,不就得到转速了吗?这个思路本身没错,但如果你真把它直接写进代码里,大概率会得到一个抖得没法看的数值,甚至电机一慢下来就直接归零。
我在一个运动控制项目里就吃过这个亏。当时用的是一款增量式编码器,标称每转2500线,四倍频之后是10000个计数。电机额定3000转每分钟,算下来满速时每秒有50万个脉冲,听起来很充裕。但实际调试时把转速降到每分钟30转,一秒内只有500个脉冲,再考虑采样窗口的边界效应,读数就开始一跳一跳的。更麻烦的是,当转速低到每分钟几转的时候,固定时间窗口内可能一个脉冲都数不到,速度直接显示为零,可电机明明在转。
这个现象背后其实藏着一个核心问题:编码器直接输出的是位置增量,不是速度。速度是位置对时间的导数,而求导这个操作对噪声极其敏感。你数脉冲的那个"一秒",本质上是在做一个粗糙的差分运算,窗口越长越平滑但延迟越大,窗口越短越实时但噪声越大。这就是为什么工业上从来不会用"数固定时间脉冲"这种原始方法,而是用M法、T法或者M/T法这些经过设计的测速方案。
所以这篇文章想做的事情很明确:把"编码器读到的到底是什么"这件事从头讲清楚,从最底层的正交脉冲信号,到计数器如何累加,再到怎么把这个累加值变成可用的速度量。适合正在做电机控制、机器人底盘、CNC或者任何需要闭环调速的开发者,也适合那些被编码器读数折磨过、想搞明白底层原理的人。我会尽量用生活化的类比把原理讲透,同时给出可以直接抄的代码和参数配置。
2. 拆开编码器的输出:A相、B相和Z相到底在说什么
2.1 正交脉冲的物理本质
增量式编码器内部通常有一个带狭缝的码盘和一个光电对管。码盘转动时,光线被狭缝交替遮挡,接收端就产生方波。为了判断方向,厂家会做两路信号,也就是A相和B相,它们的方波在相位上错开90度,这就是"正交"这个词的来源。
为什么非要错开90度?因为只有错开相位,你才能从两路信号的先后顺序判断出转动方向。如果A相上升沿到来时B相是低电平,说明是正转;如果A相上升沿到来时B相是高电平,说明是反转。这个逻辑用硬件或者软件都能实现,硬件上就是一个D触发器,软件上就是读两个引脚的状态做判断。
我习惯用一个类比来解释:想象两个人并排走路,A走在前面一点,B走在后面一点。如果队伍往右走,你总是先看到A再看到B;如果往左走,就先看到B再看到A。编码器就是靠这个"谁先谁后"来判断方向的。
2.2 四倍频:把分辨率榨干
单看A相的上升沿,一圈能数出2500个脉冲。但既然A和B都有上升沿和下降沿,而且它们相位错开,那实际上有四个边沿可以用来计数:A上升、A下降、B上升、B下降。每捕捉一个边沿就计一个数,一圈就能数出10000个计数,这就是四倍频。
四倍频的好处很直接:分辨率提高了四倍,低速时能捕捉到更细的位置变化。但代价是计数中断的频率也高了四倍,对MCU的中断处理能力有要求。我在一个项目里用STM32的定时器编码器模式做四倍频,满速时中断频率到了2MHz,CPU基本被吃满,后来改成只在需要读位置的时候去读计数器寄存器,才把负载降下来。
这里有个容易踩的坑:四倍频计数时,如果A、B两相的信号有抖动或者毛刺,会直接导致计数错误。因为每个边沿都被当作有效事件,毛刺会被当成真实的转动。所以硬件上一定要在编码器输出端加RC滤波,软件上如果条件允许,用定时器的输入滤波功能,把滤波参数设到能滤掉毛刺又不影响正常脉冲的程度。
2.3 Z相:每圈一次的绝对参考
Z相也叫零位信号,码盘每转一圈只输出一个脉冲。它的作用不是用来测速的,而是用来做位置校准的。因为增量式编码器上电时不知道自己在哪,只能从零开始计数,但实际机械位置可能是任意角度。有了Z相,系统可以在第一次遇到Z相时把计数器清零,这样每次上电后的零点就是一致的。
我在做机械臂关节的时候,Z相的作用特别明显。每次上电后先让关节慢速转一圈找Z相,找到之后把位置计数器归零,之后所有角度都是相对于这个零点的。如果没有Z相,每次上电的零点都不一样,绝对位置就无从谈起。
注意:Z相脉冲很窄,有些编码器的Z相宽度只有一个计数周期。如果用软件轮询的方式去读,很容易漏掉。建议把Z相接到有中断功能的引脚上,用边沿触发中断来捕获。
3. 计数器读到的数字,为什么不能直接当速度用
3.1 位置与速度的数学关系
计数器寄存器里存的是一个不断累加的整数,它代表的是位置,单位是"计数"。要得到速度,你需要做的是:在时间t1读一次计数C1,在时间t2读一次计数C2,然后速度等于(C2减C1)除以(t2减t1),再除以每转的计数总数,就得到每秒转数。
这个公式看起来简单,但魔鬼在细节里。首先,C2减C1这个差值在低速时可能很小,比如只有几个计数,而计数器本身可能有正负1的量化误差,算出来的速度误差就很大。其次,t2减t1这个时间间隔如果由软件定时器决定,会有中断响应延迟和抖动,这个抖动在高速时影响不大,但在低速时占比就上来了。
我做过一个实测:用1ms的定时中断去读计数器,电机稳速在每分钟60转,理论每秒1转,每毫秒应该走10个计数。但实际读出来的差值在8到12之间跳,算出来的速度就在每分钟48到72转之间波动。这个波动不是电机真的在抖,而是量化误差和定时抖动叠加的结果。
3.2 固定时间窗口的致命缺陷
固定时间窗口测速,也就是M法,它的本质是用"数脉冲"来近似"测频率"。在高速时,窗口内脉冲多,相对误差小;在低速时,窗口内脉冲少,相对误差大。当转速低到窗口内只有几个脉冲时,误差可能超过50%。
更极端的情况是,如果窗口内一个脉冲都没有,速度读数就是零。但电机可能在以极低的速度转动,只是脉冲还没攒够一个。这时候如果控制器看到速度为零,可能会加大输出试图"启动"电机,结果电机突然加速,造成震荡。
这个缺陷在需要宽调速范围的场合是致命的。比如一个云台电机,既要在高速时快速跟踪,又要在低速时精细微调,固定窗口根本覆盖不了这么大的范围。所以必须引入T法,也就是测周法。
3.3 量化误差的直观估算
假设编码器四倍频后每转10000个计数,定时窗口1ms。在每分钟1转时,每秒走10000个计数,每毫秒走10个计数。量化误差正负1个计数,相对误差就是10%。在每分钟0.1转时,每毫秒走1个计数,量化误差正负1个计数,相对误差就是100%。这个估算说明,M法的相对误差与转速成反比,转速越低误差越大。
反过来,如果改用T法,测两个相邻脉冲之间的时间间隔,用定时器的高频时钟去数这个间隔。在高速时,间隔很短,计数值小,相对误差大;在低速时,间隔很长,计数值大,相对误差小。所以T法适合低速,M法适合高速,两者正好互补。
4. M法、T法和M/T法:三种测速方案的取舍逻辑
4.1 M法:高速段的可靠选择
M法的实现最简单:开一个固定周期的定时器,比如1ms,在中断里读计数器,减去上次的值,得到增量,再除以时间和每转计数,就得到速度。它的优点是实现简单、低速时也能输出(虽然误差大),缺点是低速精度差、有量化误差。
我在高速场合基本都用M法,比如电机额定转速几千转的场合,1ms窗口内脉冲数成百上千,量化误差可以忽略。参数选择上,窗口时间要满足:最低转速下窗口内至少有几十个脉冲。比如最低转速每分钟60转,每转10000计数,每秒10000计数,1ms窗口有10个计数,还是偏少。如果改成10ms窗口,就有100个计数,误差降到1%左右,但速度环的延迟也增加到10ms,可能影响动态响应。
所以M法的窗口时间是一个权衡:窗口越长,精度越高但延迟越大;窗口越短,响应越快但精度越差。我的经验是,速度环的采样周期如果是1ms,测速窗口可以用2到5ms,用滑动平均的方式既保证精度又不引入太大延迟。
4.2 T法:低速段的精度保障
T法的思路完全不同:不去数固定时间内的脉冲,而是测两个脉冲之间的时间。具体做法是,每当编码器产生一个脉冲(或者一个计数),就记录下当前的高频定时器计数值,两次记录之差就是脉冲间隔。速度等于每转计数除以(间隔乘以定时器周期)。
T法在低速时精度极高,因为间隔很长,高频定时器的计数值很大,相对误差很小。但在高速时,间隔很短,计数值可能只有几个,相对误差就大了。而且T法在极低速时,如果电机停转,就没有脉冲,也就没有速度输出,需要额外的超时判断。
我在一个需要极低速平稳运行的转台项目里,用的就是T法。高频定时器跑在10MHz,最低转速每分钟0.1转时,每转10000计数,每秒约16.7个计数,脉冲间隔约60ms,计数值约60万,相对误差可以忽略。但转速升到每分钟1000转时,脉冲间隔只有6微秒,计数值只有60,误差就上来了。所以那个项目里我做了自动切换:低速用T法,高速切回M法。
4.3 M/T法:全速段的折中方案
M/T法试图结合两者的优点:在一个固定的较长时间窗口内,既数编码器脉冲,又数高频时钟脉冲。速度等于编码器脉冲数除以高频时钟数,再乘以时钟频率和每转计数的关系。这样在高速时,编码器脉冲多,精度由脉冲数保证;在低速时,高频时钟数多,精度由时钟数保证。
M/T法的实现比前两者复杂,需要同时开两个计数器,一个数编码器脉冲,一个数高频时钟,而且窗口的起止要和编码器脉冲边沿对齐,否则会有半个脉冲的误差。我在一个宽调速范围的伺服项目里用过M/T法,效果确实好,全速段误差都能控制在1%以内。但代码复杂度也上去了,调试的时候要特别注意两个计数器的同步问题。
| 方法 | 适用转速段 | 精度特点 | 实现复杂度 | 典型窗口 |
|---|---|---|---|---|
| M法 | 高速 | 低速误差大 | 低 | 1到10ms |
| T法 | 低速 | 高速误差大 | 中 | 脉冲间隔 |
| M/T法 | 全速段 | 全段均衡 | 高 | 10到50ms |
4.4 怎么选:一个实用的决策流程
我的经验是,先看你的调速范围。如果最高转速和最低转速之比小于10,用M法就够了,窗口时间按最低转速下至少有50个脉冲来选。如果比值在10到100之间,考虑T法或者M/T法。如果比值超过100,基本只能用M/T法或者做分段切换。
另外还要看动态响应要求。如果速度环带宽要求高,窗口时间就不能太长,这时候M/T法的长窗口可能不合适,反而要用短窗口的M法加滤波。如果对平稳性要求高、对响应要求低,可以用长窗口加滑动平均。
还有一个容易被忽略的点:编码器的线数要和调速范围匹配。如果最低转速很低,又想要好的精度,要么选高线数编码器,要么用T法,要么加机械减速比。我见过一个项目,电机最低转速每分钟1转,编码器只有100线,四倍频后400计数,每秒才6.7个计数,M法根本没法用,最后换了2500线的编码器才解决。
5. 从计数器到速度值的完整代码实现
5.1 硬件定时器编码器模式的配置要点
以常见的STM32为例,定时器的编码器模式可以自动处理A、B两相的正交解码和四倍频计数,CPU只需要读计数器寄存器就行。配置的时候有几个关键点:
第一,定时器的计数模式要设成"编码器模式",并且选择"在TI1和TI2边沿都计数",这就是四倍频。第二,输入捕获滤波器要开,滤波参数根据编码器信号质量来定,一般设到能滤掉几十纳秒的毛刺。第三,自动重装载值设成最大,比如65535,让计数器自由累加,软件里做溢出处理。
// STM32 HAL库配置编码器模式的简化示例 TIM_Encoder_InitTypeDef encoder_config = {0}; encoder_config.EncoderMode = TIM_ENCODERMODE_TI12; // 四倍频 encoder_config.IC1Polarity = TIM_ICPOLARITY_RISING; encoder_config.IC2Polarity = TIM_ICPOLARITY_RISING; encoder_config.IC1Filter = 0x0F; // 滤波参数,根据信号质量调整 encoder_config.IC2Filter = 0x0F; HAL_TIM_Encoder_Init(&htim3, &encoder_config); HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);读计数器的时候要注意,16位定时器会溢出,32位定时器基本不会。如果是16位,需要在软件里维护一个高位累加值,每次读的时候判断当前值是否跨越了0或者65535。
5.2 M法测速的代码骨架
M法的核心就是一个定时中断,在中断里读计数器、算差值、更新速度。下面是一个简化的实现:
#define ENCODER_CPR 10000.0f // 四倍频后每转计数 #define SPEED_WINDOW_S 0.002f // 2ms窗口 volatile int32_t last_count = 0; volatile float speed_rpm = 0.0f; void SpeedTimer_ISR(void) { int32_t current_count = read_encoder_count(); int32_t delta = current_count - last_count; last_count = current_count; // 速度 = 增量 / 每转计数 / 窗口时间 * 60 speed_rpm = (float)delta / ENCODER_CPR / SPEED_WINDOW_S * 60.0f; }这段代码看起来简单,但有几个细节要注意。第一,read_encoder_count要处理溢出,如果是16位定时器,delta的计算要考虑回绕。第二,delta是有符号的,正转正、反转负,这正好符合速度的方向。第三,如果窗口内delta很小,速度值会抖,可以在后面加一个一阶低通滤波。
5.3 T法测速的代码骨架
T法需要捕获编码器脉冲的边沿,用输入捕获功能记录时间戳。下面是一个思路:
volatile uint32_t last_capture = 0; volatile uint32_t capture_delta = 0; volatile float speed_rpm = 0.0f; void EncoderCapture_ISR(uint32_t capture_value) { capture_delta = capture_value - last_capture; last_capture = capture_value; if (capture_delta > 0) { // 速度 = 定时器频率 / 每转计数 / 间隔 * 60 speed_rpm = (float)TIMER_CLK / ENCODER_CPR / capture_delta * 60.0f; } }T法的关键参数是定时器的时钟频率。频率越高,低速时计数值越大,精度越好,但高速时计数值太小会溢出或者误差大。一般选几十MHz,配合预分频器调整。另外,T法在电机停转时没有脉冲,速度值会保持上一次的值,需要加一个超时判断,超过一定时间没有捕获就把速度置零。
5.4 滤波:让速度值能看
不管用哪种方法,原始速度值都会有噪声。我一般用一阶低通滤波,公式是:滤波值等于系数乘以新值加上(1减系数)乘以旧值。系数越小越平滑但延迟越大,系数越大响应越快但噪声越大。
#define ALPHA 0.3f filtered_speed = ALPHA * speed_rpm + (1.0f - ALPHA) * filtered_speed;这个系数怎么选?我的经验是,如果速度环周期是1ms,测速窗口是2ms,滤波系数取0.2到0.5之间比较合适。如果发现速度响应太慢,就加大系数;如果发现速度抖动影响电流环,就减小系数。有时候我会用滑动平均代替一阶滤波,效果更平滑但计算量稍大。
提示:滤波会引入相位延迟,在闭环控制里可能影响稳定性。如果速度环的带宽要求高,滤波系数不能太小,或者改用更高级的滤波方法比如卡尔曼滤波。
6. 那些只有实际调试才会遇到的坑
6.1 计数器溢出与回绕处理
16位定时器最大计数值是65535,正转超过65535会回绕到0,反转低于0会回绕到65535。如果软件里直接做减法,回绕的时候会得到一个巨大的错误值。我见过一个项目,电机正转时速度正常,反转时速度突然变成几万转,就是因为回绕没处理。
正确的做法是用有符号的差值计算,并且利用整数溢出的特性。在C语言里,如果两个无符号数相减,结果会自动按模运算,只要差值在半个计数范围以内,结果就是正确的。比如65535减0等于65535,但按有符号解释就是负1,正好是反转一个计数。所以只要保证两次采样之间的增量不超过32767,直接用有符号减法就是安全的。
int16_t delta = (int16_t)(current_count - last_count);这一行代码就解决了回绕问题,前提是增量不超过正负32767。如果窗口内增量可能超过这个值,就要用32位变量并且手动处理溢出。
6.2 编码器信号抖动导致的计数错误
编码器信号抖动是另一个常见问题。表现是电机静止时计数器也在缓慢变化,或者低速时速度读数有尖峰。原因可能是信号线太长、没有屏蔽、电源噪声大,或者编码器本身质量不好。
我在一个项目里遇到过电机静止时计数器每秒跳几十个计数的情况,查了半天发现是编码器电缆和电机动力线捆在一起走线,动力线的PWM噪声耦合到了编码器信号上。后来把编码器线单独走,并且加了屏蔽层接地,问题就解决了。
软件上也能做一些补救:定时器的输入滤波参数调大一些,或者在速度计算后加一个死区,增量绝对值小于某个阈值就当作零。但这些都是治标,根本还是要从布线和屏蔽上解决。
6.3 低速时的速度归零与震荡
前面提到过,M法在极低速时窗口内可能没有脉冲,速度读数为零。这时候如果速度环在运行,控制器会以为电机没转,加大输出,电机突然加速,然后速度读数又有了,控制器又减小输出,造成震荡。
解决这个问题有几个思路。一是改用T法,低速时也能测出速度。二是设置一个最小速度阈值,低于这个阈值时速度环开环或者用电流环维持。三是用位置环代替速度环,在极低速时直接控制位置增量。我在一个云台项目里用的是第二种,低速时切到开环,靠电流控制维持平稳转动,效果不错。
6.4 多圈计数与绝对位置
增量式编码器只能测相对位置,上电后需要找Z相归零。但如果系统需要多圈绝对位置,比如升降机构要知道当前在第几层,就需要在软件里维护一个多圈计数器,并且掉电保存。
我的做法是:每次Z相中断时,把当前的单圈计数清零,同时多圈计数加一或者减一(根据方向)。多圈计数存在EEPROM或者Flash里,掉电前保存,上电后恢复。但要注意,如果掉电时电机还在转动,恢复后的位置可能不准,需要重新找Z相校准。
6.5 采样周期与速度环的匹配
测速窗口和速度环周期不匹配也会出问题。如果速度环周期是1ms,测速窗口是10ms,那速度环每次用的都是10ms前的平均速度,相位滞后很大,容易震荡。反过来,如果测速窗口太短,速度噪声大,速度环也会震荡。
我的经验是,测速窗口和速度环周期最好一致,或者测速窗口是速度环周期的整数倍,并且用滑动平均的方式让每次速度环都能拿到最新的平均速度。比如速度环1ms,测速窗口5ms,可以用一个5点的滑动平均,每1ms更新一次,这样既有5ms的平滑效果,又有1ms的更新率。
7. 从脉冲到速度,本质是一次信息重建
回过头看,编码器读到的原始信息只有脉冲和方向,速度是我们通过差分运算重建出来的。这个重建过程的质量,取决于你对采样定理、量化误差、噪声和延迟的理解。M法、T法、M/T法不是三个孤立的技巧,而是同一个问题在不同转速段的不同解法。
我在实际项目里最深的体会是:不要指望一种方法包打天下。宽调速范围的项目,要么做分段切换,要么用M/T法,要么在机械上想办法(比如加减速比让编码器转速不要太低)。另外,编码器的选型和安装往往比算法更重要,线数够不够、信号干不干净、Z相可不可靠,这些底层问题不解决,再好的算法也白搭。
最后分享一个调试小技巧:如果你怀疑速度读数有问题,先把原始计数器的值通过串口或者DAC输出出来,用示波器或者上位机看波形。很多时候,你看一眼计数器的变化曲线,就能判断是编码器信号问题、计数溢出问题还是算法问题。这比盯着速度值猜要高效得多。