ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

玩转EC11编码器:AB相相位差与双平台读取实战

玩转EC11编码器:AB相相位差与双平台读取实战 先别急着看代码玩转EC11编码器搞清楚AB相那点“相位差”的事儿比什么都重要。无论是在Arduino上写个旋转菜单还是在STM32上搞个精准的电机调速旋钮EC11都是最常用、最皮实、也最容易被忽视细节的器件。很多人直接把A、B两脚接上中断引脚转起来能计数就以为完事了结果一遇到抖动、反转或者高速旋转就丢步、乱跳最后只能怀疑人生。这篇东西我不打算只给你两个能跑的例程就交差那没意思。我会从示波器上看到的实际波形出发一步一步带你把EC11的AB相相位关系、内部结构、消抖逻辑、双平台的不同读取策略以及高频脉冲丢失的坑都趟一遍。看完之后你就能根据自己项目的实际需求在Arduino和STM32之间随意迁移方案而不是只会复制粘贴。1. 内容整体设计与思路拆解1.1 为什么要盯着波形图和相位差看而不是只查数据手册EC11编码器本质上是一个机械式的增量旋转编码器内部的核心是两个触点对应输出A相和B相。当你旋转旋钮时这两个触点会依次接通和断开产生两个方波信号。关键在于这两个方波不是同步的而是存在一个固定的时间差通常我们把A相和B相之间的这个错开关系叫“正交信号”标准情况下相位差是90度。这个90度相位差有什么用呢直接决定了两个事情第一决定旋转方向第二决定你能否在高转速下不丢脉冲。用大白话来说A相和B相就像是两个接力赛运动员A跑在前头B跟在后面如果A先到终点先跳变那就是正转如果B先到终点先跳变那就是反转。这个先后顺序就是相位差最直观的体现。但是EC11毕竟是个机械器件它内部不是光栅而是金属弹片和固定触点。机械接触就带有天生的“抖动”Bounce这在示波器上看得清清楚楚原本一个干净利落的上升沿实际是连续好几组高低电平的毛刺。如果你把代码写成“检测到A相上升沿加一”那结果就是转一格计数器能加上好几十。所以真正的编码器读取方案从硬件到软件都要围绕“如何可靠地识别出90度相位差中那个有效的跳变沿”来设计。我建议你先别急着搭电路而是先用示波器或者逻辑分析仪去看一眼EC11转起来时的真实波形。我见过太多人代码写得挺复杂结果硬件上A、B两相压根没接对导致数据完全对不上最后还以为是编码器坏了。所以把原理和波形看明白了后面所有问题都迎刃而解。1.2 方案选型对比Arduino中断法、轮询法、STM32定时器编码器模式目标明确了之后选方案就顺理成章。在Arduino平台上最常见的是利用attachInterrupt()中断函数在A相或B相的跳变沿触发回调然后在回调里读取另一相的电平来判断方向。这个方案的优势是实时性强代码简单适合转速不太高的交互场景比如音量旋钮、菜单选择。缺点是单片机在中断里花费的时间不能太长如果主程序里还有大量耗时操作比如驱动OLED屏幕、跑算法中断响应延迟就容易导致脉冲丢失。另一种Arduino方案是轮询也就是在loop()里不断读取A、B两相电平记录上一次的状态通过状态变化判断旋转。这种方法省去了中断资源但实时性受主循环周期影响如果你主循环里有delay(100)那这100毫秒里转动的脉冲全都会被漏掉。实测下来轮询法只适合对实时性完全没有要求的旋钮场景。而STM32平台则完全不同。STM32的定时器硬件自带编码器模式可以直接把TIMx的CH1和CH2引脚配置成编码器输入硬件自己根据A、B相的相位关系进行加减计数。这种方式不需要占用CPU任何中断负载计数频率可以做到很高几兆赫兹级别而且带有多级滤波数字滤波器功能非常适合电机测速、高性能旋钮等需要精准捕捉每一个脉冲的场合。从我的个人经验来看如果只是做个人项目或者交互原型Arduino中断法完全够用。但如果涉及到电机闭环控制、需要精确到每一圈的位置反馈那STM32的定时器编码器模式才是正解。两者本质上都在做同一件事识别AB相的相位变化只是实现层级不一样。后面的内容我会把两种方案都拆开揉碎讲清楚。2. 核心细节解析EC11内部原理与AB相波形深度拆解2.1 从内部结构看输出信号推挽输出、上拉电阻与机械抖动EC11编码器一般有5个引脚除了A、B两相和公共端C通常接地之外还有两个引脚是按压开关的用于旋钮按键功能。这里我们只关注旋转部分。内部结构可以简化理解为一个带有金属触点的转盘和两个固定的簧片触点。每转动一个角度转盘上的导电区域会依次和A触点、B触点接触或断开从而在输出端产生高低电平。这里有一个关键点EC11内部输出并非标准的数字推挽输出而是一个机械开关结构。也就是说A、B引脚内部相当于一个开关一端连接到公共端地或者VCC取决于具体型号和接法另一端悬空。所以在外部电路上A、B引脚必须接上拉电阻通常接10kΩ到VCC或者下拉电阻连接到GND否则引脚浮空读到的电平是随机值。我习惯把公共端C接地A、B分别通过10kΩ电阻上拉到VCC然后这样读取当触点闭合时该相输出为低电平触点断开时上拉电阻把电平拉到高。这样波形看起来是反的初始空闲为高转动时产生向下的脉冲。也有些人把公共端C接正极A、B下拉到地波形就反过来初始空闲为低转动时产生向上的脉冲。这两种接法代码逻辑正好相反但原理完全一样。你在参考别人代码时一定要先搞清楚对方的硬件接法否则直接抄代码大概率会得到相反的方向判断。机械抖动的产生也源于这个结构。金属弹片在碰撞到固定触点的一瞬间不会一步到位地牢固接触而是会发生极短时间的弹跳导致电平在高低之间快速震荡几次这个过程通常持续几毫秒。如果在代码里不处理这个抖动每一个“有效的接触沿”都会被误判成多个脉冲。后面我会讲到在中断里加判断逻辑就是为了规避这个坑。2.2 波形图现场还原示波器下的正交信号与方向判定逻辑用逻辑分析仪抓取EC11波形是我每次调试编码器项目必做的第一步。抓取时把A相接在通道0B相接在通道1公共端接地然后缓缓转动旋钮你会看到类似这样的波形通道0A相先产生一个下降沿紧接着没多长时间通道1B相也产生一个下降沿。等到继续转下一个格时同样的过程再次重复但A相和B相的下降沿前后关系跟上一格保持一致。这个“A先动B后动”还是“B先动A后动”的顺序就是判断方向的核心依据。假设我定义A相接中断引脚在A相的下降沿触发中断然后在中断服务函数里读取B相的电平。如果检测到A相下降沿时B相恰好处于低电平说明B相已经先一步到位了这里需要仔细分析那就表示当前旋转方向为反向反之如果B相还是高电平说明B相还没动A相先动那就是正向。不对这里我再说得更严谨点因为很多人在这里绕晕。我们重新捋一遍逻辑。以A相下降沿作为“采样点”也就是中断触发点此时去读取B相的状态。如果旋钮是顺时针转A相下降沿到来时B相经过上拉大概率处于高电平。然后紧接着B相也下降完成一个完整的状态变化。相反如果逆时针转B相会比A相提前下降那么当A相下降沿触发中断时B相已经是低电平了。所以只要在A相中断里判断B相此时是高还是低就能反推出方向。这个逻辑可以表述为“A下降沿采样B相电平B高为正转B低为反转”。注意这只适用于A相接中断、公共端接地、上拉到VCC这种特定接法。如果你的接法是下拉到地那么高低电平逻辑完全反过来。如果手头没有逻辑分析仪用示波器的双通道功能也能看得很清楚但需要注意触发设置建议把触发模式设置为“下降沿触发”并挂在A相上然后转动旋钮这样也能稳定捕捉到A、B的相对时序。2.3 增量编码器的本质相对位置与圈数的记录原理EC11不是绝对位置编码器它不会告诉你当前旋钮处于哪个绝对角度它只会告诉你“我动了多少步”以及“我往哪个方向动了”。这在交互上完全够用因为绝大多数场景下我们只关心相对变化量比如音量1、音量-1菜单下一项、上一项。而在电机测速场景中我们通常需要知道的是单位时间内转了多少脉冲再换算成速度。这里就引出一个重要概念分辨率。EC11通常有“20脉冲/圈”或“24脉冲/圈”的规格但如果你用A、B相的双边沿采样也就是同时检测A、B的上升沿和下降沿那么每个物理脉冲能被识别出4种状态变化也就是常说的“四倍频”一圈的计数分辨率会提升到80或96。四倍频技术在编码器位置检测中应用广泛用“阅读”AB相每个跳变沿的方式用同样的机械成本换取4倍的反馈精度。但在Arduino上做四倍频有个风险中断频率翻倍。假设你每分钟转300圈20脉冲/圈四倍频后每秒就有400个中断Arduino Uno16MHz尚能应付。但如果转得更快或者主循环里有大量串口打印、屏幕刷新任务中断就会成为瓶颈。STM32的硬件编码器模式自带四倍频计数能力完全不需要软件操心这也是我极力推荐STM32做高性能编码器读取的原因。3. 实操核心环节双平台示例的完整解析3.1 硬件连接与电路搭建细节Arduino和STM32通用接线无论你用哪个平台硬件连接方式几乎一致。以公共端接地、A/B相上拉为正逻辑接法举例部件EC11引脚连接到Arduino / STM32A相数字IO引脚如Arduino D2 / STM32 PA0同上B相数字IO引脚如Arduino D3 / STM32 PA1同上公共端CGND同上上拉电阻A相10kΩ → VCC(3.3V或5V)B相同样按键开关两个开关脚串入按键电路本文略需要特别提醒的是STM32的引脚电压容忍度问题。EC11本身是无源器件不牵扯电平兼容问题但你的上拉电阻接的电压决定了输出电平。如果STM32工作在3.3V那上拉就接3.3V千万别图省事接到5V上否则时间一长可能损伤引脚。Arduino Uno用5V上拉也完全没问题大多数STM32F103系列虽然标注“5V容忍”但在这类场景里没必要冒险。另外如果是长线连接比如旋钮面板和中控板之间距离超过20厘米建议在靠近MCU引脚的地方并联一个0.1μF的瓷片电容到地A、B各一个可以有效滤除线束感应的高频噪声。但这个电容也不能太大太大会把信号的边沿变缓导致STM32误判边沿。实测0.1μF在这个距离下表现很好。3.2 Arduino平台中断方式与简易状态机实现旋转计数器在Arduino上实现EC11读取我推荐用中断方式配合状态机处理抖动。先说中断方式核心思路是只在A相的跳变沿进入中断然后在中断里读取B相电平判断方向。这种做法能有效避免B相抖动带来的误触发因为你并不是在每个跳变沿都计数而是在关键沿A相跳变才执行一次快速判断。#define ENC_A 2 #define ENC_B 3 volatile long encoderPos 0; void setup() { pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ENC_A), readEncoder, FALLING); Serial.begin(115200); } void loop() { Serial.println(encoderPos); delay(50); } void readEncoder() { if (digitalRead(ENC_B) HIGH) { encoderPos; } else { encoderPos--; } }这段代码简洁直观但它有一个隐患它只在A相下降沿触发理论上每个脉冲只计一次数也就是“单倍频”。此外它没有处理抖动如果A相下降沿出现机械抖动readEncoder可能会被连续触发多次导致单次旋转累加出错误数字。所以这个代码只是演示方向判断逻辑真要在项目里用我建议改成下面这种更稳妥的“双边沿状态机”方式。所谓“状态机”方式就是同时监测A、B两相的全部跳变沿每检测到一个沿就根据上一个状态和当前状态识别方向。这种方式的抗抖动性更好而且天生支持四倍频。在Arduino上由于需要占用两个外部中断引脚而Uno的D2、D3正好都能外部中断所以实现起来比较方便。#define ENC_A 2 #define ENC_B 3 volatile uint8_t lastState 0; volatile long encoderPos 0; void setup() { pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ENC_A), onEncoderChange, CHANGE); attachInterrupt(digitalPinToInterrupt(ENC_B), onEncoderChange, CHANGE); lastState (digitalRead(ENC_A) 1) | digitalRead(ENC_B); Serial.begin(115200); } void loop() { Serial.println(encoderPos); delay(30); } void onEncoderChange() { uint8_t state (digitalRead(ENC_A) 1) | digitalRead(ENC_B); uint8_t transition (lastState 2) | state; switch (transition) { case 0b0001: case 0b0111: case 0b1110: case 0b1000: encoderPos; break; case 0b0010: case 0b0100: case 0b1011: case 0b1101: encoderPos--; break; default: break; } lastState state; }这段代码的思路是将A、B的电平编码成一个2位的二进制数上一次状态左移两位加上当前状态组成一个4位的转移码通过查表判断是正转还是反转。状态机方式的好处在于只要状态转移正确就走相应的加减路径遇到干扰跳变时如果转移码不在预设集合里就会被丢弃不会造成错误计数。实测这个代码在机械旋钮上很稳唯一的代价是中断次数增加了3到4倍但Arduino在这种低速交互场景下完全带得动。3.3 STM32平台定时器编码器模式的硬件级解耦方案STM32的编码器接口模式是硬件外设提供的功能把“看波形、判方向、计数”这些苦力活全部收敛进定时器模块。以STM32F103系列为例一般选用TIM1、TIM2、TIM3、TIM4等。以TIM3为例把PA6配置为TIM3_CH1对应A相PA7配置为TIM3_CH2对应B相然后初始化时把TIM3的从模式设为编码器模式硬件就会自动根据TI1、TI2的相位关系来加减内部计数器CNT。编码器模式的核心配置思路是选择计数边沿组合。你可以配置为只在TI1边沿计数单倍频、只在TI2边沿计数单倍频、或者在TI1和TI2双边沿都计数四倍频。四倍频模式下计数频率是物理脉冲数的4倍位置分辨率最高。TIM3的计数方向向上/向下由硬件根据A、B相对关系自动决定并通过TIM_GetCounter()读出来的值即可获取当前位置。#include stm32f10x.h void Encoder_Init_TIM3(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0C; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0C; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM3, 0); TIM_Cmd(TIM3, ENABLE); } int32_t Encoder_GetCount(void) { return (int32_t)TIM_GetCounter(TIM3); }关于TIM_ICFilter的配置这里需要多说一句。这个参数就是硬件数字滤波器能过滤掉小于设定宽度的毛刺脉冲。值越大滤波时间越长抗抖动越好但也会把高速旋转时真正窄的脉冲吞掉。对于EC11这种机械编码器转速一般不会太高0x0C左右是比较稳妥的取值。如果旋钮转得非常快发现计数不增反减可以先考虑降低滤波强度。使用上只需要周期性地调用Encoder_GetCount()和上一次读到的值求差就能得到这段时间的增量。如果做电机速度环可以在定时器中断比如1kHz里读取这个差值乘以一个系数就是速度值。硬件自动计数的好处是哪怕主程序再忙也不会丢脉冲除非计数器溢出把TIM_Period设为0xFFFF就是为了让溢出范围足够大一般很难转满65535个脉冲。3.4 移植与适配不同MCU平台下的代码调整要点如果你已经掌握了上面两个平台的核心逻辑那把它移植到ESP32、树莓派Pico或者其他MCU上就很简单了。需要注意的核心点只有三个第一确认你的引脚是否支持外部中断以及中断触发模式是上升沿、下降沿还是任意沿变化第二检查GPIO内部是否有上拉/下拉能力Arduino的INPUT_PULLUP很方便但ESP32的某些引脚也需要外置上拉才能保证稳定第三如果是高级定时器比如STM32F4系列寄存器名和库函数会有细微差别需要对照参考手册调整。以ESP32为例它的GPIO基本都支持外部中断但如果你要接多个编码器需要注意ESP32的PCNT脉冲计数器外设这个硬件模块和STM32的定时器编码器模式非常相似也能自动根据AB相计数且只在指定脉冲模式时触发中断。这些平台迁移的核心永远是先看硬件外设有没有“编码器模式”这个专用功能如果有优先用它如果没有再上中断状态机。我个人踩过的一个坑是把Arduino代码往STM32上硬搬的时候忽略了上拉方式的不一致。Arduino的pinMode(pin, INPUT_PULLUP)很方便但STM32标准库初始化GPIO时如果忘了写GPIO_Mode_IPU上拉输入而是用了GPIO_Mode_IN_FLOATING浮空输入在电机附近强干扰环境下B相电平就容易被拉偏导致偶尔反转一个脉冲。这问题在示波器上几乎看不出来但累计误差积少成多非常折磨人。所以无论什么平台编码器引脚能开内部上拉就开内部上拉不行就在外部并联10kΩ电阻千万不要用浮空输入。4. 常见问题与排查技巧实录4.1 转一格计数器却跳好几个数问题出在哪这是EC11调试里最高频的问题。遇到这种症状第一反应肯定是抖动。如果你用的是Arduino中断单倍频方案建议改成状态机方式把无效的状态转移直接滤除掉。如果你已经用了状态机依然跳数那就需要拿示波器去看波形了。我遇到过一种特殊情况A、B两相波形看起来挺干净但机械手感就是“咔嚓”两下逻辑分析仪放大之后发现在一个物理脉冲内A相抖动时B相也跟着抖动导致状态转移出现了合法的正转转移码也出现了合法的反转转移码两个相互抵消的结果是计数器没变但你期待的是“转一格只加一”。这种情况下单纯靠软件状态机已经解决不了问题了因为抖动产生的脉冲在状态机眼里是“合法”的。思路要转回到硬件上给A、B引脚都加一个RC低通滤波器。常见取值是电阻1kΩ串联到引脚然后在引脚对地并联一个0.1μF电容。这样就把机械抖动产生的高频毛刺给削平了。加了RC之后信号边沿会变缓但MCU的GPIO检测边沿通常没问题。另外还有一个很隐蔽的问题代码里如果是用digitalRead()读取B相然后又马上做方向判断这中间的微秒级延迟在快速转动时可能导致采样到了错误电平。更稳妥的办法是在进入中断后加几个微秒的延时或者用__NOP()空指令再读B相确保引脚电平已经稳定。STM32编码器模式不存在这个问题因为硬件采样的速度远高于机械信号的变化速度。4.2 四倍频后计数方向反了如何快速修正方向反了是最容易解决但也最容易让人困惑的问题因为每个人接线的习惯不一样。如果你确定AB相接线已经反复核对无误但计数方向还是相反最快速的修正方法是在代码层面交换方向判断。在Arduino状态机代码里把encoderPos和encoderPos--整个对调即可。在STM32的TIM_EncoderInterfaceConfig()函数里把两个通道的输入极性从TIM_ICPolarity_Rising全部改成TIM_ICPolarity_Falling方向就会翻转。也可以不动代码直接在硬件上交换A、B两根线效果等价。但注意如果你在STM32中用四倍频的极性配置来做方向翻转要意识到它同时也会改变计数沿的定义也就是可能影响滤波和计数的物理意义。在实际项目中我更推荐在软件应用层做一个“方向系数”变量比如读取到的计数值乘以1或-1这样后续接线变化、结构换向时只需要改一个常量即可而不用去改动底层的外设配置。4.3 转速稍高就丢脉冲高速读取时怎么优化EC11毕竟是机械编码器额定转速和响应速度是有限的。如果你发现转速超过某个阈值后Arduino中断方式开始丢数不要急着怪编码器先看看你的中断服务函数是不是拖得太长了。中断里的digitalRead()、Serial.print()这类操作都是相当耗时的在中断服务函数里绝对不要做打印、延时等操作正确做法是把计数值存到volatile变量里在loop()中统一处理。如果确认中断函数已经足够精简但丢数依旧存在那就要考虑换用带硬件编码器接口的MCU了比如STM32让硬件自己处理高频脉冲CPU只负责定期读取计数值。用STM32做电机测速时我习惯把编码器计数频率上限算出来假设电机最高转速3000rpm编码器20脉冲/圈四倍频后每秒10000个计数沿TIM3的时钟是72MHz计数器处理这些沿绰绰有余。但如果你用的是老的8051或者低主频M0核再叠加其他中断负载就很容易扛不住。还有一个优化点是切换中断触发模式。如果你不关心四倍频只需要单倍频那就不要在CHANGE边沿上下沿全部触发中断而是只在下降沿或上升沿触发。这样能直接减少一半以上的中断次数丢脉冲概率大大降低。大原则就是在满足精度要求的条件下中断越少越好。4.4 快速排查流程参考表症状可能原因排查方向与解决措施完全没计数反应AB接错、公共端没接地用万用表测A、B对地电平转动旋钮观察电平是否跳变方向反了AB接反或判断逻辑方向相反软件中交换加减方向或硬件交换A/B接线转一格跳多步机械抖动、采样延时不足加RC滤波、使用状态机、下沿采样增加延时转得快就丢步中断过载、滤波过强精简中断函数、降低计数倍频、换硬件编码器模式偶尔往回转一格B相干扰、共地不良加滤波电容、确保所有设备可靠共地、检查上拉电阻5. 从波形到代码的思维习惯我一直觉得玩嵌入式如果只盯着代码看永远只能在一个很浅的层面上打转。EC11这个小小的器件就是一个特别好的训练素材它让你学会看波形、分析时序、理解硬件外设的工作机制然后再回到代码里去落实这些理解。当你真正在示波器上看到AB相那90度的相位差时你会发现Arduino代码里的方向判断变得无比直观STM32的编码器模式也不再是个玄学配置而是一个“哦原来硬件就是这么按照这个逻辑计数的”的自然结果。我在实际调试中最享受的时刻并不是计数值终于和手转的角度对上了而是通过波形反推出机械结构的微小瑕疵——哪个触点压力不足导致波形边沿不够陡峭哪个地方因为润滑不均导致抖动时间偏长——这种从软件问题一路追到硬件设计的能力才是调试经验里最值钱的部分。希望这篇东西能帮你少走一些我当年走过的弯路。
返回列表