feat(boards): RAK4631 WisBlock bring-up + build-system cleanup

RAK4631 board:
- SX126X_POWER_EN on P1.05 driven HIGH at init (radio is power-gated;
  without this the chip has no power and SPI hangs)
- HFXO + LFXO external crystals in embassy_nrf::Config (USB CDC-ACM
  needs +/- 500 ppm which the internal RC oscillator can't provide)
- SoftwareVbusDetect(true, true) so USB enumerates without needing a
  CLOCK_POWER interrupt handler
- Green LED on P1.03 via SimpleLed (RAK19007 Base LED1, active HIGH)
- driver/mod.rs: open simple_led feature gate for rak_wisblock_4631
- Spim::new pin-order comment on SPI setup (arg order is sck/miso/mosi,
  not sck/mosi/miso — easy to get wrong)

debug-checkpoint instrumentation (feature-gated, zero cost in release):
- radio.rs: debug_blink::set(N) at each TX-phase boundary, filling in
  the long-aspirational blink-map doc
- debug_blink.rs: checkpoint 19 between prepare_for_cad and cad(), so
  future bring-ups can distinguish modem-setup hang from DIO1-wait hang

justfile + mise.toml:
- set shell := ["mise", "exec", "--", "sh", "-c"] so recipes
  always resolve tools through mise, not whatever happens to be on PATH
- replace per-board := vars + long if/elif _info chain with a single
  case statement — one place per board, two lists (names + metadata)
- new flash-dfu recipe using adafruit-nrfutil via uvx, with VID:PID
  (239a:0029) port autodetect for the Adafruit nRF52 bootloader
- mise.toml: rust pins now carry ARM targets + clippy, so 'mise install'
  is sufficient bootstrapping (no extra rustup target add steps)

Verified TX on real hardware (RAK19007 Base + RAK4631 Core + RAK1921
OLED, US915 band): ADVERT round-trip with an external receiver.
This commit is contained in:
swaits committed 2026-04-24 00:15:29 -06:00
1 parent 0fbd8412c2
commit 647929ff8d
8 files changed
+116 -71

No files matched your search

+2 -2
View File
@@ -86,8 +86,8 @@ pub fn init_radio(
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 iv =
GenericSx127xInterfaceVariant::new(reset, dio0, None, None).expect("SX1276 interface init");
let sx_config = sx127x::Config {
chip: sx127x::Sx1276,
+22 -6
View File
@@ -20,7 +20,7 @@ type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>;
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
pub type UsbDriver = mcu::UsbNrfDriver;
pub type DisplayI2c = mcu::I2cBus;
pub type LedDriver = ();
pub type LedDriver = crate::driver::simple_led::SimpleLed<Output<'static>>;
pub type DisplayDriver = ssd1306::Ssd1306Async<
ssd1306::prelude::I2CInterface<DisplayI2c>,
@@ -78,10 +78,14 @@ impl LoRaBoard for Board {
type CommParts = UsbParts;
type DisplayParts = DisplayParts;
type DisplayDriver = DisplayDriver;
type LedDriver = ();
type LedDriver = LedDriver;
fn init() -> Self {
let p = embassy_nrf::init(Default::default());
// USB requires the 32 MHz HFXO (±50 ppm); the RC oscillator is out of spec.
let mut config = embassy_nrf::config::Config::default();
config.hfclk_source = embassy_nrf::config::HfclkSource::ExternalXtal;
config.lfclk_source = embassy_nrf::config::LfclkSource::ExternalXtal;
let p = embassy_nrf::init(config);
Self { p }
}
@@ -89,12 +93,18 @@ impl LoRaBoard for Board {
mcu::mac_address()
}
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, ()> {
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
let p = self.p;
// SX1262 is power-gated on P1.05 — must be HIGH before any SPI access.
let sx_power_en = Output::new(p.P1_05, Level::High, OutputDrive::Standard);
core::mem::forget(sx_power_en);
// ── SPI bus for SX1262 ──────────────────────────────────
let mut spi_cfg = spim::Config::default();
spi_cfg.frequency = spim::Frequency::M1;
// embassy_nrf::spim::Spim::new args: (spim, irq, sck, miso, mosi, config).
// Per RAK4631 BSP: SCK=P1.11, MISO=P1.13, MOSI=P1.12.
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);
@@ -121,7 +131,9 @@ impl LoRaBoard for Board {
};
// ── USB ─────────────────────────────────────────────────
let vbus = mcu::alloc_vbus_detect(true, false);
// SoftwareVbusDetect(vbus_detected, power_ready). Mark both true so the
// driver doesn't block waiting for a power-ready signal we never raise.
let vbus = mcu::alloc_vbus_detect(true, true);
let host = UsbParts {
driver: Driver::new(p.USBD, mcu::Irqs, vbus),
};
@@ -138,11 +150,15 @@ impl LoRaBoard for Board {
);
let display = Some(DisplayParts { i2c });
// Green LED on P1.03 (RAK19007 Base, active HIGH).
let led_pin = Output::new(p.P1_03, Level::Low, OutputDrive::Standard);
let led = crate::driver::simple_led::SimpleLed(led_pin);
BoardParts {
radio,
host,
display,
led: (),
led,
mac: Self::mac_address(),
}
}
+10 -9
View File
@@ -138,15 +138,16 @@ fn phase_depth(n: u8) -> u8 {
match n {
18 => 1, // pre-TX: CmdTransmit just read from commands channel
6 => 2, // perform_tx entered
7 => 3, // in CAD
8 => 4, // in prepare_for_tx
9 => 5, // in tx().await
10 => 6, // do_tx returned Ok
11 => 7, // respond_tx_done entered
12 => 8, // respond_tx_done: responses.send returned
13 => 9, // host_task received response from channel
14 => 10, // host_task wrote response to UART (terminal success)
15 => 11, // TX errored (terminal error)
7 => 3, // in CAD (before prepare_for_cad)
19 => 4, // after prepare_for_cad (awaiting lora.cad IRQ)
8 => 5, // in prepare_for_tx
9 => 6, // in tx().await
10 => 7, // do_tx returned Ok
11 => 8, // respond_tx_done entered
12 => 9, // respond_tx_done: responses.send returned
13 => 10, // host_task received response from channel
14 => 11, // host_task wrote response to UART (terminal success)
15 => 12, // TX errored (terminal error)
_ => 0, // not a TX phase
}
}
+1 -1
View File
@@ -37,8 +37,8 @@ use embassy_time::Timer;
use hsmc::statechart;
use crate::board::{self, Board, DisplayDriver, DisplayParts, LedDriver, LoRaBoard, RgbLed};
use crate::driver::DisplayBrightness;
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioEvent, RadioEventChannel};
use crate::driver::DisplayBrightness;
use crate::protocol::LoRaConfig;
use render::{BoardInfo, RSSI_HISTORY_LEN};
+1
View File
@@ -18,6 +18,7 @@ pub mod inverted_pin;
feature = "elecrow_thinknode_m2",
feature = "lilygo_tbeam_supreme",
feature = "lilygo_tbeam",
feature = "rak_wisblock_4631",
feature = "wio_tracker_l1",
feature = "waveshare_rp2040_lora"
))]
+25 -1
View File
@@ -369,6 +369,8 @@ async fn next_packet(
}
async fn perform_tx(lora: &mut LoRaDriver, pending: &mut Option<PendingTx>) -> RadioInput {
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(6);
let Some(tx) = pending.take() else {
return RadioInput::TransmitFailed;
};
@@ -378,7 +380,11 @@ async fn perform_tx(lora: &mut LoRaDriver, pending: &mut Option<PendingTx>) -> R
airtime_us: Instant::now().duration_since(start).as_micros() as u32,
},
Ok(TxOutcome::ChannelBusy) => RadioInput::ChannelBusy,
Err(_) => RadioInput::TransmitFailed,
Err(_) => {
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(15);
RadioInput::TransmitFailed
}
}
}
@@ -705,7 +711,13 @@ async fn do_tx(lora: &mut LoRaDriver, tx: &PendingTx) -> Result<TxOutcome, Radio
let mdltn = to_lora_mod_params(lora, &tx.config)?;
if !tx.skip_cad {
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(7);
lora.prepare_for_cad(&mdltn).await?;
// Reaches here → prepare_for_cad (SPI setup) completed.
// Hang past here means lora.cad() is waiting on DIO1.
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(19);
let mut saw_activity = false;
for _ in 0..CAD_MAX_RETRIES {
if !lora.cad(&mdltn).await? {
@@ -728,9 +740,15 @@ async fn do_tx(lora: &mut LoRaDriver, tx: &PendingTx) -> Result<TxOutcome, Radio
tx.config.iq_invert,
&mdltn,
)?;
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(8);
lora.prepare_for_tx(&mdltn, &mut tx_pkt, tx.config.tx_power_dbm as i32, &tx.data)
.await?;
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(9);
lora.tx().await?;
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(10);
Ok(TxOutcome::Transmitted)
}
@@ -781,13 +799,19 @@ pub async fn radio_task(
events: &'static RadioEventChannel,
info: &'static Info,
) {
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(1);
let lora = match LoRa::new(parts.driver, false, parts.delay).await {
Ok(l) => {
info!("radio initialized");
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(2);
l
}
Err(e) => {
error!("radio init failed: {}", e);
#[cfg(feature = "debug-checkpoint")]
crate::debug_blink::set(16);
return;
}
};