共计 2499 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
在机器人、CNC 等需要精确位置反馈的项目中,机械编码器是常用的传感器。但实际使用时会遇到几个典型问题:

- 信号抖动:机械触点振动导致单个物理动作产生多个电气信号(如原本旋转 1 格却触发 3 次计数)
- 方向误判:A/ B 相信号时序处理不当导致正转被记录为反转
- 高速漏脉冲:在高速旋转时因 MCU 处理速度不足丢失脉冲
这些问题的直接表现就是测量结果与物理运动不匹配——你可能遇到过电机明明转了 10 圈,计数器却显示 8 圈或 15 圈的尴尬情况。
技术方案对比
常见解决方案有两种:
- 查询法(Polling):在主循环中不断读取引脚状态
- 优点:实现简单,不占用中断资源
-
缺点:响应延迟高,高速时必丢脉冲
-
中断法(Interrupt):通过硬件中断即时响应信号变化
- 优点:μs 级响应,适合高速场景
- 缺点:需要合理设计 ISR(中断服务例程)
经过实测,在 100RPM 以上转速时,查询法的丢脉冲率超过 30%,而中断法可以控制在 1% 以内。因此我们选择中断方案。
核心实现
硬件中断配置
使用 Arduino 的 attachInterrupt() 函数,将编码器的 A 相连接到支持中断的引脚(UNO 的 2 / 3 引脚):
#define ENCODER_A 2
#define ENCODER_B 3
void setup() {pinMode(ENCODER_A, INPUT_PULLUP);
pinMode(ENCODER_B, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_A), handleEncoder, CHANGE);
}
去抖动算法
机械编码器信号抖动通常在 5 -20ms 之间,我们通过时间阈值过滤:
#define DEBOUNCE_TIME 10 // 单位 ms
volatile long lastInterruptTime = 0;
void handleEncoder() {if (millis() - lastInterruptTime < DEBOUNCE_TIME)
return;
lastInterruptTime = millis();
// 真正处理编码器信号的代码...
}
状态机处理
正交编码器的 A / B 相有 4 种状态组合,通过状态机准确判断方向:
状态转换图:当前状态 | A 相边沿 | B 相电平 | 动作 | 新状态
---------|---------|---------|-----------|-------
S0 | 上升沿 | LOW | 计数 +1 | S1
S0 | 上升沿 | HIGH | 计数 -1 | S3
S1 | 下降沿 | HIGH | 计数 +1 | S2
...(完整状态转换表)
完整代码实现
基于 PlatformIO 的项目结构:
#include <Arduino.h>
// 配置区
#define ENCODER_A 2
#define ENCODER_B 3
#define DEBOUNCE_TIME 10
// 全局变量
volatile long encoderCount = 0;
volatile uint8_t state = 0;
void IRAM_ATTR handleEncoder() {
static unsigned long lastTime = 0;
unsigned long now = millis();
// 消抖处理
if (now - lastTime < DEBOUNCE_TIME) return;
lastTime = now;
// 读取当前引脚状态
bool a = digitalRead(ENCODER_A);
bool b = digitalRead(ENCODER_B);
// 状态机实现
uint8_t newState = (a << 1) | b;
if (state == 0x00 && newState == 0x02) encoderCount++;
else if (state == 0x00 && newState == 0x01) encoderCount--;
// 其他状态转换...
state = newState;
}
void setup() {Serial.begin(115200);
pinMode(ENCODER_A, INPUT_PULLUP);
pinMode(ENCODER_B, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_A), handleEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_B), handleEncoder, CHANGE);
}
void loop() {
static long lastCount = 0;
if (encoderCount != lastCount) {Serial.print("Count:");
Serial.println(encoderCount);
lastCount = encoderCount;
}
}
性能优化
不同 MCU 的中断响应能力差异显著:
- Arduino UNO(ATmega328P):
- 中断响应时间约 2.5μs
- 最大支持 4kHz 的脉冲频率
- ESP32:
- 中断响应时间 <1μs
- 支持高达 10kHz 的脉冲
- 有专用脉冲计数外设(PCNT)
在高速场景建议使用 ESP32,并通过 IRAM_ATTR 宏将 ISR 放入内存加速访问。
常见问题解决
- 中断中调用 delay():
-
会导致系统死锁,改用状态机或软件定时器
-
计数器溢出:
-
对 volatile long 类型约 21 亿次计数,超限时处理:
if (encoderCount > LONG_MAX - 1000) {encoderCount = 0; // 或触发预警} -
编码器类型适配:
- 正交编码器:需处理 A / B 相 4 种状态
- 格雷码编码器:相邻状态只有 1bit 变化
扩展应用
- 多编码器系统:
- 使用中断引脚扩展芯片(如 PCA9547)
-
为每个编码器分配独立计数器
-
闭环控制:
void loop() { float target = 1000.0; float error = target - encoderCount; motorPower = Kp * error; // 简单 P 控制 }
通过这套方案,我在一个 CNC 项目中实现了±1 脉冲的精度(1000RPM 下测试)。关键点是:选择合适的中断引脚、精心调校去抖时间、避免 ISR 中的耗时操作。希望这些实践经验对你有帮助!
正文完
