From 609bee3c9d9f75f08da353affdfb449f33dfd5cb Mon Sep 17 00:00:00 2001 From: kevin Date: Sun, 29 Mar 2026 04:45:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(travelers):=20TCA9535=20=E7=9F=A9=E9=98=B5?= =?UTF-8?q?=E9=94=AE=E7=9B=98=E9=A9=B1=E5=8A=A8=20+=20=E7=94=B5=E6=BA=90?= =?UTF-8?q?=E7=AE=A1=E7=90=86=20+=20LoRa=20RST=20=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E5=BC=95=E8=84=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 TCA9535ButtonThread 驱动 (4x4 矩阵键盘, 中断/轮询模式) - 电源管理: P1.2 POWER_EN (MOS 供电) + P1.3 POWER_BOOT (2s 开/关机) - LoRa RST 通过 TCA9535 P1.4 + TCA9535GpioHal 虚拟引脚 200 控制 - 修复 LTO 链接错误 (去掉 .h/.cpp 中的 HAS_TCA9535_BUTTON 守卫) - 新增 CHANGELOG.md, 更新 readme.md (PCAL9535 升级计划) --- CHANGELOG.md | 84 +++++ .../src/input/TCA9535ButtonThread.cpp | 309 ++++++++++++++++++ .../src/input/TCA9535ButtonThread.h | 221 +++++++++++++ code/firmware-2.7.15.567b8ea/src/main.cpp | 52 +++ .../diy/esp32c3_moonshine_travelers/variant.h | 47 ++- code/readme.md | 10 +- 6 files changed, 721 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 code/firmware-2.7.15.567b8ea/src/input/TCA9535ButtonThread.cpp create mode 100644 code/firmware-2.7.15.567b8ea/src/input/TCA9535ButtonThread.h diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5d2cd69 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,84 @@ +# Changelog + +基于 [Meshtastic 官方固件 v2.7.15](https://github.com/meshtastic/firmware/tree/2.7.15) (commit `567b8ea`)。 + +格式参考 [Keep a Changelog](https://keepachangelog.com/)。 + +--- + +## [Unreleased] + +### Added + +#### CN 频段支持 +- 新增中国 CN 频段定义:470.0–510.0 MHz,100 信道,`SETTING_MAX_POWER` 宏保护默认 3 dBm +- 修改文件:`src/mesh/RadioInterface.cpp`(`RDEF(CN, ...)`) + +#### esp32c3_moonshine_travelers 旅行者版 — TCA9535PWR IO 扩展器驱动 +- 新增 `src/input/TCA9535ButtonThread.h` / `.cpp`:TCA9535PWR 4×4 矩阵键盘驱动 + - I²C 地址 0x20(A0=A1=A2=0),与 SH1106 屏幕共用 Wire 总线 + - P0.0~P0.3 行输出,P0.4~P0.7 列输入,逐行拉低扫描,50µs 行间延时 + - 支持中断模式(GPIO5 下降沿触发)和轮询模式 + - 默认按键映射:SELECT / UP / DOWN / LEFT / RIGHT / CANCEL(可通过 variant.h 覆盖) + - `src/input/TCA9535ButtonThread.h` — 类声明、寄存器宏定义、静态工具函数 + - `src/input/TCA9535ButtonThread.cpp` — 矩阵扫描、边沿检测、事件派发 + +#### 电源管理(P1.2 POWER_EN + P1.3 POWER_BOOT) +- P1.2 = POWER_EN 输出,高电平有效,驱动 MOS 管维持供电 + - `tca9535PowerEn(bool on)` — read-modify-write P1.2,static inline +- P1.3 = POWER_BOOT 输入,低电平有效(按键按下接地) + - `tca9535ReadPowerBoot()` — 读取 P1.3 状态,static inline +- 开机流程:物理按键 → MOS 导通 → ESP32 得电 → init() 检测 P1.3 持续按住 2 秒 → POWER_EN 拉高维持供电 + - 未按满 2 秒松开 → 不拉高 POWER_EN → MOS 断开 → 自动断电 +- 关机流程:运行中 P1.3 持续按住 2 秒 → 清空屏幕 → POWER_EN 拉低 → 用户松手后 MOS 断开断电 +- 电源状态机:`BOOT_PENDING` → `RUNNING` → `SHUTDOWN_PENDING` +- P1 口配置:`0xEB`(P1.2=输出, P1.3=输入, P1.4=输出) + +#### LoRa RST 通过 TCA9535 P1.4 控制 +- 新增 `TCA9535GpioHal` 自定义 HAL 子类(在 `src/main.cpp`) + - 继承 `LockingArduinoHal`,拦截虚拟引脚 200 的 `pinMode()` / `digitalWrite()` 转发到 I²C + - `LORA_RESET = TCA9535_LORA_RST_VIRTUAL_PIN(200)` + - RadioLib 的 `findChip()` / `reset()` 全链路通过 I²C 控制 P1.4 +- `tca9535LoraReset(bool high)` — read-modify-write P1.4,static inline + +#### variant.h 配置(esp32c3_moonshine_travelers) +- `HAS_TCA9535_BUTTON` — 启用 TCA9535 按键驱动 +- `TCA9535_INT_PIN 5` — 中断引脚 +- `TCA9535_POWER_EN_BIT (1u << 2)` — 电源使能位掩码 +- `TCA9535_KEY_MAP { ... }` — 4×4 矩阵按键映射 +- `TCA9535_LORA_RST_VIRTUAL_PIN 200` — LoRa RST 虚拟引脚 + +#### main.cpp 集成 +- `#ifdef HAS_TCA9535_BUTTON` 条件编译包含 TCA9535ButtonThread.h 并实例化 +- 在 `setupModules()` 后调用 `tca9535ButtonThread->init()` +- `#ifdef TCA9535_LORA_RST_VIRTUAL_PIN` 条件编译使用 `TCA9535GpioHal` 作为 RadioLib HAL + +### Changed + +- 无 + +### Fixed + +- **LTO 链接错误**:编译时 `-flto` 导致 `undefined reference to TCA9535ButtonThread::*` + - 原因:.h/.cpp 中的 `#if defined(HAS_TCA9535_BUTTON)` 守卫导致部分编译单元中符号被丢弃 + - 修复:去掉 .h/.cpp 中的条件守卫,类定义和实现始终编译;main.cpp 中的实例化仍由 `#ifdef` 控制 + +--- + +## TODO(未来计划) + +- [ ] **升级 IO 扩展器 TCA9535 → PCAL9535** + - 原因:TCA9535 无可配置内部上拉,矩阵键盘列线悬空易受电磁干扰 + - PCAL9535 pin-compatible,支持软件可配置上拉/下拉寄存器(0x41~0x46)、每引脚独立中断遮罩、可配置输出驱动强度 + - 替代方案:PCB 上列线加 10kΩ 外部上拉电阻 + +--- + +## 自定义板卡概览 + +| 板卡 | MCU | LoRa 模块 | 屏幕 | GPS | 按键输入 | 特殊功能 | +|------|-----|-----------|------|-----|----------|----------| +| esp32c3_moonshine | ESP32-C3 | E220-400M30S | 无 | 无 | BUTTON_PIN=9 | — | +| esp32c3_moonshine (fw) | ESP32-C3 | E220-400M30S / E22_400M33S | 无 | 无 | BUTTON_PIN=9 | USB CDC, 电池 ADC | +| esp32c3_moonshine_mv | ESP32-C3 | RA-01SC-P | SSD1306 | 有 | PCF8574 6 键 | NeoPixel, GPS EN | +| esp32c3_moonshine_travelers | ESP32-C3 | RA-01SC-P | SH1106 | 无 | TCA9535 4×4 矩阵 | 电源管理, LoRa RST via I²C | diff --git a/code/firmware-2.7.15.567b8ea/src/input/TCA9535ButtonThread.cpp b/code/firmware-2.7.15.567b8ea/src/input/TCA9535ButtonThread.cpp new file mode 100644 index 0000000..3cc03c5 --- /dev/null +++ b/code/firmware-2.7.15.567b8ea/src/input/TCA9535ButtonThread.cpp @@ -0,0 +1,309 @@ +#include "TCA9535ButtonThread.h" + +#include "main.h" + +using namespace concurrency; + +// ----------------------------------------------------------------------- +// 默认按键映射(4×4 矩阵,行优先:KEY[0]=ROW0·COL0 ... KEY[15]=ROW3·COL3) +// variant.h 中可用 #define TCA9535_KEY_MAP { ... } 覆盖 +// ----------------------------------------------------------------------- +#ifndef TCA9535_KEY_MAP +#define TCA9535_KEY_MAP \ + { \ + INPUT_BROKER_SELECT, /* ROW0·COL0 */ \ + INPUT_BROKER_UP, /* ROW0·COL1 */ \ + INPUT_BROKER_DOWN, /* ROW0·COL2 */ \ + INPUT_BROKER_LEFT, /* ROW0·COL3 */ \ + INPUT_BROKER_RIGHT, /* ROW1·COL0 */ \ + INPUT_BROKER_CANCEL, /* ROW1·COL1 */ \ + INPUT_BROKER_NONE, /* ROW1·COL2 */ \ + INPUT_BROKER_NONE, /* ROW1·COL3 */ \ + INPUT_BROKER_NONE, /* ROW2·COL0 */ \ + INPUT_BROKER_NONE, /* ROW2·COL1 */ \ + INPUT_BROKER_NONE, /* ROW2·COL2 */ \ + INPUT_BROKER_NONE, /* ROW2·COL3 */ \ + INPUT_BROKER_NONE, /* ROW3·COL0 */ \ + INPUT_BROKER_NONE, /* ROW3·COL1 */ \ + INPUT_BROKER_NONE, /* ROW3·COL2 */ \ + INPUT_BROKER_NONE, /* ROW3·COL3 */ \ + } +#endif + +static const input_broker_event tca9535KeyMap[TCA9535_KEY_COUNT] = TCA9535_KEY_MAP; + +// ----------------------------------------------------------------------- +// 中断标志(ISR -> runOnce 通信,volatile,只做 set/clear) +// ----------------------------------------------------------------------- +static volatile bool tca9535IntPending = false; + +#ifdef TCA9535_INT_PIN +static void IRAM_ATTR tca9535ISR() +{ + tca9535IntPending = true; +} +#endif + +// ----------------------------------------------------------------------- +// 构造 / 初始化 +// ----------------------------------------------------------------------- +TCA9535ButtonThread::TCA9535ButtonThread(const char *name, TwoWire *wire) + : OSThread(name), _wire(wire), _originName(name) +{ + if (inputBroker) + inputBroker->registerSource(this); +} + +bool TCA9535ButtonThread::init() +{ + // =================================================================== + // 第一步:配置 P1 口方向 + // P1.2 = 输出(POWER_EN),P1.3 = 输入(POWER_BOOT),P1.4 = 输出(LoRa RST) + // Configuration 寄存器:1=input, 0=output + // P1.2=bit2=0, P1.3=bit3=1, P1.4=bit4=0 → 0xEB (1110 1011) + // =================================================================== + if (!writeReg(TCA9535_REG_CONFIG_P1, 0xEB)) { + LOG_WARN("TCA9535: P1 config write failed"); + return false; + } + + // 确保 P1.4 输出高电平(LoRa RST 高 = 正常工作) + // 注意:此时不拉高 POWER_EN,等开机确认后再拉高 + tca9535LoraReset(true); + + // =================================================================== + // 第二步:开机检测 — 等待用户持续按住 P1.3 达 2 秒 + // 物理按键已使 MOS 导通(ESP32 得电),但 POWER_EN 尚未拉高 + // 用户必须持续按住 2 秒,否则 init() 返回 false → 系统不完成启动 + // =================================================================== + LOG_INFO("TCA9535: Waiting for power button hold (%d ms)...", TCA9535_POWER_BOOT_HOLD_MS); + + uint32_t holdStart = 0; + bool wasPressed = false; + + while (true) { + bool pressed = tca9535ReadPowerBoot(_wire); + + if (pressed && !wasPressed) { + // 按键刚按下,记录起始时间 + holdStart = millis(); + wasPressed = true; + } else if (!pressed && wasPressed) { + // 按键松开 — 检查是否按够时间 + uint32_t held = millis() - holdStart; + if (held >= TCA9535_POWER_BOOT_HOLD_MS) { + // 按够 2 秒,确认开机 + LOG_INFO("TCA9535: Power button held %lu ms -> boot confirmed", held); + break; + } else { + // 未按够,重新等待 + LOG_INFO("TCA9535: Power button released after %lu ms (need %d), waiting...", held, + TCA9535_POWER_BOOT_HOLD_MS); + wasPressed = false; + } + } else if (pressed && wasPressed) { + // 持续按住中,检查是否已达 2 秒(即使没松开也确认) + if ((millis() - holdStart) >= TCA9535_POWER_BOOT_HOLD_MS) { + LOG_INFO("TCA9535: Power button held >= %d ms -> boot confirmed", TCA9535_POWER_BOOT_HOLD_MS); + break; + } + } + + delay(TCA9535_POWER_BOOT_CHECK_MS); + } + + // =================================================================== + // 第三步:确认开机 → 拉高 POWER_EN 维持供电 + // =================================================================== + if (!tca9535PowerEn(true)) { + LOG_WARN("TCA9535: Failed to set POWER_EN high"); + return false; + } + LOG_INFO("TCA9535: POWER_EN set HIGH (system powered)"); + _powerState = TCA9535PowerState::RUNNING; + + // =================================================================== + // 第四步:配置 P0 口方向(矩阵键盘) + // =================================================================== + uint8_t configP0 = 0xF0; // P0.0~P0.3 输出(行),P0.4~P0.7 输入(列) + if (!writeReg(TCA9535_REG_CONFIG_P0, configP0)) { + LOG_WARN("TCA9535: P0 config write failed (addr=0x%02x)", TCA9535_I2C_ADDR); + return false; + } + + // 行输出初始状态:全部拉高(未选中任何行) + if (!writeReg(TCA9535_REG_OUTPUT_P0, TCA9535_ALL_ROWS_HIGH)) { + LOG_WARN("TCA9535: P0 output write failed"); + return false; + } + + // 极性不反转 + writeReg(TCA9535_REG_INVERT_P0, 0x00); + writeReg(TCA9535_REG_INVERT_P1, 0x00); + + // 读取初始状态,避免第一次扫描产生误报 + scanMatrix(_lastKeys); + +#ifdef TCA9535_INT_PIN + pinMode(TCA9535_INT_PIN, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(TCA9535_INT_PIN), tca9535ISR, FALLING); + LOG_INFO("TCA9535: INT on GPIO%d", TCA9535_INT_PIN); +#endif + + LOG_INFO("TCA9535 init OK (addr=0x%02x, matrix %dx%d, power=RUNNING)", TCA9535_I2C_ADDR, TCA9535_ROWS, + TCA9535_COLS); + return true; +} + +// ----------------------------------------------------------------------- +// 主循环:关机检测 → 矩阵扫描 → 边沿检测 → 事件派发 +// ----------------------------------------------------------------------- +int32_t TCA9535ButtonThread::runOnce() +{ + // =================================================================== + // 关机检测:运行中 P1.3 持续按住 2 秒 → 断电 + // =================================================================== + if (_powerState == TCA9535PowerState::RUNNING) { + bool pressed = tca9535ReadPowerBoot(_wire); + + if (pressed && _powerBtnPressStart == 0) { + // 按键刚按下,记录起始时间 + _powerBtnPressStart = millis(); + LOG_DEBUG("TCA9535: Shutdown button pressed, timing..."); + } else if (pressed && _powerBtnPressStart != 0) { + // 持续按住中 + uint32_t held = millis() - _powerBtnPressStart; + if (held >= TCA9535_POWER_BOOT_HOLD_MS) { + LOG_WARN("TCA9535: Shutdown button held %lu ms -> POWERING OFF", held); + _powerState = TCA9535PowerState::SHUTDOWN_PENDING; + + // 清空屏幕,给用户"即将关机"的视觉反馈 + // 按键仍在按着所以 MOS 还导通,系统不会立刻断电 + if (screen) + screen->setOn(false); + + tca9535PowerEn(false); + // 用户松手后 MOS 断开,系统断电 + return INT32_MAX; // 不再调度 + } + } else if (!pressed && _powerBtnPressStart != 0) { + // 按键松开,未达关机时间 + uint32_t held = millis() - _powerBtnPressStart; + LOG_DEBUG("TCA9535: Shutdown button released after %lu ms (no action)", held); + _powerBtnPressStart = 0; + } + } + + // =================================================================== + // 矩阵键盘扫描(仅 RUNNING 状态) + // =================================================================== +#ifdef TCA9535_INT_PIN + // 无中断挂起则跳过,节省 I²C 带宽 + if (!tca9535IntPending) + return TCA9535_POWER_BOOT_CHECK_MS; + tca9535IntPending = false; +#endif + + uint16_t currentKeys = 0x0000; + if (!scanMatrix(currentKeys)) { + LOG_WARN("TCA9535: scan failed"); + return 50; + } + + // 边沿检测:新按下(上升沿)和释放(下降沿) + uint16_t pressed = currentKeys & ~_lastKeys; // 新按下的键 + uint16_t released = ~currentKeys & _lastKeys; // 新释放的键(目前不处理) + + _lastKeys = currentKeys; + + // 遍历所有键位,派发按下事件 + for (uint8_t i = 0; i < TCA9535_KEY_COUNT; i++) { + if (pressed & (1u << i)) { + input_broker_event evt = tca9535KeyMap[i]; + if (evt != INPUT_BROKER_NONE) { + LOG_DEBUG("TCA9535: key[%u] (R%uC%u) pressed -> event %d", i, i / TCA9535_COLS, i % TCA9535_COLS, + (int)evt); + dispatchEvent(evt); + } + } + } + (void)released; // 目前仅处理按下边沿 + +#ifdef TCA9535_INT_PIN + return 20; // 中断模式:20ms 防抖窗口(每轮扫描间隔) +#else + return 50; // 轮询模式:50ms 扫描间隔 +#endif +} + +// ----------------------------------------------------------------------- +// 矩阵扫描:逐行拉低,读列状态 +// 返回 16 位,bit[i] = 1 表示 ROW(i/4)·COL(i%4) 被按下(低电平) +// ----------------------------------------------------------------------- +bool TCA9535ButtonThread::scanMatrix(uint16_t &keys) +{ + keys = 0x0000; + + for (uint8_t row = 0; row < TCA9535_ROWS; row++) { + // 拉低当前行,其余行保持高 + uint8_t outVal = TCA9535_ROW_MASK(row); + if (!writeReg(TCA9535_REG_OUTPUT_P0, outVal)) { + return false; + } + + // 短延时,等待电平稳定(行列电容充放电) + delayMicroseconds(50); + + // 读取 P0 输入寄存器 + uint8_t p0In = 0xFF; + if (!readReg(TCA9535_REG_INPUT_P0, p0In)) { + return false; + } + + // 提取列位(P0.4~P0.7),低电平=按下 + // 将列状态从高4位移到低位,便于索引 + uint8_t cols = (~(p0In & TCA9535_COL_MASK)) >> 4; // bit0=COL0, bit3=COL3 + + // 组装到 keys(每行 4 列) + keys |= ((uint16_t)cols << (row * TCA9535_COLS)); + } + + // 扫描完毕,恢复所有行高电平 + writeReg(TCA9535_REG_OUTPUT_P0, TCA9535_ALL_ROWS_HIGH); + + return true; +} + +// ----------------------------------------------------------------------- +// 私有:I²C 读写 +// ----------------------------------------------------------------------- +bool TCA9535ButtonThread::writeReg(uint8_t reg, uint8_t val) +{ + _wire->beginTransmission(TCA9535_I2C_ADDR); + _wire->write(reg); + _wire->write(val); + return (_wire->endTransmission() == 0); +} + +bool TCA9535ButtonThread::readReg(uint8_t reg, uint8_t &val) +{ + _wire->beginTransmission(TCA9535_I2C_ADDR); + _wire->write(reg); + if (_wire->endTransmission(false) != 0) + return false; + if (_wire->requestFrom((uint8_t)TCA9535_I2C_ADDR, (uint8_t)1) != 1) + return false; + val = _wire->read(); + return true; +} + +void TCA9535ButtonThread::dispatchEvent(input_broker_event evt) +{ + InputEvent e = {}; + e.source = _originName; + e.inputEvent = evt; + e.kbchar = 0; + e.touchX = 0; + e.touchY = 0; + this->notifyObservers(&e); +} diff --git a/code/firmware-2.7.15.567b8ea/src/input/TCA9535ButtonThread.h b/code/firmware-2.7.15.567b8ea/src/input/TCA9535ButtonThread.h new file mode 100644 index 0000000..9ffffc0 --- /dev/null +++ b/code/firmware-2.7.15.567b8ea/src/input/TCA9535ButtonThread.h @@ -0,0 +1,221 @@ +#pragma once + +/** + * TCA9535PWR I²C IO 扩展器 — 4×4 矩阵键盘 + 电源管理 + LoRa RST 控制 + * + * 硬件:TI TCA9535PWR + * - A0=0, A1=0, A2=0 → I²C 地址 0x20 + * - P0.0~P0.3:行输出(ROW0~ROW3),逐行拉低扫描 + * - P0.4~P0.7:列输入(COL0~COL3),读取按键状态 + * - P1.2:电源使能(POWER_EN),高电平有效,驱动 MOS 管维持供电 + * - P1.3:电源开机按钮(POWER_BOOT),输入,低电平有效(按键按下接地) + * - P1.4:LoRa RST 输出(通过 I²C 控制 RadioLib 复位序列) + * + * 电源管理逻辑: + * 开机:物理按键按下 → MOS 导通 → ESP32/TCA9535 得电 + * init() 读 P1.3,持续按住 2 秒 → tca9535PowerEn(true) 维持供电 + * 未按满 2 秒松开 → 不拉高 POWER_EN → MOS 断开 → 断电 + * 关机:运行中 P1.3 持续低电平 2 秒 → tca9535PowerEn(false) → 断电 + * + * 寄存器布局: + * 0x00 Input Port 0 (只读) + * 0x01 Input Port 1 + * 0x02 Output Port 0 (控制行输出电平) + * 0x03 Output Port 1 (P1.2 POWER_EN, P1.4 LoRa RST) + * 0x04 Polarity Inversion Port 0 + * 0x05 Polarity Inversion Port 1 + * 0x06 Configuration Port 0 (1=input, 0=output) + * 0x07 Configuration Port 1 + * + * 使用方式:在 variant.h 中定义以下宏,main.cpp 会自动初始化: + * #define HAS_TCA9535_BUTTON + * #define TCA9535_INT_PIN 5 // 中断引脚(可选,低电平有效) + * #define TCA9535_KEY_MAP { ... } // 4×4=16 元素的按键映射 + * #define TCA9535_LORA_RST_VIRTUAL_PIN 200 // LoRa RST 虚拟引脚号 + * #define LORA_RESET TCA9535_LORA_RST_VIRTUAL_PIN + */ + +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#include + +#ifndef TCA9535_I2C_ADDR +#define TCA9535_I2C_ADDR 0x20 // A0=A1=A2=0 +#endif + +// 矩阵行列数 +#ifndef TCA9535_ROWS +#define TCA9535_ROWS 4 +#endif +#ifndef TCA9535_COLS +#define TCA9535_COLS 4 +#endif +#define TCA9535_KEY_COUNT (TCA9535_ROWS * TCA9535_COLS) + +// 行输出掩码:P0.0~P0.3 = bit3~bit0 +// 扫描第 n 行时,将对应 bit 拉低(0),其余拉高(1) +#define TCA9535_ROW_MASK(r) (0xF0 | ~(1u << (r))) +#define TCA9535_ALL_ROWS_HIGH 0xF0 + +// 列输入掩码:P0.4~P0.7 = bit7~bit4 +#define TCA9535_COL_MASK 0xF0 + +// 电源管理常量 +#ifndef TCA9535_POWER_BOOT_HOLD_MS +#define TCA9535_POWER_BOOT_HOLD_MS 2000 // 开机/关机需持续按住的时间(毫秒) +#endif +#ifndef TCA9535_POWER_BOOT_CHECK_MS +#define TCA9535_POWER_BOOT_CHECK_MS 50 // P1.3 轮询间隔(毫秒) +#endif + +// TCA9535 寄存器定义 +#define TCA9535_REG_INPUT_P0 0x00 +#define TCA9535_REG_INPUT_P1 0x01 +#define TCA9535_REG_OUTPUT_P0 0x02 +#define TCA9535_REG_OUTPUT_P1 0x03 +#define TCA9535_REG_INVERT_P0 0x04 +#define TCA9535_REG_INVERT_P1 0x05 +#define TCA9535_REG_CONFIG_P0 0x06 +#define TCA9535_REG_CONFIG_P1 0x07 + +// P1 口引脚位掩码 +#define TCA9535_BIT_P12 (1u << 2) // POWER_EN 输出 +#define TCA9535_BIT_P13 (1u << 3) // POWER_BOOT 输入 +#define TCA9535_BIT_P14 (1u << 4) // LoRa RST 输出 + +/** + * 通过 I²C 控制 TCA9535 P1.2 上的电源使能(POWER_EN)。 + * 高电平有效:控制 MOS 管维持系统供电。 + * 此函数是 static 的,可被任意上下文调用(无需实例)。 + * @param on true=上电(高电平),false=断电(低电平) + */ +static inline bool tca9535PowerEn(bool on) +{ + // 读取当前 P1 输出寄存器值 + Wire.beginTransmission(TCA9535_I2C_ADDR); + Wire.write(TCA9535_REG_OUTPUT_P1); + if (Wire.endTransmission(false) != 0) + return false; + if (Wire.requestFrom((uint8_t)TCA9535_I2C_ADDR, (uint8_t)1) != 1) + return false; + uint8_t p1Out = Wire.read(); + + // 修改 P1.2 位 + if (on) + p1Out |= TCA9535_BIT_P12; // 拉高 = 上电 + else + p1Out &= ~TCA9535_BIT_P12; // 拉低 = 断电 + + // 写回 + Wire.beginTransmission(TCA9535_I2C_ADDR); + Wire.write(TCA9535_REG_OUTPUT_P1); + Wire.write(p1Out); + return (Wire.endTransmission() == 0); +} + +/** + * 通过 I²C 读取 TCA9535 P1.3(POWER_BOOT)输入状态。 + * @return true=按键按下(低电平),false=按键松开(高电平) + */ +static inline bool tca9535ReadPowerBoot(TwoWire *wire = &Wire) +{ + wire->beginTransmission(TCA9535_I2C_ADDR); + wire->write(TCA9535_REG_INPUT_P1); + if (wire->endTransmission(false) != 0) + return false; // I²C 错误时返回 false(未按下) + if (wire->requestFrom((uint8_t)TCA9535_I2C_ADDR, (uint8_t)1) != 1) + return false; + uint8_t p1In = wire->read(); + // P1.3 低电平 = 按下 + return !(p1In & TCA9535_BIT_P13); +} + +/** + * 通过 I²C 控制 TCA9535 P1.4 上的 LoRa RST。 + * 此函数是 static 的,可被自定义 HAL 在任意上下文调用(无需实例)。 + * @param high true=释放复位(高电平),false=触发复位(低电平) + */ +static inline bool tca9535LoraReset(bool high) +{ + // 读取当前 P1 输出寄存器值 + Wire.beginTransmission(TCA9535_I2C_ADDR); + Wire.write(TCA9535_REG_OUTPUT_P1); + if (Wire.endTransmission(false) != 0) + return false; + if (Wire.requestFrom((uint8_t)TCA9535_I2C_ADDR, (uint8_t)1) != 1) + return false; + uint8_t p1Out = Wire.read(); + + // 修改 P1.4 位 + if (high) + p1Out |= TCA9535_BIT_P14; // 拉高 = 释放复位 + else + p1Out &= ~TCA9535_BIT_P14; // 拉低 = 触发复位 + + // 写回 + Wire.beginTransmission(TCA9535_I2C_ADDR); + Wire.write(TCA9535_REG_OUTPUT_P1); + Wire.write(p1Out); + return (Wire.endTransmission() == 0); +} + +/** + * 电源管理状态机 + */ +enum class TCA9535PowerState : uint8_t { + BOOT_PENDING, // 等待开机确认(init 阶段,检测 P1.3 是否按满 2 秒) + RUNNING, // 正常运行,POWER_EN 已拉高 + SHUTDOWN_PENDING, // 关机倒计时(P1.3 持续按住中) +}; + +class TCA9535ButtonThread : public Observable, public concurrency::OSThread +{ + public: + explicit TCA9535ButtonThread(const char *name, TwoWire *wire = &Wire); + + /** + * 初始化 TCA9535 并执行开机检测: + * 1. 配置 P1 口方向(P1.2/P1.4 输出,P1.3 输入) + * 2. 等待检测 P1.3 是否持续按住 2 秒 + * 3. 如果是 → 拉高 POWER_EN,继续初始化键盘 + * 4. 如果否 → 不拉高 POWER_EN(MOS 断开 → 断电) + * 5. 配置 P0 口方向和矩阵键盘 + * @return true 如果开机确认成功且键盘初始化完成 + */ + bool init(); + + /// 当前电源状态 + TCA9535PowerState powerState() const { return _powerState; } + + protected: + int32_t runOnce() override; + + private: + TwoWire *_wire; + const char *_originName; + + // 电源管理 + TCA9535PowerState _powerState = TCA9535PowerState::BOOT_PENDING; + uint32_t _powerBtnPressStart = 0; // 按键按下时刻 (millis) + + // 上次扫描结果(16 位,每 bit 对应 row*4+col),用于边沿检测 + uint16_t _lastKeys = 0x0000; + + // 写寄存器 + bool writeReg(uint8_t reg, uint8_t val); + + // 读寄存器 + bool readReg(uint8_t reg, uint8_t &val); + + // 扫描矩阵一次,返回 16 位按键状态(bit=1 表示按下) + bool scanMatrix(uint16_t &keys); + + // 派发事件到 InputBroker + void dispatchEvent(input_broker_event evt); +}; + +// 仅在 HAS_TCA9535_BUTTON 启用时导出全局指针声明 +#ifdef HAS_TCA9535_BUTTON +extern TCA9535ButtonThread *tca9535ButtonThread; +#endif diff --git a/code/firmware-2.7.15.567b8ea/src/main.cpp b/code/firmware-2.7.15.567b8ea/src/main.cpp index 8fec629..1d67306 100644 --- a/code/firmware-2.7.15.567b8ea/src/main.cpp +++ b/code/firmware-2.7.15.567b8ea/src/main.cpp @@ -141,6 +141,43 @@ AudioThread *audioThread = nullptr; ExtensionIOXL9555 io; #endif +#ifdef HAS_TCA9535_BUTTON +#include "input/TCA9535ButtonThread.h" +TCA9535ButtonThread *tca9535ButtonThread = nullptr; +#endif + +// --------------------------------------------------------------------------- +// 自定义 HAL:拦截 TCA9535 虚拟引脚的 GPIO 操作,转发到 I²C +// 用于 LoRa RST 通过 TCA9535 P1.4 控制 +// --------------------------------------------------------------------------- +#ifdef TCA9535_LORA_RST_VIRTUAL_PIN +#include + +class TCA9535GpioHal : public LockingArduinoHal +{ + public: + TCA9535GpioHal(SPIClass &spi, SPISettings spiSettings) : LockingArduinoHal(spi, spiSettings) {} + + void pinMode(uint32_t pin, uint32_t mode) override + { + if (pin == TCA9535_LORA_RST_VIRTUAL_PIN) { + // P1.4 已由 TCA9535 驱动配置为输出,无需重复操作 + return; + } + LockingArduinoHal::pinMode(pin, mode); + } + + void digitalWrite(uint32_t pin, uint32_t value) override + { + if (pin == TCA9535_LORA_RST_VIRTUAL_PIN) { + tca9535LoraReset(value == GpioLevelHigh); + return; + } + LockingArduinoHal::digitalWrite(pin, value); + } +}; +#endif + #if HAS_TFT extern void tftSetup(void); #endif @@ -953,6 +990,19 @@ void setup() // Now that the mesh service is created, create any modules setupModules(); +#ifdef HAS_TCA9535_BUTTON + // TCA9535PWR I²C IO 扩展器按键驱动 + // Wire 已在 I2C 初始化段完成 begin(),inputBroker 已在 setupModules() 中创建 + tca9535ButtonThread = new TCA9535ButtonThread("TCA9535Btn"); + if (!tca9535ButtonThread->init()) { + LOG_WARN("TCA9535 init failed, disabling button thread"); + delete tca9535ButtonThread; + tca9535ButtonThread = nullptr; + } else { + LOG_INFO("TCA9535 button thread started"); + } +#endif + #if !MESHTASTIC_EXCLUDE_I2C // Inform modules about I2C devices ScanI2CCompleted(i2cScanner.get()); @@ -1219,6 +1269,8 @@ void setup() #elif defined(HW_SPI1_DEVICE) LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); +#elif defined(TCA9535_LORA_RST_VIRTUAL_PIN) + TCA9535GpioHal *RadioLibHAL = new TCA9535GpioHal(SPI, spiSettings); #else // HW_SPI1_DEVICE LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); #endif diff --git a/code/firmware-2.7.15.567b8ea/variants/esp32c3/diy/esp32c3_moonshine_travelers/variant.h b/code/firmware-2.7.15.567b8ea/variants/esp32c3/diy/esp32c3_moonshine_travelers/variant.h index 436c701..12259bd 100644 --- a/code/firmware-2.7.15.567b8ea/variants/esp32c3/diy/esp32c3_moonshine_travelers/variant.h +++ b/code/firmware-2.7.15.567b8ea/variants/esp32c3/diy/esp32c3_moonshine_travelers/variant.h @@ -18,6 +18,45 @@ #define ADC_CHANNEL ADC1_GPIO2_CHANNEL #define ADC_MULTIPLIER 2.0f +// ----------------------------------------------------------------------- +// TCA9535PWR I²C IO 扩展器 — 4×4 矩阵键盘 + 电源控制 + LoRa RST +// - 与屏幕共用 I²C 总线 (SDA=0, SCL=1) +// - A0=0, A1=0, A2=0 → 地址 0x20 (TCA9535_I2C_ADDR) +// - P0.0~P0.3 行输出(ROW0~ROW3),P0.4~P0.7 列输入(COL0~COL3) +// - P1.2 电源使能(POWER_EN),高电平有效,驱动 MOS 管维持供电 +// - P1.3 电源开机按钮(POWER_BOOT),输入,低电平有效 +// 开机:持续按住 2 秒 → POWER_EN 拉高维持供电 +// 关机:运行中持续按住 2 秒 → POWER_EN 拉低断电 +// - P1.4 LoRa RST 输出(通过 I²C 控制 RadioLib 复位序列) +// - 中断引脚 GPIO5,低电平有效,下降沿触发 +// ----------------------------------------------------------------------- +#define HAS_TCA9535_BUTTON +#define TCA9535_INT_PIN 5 // TCA9535 INT → GPIO5(低电平有效,下降沿触发) +#define TCA9535_POWER_EN_BIT (1u << 2) // P1.2 = 电源使能(高电平=开机) + +// 按键映射:4×4 矩阵,行优先排列 +// KEY[0]=ROW0·COL0, KEY[1]=ROW0·COL1, ..., KEY[15]=ROW3·COL3 +// 低电平有效(按下接地,列读取到低电平=按下) +#define TCA9535_KEY_MAP \ + { \ + INPUT_BROKER_SELECT, /* ROW0·COL0 */ \ + INPUT_BROKER_UP, /* ROW0·COL1 */ \ + INPUT_BROKER_DOWN, /* ROW0·COL2 */ \ + INPUT_BROKER_LEFT, /* ROW0·COL3 */ \ + INPUT_BROKER_RIGHT, /* ROW1·COL0 */ \ + INPUT_BROKER_CANCEL, /* ROW1·COL1 */ \ + INPUT_BROKER_NONE, /* ROW1·COL2 */ \ + INPUT_BROKER_NONE, /* ROW1·COL3 */ \ + INPUT_BROKER_NONE, /* ROW2·COL0 */ \ + INPUT_BROKER_NONE, /* ROW2·COL1 */ \ + INPUT_BROKER_NONE, /* ROW2·COL2 */ \ + INPUT_BROKER_NONE, /* ROW2·COL3 */ \ + INPUT_BROKER_NONE, /* ROW3·COL0 */ \ + INPUT_BROKER_NONE, /* ROW3·COL1 */ \ + INPUT_BROKER_NONE, /* ROW3·COL2 */ \ + INPUT_BROKER_NONE, /* ROW3·COL3 */ \ + } + #define RA_01SC_P @@ -31,12 +70,18 @@ #define USE_SX1262 #define USE_SX1268 +// LoRa RST 通过 TCA9535 P1.4 控制 +// 使用虚拟引脚号 200,由自定义 HAL 拦截并转发到 I²C +#define TCA9535_LORA_RST_VIRTUAL_PIN 200 +#define TCA9535_LORA_RST_REG TCA9535_REG_OUTPUT_P1 // P1 输出寄存器 +#define TCA9535_LORA_RST_BIT (1u << 4) // P1.4 + #define LORA_SCK 10 #define LORA_MISO 6 #define LORA_MOSI 7 #define LORA_CS 8 #define LORA_DIO0 RADIOLIB_NC -#define LORA_RESET RADIOLIB_NC +#define LORA_RESET TCA9535_LORA_RST_VIRTUAL_PIN #define LORA_DIO1 3 #define LORA_DIO2 RADIOLIB_NC #define LORA_BUSY 4 diff --git a/code/readme.md b/code/readme.md index be71783..8eafdf2 100644 --- a/code/readme.md +++ b/code/readme.md @@ -5,4 +5,12 @@ + #ifndef SETTING_MAX_POWER #define SETTING_MAX_POWER 3 #endif -+ RDEF(CN, 470.0f, 510.0f, 100, 0, SETTING_MAX_POWER, true, false, false), \ No newline at end of file ++ RDEF(CN, 470.0f, 510.0f, 100, 0, SETTING_MAX_POWER, true, false, false), + +# TODO: 升级 IO 扩展器 TCA9535 → PCAL9535 + +- 原因:TCA9535 无可配置内部上拉电阻,矩阵键盘列线(P0.4~P0.7)悬空易受电磁干扰误触发 +- PCAL9535 pin-compatible,增加可配置上拉/下拉寄存器(0x41~0x46),软件可控内部上拉 +- PCAL9535 还支持每引脚独立中断遮罩、可配置输出驱动强度 +- 影响文件:TCA9535ButtonThread.h/.cpp 中的 init() 需追加上拉配置寄存器写入 +- 备注:如不改芯片,可在 PCB 上列线加 10kΩ 外部上拉电阻作为替代方案 \ No newline at end of file