☰
STM32F407 EC11旋转编码器驱动:从定时器编码器模式到消抖滤波的完整实现
2026/10/15 1:28:58 网站建设 项目流程

简介:这份资源面向使用STM32F407进行运动控制开发的嵌入式工程师与学生,提供EC11旋转编码器的完整驱动函数实现,解决A、B、Z三相信号的采集、方向判别与位置速度计算问题。压缩包共2个文件,包含1个c源文件与1个h头文件,分别承载驱动逻辑实现与函数原型、数据结构声明,整体约3KB,体量轻便、便于直接移植进现有工程。内容围绕GPIO输入配置、外部中断响应、脉冲计数与方向判断、角度和速度换算以及信号丢失与噪声干扰等错误处理展开,并对外提供初始化、获取位置、获取速度等调用接口,方便其他模块集成。目前已有5270人学习下载,适合希望快速搭建高精度角度检测与运动控制方案的开发者参考,也可作为理解STM32F407中断机制与编码器工作原理的实践素材。

1. 从一次“旋钮失灵”说起:STM32F407 上 EC11 驱动到底难在哪

很多人第一次在 STM32F407 上接 EC11 旋转编码器,都会遇到同一个场景:代码明明照着例程抄了,串口打印也开了,可旋钮转起来要么纹丝不动,要么数字乱跳,往右拧反而减,往左拧反而加。更玄学的是,手一碰金属轴,计数就疯跑。这不是你手气差,而是 EC11 这类机械式增量编码器,天生就带着抖动和相位竞争两个坑,而 STM32F407 的 GPIO 速度又足够快,快到你用轮询就能把每一段抖动都采进来。

这篇笔记就围绕“STM32F407 EC11 旋转编码器驱动函数”这件事,把从硬件接法、GPIO 配置、定时器编码器模式、到中断与状态机消抖的完整落地路径讲清楚。适合两类人:一类是刚上手 F407、想用 HAL 库快速把旋钮跑起来的;另一类是用过编码器但被“反向计数、丢步、连跳”折磨过,想找一套稳定驱动函数的。下面所有代码都基于常见做法,不依赖某一份特定工程,你可以直接往自己的工程里搬。

2. 先选对模式:EC11 在 F407 上到底该用 GPIO 中断还是定时器编码器接口

2.1 EC11 的电气本质:两路方波加一个按键

EC11 输出的是 A、B 两路正交方波,外加一个按下时导通的按键引脚。旋转时 A、B 相位差 90 度,正转时 A 领先 B,反转时 B 领先 A。每转过一个机械定位点,A、B 会经历一个完整的四状态循环:00 → 01 → 11 → 10(或反向)。STM32F407 要做的就是识别这个状态序列的方向和步数。

关键点在于:机械触点闭合瞬间不是理想方波,而是几十微秒到几毫秒的抖动。如果你用普通 GPIO 外部中断,边沿触发一次就进一次中断,抖动会被当成多次旋转,这就是“转一格跳好几下”的根源。所以选型的第一件事,是决定用哪种硬件资源来消化这个抖动。

2.2 三种常见方案对比与选型理由

方案资源占用抖动容忍度适合场景
GPIO 轮询 + 状态机低,需定时调用高,靠采样周期过滤旋钮数量少、主循环有空闲
GPIO 外部中断 + 软件消抖中,中断频繁中,依赖消抖逻辑低功耗、不能常轮询
定时器编码器模式中,占用一个 TIM高,硬件四倍频多旋钮、要求高实时性

我一般会优先推荐定时器编码器模式,因为 STM32F407 的 TIM2~TIM5 都带编码器接口,硬件自动完成四倍频计数和方向判断,CPU 几乎不参与。但要注意:编码器模式只解决计数,不解决机械抖动,抖动依然会进计数器,所以后面还得配合软件滤波或定时读取。

如果只是做一个人机旋钮,GPIO 轮询加状态机反而最省事,因为你可以把采样周期设成 1ms,天然把抖动滤掉。下面两节分别给出这两条路的最小可跑通实现。

2.3 定时器编码器模式的 CubeMX 配置与读取函数

在 CubeMX 里选 TIM3,Combined Channels 设为 Encoder Mode,Encoder Mode 选 TI1 and TI2,Counter Period 设成 65535,极性都设 Rising Edge。生成代码后,核心读取函数只有几行:

/* ec11_tim.c —— 基于 TIM3 编码器模式的 EC11 读取 */ #include "ec11_tim.h" extern TIM_HandleTypeDef htim3; /* CubeMX 生成的句柄 */ /* 读取当前计数值,并清零,返回本次相对变化量 */ int16_t EC11_TIM_ReadDelta(void) { int16_t cnt = (int16_t)__HAL_TIM_GET_COUNTER(&htim3); __HAL_TIM_SET_COUNTER(&htim3, 0); /* 读后清零,避免累积溢出 */ return cnt; } /* 初始化时启动编码器接口 */ void EC11_TIM_Start(void) { HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL); }

逻辑说明:__HAL_TIM_GET_COUNTER拿到的是硬件四倍频后的计数值,正转加、反转减。每次读完清零,下一次读到的就是增量。参数上,Counter Period 设 65535 是为了让 16 位计数器在两次读取之间不会溢出;如果你读取周期很长、转速又高,可以改成 32 位定时器或缩短读取周期。注意int16_t强转是为了让反向计数得到负数,如果你用uint16_t,反转会变成 65535 这种大数,判断方向时就会翻车。

2.4 GPIO 轮询状态机:不占用定时器资源的写法

如果定时器紧张,可以用一个 1ms 的 SysTick 或软件定时任务来轮询 A、B 引脚,用状态机判断方向:

/* ec11_gpio.c —— GPIO 轮询状态机,1ms 调用一次 */ #include "ec11_gpio.h" #define EC11_A_READ() HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_0) #define EC11_B_READ() HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_1) static uint8_t last_state = 0; static int32_t ec11_count = 0; /* 每 1ms 调用一次,返回累计计数 */ int32_t EC11_GPIO_Poll(void) { uint8_t cur = (EC11_A_READ() << 1) | EC11_B_READ(); uint8_t trans = (last_state << 2) | cur; /* 拼接上一次和当前状态 */ switch (trans) { case 0b0001: case 0b0111: case 0b1110: case 0b1000: ec11_count++; /* 正转序列 */ break; case 0b0010: case 0b1011: case 0b1101: case 0b0100: ec11_count--; /* 反转序列 */ break; default: break; /* 00->00、11->11 等无效跳变忽略 */ } last_state = cur; return ec11_count; }

逻辑说明:trans把上一次状态和当前状态拼成 4 位,只有合法的相邻跳变才会计数,像 00 直接跳到 11 这种抖动跳变会被 default 分支丢掉。参数上,1ms 调用周期是关键,太快会把抖动采进来,太慢会丢步;EC11 手动旋转最快也就每秒几圈,1ms 足够。注意这个函数必须周期调用,放在主循环里要保证不被长延时阻塞,否则计数会丢。

3. 把驱动函数写稳:消抖、方向判断与按键处理的落地细节

3.1 为什么硬件编码器模式还要加软件滤波

定时器编码器模式虽然硬件计数,但机械抖动依然会让计数器多走几步。常见现象是:慢慢拧一格,计数却变了 2 或 3。解决思路不是改硬件,而是在读取层加一个“变化量阈值”或“时间窗滤波”。我一般会在读取函数外面再包一层:

/* 带时间窗滤波的读取,过滤掉 5ms 内的反向抖动 */ int16_t EC11_ReadFiltered(void) { static int16_t acc = 0; static uint32_t last_tick = 0; int16_t delta = EC11_TIM_ReadDelta(); uint32_t now = HAL_GetTick(); if (delta != 0) { if ((now - last_tick) < 5) { acc += delta; /* 5ms 内的变化先累积 */ } else { acc = delta; /* 超过时间窗,重新开始 */ } last_tick = now; } /* 累积到 ±2 以上才认为是有效旋转 */ if (acc >= 2) { acc = 0; return 1; } if (acc <= -2) { acc = 0; return -1; } return 0; }

逻辑说明:acc累积短时间内的计数,只有累积到 ±2 才输出一步,这样单次抖动很难凑够阈值。参数 5ms 和 ±2 是经验值,旋钮手感偏松可以调到 10ms 和 ±3。注意这个函数要周期性调用,不能只在需要时读一次,否则 acc 永远攒不够。

3.2 方向判断的两种写法与反向问题排查

方向判断本质是看 A、B 谁先变。用定时器编码器模式时,计数器增减就代表方向,不需要额外判断。用 GPIO 状态机时,上面 switch 里的正转序列和反转序列就是方向依据。如果你发现方向反了,不用改硬件接线,直接把返回值的符号取反,或者把 A、B 引脚在宏定义里对调即可。

但有一种“反向”是假反向:正转时计数偶尔减一下。这通常是抖动导致状态机走了非法跳变后又补了一个反向跳变。解决办法是提高采样周期一致性,别在中断里调用轮询函数,因为中断时机不确定会打乱状态序列。

3.3 按键引脚的处理:别和旋转共用中断

EC11 的按键是独立机械开关,按下时抖动更明显,通常 10~20ms。常见做法是单独用一个 GPIO 输入,配内部上拉,然后在 20ms 定时任务里读电平并做状态机:

/* 按键消抖,20ms 调用一次 */ uint8_t EC11_Key_Scan(void) { static uint8_t last = 1, stable = 1; uint8_t cur = HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2); if (cur != last) { last = cur; } else if (cur != stable) { stable = cur; if (stable == 0) return 1; /* 返回一次按下事件 */ } return 0; }

逻辑说明:连续两次读到相同电平才更新稳定状态,避免抖动误触发。参数上 20ms 调用周期对应机械按键典型抖动时间。注意按键和旋转不要共用同一个外部中断,否则按键抖动会干扰旋转计数。

3.4 一个完整的驱动头文件接口设计

把上面几块拼起来,对外只暴露几个函数,方便主循环调用:

/* ec11.h —— 对外接口 */ #ifndef EC11_H #define EC11_H #include <stdint.h> void EC11_Init(void); /* 初始化 GPIO 或定时器 */ int16_t EC11_GetDelta(void); /* 返回本次旋转步数,正转正、反转负 */ uint8_t EC11_KeyPressed(void); /* 返回 1 表示按键按下一次 */ #endif

逻辑说明:EC11_GetDelta内部可以选定时器模式或 GPIO 模式,主循环只关心增量。参数上,返回值用 int16_t 足够,因为人手旋转不会一次超过几百步。注意初始化里要把定时器编码器启动或把轮询任务注册到 1ms 调度器,否则 GetDelta 永远返回 0。

4. 避坑与排查:EC11 驱动最常见的 5 个翻车现场

4.1 现象:旋钮不动,计数一直是 0

原因:定时器编码器模式没启动,或者 GPIO 模式轮询函数没被周期调用。解决:检查HAL_TIM_Encoder_Start是否调用,GPIO 模式下确认 1ms 任务真的在跑,可以在函数里翻转一个测试引脚用示波器看。

4.2 现象:转一格跳 3~5 下

原因:机械抖动被当成多次旋转,常见于外部中断方案没消抖。解决:改用定时器编码器模式加时间窗滤波,或把 GPIO 轮询周期从 100us 放宽到 1ms。

4.3 现象:正转偶尔减、反转偶尔加

原因:A、B 两路采样不同步,状态机读到非法跳变后又补了反向跳变。解决:确保 A、B 在同一时刻读取,用(A<<1)|B一次性拼出状态,不要分两次读。

4.4 现象:手碰金属轴计数疯跑

原因:EC11 金属外壳没接地,人体静电耦合进 A、B 引脚。解决:把编码器外壳接到板子 GND,A、B 引脚加 100nF 到地,软件上把滤波阈值从 ±2 提到 ±3。

4.5 现象:按键按下时旋转计数乱掉

原因:按键和旋转共用中断或共用轮询函数,按键抖动占用了处理时间。解决:按键单独用 20ms 任务扫描,旋转用独立定时器或独立状态变量,两者不共享中断。

5. 进阶技巧:用定时器输入捕获测转速,并验证驱动稳定性

5.1 为什么还要测转速

计数只告诉你转了多少格,不告诉你转多快。有些场景需要根据旋转速度调整步进(比如慢拧微调、快拧粗调),这时候就得测转速。STM32F407 的定时器输入捕获可以测 A 相周期,周期越短转速越快。

5.2 输入捕获测速的最小实现

/* 用 TIM4 CH1 输入捕获测 A 相周期,单位 us */ uint32_t EC11_GetPeriodUs(void) { static uint32_t last_cap = 0, period = 0; uint32_t cap = HAL_TIM_ReadCapturedValue(&htim4, TIM_CHANNEL_1); if (cap != last_cap) { period = (cap > last_cap) ? (cap - last_cap) : (0xFFFF - last_cap + cap); last_cap = cap; } return period; /* 周期越小,转速越快 */ }

逻辑说明:捕获值差值就是 A 相一个周期的时间,单位取决于定时器时钟。参数上,定时器预分频设成 84-1,则计数频率 1MHz,捕获值单位就是 1us。注意要处理计数器溢出,上面用 0xFFFF 做回绕补偿。

5.3 用串口打印验证驱动稳定性的方法

把EC11_GetDelta和EC11_GetPeriodUs一起打印,正转时 delta 应为正、period 稳定;反转时 delta 为负。连续拧 100 格,看累计值是否正好 100,如果多了就是抖动没滤干净,如果少了就是丢步。这个验证方法比看单次计数靠谱得多,我一般会跑 5 分钟连续旋转,累计误差在 ±2 以内才算过关。

5.4 一个我常犯的教训

早期我为了省事,把 EC11 的 A、B 直接接在 5V 容忍引脚上,结果发现计数偶尔乱跳,查了半天才发现是引脚内部上拉太弱,长线缆上引入了干扰。后来养成习惯:EC11 一律用内部上拉加外部 10k 上拉,线长超过 10cm 就加 100nF 滤波电容。这个习惯帮我省下了大量排查时间。希望帮到你。

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

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

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

立即咨询