feat(boards): LilyGo T-Beam (classic) support

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
This commit is contained in:
swaits committed 2026-04-23 10:42:42 -06:00
1 parent 7fca5fcb50
commit a1b92356b3
15 files changed
+534 -43

No files matched your search

+80
View File
@@ -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<NoopRawMutex, SpiBus> {
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<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
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")
}
+3
View File
@@ -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;