feat(travelers): TCA9535 充电检测 + 键盘背光 + isVbusIn 修复

- 新增 P1.1 CHARGE_DET 充电检测(高电平=充电中),轮询间隔 2s
- Power.cpp isCharging()/isVbusIn() 均使用 TCA9535_CHARGE_DET_PIN 分支
- 新增 P1.0 键盘背光(高电平点亮),按键时亮,5s 无操作自动熄灭
- 修复开机供电维持:POWER_EN 在 Wire.begin() 后立即锁定
- 修复 P1 config 寄存器值 0x0A(之前 0x8D 导致 P1.2 高阻断电)
- ⚠️ 已知问题:TP4057 电压反串导致未充电时 P1.1 仍读高,需硬件修改
This commit is contained in:
2026-03-29 08:49:38 +08:00
parent 311232c9b9
commit e236c951cc
10 changed files with 435 additions and 101 deletions
+8 -3
View File
@@ -20,7 +20,7 @@
#include "meshUtils.h"
#include "sleep.h"
#ifdef TCA9535_LORA_RST_VIRTUAL_PIN
#ifdef HAS_TCA9535_BUTTON
#include "input/TCA9535ButtonThread.h"
#endif
@@ -445,7 +445,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
/// so we use EXT_PWR_DETECT GPIO pin to detect external power source
virtual bool isVbusIn() override
{
#ifdef EXT_PWR_DETECT
#ifdef TCA9535_CHARGE_DET_PIN
// 使用 TCA9535 CHARGE_DET 检测外部供电(高电平=充电中=有外部电源)
return tca9535IsCharging;
#elif defined(EXT_PWR_DETECT)
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
// if external powered that pin will be pulled down
if (digitalRead(EXT_PWR_DETECT) == LOW) {
@@ -472,7 +475,9 @@ class AnalogBatteryLevel : public HasBatteryLevel
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
}
#endif
#ifdef EXT_CHRG_DETECT
#ifdef TCA9535_CHARGE_DET_PIN
return tca9535IsCharging;
#elif defined(EXT_CHRG_DETECT)
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
#else
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
@@ -5,38 +5,67 @@
using namespace concurrency;
// 默认按键映射(4×4 矩阵,行优先:KEY[0]=ROW0·COL0 ... KEY[15]=ROW3·COL3
// 仅保留方向键,SELECT/CANCEL 由其他按键处理
// variant.h 中可用 #define TCA9535_KEY_MAP { ... } 覆盖
// -----------------------------------------------------------------------
#ifndef TCA9535_KEY_MAP
#define TCA9535_KEY_MAP \
{ \
INPUT_BROKER_NONE, /* key0 = ROW0·COL0 */ \
INPUT_BROKER_NONE, /* key1 = ROW0·COL1 */ \
INPUT_BROKER_NONE, /* key2 = ROW0·COL2 */ \
INPUT_BROKER_UP, /* key3 = ROW0·COL3 */ \
INPUT_BROKER_NONE, /* key4 = ROW1·COL0 */ \
INPUT_BROKER_NONE, /* key5 = ROW1·COL1 */ \
INPUT_BROKER_NONE, /* key6 = ROW1·COL2 */ \
INPUT_BROKER_DOWN, /* key7 = ROW1·COL3 */ \
INPUT_BROKER_NONE, /* key8 = ROW2·COL0 */ \
INPUT_BROKER_NONE, /* key9 = ROW2·COL1 */ \
INPUT_BROKER_NONE, /* key10 = ROW2·COL2 */ \
INPUT_BROKER_LEFT, /* key11 = ROW2·COL3 */ \
INPUT_BROKER_NONE, /* key12 = ROW3·COL0 */ \
INPUT_BROKER_NONE, /* key13 = ROW3·COL1 */ \
INPUT_BROKER_NONE, /* key14 = ROW3·COL2 */ \
INPUT_BROKER_RIGHT, /* key15 = ROW3·COL3 */ \
INPUT_BROKER_MATRIXKEY, /* key0 = ROW0·COL0 → '1' */ \
INPUT_BROKER_MATRIXKEY, /* key1 = ROW0·COL1 → '2' */ \
INPUT_BROKER_MATRIXKEY, /* key2 = ROW0·COL2 → '3' */ \
INPUT_BROKER_UP, /* key3 = ROW0·COL3 */ \
INPUT_BROKER_MATRIXKEY, /* key4 = ROW1·COL0 → '4' */ \
INPUT_BROKER_MATRIXKEY, /* key5 = ROW1·COL1 → '5' */ \
INPUT_BROKER_MATRIXKEY, /* key6 = ROW1·COL2 → '6' */ \
INPUT_BROKER_DOWN, /* key7 = ROW1·COL3 */ \
INPUT_BROKER_MATRIXKEY, /* key8 = ROW2·COL0 → '7' */ \
INPUT_BROKER_MATRIXKEY, /* key9 = ROW2·COL1 → '8' */ \
INPUT_BROKER_MATRIXKEY, /* key10 = ROW2·COL2 → '9' */ \
INPUT_BROKER_LEFT, /* key11 = ROW2·COL3 */ \
INPUT_BROKER_MATRIXKEY, /* key12 = ROW3·COL0 → '*' */ \
INPUT_BROKER_MATRIXKEY, /* key13 = ROW3·COL1 → '0' */ \
INPUT_BROKER_MATRIXKEY, /* key14 = ROW3·COL2 → '#' */ \
INPUT_BROKER_RIGHT, /* key15 = ROW3·COL3 */ \
}
#endif
// 默认按键字符映射(仅 INPUT_BROKER_MATRIXKEY 类型的按键使用)
// 传 ASCII 字符,CannedMessageModule 会根据 kbchar 走文本输入路径
// variant.h 中可用 #define TCA9535_KEY_CHAR_MAP { ... } 覆盖
#ifndef TCA9535_KEY_CHAR_MAP
#define TCA9535_KEY_CHAR_MAP \
{ \
'1', /* key0 = ROW0·COL0 */ \
'2', /* key1 = ROW0·COL1 */ \
'3', /* key2 = ROW0·COL2 */ \
0, /* key3 = ROW0·COL3 → 方向键,无字符 */ \
'4', /* key4 = ROW1·COL0 */ \
'5', /* key5 = ROW1·COL1 */ \
'6', /* key6 = ROW1·COL2 */ \
0, /* key7 = ROW1·COL3 → 方向键,无字符 */ \
'7', /* key8 = ROW2·COL0 */ \
'8', /* key9 = ROW2·COL1 */ \
'9', /* key10 = ROW2·COL2 */ \
0, /* key11 = ROW2·COL3 → 方向键,无字符 */ \
'*', /* key12 = ROW3·COL0 */ \
'0', /* key13 = ROW3·COL1 */ \
'#', /* key14 = ROW3·COL2 */ \
0, /* key15 = ROW3·COL3 → 方向键,无字符 */ \
}
#endif
static const input_broker_event tca9535KeyMap[TCA9535_KEY_COUNT] = TCA9535_KEY_MAP;
static const unsigned char tca9535KeyCharMap[TCA9535_KEY_COUNT] = TCA9535_KEY_CHAR_MAP;
// -----------------------------------------------------------------------
// 中断标志(ISR -> runOnce 通信,volatile,只做 set/clear
// -----------------------------------------------------------------------
static volatile bool tca9535IntPending = false;
#ifdef HAS_TCA9535_BUTTON
volatile bool tca9535IsCharging = false;
#endif
#ifdef TCA9535_INT_PIN
static void IRAM_ATTR tca9535ISR()
{
@@ -58,18 +87,23 @@ bool TCA9535ButtonThread::init()
{
// ===================================================================
// 第一步:配置 P1 口方向
// P1.2=输出(POWER_EN), P1.3=输入(POWER_BOOT), P1.4=输出(LoRa RST),
// P1.5=输出(状态灯), P1.6=输出(GPS RST), P1.7=输出(GPS EN)
// P1.0=输出(未用), P1.1=输入(CHARGE_DET), P1.2=输出(POWER_EN),
// P1.3=输入(POWER_BOOT), P1.4=输出(LoRa RST), P1.5=输出(状态灯),
// P1.6=输出(GPS RST), P1.7=输出(GPS EN)
// Configuration 寄存器:1=input, 0=output
// P1.2=0, P1.3=1, P1.4=0, P1.5=0, P1.6=0, P1.7=0 → 0x8B (1000 1011)
// bit: P1.7 P1.6 P1.5 P1.4 P1.3 P1.2 P1.1 P1.0
// 0 0 0 0 1 0 1 0 = 0x0A
// ===================================================================
if (!writeReg(TCA9535_REG_CONFIG_P1, 0x8B)) {
if (!writeReg(TCA9535_REG_CONFIG_P1, 0x0A)) {
LOG_WARN("TCA9535: P1 config write failed");
return false;
}
// P1.0 键盘背光默认熄灭(低电平)
tca9535Backlight(false);
// 确保 P1.4 输出高电平(LoRa RST 高 = 正常工作)
// 注意:此时不拉高 POWER_EN,等开机确认后再拉高
// 注意:POWER_EN 已由 main.cpp 在 Wire.begin() 后立即拉高,此处无需再操作
tca9535LoraReset(true);
// P1.5 状态灯默认熄灭(高电平)
@@ -82,54 +116,13 @@ bool TCA9535ButtonThread::init()
tca9535GpsEn(true);
// ===================================================================
// 第二步:开机检测 — 等待用户持续按住 P1.3 达 2 秒
// 物理按键已使 MOS 导通(ESP32 得电),但 POWER_EN 尚未拉高
// 用户必须持续按住 2 秒,否则 init() 返回 false → 系统不完成启动
// 第二步:POWER_EN 已由 main.cpp 在 Wire.begin() 后立即拉高(早期锁定)
// 此处只需确认状态机进入 RUNNING,不再需要等待 P1.3 按住 2 秒。
// 原因:系统从 Wire.begin() 到 tca9535ButtonThread::init() 之间需要
// 数秒的初始化时间(LoRa/WiFi/BLE/GPS 等),用户早已松开按键,
// 无法在此处等待。开机供电维持已在 main.cpp 最早期完成。
// ===================================================================
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)");
LOG_INFO("TCA9535: POWER_EN already latched in early boot, skipping boot-hold wait");
_powerState = TCA9535PowerState::RUNNING;
// ===================================================================
@@ -206,6 +199,28 @@ int32_t TCA9535ButtonThread::runOnce()
tca9535StatusLed(_statusLedOn);
}
// ===================================================================
// P1.0 键盘背光:有按键按下时点亮,5 秒无操作自动熄灭
// ===================================================================
if (_backlightOn && millis() - _backlightLastMs >= 5000) {
_backlightOn = false;
tca9535Backlight(false);
}
// ===================================================================
// 充电检测:轮询 P1.1 (CHARGE_DET),高电平=正在充电
// ===================================================================
#ifdef TCA9535_CHARGE_DET_PIN
if (millis() - _chargeDetLastMs >= 2000) {
_chargeDetLastMs = millis();
bool charging = tca9535ReadChargeDet();
if (charging != tca9535IsCharging) {
tca9535IsCharging = charging;
LOG_INFO("TCA9535: Charging %s", charging ? "DETECTED" : "STOPPED");
}
}
#endif
// ===================================================================
// 矩阵键盘扫描(仅 RUNNING 状态)
// ===================================================================
@@ -231,9 +246,17 @@ int32_t TCA9535ButtonThread::runOnce()
// 遍历所有键位,派发按下事件
for (uint8_t i = 0; i < TCA9535_KEY_COUNT; i++) {
if (pressed & (1u << i)) {
// 按键按下 → 点亮键盘背光(重置 5 秒计时)
if (!_backlightOn) {
_backlightOn = true;
_backlightLastMs = millis();
tca9535Backlight(true);
} else {
_backlightLastMs = millis(); // 已亮则刷新计时
}
input_broker_event evt = tca9535KeyMap[i];
if (evt != INPUT_BROKER_NONE) {
dispatchEvent(evt);
dispatchEvent(evt, tca9535KeyCharMap[i]);
}
}
}
@@ -308,12 +331,12 @@ bool TCA9535ButtonThread::readReg(uint8_t reg, uint8_t &val)
return true;
}
void TCA9535ButtonThread::dispatchEvent(input_broker_event evt)
void TCA9535ButtonThread::dispatchEvent(input_broker_event evt, unsigned char kbchar)
{
InputEvent e = {};
e.source = _originName;
e.inputEvent = evt;
e.kbchar = 0;
e.kbchar = kbchar;
e.touchX = 0;
e.touchY = 0;
this->notifyObservers(&e);
@@ -7,10 +7,12 @@
* - 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.1:充电检测输入(CHARGE_DET),高电平=正在充
* - P1.2:电源使能(POWER_EN),高电平有效,驱动 MOS 管维持供电(输出)
* - P1.3:电源开机按钮(POWER_BOOT),输入,低电平有效(按键按下接地)
* - P1.4LoRa RST 输出(通过 I²C 控制 RadioLib 复位序列)
* - P1.5:状态指示灯,低电平点亮
* P1 Config 寄存器 = 0x0AP1.1、P1.3 为输入,其余为输出)
*
* 电源管理逻辑:
* 开机:物理按键按下 → MOS 导通 → ESP32/TCA9535 得电
@@ -81,6 +83,7 @@
#define TCA9535_REG_CONFIG_P1 0x07
// P1 口引脚位掩码
#define TCA9535_BIT_P10 (1u << 0) // 键盘背光输出(高电平点亮)
#define TCA9535_BIT_P12 (1u << 2) // POWER_EN 输出
#define TCA9535_BIT_P13 (1u << 3) // POWER_BOOT 输入
#define TCA9535_BIT_P14 (1u << 4) // LoRa RST 输出
@@ -88,6 +91,24 @@
#define TCA9535_BIT_P16 (1u << 6) // GPS RST 输出
#define TCA9535_BIT_P17 (1u << 7) // GPS EN 输出(高电平有效)
#ifdef TCA9535_CHARGE_DET_PIN
/**
* 通过 I²C 读取 TCA9535 CHARGE_DETP1.1)输入状态。
* @return true=正在充电(高电平),false=未充电(低电平)
*/
static inline bool tca9535ReadChargeDet()
{
Wire.beginTransmission(TCA9535_I2C_ADDR);
Wire.write(TCA9535_REG_INPUT_P1);
if (Wire.endTransmission(false) != 0)
return false;
if (Wire.requestFrom((uint8_t)TCA9535_I2C_ADDR, (uint8_t)1) != 1)
return false;
uint8_t p1In = Wire.read();
return !!(p1In & TCA9535_CHARGE_DET_PIN);
}
#endif
/**
* 通过 I²C 控制 TCA9535 P1.2 上的电源使能(POWER_EN)。
* 高电平有效:控制 MOS 管维持系统供电。
@@ -191,6 +212,33 @@ static inline bool tca9535StatusLed(bool on)
return (Wire.endTransmission() == 0);
}
/**
* 通过 I²C 控制 TCA9535 P1.0 上的键盘背光。
* 高电平点亮,低电平熄灭。
* @param on true=点亮(高电平),false=熄灭(低电平)
*/
static inline bool tca9535Backlight(bool on)
{
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.0 位:高电平点亮
if (on)
p1Out |= TCA9535_BIT_P10; // 拉高 = 点亮
else
p1Out &= ~TCA9535_BIT_P10; // 拉低 = 熄灭
Wire.beginTransmission(TCA9535_I2C_ADDR);
Wire.write(TCA9535_REG_OUTPUT_P1);
Wire.write(p1Out);
return (Wire.endTransmission() == 0);
}
/**
* 通过 I²C 控制 TCA9535 P1.6 上的 GPS RST。
* @param high true=释放复位(高电平),false=触发复位(低电平)
@@ -288,6 +336,13 @@ class TCA9535ButtonThread : public Observable<const InputEvent *>, public concur
bool _statusLedOn = false;
uint32_t _statusLedToggleMs = 0;
// 充电检测轮询间隔
uint32_t _chargeDetLastMs = 0;
// P1.0 键盘背光控制(按键时点亮,5 秒无操作熄灭)
bool _backlightOn = false;
uint32_t _backlightLastMs = 0;
// 写寄存器
bool writeReg(uint8_t reg, uint8_t val);
@@ -298,10 +353,14 @@ class TCA9535ButtonThread : public Observable<const InputEvent *>, public concur
bool scanMatrix(uint16_t &keys);
// 派发事件到 InputBroker
void dispatchEvent(input_broker_event evt);
void dispatchEvent(input_broker_event evt, unsigned char kbchar = 0);
};
// 仅在 HAS_TCA9535_BUTTON 启用时导出全局指针声明
#ifdef HAS_TCA9535_BUTTON
extern TCA9535ButtonThread *tca9535ButtonThread;
// 全局充电检测状态,由 TCA9535ButtonThread::runOnce() 轮询更新
// 可被 Power 等外部模块读取(需 #ifdef TCA9535_CHARGE_DET_PIN 守卫)
extern volatile bool tca9535IsCharging;
#endif
@@ -584,6 +584,13 @@ void setup()
Wire.begin();
#elif defined(I2C_SDA) && !defined(ARCH_RP2040)
Wire.begin(I2C_SDA, I2C_SCL);
#ifdef HAS_TCA9535_BUTTON
// TCA9535 POWER_EN 必须在 I²C 初始化完成后立即拉高,否则用户松开按键后
// MOS 断电,系统在 setup() 中途就会掉电。此处无条件锁住供电,
// 后续 tca9535ButtonThread::init() 只负责键盘和状态机初始化。
tca9535PowerEn(true);
LOG_INFO("TCA9535: POWER_EN latched HIGH (early boot)");
#endif
#elif defined(ARCH_PORTDUINO)
if (portduino_config.i2cdev != "") {
LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str());
@@ -322,14 +322,27 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event))
return 1;
// Matrix keypad: If matrix key, trigger action select for canned message
// Matrix keypad: If matrix key with printable char, let it fall through to
// the normal input path (INACTIVE→FREETEXT or FREETEXT→append char).
// Only intercept as canned-message selector if kbchar is a valid 1-based index
// (e.g., RAK14004 sends kbchar=1..16).
if (event->inputEvent == INPUT_BROKER_MATRIXKEY) {
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
payload = INPUT_BROKER_MATRIXKEY;
currentMessageIndex = event->kbchar - 1;
lastTouchMillis = millis();
requestFocus();
return 1;
// Printable ASCII (32-126): treat as keyboard input, don't intercept
if (event->kbchar >= 32 && event->kbchar <= 126) {
// Fall through to normal input handling below
} else {
// 1-based index from hardware like RAK14004
int idx = event->kbchar - 1;
if (idx < 0 || idx >= messagesCount) {
return 0; // kbchar out of range, ignore
}
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
payload = INPUT_BROKER_MATRIXKEY;
currentMessageIndex = idx;
lastTouchMillis = millis();
requestFocus();
return 1;
}
}
// Always normalize navigation/select buttons for further handlers