diff --git a/build.rs b/build.rs index a4579c5c..87bdc4c8 100644 --- a/build.rs +++ b/build.rs @@ -3,13 +3,24 @@ use owo_colors::OwoColorize; use std::fs; fn main() { + // Protocol tests run on the host without a board feature. Cargo doesn't + // expose `cfg(test)` to build scripts, so we rely on an env var set by + // the justfile `test` recipe. See: `just test` + if std::env::var_os("DONGLORA_HOST_TEST").is_some() { + return; + } + let board_dir = "src/board"; let boards: Vec = fs::read_dir(board_dir) .expect("failed to read src/board directory") .filter_map(|e| { let p = e.ok()?.path(); let stem = p.file_stem()?.to_str()?; - if p.is_file() && p.extension()? == "rs" && stem != "mod" && stem != "traits" { + // Exclude non-board files: mod.rs, traits.rs, and platform helpers (esp32s3.rs). + if p.is_file() + && p.extension()? == "rs" + && !matches!(stem, "mod" | "traits" | "esp32s3") + { return Some(p.file_stem()?.to_str()?.to_string()); } None diff --git a/src/board/PORTING.md b/src/board/PORTING.md index 639991ff..e55fb3bd 100644 --- a/src/board/PORTING.md +++ b/src/board/PORTING.md @@ -5,9 +5,15 @@ ### 1. Create the board file Create `src/board/your_board.rs`. Use an existing board as a template: -- ESP32-S3 boards: copy `heltec_v4.rs` +- ESP32-S3 boards: copy `heltec_v4.rs` (uses shared helpers from `esp32s3.rs`) - nRF52840 boards: copy `rak_wisblock_4631.rs` +ESP32-S3 boards share common type aliases, peripheral bundles, and init helpers +via `src/board/esp32s3.rs`. Your board file only needs the trait impl and +board-specific init (Vext GPIO, display reset). If you add a new ESP32-S3 board, +add its feature to the `#[cfg(any(...))]` gate on `mod esp32s3` in +`src/board/mod.rs.j2`. + ### 2. Define concrete types Your board must export these type aliases: @@ -71,9 +77,9 @@ In `Cargo.toml`, add a feature with your board's HAL dependencies: your_board = ["dep:your-hal", ...] ``` -### 7. Add to Justfile +### 7. Add to justfile -Add your board's feature/target/chip to the `boards` list at the top of `Justfile`. +Add your board's feature/target/chip to the `boards` list at the top of `justfile`. ### 8. Build and test diff --git a/src/board/esp32s3.rs b/src/board/esp32s3.rs new file mode 100644 index 00000000..6d8249ee --- /dev/null +++ b/src/board/esp32s3.rs @@ -0,0 +1,150 @@ +//! Shared types and initialization helpers for ESP32-S3 boards. +//! +//! All ESP32-S3 boards in this project share the same SPI/DMA, USB OTG, +//! and I2C peripheral wiring. This module provides the common type aliases, +//! peripheral bundle structs, and init functions. Each board file re-exports +//! these types and calls the helpers from its own `Board::into_parts()`. + +use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_time::Delay; +use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; +use esp_hal::gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull}; +use esp_hal::i2c::master::{Config as I2cConfig, 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; + +use lora_phy::iv::GenericSx126xInterfaceVariant; +use lora_phy::sx126x::{self, Sx1262, Sx126x}; + +// ── Concrete peripheral types ──────────────────────────────────────── + +type SpiBus = SpiDmaBus<'static, esp_hal::Async>; +type Nss = Output<'static>; +type Iv = GenericSx126xInterfaceVariant, Input<'static>>; +type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>; +pub type RadioDriver = Sx126x; + +pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>; + +pub type DisplayI2c = I2c<'static, esp_hal::Async>; + +// ── Peripheral bundles ─────────────────────────────────────────────── + +pub struct RadioParts { + pub driver: RadioDriver, + pub delay: Delay, +} + +pub struct UsbParts { + pub driver: UsbDriver, +} + +pub struct DisplayParts { + pub i2c: DisplayI2c, + pub mac: [u8; 6], +} + +// ── Shared initialization helpers ─────────────────────────────────── + +/// Start the ESP-RTOS scheduler and Embassy time driver. +pub fn start_timer(timg0: esp_hal::peripherals::TIMG0<'static>) { + let timg0 = TimerGroup::new(timg0); + esp_rtos::start(timg0.timer0); +} + +/// Initialize SPI + DMA + SX1262 radio. +/// +/// All current ESP32-S3 boards use the same SPI/radio pin mapping: +/// SCK=GPIO9, MOSI=GPIO10, MISO=GPIO11, NSS=GPIO8, +/// Reset=GPIO12, DIO1=GPIO14, BUSY=GPIO13. +#[allow(clippy::too_many_arguments)] +pub fn init_radio( + spi2: esp_hal::peripherals::SPI2<'static>, + dma_ch0: esp_hal::peripherals::DMA_CH0<'static>, + sck: esp_hal::peripherals::GPIO9<'static>, + mosi: esp_hal::peripherals::GPIO10<'static>, + miso: esp_hal::peripherals::GPIO11<'static>, + nss_pin: esp_hal::peripherals::GPIO8<'static>, + reset_pin: esp_hal::peripherals::GPIO12<'static>, + dio1_pin: esp_hal::peripherals::GPIO14<'static>, + busy_pin: esp_hal::peripherals::GPIO13<'static>, +) -> RadioParts { + 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_ch0); + + 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(); + let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi)); + + 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 dio1 = Input::new(dio1_pin, InputConfig::default().with_pull(Pull::Down)); + let busy = Input::new(busy_pin, InputConfig::default().with_pull(Pull::Down)); + + let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None) + .expect("SX1262 interface init"); + + let sx_config = sx126x::Config { + chip: Sx1262, + tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7), + use_dcdc: true, + rx_boost: false, + }; + + RadioParts { + driver: Sx126x::new(spi_device, iv, sx_config), + delay: Delay, + } +} + +/// Initialize USB OTG CDC-ACM driver. +pub fn init_usb( + usb0: esp_hal::peripherals::USB0<'static>, + dp: esp_hal::peripherals::GPIO20<'static>, + dm: esp_hal::peripherals::GPIO19<'static>, +) -> UsbParts { + let usb_inst = esp_hal::otg_fs::Usb::new(usb0, dp, dm); + static EP_OUT_BUF: StaticCell<[u8; 1024]> = StaticCell::new(); + let ep_out_buf = EP_OUT_BUF.init([0u8; 1024]); + UsbParts { + driver: esp_hal::otg_fs::asynch::Driver::new( + usb_inst, + ep_out_buf, + esp_hal::otg_fs::asynch::Config::default(), + ), + } +} + +/// Initialize I2C bus for display. +pub fn init_display_i2c( + i2c0: esp_hal::peripherals::I2C0<'static>, + sda: esp_hal::peripherals::GPIO17<'static>, + scl: esp_hal::peripherals::GPIO18<'static>, +) -> DisplayI2c { + I2c::new(i2c0, I2cConfig::default()) + .expect("I2C init") + .with_sda(sda) + .with_scl(scl) + .into_async() +} diff --git a/src/board/heltec_v3.rs b/src/board/heltec_v3.rs index 2f3745ad..5a55fb36 100644 --- a/src/board/heltec_v3.rs +++ b/src/board/heltec_v3.rs @@ -1,47 +1,10 @@ +use esp_hal::gpio::{Level, Output, OutputConfig}; + +use super::esp32s3; use super::traits::LoRaBoard; -use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; -use embassy_time::Delay; -use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; -use esp_hal::gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull}; -use esp_hal::i2c::master::{Config as I2cConfig, 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; -use lora_phy::iv::GenericSx126xInterfaceVariant; -use lora_phy::sx126x::{self, Sx1262, Sx126x}; - -// ── Concrete peripheral types ──────────────────────────────────────── - -type SpiBus = SpiDmaBus<'static, esp_hal::Async>; -type Nss = Output<'static>; -type Iv = GenericSx126xInterfaceVariant, Input<'static>>; -type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>; -pub type RadioDriver = Sx126x; - -pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>; - -pub type DisplayI2c = I2c<'static, esp_hal::Async>; - - -// ── Peripheral bundles ─────────────────────────────────────────────── - -pub struct RadioParts { - pub driver: RadioDriver, - pub delay: Delay, -} - -pub struct UsbParts { - pub driver: UsbDriver, -} - -pub struct DisplayParts { - pub i2c: DisplayI2c, - pub mac: [u8; 6], -} +#[allow(unused_imports)] // Re-exported for other modules (display, radio, usb tasks) +pub use super::esp32s3::{DisplayI2c, DisplayParts, RadioDriver, RadioParts, UsbDriver, UsbParts}; // ── Board init ─────────────────────────────────────────────────────── @@ -67,78 +30,19 @@ impl Board { pub fn into_parts(self) -> (RadioParts, UsbParts, Option) { let p = self.p; - // ── esp-rtos scheduler + embassy time driver ────────────── - let timg0 = TimerGroup::new(p.TIMG0); - esp_rtos::start(timg0.timer0); + esp32s3::start_timer(p.TIMG0); - // ── Vext power: GPIO21, active LOW to enable OLED ────────── + // Vext power: GPIO21, active LOW to enable OLED let vext = Output::new(p.GPIO21, Level::Low, OutputConfig::default()); core::mem::forget(vext); // hold pin low permanently; drop would reset it - // ── SPI + DMA for SX1262 ─────────────────────────────────── - let spi = Spi::new( - p.SPI2, - SpiConfig::default() - .with_frequency(Rate::from_mhz(1)) - .with_mode(SpiMode::_0), - ) - .expect("SPI init") - .with_sck(p.GPIO9) - .with_mosi(p.GPIO10) - .with_miso(p.GPIO11) - .with_dma(p.DMA_CH0); - - 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(); - let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi)); - - let nss = Output::new(p.GPIO8, Level::High, OutputConfig::default()); - let spi_device = SpiDevice::new(spi_bus, nss); - - // ── SX1262 control pins ──────────────────────────────────── - let reset = Output::new(p.GPIO12, Level::High, OutputConfig::default()); - let dio1 = Input::new(p.GPIO14, InputConfig::default().with_pull(Pull::Down)); - let busy = Input::new(p.GPIO13, InputConfig::default().with_pull(Pull::Down)); - - let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None).expect("SX1262 interface init"); - - let sx_config = sx126x::Config { - chip: Sx1262, - tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7), - use_dcdc: true, - rx_boost: false, - }; - - let radio = RadioParts { - driver: Sx126x::new(spi_device, iv, sx_config), - delay: Delay, - }; - - // ── USB OTG CDC-ACM driver ────────────────────────────── - 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 usb = UsbParts { - driver: esp_hal::otg_fs::asynch::Driver::new( - usb_inst, - ep_out_buf, - esp_hal::otg_fs::asynch::Config::default(), - ), - }; - - // ── Display (SSD1306 OLED on I2C, 0x3C) ─────────────────── - let i2c = I2c::new(p.I2C0, I2cConfig::default()) - .expect("I2C init") - .with_sda(p.GPIO17) - .with_scl(p.GPIO18) - .into_async(); - let mac = esp_hal::efuse::Efuse::mac_address(); + let radio = esp32s3::init_radio( + p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11, p.GPIO8, p.GPIO12, p.GPIO14, + p.GPIO13, + ); + let usb = esp32s3::init_usb(p.USB0, p.GPIO20, p.GPIO19); + let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18); + let mac = Self::mac_address(); let display = Some(DisplayParts { i2c, mac }); (radio, usb, display) diff --git a/src/board/heltec_v4.rs b/src/board/heltec_v4.rs index d5289545..95c1b6b5 100644 --- a/src/board/heltec_v4.rs +++ b/src/board/heltec_v4.rs @@ -1,47 +1,10 @@ -use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; -use embassy_time::Delay; -use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; -use esp_hal::gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull}; -use esp_hal::i2c::master::{Config as I2cConfig, 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; - -use lora_phy::iv::GenericSx126xInterfaceVariant; -use lora_phy::sx126x::{self, Sx1262, Sx126x}; +use esp_hal::gpio::{Level, Output, OutputConfig}; +use super::esp32s3; use super::traits::LoRaBoard; -// ── Concrete peripheral types ──────────────────────────────────────── - -type SpiBus = SpiDmaBus<'static, esp_hal::Async>; -type Nss = Output<'static>; -type Iv = GenericSx126xInterfaceVariant, Input<'static>>; -type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>; -pub type RadioDriver = Sx126x; - -pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>; - -pub type DisplayI2c = I2c<'static, esp_hal::Async>; - -// ── Peripheral bundles ─────────────────────────────────────────────── - -pub struct RadioParts { - pub driver: RadioDriver, - pub delay: Delay, -} - -pub struct UsbParts { - pub driver: UsbDriver, -} - -pub struct DisplayParts { - pub i2c: DisplayI2c, - pub mac: [u8; 6], -} +#[allow(unused_imports)] // Re-exported for other modules (display, radio, usb tasks) +pub use super::esp32s3::{DisplayI2c, DisplayParts, RadioDriver, RadioParts, UsbDriver, UsbParts}; // ── Board implementation ──────────────────────────────────────────── @@ -67,86 +30,29 @@ impl Board { pub fn into_parts(self) -> (RadioParts, UsbParts, Option) { let p = self.p; - // ── esp-rtos scheduler + embassy time driver ────────────── - let timg0 = TimerGroup::new(p.TIMG0); - esp_rtos::start(timg0.timer0); + esp32s3::start_timer(p.TIMG0); - // ── Vext power: GPIO36 ───────────────────────────────────── + // Vext power: GPIO36, active LOW to enable peripherals let vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default()); core::mem::forget(vext); // hold pin low permanently; drop would reset it - // ── SPI + DMA for SX1262 ─────────────────────────────────── - let spi = Spi::new( - p.SPI2, - SpiConfig::default() - .with_frequency(Rate::from_mhz(1)) - .with_mode(SpiMode::_0), - ) - .expect("SPI init") - .with_sck(p.GPIO9) - .with_mosi(p.GPIO10) - .with_miso(p.GPIO11) - .with_dma(p.DMA_CH0); + let radio = esp32s3::init_radio( + p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11, p.GPIO8, p.GPIO12, p.GPIO14, + p.GPIO13, + ); - 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(); - let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi)); - - let nss = Output::new(p.GPIO8, Level::High, OutputConfig::default()); - let spi_device = SpiDevice::new(spi_bus, nss); - - // ── SX1262 control pins ──────────────────────────────────── - let reset = Output::new(p.GPIO12, Level::High, OutputConfig::default()); - let dio1 = Input::new(p.GPIO14, InputConfig::default().with_pull(Pull::Down)); - let busy = Input::new(p.GPIO13, InputConfig::default().with_pull(Pull::Down)); - - let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None) - .expect("SX1262 interface init"); - - let sx_config = sx126x::Config { - chip: Sx1262, - tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7), - use_dcdc: true, - rx_boost: false, - }; - - let radio = RadioParts { - driver: Sx126x::new(spi_device, iv, sx_config), - delay: Delay, - }; - - // ── USB OTG CDC-ACM ──────────────────────────────────────── // Note: switches internal USB PHY from Serial-JTAG to OTG. // espflash --monitor will stop working after this point. - 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 usb = UsbParts { - driver: esp_hal::otg_fs::asynch::Driver::new( - usb_inst, - ep_out_buf, - esp_hal::otg_fs::asynch::Config::default(), - ), - }; + let usb = esp32s3::init_usb(p.USB0, p.GPIO20, p.GPIO19); - // ── Display (SSD1315 OLED on I2C, 0x3C) ─────────────────── + // SSD1315 display reset: pulse GPIO21 low->high before I2C init let mut display_rst = Output::new(p.GPIO21, Level::Low, OutputConfig::default()); esp_hal::delay::Delay::new().delay_millis(10); display_rst.set_high(); esp_hal::delay::Delay::new().delay_millis(10); core::mem::forget(display_rst); // hold reset high permanently - let i2c = I2c::new(p.I2C0, I2cConfig::default()) - .expect("I2C init") - .with_sda(p.GPIO17) - .with_scl(p.GPIO18) - .into_async(); + let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18); let mac = Self::mac_address(); let display = Some(DisplayParts { i2c, mac }); diff --git a/src/board/mod.rs.j2 b/src/board/mod.rs.j2 index aa1a69b8..063145d2 100644 --- a/src/board/mod.rs.j2 +++ b/src/board/mod.rs.j2 @@ -2,6 +2,9 @@ pub mod traits; pub use traits::*; +#[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] +mod esp32s3; + cfg_if::cfg_if! { {%- for board in boards %} {% if loop.first %}if{% else %}else if{% endif %} #[cfg(feature = "{{ board }}")] {