☰
NodeMCU Firmware rotary 模块指南:读取旋转编码器与按压开关事件
2026/9/27 10:13:23 网站建设 项目流程
  • 物联网
  • 嵌入式

【免费下载链接】nodemcu-firmware

Lua based interactive firmware for ESP8266, ESP8285 and ESP32

项目地址:https://gitcode.com/gh_mirrors/no/nodemcu-firmware
点击查看免费下载

导读

rotary是 NodeMCU Firmware 内置的 Lua 模块,用于驱动廉价的旋转编码器(rotary encoder / quadrature switch),这类五引脚器件常见于车载音响、旋钮式音量控制等场景:其中三只引脚以格雷码(gray code)输出旋转方向与步数,另外两只引脚对应一个按压开关(push switch)。本指南将完整讲解该模块的事件常量、rotary.setup()、rotary.on()、rotary.getpos()、rotary.close()四个 API 的用法与参数细节,并结合 app/modules/rotary.c 与 app/driver/rotary.c 的源码,说明其事件队列、防抖与长按/双击判定等底层实现原理。读完本文,你将能独立接线、启用模块并编写出可响应旋钮旋转、单击、双击、长按等事件的 NodeMCU 应用。

认识旋转编码器与接线方式

本模块面向的器件没有绝对位置编码,只能输出顺时针 / 逆时针旋转的相对步数。这类开关通常为五引脚:三引脚用于正交(quadrature)旋转检测,两引脚用于按压开关。

接线要求(来自 docs/modules/rotary.md):

  • 将正交编码器的公共引脚(common)接到GND;
  • A 相、B 相分别接到 NodeMCU 的两个 GPIO;
  • 按压开关的一个引脚接地,另一个引脚接到 NodeMCU 的一个 GPIO。

从源码看,app/driver/rotary.c 在rotary_setup()中会把 A/B 相与按压引脚全部配置为PLATFORM_GPIO_INT模式并启用内部上拉(PLATFORM_GPIO_PULLUP),同时注册任意边沿中断GPIO_PIN_INTR_ANYEDGE。因此外部无需额外上拉电阻,接线完成后即可直接使用。

器件购买与常见型号(仅供参考)

原文档列出了一些常见购买渠道:Amazon、eBay、Adafruit、AliExpress 上搜索rotary encoder push button即可找到多种规格。另有焊在标准 0.1" 间距板上的成品模块KY-040,其引脚命名比较特殊,且实践中通常需要连接 VCC 才能正常工作。需要提醒的是,这些外链与器件规格属于原文档给出的经验信息,实际选购时请以具体器件的数据手册为准。

启用 rotary 模块(编译期配置)

rotary模块并非默认编译进固件。在 app/include/user_modules.h 中该模块默认被注释掉:

//#define LUA_USE_MODULES_ROTARY

如需使用,应在构建固件前取消该行注释。同时,app/modules/rotary.c 顶部有一段强制编译检查:

#if !defined(GPIO_INTERRUPT_ENABLE) || !defined(GPIO_INTERRUPT_HOOK_ENABLE) #error Must have GPIO_INTERRUPT and GPIO_INTERRUPT_HOOK if using ROTARY module #endif

也就是说,编译时必须同时开启GPIO_INTERRUPT_ENABLE与GPIO_INTERRUPT_HOOK_ENABLE,否则直接编译失败。这与模块的实现方式一致:旋钮和按键全部通过 GPIO 中断驱动,并借助platform_gpio_register_intr_hook()(实现于 app/platform/platform.c)向平台 GPIO 中断分发器挂接钩子函数rotary_interrupt。

事件类型常量

模块将开关动作抽象为六类事件,均以整数常量形式暴露给 Lua(定义于 app/modules/rotary.c 的ROTARY_PRESS_INDEX~ROTARY_DBLCLICK_INDEX,对应掩码如下):

常量值含义
rotary.PRESS1按压开关被按下
rotary.LONGPRESS2长按事件
rotary.RELEASE4按压开关松开
rotary.TURN8旋钮旋转
rotary.CLICK16单击(在松开之后判定)
rotary.DBLCLICK32双击(在第二次松开之后判定)
rotary.ALL63以上全部事件类型的按位或

这些值两两不重叠(1、2、4、8、16、32),因此可以像位掩码一样自由组合,例如用rotary.TURN | rotary.CLICK只关心旋转和单击,或直接使用rotary.ALL接收所有事件。若向rotary.on()传入非法的事件类型掩码,模块会抛出错误。

rotary.setup() —— 初始化通道

rotary.setup(channel, pina, pinb[, pinpress[, longpress_time_ms[, dblclick_time_ms]]])

参数说明:

  • channel:模块最多同时支持3 个旋转开关,取值只能是 0、1 或 2。该上限由 app/include/driver/rotary.h 中的#define ROTARY_CHANNEL_COUNT 3决定。
  • pina:连接开关 A 相的 GPIO 编号(不能是 0)。
  • pinb:连接开关 B 相的 GPIO 编号(不能是 0)。
  • pinpress(可选):连接按压开关的 GPIO 编号(不能是 0);省略则只检测旋转。
  • longpress_time_ms(可选):按多久才算长按,默认500 毫秒。源码中默认值LONGPRESS_DELAY_US = 500000微秒与之对应,Lua 传入的毫秒值会乘以 1000 转换为微秒存储。
  • dblclick_time_ms(可选):从“松开”到“再次按下”之间允许的最大间隔,用于判定双击,默认500 毫秒。对应源码中的CLICK_DELAY_US = 500000。

返回值:无。参数非法或底层初始化失败(如 GPIO 不存在、通道冲突等)时直接抛出 Lua 错误。

注意:文档中pina/pinb/pinpress均要求“GPIO number excluding 0”,即 GPIO0 不可用于本模块。另外,三个通道共用一套 GPIO 中断钩子,若某个引脚已被其它使用中断钩子的模块(如 softuart、somfy、wiegand 等)占用,platform_gpio_register_intr_hook()会因位掩码冲突而拒绝注册。

示例(通道 0,A 相 GPIO5,B 相 GPIO6,按压 GPIO7):

rotary.setup(0, 5, 6, 7)

从 app/driver/rotary.c 的实现看,rotary_setup()会对 A/B 相(以及可选的按压引脚)逐个配置中断并加入pin_mask,最终通过set_gpio_bits()注册中断钩子。若对同一通道重复调用setup(),底层会先自动rotary_close()清理旧资源再重新配置。

rotary.on() —— 注册事件回调

rotary.on(channel, eventtype[, callback])

参数说明:

  • channel:0、1 或 2。
  • eventtype:要监听的事件类型,可以是上文任一常量的按位或组合。
  • callback:事件发生时被调用的函数;如果传nil或省略该参数,则取消对应事件的注册。

回调函数被调用时会收到三个参数:

  1. eventtype:本次触发的事件类型(对应PRESS/LONGPRESS/RELEASE/TURN/CLICK/DBLCLICK之一);
  2. pos:旋钮当前的位置,是一个有符号 32 位整数,数值增大表示顺时针旋转;
  3. when:事件发生的时刻,单位为微秒,同样以 32 位整数表示,注意该值大约每 1 小时左右会回绕一次,比较时间差时要注意符号运算。

示例:

rotary.on(0, rotary.ALL, function (type, pos, when) print("Position=" .. pos .. " event type=" .. type .. " time=" .. when) end)

说明:原文档示例写作print "Position=" .. ...,这是 Lua 5.3 中print作为语句的写法;在默认的 Lua 5.1 构建中请使用print(...)函数调用形式。当前仓库同时包含 app/lua(Lua 5.1)与 app/lua53 两套解释器,具体语法以你构建固件时选择的 Lua 版本为准。

事件送达的语义与注意事项

原文档对事件送达作了细致说明,这些行为与 app/modules/rotary.c 的lrotary_dequeue_single()处理逻辑完全吻合:

  • 事件按顺序送达,但可能存在TURN 事件丢失。底层驱动使用容量为 8 的环形队列(见 app/driver/rotary.c 的QUEUE_SIZE 8),当队列写满时,新的状态会覆盖队尾最近一次记录而非强制入队。
  • 如果事件积压严重,PRESS和RELEASE事件也可能被丢弃。
  • 多个待处理的 TURN 事件通常合并为一次回调,并以其最终位置作为参数,回调函数拿到的pos是累计后的最新位置。

位置刻度与旋转分辨率

有些旋钮每个定位档位对应 4 步正交脉冲。此时应用层应把pos除以 4 来换算成实际的“格数”。底层驱动在每个 A/B 相沿变化时对位置执行+1或-1(参见rotary_interrupt中按 4 个微相位状态机的判定),因此原始计数值为 1/4 步精度。

由于位置是有符号 32 位整数,理论上可表达 ±30 位的旋转量;但实际机械旋钮寿命通常远低于此(部分型号额定寿命不足 5 万转),应用不必担心计数溢出问题。

单击、双击与长按的判定时序

  • CLICK与LONGPRESS事件都是基于超时定时器派发的:按下后若持续按压超过longpress_time_ms,则触发LONGPRESS;松开后若在dblclick_time_ms内没有再次按下,则在超时时刻补发CLICK。
  • DBLCLICK的判定条件是完整的PRESS → RELEASE → PRESS → RELEASE序列,且中间那次RELEASE与随后的PRESS之间的间隔必须小于dblclick_time_ms。一旦判定为双击,模块会抑制随之而来的CLICK事件(源码中通过清零last_recent_event_was_release实现)。

底层对应 app/modules/rotary.c 的lrotary_check_timer():它维护last_recent_event_was_press与last_recent_event_was_release两个状态位,并借助ETSTimer(os_timer_arm)在需要时安排超时回调。

rotary.getpos() —— 查询当前位置与按压状态

pos, press = rotary.getpos(channel)

参数:

  • channel:0、1 或 2。

返回值:

  • pos:旋钮当前累计位置(有符号 32 位整数)。
  • press:布尔值,表示按压开关当前是否处于按下状态。

示例:

print(rotary.getpos(0))

从 app/modules/rotary.c 的lrotary_getpos()可以看到,驱动层把“按下状态”编码在位置值的最高位(0x80000000),模块层取出该位后转换为布尔值返回,因此在纯位置查询场景中它是无阻塞的即时读取。

rotary.close() —— 释放通道资源

rotary.close(channel)

参数:

  • channel:0、1 或 2。

关闭后,该通道的中断、钩子位掩码与内存都会被释放(底层rotary_close()会逐一关闭 A/B 相与按压引脚的中断并恢复为上拉输入模式),后续可以重新setup()复用该通道。关闭不存在的通道不会报错。

示例:

rotary.close(0)

底层实现要点:中断驱动的解码与防抖

为方便读者深入理解,这里结合 app/driver/rotary.c 补充几个关键实现细节:

  1. 任意边沿中断 + 软件解码:A/B 相均注册GPIO_PIN_INTR_ANYEDGE,中断服务函数rotary_interrupt通过读取GPIO_STATUS判断哪些引脚变化,再读取GPIO_IN实时采样电平,将 A/B 相组合映射为 0~3 的微相位,与上次相位求差:差为 1 时位置+1,差为 3 时位置-1,差为 2 说明漏了一次沿(位置额外标记+1000000以便上层察觉异常)。该函数被标记为ICACHE_RAM_ATTR,运行于中断上下文。
  2. 按压开关 10ms 防抖:rotary_interrupt中按压状态变化后 10ms 内的再次跳变会被忽略(now - d->last_press_change_time > 10 * 1000),按下/松开状态以最高位0x80000000表示。
  3. 中断 → 任务队列:ISR 只负责入队和投递一个中优先级任务(task_post_medium),真正的 Lua 回调派发发生在任务上下文(lrotary_task→lrotary_dequeue_single),避免在中断中执行 Lua 代码,也保证了多个通道事件能被依次、有序地派发。
  4. 回调生命周期:模块通过luaL_ref/luaL_unref在注册表中管理回调引用,on()传nil即释放对应槽位;setup()和close()都会清理旧回调,防止悬空引用。

综合示例:一个可用的旋钮控制脚本

将前面各 API 组合起来,即可实现一个带单击、双击、长按与旋转计数的完整脚本(可直接通过 ESPlorer 或串口工具上传运行):

-- 接线:A→GPIO5,B→GPIO6,按键→GPIO7,公共端接 GND rotary.setup(0, 5, 6, 7) rotary.on(0, rotary.ALL, function (type, pos, when) local kind if type == rotary.PRESS then kind = "PRESS" elseif type == rotary.LONGPRESS then kind = "LONGPRESS" elseif type == rotary.RELEASE then kind = "RELEASE" elseif type == rotary.TURN then kind = "TURN" elseif type == rotary.CLICK then kind = "CLICK" elseif type == rotary.DBLCLICK then kind = "DBLCLICK" end print("event=" .. kind .. " pos=" .. pos) end) -- 如果旋钮是每档 4 步,用 (pos / 4) 得到档位数 -- 查询当前状态示例: -- local p, pressed = rotary.getpos(0)

需要清理通道时调用:

rotary.close(0)

小结

  • rotary模块适合用最少的 GPIO 驱动常见的正交旋转编码器,支持旋转计数与按压按键两类输入,最多同时管理3 个通道。
  • 事件系统基于位掩码常量(PRESS/LONGPRESS/RELEASE/TURN/CLICK/DBLCLICK/ALL),一个回调即可覆盖全部事件类型。
  • 长按与双击阈值(均默认 500ms)可在setup()中按需调整;TURN 事件可能合并或丢失、CLICK/DBLCLICK 依赖超时判定等行为特性,需要在应用逻辑中予以考虑。
  • 使用前提:构建固件时必须启用LUA_USE_MODULES_ROTARY,并同时开启GPIO_INTERRUPT_ENABLE与GPIO_INTERRUPT_HOOK_ENABLE。

相关参考文件:docs/modules/rotary.md(模块官方文档)、app/modules/rotary.c(Lua API 实现)、app/driver/rotary.c(中断解码与事件队列驱动)、app/include/driver/rotary.h(通道数与事件结构定义)、app/include/user_modules.h(模块编译开关)、app/platform/platform.c(GPIO 中断钩子注册机制)。

  • 物联网
  • 嵌入式

【免费下载链接】nodemcu-firmware

Lua based interactive firmware for ESP8266, ESP8285 and ESP32

项目地址:https://gitcode.com/gh_mirrors/no/nodemcu-firmware
点击查看免费下载
上一篇:RF-DETR 训练日志与实验追踪完全指南:TensorBoard、W&B、MLflow 的多 Logger 配置实战
下一篇:终极指南:炉石传说自动化脚本如何让你的游戏时间效率提升7倍

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询