port thinknode m9

This commit is contained in:
liquidraver
2026-07-18 20:30:38 +02:00
parent 434a49375e
commit 16c7ae506a
18 changed files with 801 additions and 10 deletions
+1 -1
View File
@@ -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")
@@ -0,0 +1,3 @@
config HEAP_MEM_POOL_ADD_SIZE_BOARD
int
default 4096
@@ -0,0 +1,3 @@
config BOARD_THINKNODE_M9
select SOC_ESP32S3_WROOM_N16R8
select SOC_ESP32S3_PROCPU if BOARD_THINKNODE_M9_ESP32S3_PROCPU
@@ -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).
@@ -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,
};
@@ -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)
@@ -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
@@ -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 <zephyr/dt-bindings/pwm/pwm.h>
/ {
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 ≈ 03.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 = <ADC_ACQ_TIME_DEFAULT>;
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"
@@ -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
@@ -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>;
};
};
};
@@ -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 <zephyr/dt-bindings/pinctrl/esp-pinctrl-common.h>
#include <zephyr/dt-bindings/pinctrl/esp32s3-pinctrl.h>
#include <zephyr/dt-bindings/pinctrl/esp32s3-gpio-sigmap.h>
&pinctrl {
/* SPI2 — LR1110 radio + ST7789 TFT (CS per device via cs-gpios) */
spim2_default: spim2_default {
group1 {
pinmux = <SPIM2_MISO_GPIO38>,
<SPIM2_SCLK_GPIO40>;
};
group2 {
pinmux = <SPIM2_MOSI_GPIO47>;
output-low;
};
};
/* I2C0 — PCF8563 RTC + optional environment sensors */
i2c0_default: i2c0_default {
group1 {
pinmux = <I2C0_SDA_GPIO7>,
<I2C0_SCL_GPIO6>;
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 = <UART1_TX_GPIO3>;
output-high;
};
group2 {
pinmux = <UART1_RX_GPIO2>;
bias-pull-up;
};
};
/* LEDC channel 0 — buzzer PWM on GPIO9 */
ledc0_default: ledc0_default {
group1 {
pinmux = <LEDC_CH0_GPIO9>;
output-enable;
};
};
};
@@ -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 <espressif/esp32s3/esp32s3_wroom_n16r8.dtsi>
#include "thinknode_m9-pinctrl.dtsi"
#include <espressif/partitions_0x0_default_16M.dtsi>
/ {
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 = <DT_FREQ_M(160)>;
};
&cpu1 {
clock-frequency = <DT_FREQ_M(160)>;
};
&usb_serial {
status = "okay";
};
&i2c0 {
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;
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";
};
@@ -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
+1
View File
@@ -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.
+5 -2
View File
@@ -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.
@@ -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
@@ -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";
};
+17 -7
View File
@@ -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 <COMx>\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 */
}