
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的定时器编码器模式做四倍频满速时中断频率到了2MHzCPU基本被吃满后来改成只在需要读位置的时候去读计数器寄存器才把负载降下来。这里有个容易踩的坑四倍频计数时如果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到10msT法低速高速误差大中脉冲间隔M/T法全速段全段均衡高10到50ms4.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输出出来用示波器或者上位机看波形。很多时候你看一眼计数器的变化曲线就能判断是编码器信号问题、计数溢出问题还是算法问题。这比盯着速度值猜要高效得多。