refactor: LoRaBoard trait for compile-time board completeness verification
Every board's Board struct now implements the LoRaBoard trait, which enforces that NAME, TX_POWER_RANGE, init(), and mac_address() are all provided. Missing any of these is a compile error — no "good intentions" needed. The trait verifies metadata and initialization. Tasks use the board's concrete types directly (Embassy tasks must be concrete). Board-specific type aliases (RadioDriver, UsbDriver, DisplayI2c) and struct definitions (RadioParts, UsbParts, DisplayParts) remain in each board file. - New: src/board/traits.rs (LoRaBoard trait definition) - Modified: all 3 board files (implement trait, remove standalone constants) - Modified: main.rs (import trait for init) - Modified: radio/task.rs (use Board::TX_POWER_RANGE via trait) - Modified: display/task.rs (use Board::NAME via trait) - Modified: build.rs (exclude traits.rs from board discovery) - Removed: embedded-hal-async direct dep (not needed for trait approach)
This commit is contained in:
11 files changed
+107
-55
No files matched your search
+12
-4
@@ -1,3 +1,4 @@
|
||||
use super::traits::LoRaBoard;
|
||||
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use embassy_time::Delay;
|
||||
@@ -25,8 +26,6 @@ pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||
|
||||
pub type DisplayI2c = I2c<'static, esp_hal::Async>;
|
||||
|
||||
/// TX power range for this board's radio (SX1262).
|
||||
pub const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||
|
||||
@@ -50,12 +49,21 @@ pub struct Board {
|
||||
p: esp_hal::peripherals::Peripherals,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn init() -> Self {
|
||||
impl LoRaBoard for Board {
|
||||
const NAME: &'static str = "Heltec V3";
|
||||
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
fn init() -> Self {
|
||||
let p = esp_hal::init(esp_hal::Config::default());
|
||||
Self { p }
|
||||
}
|
||||
|
||||
fn mac_address() -> [u8; 6] {
|
||||
esp_hal::efuse::Efuse::mac_address()
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
||||
let p = self.p;
|
||||
|
||||
|
||||
+19
-13
@@ -13,6 +13,8 @@ use static_cell::StaticCell;
|
||||
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
||||
|
||||
use super::traits::LoRaBoard;
|
||||
|
||||
// ── Concrete peripheral types ────────────────────────────────────────
|
||||
|
||||
type SpiBus = SpiDmaBus<'static, esp_hal::Async>;
|
||||
@@ -25,9 +27,6 @@ pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||
|
||||
pub type DisplayI2c = I2c<'static, esp_hal::Async>;
|
||||
|
||||
/// TX power range for this board's radio (SX1262).
|
||||
pub const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||
|
||||
pub struct RadioParts {
|
||||
@@ -44,18 +43,27 @@ pub struct DisplayParts {
|
||||
pub mac: [u8; 6],
|
||||
}
|
||||
|
||||
// ── Board init ───────────────────────────────────────────────────────
|
||||
// ── Board implementation ────────────────────────────────────────────
|
||||
|
||||
pub struct Board {
|
||||
p: esp_hal::peripherals::Peripherals,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn init() -> Self {
|
||||
impl LoRaBoard for Board {
|
||||
const NAME: &'static str = "Heltec V4";
|
||||
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
fn init() -> Self {
|
||||
let p = esp_hal::init(esp_hal::Config::default());
|
||||
Self { p }
|
||||
}
|
||||
|
||||
fn mac_address() -> [u8; 6] {
|
||||
esp_hal::efuse::Efuse::mac_address()
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
||||
let p = self.p;
|
||||
|
||||
@@ -64,7 +72,6 @@ impl Board {
|
||||
esp_hal_embassy::init(timg0.timer0);
|
||||
|
||||
// ── Vext power: GPIO36 ─────────────────────────────────────
|
||||
// V4 datasheet says "pull low to enable Vext"
|
||||
let vext = Output::new(p.GPIO36, Level::Low);
|
||||
core::mem::forget(vext); // hold pin low permanently; drop would reset it
|
||||
|
||||
@@ -101,7 +108,8 @@ impl Board {
|
||||
let dio1 = Input::new(p.GPIO14, Pull::Down);
|
||||
let busy = Input::new(p.GPIO13, Pull::Down);
|
||||
|
||||
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None).expect("SX1262 interface init");
|
||||
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None)
|
||||
.expect("SX1262 interface init");
|
||||
|
||||
let sx_config = sx126x::Config {
|
||||
chip: Sx1262,
|
||||
@@ -115,9 +123,8 @@ impl Board {
|
||||
delay: Delay,
|
||||
};
|
||||
|
||||
// ── USB Serial (via USB-Serial-JTAG, shares PHY safely) ───
|
||||
// ── USB OTG CDC-ACM driver ──────────────────────────────
|
||||
// Note: this switches the internal USB PHY from Serial-JTAG to OTG.
|
||||
// ── 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();
|
||||
@@ -131,7 +138,6 @@ impl Board {
|
||||
};
|
||||
|
||||
// ── Display (SSD1315 OLED on I2C, 0x3C) ───────────────────
|
||||
// Reset display controller
|
||||
let mut display_rst = Output::new(p.GPIO21, Level::Low);
|
||||
esp_hal::delay::Delay::new().delay_millis(10);
|
||||
display_rst.set_high();
|
||||
@@ -142,7 +148,7 @@ impl Board {
|
||||
.with_sda(p.GPIO17)
|
||||
.with_scl(p.GPIO18)
|
||||
.into_async();
|
||||
let mac = esp_hal::efuse::Efuse::mac_address();
|
||||
let mac = Self::mac_address();
|
||||
let display = Some(DisplayParts { i2c, mac });
|
||||
|
||||
(radio, usb, display)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// @generated by build.rs — DO NOT EDIT
|
||||
pub mod traits;
|
||||
pub use traits::*;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
{%- for board in boards %}
|
||||
{% if loop.first %}if{% else %}else if{% endif %} #[cfg(feature = "{{ board }}")] {
|
||||
|
||||
@@ -12,6 +12,8 @@ use static_cell::StaticCell;
|
||||
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
||||
|
||||
use super::traits::LoRaBoard;
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
USBD => embassy_nrf::usb::InterruptHandler<USBD>;
|
||||
SPIM3 => embassy_nrf::spim::InterruptHandler<embassy_nrf::peripherals::SPI3>;
|
||||
@@ -33,9 +35,6 @@ pub type UsbDriver = Driver<'static, USBD, &'static embassy_nrf::usb::vbus_detec
|
||||
/// I2C bus for an optional SSD1306 OLED.
|
||||
pub type DisplayI2c = Twim<'static, embassy_nrf::peripherals::TWISPI0>;
|
||||
|
||||
/// TX power range for this board's radio (SX1262).
|
||||
pub const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||
|
||||
pub struct RadioParts {
|
||||
@@ -58,12 +57,34 @@ pub struct Board {
|
||||
p: Peripherals,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn init() -> Self {
|
||||
impl super::traits::LoRaBoard for Board {
|
||||
const NAME: &'static str = "RAK WisBlock 4631";
|
||||
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
fn init() -> Self {
|
||||
let p = embassy_nrf::init(Default::default());
|
||||
Self { p }
|
||||
}
|
||||
|
||||
fn mac_address() -> [u8; 6] {
|
||||
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,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
|
||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
||||
let p = self.p;
|
||||
|
||||
@@ -111,22 +132,7 @@ impl Board {
|
||||
// via SSD1306 init (fails gracefully if no display attached).
|
||||
let i2c_cfg = twim::Config::default();
|
||||
let i2c = Twim::new(p.TWISPI0, Irqs, p.P0_13, p.P0_14, i2c_cfg);
|
||||
// Read device address from FICR (factory info registers).
|
||||
// FICR is at fixed address 0x10000000 on nRF52840.
|
||||
let mac = unsafe {
|
||||
let ficr = 0x10000000 as *const u32;
|
||||
// DEVICEADDR[0] at offset 0x0A4, DEVICEADDR[1] at offset 0x0A8
|
||||
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,
|
||||
]
|
||||
};
|
||||
let mac = Self::mac_address();
|
||||
let display = Some(DisplayParts { i2c, mac });
|
||||
|
||||
(radio, usb, display)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Board abstraction trait.
|
||||
//!
|
||||
//! Every board must implement [`LoRaBoard`]. The compiler enforces completeness —
|
||||
//! missing constants or methods are compile errors, not runtime surprises.
|
||||
//!
|
||||
//! Board modules also export concrete types (`RadioParts`, `UsbParts`,
|
||||
//! `DisplayParts`, `RadioDriver`, `UsbDriver`, `DisplayI2c`) that the tasks
|
||||
//! use directly. The trait verifies metadata and initialization; the types
|
||||
//! verify hardware compatibility.
|
||||
//!
|
||||
//! See `src/board/PORTING.md` for a step-by-step guide to adding a new board.
|
||||
|
||||
/// Compile-time contract for a DongLoRa board.
|
||||
///
|
||||
/// Every board's `Board` struct must implement this trait. It verifies that
|
||||
/// the board provides all required metadata and initialization methods.
|
||||
/// The tasks use the board's concrete types directly (not the trait's
|
||||
/// associated types) because Embassy tasks must be concrete.
|
||||
pub trait LoRaBoard {
|
||||
/// Human-readable board name (shown on display splash screen).
|
||||
const NAME: &'static str;
|
||||
|
||||
/// TX power range in dBm (min, max) for this board's radio + PA.
|
||||
const TX_POWER_RANGE: (i8, i8);
|
||||
|
||||
/// Initialize the board hardware.
|
||||
fn init() -> Self;
|
||||
|
||||
/// Read the board's unique hardware address (MAC, device ID, etc.).
|
||||
fn mac_address() -> [u8; 6];
|
||||
}
|
||||
Reference in new issue
Block a user