From a1b92356b39b5aadeaea13e26d6a1eae564700a1 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 23 Apr 2026 10:33:06 -0600 Subject: [PATCH] feat(boards): LilyGo T-Beam (classic) support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First classic-ESP32 (non-S3) board in the firmware, and first SX127x radio. ESP32 + SX1276 + SSD1306 OLED + AXP192 PMIC, CP2102 USB-UART bridge. Pin map verified from MeshCore variants/lilygo_tbeam_SX1276. The AXP192 gates power to the LoRa chip and OLED. Its I²C bus is shared with the display, so the firmware builds I²C0 in blocking mode first for the PMIC init, then converts the same handle to async and hands it to the display task — avoids double-consuming the peripheral. Lora-phy's Sx127x support drives DIO0 as the sole IRQ (SX127x has no BUSY line); the chip runs from a crystal, tx_boost routes +20 dBm through PA_BOOST. The `esp-hal` / `esp-rtos` / `esp-backtrace` / `esp-println` deps have been restructured so the chip flavour (esp32 vs esp32s3) is selected at the per-board feature level instead of baked into the dep lines. This keeps the existing S3 boards building on esp32s3 while the new T-Beam feature selects classic esp32 via the same dependency set. LoRaBoard gains supported_sf_bitmap() / supported_bw_bitmap() methods (with SX126x defaults) so the T-Beam can report SF6–SF12 via GET_INFO — the SX1276 has no SF5. radio_chip_id() reports 0x0011 per donglora-protocol RadioChipId::Sx1276. freq_range_hz widens to the SX1276 datasheet span of 137 MHz–1020 MHz. New modules: - src/hal/esp32.rs — classic-ESP32 MCU primitives - src/board/esp32.rs — Sx127x radio + SSD1306 display pipe - src/board/lilygo_tbeam.rs — the board file - src/driver/axp.rs — AXP192 init added alongside AXP2101 --- CHANGELOG.md | 32 +++++++++ Cargo.lock | 25 +++++++ Cargo.toml | 39 +++++++++-- README.md | 1 + justfile | 4 +- src/board/esp32.rs | 103 ++++++++++++++++++++++++++++ src/board/lilygo_tbeam.rs | 137 ++++++++++++++++++++++++++++++++++++++ src/board/mod.rs.j2 | 4 ++ src/board/traits.rs | 15 +++++ src/driver/axp.rs | 111 ++++++++++++++++++++++-------- src/driver/mod.rs | 3 +- src/hal/esp32.rs | 80 ++++++++++++++++++++++ src/hal/mod.rs | 3 + src/host/mod.rs | 6 +- src/main.rs | 14 ++-- 15 files changed, 534 insertions(+), 43 deletions(-) create mode 100644 src/board/esp32.rs create mode 100644 src/board/lilygo_tbeam.rs create mode 100644 src/hal/esp32.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index da8513b..f0c1fd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/). 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). +- LilyGo T-Beam (classic) board support (ESP32 + SX1276 + SSD1306 + OLED + AXP192 PMIC, CP2102 USB-UART bridge). This is the first + board in the firmware on classic ESP32 (non-S3) and the first on + the SX127x radio family. + - Pin map verified against MeshCore's `variants/lilygo_tbeam_SX1276`. + - New `src/hal/esp32.rs` (classic-ESP32 MCU primitives, peripheral + DMA via `DMA_SPI2`) and `src/board/esp32.rs` (SX1276 radio helper + + SSD1306 display pipe). + - AXP192 init added to `src/driver/axp.rs`: DCDC1 = 3.3 V for the + OLED, LDO2 = 3.3 V for the SX1276 PA_BOOST rail. Shares the + OLED I²C bus (SDA=21, SCL=22); the I²C instance is built in + blocking mode for PMIC init and converted to async for the + display task. + - SX127x support via lora-phy 3's `Sx127x` + + `GenericSx127xInterfaceVariant` (DIO0 as the sole IRQ; no + BUSY line on SX127x). XTAL clocked (no TCXO), `tx_boost = true` + for the PA_BOOST path. + - `LoRaBoard` trait gains `supported_sf_bitmap()` and + `supported_bw_bitmap()` methods with SX126x defaults + (`0x1FE0`, `0x03FF`). T-Beam classic overrides SF to `0x1FC0` + (SF6–SF12) — the SX1276 has no SF5. + - `radio_chip_id()` reports `0x0011` (`RadioChipId::Sx1276`) and + `freq_range_hz()` widens to 137 MHz–1020 MHz per the SX1276 + datasheet. + +### Changed + +- `Cargo.toml`: the `esp32s3` chip feature has been moved out of + the `esp-hal` / `esp-rtos` / `esp-backtrace` / `esp-println` + dependency lines and into each ESP board's feature list (as + `esp-hal/esp32s3`, etc.). This lets `lilygo_tbeam` select the + classic `esp32` chip flavour from the same dependency set. ## [1.2.1] - 2026-04-23 diff --git a/Cargo.lock b/Cargo.lock index b03477a..cd33702 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1060,6 +1060,8 @@ dependencies = [ "rand_core 0.6.4", "rand_core 0.9.5", "riscv", + "sha1", + "sha2", "strum", "ufmt-write", "xtensa-lx", @@ -1116,6 +1118,7 @@ dependencies = [ "cfg-if", "document-features", "esp-metadata-generated", + "esp32", "esp32s3", ] @@ -2204,6 +2207,28 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2-const-stable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2c8d317..b61b4e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,22 +45,20 @@ ssd1306 = { version = "0.10", features = ["async"] } embedded-io-async = "0.7" embassy-futures = "0.1" -# ESP32-S3 (Heltec V3) — requires esp toolchain (espup) +# Xtensa ESP crates — requires esp toolchain (espup). The chip feature +# (esp32 / esp32s3) is selected per board via the board-level feature flag +# below, so the same dep lines support both classic ESP32 and ESP32-S3. esp-backtrace = { version = "0.18", optional = true, features = [ - "esp32s3", "panic-handler", "defmt", ] } esp-hal = { version = "1.0", optional = true, features = [ - "esp32s3", "unstable", ] } esp-println = { version = "0.16", optional = true, features = [ - "esp32s3", "defmt-espflash", ] } esp-rtos = { version = "0.2", optional = true, features = [ - "esp32s3", "embassy", ] } @@ -110,6 +108,10 @@ heltec_v3 = [ "dep:esp-hal", "dep:esp-println", "dep:esp-rtos", + "esp-backtrace/esp32s3", + "esp-hal/esp32s3", + "esp-println/esp32s3", + "esp-rtos/esp32s3", ] heltec_v3_uart = [ "embassy", @@ -117,6 +119,10 @@ heltec_v3_uart = [ "dep:esp-hal", "dep:esp-println", "dep:esp-rtos", + "esp-backtrace/esp32s3", + "esp-hal/esp32s3", + "esp-println/esp32s3", + "esp-rtos/esp32s3", ] heltec_v4 = [ "embassy", @@ -124,6 +130,10 @@ heltec_v4 = [ "dep:esp-hal", "dep:esp-println", "dep:esp-rtos", + "esp-backtrace/esp32s3", + "esp-hal/esp32s3", + "esp-println/esp32s3", + "esp-rtos/esp32s3", ] elecrow_thinknode_m2 = [ "embassy", @@ -131,6 +141,10 @@ elecrow_thinknode_m2 = [ "dep:esp-hal", "dep:esp-println", "dep:esp-rtos", + "esp-backtrace/esp32s3", + "esp-hal/esp32s3", + "esp-println/esp32s3", + "esp-rtos/esp32s3", ] lilygo_tbeam_supreme = [ "embassy", @@ -138,6 +152,21 @@ lilygo_tbeam_supreme = [ "dep:esp-hal", "dep:esp-println", "dep:esp-rtos", + "esp-backtrace/esp32s3", + "esp-hal/esp32s3", + "esp-println/esp32s3", + "esp-rtos/esp32s3", +] +lilygo_tbeam = [ + "embassy", + "dep:esp-backtrace", + "dep:esp-hal", + "dep:esp-println", + "dep:esp-rtos", + "esp-backtrace/esp32", + "esp-hal/esp32", + "esp-println/esp32", + "esp-rtos/esp32", ] rak_wisblock_4631 = [ "embassy", diff --git a/README.md b/README.md index 38afb64..bede9b1 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 | `lilygo_tbeam` | ESP32 | SX1276 | CP2102 bridge | xtensa-esp32-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 | diff --git a/justfile b/justfile index e81b891..ad89a17 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 := "lilygo_tbeam xtensa-esp32-none-elf esp32" 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" @@ -19,7 +20,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 lilygo_tbeam_supreme rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1" +boards := "heltec_v3 heltec_v3_uart heltec_v4 elecrow_thinknode_m2 lilygo_tbeam lilygo_tbeam_supreme rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1" # Install all required tools and toolchains setup: @@ -215,6 +216,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" ]; then echo "{{lilygo_tbeam}}"; \ 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}}"; \ diff --git a/src/board/esp32.rs b/src/board/esp32.rs new file mode 100644 index 0000000..fa36fb0 --- /dev/null +++ b/src/board/esp32.rs @@ -0,0 +1,103 @@ +//! Shared types and peripheral wiring for classic-ESP32 SX1276 boards. +//! +//! Counterpart to `board::esp32s3` for the SX126x line. Builds an +//! `Sx127x` driver from an already-initialized SPI bus plus the +//! board's reset and DIO0 pins. SX127x has no `BUSY` line — DIO0 carries +//! every IRQ (TX done, RX done, header valid, …) and lora-phy's +//! `GenericSx127xInterfaceVariant` waits on it directly. + +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, InputPin, Level, Output, OutputConfig, OutputPin, Pull}; + +use lora_phy::iv::GenericSx127xInterfaceVariant; +use lora_phy::sx127x::{self, Sx127x}; + +use crate::hal::esp32 as mcu; + +// ── Re-export MCU types ───────────────────────────────────────────── + +pub use mcu::{I2cBus as DisplayI2c, SpiBus}; + +// ── Concrete peripheral types ─────────────────────────────────────── + +type Nss = Output<'static>; +type Iv = GenericSx127xInterfaceVariant, Input<'static>>; +type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>; +pub type RadioDriver = Sx127x; + +// ── Peripheral bundles ────────────────────────────────────────────── + +pub struct RadioParts { + pub driver: RadioDriver, + pub delay: Delay, +} + +pub struct DisplayParts { + pub i2c: DisplayI2c, +} + +// ── Display driver ────────────────────────────────────────────────── + +pub type DisplayDriver = ssd1306::Ssd1306Async< + ssd1306::prelude::I2CInterface, + ssd1306::size::DisplaySize128x64, + ssd1306::mode::BufferedGraphicsModeAsync, +>; + +/// Construct and initialize an SSD1306 display from raw I²C. +pub async fn create_display(i2c: DisplayI2c) -> Option { + use ssd1306::mode::DisplayConfigAsync; + use ssd1306::prelude::{Brightness, DisplayRotation}; + use ssd1306::size::DisplaySize128x64; + use ssd1306::{I2CDisplayInterface, Ssd1306Async}; + + let interface = I2CDisplayInterface::new(i2c); + let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0) + .into_buffered_graphics_mode(); + + embassy_time::Timer::after_millis(100).await; + if display.init().await.is_err() { + defmt::error!("SSD1306 display init failed"); + return None; + } + let _ = display.set_brightness(Brightness::BRIGHTEST).await; + Some(display) +} + +// ── Shared peripheral init helpers ────────────────────────────────── + +/// Construct an SX1276 radio from an initialized SPI bus. +/// +/// `dio0` is the primary IRQ (TX/RX done). The SX1276 module on the T-Beam +/// runs from a crystal oscillator, not a TCXO, and power comes from the +/// AXP192's PA_BOOST rail — `tx_boost = true` routes TX through the PA +/// output for +20 dBm. +pub fn init_radio( + spi_bus: &'static embassy_sync::mutex::Mutex, + nss_pin: impl OutputPin + 'static, + reset_pin: impl OutputPin + 'static, + dio0_pin: impl InputPin + 'static, +) -> RadioParts { + let nss = Output::new(nss_pin, Level::High, OutputConfig::default()); + let spi_device = SpiDevice::new(spi_bus, nss); + + let reset = Output::new(reset_pin, Level::High, OutputConfig::default()); + let dio0 = Input::new(dio0_pin, InputConfig::default().with_pull(Pull::Down)); + + let iv = GenericSx127xInterfaceVariant::new(reset, dio0, None, None) + .expect("SX1276 interface init"); + + let sx_config = sx127x::Config { + chip: sx127x::Sx1276, + tcxo_used: false, + tx_boost: true, + rx_boost: false, + }; + + RadioParts { + driver: Sx127x::new(spi_device, iv, sx_config), + delay: Delay, + } +} diff --git a/src/board/lilygo_tbeam.rs b/src/board/lilygo_tbeam.rs new file mode 100644 index 0000000..4be647d --- /dev/null +++ b/src/board/lilygo_tbeam.rs @@ -0,0 +1,137 @@ +//! LilyGo T-Beam (classic) — ESP32 + SX1276 + SSD1306 OLED + AXP192 PMIC. +//! +//! First-generation T-Beam. ESP32 (dual-core Xtensa LX6, no native USB), +//! SX1276 module with XTAL (no TCXO), 128×64 SSD1306 OLED, AXP192 PMIC +//! gating the LoRa and OLED rails, CP2102 USB-UART bridge for host I/O. +//! +//! The AXP192 shares an I²C bus with the OLED (SDA=21, SCL=22). PMIC init +//! runs blocking on that bus first, then the same pins are re-initialized +//! in async mode for the display task. +//! +//! Pin map (verified from MeshCore variants/lilygo_tbeam_SX1276/): +//! LoRa SPI: SCK=5, MOSI=27, MISO=19, CS=18 +//! LoRa ctl: RESET=23, DIO0=26 (primary IRQ — SX127x has no BUSY) +//! OLED I²C: SDA=21, SCL=22, SSD1306 @ 0x3C (shared with AXP192 @ 0x34) +//! UART host: TX=1, RX=3 via CP2102 bridge (USB 10c4:ea60) +//! LED: GPIO4 (active HIGH — reuses MeshCore's P_LORA_TX_LED) +//! +//! GPS / battery / IMU / PCF8563 RTC are populated on the board but unused +//! by this firmware. + +use esp_hal::gpio::{Level, Output, OutputConfig}; +use esp_hal::i2c::master::{Config as I2cConfig, I2c}; + +use super::esp32; +use super::traits::{BoardParts, LoRaBoard}; +use crate::driver::axp; +use crate::driver::simple_led::SimpleLed; +use crate::hal::esp32 as mcu; +use crate::protocol::RadioChipId; + +pub use super::esp32::{create_display, DisplayDriver, DisplayParts, RadioDriver, RadioParts}; + +// ── Concrete peripheral types ─────────────────────────────────────── + +pub type UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>; +pub type LedDriver = SimpleLed>; + +// ── Peripheral bundles ────────────────────────────────────────────── + +pub struct UartParts { + pub driver: UartDriver, +} + +// ── Board init ────────────────────────────────────────────────────── + +pub struct Board { + p: esp_hal::peripherals::Peripherals, +} + +impl LoRaBoard for Board { + const NAME: &'static str = "LilyGo T-Beam"; + // SX1276 PA_BOOST range per the datasheet: +2 dBm min, +20 dBm max. + const TX_POWER_RANGE: (i8, i8) = (2, 20); + + 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 radio_chip_id() -> u16 { + RadioChipId::Sx1276.as_u16() + } + + fn freq_range_hz() -> (u32, u32) { + // SX1276 tuning window per datasheet (LF + HF bands combined). + (137_000_000, 1_020_000_000) + } + + fn supported_sf_bitmap() -> u16 { + // SX127x supports SF6–SF12 (no SF5 like the SX126x). + 0x1FC0 + } + + fn into_parts(self) -> BoardParts { + let p = self.p; + + mcu::start_timer(p.TIMG0, p.SW_INTERRUPT); + + // AXP192 and SSD1306 share I²C0 (SDA=21, SCL=22). Build the bus + // once as blocking, drive the PMIC init, then convert to async for + // the display task — avoids re-consuming the I²C0 peripheral. + // PMIC failure isn't fatal to boot: log and proceed so the rest of + // the firmware can still emit diagnostics. + let mut i2c_blocking = I2c::new(p.I2C0, I2cConfig::default()) + .expect("I2C0 init") + .with_sda(p.GPIO21) + .with_scl(p.GPIO22); + if let Err(_e) = axp::axp192_init_lora_oled(&mut i2c_blocking) { + defmt::error!("AXP192 init failed — LoRa rail may be dead"); + } + // Let DCDC1 / LDO2 settle before SPI or display traffic. + esp_hal::delay::Delay::new().delay_millis(20); + + // SPI2 for SX1276: SCK=5, MOSI=27, MISO=19 + let spi_bus = mcu::init_spi(p.SPI2, p.DMA_SPI2, p.GPIO5, p.GPIO27, p.GPIO19); + + // SX1276: CS=18, RESET=23, DIO0=26 (primary IRQ) + let radio = esp32::init_radio(spi_bus, p.GPIO18, p.GPIO23, p.GPIO26); + + // UART0 via CP2102 bridge: TX=1, RX=3 (ESP32 default console) + use esp_hal::uart::{Config as UartConfig, Uart}; + let host = UartParts { + driver: Uart::new(p.UART0, UartConfig::default()) + .expect("UART init") + .with_tx(p.GPIO1) + .with_rx(p.GPIO3) + .into_async(), + }; + + // Hand the (now async) I²C bus to the SSD1306 display task. + let display = Some(DisplayParts { + i2c: i2c_blocking.into_async(), + }); + + // TX activity LED on GPIO4 (active HIGH). + let led_pin = Output::new(p.GPIO4, Level::Low, OutputConfig::default()); + let led = SimpleLed(led_pin); + + BoardParts { + radio, + host, + display, + led, + mac: Self::mac_address(), + } + } +} diff --git a/src/board/mod.rs.j2 b/src/board/mod.rs.j2 index ba6daa7..4b5b704 100644 --- a/src/board/mod.rs.j2 +++ b/src/board/mod.rs.j2 @@ -3,7 +3,11 @@ pub mod traits; pub use traits::*; {% for helper, helper_features in helpers|items -%} +{% if helper_features|length == 1 -%} +#[cfg(feature = "{{ helper_features[0] }}")] +{% else -%} #[cfg(any({% for f in helper_features %}feature = "{{ f }}"{% if not loop.last %}, {% endif %}{% endfor %}))] +{% endif -%} mod {{ helper }}; {% endfor %} cfg_if::cfg_if! { diff --git a/src/board/traits.rs b/src/board/traits.rs index 955f70d..c744bc6 100644 --- a/src/board/traits.rs +++ b/src/board/traits.rs @@ -65,6 +65,21 @@ pub trait LoRaBoard: Sized { (150_000_000, 960_000_000) } + /// Bitmap of supported LoRa spreading factors — bit N = SF N. Defaults + /// to SX126x's SF5–SF12 (`0x1FE0`). SX127x boards must override to + /// `0x1FC0` (SF6–SF12); LLCC68 boards to `0x0FE0` (SF5–SF11). + fn supported_sf_bitmap() -> u16 { + 0x1FE0 + } + + /// Bitmap of supported LoRa bandwidths. Bit positions match + /// `LoRaBandwidth::as_u8()` (sub-GHz BW enum values 0..9). Defaults + /// to all sub-GHz BWs (`0x03FF`). SX128x boards flip the 2.4 GHz + /// bits when we add them. + fn supported_bw_bitmap() -> u16 { + 0x03FF + } + /// Decompose initialized board into peripheral bundles for each task. fn into_parts( self, diff --git a/src/driver/axp.rs b/src/driver/axp.rs index e77044a..23312d6 100644 --- a/src/driver/axp.rs +++ b/src/driver/axp.rs @@ -29,36 +29,93 @@ 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; +#[cfg(feature = "lilygo_tbeam_supreme")] +mod axp2101 { + use super::{I2c, PMIC_ADDR}; -/// Voltage code for 3.3 V on ALDOx: 0.5 V + 28 × 100 mV. -const AXP2101_V_3V3: u8 = 28; + /// LDO on/off control register. Bits: + /// 0 = ALDO1, 1 = ALDO2, 2 = ALDO3, 3 = ALDO4, 4 = BLDO1, 5 = BLDO2. + const REG_LDO_ONOFF: u8 = 0x90; + /// ALDO1 voltage register (OLED rail on T-Beam Supreme). + const REG_ALDO1_V: u8 = 0x92; + /// ALDO3 voltage register (LoRa rail on T-Beam Supreme). + const REG_ALDO3_V: u8 = 0x94; -/// Mask enabling ALDO1 + ALDO3 in [`AXP2101_REG_LDO_ONOFF`]. -const AXP2101_ALDO1_ALDO3: u8 = 0b0000_0101; + /// Voltage code for 3.3 V on ALDOx: 0.5 V + 28 × 100 mV. + const V_3V3: u8 = 28; -/// 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])?; + /// Mask enabling ALDO1 + ALDO3 in [`REG_LDO_ONOFF`]. + const ALDO1_ALDO3: u8 = 0b0000_0101; - 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], - )?; + /// 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 init_lora_oled(i2c: &mut I) -> Result<(), I::Error> { + i2c.write(PMIC_ADDR, &[REG_ALDO3_V, V_3V3])?; + i2c.write(PMIC_ADDR, &[REG_ALDO1_V, V_3V3])?; - Ok(()) + let mut onoff = [0u8; 1]; + i2c.write_read(PMIC_ADDR, &[REG_LDO_ONOFF], &mut onoff)?; + i2c.write(PMIC_ADDR, &[REG_LDO_ONOFF, onoff[0] | ALDO1_ALDO3])?; + + Ok(()) + } } + +#[cfg(feature = "lilygo_tbeam_supreme")] +pub use axp2101::init_lora_oled as axp2101_init_lora_oled; + +// ── AXP192 (T-Beam classic) ────────────────────────────────────────── + +#[cfg(feature = "lilygo_tbeam")] +mod axp192 { + use super::{I2c, PMIC_ADDR}; + + /// DCDC/LDO enable register. Bit layout: + /// 0 = DCDC1, 1 = DCDC3, 2 = LDO2, 3 = LDO3, 4 = DCDC2, 6 = EXTEN. + const REG_ONOFF: u8 = 0x12; + /// DCDC1 output voltage register (OLED rail on T-Beam classic). + /// 7-bit code, 25 mV step, 0.7 V offset → 3.3 V = 104 = `0x68`. + const REG_DCDC1_V: u8 = 0x26; + /// LDO2/LDO3 voltage register. Upper nibble = LDO2 (LoRa), lower nibble + /// = LDO3 (GPS). 100 mV step, 1.8 V offset → 3.3 V = 15 = `0xF`. + const REG_LDO23_V: u8 = 0x28; + + /// DCDC1 code for 3.3 V. + const DCDC1_3V3: u8 = 0x68; + /// LDO2 nibble for 3.3 V in the upper half of [`REG_LDO23_V`]. + const LDO2_3V3_UPPER: u8 = 0xF0; + + /// Mask enabling DCDC1 + LDO2 in [`REG_ONOFF`]. + const DCDC1_LDO2: u8 = 0b0000_0101; + + /// Bring up LoRa (LDO2) and OLED (DCDC1) rails at 3.3 V on the + /// classic T-Beam's AXP192. + /// + /// GPS (LDO3) and EXTEN are not touched — this firmware has no GPS, + /// and any board-default for LDO3 voltage is preserved by + /// read-modify-write. Voltage is programmed before the enable bit + /// flips so the rail doesn't glitch through its reset default. + pub fn init_lora_oled(i2c: &mut I) -> Result<(), I::Error> { + let mut ldo23 = [0u8; 1]; + i2c.write_read(PMIC_ADDR, &[REG_LDO23_V], &mut ldo23)?; + i2c.write( + PMIC_ADDR, + &[REG_LDO23_V, (ldo23[0] & 0x0F) | LDO2_3V3_UPPER], + )?; + + i2c.write(PMIC_ADDR, &[REG_DCDC1_V, DCDC1_3V3])?; + + let mut onoff = [0u8; 1]; + i2c.write_read(PMIC_ADDR, &[REG_ONOFF], &mut onoff)?; + i2c.write(PMIC_ADDR, &[REG_ONOFF, onoff[0] | DCDC1_LDO2])?; + + Ok(()) + } +} + +#[cfg(feature = "lilygo_tbeam")] +pub use axp192::init_lora_oled as axp192_init_lora_oled; diff --git a/src/driver/mod.rs b/src/driver/mod.rs index ce50268..276ab15 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -5,7 +5,7 @@ ))] pub mod sh1106; -#[cfg(feature = "lilygo_tbeam_supreme")] +#[cfg(any(feature = "lilygo_tbeam_supreme", feature = "lilygo_tbeam"))] pub mod axp; #[cfg(any( @@ -14,6 +14,7 @@ pub mod axp; feature = "heltec_v4", feature = "elecrow_thinknode_m2", feature = "lilygo_tbeam_supreme", + feature = "lilygo_tbeam", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora" ))] diff --git a/src/hal/esp32.rs b/src/hal/esp32.rs new file mode 100644 index 0000000..fc28902 --- /dev/null +++ b/src/hal/esp32.rs @@ -0,0 +1,80 @@ +//! ESP32 (classic / non-S3) MCU initialization primitives. +//! +//! Mirror of `hal::esp32s3` with a single delta: the classic ESP32 uses a +//! peripheral-bound DMA (`DMA_SPI2`) rather than the S3's channel-addressed +//! GDMA (`DMA_CH0`). Timer, I²C, and eFuse access are otherwise identical +//! between the two Xtensa parts in this esp-hal vintage. + +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::I2c; +use esp_hal::spi::master::{Config as SpiConfig, Spi, SpiDmaBus}; +use esp_hal::spi::Mode as SpiMode; +use esp_hal::time::Rate; +use esp_hal::timer::timg::TimerGroup; +use static_cell::StaticCell; + +// ── MCU-level types ───────────────────────────────────────────────── + +pub type SpiBus = SpiDmaBus<'static, esp_hal::Async>; +pub type I2cBus = I2c<'static, esp_hal::Async>; + +// ── Timer ─────────────────────────────────────────────────────────── + +/// Start the ESP-RTOS scheduler and Embassy time driver. +pub fn start_timer( + timg0: esp_hal::peripherals::TIMG0<'static>, + sw_int: esp_hal::peripherals::SW_INTERRUPT<'static>, +) { + let timg0 = TimerGroup::new(timg0); + let sw_ints = esp_hal::interrupt::software::SoftwareInterruptControl::new(sw_int); + esp_rtos::start(timg0.timer0, sw_ints.software_interrupt0); +} + +// ── SPI bus ───────────────────────────────────────────────────────── + +/// Initialize SPI2 with peripheral-bound DMA and wrap in a shared bus +/// (`StaticCell` + `Mutex`). +/// +/// Generic over pin type. The classic ESP32's matrix of GPIO → peripheral +/// routes lets any GPIO serve as any SPI signal — same as the S3. +pub fn init_spi( + spi2: esp_hal::peripherals::SPI2<'static>, + dma_spi2: esp_hal::peripherals::DMA_SPI2<'static>, + sck: impl PeripheralOutput<'static>, + mosi: impl PeripheralOutput<'static>, + miso: impl PeripheralInput<'static>, +) -> &'static embassy_sync::mutex::Mutex { + let spi = Spi::new( + spi2, + SpiConfig::default() + .with_frequency(Rate::from_mhz(1)) + .with_mode(SpiMode::_0), + ) + .expect("SPI init") + .with_sck(sck) + .with_mosi(mosi) + .with_miso(miso) + .with_dma(dma_spi2); + + let (rx_buf, rx_desc, tx_buf, tx_desc) = esp_hal::dma_buffers!(256); + let dma_rx = DmaRxBuf::new(rx_desc, rx_buf).expect("DMA RX buf"); + let dma_tx = DmaTxBuf::new(tx_desc, tx_buf).expect("DMA TX buf"); + + let spi = spi.with_buffers(dma_rx, dma_tx).into_async(); + + static SPI_BUS: StaticCell> = + StaticCell::new(); + SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi)) +} + +// ── MAC address ───────────────────────────────────────────────────── + +/// Read the factory-programmed MAC address from eFuse. +pub fn mac_address() -> [u8; 6] { + esp_hal::efuse::base_mac_address() + .as_bytes() + .try_into() + .expect("MAC is 6 bytes") +} diff --git a/src/hal/mod.rs b/src/hal/mod.rs index a671676..248c56b 100644 --- a/src/hal/mod.rs +++ b/src/hal/mod.rs @@ -12,6 +12,9 @@ ))] pub mod esp32s3; +#[cfg(feature = "lilygo_tbeam")] +pub mod esp32; + #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))] pub mod nrf52840; diff --git a/src/host/mod.rs b/src/host/mod.rs index 9b39ddb..0e870d6 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -6,7 +6,11 @@ pub mod framing; cfg_if::cfg_if! { - if #[cfg(any(feature = "heltec_v3_uart", feature = "elecrow_thinknode_m2"))] { + if #[cfg(any( + feature = "heltec_v3_uart", + feature = "elecrow_thinknode_m2", + feature = "lilygo_tbeam" + ))] { mod uart; pub use uart::host_task; } else { diff --git a/src/main.rs b/src/main.rs index 9b53c76..c247f9c 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", feature = "lilygo_tbeam_supreme"))] { + } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2", feature = "lilygo_tbeam_supreme", feature = "lilygo_tbeam"))] { 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", feature = "lilygo_tbeam_supreme"))] { + } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2", feature = "lilygo_tbeam_supreme", feature = "lilygo_tbeam"))] { #[esp_rtos::main] async fn main(spawner: Spawner) { run(spawner).await; @@ -140,6 +140,8 @@ fn build_info(mac: [u8; 6]) -> Info { let (fmin, fmax) = ::freq_range_hz(); let chip_id = ::radio_chip_id(); let (tx_min, tx_max) = board::Board::TX_POWER_RANGE; + let supported_sf = ::supported_sf_bitmap(); + let supported_bw = ::supported_bw_bitmap(); let mut mcu_uid = [0u8; MAX_MCU_UID_LEN]; mcu_uid[..6].copy_from_slice(&mac); @@ -152,12 +154,8 @@ fn build_info(mac: [u8; 6]) -> Info { fw_patch: parse_u8_or(env!("CARGO_PKG_VERSION_PATCH"), 0), radio_chip_id: chip_id, capability_bitmap: cap::LORA | cap::CAD_BEFORE_TX, - // SX126x supports SF5–SF12 (LLCC68 tops out at SF11 — that board - // can override this via a board-level trait method later). - supported_sf_bitmap: 0x1FE0, - // Sub-GHz BW enum values 0..9. SX128x would flip 10..13; add as - // boards come online. - supported_bw_bitmap: 0x03FF, + supported_sf_bitmap: supported_sf, + supported_bw_bitmap: supported_bw, max_payload_bytes: crate::protocol::MAX_OTA_PAYLOAD as u16, // Real channel depths from channel.rs. rx_queue_capacity: 32,