diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d520c..da8513b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added + +- LilyGo T-Beam S3 Supreme board support (ESP32-S3 + SX1262 + SH1106 + OLED + AXP2101 PMIC, native USB-C). + - Pin map verified against MeshCore's + `variants/lilygo_tbeam_supreme_SX1262`. + - New `src/driver/axp.rs` module with an AXP2101 init sequence + (ALDO3 = 3.3 V LoRa, ALDO1 = 3.3 V OLED). The PMIC gates LoRa and + display power — without it the SX1262 never responds on SPI. + - PMIC lives on a dedicated I²C1 bus (SDA=42, SCL=41), separate + from the SH1106 on I²C0 (SDA=17, SCL=18). I²C1 is used blocking + for a one-shot init and then dropped. + - TCXO driven at 1.6 V via DIO3 (MeshCore uses the same). + ## [1.2.1] - 2026-04-23 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 392e587..2c8d317 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,13 @@ elecrow_thinknode_m2 = [ "dep:esp-println", "dep:esp-rtos", ] +lilygo_tbeam_supreme = [ + "embassy", + "dep:esp-backtrace", + "dep:esp-hal", + "dep:esp-println", + "dep:esp-rtos", +] rak_wisblock_4631 = [ "embassy", "dep:cortex-m", diff --git a/README.md b/README.md index 8fd18f2..38afb64 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ All commands are run from the `firmware/` directory. | 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 | +| LilyGo T-Beam S3 Supreme | `lilygo_tbeam_supreme` | ESP32-S3 | SX1262 | Native CDC-ACM | 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 0d0c86c..e81b891 100644 --- a/justfile +++ b/justfile @@ -10,6 +10,7 @@ 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" +lilygo_tbeam_supreme := "lilygo_tbeam_supreme 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" @@ -18,7 +19,7 @@ builds_dir := "builds" version := `sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml` # All known boards -boards := "heltec_v3 heltec_v3_uart heltec_v4 elecrow_thinknode_m2 rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1" +boards := "heltec_v3 heltec_v3_uart heltec_v4 elecrow_thinknode_m2 lilygo_tbeam_supreme rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1" # Install all required tools and toolchains setup: @@ -214,6 +215,7 @@ _info name: 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}}" == "lilygo_tbeam_supreme" ]; then echo "{{lilygo_tbeam_supreme}}"; \ 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/lilygo_tbeam_supreme.rs b/src/board/lilygo_tbeam_supreme.rs new file mode 100644 index 0000000..ee70216 --- /dev/null +++ b/src/board/lilygo_tbeam_supreme.rs @@ -0,0 +1,153 @@ +//! LilyGo T-Beam S3 Supreme — ESP32-S3 + SX1262 + SH1106 OLED + AXP2101 PMIC. +//! +//! Big-brother of the classic T-Beam: ESP32-S3-WROOM-1 module, SX1262 with a +//! TCXO, 128×64 SH1106 OLED, AXP2101 PMIC on a dedicated I²C1 bus. Native USB +//! (no UART bridge chip). +//! +//! The AXP2101 gates power to the SX1262 and OLED. Without its init sequence +//! the LoRa chip never responds on SPI and `LoRa::new` fails. +//! +//! Pin map (verified from MeshCore variants/lilygo_tbeam_supreme_SX1262/): +//! LoRa SPI: SCK=12, MOSI=11, MISO=13, CS=10 +//! LoRa ctl: RESET=5, DIO1=1, BUSY=4 (TCXO @ 1.6 V via DIO3) +//! OLED I²C: SDA=17, SCL=18, SH1106 @ 0x3C (I²C0) +//! PMIC I²C: SDA=42, SCL=41, AXP2101 @ 0x34 (I²C1, blocking, PMIC-only) +//! USB: native USB-C on GPIO19/20 (fixed ESP32-S3 D+/D-) +//! LED: GPIO6 (active HIGH indicator, reuses MeshCore's P_LORA_TX_LED) +//! +//! GPS / battery / IMU / SD / BME280 are unused — DongLoRa is a transparent +//! LoRa-over-USB bridge. The PMIC init only enables ALDO1 (OLED) and ALDO3 +//! (LoRa); other rails are left at their AXP2101 power-on defaults. + +use esp_hal::gpio::{Level, Output, OutputConfig}; +use esp_hal::i2c::master::{Config as I2cConfig, I2c}; +use static_cell::StaticCell; + +use super::esp32s3; +use super::traits::{BoardParts, LoRaBoard}; +use crate::driver::axp; +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 UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>; +pub type LedDriver = SimpleLed>; + +// ── Peripheral bundles ────────────────────────────────────────────── + +pub struct UsbParts { + pub driver: UsbDriver, +} + +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 = "LilyGo T-Beam S3 Supreme"; + const TX_POWER_RANGE: (i8, i8) = (-9, 22); + + type RadioParts = RadioParts; + type CommParts = UsbParts; + 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); + + // AXP2101 PMIC on I²C1 (SDA=42, SCL=41). Blocking, one-shot: we only + // need to flip the LoRa/OLED rails on, then the bus can be dropped. + // If this fails the LoRa chip stays unpowered — log and keep going so + // the rest of the boot path can still produce diagnostics. + { + let mut pmic = I2c::new(p.I2C1, I2cConfig::default()) + .expect("I2C1 init") + .with_sda(p.GPIO42) + .with_scl(p.GPIO41); + if let Err(_e) = axp::axp2101_init_lora_oled(&mut pmic) { + defmt::error!("AXP2101 init failed — LoRa rail may be dead"); + } + } + // Let ALDO1/ALDO3 settle before touching the LoRa SPI or OLED I²C. + esp_hal::delay::Delay::new().delay_millis(20); + + // 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=5, DIO1=1, BUSY=4, TCXO @ 1.6 V via DIO3. + let radio = esp32s3::init_radio( + spi_bus, + p.GPIO10, + p.GPIO5, + p.GPIO1, + p.GPIO4, + lora_phy::sx126x::TcxoCtrlVoltage::Ctrl1V6, + ); + + // Native USB-C on GPIO19/20. Switches the internal USB PHY from + // Serial-JTAG to OTG. + let usb_inst = esp_hal::otg_fs::Usb::new(p.USB0, p.GPIO20, p.GPIO19); + static EP_OUT_BUF: StaticCell<[u8; 1024]> = StaticCell::new(); + let ep_out_buf = EP_OUT_BUF.init([0u8; 1024]); + let host = UsbParts { + driver: esp_hal::otg_fs::asynch::Driver::new( + usb_inst, + ep_out_buf, + esp_hal::otg_fs::asynch::Config::default(), + ), + }; + + // I2C0 for SH1106 OLED: SDA=17, SCL=18 (async, handed to display task). + let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18); + let display = Some(DisplayParts { i2c }); + + // Activity 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/driver/axp.rs b/src/driver/axp.rs new file mode 100644 index 0000000..e77044a --- /dev/null +++ b/src/driver/axp.rs @@ -0,0 +1,64 @@ +//! AXP PMIC init — turn on the rails that feed LoRa and the OLED. +//! +//! T-Beam–class boards gate LoRa, OLED (and GPS/sensors, which this firmware +//! does not use) through an X-Powers PMIC on I²C. Until the PMIC is told to +//! enable the right LDOs/DCDCs, the SX126x/SX127x chip sits unpowered and +//! `LoRa::new` fails. This module's only job is to flip those switches. +//! +//! Two PMICs are covered: +//! +//! * [`axp2101_init_lora_oled`] — LilyGo T-Beam S3 Supreme. ALDO3 = 3.3 V +//! feeds the SX1262; ALDO1 = 3.3 V feeds the SH1106 OLED. Both live on a +//! dedicated I²C1 bus (separate from the display). +//! * [`axp192_init_lora_oled`] — LilyGo T-Beam (classic). LDO2 = 3.3 V feeds +//! the SX1276; DCDC1 = 3.3 V feeds the SSD1306 OLED. PMIC shares the +//! display I²C bus. +//! +//! Everything unrelated to LoRa + OLED (GPS, sensors, SD, batt ADC) is left +//! alone. Out-of-scope for a transparent LoRa-over-USB bridge. +//! +//! Init is blocking on purpose. It runs once during `into_parts`, which is a +//! synchronous function called before task spawning. Using the blocking +//! `embedded_hal` trait keeps the PMIC wiring out of the async machinery and +//! lets the caller drop the I²C bus immediately after init (no bus handoff). + +use embedded_hal::i2c::I2c; + +/// Both chips respond at this 7-bit I²C address. +const PMIC_ADDR: u8 = 0x34; + +// ── AXP2101 (T-Beam S3 Supreme) ────────────────────────────────────── + +/// LDO on/off control register. Bits: +/// 0 = ALDO1, 1 = ALDO2, 2 = ALDO3, 3 = ALDO4, 4 = BLDO1, 5 = BLDO2. +const AXP2101_REG_LDO_ONOFF: u8 = 0x90; +/// ALDO1 voltage register (OLED rail on T-Beam Supreme). +const AXP2101_REG_ALDO1_V: u8 = 0x92; +/// ALDO3 voltage register (LoRa rail on T-Beam Supreme). +const AXP2101_REG_ALDO3_V: u8 = 0x94; + +/// Voltage code for 3.3 V on ALDOx: 0.5 V + 28 × 100 mV. +const AXP2101_V_3V3: u8 = 28; + +/// Mask enabling ALDO1 + ALDO3 in [`AXP2101_REG_LDO_ONOFF`]. +const AXP2101_ALDO1_ALDO3: u8 = 0b0000_0101; + +/// Bring up LoRa (ALDO3) and OLED (ALDO1) rails at 3.3 V. +/// +/// Voltage registers are set before the enable bits to avoid glitching the +/// rails through the reset-default voltage. Other rails are left untouched +/// (read-modify-write on the enable register) so any board-specific OTP +/// defaults stay intact. +pub fn axp2101_init_lora_oled(i2c: &mut I) -> Result<(), I::Error> { + i2c.write(PMIC_ADDR, &[AXP2101_REG_ALDO3_V, AXP2101_V_3V3])?; + i2c.write(PMIC_ADDR, &[AXP2101_REG_ALDO1_V, AXP2101_V_3V3])?; + + let mut onoff = [0u8; 1]; + i2c.write_read(PMIC_ADDR, &[AXP2101_REG_LDO_ONOFF], &mut onoff)?; + i2c.write( + PMIC_ADDR, + &[AXP2101_REG_LDO_ONOFF, onoff[0] | AXP2101_ALDO1_ALDO3], + )?; + + Ok(()) +} diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 8802112..ce50268 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -1,11 +1,19 @@ -#[cfg(any(feature = "wio_tracker_l1", feature = "elecrow_thinknode_m2"))] +#[cfg(any( + feature = "wio_tracker_l1", + feature = "elecrow_thinknode_m2", + feature = "lilygo_tbeam_supreme" +))] pub mod sh1106; +#[cfg(feature = "lilygo_tbeam_supreme")] +pub mod axp; + #[cfg(any( feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2", + feature = "lilygo_tbeam_supreme", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora" ))] @@ -51,7 +59,11 @@ where } } -#[cfg(any(feature = "wio_tracker_l1", feature = "elecrow_thinknode_m2"))] +#[cfg(any( + feature = "wio_tracker_l1", + feature = "elecrow_thinknode_m2", + feature = "lilygo_tbeam_supreme" +))] impl DisplayBrightness for sh1106::Sh1106 where I: embedded_hal_async::i2c::I2c, diff --git a/src/hal/mod.rs b/src/hal/mod.rs index 9e34d95..a671676 100644 --- a/src/hal/mod.rs +++ b/src/hal/mod.rs @@ -7,7 +7,8 @@ feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", - feature = "elecrow_thinknode_m2" + feature = "elecrow_thinknode_m2", + feature = "lilygo_tbeam_supreme" ))] pub mod esp32s3; diff --git a/src/main.rs b/src/main.rs index 11e5d88..9b53c76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,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", feature = "elecrow_thinknode_m2"))] { + } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2", feature = "lilygo_tbeam_supreme"))] { use esp_backtrace as _; use esp_println as _; } @@ -72,7 +72,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", feature = "elecrow_thinknode_m2"))] { + } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2", feature = "lilygo_tbeam_supreme"))] { #[esp_rtos::main] async fn main(spawner: Spawner) { run(spawner).await;