Files
wadamesh/variants/thinknode_m9/M9Board.cpp
T
Kaj SchittecatandClaude Fable 5 a38ea1e3bb touch: Elecrow ThinkNode M9 port (ESP32-S3 + LR1110 + I2C QWERTY) — bring-up builds 1-9
New board: ThinkNode_M9_companion_radio_touch env, variants/thinknode_m9/
(board glue, LR1110 target, keyboard driver, partitions, M9_PORT.md pin
reference), boards/thinknode_m9.json, device_caps HAS_THINKNODE_M9 block,
HAS_M9_KEYBOARD input path in the touch UI (d-pad focus nav, tab jump keys,
QWERTY typing).

Based on a contributed bring-up patch by ded (SalishMesh), who also remote-
tested every build on a preproduction unit. Hard-won fixes on top of the
patch, each verified on hardware:

- LR1110 radio init -706: preprod chips run transceiver FW 0x0303, which
  rejects DriveDiosInSleepMode (0x012A, added in FW 0x0308) that RadioLib
  7.x sends unconditionally during begin(). New build-time patch
  scripts/build/patch_radiolib_lr11x0.py tolerates it on old FW;
  radio_init() prints the chip's device/FW/errors either way.
- TCXO: DIO3 at 3.3 V (the patch's active-oscillator/tcxo=0 theory gave
  -707 on hardware).
- Display: rotation 1 (not 3), plus the missing landscape s_ui_rotation
  override so LVGL renders 320x240 instead of portrait-into-landscape.
- Keyboard: the controller is a separate ESP32-S2 I2C slave (0x6C) with
  addressed registers - reads select register 0x01 first (bare reads
  return the HW-version register forever). Boot probe logs the
  controller's HW/FW; backlight duty register wired.
- USB pad release: the keyboard bus lives on GPIO20/21 = the S3's native
  USB D-/D+ pads, owned from reset by the ROM USB-Serial-JTAG (D+ pullup
  on SDA). M9Board::begin() releases them before any bus init.
- GPIO35-37 are octal-PSRAM pins on the S3R8 - never drive them (the
  patch's "SD CS = 36" misread package pin 36 = GPIO48; SD is on the
  shared SPI bus, support returns with PIN_SD_CS=48 later).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:49:25 +02:00

111 lines
4.9 KiB
C++

#include <Arduino.h>
#include <SPI.h>
#include "M9Board.h"
#include "soc/usb_serial_jtag_reg.h"
void ThinkNodeM9Board::begin() {
// Release the S3's native USB pads. GPIO19/20 are USB D-/D+ and the ROM's
// USB-Serial-JTAG peripheral owns those pads from reset, including a D+
// pull-up ON GPIO20. The M9's console goes through an external UART bridge
// (U3) instead, and the schematic reuses GPIO20/21 as the KEYBOARD I2C bus
// (nets ESP32-2_SDA/SCL) and GPIO19 as LCD_TE — so without this the
// keyboard SDA line is clamped by the USB PHY and the controller never
// answers (bring-up #8: probe found nothing on a correctly-wired bus).
REG_CLR_BIT(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_USB_PAD_ENABLE);
ESP32Board::begin(); // attaches PIN_VBAT_READ, brings up Wire on PIN_BOARD_SDA/SCL (7/6)
// The one place this board's init genuinely has to differ from T-Deck/
// Heltec V4's pattern: their displays use ST7789LCDDisplay's dedicated-
// instance branch (LILYGO_TDECK / HELTEC_LORA_V4_TFT), which begins its
// own SPI bus internally — so neither board's board.begin() touches SPI
// at all, and the radio's bus only gets begun later, inside radio_init().
// M9's display is on the class's "default" branch (display(&SPI, ...)) —
// the only safe branch available without borrowing another board's
// identity macro (HELTEC_LORA_V4_TFT is also checked in shared UITask.cpp
// against HeltecV4Board-only methods, which would be a compile error here)
// — and that branch does NOT begin the bus itself, it assumes the caller
// already has. So a single bare SPI.begin() has to happen here, before
// display.begin() runs (next, in main.cpp). Everything else in this
// function mirrors TDeckBoard::begin()/HeltecV4Board::begin() as closely
// as M9's actual hardware allows — no CS-deselect dance, no NSS pre-drive:
// neither working board does either, and they don't need it.
SPI.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, PIN_TFT_CS);
// Shared-SPI bus discipline: park the bus devices' chip-selects HIGH before
// any traffic flows (LR1110 NSS=39, TFT CS=16). NOTE: do NOT touch GPIO35-37
// on this board — the S3R8's octal PSRAM owns them (driving GPIO36 as a GPIO
// wedged the PSRAM bus and hung boot right after prefs init, bring-up #3).
// SCHEMATIC TRUTH (read from the V1.0 sheet, bring-up #9): the microSD *is*
// on this shared SPI bus with CS = GPIO48 — the patch's "SD CS = 36" misread
// the S3's PACKAGE pin 36 (SPICLK_N = GPIO48) as GPIO36. SD support returns
// with PIN_SD_CS=48 once the keyboard/radio bring-up settles (M9_PORT.md).
pinMode(PIN_TFT_CS, OUTPUT);
digitalWrite(PIN_TFT_CS, HIGH);
pinMode(P_LORA_NSS, OUTPUT);
digitalWrite(P_LORA_NSS, HIGH);
// Bring up both rails at boot and leave them claimed for now. Display
// power-down (turnOff()) releases periph_power via the pointer passed into
// ST7789LCDDisplay's constructor in target.cpp; the backlight is ours to
// manage explicitly until UI-side sleep/backlight-timeout wiring exists for
// this board (TODO — see M9_PORT.md).
periph_power.begin();
backlight.begin();
periph_power.claim();
backlight.claim();
pinMode(PIN_USER_BTN, INPUT_PULLUP); // BOOT button, GPIO0, active LOW
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
uint64_t wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1ULL << P_LORA_DIO_1)) {
startup_reason = BD_STARTUP_RX_PACKET; // woke from an incoming LoRa packet
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
void ThinkNodeM9Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Hold DIO1 / NSS at their required levels through sleep (LR1110 IRQ wake).
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH);
} else {
esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1) | (1ULL << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH);
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000ULL);
}
esp_deep_sleep_start(); // CPU halts here and never returns
}
void ThinkNodeM9Board::powerOff() {
enterDeepSleep(0);
}
uint16_t ThinkNodeM9Board::getBattMilliVolts() {
#if defined(PIN_VBAT_READ)
// 1:1 divider (manufacturer-confirmed) — unlike the ESP32Board base class's
// hardcoded `2 * raw`, the pin voltage IS the battery voltage here.
analogReadResolution(12);
uint32_t sum = 0;
const int kSamples = 8;
for (int i = 0; i < kSamples; i++) {
sum += analogReadMilliVolts(PIN_VBAT_READ);
}
return (uint16_t)(sum / kSamples);
#else
return 0;
#endif
}