diff --git a/build.sh b/build.sh index 97eefca..d1ced24 100644 --- a/build.sh +++ b/build.sh @@ -45,7 +45,6 @@ ESP32_boards=( heltec_wifi_lora32_v43/esp32s3/procpu heltec_wireless_tracker/esp32s3/procpu heltec_wireless_tracker_v2/esp32s3/procpu - thinknode_m9/esp32s3/procpu ttgo_tbeam/esp32/procpu ) diff --git a/gen_provider_catalog.py b/gen_provider_catalog.py index 62d587f..4a5bda6 100644 --- a/gen_provider_catalog.py +++ b/gen_provider_catalog.py @@ -163,7 +163,10 @@ BOARDS = [ dict(stem="heltec_wifi_lora32_v4-esp32s3-procpu", kind="esp32", device="Heltec v4"), dict(stem="heltec_wireless_tracker-esp32s3-procpu", kind="esp32", device="Heltec Wireless Tracker"), dict(stem="heltec_wireless_tracker_v2-esp32s3-procpu", kind="esp32", device="Heltec Wireless Tracker v2"), - dict(stem="thinknode_m9-esp32s3-procpu", kind="esp32", device="Elecrow ThinkNode M9"), + # ThinkNode M9 pulled from the build.sh release matrix 2026-07-22 (bring-up + # still in progress — keypad/GPS/battery unverified on hardware). Restore + # this line together with the build.sh entry once it's release-ready. + # dict(stem="thinknode_m9-esp32s3-procpu", kind="esp32", device="Elecrow ThinkNode M9"), # Classic ESP32 T-Beam: ships -merged.bin (full-flash, 0x0) too. dict(stem="ttgo_tbeam-esp32-procpu", kind="esp32", device="LilyGo T-Beam (SX1262)"), diff --git a/zephcore/boards/esp32/thinknode_m9/board.overlay b/zephcore/boards/esp32/thinknode_m9/board.overlay index 8076254..8b2769d 100644 --- a/zephcore/boards/esp32/thinknode_m9/board.overlay +++ b/zephcore/boards/esp32/thinknode_m9/board.overlay @@ -240,7 +240,10 @@ /* LittleFS auto-mount — standard /lfs mount point */ #include "../../common/filesystem.dtsi" -/* USB OTG companion transport. thinknode_m9 consoles over USB Serial/JTAG, - * which shares D+/D- with USB OTG, so the console is rerouted to uart0. */ +/* Console + shell on uart0 — the USB-C port is a CH9102-class UART bridge + * into UART0 (GPIO43/44), verified on hardware. No USB OTG transport on this + * board: the S3's native D+/D- pads (GPIO19/20) are not bonded to the + * connector, and GPIO20/21 carry the STC8H keypad I2C bus — enabling the OTG + * or USB Serial/JTAG PHY would claim the keypad's SDA pin. BLE is the + * companion transport. */ #include "../../common/esp32s3_console_uart0.dtsi" -#include "../../common/esp32s3_usb_otg.dtsi" diff --git a/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts b/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts index 937f57c..55b5aad 100644 --- a/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts +++ b/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts @@ -30,10 +30,15 @@ model = "Elecrow ThinkNode M9"; compatible = "elecrow,thinknode-m9"; + /* No zephyr,console here: the M9's USB-C data lines ride a CH9102-class + * USB-UART bridge into UART0 (verified on hardware — the native USB + * Serial/JTAG console produced no output). The S3's native USB pads + * (GPIO19/20) are not wired to the connector at all; GPIO20/21 carry the + * STC8H keypad I2C bus instead. Every build of this board (app overlay + * and lr1110_updater overlay alike) routes console + shell to uart0 via + * boards/common/esp32s3_console_uart0.dtsi. */ chosen { zephyr,sram = &sram1; - zephyr,console = &usb_serial; - zephyr,shell-uart = &usb_serial; zephyr,flash = &flash0; zephyr,code-partition = &slot0_partition; zephyr,bt-hci = &esp32_bt_hci; @@ -69,9 +74,11 @@ clock-frequency = ; }; -&usb_serial { - status = "okay"; -}; +/* USB Serial/JTAG stays disabled (SoC default): its PHY pads are GPIO19/20, + * and on this board GPIO20 is the keypad's I2C1 SDA. The USJ driver's init + * re-attaches the USB pad (usb_serial_jtag_ll_phy_enable_pad) and enables the + * D+ pull-up, fighting the keypad bus. Native USB is unbonded from the + * connector anyway — see the chosen-node comment. */ &i2c0 { status = "okay"; diff --git a/zephcore/dts/bindings/input/zephcore,stc8h-keypad.yaml b/zephcore/dts/bindings/input/zephcore,stc8h-keypad.yaml index 0026193..d7f326d 100644 --- a/zephcore/dts/bindings/input/zephcore,stc8h-keypad.yaml +++ b/zephcore/dts/bindings/input/zephcore,stc8h-keypad.yaml @@ -8,13 +8,15 @@ description: | over I2C. Used by the Elecrow ThinkNode M9, whose keypad is its only input device — there is no user button on that board. - Protocol: - reg 0x01..0x04 battery millivolts, little-endian - reg 0x05 code of the key currently pressed (0 = none) + Protocol (as recovered from the stock keypad firmware — note the key + register is 0x01, not the 0x05 "matrix key" register that the reference + header documents; that define is dead code and 0x05 reads as 0xFF): + reg 0x01 code of the last key pressed (latched; 0x88 = invalid) + reg 0x01..0x04 battery millivolts, little-endian (same base register) reg 0x06 state; writing 0x01 requests sleep The MCU raises its interrupt line (idle-low, rising edge) when a key is - pressed. The driver reads register 0x05 on that edge and reports the result + pressed. The driver reads register 0x01 on that edge and reports the result through Zephyr's input subsystem. If no interrupt GPIO is supplied the driver falls back to polling. diff --git a/zephcore/helpers/input/stc8h_keypad.c b/zephcore/helpers/input/stc8h_keypad.c index 77be9f0..92a6428 100644 --- a/zephcore/helpers/input/stc8h_keypad.c +++ b/zephcore/helpers/input/stc8h_keypad.c @@ -22,13 +22,12 @@ * helpers/ui-joystick/joystick_defs.h). Everything else is a named key and is * translated here. * - * The named codes below are the ones confirmed from two independent firmwares - * for this keypad. Arrow keys are NOT among them, and no reachable reference - * documents them — so any code this driver does not recognise is reported at - * INFO level with its hex value. Press the arrow keys once on real hardware - * and the log names them; add them to key_map[] and navigation is complete. - * That is deliberate: guessing arrow codes would produce a driver that looks - * finished and silently does the wrong thing. + * The named codes below (arrows included) come from the stock keypad + * firmware's protocol, as recovered from a linked reference-firmware ELF by a + * second independent firmware — the register bodies there are the shipped + * hardware truth, not header guesses. Any code this driver still does not + * recognise is reported at INFO level with its hex value so it can be added + * to key_map[]. */ #define DT_DRV_COMPAT zephcore_stc8h_keypad @@ -45,13 +44,23 @@ #include LOG_MODULE_REGISTER(stc8h_keypad, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); -/* Register map — see dts/bindings/input/zephcore,stc8h-keypad.yaml */ +/* Register map — see dts/bindings/input/zephcore,stc8h-keypad.yaml. + * + * The key register is 0x01, NOT the 0x05 "matrix key" register that the + * reference header documents: the recovered implementation bodies read the + * key from 0x01, and the 0x05 define is dead code nothing consumes. Reading + * 0x05 returns constant 0xFF — that was this driver's original bug (keys + * fully dead, phantom 0xFF at init). The overlap with the battery registers + * (0x01..0x04 little-endian) is odd but is what the shipped firmware does; + * it is also why key reads are IRQ-gated below. */ #define STC8H_REG_BATTERY 0x01 /* 0x01..0x04, little-endian millivolts */ -#define STC8H_REG_KEY 0x05 +#define STC8H_REG_KEY 0x01 #define STC8H_REG_STATE 0x06 #define STC8H_STATE_SLEEP 0x01 -#define STC8H_KEY_NONE 0x00 +#define STC8H_KEY_NONE 0x00 +#define STC8H_KEY_INVALID 0x88 /* helper MCU's "invalid key" marker */ +#define STC8H_KEY_ALL_ONES 0xFF /* error sentinel / floating-bus read */ struct stc8h_config { struct i2c_dt_spec i2c; @@ -63,22 +72,27 @@ struct stc8h_data { const struct device *dev; struct gpio_callback irq_cb; struct k_work_delayable work; + atomic_t irq_pending; uint8_t last_key; }; /* - * Named keys, as reported by the helper MCU. - * - * Only codes corroborated by two independent firmwares for this keypad are - * listed. The two function keys are mapped to page navigation because on a - * board with no other input they are the only way to move between screens; - * the UI treats them as prev/next. + * Named keys, as reported by the helper MCU. Codes from the recovered stock + * keypad protocol (see file header). The two function keys are mapped to page + * navigation because on a board with no other input they are the only way to + * move between screens; the UI treats them as prev/next. */ static const struct { uint8_t raw; uint16_t code; } key_map[] = { { 0x0D, INPUT_KEY_ENTER }, /* enter / confirm */ + { 0xB5, INPUT_KEY_UP }, /* arrow up */ + { 0xB6, INPUT_KEY_DOWN }, /* arrow down */ + { 0xB4, INPUT_KEY_LEFT }, /* arrow left */ + { 0xB7, INPUT_KEY_RIGHT }, /* arrow right */ + { 0x08, INPUT_KEY_BACK }, /* del -> back / cancel */ + { 0x89, INPUT_KEY_BACK }, /* del long-press -> back / cancel */ { 0x86, INPUT_KEY_ESC }, /* back */ { 0x82, INPUT_KEY_HOME }, /* home */ { 0x83, INPUT_KEY_MENU }, /* context menu */ @@ -148,19 +162,44 @@ static void stc8h_report(const struct device *dev, uint8_t raw) LOG_INF("unmapped keypad code 0x%02X — add it to key_map[] to bind it", raw); } +static bool stc8h_key_valid(uint8_t raw) +{ + return raw != STC8H_KEY_NONE && raw != STC8H_KEY_INVALID && + raw != STC8H_KEY_ALL_ONES; +} + static void stc8h_work_handler(struct k_work *work) { struct k_work_delayable *dwork = k_work_delayable_from_work(work); struct stc8h_data *data = CONTAINER_OF(dwork, struct stc8h_data, work); const struct device *dev = data->dev; const struct stc8h_config *cfg = dev->config; + bool irq_event = atomic_cas(&data->irq_pending, 1, 0); uint8_t key = STC8H_KEY_NONE; if (stc8h_read_reg(cfg, STC8H_REG_KEY, &key) == 0) { - /* Report on the transition only — the register reads the key - * that is *currently down*, so a held key would otherwise - * repeat at the poll rate. */ - if (key != STC8H_KEY_NONE && key != data->last_key) { + if (irq_event) { + /* KB_INT edge latched: this read is a fresh keypress — + * report unconditionally. The register latches the + * *last* key (it does not return to 0 on release), so + * comparing against last_key here would swallow every + * repeated press of the same key. This mirrors the + * reference driver, which only ever reads the key + * register in response to the interrupt. */ + if (stc8h_key_valid(key)) { + stc8h_report(dev, key); + } + } else if (cfg->irq.port == NULL) { + /* No IRQ line: transition polling is all we have. */ + if (stc8h_key_valid(key) && key != data->last_key) { + stc8h_report(dev, key); + } + } else if (stc8h_key_valid(key) && key != data->last_key && + gpio_pin_get_dt(&cfg->irq) > 0) { + /* Poll with an IRQ line = missed-edge safety net. + * Only report while the key is still held (INT level + * active) — the latched register would otherwise + * re-report the stale last key forever. */ stc8h_report(dev, key); } data->last_key = key; @@ -177,7 +216,10 @@ static void stc8h_irq_handler(const struct device *port, struct gpio_callback *c ARG_UNUSED(port); ARG_UNUSED(pins); - /* I2C cannot be touched from an ISR — hand off to the work queue. */ + /* I2C cannot be touched from an ISR — hand off to the work queue. + * The flag tells the handler this run is a real key edge, so it must + * report even when the latched register still holds the same code. */ + atomic_set(&data->irq_pending, 1); k_work_reschedule(&data->work, K_NO_WAIT); } diff --git a/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.conf b/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.conf index 043f971..fea5015 100644 --- a/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.conf +++ b/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.conf @@ -1,8 +1,9 @@ # ThinkNode M9 (ESP32-S3) LR1110 updater — overrides for the nRF-flavored prj.conf # -# Console is the native USB Serial/JTAG (base DTS chosen) — no USB device -# stack, no CDC ACM, no UF2, no DT code partition (ESP Simple Boot links the -# image itself and west flash writes it from 0x0). +# Console rides uart0 (CH9102-class USB-C bridge; see board.overlay — native +# USB Serial/JTAG is unbonded and its pads carry the keypad I2C bus) — no USB +# device stack, no CDC ACM, no UF2, no DT code partition (ESP Simple Boot +# links the image itself and west flash writes it from 0x0). CONFIG_BUILD_OUTPUT_UF2=n CONFIG_USE_DT_CODE_PARTITION=n