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:
15 files changed
+534
-43
No files matched your search
@@ -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<Sx1276>` 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<Output<'static>, Input<'static>>;
|
||||
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
||||
pub type RadioDriver = Sx127x<RadioSpiDevice, Iv, sx127x::Sx1276>;
|
||||
|
||||
// ── 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<DisplayI2c>,
|
||||
ssd1306::size::DisplaySize128x64,
|
||||
ssd1306::mode::BufferedGraphicsModeAsync<ssd1306::size::DisplaySize128x64>,
|
||||
>;
|
||||
|
||||
/// Construct and initialize an SSD1306 display from raw I²C.
|
||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
||||
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<NoopRawMutex, SpiBus>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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<Output<'static>>;
|
||||
|
||||
// ── 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<RadioParts, UartParts, DisplayParts, LedDriver> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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! {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user