From 82701c183b4999077a4e286dd93ae4414c48d3a1 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 7 Apr 2026 09:30:10 -0600 Subject: [PATCH] refactor: extract hal/ and driver/ modules - Created hal/esp32s3.rs with MCU primitives (SPI, I2C, USB, UART, timer, MAC) - Created hal/nrf52840.rs with shared nRF boilerplate (interrupts, bus wrapping, VBUS, MAC) - Moved SH1106 OLED driver from display/ to driver/ (clear separation from app code) - Board esp32s3.rs now delegates MCU init to hal, keeps only peripheral wiring (RadioParts) - nRF board files use hal for shared StaticCell patterns, mac_address, interrupt bindings - Board files still own pin assignments and peripheral construction --- firmware/src/board/esp32s3.rs | 127 +++------------------ firmware/src/board/heltec_v3.rs | 18 +-- firmware/src/board/heltec_v4.rs | 16 +-- firmware/src/board/rak_wisblock_4631.rs | 84 ++++---------- firmware/src/board/wio_tracker_l1.rs | 82 ++++--------- firmware/src/display/mod.rs | 4 +- firmware/src/display/task.rs | 2 +- firmware/src/driver/mod.rs | 2 + firmware/src/{display => driver}/sh1106.rs | 0 firmware/src/hal/esp32s3.rs | 121 ++++++++++++++++++++ firmware/src/hal/mod.rs | 10 ++ firmware/src/hal/nrf52840.rs | 82 +++++++++++++ firmware/src/main.rs | 4 + 13 files changed, 300 insertions(+), 252 deletions(-) create mode 100644 firmware/src/driver/mod.rs rename firmware/src/{display => driver}/sh1106.rs (100%) create mode 100644 firmware/src/hal/esp32s3.rs create mode 100644 firmware/src/hal/mod.rs create mode 100644 firmware/src/hal/nrf52840.rs diff --git a/firmware/src/board/esp32s3.rs b/firmware/src/board/esp32s3.rs index df74a8a1..fff4f37a 100644 --- a/firmware/src/board/esp32s3.rs +++ b/firmware/src/board/esp32s3.rs @@ -1,42 +1,30 @@ -//! Shared types and initialization helpers for ESP32-S3 boards. +//! Shared types and peripheral wiring 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()`. +//! Delegates MCU-level init to `hal::esp32s3` and defines peripheral-level +//! types (RadioDriver, Parts structs) used by ESP32-S3 board files. 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 ──────────────────────────────────────── +use crate::hal::esp32s3 as mcu; + +// ── Re-export MCU types ───────────────────────────────────────────── + +pub use mcu::{I2cBus as DisplayI2c, SpiBus, UartDriver, UsbOtgDriver as UsbDriver}; + +// ── 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; -#[cfg(not(feature = "heltec_v3"))] -pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>; - -#[cfg(feature = "heltec_v3")] -pub type UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>; - -pub type DisplayI2c = I2c<'static, esp_hal::Async>; - -// ── Peripheral bundles ─────────────────────────────────────────────── +// ── Peripheral bundles ────────────────────────────────────────────── pub struct RadioParts { pub driver: RadioDriver, @@ -58,53 +46,16 @@ pub struct DisplayParts { pub mac: [u8; 6], } -// ── Shared initialization helpers ─────────────────────────────────── +// ── Shared peripheral init 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)] +/// Construct SX1262 radio from an initialized SPI bus. 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>, + spi_bus: &'static embassy_sync::mutex::Mutex, 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); @@ -127,53 +78,3 @@ pub fn init_radio( delay: Delay, } } - -#[cfg(not(feature = "heltec_v3"))] -/// 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(), - ), - } -} - -#[cfg(feature = "heltec_v3")] -/// Initialize UART0 for boards with USB-UART bridge chips (e.g. CP2102). -pub fn init_uart( - uart0: esp_hal::peripherals::UART0<'static>, - tx: esp_hal::peripherals::GPIO43<'static>, - rx: esp_hal::peripherals::GPIO44<'static>, -) -> UartParts { - use esp_hal::uart::{Config as UartConfig, Uart}; - - let uart = Uart::new(uart0, UartConfig::default()) - .expect("UART init") - .with_tx(tx) - .with_rx(rx) - .into_async(); - - UartParts { driver: uart } -} - -/// 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/firmware/src/board/heltec_v3.rs b/firmware/src/board/heltec_v3.rs index 02f7121b..4c089a82 100644 --- a/firmware/src/board/heltec_v3.rs +++ b/firmware/src/board/heltec_v3.rs @@ -2,11 +2,12 @@ use esp_hal::gpio::{Level, Output, OutputConfig}; use super::esp32s3; use super::traits::LoRaBoard; +use crate::hal::esp32s3 as mcu; #[allow(unused_imports)] // Re-exported for other modules (display, radio, uart tasks) pub use super::esp32s3::{DisplayI2c, DisplayParts, RadioDriver, RadioParts, UartDriver, UartParts}; -// ── Board init ─────────────────────────────────────────────────────── +// ── Board init ────────────────────────────────────────────────────── pub struct Board { p: esp_hal::peripherals::Peripherals, @@ -22,7 +23,7 @@ impl LoRaBoard for Board { } fn mac_address() -> [u8; 6] { - esp_hal::efuse::Efuse::mac_address() + mcu::mac_address() } } @@ -30,16 +31,17 @@ impl Board { pub fn into_parts(self) -> (RadioParts, UartParts, Option) { let p = self.p; - esp32s3::start_timer(p.TIMG0); + mcu::start_timer(p.TIMG0); // 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 - 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 uart = esp32s3::init_uart(p.UART0, p.GPIO43, p.GPIO44); + let spi_bus = mcu::init_spi(p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11); + let radio = esp32s3::init_radio(spi_bus, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13); + let uart = UartParts { + driver: mcu::init_uart(p.UART0, p.GPIO43, p.GPIO44), + }; // SSD1306 display reset: pulse GPIO21 low->high before I2C init let mut display_rst = Output::new(p.GPIO21, Level::Low, OutputConfig::default()); @@ -48,7 +50,7 @@ impl Board { esp_hal::delay::Delay::new().delay_millis(10); core::mem::forget(display_rst); // hold reset high permanently - let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18); + let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18); let mac = Self::mac_address(); let display = Some(DisplayParts { i2c, mac }); diff --git a/firmware/src/board/heltec_v4.rs b/firmware/src/board/heltec_v4.rs index 8e9c1150..c1872e30 100644 --- a/firmware/src/board/heltec_v4.rs +++ b/firmware/src/board/heltec_v4.rs @@ -2,6 +2,7 @@ use esp_hal::gpio::{Level, Output, OutputConfig}; use super::esp32s3; use super::traits::LoRaBoard; +use crate::hal::esp32s3 as mcu; #[allow(unused_imports)] // Re-exported for other modules (display, radio, usb tasks) pub use super::esp32s3::{DisplayI2c, DisplayParts, RadioDriver, RadioParts, UsbDriver, UsbParts}; @@ -22,7 +23,7 @@ impl LoRaBoard for Board { } fn mac_address() -> [u8; 6] { - esp_hal::efuse::Efuse::mac_address() + mcu::mac_address() } } @@ -30,19 +31,20 @@ impl Board { pub fn into_parts(self) -> (RadioParts, UsbParts, Option) { let p = self.p; - esp32s3::start_timer(p.TIMG0); + mcu::start_timer(p.TIMG0); // 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 - 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 spi_bus = mcu::init_spi(p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11); + let radio = esp32s3::init_radio(spi_bus, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13); // Note: switches internal USB PHY from Serial-JTAG to OTG. // espflash --monitor will stop working after this point. - let usb = esp32s3::init_usb(p.USB0, p.GPIO20, p.GPIO19); + let usb = UsbParts { + driver: mcu::init_usb(p.USB0, p.GPIO20, p.GPIO19), + }; // SSD1315 display reset: pulse GPIO21 low->high before I2C init let mut display_rst = Output::new(p.GPIO21, Level::Low, OutputConfig::default()); @@ -51,7 +53,7 @@ impl Board { esp_hal::delay::Delay::new().delay_millis(10); core::mem::forget(display_rst); // hold reset high permanently - let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18); + let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18); let mac = Self::mac_address(); let display = Some(DisplayParts { i2c, mac }); diff --git a/firmware/src/board/rak_wisblock_4631.rs b/firmware/src/board/rak_wisblock_4631.rs index 2af4a0ea..5dc937e7 100644 --- a/firmware/src/board/rak_wisblock_4631.rs +++ b/firmware/src/board/rak_wisblock_4631.rs @@ -1,40 +1,27 @@ use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice; use embassy_nrf::gpio::{Input, Level, Output, OutputDrive, Pull}; use embassy_nrf::spim::{self, Spim}; -use embassy_nrf::twim::{self, Twim}; +use embassy_nrf::twim; use embassy_nrf::usb::Driver; -use embassy_nrf::{bind_interrupts, Peripherals}; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_time::Delay; -use static_cell::StaticCell; use lora_phy::iv::GenericSx126xInterfaceVariant; use lora_phy::sx126x::{self, Sx1262, Sx126x}; use super::traits::LoRaBoard; +use crate::hal::nrf52840 as mcu; -bind_interrupts!(struct Irqs { - USBD => embassy_nrf::usb::InterruptHandler; - SPIM3 => embassy_nrf::spim::InterruptHandler; - TWISPI0 => embassy_nrf::twim::InterruptHandler; -}); +// ── Concrete peripheral types ─────────────────────────────────────── -// ── Concrete peripheral types ──────────────────────────────────────── - -type SpiBus = Spim<'static>; type Nss = Output<'static>; type Iv = GenericSx126xInterfaceVariant, Input<'static>>; -type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>; -/// SX126x driver (pre-LoRa construction — LoRa::new is async). +type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>; pub type RadioDriver = Sx126x; +pub type UsbDriver = mcu::UsbNrfDriver; +pub type DisplayI2c = mcu::I2cBus; -/// USB driver for CDC-ACM. -pub type UsbDriver = Driver<'static, &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect>; - -/// I2C bus for an optional SSD1306 OLED. -pub type DisplayI2c = Twim<'static>; - -// ── Peripheral bundles ─────────────────────────────────────────────── +// ── Peripheral bundles ────────────────────────────────────────────── pub struct RadioParts { pub driver: RadioDriver, @@ -50,13 +37,13 @@ pub struct DisplayParts { pub mac: [u8; 6], } -// ── Board init ─────────────────────────────────────────────────────── +// ── Board init ────────────────────────────────────────────────────── pub struct Board { - p: Peripherals, + p: embassy_nrf::Peripherals, } -impl super::traits::LoRaBoard for Board { +impl LoRaBoard for Board { const NAME: &'static str = "RAK WisBlock 4631"; const TX_POWER_RANGE: (i8, i8) = (-9, 22); @@ -66,23 +53,7 @@ impl super::traits::LoRaBoard for Board { } fn mac_address() -> [u8; 6] { - // SAFETY: 0x10000000 is the nRF52840 FICR (Factory Information Configuration - // Registers) base address. Offsets 0x0A4 and 0x0A8 are the DEVICEADDR[0] and - // DEVICEADDR[1] registers containing the factory-programmed device address. - // read_volatile is correct for memory-mapped hardware registers. - unsafe { - let ficr = 0x10000000 as *const u32; - let addr0 = core::ptr::read_volatile(ficr.byte_add(0x0A4)); - let addr1 = core::ptr::read_volatile(ficr.byte_add(0x0A8)); - [ - addr0 as u8, - (addr0 >> 8) as u8, - (addr0 >> 16) as u8, - (addr0 >> 24) as u8, - addr1 as u8, - (addr1 >> 8) as u8, - ] - } + mcu::mac_address() } } @@ -90,19 +61,16 @@ impl Board { pub fn into_parts(self) -> (RadioParts, UsbParts, Option) { let p = self.p; - // ── SPI bus for SX1262 ─────────────────────────────────── + // ── SPI bus for SX1262 ────────────────────────────────── let mut spi_cfg = spim::Config::default(); spi_cfg.frequency = spim::Frequency::M1; - let spi = Spim::new(p.SPI3, Irqs, p.P1_11, p.P1_13, p.P1_12, spi_cfg); - - static SPI_BUS: StaticCell> = - StaticCell::new(); - let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi)); + let spi = Spim::new(p.SPI3, mcu::Irqs, p.P1_11, p.P1_13, p.P1_12, spi_cfg); + let spi_bus = mcu::share_spi_bus(spi); let nss = Output::new(p.P1_10, Level::High, OutputDrive::Standard); let spi_device = SpiDevice::new(spi_bus, nss); - // ── SX1262 control pins ────────────────────────────────── + // ── SX1262 control pins ───────────────────────────────── let reset = Output::new(p.P1_06, Level::High, OutputDrive::Standard); let dio1 = Input::new(p.P1_15, Pull::Down); let busy = Input::new(p.P1_14, Pull::Down); @@ -122,23 +90,17 @@ impl Board { delay: Delay, }; - // ── USB ────────────────────────────────────────────────── - static VBUS: StaticCell = - StaticCell::new(); - let vbus = VBUS.init(embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new( - true, false, - )); + // ── USB ───────────────────────────────────────────────── + let vbus = mcu::alloc_vbus_detect(true, false); let usb = UsbParts { - driver: Driver::new(p.USBD, Irqs, vbus), + driver: Driver::new(p.USBD, mcu::Irqs, vbus), }; - // ── Display (optional RAK1921 SSD1306 OLED on I2C) ────── - // Always provide the I2C bus; display_task detects presence - // via SSD1306 init (fails gracefully if no display attached). - static TWIM_BUF: StaticCell<[u8; 256]> = StaticCell::new(); - let twim_buf = TWIM_BUF.init([0u8; 256]); - let i2c_cfg = twim::Config::default(); - let i2c = Twim::new(p.TWISPI0, Irqs, p.P0_13, p.P0_14, i2c_cfg, twim_buf); + // ── Display (optional RAK1921 SSD1306 OLED on I2C) ───── + let twim_buf = mcu::alloc_i2c_buffer(); + let i2c = embassy_nrf::twim::Twim::new( + p.TWISPI0, mcu::Irqs, p.P0_13, p.P0_14, twim::Config::default(), twim_buf, + ); let mac = Self::mac_address(); let display = Some(DisplayParts { i2c, mac }); diff --git a/firmware/src/board/wio_tracker_l1.rs b/firmware/src/board/wio_tracker_l1.rs index 3add31a0..464c901f 100644 --- a/firmware/src/board/wio_tracker_l1.rs +++ b/firmware/src/board/wio_tracker_l1.rs @@ -1,40 +1,27 @@ use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice; use embassy_nrf::gpio::{Input, Level, Output, OutputDrive, Pull}; use embassy_nrf::spim::{self, Spim}; -use embassy_nrf::twim::{self, Twim}; +use embassy_nrf::twim; use embassy_nrf::usb::Driver; -use embassy_nrf::{bind_interrupts, Peripherals}; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_time::Delay; -use static_cell::StaticCell; use lora_phy::iv::GenericSx126xInterfaceVariant; use lora_phy::sx126x::{self, Sx1262, Sx126x}; use super::traits::LoRaBoard; +use crate::hal::nrf52840 as mcu; -bind_interrupts!(struct Irqs { - USBD => embassy_nrf::usb::InterruptHandler; - SPIM3 => embassy_nrf::spim::InterruptHandler; - TWISPI0 => embassy_nrf::twim::InterruptHandler; -}); +// ── Concrete peripheral types ─────────────────────────────────────── -// ── Concrete peripheral types ──────────────────────────────────────── - -type SpiBus = Spim<'static>; type Nss = Output<'static>; type Iv = GenericSx126xInterfaceVariant, Input<'static>>; -type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>; -/// SX126x driver (pre-LoRa construction — LoRa::new is async). +type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>; pub type RadioDriver = Sx126x; +pub type UsbDriver = mcu::UsbNrfDriver; +pub type DisplayI2c = mcu::I2cBus; -/// USB driver for CDC-ACM. -pub type UsbDriver = Driver<'static, &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect>; - -/// I2C bus for SH1106 OLED. -pub type DisplayI2c = Twim<'static>; - -// ── Peripheral bundles ─────────────────────────────────────────────── +// ── Peripheral bundles ────────────────────────────────────────────── pub struct RadioParts { pub driver: RadioDriver, @@ -50,13 +37,13 @@ pub struct DisplayParts { pub mac: [u8; 6], } -// ── Board init ─────────────────────────────────────────────────────── +// ── Board init ────────────────────────────────────────────────────── pub struct Board { - p: Peripherals, + p: embassy_nrf::Peripherals, } -impl super::traits::LoRaBoard for Board { +impl LoRaBoard for Board { const NAME: &'static str = "Wio Tracker L1"; const TX_POWER_RANGE: (i8, i8) = (-9, 22); @@ -69,23 +56,7 @@ impl super::traits::LoRaBoard for Board { } fn mac_address() -> [u8; 6] { - // SAFETY: 0x10000000 is the nRF52840 FICR (Factory Information Configuration - // Registers) base address. Offsets 0x0A4 and 0x0A8 are the DEVICEADDR[0] and - // DEVICEADDR[1] registers containing the factory-programmed device address. - // read_volatile is correct for memory-mapped hardware registers. - unsafe { - let ficr = 0x10000000 as *const u32; - let addr0 = core::ptr::read_volatile(ficr.byte_add(0x0A4)); - let addr1 = core::ptr::read_volatile(ficr.byte_add(0x0A8)); - [ - addr0 as u8, - (addr0 >> 8) as u8, - (addr0 >> 16) as u8, - (addr0 >> 24) as u8, - addr1 as u8, - (addr1 >> 8) as u8, - ] - } + mcu::mac_address() } } @@ -93,19 +64,16 @@ impl Board { pub fn into_parts(self) -> (RadioParts, UsbParts, Option) { let p = self.p; - // ── SPI bus for SX1262 ─────────────────────────────────── + // ── SPI bus for SX1262 ────────────────────────────────── let mut spi_cfg = spim::Config::default(); spi_cfg.frequency = spim::Frequency::M1; - let spi = Spim::new(p.SPI3, Irqs, p.P0_30, p.P0_03, p.P0_28, spi_cfg); - - static SPI_BUS: StaticCell> = - StaticCell::new(); - let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi)); + let spi = Spim::new(p.SPI3, mcu::Irqs, p.P0_30, p.P0_03, p.P0_28, spi_cfg); + let spi_bus = mcu::share_spi_bus(spi); let nss = Output::new(p.P1_14, Level::High, OutputDrive::Standard); let spi_device = SpiDevice::new(spi_bus, nss); - // ── SX1262 control pins ────────────────────────────────── + // ── SX1262 control pins ───────────────────────────────── let reset = Output::new(p.P1_07, Level::High, OutputDrive::Standard); let dio1 = Input::new(p.P0_07, Pull::Down); let busy = Input::new(p.P1_10, Pull::Down); @@ -127,25 +95,21 @@ impl Board { delay: Delay, }; - // ── USB ────────────────────────────────────────────────── - static VBUS: StaticCell = - StaticCell::new(); - let vbus = VBUS.init(embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new( - true, true, - )); + // ── USB ───────────────────────────────────────────────── + let vbus = mcu::alloc_vbus_detect(true, true); let usb = UsbParts { - driver: Driver::new(p.USBD, Irqs, vbus), + driver: Driver::new(p.USBD, mcu::Irqs, vbus), }; // ── BAT_CTL: P0.04, active HIGH — enables power to OLED and battery sense let bat_ctl = Output::new(p.P0_04, Level::High, OutputDrive::Standard); core::mem::forget(bat_ctl); - // ── Display (SH1106 OLED on I2C) ───────────────────────── - static TWIM_BUF: StaticCell<[u8; 256]> = StaticCell::new(); - let twim_buf = TWIM_BUF.init([0u8; 256]); - let i2c_cfg = twim::Config::default(); - let i2c = Twim::new(p.TWISPI0, Irqs, p.P0_06, p.P0_05, i2c_cfg, twim_buf); + // ── Display (SH1106 OLED on I2C) ──────────────────────── + let twim_buf = mcu::alloc_i2c_buffer(); + let i2c = embassy_nrf::twim::Twim::new( + p.TWISPI0, mcu::Irqs, p.P0_06, p.P0_05, twim::Config::default(), twim_buf, + ); let mac = Self::mac_address(); let display = Some(DisplayParts { i2c, mac }); diff --git a/firmware/src/display/mod.rs b/firmware/src/display/mod.rs index c92bc917..a6e8de13 100644 --- a/firmware/src/display/mod.rs +++ b/firmware/src/display/mod.rs @@ -1,7 +1,5 @@ -//! OLED display driver with radio status dashboard. +//! OLED display dashboard: radio status, sparkline, splash screen. mod render; -#[cfg(feature = "wio_tracker_l1")] -pub mod sh1106; mod task; pub use task::display_task; diff --git a/firmware/src/display/task.rs b/firmware/src/display/task.rs index 5b28e253..a869c412 100644 --- a/firmware/src/display/task.rs +++ b/firmware/src/display/task.rs @@ -123,7 +123,7 @@ pub async fn display_task( cfg_if::cfg_if! { if #[cfg(feature = "wio_tracker_l1")] { - let mut display = super::sh1106::Sh1106::new(parts.i2c, 0x3D); + let mut display = crate::driver::sh1106::Sh1106::new(parts.i2c, 0x3D); Timer::after_millis(100).await; diff --git a/firmware/src/driver/mod.rs b/firmware/src/driver/mod.rs new file mode 100644 index 00000000..cbba9cfc --- /dev/null +++ b/firmware/src/driver/mod.rs @@ -0,0 +1,2 @@ +#[allow(dead_code)] +pub mod sh1106; diff --git a/firmware/src/display/sh1106.rs b/firmware/src/driver/sh1106.rs similarity index 100% rename from firmware/src/display/sh1106.rs rename to firmware/src/driver/sh1106.rs diff --git a/firmware/src/hal/esp32s3.rs b/firmware/src/hal/esp32s3.rs new file mode 100644 index 00000000..c110db23 --- /dev/null +++ b/firmware/src/hal/esp32s3.rs @@ -0,0 +1,121 @@ +//! ESP32-S3 MCU initialization primitives. +//! +//! Provides low-level bus and peripheral init for ESP32-S3 boards. +//! Board files call these with specific pins and construct higher-level +//! drivers (Sx126x, SSD1306, etc.) themselves. + +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use esp_hal::dma::{DmaRxBuf, DmaTxBuf}; +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; + +// ── MCU-level types ───────────────────────────────────────────────── + +pub type SpiBus = SpiDmaBus<'static, esp_hal::Async>; +pub type I2cBus = I2c<'static, esp_hal::Async>; +pub type UsbOtgDriver = esp_hal::otg_fs::asynch::Driver<'static>; +pub type UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>; + +// ── Timer ─────────────────────────────────────────────────────────── + +/// 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); +} + +// ── SPI bus ───────────────────────────────────────────────────────── + +/// Initialize SPI2 with DMA and wrap in a shared bus (StaticCell + Mutex). +/// +/// Returns a reference to the shared SPI bus suitable for `SpiDevice::new()`. +pub fn init_spi( + 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>, +) -> &'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_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(); + SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi)) +} + +// ── I2C bus ───────────────────────────────────────────────────────── + +/// Initialize I2C0 for peripherals (display, sensors, etc.). +pub fn init_i2c( + i2c0: esp_hal::peripherals::I2C0<'static>, + sda: esp_hal::peripherals::GPIO17<'static>, + scl: esp_hal::peripherals::GPIO18<'static>, +) -> I2cBus { + I2c::new(i2c0, I2cConfig::default()) + .expect("I2C init") + .with_sda(sda) + .with_scl(scl) + .into_async() +} + +// ── USB OTG ───────────────────────────────────────────────────────── + +/// Initialize USB OTG FS driver for CDC-ACM. +pub fn init_usb( + usb0: esp_hal::peripherals::USB0<'static>, + dp: esp_hal::peripherals::GPIO20<'static>, + dm: esp_hal::peripherals::GPIO19<'static>, +) -> UsbOtgDriver { + 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]); + esp_hal::otg_fs::asynch::Driver::new( + usb_inst, + ep_out_buf, + esp_hal::otg_fs::asynch::Config::default(), + ) +} + +// ── UART ──────────────────────────────────────────────────────────── + +/// Initialize UART0 for boards with USB-UART bridge chips (e.g. CP2102). +pub fn init_uart( + uart0: esp_hal::peripherals::UART0<'static>, + tx: esp_hal::peripherals::GPIO43<'static>, + rx: esp_hal::peripherals::GPIO44<'static>, +) -> UartDriver { + use esp_hal::uart::{Config as UartConfig, Uart}; + + Uart::new(uart0, UartConfig::default()) + .expect("UART init") + .with_tx(tx) + .with_rx(rx) + .into_async() +} + +// ── MAC address ───────────────────────────────────────────────────── + +/// Read the factory-programmed MAC address from eFuse. +pub fn mac_address() -> [u8; 6] { + esp_hal::efuse::Efuse::mac_address() +} diff --git a/firmware/src/hal/mod.rs b/firmware/src/hal/mod.rs new file mode 100644 index 00000000..d430ab39 --- /dev/null +++ b/firmware/src/hal/mod.rs @@ -0,0 +1,10 @@ +//! MCU family initialization primitives. +//! +//! Each submodule provides low-level bus and peripheral init for one MCU family. +//! Board files call these with specific pins and construct higher-level drivers. + +#[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] +pub mod esp32s3; + +#[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))] +pub mod nrf52840; diff --git a/firmware/src/hal/nrf52840.rs b/firmware/src/hal/nrf52840.rs new file mode 100644 index 00000000..1bf4c029 --- /dev/null +++ b/firmware/src/hal/nrf52840.rs @@ -0,0 +1,82 @@ +//! nRF52840 MCU initialization primitives. +//! +//! Provides shared boilerplate for nRF52840 boards: interrupt bindings, +//! type aliases, StaticCell bus wrapping, USB init, and MAC address reading. + +use embassy_nrf::bind_interrupts; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use static_cell::StaticCell; + +// ── Interrupts (shared by all nRF52840 boards) ───────────────────── + +bind_interrupts!(pub struct Irqs { + USBD => embassy_nrf::usb::InterruptHandler; + SPIM3 => embassy_nrf::spim::InterruptHandler; + TWISPI0 => embassy_nrf::twim::InterruptHandler; +}); + +// ── MCU-level types ───────────────────────────────────────────────── + +pub type SpiBus = embassy_nrf::spim::Spim<'static>; +pub type I2cBus = embassy_nrf::twim::Twim<'static>; +pub type UsbNrfDriver = + embassy_nrf::usb::Driver<'static, &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect>; + +// ── SPI bus sharing ───────────────────────────────────────────────── + +/// Wrap an initialized SPI peripheral in a shared bus (StaticCell + Mutex). +pub fn share_spi_bus( + spi: SpiBus, +) -> &'static embassy_sync::mutex::Mutex { + static SPI_BUS: StaticCell> = + StaticCell::new(); + SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi)) +} + +// ── I2C DMA buffer ────────────────────────────────────────────────── + +/// Allocate a DMA-safe buffer for Twim I2C. +/// +/// Every nRF52840 Twim user needs a `&'static mut [u8]` buffer. +/// This provides one via StaticCell so board files don't repeat the pattern. +pub fn alloc_i2c_buffer() -> &'static mut [u8; 256] { + static TWIM_BUF: StaticCell<[u8; 256]> = StaticCell::new(); + TWIM_BUF.init([0u8; 256]) +} + +// ── USB VBUS detection ────────────────────────────────────────────── + +/// Allocate and initialize software VBUS detection. +pub fn alloc_vbus_detect( + vbus_detect: bool, + self_powered: bool, +) -> &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect { + static VBUS: StaticCell = StaticCell::new(); + VBUS.init(embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new( + vbus_detect, + self_powered, + )) +} + +// ── MAC address ───────────────────────────────────────────────────── + +/// Read the factory-programmed device address from FICR registers. +pub fn mac_address() -> [u8; 6] { + // SAFETY: 0x10000000 is the nRF52840 FICR (Factory Information Configuration + // Registers) base address. Offsets 0x0A4 and 0x0A8 are the DEVICEADDR[0] and + // DEVICEADDR[1] registers containing the factory-programmed device address. + // read_volatile is correct for memory-mapped hardware registers. + unsafe { + let ficr = 0x10000000 as *const u32; + let addr0 = core::ptr::read_volatile(ficr.byte_add(0x0A4)); + let addr1 = core::ptr::read_volatile(ficr.byte_add(0x0A8)); + [ + addr0 as u8, + (addr0 >> 8) as u8, + (addr0 >> 16) as u8, + (addr0 >> 24) as u8, + addr1 as u8, + (addr1 >> 8) as u8, + ] + } +} diff --git a/firmware/src/main.rs b/firmware/src/main.rs index 68028861..a395ec37 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -21,6 +21,10 @@ mod board; mod channel; #[cfg(not(test))] mod display; +#[cfg(not(test))] +mod driver; +#[cfg(not(test))] +mod hal; mod protocol; #[cfg(not(test))] mod radio;