From 8b549d49a9a4305eb5e578dc78aff3d880d0bcfa Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Sat, 11 Apr 2026 22:07:15 -0600 Subject: [PATCH] feat: add ELECROW ThinkNode-M2 board support ESP32-S3 + SX1262 + 1.3" SH1106 OLED + CP2102 UART bridge + 1000 mAh Li-Po. Pin map verified against Meshtastic's variants/esp32s3/ELECROW-ThinkNode-M2. DIO3 TCXO at 3.3 V (vs 1.8 V on Heltec), VEXT_ENABLE active HIGH (opposite polarity), dedicated LORA_POWER_EN on GPIO48 sequenced before SPI init. Reuses the existing async SH1106 driver at I2C address 0x3C. Refactors hal::esp32s3::{init_spi,init_i2c} and board::esp32s3::init_radio to accept generic GPIO bounds so ESP32-S3 boards with different pin assignments can share the helpers. init_radio now takes a TcxoCtrlVoltage parameter instead of hardcoding Ctrl1V8. Incidental cleanups needed to make `just check-all` green: - Replace the core::mem::forget(output) pattern with let _foo = ...; current esp-hal makes Output non-Drop, so forget is a no-op that trips clippy's forget_non_drop lint. - Switch justfile xtensa builds from `cargo +nightly` with a RUSTC override to `cargo +esp` so the compiler and rust-src versions stay aligned. Public nightly's rust-src had drifted two months ahead of the esp fork and broke -Zbuild-std on core. --- CHANGELOG.md | 25 ++++++ Cargo.toml | 6 ++ README.md | 1 + justfile | 25 +++--- src/board/elecrow_thinknode_m2.rs | 141 ++++++++++++++++++++++++++++++ src/board/esp32s3.rs | 23 +++-- src/board/heltec_v3.rs | 19 ++-- src/board/heltec_v3_uart.rs | 19 ++-- src/board/heltec_v4.rs | 19 ++-- src/driver/mod.rs | 4 +- src/hal/esp32s3.rs | 18 ++-- src/hal/mod.rs | 2 +- src/host/mod.rs | 2 +- src/main.rs | 4 +- 14 files changed, 262 insertions(+), 46 deletions(-) create mode 100644 src/board/elecrow_thinknode_m2.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fe451..f1a220d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added + +- ELECROW ThinkNode-M2 board support (ESP32-S3 + SX1262 + 1.3" SH1106 OLED + via CP2102 UART bridge, 1000 mAh Li-Po handheld) + - Pin map verified against Meshtastic's + `variants/esp32s3/ELECROW-ThinkNode-M2` + - DIO3 TCXO driven at 3.3 V (not 1.8 V like Heltec) + - Dedicated `LORA_POWER_EN` (GPIO48) sequenced HIGH before SPI init + - `VEXT_ENABLE` (GPIO46) is active HIGH — opposite polarity from Heltec + - Reuses the existing async SH1106 driver at I2C address 0x3C + ### Changed +- `hal::esp32s3::{init_spi, init_i2c}` and `board::esp32s3::init_radio` are + now generic over GPIO type, so ESP32-S3 boards with different pin + assignments can share the helpers. +- `board::esp32s3::init_radio` takes a `TcxoCtrlVoltage` parameter instead + of hardcoding `Ctrl1V8` — needed for ThinkNode-M2's 3.3 V TCXO. +- SSD1306 display items in `board::esp32s3` are now cfg-gated to Heltec + boards (ThinkNode-M2 supplies its own SH1106 `create_display`). +- `justfile` uses `cargo +esp` for Xtensa targets instead of `cargo +nightly` + with a `RUSTC=esp` override — the self-contained esp toolchain keeps its + cargo, rustc, and rust-src versions aligned. Public nightly's rust-src + had drifted ahead of the esp fork, breaking `-Zbuild-std`. - Migrated to embassy-executor 0.10 and embassy-rp 0.10 - ESP crates (esp-hal, esp-rtos, esp-backtrace, esp-println) patched from esp-rs/esp-hal main branch until esp-rtos publishes with 0.10 support @@ -22,6 +44,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Fixed +- `core::mem::forget(output)` pattern in ESP32-S3 board files is now + `let _vext = Output::new(...)` — current esp-hal makes `Output` non-Drop, + which made `forget` a no-op and tripped clippy's `forget_non_drop` lint. - UART boards (heltec_v3_uart) display stuck on splash screen after boot. Host task sent `DisplayCommand::Reset` (which sets disconnected=true) instead of `DisplayCommand::On`. UART has no DTR, so nothing ever cleared it. diff --git a/Cargo.toml b/Cargo.toml index 65b3a31..a3789ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,12 @@ heltec_v4 = [ "dep:esp-println", "dep:esp-rtos", ] +elecrow_thinknode_m2 = [ + "dep:esp-backtrace", + "dep:esp-hal", + "dep:esp-println", + "dep:esp-rtos", +] rak_wisblock_4631 = [ "dep:cortex-m", "dep:cortex-m-rt", diff --git a/README.md b/README.md index c08fa1c..8fd18f2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ All commands are run from the `firmware/` directory. | Heltec V3 | `heltec_v3` | ESP32-S3 | SX1262 | Native CDC-ACM | xtensa-esp32s3-none-elf | | Heltec V3 (UART) | `heltec_v3_uart` | ESP32-S3 | SX1262 | CP2102 bridge | xtensa-esp32s3-none-elf | | Heltec V4 | `heltec_v4` | ESP32-S3 | SX1262 | Native CDC-ACM | xtensa-esp32s3-none-elf | +| ELECROW ThinkNode-M2 | `elecrow_thinknode_m2` | ESP32-S3 | SX1262 | CP2102 bridge | xtensa-esp32s3-none-elf | | RAK WisBlock 4631 | `rak_wisblock_4631` | nRF52840 | SX1262 | Native CDC-ACM | thumbv7em-none-eabihf | | Waveshare RP2040-LoRa | `waveshare_rp2040_lora` | RP2040 | SX1262 | Native CDC-ACM | thumbv6m-none-eabi | | Wio Tracker L1 | `wio_tracker_l1` | nRF52840 | SX1262 | Native CDC-ACM | thumbv7em-none-eabihf | diff --git a/justfile b/justfile index c12dea9..3cf052c 100644 --- a/justfile +++ b/justfile @@ -1,11 +1,15 @@ set shell := ["bash", "-c"] # Board definitions: feature target chip -# Xtensa targets use nightly cargo + esp rustc + -Zbuild-std=core. +# Xtensa targets use the self-contained esp toolchain (espup-installed) with +# `-Zbuild-std=core,alloc`. Using `+esp` rather than `+nightly` keeps the +# compiler and rust-src versions aligned — the public nightly drifts ahead of +# the esp fork and breaks core-stdlib compilation otherwise. # Tool versions are pinned in mise.toml; run `just setup` to install everything. heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3" heltec_v3_uart := "heltec_v3_uart xtensa-esp32s3-none-elf esp32s3" heltec_v4 := "heltec_v4 xtensa-esp32s3-none-elf esp32s3" +elecrow_thinknode_m2 := "elecrow_thinknode_m2 xtensa-esp32s3-none-elf esp32s3" rak_wisblock_4631 := "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA" wio_tracker_l1 := "wio_tracker_l1 thumbv7em-none-eabihf nRF52840_xxAA" waveshare_rp2040_lora := "waveshare_rp2040_lora thumbv6m-none-eabi rp2040" @@ -14,7 +18,7 @@ builds_dir := "builds" version := `sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml` # All known boards -boards := "heltec_v3 heltec_v3_uart heltec_v4 rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1" +boards := "heltec_v3 heltec_v3_uart heltec_v4 elecrow_thinknode_m2 rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1" # Install all required tools and toolchains setup: @@ -111,18 +115,18 @@ _ensure_tools: @mise trust --yes . 2>/dev/null; mise install --quiet # Run a cargo command for a board. -# Xtensa: nightly cargo + esp rustc (via RUSTC override) + -Zbuild-std=core +# Xtensa: esp toolchain (cargo + rustc + rust-src together) + -Zbuild-std=core [private] _cargo board cmd extra_args="": @just _ensure_tools @read -r feat target chip <<< "$(just _info {{board}})"; \ - env=""; extra=""; \ + extra=""; buildstd=""; \ case "$target" in xtensa-*) \ just _require_esp_toolchain; \ [ -f "$HOME/export-esp.sh" ] && . "$HOME/export-esp.sh"; \ - env="RUSTC=$(rustup which rustc --toolchain esp)"; extra="+nightly"; buildstd="-Zbuild-std=core,alloc";; \ + extra="+esp"; buildstd="-Zbuild-std=core,alloc";; \ esac; \ - eval $env cargo $extra {{cmd}} --target "$target" --features "$feat" $buildstd {{extra_args}} + cargo $extra {{cmd}} --target "$target" --features "$feat" $buildstd {{extra_args}} # Ensure a board's toolchain is available, auto-installing if needed [private] @@ -133,17 +137,15 @@ _can_build board: *) rustup target list --installed | grep -q "^$target$" || rustup target add "$target" ;; \ esac -# Ensure the ESP Xtensa toolchain is installed (espup installed via mise) +# Ensure the ESP Xtensa toolchain is installed (espup installed via mise). +# The esp toolchain ships with its own cargo, rustc, and rust-src — no extra +# components are needed on top. [private] _require_esp_toolchain: @just _ensure_tools @if ! rustup toolchain list | grep -q "^esp"; then \ echo "ESP toolchain not found, installing via espup (this may take a while)..." >&2; \ espup install || { echo "error: espup install failed" >&2; exit 1; }; \ - fi; \ - if ! rustup component list --toolchain nightly --installed 2>/dev/null | grep -q "^rust-src"; then \ - echo "Installing nightly rust-src (needed for -Zbuild-std)..." >&2; \ - rustup component add rust-src --toolchain nightly || { echo "error: failed to install rust-src" >&2; exit 1; }; \ fi # Find a serial port matching any of the given VID:PID pairs (checked in order) @@ -211,6 +213,7 @@ _info name: @if [ "{{name}}" == "heltec_v3" ]; then echo "{{heltec_v3}}"; \ elif [ "{{name}}" == "heltec_v3_uart" ]; then echo "{{heltec_v3_uart}}"; \ elif [ "{{name}}" == "heltec_v4" ]; then echo "{{heltec_v4}}"; \ + elif [ "{{name}}" == "elecrow_thinknode_m2" ]; then echo "{{elecrow_thinknode_m2}}"; \ elif [ "{{name}}" == "rak_wisblock_4631" ]; then echo "{{rak_wisblock_4631}}"; \ elif [ "{{name}}" == "waveshare_rp2040_lora" ]; then echo "{{waveshare_rp2040_lora}}"; \ elif [ "{{name}}" == "wio_tracker_l1" ]; then echo "{{wio_tracker_l1}}"; \ diff --git a/src/board/elecrow_thinknode_m2.rs b/src/board/elecrow_thinknode_m2.rs new file mode 100644 index 0000000..90bda3f --- /dev/null +++ b/src/board/elecrow_thinknode_m2.rs @@ -0,0 +1,141 @@ +//! ELECROW ThinkNode-M2 — ESP32-S3 Meshtastic handset with SH1106 OLED. +//! +//! Tamagotchi-form-factor handheld: ESP32-S3-WROOM-1-N4 + SX1262 + 1.3" SH1106 +//! OLED + CP2102 UART bridge + 1000 mAh Li-Po. Sold pre-loaded with Meshtastic; +//! this firmware turns it into a DongLoRa radio bridge. +//! +//! Pin map (verified from Meshtastic's variants/esp32s3/ELECROW-ThinkNode-M2 +//! and PR meshtastic/firmware#6354): +//! LoRa SPI: SCK=12, MOSI=11, MISO=13, CS=10 +//! LoRa ctl: RESET=21, BUSY=14, DIO1=3 +//! LoRa pwr: GPIO48 (must be HIGH before any SPI traffic) +//! OLED: I2C0 SDA=16, SCL=15, SH1106 @ 0x3C +//! VEXT: GPIO46 (active HIGH — gates OLED supply) +//! UART: TX=43, RX=44 via CP2102 bridge +//! LED: GPIO6 (active HIGH indicator) +//! +//! Notable differences from the Heltec ESP32-S3 boards: +//! * Display is SH1106 (not SSD1306) — uses the existing sh1106 driver. +//! * VEXT_ENABLE polarity is reversed (active HIGH here, active LOW on Heltec). +//! * Dedicated LoRa power enable on GPIO48 — must be sequenced before SPI init. +//! * TCXO is driven at 3.3 V via DIO3 (Heltec uses 1.8 V). + +use esp_hal::gpio::{Level, Output, OutputConfig}; + +use super::esp32s3; +use super::traits::{BoardParts, LoRaBoard}; +use crate::driver::sh1106::Sh1106; +use crate::driver::simple_led::SimpleLed; +use crate::hal::esp32s3 as mcu; + +pub use super::esp32s3::{DisplayI2c, RadioDriver, RadioParts}; + +// ── Concrete peripheral types ─────────────────────────────────────── + +pub type DisplayDriver = Sh1106; +pub type UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>; +pub type LedDriver = SimpleLed>; + +// ── Peripheral bundles ────────────────────────────────────────────── + +pub struct UartParts { + pub driver: UartDriver, +} + +pub struct DisplayParts { + pub i2c: DisplayI2c, +} + +// ── Display init ──────────────────────────────────────────────────── + +pub async fn create_display(i2c: DisplayI2c) -> Option { + let mut display = Sh1106::new(i2c, 0x3C); + + embassy_time::Timer::after_millis(100).await; + if display.init().await.is_err() { + defmt::error!("SH1106 display init failed"); + return None; + } + let _ = display.set_brightness(0xFF).await; + Some(display) +} + +// ── Board init ────────────────────────────────────────────────────── + +pub struct Board { + p: esp_hal::peripherals::Peripherals, +} + +impl LoRaBoard for Board { + const NAME: &'static str = "ELECROW ThinkNode-M2"; + const TX_POWER_RANGE: (i8, i8) = (-9, 22); + + type RadioParts = RadioParts; + type CommParts = UartParts; + type DisplayParts = DisplayParts; + type DisplayDriver = DisplayDriver; + type LedDriver = LedDriver; + + fn init() -> Self { + let p = esp_hal::init(esp_hal::Config::default()); + Self { p } + } + + fn mac_address() -> [u8; 6] { + mcu::mac_address() + } + + fn into_parts(self) -> BoardParts { + let p = self.p; + + mcu::start_timer(p.TIMG0, p.SW_INTERRUPT); + + // VEXT_ENABLE: GPIO46, active HIGH — powers OLED rail (opposite of Heltec). + // Output has no Drop impl, so the pin stays HIGH for program lifetime. + let _vext = Output::new(p.GPIO46, Level::High, OutputConfig::default()); + esp_hal::delay::Delay::new().delay_millis(10); + + // LORA_POWER_EN: GPIO48, active HIGH — must be HIGH before any SPI traffic. + let _lora_pwr = Output::new(p.GPIO48, Level::High, OutputConfig::default()); + esp_hal::delay::Delay::new().delay_millis(10); + + // SPI2 for SX1262: SCK=12, MOSI=11, MISO=13 + let spi_bus = mcu::init_spi(p.SPI2, p.DMA_CH0, p.GPIO12, p.GPIO11, p.GPIO13); + + // SX1262: CS=10, RESET=21, DIO1=3, BUSY=14, TCXO driven at 3.3 V via DIO3. + let radio = esp32s3::init_radio( + spi_bus, + p.GPIO10, + p.GPIO21, + p.GPIO3, + p.GPIO14, + lora_phy::sx126x::TcxoCtrlVoltage::Ctrl3V3, + ); + + // UART0 via CP2102 bridge: TX=43, RX=44 + use esp_hal::uart::{Config as UartConfig, Uart}; + let host = UartParts { + driver: Uart::new(p.UART0, UartConfig::default()) + .expect("UART init") + .with_tx(p.GPIO43) + .with_rx(p.GPIO44) + .into_async(), + }; + + // I2C0 for SH1106 OLED: SDA=16, SCL=15 + let i2c = mcu::init_i2c(p.I2C0, p.GPIO16, p.GPIO15); + let display = Some(DisplayParts { i2c }); + + // LED on GPIO6 (active HIGH) + let led_pin = Output::new(p.GPIO6, Level::Low, OutputConfig::default()); + let led = SimpleLed(led_pin); + + BoardParts { + radio, + host, + display, + led, + mac: Self::mac_address(), + } + } +} diff --git a/src/board/esp32s3.rs b/src/board/esp32s3.rs index 1a1cda5..512cd6b 100644 --- a/src/board/esp32s3.rs +++ b/src/board/esp32s3.rs @@ -7,7 +7,7 @@ use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_time::Delay; -use esp_hal::gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull}; +use esp_hal::gpio::{Input, InputConfig, InputPin, Level, Output, OutputConfig, OutputPin, Pull}; use lora_phy::iv::GenericSx126xInterfaceVariant; use lora_phy::sx126x::{self, Sx1262, Sx126x}; @@ -32,12 +32,17 @@ pub struct RadioParts { pub delay: Delay, } +// SSD1306 display support is Heltec-specific. ESP32-S3 boards that use a +// different controller (e.g. ThinkNode-M2 uses SH1106) provide their own +// DisplayParts / DisplayDriver / create_display directly. +#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))] pub struct DisplayParts { pub i2c: DisplayI2c, } // ── Display driver ────────────────────────────────────────────────── +#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))] pub type DisplayDriver = ssd1306::Ssd1306Async< ssd1306::prelude::I2CInterface, ssd1306::size::DisplaySize128x64, @@ -45,6 +50,7 @@ pub type DisplayDriver = ssd1306::Ssd1306Async< >; /// Construct and initialize an SSD1306 display from raw I2C. +#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))] pub async fn create_display(i2c: DisplayI2c) -> Option { use ssd1306::mode::DisplayConfigAsync; use ssd1306::prelude::{Brightness, DisplayRotation}; @@ -67,12 +73,17 @@ pub async fn create_display(i2c: DisplayI2c) -> Option { // ── Shared peripheral init helpers ────────────────────────────────── /// Construct SX1262 radio from an initialized SPI bus. +/// +/// Generic over GPIO type so boards with different radio pin assignments can +/// share this helper. `tcxo_voltage` selects the DIO3-driven TCXO supply: +/// Heltec boards use `Ctrl1V8`, the ELECROW ThinkNode-M2 uses `Ctrl3V3`, etc. pub fn init_radio( spi_bus: &'static embassy_sync::mutex::Mutex, - nss_pin: esp_hal::peripherals::GPIO8<'static>, - reset_pin: esp_hal::peripherals::GPIO12<'static>, - dio1_pin: esp_hal::peripherals::GPIO14<'static>, - busy_pin: esp_hal::peripherals::GPIO13<'static>, + nss_pin: impl OutputPin + 'static, + reset_pin: impl OutputPin + 'static, + dio1_pin: impl InputPin + 'static, + busy_pin: impl InputPin + 'static, + tcxo_voltage: sx126x::TcxoCtrlVoltage, ) -> RadioParts { let nss = Output::new(nss_pin, Level::High, OutputConfig::default()); let spi_device = SpiDevice::new(spi_bus, nss); @@ -86,7 +97,7 @@ pub fn init_radio( let sx_config = sx126x::Config { chip: Sx1262, - tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V8), + tcxo_ctrl: Some(tcxo_voltage), use_dcdc: true, rx_boost: false, }; diff --git a/src/board/heltec_v3.rs b/src/board/heltec_v3.rs index 25f0ee2..854beb6 100644 --- a/src/board/heltec_v3.rs +++ b/src/board/heltec_v3.rs @@ -53,12 +53,19 @@ impl LoRaBoard for Board { mcu::start_timer(p.TIMG0, p.SW_INTERRUPT); - // Vext power: GPIO36, active LOW to enable peripherals - let vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default()); - core::mem::forget(vext); + // Vext power: GPIO36, active LOW to enable peripherals. + // Output has no Drop impl, so the pin stays configured for program lifetime. + let _vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default()); let spi_bus = mcu::init_spi(p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11); - let radio = esp32s3::init_radio(spi_bus, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13); + let radio = esp32s3::init_radio( + spi_bus, + p.GPIO8, + p.GPIO12, + p.GPIO14, + p.GPIO13, + lora_phy::sx126x::TcxoCtrlVoltage::Ctrl1V8, + ); // Note: switches internal USB PHY from Serial-JTAG to OTG. let usb_inst = esp_hal::otg_fs::Usb::new(p.USB0, p.GPIO20, p.GPIO19); @@ -72,12 +79,12 @@ impl LoRaBoard for Board { ), }; - // SSD1306 display reset: pulse GPIO21 low->high before I2C init + // SSD1306 display reset: pulse GPIO21 low->high before I2C init. + // Output has no Drop impl; pin stays HIGH after this block. let mut display_rst = Output::new(p.GPIO21, Level::Low, OutputConfig::default()); esp_hal::delay::Delay::new().delay_millis(10); display_rst.set_high(); esp_hal::delay::Delay::new().delay_millis(10); - core::mem::forget(display_rst); let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18); let display = Some(DisplayParts { i2c }); diff --git a/src/board/heltec_v3_uart.rs b/src/board/heltec_v3_uart.rs index 5060416..dbc0633 100644 --- a/src/board/heltec_v3_uart.rs +++ b/src/board/heltec_v3_uart.rs @@ -52,12 +52,19 @@ impl LoRaBoard for Board { mcu::start_timer(p.TIMG0, p.SW_INTERRUPT); - // Vext power: GPIO36, active LOW to enable peripherals - let vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default()); - core::mem::forget(vext); + // Vext power: GPIO36, active LOW to enable peripherals. + // Output has no Drop impl, so the pin stays configured for program lifetime. + let _vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default()); let spi_bus = mcu::init_spi(p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11); - let radio = esp32s3::init_radio(spi_bus, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13); + let radio = esp32s3::init_radio( + spi_bus, + p.GPIO8, + p.GPIO12, + p.GPIO14, + p.GPIO13, + lora_phy::sx126x::TcxoCtrlVoltage::Ctrl1V8, + ); use esp_hal::uart::{Config as UartConfig, Uart}; let host = UartParts { @@ -68,12 +75,12 @@ impl LoRaBoard for Board { .into_async(), }; - // SSD1306 display reset: pulse GPIO21 low->high before I2C init + // SSD1306 display reset: pulse GPIO21 low->high before I2C init. + // Output has no Drop impl; pin stays HIGH after this block. let mut display_rst = Output::new(p.GPIO21, Level::Low, OutputConfig::default()); esp_hal::delay::Delay::new().delay_millis(10); display_rst.set_high(); esp_hal::delay::Delay::new().delay_millis(10); - core::mem::forget(display_rst); let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18); let display = Some(DisplayParts { i2c }); diff --git a/src/board/heltec_v4.rs b/src/board/heltec_v4.rs index b9bcb6e..7fe0179 100644 --- a/src/board/heltec_v4.rs +++ b/src/board/heltec_v4.rs @@ -46,12 +46,19 @@ impl LoRaBoard for Board { mcu::start_timer(p.TIMG0, p.SW_INTERRUPT); - // Vext power: GPIO36, active LOW to enable peripherals - let vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default()); - core::mem::forget(vext); + // Vext power: GPIO36, active LOW to enable peripherals. + // Output has no Drop impl, so the pin stays configured for program lifetime. + let _vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default()); let spi_bus = mcu::init_spi(p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11); - let radio = esp32s3::init_radio(spi_bus, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13); + let radio = esp32s3::init_radio( + spi_bus, + p.GPIO8, + p.GPIO12, + p.GPIO14, + p.GPIO13, + lora_phy::sx126x::TcxoCtrlVoltage::Ctrl1V8, + ); // Note: switches internal USB PHY from Serial-JTAG to OTG. let usb_inst = esp_hal::otg_fs::Usb::new(p.USB0, p.GPIO20, p.GPIO19); @@ -65,12 +72,12 @@ impl LoRaBoard for Board { ), }; - // SSD1315 display reset: pulse GPIO21 low->high before I2C init + // SSD1315 display reset: pulse GPIO21 low->high before I2C init. + // Output has no Drop impl; pin stays HIGH after this block. let mut display_rst = Output::new(p.GPIO21, Level::Low, OutputConfig::default()); esp_hal::delay::Delay::new().delay_millis(10); display_rst.set_high(); esp_hal::delay::Delay::new().delay_millis(10); - core::mem::forget(display_rst); let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18); let display = Some(DisplayParts { i2c }); diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 72b02eb..0b48406 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -1,5 +1,5 @@ -#[cfg(feature = "wio_tracker_l1")] +#[cfg(any(feature = "wio_tracker_l1", feature = "elecrow_thinknode_m2"))] pub mod sh1106; -#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))] +#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))] pub mod simple_led; diff --git a/src/hal/esp32s3.rs b/src/hal/esp32s3.rs index 36ba83f..746e0c6 100644 --- a/src/hal/esp32s3.rs +++ b/src/hal/esp32s3.rs @@ -6,6 +6,7 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; +use esp_hal::gpio::interconnect::{PeripheralInput, PeripheralOutput}; use esp_hal::i2c::master::{Config as I2cConfig, I2c}; use esp_hal::spi::master::{Config as SpiConfig, Spi, SpiDmaBus}; use esp_hal::spi::Mode as SpiMode; @@ -33,12 +34,16 @@ pub fn start_timer( // ── SPI bus ───────────────────────────────────────────────────────── /// Initialize SPI2 with DMA and wrap in a shared bus (StaticCell + Mutex). +/// +/// Generic over pin type so boards with different SPI pin assignments can +/// share this helper. Any GPIO that implements the esp-hal interconnect +/// traits (which is every GPIO on the ESP32-S3) can be passed directly. pub fn init_spi( spi2: esp_hal::peripherals::SPI2<'static>, dma_ch0: esp_hal::peripherals::DMA_CH0<'static>, - sck: esp_hal::peripherals::GPIO9<'static>, - mosi: esp_hal::peripherals::GPIO10<'static>, - miso: esp_hal::peripherals::GPIO11<'static>, + sck: impl PeripheralOutput<'static>, + mosi: impl PeripheralOutput<'static>, + miso: impl PeripheralInput<'static>, ) -> &'static embassy_sync::mutex::Mutex { let spi = Spi::new( spi2, @@ -66,10 +71,13 @@ pub fn init_spi( // ── I2C bus ───────────────────────────────────────────────────────── /// Initialize I2C0 for peripherals (display, sensors, etc.). +/// +/// Generic over pin type. I2C lines are bidirectional (open-drain), so both +/// `PeripheralInput` and `PeripheralOutput` bounds are required. pub fn init_i2c( i2c0: esp_hal::peripherals::I2C0<'static>, - sda: esp_hal::peripherals::GPIO17<'static>, - scl: esp_hal::peripherals::GPIO18<'static>, + sda: impl PeripheralInput<'static> + PeripheralOutput<'static>, + scl: impl PeripheralInput<'static> + PeripheralOutput<'static>, ) -> I2cBus { I2c::new(i2c0, I2cConfig::default()) .expect("I2C init") diff --git a/src/hal/mod.rs b/src/hal/mod.rs index d90afa1..6fac872 100644 --- a/src/hal/mod.rs +++ b/src/hal/mod.rs @@ -3,7 +3,7 @@ //! Each submodule provides low-level bus and peripheral init for one MCU family. //! Board files call these with specific pins and construct higher-level drivers. -#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))] +#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2"))] pub mod esp32s3; #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))] diff --git a/src/host/mod.rs b/src/host/mod.rs index 15467ba..9b39ddb 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -6,7 +6,7 @@ pub mod framing; cfg_if::cfg_if! { - if #[cfg(feature = "heltec_v3_uart")] { + if #[cfg(any(feature = "heltec_v3_uart", feature = "elecrow_thinknode_m2"))] { mod uart; pub use uart::host_task; } else { diff --git a/src/main.rs b/src/main.rs index f41e83d..49daed6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,7 @@ cfg_if::cfg_if! { if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))] { use defmt_rtt as _; use panic_probe as _; - } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))] { + } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2"))] { use esp_backtrace as _; use esp_println as _; } @@ -66,7 +66,7 @@ cfg_if::cfg_if! { async fn main(spawner: Spawner) { run(spawner).await; } - } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))] { + } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2"))] { #[esp_rtos::main] async fn main(spawner: Spawner) { run(spawner).await;