STM32驱动EC11编码器:正交信号消抖与方向检测完整方案

发布时间:2026/10/19 2:43:23
STM32驱动EC11编码器:正交信号消抖与方向检测完整方案
简介EC11编码器是常见的增量式旋转编码器配合STM32微控制器可实现旋转角度、速度测量与方向判断。这份程序源码专为嵌入式初学者和开发者设计完整演示了从编码器信号采集、定时器输入捕获、中断服务处理到串口上报上位机的全链路适合用作物联网设备、人机界面旋钮等场景的实践教学或项目基础。资源包共120个文件以C源文件、H头文件为主另含KEIL工程配置、编译生成的hex/axf、链接映射文件及标准外设库文件整体大小2.23MB目录划分明晰便于直接打开工程阅读、编译与烧录调试。目前已有4161人学习下载是相关社区中较受欢迎的编码器示例。源码详细给出了A/B相脉冲边沿判别、旋转方向判断、速度与角度计算等关键代码并配置了USART串口通信可将处理结果实时发送到上位机注释完整配合示波器观察和串口调试助手即可快速验证功能是深入理解STM32中断、定时器捕获和串口通信的高质量参考。1. 从一次旋钮调试翻车说起EC11 编码器不是转起来就完事手里有个面板调节项目客户要求用旋钮调参数选型时图便宜订了一批 EC11 编码器。焊好板子、烧了最简单的读电平程序结果拧一格数值跳三四个反向拧还会偶尔往正向跑。把示波器夹到 A、B 脚上才看清触点抖动根本不是普通按键消抖能压住的信号边沿毛刺叠着机械回弹逻辑分析仪里满屏都是无效跳变。后来换了带硬件滤波的驱动程序又踩了倍频和半格定位的坑总算把这颗小零件捋顺了。这份 EC11 编码器基于 STM32 的程序源码解决的就是我这套流程里遇到的全部问题。它把机械编码器的正交信号读取、消抖、方向判断、倍频计数和按键检测集成在一个工程里适合正在做仪器面板、音量旋钮、菜单选择或者任何需要“人拧一下、单片机知道拧了多远多快”的场景。新手可以直接烧录看现象熟手可以拿里面的状态机思路去改自己的驱动层下面我会从工程结构一路拆到怎么改。2. 技术架构与工程结构EC11 信号怎么进 STM322.1 为什么用外部中断加定时器而不是轮询EC11 内部是两个触点转动时输出两路相位差 90° 的方波专业叫正交信号。判断方向不能只看一路电平要看两路的相对变化A 相位领先 B 是正转B 领先 A 是反转。选 STM32 做这个场景是因为它每个 GPIO 都能配外部中断而且定时器有编码器接口模式硬件层面就能解正交信号不用 CPU 反复去读引脚。源码里用的是“外部中断 定时器计数”的混合方案定时器不配编码器模式而是把它当作普通计数器在 A、B 两路的外部中断回调里读电平判断方向。这个做法比纯中断响应多一点代码但好处是消抖逻辑完全受控你可以根据旋钮手感动态调整采样窗口不像硬件编码器模式那样只能依靠固定的滤波参数。常见开发板里的 EC11 模块已经带了 10kΩ 上拉电阻到 3.3V所以源码默认配置为内部上拉输入兼容没有外部上拉的裸板。2.2 工程目录与源文件职责拿到源码先别急着编译先看目录结构。典型的 STM32 标准外设库工程会分成这些部分ec11_driver/ ├── Core/ │ ├── Inc/ │ │ ├── ec11.h │ │ ├── stm32f1xx_it.h │ │ └── main.h │ └── Src/ │ ├── main.c │ ├── ec11.c │ └── stm32f1xx_it.c ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ ├── Hardware/ │ └── (串口、LED等辅助模块) └── Project/ └── (工程文件)ec11.h里暴露了三个核心接口EC11_Init()、EC11_GetCounter()、EC11_GetKeyState()。main.c里只关心业务逻辑比如把计数显示到 OLED 或者传给 PID 设定值。stm32f1xx_it.c中能看到 EXTI0_IRQHandler、EXTI1_IRQHandler 和定时器中断处理函数这三个中断服务函数是驱动的心脏。HAL 库的回调函数HAL_GPIO_EXTI_Callback用来统一入口但它没法区分具体是哪个引脚触发需要在回调里再读一次电平状态来确认。2.3 引脚分配与硬件接线注意事项源码默认的引脚配置是 PA0 接 EC11 的 A 相PA1 接 B 相PE4 接按键输出。PA0 和 PA1 正好是 EXTI0 和 EXTI1 的中断源这样分配可以省去重映射的麻烦。接线时注意 EC11 的五针封装两排各两脚中间一脚是按键实际只有 A、B、按键、接地共四根线要接单片机。我调试时吃过一个亏面包板上把 EC11 的公共端接到了开发板的 5V 引脚而模块上的上拉电阻是到 3.3V 的导致逻辑电平不匹配中断频繁触发。EC11 本身是无源器件供电电压取决于上拉电阻接的电源一般接 3.3V 和 5V 都能工作但和 STM32 直连时建议统一用 3.3V否则 GPIO 保护二极管会导通长时间用有风险。另外 A、B 两路最好各串一个 1kΩ 电阻防止线束抖动时产生的尖峰脉冲直接打坏引脚。3. 核心模块拆解一个旋钮事件从引脚到计数器的完整链路3.1 中断服务函数里干的三件事外部中断每次触发意味着 A 或 B 的电平发生了变化但这时还不知道是正转还是反转。代码的第一件事是记录当前两相电平然后根据“上一次记录的状态”和“当前状态”组成一个四位的状态值查表决定是正转一格还是反转一格。这比单纯判断“谁先跳变”更稳因为机械开关抖动时会出现多次边沿查表法能配合消抖时间窗口滤掉无效状态。// stm32f1xx_it.c 中断回调片段 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if ((GPIO_Pin GPIO_PIN_0) || (GPIO_Pin GPIO_PIN_1)) { // 读取当前电平0 或 1 uint8_t a_level (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) ? 1 : 0; uint8_t b_level (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) GPIO_PIN_SET) ? 1 : 0; uint8_t state (a_level 1) | b_level; // 记录两次变化的间隔时间 uint32_t now HAL_GetTick(); if ((now - last_rotate_tick) EC11_FILTER_MS) { return; // 间隔太短判定为抖动直接丢弃 } last_rotate_tick now; // 查表更新方向 int8_t delta ec11_direction_table[last_state][state]; if (delta ! 0) { if (delta 0) counter; else counter--; } last_state state; } // 按键引脚处理省略 }逻辑说明a_level和b_level是 A、B 两路的原始电平组成 0 到 3 的数值。EC11_FILTER_MS是消抖时间阈值源码里定义为 2实测手感不错。ec11_direction_table是一个 4x4 的二维数组行是上次状态列是当前状态数组值存储1、-1或0。只有合法的正交跳变相邻状态变化才会产生计数直接跳过多个状态的情况不会累加。参数说明EC11_FILTER_MS如果设成 0等于没有消抖手一抖就会误计数设超过 10快速旋转时会丢步因为每个有效状态间隔可能小于这个阈值。2 到 5 是我测试过比较合理的范围具体值取决于你用的编码器手感。3.2 方向查表法比过零判断好在哪常见的编码器方向判断写法是读取 A 相边沿如果 B 相是高电平就正转否则反转。这个思路在理想方波下没问题但 EC11 的触点抖动会造成同一时刻多次触发B 相电平不稳定方向就会来回翻转。而查表法里每两两相邻状态之间只允许一次计数任何跳过状态的跳变都会被忽略。// ec11.c 中的方向状态表 const int8_t ec11_direction_table[4][4] { // 当前状态 // 0 00, 1 01, 2 10, 3 11 // 行上次状态 列当前状态 { 0, -1, 1, 0 }, // 上次 00 { 1, 0, 0, -1 }, // 上次 01 { -1, 0, 0, 1 }, // 上次 10 { 0, 1, -1, 0 }, // 上次 11 };逻辑说明这个表的规则是AB 两相信号顺时针转动时状态顺序是 00 → 01 → 11 → 10 → 00逆时针反向。对角线上的 0 表示两次中断读到相同状态属于无效触发而跳过中间状态的跳变比如从 00 直接到 11在物理上不可能除非拧得太快或抖动太剧烈表中也置 0相当于天然带了一层“合法性过滤”。参数注意这份源码里没有做 4 倍频而是每完成一次完整的 00→01→11→10 循环才计 1。也就是说表里的每一项代表一个状态变化但用户从实际手感上拧一格 EC11 内部会产生一个完整的周期所以代码把每 4 次合法跳变累加成 1 个计数。这里有个可选配置如果你希望手感更灵敏每拧一格计数变 4可以在表驱动循环里去掉除以 4 的操作但那样快速旋转时容易计数溢出。3.3 按键检测怎么避免和旋转互相干扰EC11 中间那个按键通常是直通开关按下去时公共端接地。源码把按键检测放在一个 10ms 定时器中断里做扫描而不是用外部中断因为按键按下的瞬间会有几十毫秒的抖动如果也用 EXTI会频繁唤醒 CPU。定时间隔扫描配合两次确认逻辑比中断加重试更简单。// ec11.c 中的按键扫描函数由 SysTick 每 10ms 调用一次 void EC11_KeyScan(void) { static uint8_t last_level 1; uint8_t now_level (HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_4) GPIO_PIN_SET) ? 1 : 0; // 两次读到低电平才认为是按下滤除单次抖动 if (now_level 0 last_level 0) { if (key_confirm 0) { key_confirm--; if (key_confirm 0) { key_press_flag 1; // 业务层读取后清零 } } } else { key_confirm 2; } last_level now_level; }逻辑说明last_level保存上次扫描的按键电平只有连续两次都读到 0按下才置位按键标志。key_confirm是去抖计数初始为 2每 10ms 扫描一次相当于 20ms 的稳定窗口。旋转时手指可能碰到按键边缘如果外部中断路径没有和按键共用引脚就不会误触发但如果你的板子把按键和 A/B 相布得太近按键按下瞬间会因为寄生电容干扰 A/B 引脚现象是“按键按下时计数器乱跳”。那种情况不能靠改软件消抖必须把按键线分开走。4. 运行与验证编译烧录后用逻辑分析仪看波形4.1 从零编译到烧录的完整步骤拿到源码先不要急着改功能按下面的顺序走一遍确认驱动层是好的再做集成。我用的是 STM32CubeIDE也可以用 Keil两者对 HAL 库工程都友好。# 假设你已经用 STM32CubeMX 根据自己的板子重新生成过工程 # 1. 把 ec11.c 加入工程编译路径 # 2. 在 main.c 的 while(1) 里调用以下业务代码 # while (1) { # if (EC11_GetKeyFlag()) { # printf(key pressed\n); # } # printf(counter: %d\n, EC11_GetCounter()); # HAL_Delay(50); # } # 3. 编译生成 .hex 文件 # 4. 用 ST-Link 烧录cmd 下执行 st-flash write build/ec11_driver.hex 0x08000000逻辑说明先编译再烧录烧录地址固定从0x08000000开始这是 STM32 片上 Flash 的起始地址。如果你的开发板上有外部 Flash 或者 Boot 配置不同地址要相应调整。EC11_GetCounter()返回的是有符号计数顺时针增加、逆时针减少这个值可以直接当作菜单光标位置或者音量等级。参数注意HAL_Delay(50)只存在于测试例程实际产品里不要在中断里调用延时函数否则旋转快时中断回调会被长时间占用。我一开始在while(1)里轮询打印串口波特率 115200打印 50ms 一次足够看到计数器变化如果打印间隔太短串口本身会成为瓶颈Counter 值会积压。4.2 硬件层面验证信号质量的土办法没有逻辑分析仪也可以验证用 STM32 的 ADC 采样引脚电平然后通过串口画波形但更直接的办法是看计数方向。把 A、B 两相用杜邦线引出来用手缓慢转一圈观察串口输出如果正转时数值持续增加反转持续减少说明方向配置没问题。如果反转时数值反而增加说明 A、B 接反了软件上不用改线交换ec11_direction_table里的左右逻辑即可当然硬件上互换 A、B 也是同样效果。// 验证方向是否反了的临时补丁位于 ec11.h // 如果发现反向计数把下面这行注释掉 #define EC11_SWAP_AB 0 // 置 1 时在初始化里交换 PA0/PA1 的映射逻辑不换硬件逻辑说明EC11_SWAP_AB为 0 时按物理引脚读取为 1 时在读取函数里把 A、B 的位序互换。这个宏只在调试期有用正式产品里应当通过硬件保证接线正确因为软件互换虽然能用但会让人觉得工程不严谨。实际波形测量时把逻辑分析仪的采样率设在 1MHz 以上否则 10ms 以内的毛刺会被漏采你看到的是“干净”波形然后就误以为消抖没起作用。4.3 逻辑分析仪下的抖动长什么样用逻辑分析仪抓一次转动过程理想信号是 A、B 两路各 50% 占空比的方波相位差 90°。但 EC11 实际输出在边沿处有密密麻麻的窄脉冲宽度在 10μs 到几百 μs 不等。如果你的代码没有消抖这些窄脉冲每一个都会触发 EXTI计数器会像彩票机一样乱滚。源码里的EC11_FILTER_MS用的是毫秒级判断如果误码率还高可以把判断单位改成微秒级用DWT-CYCCNT计时但这需要额外初始化 DWT 外设。我的经验是标准 EC11 毫秒级足够质量差的编码器建议换成定时器输入捕获模式做硬件滤波。5. 避坑与排查EC11 调试中的典型翻车现场5.1 现象旋转时计数器跳两个数但不是倍频有次我从淘宝买的编码器拧一格计数 2换成另一家的又变正常。原因分析EC11 的定位点数detent和每转脉冲数PPR不是固定值常见的有 15 脉冲、20 脉冲。程序里按每 4 次状态变化计一格但如果编码器内部每物理一格产生两个完整周期就会翻倍。解决方法是看数据手册的脉冲数标注然后在EC11_GetCounter()里做除法映射。不能靠改消抖时间解决那只会把有效脉冲一起滤掉。5.2 现象快速旋转时计数方向偶尔反跳手感是拧快了数值先增后减像在“打嗝”。原因分析外部中断回调执行时如果此时另一相也发生了边沿HAL 库的中断标志会被挂起回调结束立即再次触发。由于我们记录last_state时读到的电平可能已经越过两个状态查表时会出现“非法跳变”被丢弃而之前的合法状态又没计数于是表现为丢步甚至反向。解决方法是把消抖判断放到查表之后、计数之前如果两次中断间隔小于消抖阈值也只更新last_state不计数但要额外校验当前状态不能跟上次相同。5.3 现象按下按键时旋转计数乱串原理按键引脚和 A/B 引脚在 PCB 布线时靠得太近按键按下瞬间的电流突变通过寄生电容耦合到相邻信号线。代码层面怎么调都没用必须检查硬件。解决方法是确认按键引脚与 A/B 相距离至少 2mm且在 A/B 线上各加一个 100nF 对地电容。如果是面包板就把按键线单独飞线不要和 A/B 扎在一起。5.4 现象用 HAL 库别人能跑我的不行原因是 HAL 库的HAL_GPIO_EXTI_Callback是弱定义如果你在别的文件里也实现了同名回调链接器会报重复定义或者只链接其中一个导致部分功能失效。解决方法是只保留一个回调实现或者把旋转处理和按键处理都放进同一个回调里通过GPIO_Pin参数分流。我见过有人把回调写在两个模块里结果调试半天方向错乱查下来是回调被覆盖了。6. 二次开发把旋钮变成参数调节器拿到这份源码除了直接跑最常见的改造方向是把计数器映射成菜单参数。我的建议是在ec11.c里增加一个“速度感知”功能旋转时记录最近几次中断的时间差如果时间差小于某个阈值说明用户在快速旋转计数步长自动加大这样音量调节可以做到“慢拧微调、快拧飞跳”。// 在 ec11.c 中增加的速度感知变量 static uint16_t fast_threshold_tick 100; // 连续两次中断间隔小于100ms视为快速 static uint8_t step_multiplier 1; // 在外部中断回调里计算间隔 uint32_t diff now - last_rotate_tick; if (diff fast_threshold_tick) { step_multiplier 5; // 快速时步进变大 } else { step_multiplier 1; } counter step_multiplier * delta;逻辑说明diff是两次有效旋转事件的时间差单位是HAL_GetTick()的毫秒。快速拧的时候连续事件间隔可能在几十毫秒内step_multiplier变成 5慢拧时是 1。这个方案比单纯加速计数的好处是菜单项移动时能感受到明显的档位差异适合做汽车仪表盘音量或空调风量调节。参数fast_threshold_tick可以做成可配置项放在一个结构体里便于模块化。改造验证方法把计数器通过 I2C 发送给 SSD1306 OLED 显示同时显示步长倍率。快速旋转五圈观察步长是否从 1 切换成 5 并且 OLED 不闪烁。注意在快速模式下要做溢出保护计数器超过业务上限时自动回绕或钳位不然用户从 999 拧到 1000 会直接变成负值体验很怪。我自己的血泪经验是所有旋钮类的参数调节都要做“边界重复触发”处理——当参数已到最大值时继续正转不应再计数而不是回绕。从那以后我每次写编码器驱动都会强制走一遍边界测试先把手感调顺再写业务逻辑希望帮到你。本文还有配套的精品资源点击获取