上一篇文章写了怎么让编码电机走直线、转直角。但有读者问了一个很实在的问题:
“你怎么知道电机是正转还是反转?转速到底怎么测出来的?”
这其实是两个问题。先说编码器怎么判方向,再说怎么算转速。两个问题都回到同一个信号上去——编码器输出的那两路方波。
正交编码器:两路信号,一个答案
带编码器的电机屁股后面通常甩出四根线:电机电源正负,加上两根信号线,分别叫 A 相和 B 相。
这两根线输出的都是方波脉冲。电机转一圈,A 和 B 各输出固定数量的脉冲(常见 12、20、48 个)。关键不是脉冲数量,而是它们谁先跳变。

正转:A 领先 B
电机正转时,A 相的上跳沿永远比 B 相早到 90°(电气角度)。你同时盯着两个信号看,A 跳上去了,B 还是低电平——这就表明在正转。
反转:B 领先 A
反转时反过来。B 先跳上去,A 才跟着跳。读到的时序关系完全镜像。
这个相位关系是编码器最核心的信息。哪怕没有其他传感器,只看这两根线的跳变顺序,就能知道电机往哪转。
正交解码:四倍频与方向判断
解码的方式不唯一。你可以在每个上升沿读一次,这叫单倍频;也可以在上升沿和下降沿都读,翻倍成两倍频;更激进的做法是 A、B 两相的四个边沿都触发中断,四倍频。
下面的代码就是四倍频解码——每个边沿进一次中断,读两相电平,比较前后状态判方向:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// motor.js 中的四倍频正交解码
setWatch(function(e) {
var a = digitalRead(motorPins.left.encA);
var b = digitalRead(motorPins.left.encB);
if (a !== encoder.left.lastA) {
if (a === b) encoder.left.count++; // 正转
else encoder.left.count--; // 反转
}
encoder.left.lastA = a;
encoder.left.lastB = b;
}, motorPins.left.encA, {repeat: true, edge: 'both'});
setWatch(function(e) {
var a = digitalRead(motorPins.left.encA);
var b = digitalRead(motorPins.left.encB);
if (b !== encoder.left.lastB) {
if (a === b) encoder.left.count--; // 反转
else encoder.left.count++; // 正转
}
encoder.left.lastA = a;
encoder.left.lastB = b;
}, motorPins.left.encB, {repeat: true, edge: 'both'});
为什么要在 A 和 B 上都设中断? 因为只监听一路信号,你只能知道脉冲数,不知道方向。两路都监听,每次跳变都检查另一相的电平——”A 上升时 B 是高还是低?”——答案就是方向。
编码器计数 encoder.left.count 就是一个有符号整数。正转累加,反转递减,始终反映旋转的净位移。
一个实战细节:
input_pullup比input重要得多。电机刚启动时电流大、干扰多,悬空的编码器引脚会疯狂误触发。内部上拉把电平钉住,误计数从每分钟几百次降到几乎为零。motor.js第一版就是踩了这个坑,第二版才老老实实加上input_pullup。
测速:从脉冲到 RPM 的两种路径
有了计数,下一步是算速度。没有速度反馈,小车跑不直、机器人走不准。
方法一:固定时间窗口计数法
最简单。固定一个时间窗口(通常 1 秒),数这个窗口内来了多少个脉冲,换算成转速。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// fans_1.js 中的秒级计数
var counter = 0;
function onChanged(e) {
counter++;
digitalWrite(C13, e.state);
}
function onSecond(e) {
// counter × (60s / 脉冲数每转) = RPM
console.log(counter * 60 / 2);
counter = 0;
}
setWatch(onChanged, SENSE, { edge: "both", repeat: true });
setInterval(onSecond, 1000);
PC 风扇的测速线每转输出 2 个脉冲,所以乘 60 除以 2。
优点是简单,缺点是延迟。你永远要等整秒结束才知道这秒的平均速度。转速突然飙高,你要傻等一整秒才看到 —— 对实时控制来说太慢了。
方法二:脉冲间隔法
不等了 —— 直接在脉冲到来的时刻算瞬时速度。
1
2
3
4
5
6
7
8
9
10
11
12
13
// fans_1.js 中的脉冲间隔测速
var lastPulseTime;
function onChanged(e) {
if (e.state) { // 只在上升沿算
var timeDiff = e.time - lastPulseTime; // 相邻脉冲间隔
lastPulseTime = e.time;
rpm = 60 / timeDiff; // 瞬时 RPM
digitalWrite(A9, rpm < 900);
}
counter++;
console.log(rpm);
}
核心公式一行:
1
RPM = 60 / Δt
其中 Δt 是两个相邻脉冲的时间间隔(秒)。一个脉冲来了,记下时间戳;下一个脉冲来了,减一下,60 / 差值 就是瞬时转速。
两种方法的延迟对比:
| 方法 | 延迟 | 适用场景 |
|---|---|---|
| 计数法 | 固定 1 秒 | 低速监控、日志记录 |
| 间隔法 | ≤1 个脉冲周期 | 实时控制、PID 反馈 |
对 PC 风扇(720 RPM、约 12 Hz 信号),间隔法的延迟最多 83 ms,比计数法的 1 秒快了 12 倍。电机转速越高,延迟越短 —— 高速时需要的正是更快的反馈。
给编码电机做一次完整测速
把方向判断和速度测量结合起来,就是完整的编码电机测速驱动:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// motor2.js - 编码器测速示例
var encoder = {
left: {
count: 0, lastA: 0, lastB: 0,
lastTime: 0, speed: 0, lastCount: 0
}
};
// 正交解码(四倍频)
function leftEncoderHandler() {
var a = digitalRead(pins.left.encA);
var b = digitalRead(pins.left.encB);
if (a !== encoder.left.lastA || b !== encoder.left.lastB) {
if (a === b) encoder.left.count++; // 方向判定
else encoder.left.count--;
encoder.left.lastA = a;
encoder.left.lastB = b;
}
}
setWatch(leftEncoderHandler, pins.left.encA, {repeat:true, edge:'both'});
setWatch(leftEncoderHandler, pins.left.encB, {repeat:true, edge:'both'});
// 定期计算速度(脉冲/秒)
setInterval(function() {
var now = getTime();
var dt = now - encoder.left.lastTime;
if (dt > 0) {
encoder.left.speed = (encoder.left.count - encoder.left.lastCount) / dt;
encoder.left.lastCount = encoder.left.count;
encoder.left.lastTime = now;
}
}, 100);
这里用了计数法的变体而不是脉冲间隔法:每 100 ms 读一次累计计数的差值,除以时间窗口得到平均速度。比 1 秒窗口更新快 10 倍,比纯脉冲间隔法稳定(不会因为个别脉冲抖动而跳变)。
实际应用中,我一般这样选:
- PID 闭环控制 → 100 ms 滑动窗口,兼顾更新率和稳定性
- 状态监控、报警 → 脉冲间隔法,响应最快
- 里程累计 → 原始累计计数,不需要换算成速度
怎样验证测速准不准?
最直观的方法:把测出来的转速跟理论值对比。
给定一个 PWM 占空比(比如 50%),用示波器看编码器波形,算算频率。Espruino 串口打印的 RPM 换算回频率(RPM × 脉冲每圈 / 60),应该跟示波器读数吻合。
没有示波器也行。给电机一个固定 PWM,console.log 连续打 20 秒转速读数,看方差。电机机械惯性大,转速本身不该剧烈抖动——如果读数在 ±30% 范围内乱跳,大概率是编码器误触发,检查上拉电阻和电源滤波。
另一个简单办法:让电机空载匀速转,记下 10 秒内进中断的总次数,除以 10 得到每秒脉冲数,再换算成 RPM。跟串口打印的数值对比,误差应该在 1-2% 以内。
总结:构建测速环节的技术要点
编码电机测速这件事情,拆开来看就三层:
- 信号层 — 正交编码器的 A/B 两相,相位差判方向,边沿跳变数脉冲。用
input_pullup抗干扰。 - 解码层 — 每个边沿都进中断,读两相电平比较。四倍频解码每转能拿到 4xPPR 个脉冲,低速下精度更高。
- 转速层 — 计数法简单但有固定延迟,间隔法响应快但容易受单脉冲抖动影响。实际工程中 100 ms 滑动窗口是很好的折中。
这三个层面跟 DRV8833 的驱动力控制是正交的。驱动只管给电,编码器只管反馈——两者各司其职,PID 在中间把它们串起来。
文件
fans_1.js— 脉冲间隔法测速原型(PC 风扇场景)motor.js— 基础版编码器驱动(四倍频正交解码 + DRV8833 方向控制)motor2.js— 改进版编码器驱动(含速度计算、里程换算、PID 闭环)images/encoder-forward.svg— 正转编码器 A/B 相波形图images/encoder-reverse.svg— 反转编码器 A/B 相波形图images/pulse-interval-rpm.svg— 脉冲间隔测速原理图
从 PC 风扇到编码电机,测速的本质就一句话:数脉冲,量间隔。 前者告诉你转了多少,后者告诉你转得多快。两样凑齐了,电机就有了心跳。