9.5 KiB
9.5 KiB
2026-03-29 工作日志
项目初始扫描
- 首次读取 firmware-2.7.15.567b8ea 代码结构
- 确认项目为 Meshtastic 官方固件的自定义 fork
- 发现 4 个自研板卡变体:esp32c3_moonshine / moonshine(带ADC/USB CDC)/ moonshine_mv(全功能)/ moonshine_travelers
- 详细记录写入 MEMORY.md
TCA9535PWR 矩阵键盘驱动实现
- 目标板卡:esp32c3_moonshine_travelers
- 新建驱动文件:
src/input/TCA9535ButtonThread.h- 驱动类声明,含寄存器宏、extern 指针src/input/TCA9535ButtonThread.cpp- 4×4 矩阵扫描实现
- 修改
variants/esp32c3/diy/esp32c3_moonshine_travelers/variant.h:- 添加
HAS_TCA9535_BUTTON宏 - 添加
TCA9535_KEY_MAP(4×4 矩阵行优先映射) TCA9535_INT_PIN=5(GPIO5,低电平有效,下降沿触发)
- 添加
- 修改
src/main.cpp:include TCA9535ButtonThread.h,在 setupModules() 后初始化线程 - I²C 地址:0x20(A0=A1=A2=0),与 SH1106 屏幕共用 Wire(SDA=0, SCL=1)
- 矩阵接法:P0.0
P0.3 行输出,P0.4P0.7 列输入,逐行拉低扫描,50µs 行间延时
LTO 链接错误修复
- 编译报
undefined reference to TCA9535ButtonThread::*(LTO -flto 导致) - 原因:.h/.cpp 中的
#if defined(HAS_TCA9535_BUTTON)守卫导致在部分编译单元中符号被丢弃 - 修复:去掉 .h/.cpp 中的条件守卫,让类定义和实现始终编译
- main.cpp 中的实例化和初始化仍由
#ifdef HAS_TCA9535_BUTTON控制 - extern 指针声明保留在
#ifdef中,非目标板卡不会引用
- main.cpp 中的实例化和初始化仍由
RadioLib LoRa RST 通过 TCA9535 P1.4 控制
- 自定义 HAL 子类
TCA9535GpioHal拦截虚拟引脚 200,转发到 I²C tca9535LoraReset(bool high)静态函数,read-modify-write P1.4
电源管理(P1.2 + P1.3)
- P1.2 = POWER_EN 输出,高电平有效,驱动 MOS 管维持供电
- P1.3 = POWER_BOOT 输入,低电平有效(按键按下接地)
- 开机流程:物理按键 → MOS 导通 → ESP32 得电 → init() 等 P1.3 持续按住 2 秒 → POWER_EN 拉高
- 未按够 2 秒松开 → 不拉高 POWER_EN → 断电
- 关机流程:运行中 P1.3 持续按住 2 秒 → POWER_EN 拉低 → 断电
- 电源状态机:BOOT_PENDING → RUNNING → SHUTDOWN_PENDING
- init() 中 P1 config = 0xEB(P1.2=输出, P1.3=输入, P1.4=输出)
- 新增
tca9535ReadPowerBoot()静态函数,读取 P1.3 输入状态 - 新增
tca9535PowerEn(bool on)静态函数,read-modify-write P1.2
GPS 启用(GP-02 模块)
- 问题:
tca9535GpsEn(false)在 TCA9535 init 中关闭了 GPS 电源,而 GPS::setup()->probe() 时 powerState=GPS_OFF,writePinEN(true) 只在 GPS_ACTIVE 时才调用 → probe 时模块没电 - 修复:在 main.cpp 的
createGps()+ enablePin 桥接之后,立即tca9535GpsEn(true)+delay(1000)给 GP-02 冷启动上电时间 - GP-02 接线:VCC→3.3V, GND→GND, TX→GPIO20(ESP RX), RX→GPIO21(ESP TX),EN/RST/PPS/VRTC 悬空即可
- 编译验证通过(esp32c3_moonshine_travelers SUCCESS)
GPS 修复:P1.6/P1.7 默认高电平 + RX/TX 交换
- 用户要求 GPS_RST(P1.6) 和 GPS_EN(P1.7) 通电后直接设为高电平
- TCA9535ButtonThread.cpp init() 中改为
tca9535GpsEn(true)和tca9535GpsReset(true) - 移除 main.cpp 中的 delay workaround
- TCA9535ButtonThread.cpp init() 中改为
- GPS 仍然检测不到 → 交换 GPS_RX_PIN(21→20) 和 GPS_TX_PIN(20→21)
- 更新 CHANGELOG.md,提交 311232c,push
九宫格键盘映射
- 添加 4×4 矩阵中的数字键:key0-2=1-3, key4-6=4-6, key8-10=7-9, key12=*, key13=0, key14=#
- 方向键保留:key3=UP, key7=DOWN, key11=LEFT, key15=RIGHT
- 新增
TCA9535_KEY_CHAR_MAP数组,dispatchEvent()传kbchar参数
⚠️ TCA9535 MATRIXKEY 崩溃修复 + 键盘输入修复
- 崩溃现象:按下矩阵键后立即 Load access fault at 0x40058766
- 根因:
TCA9535_KEY_CHAR_MAP传的是 ASCII 字符(如 '0'=0x30, '9'=0x39),但CannedMessageModule把kbchar当 1-based 索引用(currentMessageIndex = event->kbchar - 1)。ASCII '0'=48 → index=47 → 数组越界 → 野指针 → 崩溃 - 参考 RAK14004 实现:
kbI2cBase.cpp中PrintDataBuf = aCount*4 + bCount + 1传的是 1-16 的索引 - 第一版修复(错误方向):把 TCA9535_KEY_CHAR_MAP 改为传索引 1-12
- 问题:这样矩阵键只能选 canned message,无法进入文本输入
- 正确修复:
TCA9535_KEY_CHAR_MAP恢复为 ASCII 字符('0'-'9', '*', '#')CannedMessageModule::handleInputEvent()修改 MATRIXKEY 拦截逻辑:- 如果
kbchar是可打印 ASCII(32-126):不拦截,让事件 fall through 到正常输入路径 → INACTIVE 状态进入 FREETEXT,FREETEXT 状态追加字符 - 如果
kbchar是 1-based 索引(如 RAK14004):继续走 canned message 选择路径 - 加边界检查
idx < 0 || idx >= messagesCount防御性忽略
- 如果
P1.1 充电检测 (CHARGE_DET)
- P1.1 = CHARGE_DET 输入,高电平=正在充电
- P1 config 从 0x8B 改为 0x8D(P1.1 配置为输入)
- variant.h 新增
TCA9535_CHARGE_DET_PIN (1u << 1) - TCA9535ButtonThread.h 新增
tca9535ReadChargeDet()静态内联函数 - TCA9535ButtonThread.cpp 的 runOnce() 每 2 秒轮询 P1.1,更新全局
tca9535IsCharging - Power.cpp 的
AnalogBatteryLevel::isCharging()读取tca9535IsCharging(#ifdef TCA9535_CHARGE_DET_PIN) - Power.cpp include 条件从
TCA9535_LORA_RST_VIRTUAL_PIN改为HAS_TCA9535_BUTTON
快捷回复 ↔ 九宫格输入导航
- INACTIVE:UP/DOWN 进入快捷回复列表
- ACTIVE(列表):LEFT/RIGHT 进入九宫格 FREETEXT
- FREETEXT:LEFT/RIGHT 回列表,保留输入文字
*= 退格,#= 切换输入模式isUpEvent()/isDownEvent()移除 ACTIVE 状态对 LEFT/RIGHT 的映射
T9 Multi-tap 输入法实现
- 三种模式:abc(小写) / ABC(大写) / 123(数字),按
#循环切换 - 数字键 2-9 multi-tap 选字母,800ms 超时自动确认
- 数字键 0 = 空格,数字键 1 = 标点循环(
. , ! ?) - 屏幕左下角显示当前输入模式标签
- 光标位置实时预览 multi-tap 字符
⚠️ T9 commitMultiTap 递归崩溃修复
- 崩溃:
commitMultiTap()内调用runOnce()→runOnce()检测超时又调用commitMultiTap()→ 无限递归 → 栈溢出 → Store access fault - 反汇编确认 MEPC=0x40383d3e(systimer 中间件),S11=0x42066020 →
CannedMessageModule::runOnce() - 修复:添加
committingMultiTap重入保护标志,runOnce()中检查!committingMultiTap再调用commitMultiTap()
⚠️ T9 payload 残留 + 显示修复
- 连续按两次同一按键回到主页面:疑似 payload 未清零导致 runOnce 非预期行为 + showMultiTapPreview 无用 String 分配导致堆碎片
- 修复:
- 所有
runOnce()调用后立即payload = 0(*退格、可打印字符、INPUT_BROKER_BACK 退格、commitMultiTap) runOnce()FREETEXT 分支末尾加payload = 0作为最后防线- 移除
showMultiTapPreview()中无用previewTextString 分配(预览字符由 drawFrame 渲染)
- 所有
- 输入法模式标签(abc/ABC/123)从左上角移到屏幕右下角(TEXT_ALIGN_RIGHT)
⚠️ 任意按两个按键回到主页面 — REGENERATE_FRAMESET 重复触发 focus 丢失
- 根因:
commitMultiTap()→runOnce()→notifyObservers(REGENERATE_FRAMESET)→Screen::setFrames(FOCUS_MODULE)消费了requestFocus()→_requestingFocus=false然后同一调用链中后续的notifyObservers(REGENERATE_FRAMESET)→setFrames(FOCUS_MODULE)→focusedModule=255(默认值)→ui->switchToFrame(255)→ 跳到主页面 - 关键机制:
MeshModule::isRequestingFocus()是一次性的——调用一次就清零_requestingFocus。setFrames(FOCUS_MODULE)中focusedModule默认 255,没有 focus request 就跳第一帧 - 修复(4处):
commitMultiTap()末尾移除多余的notifyObservers(REGENERATE_FRAMESET)+forceDisplay()(runOnce()已触发)showMultiTapPreview()改用REDRAW_ONLY代替REGENERATE_FRAMESET#键(切换输入模式)commitMultiTap()后改用REDRAW_ONLYrunOnce()中 multi-tap 超时自动commitMultiTap()后直接 return,跳过后续notifyObservers- LEFT/RIGHT 返回 ACTIVE 列表时补加
requestFocus()(因为commitMultiTap已消费前一个 focus)
commitMultiTap()改为返回bool表示是否实际提交了字符- 编译验证通过(SUCCESS 33s)
⚠️ T9 输入法逻辑修正 — 光标+大小写+数字模式
- 问题1:光标不跳转:
drawFrame中 preview 字符插入到displayText但displayCursor没有 +1,导致光标_画在 preview 字符之前而不是之后。修复:有 preview 时displayCursor = cursor + 1 - 问题2:大小写模式下出现数字:旧
t9Map每个 key 的 index 0 是数字本身,在 UPPER/LOWER 模式下 multiTapIndex=0 时显示的是数字 - 问题3:0=空格、1=标点、数字只在数字模式输入:用户要求的行为
- 修复:
t9Map重命名为t9LetterMap,去掉 index 0 的数字,每个 key 只包含字母模式下的字符- 0:
{" ", nullptr}(空格) - 1:
{".", ",", "!", "?", nullptr}(标点) - 2-9: 纯字母
- 0:
- DIGIT 模式下数字键直接输出数字,不走 multi-tap 循环(
payload = '0' + key; runOnce()) - UPPER/LOWER 模式下使用
t9LetterMap+ multi-tap 循环,应用大小写转换 commitMultiTap()简化:不再有 index 0 是数字的特殊情况drawFramepreview 逻辑同步简化 + 光标 +1
- 编译验证通过(SUCCESS 36s)