feat(boards): LilyGo T-Beam S3 Supreme support
ESP32-S3 + SX1262 + SH1106 OLED + AXP2101 PMIC, native USB-C. The AXP2101 PMIC gates power to the LoRa chip and the OLED. Without its init sequence (ALDO3 = 3.3 V for LoRa, ALDO1 = 3.3 V for OLED) the SX1262 never responds on SPI and radio init fails. Pin map verified against MeshCore's variants/lilygo_tbeam_supreme_SX1262. 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 at 1.6 V via DIO3. New src/driver/axp.rs holds the AXP2101 init sequence — kept minimal (only the LoRa + OLED rails), since DongLoRa is a LoRa-over-USB bridge and every other rail (GPS, BME280, IMU, SD, batt ADC) is out of scope.
This commit is contained in:
9 files changed
+260
-6
No files matched your search
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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}}"; \
|
||||
|
||||
@@ -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<DisplayI2c>;
|
||||
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||
pub type LedDriver = SimpleLed<Output<'static>>;
|
||||
|
||||
// ── Peripheral bundles ──────────────────────────────────────────────
|
||||
|
||||
pub struct UsbParts {
|
||||
pub driver: UsbDriver,
|
||||
}
|
||||
|
||||
pub struct DisplayParts {
|
||||
pub i2c: DisplayI2c,
|
||||
}
|
||||
|
||||
// ── Display init ────────────────────────────────────────────────────
|
||||
|
||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
||||
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<RadioParts, UsbParts, DisplayParts, LedDriver> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<I: I2c>(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(())
|
||||
}
|
||||
+14
-2
@@ -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<I> DisplayBrightness for sh1106::Sh1106<I>
|
||||
where
|
||||
I: embedded_hal_async::i2c::I2c,
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user