- 物联网
- 嵌入式
【免费下载链接】nodemcu-firmware
Lua based interactive firmware for ESP8266, ESP8285 and ESP32
导读
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.PRESS | 1 | 按压开关被按下 |
rotary.LONGPRESS | 2 | 长按事件 |
rotary.RELEASE | 4 | 按压开关松开 |
rotary.TURN | 8 | 旋钮旋转 |
rotary.CLICK | 16 | 单击(在松开之后判定) |
rotary.DBLCLICK | 32 | 双击(在第二次松开之后判定) |
rotary.ALL | 63 | 以上全部事件类型的按位或 |
这些值两两不重叠(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或省略该参数,则取消对应事件的注册。
回调函数被调用时会收到三个参数:
eventtype:本次触发的事件类型(对应PRESS/LONGPRESS/RELEASE/TURN/CLICK/DBLCLICK之一);pos:旋钮当前的位置,是一个有符号 32 位整数,数值增大表示顺时针旋转;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(...)函数调用形式。当前仓库同时包含 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 补充几个关键实现细节:
- 任意边沿中断 + 软件解码: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,运行于中断上下文。 - 按压开关 10ms 防抖:
rotary_interrupt中按压状态变化后 10ms 内的再次跳变会被忽略(now - d->last_press_change_time > 10 * 1000),按下/松开状态以最高位0x80000000表示。 - 中断 → 任务队列:ISR 只负责入队和投递一个中优先级任务(
task_post_medium),真正的 Lua 回调派发发生在任务上下文(lrotary_task→lrotary_dequeue_single),避免在中断中执行 Lua 代码,也保证了多个通道事件能被依次、有序地派发。 - 回调生命周期:模块通过
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
相关推荐
LifeOS Interceptor 多页对比事实抽取:MultiPageCompare 工作流实战指南
LifeOS Interceptor 多页对比事实抽取:MultiPageCompare 工作流实战指南 多页对比问题——"Python 和 JavaScrip
物联网嵌入式ManyMC启动器:Apple Silicon Mac用户的终极Minecraft原生体验解决方案
ManyMC启动器:Apple Silicon Mac用户的终极Minecraft原生体验解决方案 ManyMC是一款专为Apple Silicon芯片(M1/
桌面应用游戏开发Juggl移动端优化指南:在手机上使用交互式知识图谱的10个最佳实践
Juggl移动端优化指南:在手机上使用交互式知识图谱的10个最佳实践 Juggl是Obsidian中一款强大的交互式知识图谱插件,它提供了完全可定制和可扩展的图
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考