diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index edae244..25ddc83 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -269,7 +269,7 @@ if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wi set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf52.*") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") -elseif(BOARD MATCHES ".*esp32.*" OR BOARD MATCHES "station_g2" OR BOARD MATCHES "lilygo_tlora_c6" OR BOARD MATCHES "heltec_wifi_lora32_v3" OR BOARD MATCHES "heltec_wifi_lora32_v4") +elseif(BOARD MATCHES ".*esp32.*" OR BOARD MATCHES "station_g2" OR BOARD MATCHES "thinknode_m9" OR BOARD MATCHES "lilygo_tlora_c6" OR BOARD MATCHES "heltec_wifi_lora32_v3" OR BOARD MATCHES "heltec_wifi_lora32_v4") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_common.conf") elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*esp32.*") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_common.conf") diff --git a/zephcore/boards/esp32/thinknode_m9/Kconfig b/zephcore/boards/esp32/thinknode_m9/Kconfig new file mode 100644 index 0000000..959ef82 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/Kconfig @@ -0,0 +1,3 @@ +config HEAP_MEM_POOL_ADD_SIZE_BOARD + int + default 4096 diff --git a/zephcore/boards/esp32/thinknode_m9/Kconfig.thinknode_m9 b/zephcore/boards/esp32/thinknode_m9/Kconfig.thinknode_m9 new file mode 100644 index 0000000..aad86ff --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/Kconfig.thinknode_m9 @@ -0,0 +1,3 @@ +config BOARD_THINKNODE_M9 + select SOC_ESP32S3_WROOM_N16R8 + select SOC_ESP32S3_PROCPU if BOARD_THINKNODE_M9_ESP32S3_PROCPU diff --git a/zephcore/boards/esp32/thinknode_m9/README.md b/zephcore/boards/esp32/thinknode_m9/README.md new file mode 100644 index 0000000..6e61a71 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/README.md @@ -0,0 +1,170 @@ +# Elecrow ThinkNode M9 + +ESP32-S3 handheld with LR1110 radio, ST7789 320x240 TFT, CC1167Q GPS, PCF8563 +RTC, buzzer, STC8H I2C keyboard, QMI8658 IMU + QMC6309 magnetometer, and an SD +slot (keyboard/IMU/SD not ported — see below). + +Ported 2026-07-18 from Arduino MeshCore `variants/thinknode_m9` (which itself +landed upstream only days earlier), cross-checked against a second, +independently developed firmware for this board and against the Elecrow V1.0 +schematic. **Not yet validated on hardware.** + +## Build + +```bash +# Companion +west build -b thinknode_m9/esp32s3/procpu zephcore --pristine +west flash --esp-device COMX + +# Repeater +west build -b thinknode_m9/esp32s3/procpu zephcore --pristine -- \ + -DEXTRA_CONF_FILE="boards/common/repeater.conf" +``` + +Plain builds are ESP Simple Boot (self-contained `zephyr.bin`); WiFi-OTA and +release builds use `--sysbuild` + MCUboot like the other S3 boards (see +top-level CLAUDE.md). + +## LR1110 radio firmware updater + +First ESP32 target for `tools/lr1110_updater`: + +```bash +west build -b thinknode_m9/esp32s3/procpu zephcore/tools/lr1110_updater --pristine +west flash --esp-device COMX +``` + +Console output arrives on the native USB Serial/JTAG port. After the updater +reports success, flash the main firmware over the same port (`west flash` — +no DFU mode needed; esptool resets the chip itself). Identity/prefs/contacts +in high flash are untouched by the updater flash. + +## Verified against Arduino MeshCore (2026-07-18 desk check) + +- **PSRAM**: upstream `boards/thinknode_m9.json` says `memory_type: qio_opi`, + 16 MB flash → WROOM-1-**N16R8** + `CONFIG_SPIRAM_MODE_OCT=y` confirmed. +- **MADCTL**: Arduino ends at **0xA0** (`landscapeScreen()` 0x60, then + `DISPLAY_FLIP_VERTICALLY` overwrites with MV|MY) — DTS uses 0xA0. +- **Panel inversion**: Arduino init sends INVON; Zephyr st7789v inverts by + default (no `inversion-off` prop set) — matched. +- **GPS UART direction**: `Serial1.setPins(PIN_GPS_TX=2, PIN_GPS_RX=3)` and + arduino-esp32 `setPins(rx, tx)` → MCU RX=GPIO2, TX=GPIO3 (the apparent + ini-vs-variant.h swap is only naming perspective; both agree). +- **GPS EN/RESET behavior**: `MicroNMEALocationProvider` drives EN LOW-to-run + and holds RESET HIGH when stopped, 10 ms release pulse on start — exactly + what `ZephyrGPSManager`'s sequence does with the `gps-enable` (active-LOW) + and `gps-reset` (active-HIGH) aliases here. +- **RF switch at TX**: our LR11xx driver always selects the HP PA, so the chip + applies `rfswitch-tx-hp` (0x02 = DIO5 L, DIO6 H) — same as Arduino's + `MODE_TX_HP` at `LORA_TX_POWER=22`. +- **Battery**: MeshCore's ESP32Board hardcodes `2 * analogReadMilliVolts()` — + the 2:1 divider assumption (`vbat-mv-multiplier = 8800`) matches Arduino. +- **Upstream quirk not copied**: `ThinkNodeM9Board::getIRQGpio()` returns + `LORA_DIO0`=41, which is the **BUSY** pin; the RadioLib `Module(...)` ctor + uses DIO1=42 as IRQ. We follow the working wiring (dio1-gpios = 42) — + the second implementation names them correctly (IRQ = DIO1 = 42). + +## Verified against a second independent firmware (second desk check) + +- **GPS module is NOT an L76K** — MeshCore's `GPS_L76K` define is another + copy-paste artifact. The module identifies as **CC1167Q** to the *Unicore* + (`$PDTINFO`) probe family, which also explains the 115200 default baud + (a CASIC L76K would default to 9600). The DTS therefore uses + `gnss-nmea-generic` (plain NMEA, no vendor commands) rather than the CASIC + air530z driver. +- **GPS baud 115200** — both implementations agree; resolved. +- **GPS STANDBY (GPIO10)** — driven by *neither* implementation; left + completely untouched, matching both. +- **Battery** — the second implementation uses a 2.0 multiplier on ADC unit + 2 channel 2 → triple-confirms GPIO13 = ADC unit 2 ch 2 at 2:1. Its + measured OCV table is imported as [battery_curve.c](battery_curve.c). +- **No user button** — no button GPIO in either implementation (input is + the keyboard; canned-messages UI). No `sw0` is correct, not a gap. +- **No LED** — no LED pin in either implementation; MeshCore's `PIN_LED 13` + (colliding with `PIN_VBAT_READ 13`) confirmed a bug. +- **RF switch table** — both implementations carry the identical DIO5/DIO6 + truth table; our DTS bitmasks match both. +- **SD card CS (GPIO48)** — the second implementation parks all three CS + pins HIGH before radio init because the slot shares SPI2. Our base DTS + does the same with a `sd-cs-park` gpio-hog (also covers the standalone + LR1110 updater). +- **Charge/power sense** — GPIO1 = external-power detect, **active LOW**; + GPIO8 = charger DONE. Not consumed by ZephCore yet. +- **Keyboard identified** — STC8H helper MCU at **I2C 0x6C** on the second + bus (SDA=20 SCL=21), `KB_INT` GPIO12 idle-low/rising-edge, backlight + GPIO46. Registers: 0x01 battery, 0x03 long-press ms (16-bit write), 0x05 + matrix key, 0x06 state. +- **Also on the sensor bus** — QMI8658 IMU + QMC6309 magnetometer. + Unported; no address collision with the PCF8563 @0x51. + +## Verified against the Elecrow V1.0 schematic (third desk check) + +Net-level facts from `Think Node M9_V1.0.sch` (2026-06-23). The text layer +gives nets and part numbers, not GPIO pairings — pin numbers stay +firmware-sourced (both firmwares agree on all of them). + +- **VEXT (GPIO18) scope resolved** — the net is `VDD_PERIPH_EN`, gating a + rail that feeds the display (`DISPLAY_VDD`), the RTC's main supply + (`RTC_3V3`; the button cell `BUTTON-CELL`/BAT2 only backs up timekeeping) + and the I2C sensors. `tft_pwr_enable` therefore has `regulator-boot-on` + so the rail is up before RTC autodiscover / sensor scan, matching + Arduino's unconditional `board.begin()` write. +- **GPS rails confirmed** — the module has its own switched supply + (`GPS_EN` → `GPS_VDD`), so screenless/VEXT-off states never affect it; + its `ON_OFF` and `N_RESET` pins are wired (`GPS_ON/OFF`, `GPS_RST` nets), + and a `GPS_BAT`/`BACKUP_POWER` rail keeps ephemeris alive for warm starts. + Module symbol is generic ("GNSS MOD", 18-pin, TXD/RXD/1PPS/ON_OFF/VBAT/ + SDA/SCL) — no part number in the schematic; CC1167Q identification stands + on the `$PDTINFO` probe response. +- **USB is a UART bridge** — USB-C data goes through a CP2102/CH9102-class + bridge (U3, DTR/RTS auto-program transistors Q12/Q13) onto `RXD0_H`/ + `TXD0_H` = UART0. Our console-on-uart0 routing therefore lands exactly on + the USB-C port. Whether the S3's native D+/D− (GPIO19/20) also reach the + connector is unclear — MeshCore claims `ST7789_TE`=19, which would rule it + out — so the USB-OTG companion transport may be dead hardware on this + board; BLE is the primary companion path (verify OTG enumeration once, + drop the include if dead). +- **Power button is a hardware latch** — `POWER_EN` latch nets + an + `ESP32_WAKEUP` line; no firmware-readable user button at schematic level + either. Confirms the no-`sw0` model for the third time. +- **RF switch is a discrete SP3T** (U8: V1/V2 control, J1/J2/J3 ports) driven + by `RFSW0_V1`/`RFSW1_V2` = LR1110 DIO5/DIO6 — the 2-line table confirmed + at net level; DIO1 is the `INTERRUPT` net; `VTCXO` net feeds the TCXO. +- **Charger is an LGS4056HDA-4.35** — the suffix is the 4.35 V-termination + variant, so the pack may charge above the 4200 mV top of the imported OCV + curve (100% would then read early). Check the charged-battery voltage at + bring-up and stretch the curve if it really terminates at 4.35 V. +- **Keyboard matrix is driven by a second MCU** (nets `ESP32-2_*` — the + schematic symbol is another ESP32-S3 template; whatever ships speaks the + STC8H protocol at I2C 0x6C). SD card has its own switched `SD_3V3` rail. + +## Bring-up verification list + +Still unverified on hardware, in rough priority order: + +1. **Display init params** — gamma/porch/vcom inherited from Heltec T114's + ST7789V (no board-specific init values exist in any reference); check + contrast/colors. If mirrored or upside down, swap `mdac` 0xA0 ↔ 0x60. +2. **GPS identity/output** — confirm NMEA at 115200 and that the CC1167Q + talks standard sentences (it should; both upstreams parse plain NMEA). + If a fix never arrives, probe `$PDTINFO` manually. +3. **Battery calibration** — the 2:1 divider is what both upstreams assume; + sanity-check `get bat` against a multimeter once, and check whether the + 4.35 V charger variant actually charges above 4.20 V (OCV curve top). +4. **USB-OTG companion transport** — see the schematic section above: + verify whether the native USB pads reach the USB-C connector at all. +5. **rx-boosted** — deliberately ON (both upstreams leave boosted gain off / + unset; every other ZephCore LR1110 board uses it). Verify RX sensitivity + and TX power on air. +6. **ADC2 vs WiFi** — battery reads ride ADC unit 2 (GPIO13), which WiFi also + uses; expect intermittent read failures in `wifi_ota.conf` builds. + +## Not ported + +- **STC8H I2C keyboard** (@0x6C, second bus SDA=20 SCL=21, INT=12, + backlight=46) — Arduino MeshCore doesn't consume it either; the register + map above is the protocol reference when ZephCore grows keyboard support. +- **QMI8658 IMU / QMC6309 magnetometer** — no ZephCore use case yet. +- **SD card** (CS=48 on the shared SPI) — unused, CS parked HIGH via gpio-hog. +- **Ext-power detect** (GPIO1, active LOW) / **charge DONE** (GPIO8), + **GPS PPS** (GPIO4), **GPS STANDBY** (GPIO10). diff --git a/zephcore/boards/esp32/thinknode_m9/battery_curve.c b/zephcore/boards/esp32/thinknode_m9/battery_curve.c new file mode 100644 index 0000000..0d9c5d2 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/battery_curve.c @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: MIT + * + * ThinkNode M9 single-cell LiPo OCV curve — 11 points at 10% steps. + * Index 0 = 100% (4200 mV), index 10 = 0% (3100 mV). + */ + +#include "battery_curve.h" + +static const uint16_t ocv_thinknode_m9[11] = { + 4200, 4080, 3980, 3920, 3870, /* 100 .. 60% */ + 3820, 3790, 3750, 3700, 3600, /* 50 .. 10% */ + 3100, /* 0% */ +}; + +const battery_curve_t battery_curve_default = { + .ocv_mv = ocv_thinknode_m9, + .num_points = 11, + .num_cells = 1, +}; diff --git a/zephcore/boards/esp32/thinknode_m9/board.cmake b/zephcore/boards/esp32/thinknode_m9/board.cmake new file mode 100644 index 0000000..f3c3ac1 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/board.cmake @@ -0,0 +1,7 @@ +if(NOT "${OPENOCD}" MATCHES "^${ESPRESSIF_TOOLCHAIN_PATH}/.*") + set(OPENOCD OPENOCD-NOTFOUND) +endif() +find_program(OPENOCD openocd PATHS ${ESPRESSIF_TOOLCHAIN_PATH}/openocd-esp32/bin NO_DEFAULT_PATH) + +include(${ZEPHYR_BASE}/boards/common/esp32.board.cmake) +include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake) diff --git a/zephcore/boards/esp32/thinknode_m9/board.conf b/zephcore/boards/esp32/thinknode_m9/board.conf new file mode 100644 index 0000000..757a437 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/board.conf @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: MIT +# Elecrow ThinkNode M9 — ZephCore board configuration +# +# Hardware: +# ESP32-S3-WROOM-1 (16MB flash, 8MB OPI PSRAM assumed — N16R8) +# LR1110 on shared SPI2 (TCXO 3.3V, DIO5/DIO6 RF switch, HP PA) +# ST7789 320x240 TFT on shared SPI2 (mono-tft wrapper) +# L76K GPS on UART1 @ 115200 +# PCF8563 RTC @ 0x51 on I2C0; buzzer GPIO9 (LEDC) +# +# Include order: prj.conf → zephcore_common.conf → esp32_common.conf → board.conf + +# Board identification (matches Arduino variant name) +CONFIG_ZEPHCORE_BOARD_NAME="ThinkNode M9" +CONFIG_BT_DIS_MODEL_NUMBER_STR="Elecrow ThinkNode M9" + +# Radio type (TCXO, RF switch, PA config in DTS) +CONFIG_ZEPHCORE_RADIO_LR1110=y + +# Flash size — 16MB (override Zephyr default) +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y + +# PSRAM mode — WROOM-1-N16R8 is OPI octal. +# Enable is auto-detected from DTS by Kconfig.psram; only the mode is set here. +CONFIG_SPIRAM_MODE_OCT=y diff --git a/zephcore/boards/esp32/thinknode_m9/board.overlay b/zephcore/boards/esp32/thinknode_m9/board.overlay new file mode 100644 index 0000000..d315ab5 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/board.overlay @@ -0,0 +1,232 @@ +/* + * SPDX-License-Identifier: MIT + * Elecrow ThinkNode M9 — ZephCore overlay + * + * Adds the display stack, GPS driver, buzzer, battery ADC, RTC and + * filesystem on top of the base board DTS (which already carries the + * LR1110 radio — kept there so tools/lr1110_updater works without this + * overlay). Pin mapping from Arduino MeshCore variants/thinknode_m9. + * + * Display SPI2: CS=16 (cs-gpios[1]), DC=15, RST=14, BL=17 (active-LOW) + * Panel power: VEXT=18 (active-LOW) + * GPS UART1: RX=2, TX=3 @ 115200; EN=11 (active-LOW), RESET=5 + * Buzzer: GPIO9 (LEDC ch0) + * Battery ADC: GPIO13 = ADC unit 2 ch 2 (&adc1), 2:1 divider (matches + * MeshCore ESP32Board's hardcoded 2x) + * + * Not ported (unused by Arduino MeshCore): STC8H I2C keyboard @0x6C (bus + * SDA=20 SCL=21, INT=12, LED=46), SD card (CS=48 — parked HIGH by a hog in + * the base DTS), ext-power detect GPIO1 (active-LOW) + charge-DONE GPIO8, + * GPS PPS (4), GPS STANDBY (10), QMI8658 IMU + QMC6309 magnetometer on the + * sensor bus. + */ + +#include + +/ { + aliases { + pwm-0 = &ledc0; + buzzer = &buzzer; + gps-enable = &gps_enable_pin; + gps-reset = &gps_reset_pin; + }; + + chosen { + zephyr,display = &mono_tft; + }; + + /* + * VDD_PERIPH rail — GPIO18 (VEXT_ENABLE), active-LOW. + * The V1.0 schematic shows this is NOT display-only: VDD_PERIPH_EN + * gates a rail feeding the panel (DISPLAY_VDD), the RTC's main supply + * (button cell only backs up timekeeping) and the I2C sensors — + * `regulator-boot-on` powers it at driver init, before the RTC + * autodiscover and sensor scan run, matching Arduino's unconditional + * board.begin() write. display.c still treats it as the panel supply + * (never disables it); the label stays tft_pwr_enable for that hookup. + */ + tft_pwr_enable: tft-pwr-enable { + compatible = "regulator-fixed"; + regulator-name = "tft-pwr-enable"; + enable-gpios = <&gpio0 18 GPIO_ACTIVE_LOW>; + regulator-boot-on; + }; + + /* + * TFT backlight — GPIO17 (TFT_LEDA_CTL), active-LOW. + * Named disp_pwr_enable so display.c auto-gates it with display on/off. + * (Arduino dims it with 44 kHz PWM; fixed on/off is the first pass.) + */ + disp_pwr_enable: disp-pwr-enable { + compatible = "regulator-fixed"; + regulator-name = "disp-pwr-enable"; + enable-gpios = <&gpio0 17 GPIO_ACTIVE_LOW>; + }; + + /* + * Monochrome wrapper around the ST7789. + * Reports PIXEL_FORMAT_MONO01 to CFB (~9.6 KB framebuffer instead of + * ~150 KB RGB565) and converts each row on flush. + */ + mono_tft: mono-tft { + compatible = "zephcore,mono-tft"; + tft-dev = <&tft>; + width = <320>; + height = <240>; + }; + + /* + * MIPI-DBI SPI wrapper for the ST7789 — rides the shared SPI2 bus. + * The child's reg (1) picks cs-gpios[1] (GPIO16) declared in the base DTS. + * DC=GPIO15, RST=GPIO14 (active-LOW). Write-only (MISO belongs to LR1110). + */ + mipi_dbi { + compatible = "zephyr,mipi-dbi-spi"; + spi-dev = <&spi2>; + dc-gpios = <&gpio0 15 GPIO_ACTIVE_HIGH>; + reset-gpios = <&gpio0 14 GPIO_ACTIVE_LOW>; + write-only; + #address-cells = <1>; + #size-cells = <0>; + + tft: st7789v@1 { + compatible = "sitronix,st7789v"; + mipi-max-frequency = <20000000>; + reg = <1>; + width = <320>; + height = <240>; + x-offset = <0>; + y-offset = <0>; + /* MADCTL=0xA0: MY|MV, RGB order (landscape 320x240 on the + * native-portrait 240x320 panel). This is what Arduino ends + * up with: landscapeScreen() sets 0x60 (MX|MV), then + * DISPLAY_FLIP_VERTICALLY=1 calls flipScreenVertically() + * which overwrites MADCTL with MV|MY = 0xA0 (ST7789Spi.h). + * If mirrored/upside-down at bring-up, try 0x60. + * Panel inversion (INVON): Arduino sends it, and the Zephyr + * st7789v driver defaults to inversion ON (no `inversion-off` + * prop here) — matched. */ + mdac = <0xA0>; + colmod = <0x55>; + vcom = <0x20>; + gctrl = <0x35>; + vrhs = <0x0b>; + vdvs = <0x20>; + gamma = <0x01>; + lcm = <0x2c>; + porch-param = [0c 0c 00 33 33]; + cmd2en-param = [5a 69 02 01]; + pwctrl1-param = [a4 a1]; + pvgam-param = [70 2c 2e 15 10 09 48 33 53 0b 19 18 20 25]; + nvgam-param = [70 2c 2e 15 10 09 48 33 53 0b 19 18 20 25]; + ram-param = [00 f0]; + rgb-param = [40 02 14]; + mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE"; + }; + }; + + /* GPS power enable — GPIO11, active-LOW (PIN_GPS_EN_ACTIVE=LOW). + * ZephCore's GPS manager drives this via the gps-enable alias. */ + gps_en: gps-enable { + compatible = "gpio-leds"; + gps_enable_pin: gps_enable { + gpios = <&gpio0 11 GPIO_ACTIVE_LOW>; + label = "GPS Enable"; + }; + }; + + /* GPS reset — GPIO5, active-HIGH per Arduino (PIN_GPS_RESET_ACTIVE=HIGH). */ + gps_rst: gps-reset { + compatible = "gpio-leds"; + gps_reset_pin: gps_reset { + gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>; + label = "GPS Reset"; + }; + }; + + /* Buzzer on LEDC channel 0 (GPIO9) */ + pwmbuzzer { + compatible = "pwm-leds"; + buzzer: buzzer { + pwms = <&ledc0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>; + }; + }; + + /* Battery ADC — ZephCore reads via zephyr,user { io-channels }. + * GPIO13 = ADC **unit 2** channel 2 → &adc1 (the DT label is 0-indexed, + * the unit is 1-indexed — same trap as heltec_wireless_tracker). + * ADC_GAIN_1_4 = 12 dB attenuation ≈ 0–3.1 V usable; nominal full scale + * 1100 mV × 4 = 4400 mV. Divider ratio unverified — assumed 2:1 + * (multiplier 4400 × 2.0 = 8800). Calibrate at bring-up. + * Caveat: ADC unit 2 is shared with WiFi — battery reads can fail while + * WiFi is active (wifi_ota builds). */ + zephyr,user { + io-channels = <&adc1 2>; + vbat-mv-multiplier = <8800>; + }; +}; + +/* Battery ADC channel configuration (ADC unit 2 → GPIO13). */ +&adc1 { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + channel@2 { + reg = <2>; + zephyr,gain = "ADC_GAIN_1_4"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,resolution = <12>; + }; +}; + +/* GPS on UART1 — generic NMEA driver (no vendor commands). + * The module is NOT the L76K that MeshCore's variant.h claims — it + * identifies as "CC1167Q" to the Unicore-family $PDTINFO probe, which also + * explains the 115200 default baud (a CASIC L76K would be 9600). Under + * module-identity uncertainty gnss-nmea-generic is the safe choice — + * CASIC-specific driver init would be wrong here, and plain NMEA works + * either way. Power is gated by the manager via gps-enable/gps-reset. + * GPS_STANDBY (GPIO10) is deliberately left untouched — Arduino MeshCore + * never drives it either. */ +&uart1 { + gnss: gnss-nmea-generic { + compatible = "gnss-nmea-generic"; + }; +}; + +/* LEDC PWM controller — buzzer timer/channel */ +&ledc0 { + pinctrl-0 = <&ledc0_default>; + pinctrl-names = "default"; + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + channel0@0 { + reg = <0x0>; + timer = <0>; + }; +}; + +&i2c0 { + #include "../../common/sensors-i2c.dtsi" + /* ThinkNode M9 has a battery-backed PCF8563 @ 0x51 — opt in to boot-time + * time restore (auto-detected at runtime among the supported chips). */ + #include "../../common/rtc-i2c.dtsi" +}; + +/* + * Flash partitions live in partitions.overlay (this dir) so the identical map + * is fed to both the app and the MCUboot image — see that file and + * sysbuild/CMakeLists.txt. + */ + +/* 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. */ +#include "../../common/esp32s3_console_uart0.dtsi" +#include "../../common/esp32s3_usb_otg.dtsi" diff --git a/zephcore/boards/esp32/thinknode_m9/board.yml b/zephcore/boards/esp32/thinknode_m9/board.yml new file mode 100644 index 0000000..3080ba1 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/board.yml @@ -0,0 +1,9 @@ +# Copyright (c) 2026 ZephCore +# SPDX-License-Identifier: MIT + +board: + name: thinknode_m9 + full_name: Elecrow ThinkNode M9 (ESP32-S3 + LR1110 + ST7789 TFT) + vendor: elecrow + socs: + - name: esp32s3 diff --git a/zephcore/boards/esp32/thinknode_m9/partitions.overlay b/zephcore/boards/esp32/thinknode_m9/partitions.overlay new file mode 100644 index 0000000..5a300ab --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/partitions.overlay @@ -0,0 +1,53 @@ +/* + * Flash partition layout — SHARED by the app image and MCUboot. + * + * Fed to BOTH images (app via EXTRA_DTC_OVERLAY_FILE, MCUboot via + * mcuboot_EXTRA_DTC_OVERLAY_FILE in sysbuild/CMakeLists.txt) so their flash maps + * are identical — otherwise MCUboot stages/looks for the OTA image at a different + * address than the app writes it to and OTA silently reverts on reboot. + * + * slot0 starts at 0x10000 (right after the 64KB MCUboot region), matching the + * Arduino/ESP-IDF app offset (Mesh America "flash-update" compatible). sys_partition + * is reclaimed. Slots are equal size and slot1 ends exactly on the unchanged + * storage/lfs offsets, so a -merged reflash keeps identity/prefs/contacts/BLE-bonds. + * + * 16MB layout. + */ +/delete-node/ &sys_partition; +/delete-node/ &slot0_lpcore_partition; +/delete-node/ &slot1_lpcore_partition; +/delete-node/ &storage_partition; +/delete-node/ &scratch_partition; +/delete-node/ &coredump_partition; +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; + +/ { + chosen { + zephyr,settings-partition = &storage_partition; + }; +}; + +&flash0 { + partitions { + /* App slots — equal size (swap-mode compatible), slot0 at 0x10000. */ + slot0_partition: partition@10000 { + label = "image-0"; + reg = <0x10000 0x7C6000>; + }; + slot1_partition: partition@7D6000 { + label = "image-1"; + reg = <0x7D6000 0x7C6000>; + }; + /* BLE bond NVS — offset unchanged so bonds survive the reflash. */ + storage_partition: partition@F9C000 { + label = "storage"; + reg = <0xF9C000 0x4000>; + }; + /* LittleFS (identity/prefs/contacts) — offset unchanged. */ + lfs_partition: partition@FA0000 { + label = "lfs"; + reg = <0xFA0000 0x60000>; + }; + }; +}; diff --git a/zephcore/boards/esp32/thinknode_m9/thinknode_m9-pinctrl.dtsi b/zephcore/boards/esp32/thinknode_m9/thinknode_m9-pinctrl.dtsi new file mode 100644 index 0000000..1e4f770 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/thinknode_m9-pinctrl.dtsi @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: MIT + * Elecrow ThinkNode M9 — Pin Control Definitions + * + * Pin mapping (from Arduino MeshCore variants/thinknode_m9): + * Shared SPI2: SCLK=GPIO40, MOSI=GPIO47, MISO=GPIO38 + * (LR1110 NSS=GPIO39, ST7789 CS=GPIO16, SD CS=GPIO48 unused) + * I2C0: SDA=GPIO7, SCL=GPIO6 (PCF8563 RTC + env sensors) + * UART1: RX=GPIO2, TX=GPIO3 (L76K GPS; upstream setPins(2,3)) + * LEDC ch0: GPIO9 (buzzer) + * Console: uart0 GPIO43/44 via esp32s3_console_uart0.dtsi (board.overlay) + */ + +#include +#include +#include + +&pinctrl { + /* SPI2 — LR1110 radio + ST7789 TFT (CS per device via cs-gpios) */ + spim2_default: spim2_default { + group1 { + pinmux = , + ; + }; + group2 { + pinmux = ; + output-low; + }; + }; + + /* I2C0 — PCF8563 RTC + optional environment sensors */ + i2c0_default: i2c0_default { + group1 { + pinmux = , + ; + bias-pull-up; + drive-open-drain; + output-high; + }; + }; + + /* UART1 — L76K GPS (GPIO2 = GPS TX → CPU RX, GPIO3 = GPS RX → CPU TX) */ + uart1_default: uart1_default { + group1 { + pinmux = ; + output-high; + }; + group2 { + pinmux = ; + bias-pull-up; + }; + }; + + /* LEDC channel 0 — buzzer PWM on GPIO9 */ + ledc0_default: ledc0_default { + group1 { + pinmux = ; + output-enable; + }; + }; +}; diff --git a/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts b/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts new file mode 100644 index 0000000..45f53ff --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts @@ -0,0 +1,149 @@ +/* + * SPDX-License-Identifier: MIT + * Elecrow ThinkNode M9 — ESP32-S3, 16MB flash, 8MB PSRAM + * + * Hardware: + * MCU: ESP32-S3-WROOM-1 (Xtensa LX7, dual-core @ 160 MHz) + * Flash: 16 MB + * PSRAM: 8 MB (OPI) — module variant assumed N16R8, verify at bring-up + * Radio: Semtech LR1110 on shared SPI2 (TCXO 3.3V, DIO5/DIO6 RF switch) + * Display: ST7789 320x240 TFT on shared SPI2 (defined in board.overlay) + * GPS: CC1167Q (Unicore family; NOT the L76K MeshCore's variant.h + * claims) on UART1 @ 115200 + * RTC: PCF8563 @ 0x51 on I2C0 + * Console: Native USB Serial/JTAG (main firmware reroutes to uart0 and + * gives the USB port to the OTG companion transport — see + * board.overlay). + * + * The LR1110 node lives HERE (not board.overlay) so the standalone + * tools/lr1110_updater build — which replaces board.overlay with its own + * minimal overlay — still sees the radio. + */ + +/dts-v1/; + +#include +#include "thinknode_m9-pinctrl.dtsi" +#include + +/ { + model = "Elecrow ThinkNode M9"; + compatible = "elecrow,thinknode-m9"; + + 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; + }; + + aliases { + uart-0 = &uart0; + i2c-0 = &i2c0; + watchdog0 = &wdt0; + lora0 = &lora; + }; +}; + +/* Reduce CPU clock from 240 MHz to 160 MHz — HAL Kconfig minimum supported */ +&cpu0 { + clock-frequency = ; +}; + +&cpu1 { + clock-frequency = ; +}; + +&usb_serial { + status = "okay"; +}; + +&i2c0 { + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c0_default>; + pinctrl-names = "default"; +}; + +&spi2 { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + pinctrl-0 = <&spim2_default>; + pinctrl-names = "default"; + + /* CS 0 = LR1110 NSS (GPIO39), CS 1 = ST7789 TFT (GPIO16). + * The TFT itself is declared in board.overlay (mipi-dbi child @1). */ + cs-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>, + <&gpio0 16 GPIO_ACTIVE_LOW>; + + lora: lora@0 { + compatible = "semtech,lr1110"; + reg = <0>; + spi-max-frequency = <8000000>; + + reset-gpios = <&gpio1 13 GPIO_ACTIVE_LOW>; /* GPIO45 */ + busy-gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; /* GPIO41 */ + dio1-gpios = <&gpio1 10 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* GPIO42 */ + + /* TCXO 3.3V via DIO3 (Arduino LR11X0_DIO3_TCXO_VOLTAGE=3.3) */ + tcxo-voltage-mv = <3300>; + tcxo-startup-delay-ms = <5>; + + /* +3dB RX sensitivity for +2mA — deliberate divergence from the + * Arduino variant, which leaves boosted gain off (looks like an + * oversight; T1000-E and every other ZephCore LR1110 board use it). */ + rx-boosted; + + /* RF switch truth table from Arduino rfswitch_table[]: + * bit0 = DIO5, bit1 = DIO6. + * STBY: DIO5=L DIO6=L RX: DIO5=H DIO6=L + * TX: DIO5=H DIO6=H TX_HP: DIO5=L DIO6=H + * (MODE_TX = low-power PA path, MODE_TX_HP = high-power PA path) */ + rfswitch-enable = <0x03>; + rfswitch-standby = <0x00>; + rfswitch-rx = <0x01>; + rfswitch-tx = <0x03>; + rfswitch-tx-hp = <0x02>; + rfswitch-gnss = <0x00>; + }; +}; + +/* GPS — 115200 baud (the macro Arduino MeshCore actually consumes; the + * variant.h 9600 value is a dead macro). GNSS driver node + enable/reset + * pins live in board.overlay. */ +&uart1 { + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart1_default>; + pinctrl-names = "default"; +}; + +&gpio0 { status = "okay"; }; + +&gpio1 { + status = "okay"; + + /* Park the SD card's CS (GPIO48) HIGH (deselected — the CS itself is + * active-low). The SD slot shares SPI2 with the LR1110 and the TFT but + * has no Zephyr driver; left floating, an inserted card could drive MISO + * and corrupt radio/display transfers. Also protects the standalone + * lr1110_updater (this hog lives in the base DTS). */ + sd-cs-park { + gpio-hog; + gpios = <16 GPIO_ACTIVE_HIGH>; /* GPIO48 */ + output-high; /* physical HIGH = deselected */ + }; +}; +&wdt0 { status = "okay"; }; +&trng0 { status = "okay"; }; + +&esp32_bt_hci { + status = "okay"; +}; + +&wifi { + status = "okay"; +}; diff --git a/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu_defconfig b/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu_defconfig new file mode 100644 index 0000000..92c0923 --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu_defconfig @@ -0,0 +1,12 @@ +# Elecrow ThinkNode M9 — minimal Zephyr defconfig (procpu) +CONFIG_CONSOLE=y +CONFIG_SERIAL=y +CONFIG_UART_CONSOLE=y +CONFIG_GPIO=y +CONFIG_CLOCK_CONTROL=y +CONFIG_PINCTRL=y +CONFIG_SPI=y +CONFIG_I2C=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_NVS=y diff --git a/zephcore/boards/example_board/README.md b/zephcore/boards/example_board/README.md index fdd8fd2..71271ab 100644 --- a/zephcore/boards/example_board/README.md +++ b/zephcore/boards/example_board/README.md @@ -47,6 +47,7 @@ SWD flash: `west flash` (requires J-Link, pyocd, or nrfjprog connected). | Heltec Wireless Tracker V1.1 | `west build -b heltec_wireless_tracker/esp32s3/procpu zephcore` | `west flash` | | Heltec Wireless Tracker V2 | `west build -b heltec_wireless_tracker_v2/esp32s3/procpu zephcore` | `west flash` | | LilyGo T-Beam v1.2 | `west build -b ttgo_tbeam/esp32/procpu zephcore` | `west flash` | +| ThinkNode M9 | `west build -b thinknode_m9/esp32s3/procpu zephcore` | `west flash` | **Heltec V3 console:** ZephCore routes console/shell to `uart0` on V3. Use the UART serial port for boot logs and CLI. diff --git a/zephcore/tools/lr1110_updater/CMakeLists.txt b/zephcore/tools/lr1110_updater/CMakeLists.txt index 719fa4d..7d3b28c 100644 --- a/zephcore/tools/lr1110_updater/CMakeLists.txt +++ b/zephcore/tools/lr1110_updater/CMakeLists.txt @@ -1,11 +1,14 @@ # SPDX-License-Identifier: MIT # ZephCore LR1110 Firmware Updater — updates LR1110 radio firmware to 0x0401. # -# Board-specific: only builds for boards with an LR1110 radio (e.g., t1000_e). +# Board-specific: only builds for boards with an LR1110 radio +# (t1000_e, thinknode_m9). # # Build: west build -b t1000_e/nrf52840 zephcore/tools/lr1110_updater --pristine -# Flash: drag-drop build/zephyr/zephyr.uf2 onto UF2 drive +# west build -b thinknode_m9/esp32s3/procpu zephcore/tools/lr1110_updater --pristine +# Flash: nRF52: drag-drop build/zephyr/zephyr.uf2 onto UF2 drive # (or: nrfjprog --program build/zephyr/zephyr.hex --sectorerase -r) +# ESP32: west flash --esp-device COMX # # After the updater finishes, flash ZephCore main firmware. diff --git a/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.conf b/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.conf new file mode 100644 index 0000000..043f971 --- /dev/null +++ b/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.conf @@ -0,0 +1,12 @@ +# 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). + +CONFIG_BUILD_OUTPUT_UF2=n +CONFIG_USE_DT_CODE_PARTITION=n +CONFIG_USB_DEVICE_STACK_NEXT=n +CONFIG_UART_LINE_CTRL=n +CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n +CONFIG_CDC_ACM_SERIAL_ENABLE_AT_BOOT=n diff --git a/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.overlay b/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.overlay new file mode 100644 index 0000000..20a078b --- /dev/null +++ b/zephcore/tools/lr1110_updater/boards/esp32/thinknode_m9/board.overlay @@ -0,0 +1,21 @@ +/* + * ThinkNode M9 LR1110 Updater overlay + * SPDX-License-Identifier: MIT + * + * Minimal overlay — the LR1110 lives in the base board DTS (SPI2), and the + * base chosen console is the native USB Serial/JTAG, which is exactly what + * the updater wants (the main firmware's uart0 reroute lives in the main + * board.overlay, which this file replaces). + * + * Disables peripherals not needed for the update. + */ + +/* Disable UART1 (GPS) — not needed */ +&uart1 { + status = "disabled"; +}; + +/* Disable I2C0 (RTC/sensors) — not needed */ +&i2c0 { + status = "disabled"; +}; diff --git a/zephcore/tools/lr1110_updater/src/main.c b/zephcore/tools/lr1110_updater/src/main.c index 1bcb80e..6d525d8 100644 --- a/zephcore/tools/lr1110_updater/src/main.c +++ b/zephcore/tools/lr1110_updater/src/main.c @@ -64,16 +64,26 @@ static void led_toggle(void) {} /* ── Helpers ──────────────────────────────────────────────────── */ -static void reboot_to_uf2(void) +static void updater_done(void) { +#if defined(CONFIG_SOC_SERIES_NRF52) printk("\nRebooting into UF2 DFU mode...\n"); printk("You can now drag-drop ZephCore firmware UF2.\n"); k_msleep(500); -#if defined(CONFIG_SOC_SERIES_NRF52) nrf_power_gpregret_set(NRF_POWER, 0, BOOTLOADER_DFU_UF2_MAGIC); -#endif sys_reboot(SYS_REBOOT_COLD); +#else + /* ESP32: no persistent DFU mode to reboot into, and a plain reboot + * would just re-run the updater. Idle here — west flash / esptool + * resets the chip itself when writing the main firmware. */ + printk("\nFlash ZephCore main firmware over USB now:\n"); + printk(" west flash --esp-device \n"); + while (1) { + led_toggle(); + k_msleep(1000); + } +#endif } static void fatal_error(const char *msg) @@ -149,7 +159,7 @@ int main(void) printk("\nAlready running target firmware 0x%04X — no update needed!\n", TARGET_FW_VERSION); led_off(); - reboot_to_uf2(); + updater_done(); return 0; } @@ -306,9 +316,9 @@ int main(void) led_off(); } - /* ── Reboot to UF2 DFU ── */ - printk("\n[10/10] Done! Rebooting to UF2 DFU mode...\n"); - reboot_to_uf2(); + /* ── Hand off to main-firmware flashing ── */ + printk("\n[10/10] Done!\n"); + updater_done(); return 0; /* never reached */ }