旋转编码器
大约 6 分钟
旋转编码器
功能说明
旋转编码器用于检测旋转方向和角度,支持正转/反转计数和按键功能。适用于音量调节、菜单导航、位置控制等场景。
驱动实现状态:✅ 完整驱动已实现 (v2.1)
核心特性
- ✅ GPIO中断模式 - 兼容所有ESP32系列芯片(ESP32/ESP32-S3/ESP32-C3/ESP32-C6)
- ✅ 正交解码原理 - A/B相相差90度相位,精准判断旋转方向
- ✅ 实时计数读取 - 通过readPin() API获取计数器状态
- ✅ 计数器重置 - 通过writePin() API写入STATE_LOW归零计数器
- ✅ 中断处理优化 - 使用IRAM_ATTR属性,确保在IRAM中执行提高响应速度
- ✅ 线程安全设计 - 使用volatile int32_t确保中断上下文安全
- ✅ 计数器存储 - std::map<String, volatile int32_t>管理多编码器实例
💡 未来优化方向:ESP32/ESP32-S3可升级为PCNT硬件计数器(更高精度,无需CPU干预)
工作原理
正交解码机制
编码器使用A/B两相正交信号进行方向判断:
顺时针旋转(CW):
A相: ─┐ ┌─┐ ┌─┐ ┌─
└───┘ └───┘ └───
B相: ───┐ ┌─┐ ┌─┐ ┌
└───┘ └───┘ └───┘
方向判断:A相中断时,B相为HIGH → 计数+1
逆时针旋转(CCW):
A相: ─┐ ┌─┐ ┌─┐ ┌─
└───┘ └───┘ └───
B相: ┌─┐ ┌─┐ ┌─┐
───┘ └───┘ └───┘ └
方向判断:A相中断时,B相为LOW → 计数-1中断处理流程
void IRAM_ATTR PeripheralManager::handleEncoderInterrupt(void* arg) {
// 1. 解析外设配置
PeripheralConfig* config = static_cast<PeripheralConfig*>(arg);
if (!config || config->type != PeripheralType::ENCODER) return;
// 2. 读取A/B相状态
uint8_t pinA = config->pins[0];
uint8_t pinB = config->pins[1];
int stateA = digitalRead(pinA);
int stateB = digitalRead(pinB);
// 3. 判断旋转方向
// B相为HIGH时顺时针,B相为LOW时逆时针
int32_t direction = stateB ? 1 : -1;
// 4. 更新计数器(中断安全)
String peripheralId = config->id;
auto& pm = PeripheralManager::getInstance();
auto it = pm.encoderCounters.find(peripheralId);
if (it != pm.encoderCounters.end()) {
it->second += direction;
} else {
pm.encoderCounters[peripheralId] = direction;
}
}初始化流程
// 1. 验证引脚数量(至少2个:A相和B相)
if (config.pinCount < 2) return false;
// 2. 配置GPIO为上拉输入模式
pinMode(pinA, INPUT_PULLUP);
pinMode(pinB, INPUT_PULLUP);
// 3. 初始化计数器
if (config.params.encoder.useInterrupt) {
encoderCounters[config.id] = 0;
// 4. 附加中断到A相(上升沿和下降沿都触发)
attachInterruptArg(pinA, handleEncoderInterrupt,
const_cast<PeripheralConfig*>(&config), CHANGE);
}支持的外设类型
| 类型 | type值 | 说明 |
|---|---|---|
| ENCODER | 43 | 旋转编码器(双通道A/B相) |
硬件接线
| 编码器引脚 | 功能 | 说明 |
|---|---|---|
| CLK (A) | GPIO | A相信号 |
| DT (B) | GPIO | B相信号 |
| SW | GPIO(可选) | 按键信号(可作为单独输入) |
| VCC | 3.3V | 电源 |
| GND | GND | 地 |
使用方式
方式1:Web界面配置(推荐)
旋转编码器需要配置A/B两个信号。完整配置流程如下:
步骤1:登录设备管理页面
- 浏览器输入 ESP32 IP 地址
- 登录后导航到 外设配置
步骤2:添加旋转编码器外设
点击 添加外设 按钮
填写配置:
字段 填写示例 说明 外设ID encoder1唯一标识符 名称 旋转编码器显示名称 外设类型 旋转编码器 (type: 43) 双通道AB相 CLK引脚(A) 32A相信号 DT引脚(B) 33B相信号 每转脉冲数 20通常20 使用中断 true推荐使用 点击 保存
步骤3:验证配置
- 在外设列表中找到刚添加的外设
- 点击 测试 按钮
- 旋转编码器查看计数值变化
💡 提示:如果编码器有按键引脚(SW引脚),需单独配置为GPIO数字输入(type: 13)
方式2:JSON配置文件方式
将配置添加到 data/config/peripherals.json 的 peripherals 数组中:
{
"id": "encoder1",
"name": "旋转编码器",
"type": 43,
"enabled": false,
"pins": [32, 33],
"params": {
"resolution": 20,
"useInterrupt": true
}
}参数说明
| 参数 | 说明 |
|---|---|
| resolution | 每转脉冲数(通常 20) |
| useInterrupt | 是否使用中断模式(推荐 true) |
pins[0] = CLK(A相) 引脚,pins[1] = DT(B相) 引脚
API接口文档
读取编码器状态
// 函数签名
GPIOState PeripheralManager::readPin(const String& peripheralId);
// 使用示例
GPIOState state = pm.readPin("encoder_01");
// 返回值说明:
// - STATE_HIGH: 计数器非零(有旋转动作)
// - STATE_LOW: 计数器为零(无旋转或已重置)
// - STATE_UNDEFINED: 外设不存在或未初始化底层实现:
// 编码器:返回计数值(转换为GPIOState,HIGH表示非零计数)
if (config->type == PeripheralType::ENCODER) {
auto it = encoderCounters.find(peripheralId);
if (it == encoderCounters.end()) {
return GPIOState::STATE_UNDEFINED;
}
return it->second != 0 ? GPIOState::STATE_HIGH : GPIOState::STATE_LOW;
}重置编码器计数器
// 函数签名
bool PeripheralManager::writePin(const String& peripheralId, GPIOState state);
// 使用示例
pm.writePin("encoder_01", GPIOState::STATE_LOW); // 计数器归零
// 返回值:
// - true: 重置成功
// - false: 外设不存在底层实现:
// 编码器:支持重置计数器(写入LOW表示重置)
if (config->type == PeripheralType::ENCODER) {
if (state == GPIOState::STATE_LOW) {
encoderCounters[peripheralId] = 0;
LOG_INFOF("Peripheral Manager: Encoder '%s' counter reset to 0",
peripheralId.c_str());
}
return true;
}数据上报格式
编码器计数值通过 MQTT 上报:
[{"id": "encoder1", "value": "42"}]旋转编码器数据表示旋转角度累计值。
外设联动执行
Web界面配置步骤
编码器作为平台触发源
- 切换到 外设执行引擎 标签
- 点击 添加规则 按钮
- 配置平台触发条件:
- 触发类型:平台触发
- 触发源:encoder1
- 比较操作:大于
- 比较值:100
- 添加动作(如控制灯光、蜂鸣器等)
- 点击 保存
编码器按键作为事件触发源
- 将编码器SW引脚配置为GPIO数字输入(type: 13)
- 配置事件触发条件:
- 触发类型:事件触发
- 事件源:encoder1_btn
- 事件类型:button_click
- 添加动作
- 点击 保存
💡 提示:推荐使用中断模式而非轮询模式,能提高旋转响应速度
JSON配置示例
作为平台触发源资源
编码器计数值变化可触发联动:
{
"triggerType": 0,
"triggerPeriphId": "encoder1",
"operatorType": 2,
"compareValue": "100"
}编码器按键事件
将编码器 SW 引脚配置为GPIO数字输入(上拉),即可使用按键事件:
{
"id": "encoder1_btn",
"name": "编码器按键",
"type": 13,
"enabled": false,
"pins": [25],
"params": {
"initialState": 0,
"pwmChannel": 0,
"pwmFrequency": 1000,
"pwmResolution": 8,
"defaultDuty": 0
}
}测试覆盖
单元测试(7个)
| 测试用例 | 验证内容 |
|---|---|
| test_encoder_type_enum_value | 枚举值验证(type=43) |
| test_encoder_config_validation_valid | 有效配置验证 |
| test_encoder_config_validation_missing_pins | 引脚不足检测 |
| test_encoder_config_validation_zero_resolution | 分辨率=0检测 |
| test_encoder_data_transparency | 数据透传验证 |
| test_encoder_read_counter | 计数器读取功能 |
| test_encoder_reset_counter | 计数器重置功能 |
E2E测试(2个)
| 测试用例 | 验证内容 |
|---|---|
| test_e2e_encoder_peripheral_workflow | 编码器完整工作流程 |
| test_e2e_encoder_mqtt_integration | 编码器MQTT集成 |
集成测试
- ✅ MQTT联动触发
- ✅ 计数器读写完整流程
- ✅ 中断响应速度测试
- ✅ 多编码器实例并发测试
注意事项
- 中断模式:推荐使用中断模式,轮询模式在高频率旋转时可能丢失计数
- 上拉电阻:编码器通常需要上拉电阻确保信号稳定
- 引脚选择:确保使用支持中断的 GPIO 引脚
- 计数器溢出:长时间运行需注意计数值溢出,必要时在应用层处理
- 多编码器:系统支持多个编码器实例,每个独立计数
- 线程安全:中断处理函数使用IRAM_ATTR,确保响应速度
芯片兼容性
| 芯片型号 | GPIO中断 | PCNT硬件计数器 | 说明 |
|---|---|---|---|
| ESP32 | ✅ 支持 | ✅ 支持 | 可使用PCNT优化 |
| ESP32-S3 | ✅ 支持 | ✅ 支持 | 可使用PCNT优化 |
| ESP32-C3 | ✅ 支持 | ❌ 不支持 | 仅GPIO中断 |
| ESP32-C6 | ✅ 支持 | ❌ 不支持 | 仅GPIO中断 |
📌 说明:当前实现使用GPIO中断模式,兼容所有ESP32系列。未来可为ESP32/ESP32-S3添加PCNT硬件计数器支持。
