☰
EC11编码器基于STM32的程序源码:消抖与方向判定实战
2026/10/19 8:06:24 网站建设 项目流程

简介:面向嵌入式开发者的EC11编码器基于STM32的完整工程源码,实现旋转角度、方向与速度的采集,并通过串口向上位机发送数据。项目基于STM32F103标准外设库开发,涵盖定时器输入捕获中断、编码器信号处理、串口通信等核心模块,代码层次分明、注释齐全,适合初学者学习编码器原理与STM32中断应用。资源压缩包共120个文件,以C源码、H头文件为主,附有Keil工程配置与编译中间文件,整体仅2.23MB,便于快速下载与实验。已有4161人参与学习,源码直接覆盖初始化配置、中断服务、数据处理和串口发送等关键环节,针对EC11的AB相脉冲计数与方向判别逻辑清晰,可在此基础上快速移植到实际项目。借助该工程,读者可掌握增量编码器测速、测角的一般方法,理清定时器捕获与中断响应的配合思路,为后续嵌入式开发打下坚实基础。

1. EC11编码器基于STM32的程序源码:为什么旋钮比按键难写十倍

EC11编码器配上STM32做旋钮输入,是仪器面板、音频设备、菜单系统里最常见的组合,但很多开发者第一次接上去就翻车。你以为读两个GPIO的电平就行,实际跑起来才发现方向反、数值跳变、转快一点就丢步,一个旋钮能折腾一下午。这份EC11基于STM32的程序源码把AB相采样、消抖、方向判定和计数逻辑全部封装好,给出完整可编译的工程代码,适合正在做旋钮交互的嵌入式开发者直接移植或对照修改。新手照着接线就能跑通,熟手可以拿来做状态机和滤波思路的参考。

2. EC11的信号机制与方案选型:为什么消抖比读电平更关键

EC11本质是机械式旋转编码器,内部是金属触点和固定触点配合,旋转一格会产生两路正交方波信号,方向信息藏在AB两相的相位差里。和光电编码器相比,EC11的优势是便宜、手感明确、不需要复杂的解码电路,缺点是触点接触过程中会有机械抖动,信号边沿不干净。这个抖动如果不在软件层面处理掉,后续的方向判定和计数都会出问题。

2.1 AB相正交输出:一个旋转周期里的四种状态

EC11转动时,A相和B相分别输出高低电平。以顺时针转一格为例,AB组合会按 00 → 01 → 11 → 10 的顺序变化一次;逆时针则是 00 → 10 → 11 → 01。也就是说,完整走一格要经历四个边沿状态,每两个相邻状态的变化都能判定一次方向。这个特性决定了两种常见解码方案:

第一种是边沿触发法:只在A相上升沿中断里读取B相电平,B为低判定顺时针,B为高判定逆时针。优点是代码量极少,缺点是只用了四分之一的状态信息,且对抖动敏感,一个毛刺就可能误判一次方向。

第二种是状态查表法:每次都读取AB组合值,与上一次的值对比,算出格雷码序号差。序号差为1是正转,为-1是反转,其他差值一律视为抖动忽略。这种方案用满全部状态信息,抗干扰能力明显更强。

常见做法是把两种方案结合:用状态机记录上一次AB值,用查表法判定有效跳变和方向,同时丢弃非法的状态跳变(比如00直接跳11这种情况视为抖动)。

2.2 为什么选STM32而不是纯逻辑芯片或外部计数器

纯逻辑芯片如正交解码器虽然能硬解码EC11信号,但只能输出方向和脉冲计数,没法处理按键、长按、双击这类附加交互。STM32的方案是把解码逻辑放进定时器中断或主循环里,同一个旋钮既能计数,又能识别按下、长按、组合操作。另一层考虑是成本——MCU本来就要跑显示和通信,多一个旋钮只是多花几个GPIO,没必要再外挂解码芯片。

EC11还有一个按键引脚,拧动和按下是独立信号。按键手感类似轻触开关,按下时输出低电平(内部上拉时)。按键消抖和旋转消抖要分开处理,否则按下瞬间的机械抖动可能被误判成旋转,这是很多用户拿到源码后遇到的第一个困惑点。

2.3 定时器扫描与外部中断:两个方案的边界

方案优点缺点适用场景
外部中断(上升沿触发)响应快,CPU占用低高速旋转时容易漏中断,抖动毛刺可能触发多次转速低、对实时性要求高的场景
定时器周期扫描(0.5~1ms)稳定可靠,天然滤除窄毛刺占用一个定时器和固定CPU开销绝大多数旋钮应用,推荐首选

实测中,定时器扫描方案在100转/分钟的旋转速度下依然不会丢步。如果转速要求特别高(例如超过300转/分钟),可以把扫描周期缩短到0.25ms,或者退回中断方案但必须加硬件RC滤波。我一般会优先选择定时器扫描,因为它的行为可预期,调试起来也更直观。

3. 驱动代码实现:从GPIO配置到方向判定与按键消抖

这一章直接给可抄的代码。工程基于HAL库,用CubeMX生成后,手工补充编码器逻辑。代码分三层:GPIO初始化、定时器扫描任务(含状态机)、应用层接口。

3.1 GPIO初始化与基础配置

EC11占用两个GPIO作为AB相输入,再加一个GPIO给按键(可选)。推荐全部配置为上拉输入,STM32内部上拉电阻一般够用,但如果线缆超过20厘米,建议外部再加10kΩ上拉到3.3V,减少长线引入的噪声。

// ec11_gpio.c #include "main.h" // 假设A相接PB0,B相接PB1,按键接PB2 #define EC11_A_PIN GPIO_PIN_0 #define EC11_B_PIN GPIO_PIN_1 #define EC11_KEY_PIN GPIO_PIN_2 #define EC11_GPIO_PORT GPIOB void EC11_GPIO_Init(void) { __HAL_RCC_GPIOB_CLK_ENABLE(); // 使能GPIOB时钟 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = EC11_A_PIN | EC11_B_PIN | EC11_KEY_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉,避免浮空 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速输入足够 HAL_GPIO_Init(EC11_GPIO_PORT, &GPIO_InitStruct); }

这段代码的逻辑很直接:打开GPIOB外设时钟,把三个引脚统一配置为输入模式,内部上拉确保引脚悬空时读到确定的高电平。速度等级实际上对输入模式没有影响,但保持低速可以减少不必要的电气噪声。参数关键在于 GPIO_PULLUP,如果改成 GPIO_NOPULL,上电瞬间读到随机电平的概率会明显增加。

3.2 定时器扫描任务:状态机消抖与方向判定

这是整个源码的核心。用一个基础定时器产生0.5ms周期中断,在中断回调里调用扫描函数。扫描函数读取AB电平,通过查表法判断方向和有效跳变。

// ec11_scan.c #include "main.h" // 定义上一次AB组合状态 static uint8_t last_ab = 0; // 方向判定结果:1顺时针,-1逆时针,0无变化 static int8_t ec11_direction = 0; // 旋转计数值(应用层直接读取) volatile int32_t ec11_counter = 0; // AB组合状态对应表 // 索引 = (上一次AB << 2) | 当前AB // 值为1表示顺时针,-1表示逆时针,0表示无效跳变 const int8_t ec11_state_table[16] = { /* 00 -> 00 */ 0, /* 00 -> 01 */ 1, /* 00 -> 11 */ 0, /* 00 -> 10 */ -1, /* 01 -> 00 */ -1, /* 01 -> 01 */ 0, /* 01 -> 11 */ 1, /* 01 -> 10 */ 0, /* 11 -> 00 */ 0, /* 11 -> 01 */ -1, /* 11 -> 11 */ 0, /* 11 -> 10 */ 1, /* 10 -> 00 */ 1, /* 10 -> 01 */ 0, /* 10 -> 11 */ -1, /* 10 -> 10 */ 0 }; void EC11_Scan_Task(void) { uint8_t current_ab; uint8_t index; // 读取当前AB电平 // A相接PB0,B相接PB1,组合成一个2位二进制数 current_ab = (HAL_GPIO_ReadPin(EC11_GPIO_PORT, EC11_A_PIN) ? 2 : 0) | (HAL_GPIO_ReadPin(EC11_GPIO_PORT, EC11_B_PIN) ? 1 : 0); // 与上一次状态组合成查表索引 index = (last_ab << 2) | current_ab; ec11_direction = ec11_state_table[index]; // 如果是有效旋转跳变,更新计数器 if (ec11_direction != 0) { ec11_counter += ec11_direction; last_ab = current_ab; // 仅在有效跳变时更新状态 } // 注意:非法跳变(比如00到11)不更新last_ab, // 等于丢弃了一次抖动毛刺 }

这段代码的逻辑核心是格雷码状态机。AB组合从00到01再到11再到10,对应顺时针;反过来是逆时针。如果毛刺导致跳变跨过了中间状态(例如00直接到11),查表结果是0,既不计数值也不更新状态,等于天然滤除了一次抖动。

参数说明:查表索引用上一次状态的高两位和当前状态的低两位拼成4位数字,范围0到15。表中除了相邻状态跳变给1或-1,其余全部置0。这个表可以直接放在ROM里,不需要每次重新计算。定时器扫描周期0.5ms意味着能滤除窄于0.5ms的毛刺信号,大多数机械开关的抖动时间在1~5ms之间,0.5ms的采样周期配合状态机判据,实测能覆盖绝大多数EC11个体的抖动特性。

3.3 按键消抖与按下事件识别

按键消抖不能和旋转共用同一套逻辑,因为按键按下时的抖动会导致电平在0和1之间跳跃,如果套用状态查表,可能产生误判。这里采用延时消抖法:检测到电平变化后,延时20ms再次读取,确认稳定后再触发事件。

// ec11_key.c #include "main.h" #define KEY_DEBOUNCE_MS 20 static uint8_t key_last_level = 1; // 上次稳定的按键电平 static uint8_t key_press_event = 0; // 按下事件标志 void EC11_Key_Scan_Task(void) { static uint16_t debounce_cnt = 0; uint8_t level; level = HAL_GPIO_ReadPin(EC11_GPIO_PORT, EC11_KEY_PIN) ? 1 : 0; if (level != key_last_level) { debounce_cnt++; if (debounce_cnt >= KEY_DEBOUNCE_MS) { key_last_level = level; if (level == 0) // 按下(低电平有效) { key_press_event = 1; } debounce_cnt = 0; } } else { debounce_cnt = 0; // 电平回弹,清零计数 } } // 应用层调用:查询并清除按下事件 uint8_t EC11_Get_Key_Pressed(void) { if (key_press_event) { key_press_event = 0; return 1; } return 0; }

这个消抖逻辑的特点是:电平变化后不是立即确认,而是持续20ms内保持不变才算有效。如果中间发生回弹抖动,计数器清零重来。20ms这个参数可以根据实际手感调整,手感偏软(回弹慢)的开关可以加大到30ms,手感干脆的可以减到10ms。

调用方式:在同一个定时器中断里同时调用 EC11_Scan_Task() 和 EC11_Key_Scan_Task(),应用层在主循环中轮询 EC11_Get_Key_Pressed() 和读取 ec11_counter 变量。注意ec11_counter声明为volatile,防止编译器优化导致读取到过期的缓存值。

3.4 应用层封装:把计数与按键事件接进你的菜单系统

有了计数和按键事件,后续对接就简单了。常见做法是维护一个菜单索引变量,旋钮改变它,按下确认,长按返回。下面给一个最小封装示例。

// app_menu.c #include "main.h" extern volatile int32_t ec11_counter; extern int8_t ec11_direction; static int32_t menu_index = 0; #define MENU_MAX 10 #define MENU_MIN 0 void Menu_Update(void) { static int32_t last_counter = 0; // 读一次计数值,与上次比较 int32_t delta = ec11_counter - last_counter; last_counter = ec11_counter; menu_index += delta; if (menu_index > MENU_MAX) menu_index = MENU_MAX; if (menu_index < MENU_MIN) menu_index = MENU_MIN; // 按下按键:进入当前菜单项 if (EC11_Get_Key_Pressed()) { Menu_Enter_Item(menu_index); } }

应用层只感知计数的增量变化,不必关心底层是哪个引脚产生了中断。这样后续换引脚、换扫描方案都不用动应用代码。注意菜单索引做了边界钳位处理,避免旋钮转多了越界访问数组。

4. EC11实测避坑:方向反、跳变、高速丢步的排查记录

4.1 现象:顺时针旋转计数反而减小

原因:最常见的是AB两相接线接反,或者PCB上丝印标注与实际走线不一致。代码查表逻辑默认A相超前B相为顺时针,如果实际硬件中B相超前A相,结果自然相反。

解决:先交换两根信号线试试,如果不想改硬件,也可以在查表表项上做镜像处理——把表中所有值为1的项和值为-1的项互换。我一般建议在软件里做方向开关,因为不同型号的EC11引脚定义可能不同,留一个宏定义切换最省事。

4.2 现象:旋钮停在某一格时数值偶尔跳变

原因:机械触点氧化或磨损导致接触电阻增大,信号在边界处反复触发。也可能是扫描周期太短,滤波能力不足。

解决:把扫描周期从0.5ms放宽到1ms,同时确认EC11供电正常。部分廉价的EC11个体在低电压下(低于2.5V)抖动明显加剧,实测3.3V供电下问题大幅减少。如果仍然跳变,检查PCB走线附近是否有大电流回路,必要时加100nF电容在A、B引脚对地。

4.3 现象:快速旋转时数值明显偏小,转十格只计五六格

原因:扫描周期太长,或者中断回调里执行了耗时操作。0.5ms扫描配合状态机应该能覆盖常规手动转速,但如果中断里调用了I2C读写、浮点计算这类高耗时函数,一个周期内读不到完整的AB状态转换。

解决:中断里只做电平读取和查表,计数累加放到主循环。如果转速要求特别高,把定时器周期缩短到0.25ms,前提是确认CPU负载不超标。实测中,GD32和STM32的GPIO读取延迟在几十纳秒量级,瓶颈不在读取本身,而在中断回调里是否塞了多余代码。

4.4 现象:按下旋钮按键时误触发旋转计数

原因:EC11内部结构决定,按下时拨杆会产生横向位移,A、B触点可能出现短暂的虚假变化。按键信号与AB信号在PCB上走线过近,也会引入串扰。

解决:按键消抖时间从20ms加长到50ms,同时在PCB上把按键引脚到MCU的走线包地处理。如果按键误触旋转的情况只在快速按下时出现,可以考虑在检测到按键按下后的50ms内忽略旋转跳变,这个策略直接加到扫描函数里。

4.5 现象:上电瞬间计数值随机偏差

原因:GPIO浮空或内部上拉电阻没有在时钟使能后立即生效,上电初始化阶段读到随机电平,状态机误判为有效旋转。

解决:在GPIO初始化后增加一段50ms延时,或者把EC11_Scan_Task的第一次调用放在主循环第一轮之后。更稳妥的做法是在系统时钟稳定后先调用一次EC11_GPIO_Init,再延时100ms,之后清零last_ab和ec11_counter,确保状态机从已知状态开始。这个坑在电池供电设备上尤其明显,上电瞬间电源电压爬升引起的电平扰动是外部原因,但初始化顺序可以完全由软件控制。

提示:EC11和普通开关不同,它没有物理的“绝对位置”,上电时计数器初始化为0,旋转产生的只是相对增量。如果产品需要保存上次的位置,建议定期把ec11_counter写入Flash或外部存储。

5. 进阶玩法:倍频计数、菜单联动与一个串口调试技巧

EC11每转一格产生一次完整的AB状态循环,也就是四步跳变。上面状态机方案每步有效跳变都计数一次,相当于一格的计数增量为4。如果你希望一格对应计数1,可以在查表结果除以4,但这样会丢失半格位置的精度。折中方案是保留4倍计数,在应用层把delta除以4取整,这样既保证方向判定精度,又不会让菜单索引跳得太快。

与OLED菜单联动是EC11最典型的使用场景。旋钮负责调数值、切换页面,按键负责确认和返回。实际设计中建议把菜单索引显示在屏幕中央,旋钮的计数变化实时刷新显示,按键按下后用整屏刷新避免残影。这不算复杂,但有一件事要注意:OLED刷新本身要几十毫秒,如果在旋转过程中频繁刷新会显得卡顿,常见做法是把刷新频率限制到20帧,只在计数变化超过4时才触发重绘。

调试技巧更重要。EC11最烦人的问题就是方向反了或者跳变,但你看程序看不出问题。我习惯在调试阶段写一个串口输出函数,每50ms打印一次当前AB电平和计数器值,然后手动旋转旋钮,观察打印的AB组合顺序。

// debug_ec11.c #include "main.h" // 在串口调试助手中观察旋转时的电平变化 void EC11_Debug_Print(void) { static uint8_t last_print_ab = 0xFF; uint8_t current_ab; current_ab = (HAL_GPIO_ReadPin(EC11_GPIO_PORT, EC11_A_PIN) ? 2 : 0) | (HAL_GPIO_ReadPin(EC11_GPIO_PORT, EC11_B_PIN) ? 1 : 0); if (current_ab != last_print_ab) { printf("AB=%d%d, counter=%d\n", (current_ab >> 1) & 1, current_ab & 1, ec11_counter); last_print_ab = current_ab; } }

顺时针旋转时,串口输出应该按 00、01、11、10、00、01…的顺序循环;逆时针则是 00、10、11、01、00。如果看到顺序是反的,说明接线反了;如果看到顺序跳变,比如00直接跳到11,说明硬件接触不良或者扫描周期跟不上转角速度。

从那以后我每次调EC11驱动,都强制自己先跑一遍串口打印确认AB顺序,再动代码逻辑。这个方法帮我省下来的排障时间,比写整个驱动的时间还多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询