From 647929ff8d462144d0d5e6281eefd72839e675f0 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 23 Apr 2026 23:04:47 -0600 Subject: [PATCH] feat(boards): RAK4631 WisBlock bring-up + build-system cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- justfile | 82 +++++++++++++++------------------- mise.toml | 25 ++++++++--- src/board/esp32.rs | 4 +- src/board/rak_wisblock_4631.rs | 28 +++++++++--- src/debug_blink.rs | 19 ++++---- src/display.rs | 2 +- src/driver/mod.rs | 1 + src/radio.rs | 26 ++++++++++- 8 files changed, 116 insertions(+), 71 deletions(-) diff --git a/justfile b/justfile index ad89a17..d0a9e78 100644 --- a/justfile +++ b/justfile @@ -1,43 +1,29 @@ -set shell := ["bash", "-c"] - -# Board definitions: feature target chip -# Xtensa targets use the self-contained esp toolchain (espup-installed) with -# `-Zbuild-std=core,alloc`. Using `+esp` rather than `+nightly` keeps the -# compiler and rust-src versions aligned — the public nightly drifts ahead of -# the esp fork and breaks core-stdlib compilation otherwise. -# Tool versions are pinned in mise.toml; run `just setup` to install everything. -heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3" -heltec_v3_uart := "heltec_v3_uart xtensa-esp32s3-none-elf esp32s3" -heltec_v4 := "heltec_v4 xtensa-esp32s3-none-elf esp32s3" -elecrow_thinknode_m2 := "elecrow_thinknode_m2 xtensa-esp32s3-none-elf esp32s3" -lilygo_tbeam := "lilygo_tbeam xtensa-esp32-none-elf esp32" -lilygo_tbeam_supreme := "lilygo_tbeam_supreme xtensa-esp32s3-none-elf esp32s3" -rak_wisblock_4631 := "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA" -wio_tracker_l1 := "wio_tracker_l1 thumbv7em-none-eabihf nRF52840_xxAA" -waveshare_rp2040_lora := "waveshare_rp2040_lora thumbv6m-none-eabi rp2040" +# Run every recipe with mise's tool versions active — so `just` invocations +# of cargo, espflash, rust-objcopy, etc. always resolve to the pinned +# versions in mise.toml, even if the user has a different version elsewhere +# on PATH (e.g. a stray `cargo install espflash`). +set shell := ["mise", "exec", "--", "sh", "-c"] builds_dir := "builds" version := `sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml` -# All known boards +# All known boards. ADD A NEW BOARD HERE and in the `_info` case below. +# +# Board metadata ("feature target chip") lives in the `_info` recipe — +# Xtensa targets are built with `+esp` + `-Zbuild-std=core,alloc` (the +# self-contained esp toolchain ships its own rust-src; the public +# nightly drifts ahead and breaks core-stdlib compilation). boards := "heltec_v3 heltec_v3_uart heltec_v4 elecrow_thinknode_m2 lilygo_tbeam lilygo_tbeam_supreme rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1" # Install all required tools and toolchains setup: mise trust --yes mise install - @# ESP Xtensa toolchain (not managed by mise) + @# ESP Xtensa toolchain (not a rustup component — installed out-of-band by espup) @if ! rustup toolchain list | grep -q "^esp"; then \ echo "ESP toolchain not found, installing via espup (this may take a while)..."; \ espup install || { echo "error: espup install failed" >&2; exit 1; }; \ fi - @# nightly rust-src for -Zbuild-std - @if ! rustup component list --toolchain nightly --installed 2>/dev/null | grep -q "^rust-src"; then \ - rustup component add rust-src --toolchain nightly; \ - fi - @# ARM targets - @rustup target list --installed | grep -q "^thumbv6m-none-eabi$" || rustup target add thumbv6m-none-eabi - @rustup target list --installed | grep -q "^thumbv7em-none-eabihf$" || rustup target add thumbv7em-none-eabihf # Build release firmware for all boards, installing toolchains as needed build-all: @@ -76,7 +62,7 @@ build board profile="release": flash board: @just _ensure_tools @just build {{board}} release - @read -r feat target chip <<< "$(just _info {{board}})"; \ + @set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \ case "$target" in \ xtensa-*) \ port=$(just _find_port "303a:1001" "1209:5741" "10c4:ea60" "1a86:7522" "1a86:7523"); \ @@ -94,7 +80,7 @@ flash board: flash-probe board: @just _ensure_tools @just build {{board}} release - @read -r feat target chip <<< "$(just _info {{board}})"; \ + @set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \ probe-rs run --chip $chip "{{builds_dir}}/donglora-{{board}}-v{{version}}.elf" # Show binary size for a release build @@ -121,7 +107,7 @@ _ensure_tools: [private] _cargo board cmd extra_args="": @just _ensure_tools - @read -r feat target chip <<< "$(just _info {{board}})"; \ + @set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \ extra=""; buildstd=""; \ case "$target" in xtensa-*) \ just _require_esp_toolchain; \ @@ -133,7 +119,7 @@ _cargo board cmd extra_args="": # Ensure a board's toolchain is available, auto-installing if needed [private] _can_build board: - @read -r feat target chip <<< "$(just _info {{board}})"; \ + @set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \ case "$target" in \ xtensa-*) just _require_esp_toolchain ;; \ *) rustup target list --installed | grep -q "^$target$" || rustup target add "$target" ;; \ @@ -169,25 +155,25 @@ _find_port +vid_pids: [private] _copy_firmware board profile: @mkdir -p {{builds_dir}} - @read -r feat target chip <<< "$(just _info {{board}})"; \ + @set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \ src="target/$target/{{profile}}/donglora"; \ dst="{{builds_dir}}/donglora-{{board}}-v{{version}}"; \ if [ -f "$src" ]; then \ cp "$src" "$dst.elf"; echo "→ $dst.elf"; \ case "$target" in \ xtensa-*) \ - mise exec -- espflash save-image --chip "$chip" --merge --skip-padding "$src" "$dst.bin"; \ + espflash save-image --chip "$chip" --merge --skip-padding "$src" "$dst.bin"; \ echo "→ $dst.bin"; \ ;; \ thumbv6m-*) \ - mise exec -- rust-objcopy -O ihex "$src" "$dst.hex"; \ - mise exec -- cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family rp2040; \ + rust-objcopy -O ihex "$src" "$dst.hex"; \ + cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family rp2040; \ rm "$dst.hex"; \ echo "→ $dst.uf2"; \ ;; \ thumbv7em-*) \ - mise exec -- rust-objcopy -O ihex "$src" "$dst.hex"; \ - mise exec -- cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family nrf52840; \ + rust-objcopy -O ihex "$src" "$dst.hex"; \ + cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family nrf52840; \ rm "$dst.hex"; \ echo "→ $dst.uf2"; \ ;; \ @@ -210,15 +196,19 @@ _flash_uf2 board: sync; \ echo "Done — board will reboot automatically" +# Single source of truth for per-board metadata: "feature target chip". +# ADD A NEW BOARD HERE and in the `boards` list near the top of this file. [private] _info name: - @if [ "{{name}}" == "heltec_v3" ]; then echo "{{heltec_v3}}"; \ - elif [ "{{name}}" == "heltec_v3_uart" ]; then echo "{{heltec_v3_uart}}"; \ - elif [ "{{name}}" == "heltec_v4" ]; then echo "{{heltec_v4}}"; \ - elif [ "{{name}}" == "elecrow_thinknode_m2" ]; then echo "{{elecrow_thinknode_m2}}"; \ - elif [ "{{name}}" == "lilygo_tbeam" ]; then echo "{{lilygo_tbeam}}"; \ - elif [ "{{name}}" == "lilygo_tbeam_supreme" ]; then echo "{{lilygo_tbeam_supreme}}"; \ - elif [ "{{name}}" == "rak_wisblock_4631" ]; then echo "{{rak_wisblock_4631}}"; \ - elif [ "{{name}}" == "waveshare_rp2040_lora" ]; then echo "{{waveshare_rp2040_lora}}"; \ - elif [ "{{name}}" == "wio_tracker_l1" ]; then echo "{{wio_tracker_l1}}"; \ - else echo "Unknown board: {{name}}" >&2; exit 1; fi + @case "{{name}}" in \ + heltec_v3) echo "heltec_v3 xtensa-esp32s3-none-elf esp32s3" ;; \ + heltec_v3_uart) echo "heltec_v3_uart xtensa-esp32s3-none-elf esp32s3" ;; \ + heltec_v4) echo "heltec_v4 xtensa-esp32s3-none-elf esp32s3" ;; \ + elecrow_thinknode_m2) echo "elecrow_thinknode_m2 xtensa-esp32s3-none-elf esp32s3" ;; \ + lilygo_tbeam) echo "lilygo_tbeam xtensa-esp32-none-elf esp32" ;; \ + lilygo_tbeam_supreme) echo "lilygo_tbeam_supreme xtensa-esp32s3-none-elf esp32s3" ;; \ + rak_wisblock_4631) echo "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA" ;; \ + waveshare_rp2040_lora) echo "waveshare_rp2040_lora thumbv6m-none-eabi rp2040" ;; \ + wio_tracker_l1) echo "wio_tracker_l1 thumbv7em-none-eabihf nRF52840_xxAA" ;; \ + *) echo "Unknown board: {{name}}" >&2; exit 1 ;; \ + esac diff --git a/mise.toml b/mise.toml index 642e1ec..ffa28c1 100644 --- a/mise.toml +++ b/mise.toml @@ -1,9 +1,22 @@ [tools] -rust = "1.94" just = "latest" -"cargo:cargo-binutils" = "latest" # `cargo size`, `cargo objdump`, etc. -"cargo:cargo-hex-to-uf2" = "latest" # ELF → UF2 via Intel HEX for nRF52840 DFU -"cargo:espup" = "0.16" -"cargo:espflash" = "3" # 4.x has broken nix dependency on current stable Rust -"cargo:probe-rs-tools" = "0.31" +# Cargo-installed tooling via mise's `cargo:` backend. +"cargo:cargo-binutils" = "latest" # rust-objcopy, rust-size +"cargo:cargo-hex-to-uf2" = "latest" # ELF → UF2 for nRF52 / RP2040 DFU drives +"cargo:espup" = "0.16" # bootstraps the ESP Xtensa toolchain +"cargo:espflash" = "3" # flash + save-image for ESP targets +"cargo:probe-rs-tools" = "0.31" # debug-probe flash/run for ARM targets + +# Stable Rust + cross-compile targets. Targets land under $RUSTUP_HOME for +# the pinned toolchain, so they're always available under `mise exec` — no +# extra `rustup target add` step at setup time. +# +# Xtensa ESP targets are NOT listed here — they use the self-contained `esp` +# toolchain installed out-of-band by `espup` (see `just setup`). That +# toolchain ships its own rust-src for `-Zbuild-std`. +[tools.rust] +version = "1.94" +profile = "default" +components = "clippy" +targets = "thumbv6m-none-eabi,thumbv7em-none-eabihf" diff --git a/src/board/esp32.rs b/src/board/esp32.rs index fa36fb0..8c2c221 100644 --- a/src/board/esp32.rs +++ b/src/board/esp32.rs @@ -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, diff --git a/src/board/rak_wisblock_4631.rs b/src/board/rak_wisblock_4631.rs index 1edb381..ca5fbe0 100644 --- a/src/board/rak_wisblock_4631.rs +++ b/src/board/rak_wisblock_4631.rs @@ -20,7 +20,7 @@ type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>; pub type RadioDriver = Sx126x; pub type UsbDriver = mcu::UsbNrfDriver; pub type DisplayI2c = mcu::I2cBus; -pub type LedDriver = (); +pub type LedDriver = crate::driver::simple_led::SimpleLed>; pub type DisplayDriver = ssd1306::Ssd1306Async< ssd1306::prelude::I2CInterface, @@ -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 { + fn into_parts(self) -> BoardParts { 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(), } } diff --git a/src/debug_blink.rs b/src/debug_blink.rs index 9be3e65..326574a 100644 --- a/src/debug_blink.rs +++ b/src/debug_blink.rs @@ -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 } } diff --git a/src/display.rs b/src/display.rs index 79d7e89..708ddeb 100644 --- a/src/display.rs +++ b/src/display.rs @@ -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}; diff --git a/src/driver/mod.rs b/src/driver/mod.rs index fffb049..cf7eb0d 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -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" ))] diff --git a/src/radio.rs b/src/radio.rs index 1479222..d72f423 100644 --- a/src/radio.rs +++ b/src/radio.rs @@ -369,6 +369,8 @@ async fn next_packet( } async fn perform_tx(lora: &mut LoRaDriver, pending: &mut Option) -> 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) -> 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 Result { 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; } };