feat: add Waveshare RP2040-LoRa board support
First RP2040 board and third MCU family (after ESP32-S3 and nRF52840). Hardware: RP2040 + SX1262 radio, 850-930 MHz HF, PE4259 antenna switch, LED on GPIO25, no OLED, USB-C via FPC ribbon cable to adapter board. New files: - src/hal/rp2040.rs: MCU primitives (SPI bus, USB driver, flash unique ID MAC) - src/board/waveshare_rp2040_lora.rs: board implementation - ld/rp2040-memory.x: linker memory layout Also fixes cortex-m-rt/set-vtor (moved from global dep to per-board feature, since Cortex-M0+ has no VTOR register) and makes build.rs memory.x selection target-aware.
This commit is contained in:
14 files changed
+842
-14
No files matched your search
@@ -7,6 +7,7 @@
|
||||
Create `src/board/your_board.rs`. Use an existing board as a template:
|
||||
- ESP32-S3 boards: copy `heltec_v4.rs` (uses shared helpers from `board/esp32s3.rs` and `hal/esp32s3.rs`)
|
||||
- nRF52840 boards: copy `rak_wisblock_4631.rs` (uses shared helpers from `hal/nrf52840.rs`)
|
||||
- RP2040 boards: copy `waveshare_rp2040_lora.rs` (uses shared helpers from `hal/rp2040.rs`)
|
||||
- New MCU family: create a new `hal/<mcu>.rs` with MCU primitives first
|
||||
|
||||
`build.rs` auto-discovers your board file — any `.rs` file in `src/board/` that
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||
use embassy_rp::gpio::{Input, Level, Output, Pull};
|
||||
use embassy_rp::spi;
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use embassy_time::Delay;
|
||||
|
||||
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
||||
|
||||
use super::traits::{BoardParts, LoRaBoard};
|
||||
use crate::hal::rp2040 as mcu;
|
||||
|
||||
// ── Concrete peripheral types ──────────────────────────────────────
|
||||
|
||||
type Nss = Output<'static>;
|
||||
type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
||||
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>;
|
||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||
pub type UsbDriver = mcu::UsbRpDriver;
|
||||
pub type LedDriver = crate::driver::simple_led::SimpleLed<Output<'static>>;
|
||||
|
||||
// Display types — never instantiated (no OLED on this board), but the
|
||||
// trait requires concrete associated types that satisfy the bounds.
|
||||
pub type DisplayI2c = embassy_rp::i2c::I2c<'static, embassy_rp::peripherals::I2C0, embassy_rp::i2c::Async>;
|
||||
pub type DisplayDriver = ssd1306::Ssd1306Async<
|
||||
ssd1306::prelude::I2CInterface<DisplayI2c>,
|
||||
ssd1306::size::DisplaySize128x64,
|
||||
ssd1306::mode::BufferedGraphicsModeAsync<ssd1306::size::DisplaySize128x64>,
|
||||
>;
|
||||
|
||||
// ── Peripheral bundles ─────────────────────────────────────────────
|
||||
|
||||
pub struct RadioParts {
|
||||
pub driver: RadioDriver,
|
||||
pub delay: Delay,
|
||||
}
|
||||
|
||||
pub struct UsbParts {
|
||||
pub driver: UsbDriver,
|
||||
}
|
||||
|
||||
pub struct DisplayParts {
|
||||
pub i2c: DisplayI2c,
|
||||
}
|
||||
|
||||
// ── Display init ───────────────────────────────────────────────────
|
||||
|
||||
pub async fn create_display(_i2c: DisplayI2c) -> Option<DisplayDriver> {
|
||||
None
|
||||
}
|
||||
|
||||
// ── Board init ─────────────────────────────────────────────────────
|
||||
|
||||
pub struct Board {
|
||||
p: embassy_rp::Peripherals,
|
||||
}
|
||||
|
||||
impl LoRaBoard for Board {
|
||||
const NAME: &'static str = "Waveshare RP2040-LoRa";
|
||||
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
type RadioParts = RadioParts;
|
||||
type CommParts = UsbParts;
|
||||
type DisplayParts = DisplayParts;
|
||||
type DisplayDriver = DisplayDriver;
|
||||
type LedDriver = LedDriver;
|
||||
|
||||
fn init() -> Self {
|
||||
let p = embassy_rp::init(Default::default());
|
||||
Self { p }
|
||||
}
|
||||
|
||||
fn mac_address() -> [u8; 6] {
|
||||
mcu::mac_address()
|
||||
}
|
||||
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
|
||||
let p = self.p;
|
||||
|
||||
// ── MAC address from flash unique ID ───────────────────
|
||||
let mut flash = embassy_rp::flash::Flash::<_, _, { 2 * 1024 * 1024 }>::new_blocking(p.FLASH);
|
||||
mcu::init_mac(&mut flash);
|
||||
|
||||
// ── SPI1 bus for SX1262 ────────────────────────────────
|
||||
//
|
||||
// Pin mapping from Meshtastic & MeshCore firmware (confirmed working):
|
||||
// SCK = GPIO14, MOSI = GPIO15, MISO = GPIO24, CS = GPIO13
|
||||
//
|
||||
// The Waveshare wiki (https://www.waveshare.com/wiki/RP2040-LoRa) lists
|
||||
// different pins (MOSI=11, MISO=8, CS=9) but those don't work in practice.
|
||||
// Both sets are valid SPI1 alternate functions on RP2040.
|
||||
let mut spi_cfg = spi::Config::default();
|
||||
spi_cfg.frequency = 1_000_000;
|
||||
let spi = spi::Spi::new(
|
||||
p.SPI1, p.PIN_14, p.PIN_15, p.PIN_24, p.DMA_CH0, p.DMA_CH1, spi_cfg,
|
||||
);
|
||||
let spi_bus = mcu::share_spi_bus(spi);
|
||||
|
||||
let nss = Output::new(p.PIN_13, Level::High);
|
||||
let spi_device = SpiDevice::new(spi_bus, nss);
|
||||
|
||||
let reset = Output::new(p.PIN_23, Level::High);
|
||||
let dio1 = Input::new(p.PIN_16, Pull::Down);
|
||||
let busy = Input::new(p.PIN_18, Pull::Down);
|
||||
|
||||
// PE4259 antenna switch: GPIO17 = !CTRL (rx_enable).
|
||||
// DIO2 is internally wired as CTRL (tx_enable), managed by the SX1262.
|
||||
let rx_enable = Output::new(p.PIN_17, Level::Low);
|
||||
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, Some(rx_enable), None)
|
||||
.expect("SX1262 interface init");
|
||||
|
||||
let sx_config = sx126x::Config {
|
||||
chip: Sx1262,
|
||||
tcxo_ctrl: None, // No TCXO on this board
|
||||
use_dcdc: true,
|
||||
rx_boost: false,
|
||||
};
|
||||
|
||||
let radio = RadioParts {
|
||||
driver: Sx126x::new(spi_device, iv, sx_config),
|
||||
delay: Delay,
|
||||
};
|
||||
|
||||
// ── USB ────────────────────────────────────────────────
|
||||
let host = UsbParts {
|
||||
driver: embassy_rp::usb::Driver::new(p.USB, mcu::Irqs),
|
||||
};
|
||||
|
||||
// ── LED on GPIO25 ──────────────────────────────────────
|
||||
let led_pin = Output::new(p.PIN_25, Level::Low);
|
||||
let led = crate::driver::simple_led::SimpleLed(led_pin);
|
||||
|
||||
BoardParts {
|
||||
radio,
|
||||
host,
|
||||
display: None,
|
||||
led,
|
||||
mac: Self::mac_address(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
#[cfg(feature = "wio_tracker_l1")]
|
||||
pub mod sh1106;
|
||||
|
||||
#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "wio_tracker_l1"))]
|
||||
#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))]
|
||||
pub mod simple_led;
|
||||
@@ -8,3 +8,6 @@ pub mod esp32s3;
|
||||
|
||||
#[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))]
|
||||
pub mod nrf52840;
|
||||
|
||||
#[cfg(feature = "waveshare_rp2040_lora")]
|
||||
pub mod rp2040;
|
||||
@@ -0,0 +1,76 @@
|
||||
//! RP2040 MCU initialization primitives.
|
||||
//!
|
||||
//! Provides shared boilerplate for RP2040 boards: interrupt bindings,
|
||||
//! type aliases, StaticCell bus wrapping, USB init, and MAC address reading.
|
||||
|
||||
use embassy_rp::bind_interrupts;
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
// ── Interrupts (shared by all RP2040 boards) ──────────────────────
|
||||
|
||||
bind_interrupts!(pub struct Irqs {
|
||||
USBCTRL_IRQ => embassy_rp::usb::InterruptHandler<embassy_rp::peripherals::USB>;
|
||||
});
|
||||
|
||||
// ── MCU-level types ────────────────────────────────────────────────
|
||||
|
||||
pub type SpiBus = embassy_rp::spi::Spi<'static, embassy_rp::peripherals::SPI1, embassy_rp::spi::Async>;
|
||||
pub type UsbRpDriver = embassy_rp::usb::Driver<'static, embassy_rp::peripherals::USB>;
|
||||
|
||||
// ── 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<NoopRawMutex, SpiBus> {
|
||||
static SPI_BUS: StaticCell<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
|
||||
StaticCell::new();
|
||||
SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi))
|
||||
}
|
||||
|
||||
// ── MAC address ────────────────────────────────────────────────────
|
||||
|
||||
/// Store for the MAC address derived from flash unique ID.
|
||||
/// Populated once during `init_mac()`, read by `mac_address()`.
|
||||
static MAC: StaticCell<[u8; 6]> = StaticCell::new();
|
||||
static mut MAC_REF: Option<&'static [u8; 6]> = None;
|
||||
|
||||
/// Read the flash chip's unique ID and derive a 6-byte MAC address.
|
||||
///
|
||||
/// Must be called once during board init before `mac_address()`.
|
||||
pub fn init_mac<const FLASH_SIZE: usize>(
|
||||
flash: &mut embassy_rp::flash::Flash<'_, embassy_rp::peripherals::FLASH, embassy_rp::flash::Blocking, FLASH_SIZE>,
|
||||
) {
|
||||
let mut uid = [0u8; 8];
|
||||
flash.blocking_unique_id(&mut uid).expect("flash unique ID");
|
||||
|
||||
// XOR the first 2 bytes into bytes 2..8 to fold 8 bytes into 6,
|
||||
// then set the locally-administered bit (byte 0, bit 1).
|
||||
let mac = [
|
||||
uid[2] ^ uid[0] | 0x02,
|
||||
uid[3] ^ uid[1],
|
||||
uid[4],
|
||||
uid[5],
|
||||
uid[6],
|
||||
uid[7],
|
||||
];
|
||||
|
||||
let stored = MAC.init(mac);
|
||||
// SAFETY: Written once during single-threaded init, before the executor
|
||||
// starts. All subsequent reads are from &'static reference.
|
||||
unsafe {
|
||||
MAC_REF = Some(stored);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the MAC address derived from the flash unique ID.
|
||||
///
|
||||
/// Panics if `init_mac()` has not been called.
|
||||
pub fn mac_address() -> [u8; 6] {
|
||||
// SAFETY: MAC_REF is set once during init before the executor starts,
|
||||
// and only read (never written) after that.
|
||||
unsafe {
|
||||
*MAC_REF.expect("init_mac() must be called before mac_address()")
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -41,7 +41,7 @@ use crate::channel::{CommandChannel, DisplayCommandChannel, ResponseChannel, Sta
|
||||
|
||||
#[cfg(not(test))]
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))] {
|
||||
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"))] {
|
||||
@@ -61,7 +61,7 @@ static DISPLAY_COMMANDS: DisplayCommandChannel = DisplayCommandChannel::new();
|
||||
|
||||
#[cfg(not(test))]
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))] {
|
||||
if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))] {
|
||||
#[embassy_executor::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
run(spawner).await;
|
||||
|
||||
Reference in new issue
Block a user