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
+532
-41
No files matched your search
@@ -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<Sx1276>` +
|
||||
`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
|
||||
|
||||
|
||||
Generated
+25
@@ -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"
|
||||
|
||||
+34
-5
@@ -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",
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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}}"; \
|
||||
|
||||
@@ -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,
|
||||
|
||||
+82
-25
@@ -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<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])?;
|
||||
/// Mask enabling ALDO1 + ALDO3 in [`REG_LDO_ONOFF`].
|
||||
const 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 init_lora_oled<I: I2c>(i2c: &mut I) -> Result<(), I::Error> {
|
||||
i2c.write(PMIC_ADDR, &[REG_ALDO3_V, V_3V3])?;
|
||||
i2c.write(PMIC_ADDR, &[REG_ALDO1_V, 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],
|
||||
)?;
|
||||
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<I: I2c>(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;
|
||||
+2
-1
@@ -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"
|
||||
))]
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
+5
-1
@@ -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 {
|
||||
|
||||
+6
-8
@@ -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) = <board::Board as LoRaBoard>::freq_range_hz();
|
||||
let chip_id = <board::Board as LoRaBoard>::radio_chip_id();
|
||||
let (tx_min, tx_max) = board::Board::TX_POWER_RANGE;
|
||||
let supported_sf = <board::Board as LoRaBoard>::supported_sf_bitmap();
|
||||
let supported_bw = <board::Board as LoRaBoard>::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,
|
||||
|
||||
Reference in new issue
Block a user