pm-kit: defmt+probe-rs diagnostics + flip-link toolchain
Adopt proper embedded tooling for the blank-screen debug (user has a Pi Debug Probe):
- flip-link linker (baked into pm-rust:2): stack overflow faults cleanly instead of
silently corrupting .bss/.data (the SPI buffer -> black screen class of bug).
- defmt + defmt-rtt + panic-probe: firmware logs boot/heap-free/display/parse/loop
heartbeat over RTT; panics print message+location. .cargo runner = probe-rs run.
- Restore the full live metronome (from 08b0940) as the instrumented target.
- deploy + serve pm-kit.elf (probe-rs decodes defmt strings from the ELF).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7faca6d0d7
commit
8f4264f4d2
8 changed files with 175 additions and 15 deletions
|
|
@ -53,6 +53,12 @@ if [[ -f "$SRC_DIR/rust/pm-kit/pm-kit.uf2" ]]; then
|
||||||
cp "$SRC_DIR/rust/pm-kit/pm-kit.uf2" "$DEST_DIR/pm-kit.uf2"
|
cp "$SRC_DIR/rust/pm-kit/pm-kit.uf2" "$DEST_DIR/pm-kit.uf2"
|
||||||
echo " pm-kit.uf2 ($(stat -c '%s' "$DEST_DIR/pm-kit.uf2") bytes) # Rust RP2350 firmware (alpha live metronome)"
|
echo " pm-kit.uf2 ($(stat -c '%s' "$DEST_DIR/pm-kit.uf2") bytes) # Rust RP2350 firmware (alpha live metronome)"
|
||||||
fi
|
fi
|
||||||
|
# ELF with defmt info — `probe-rs run --chip RP235x pm-kit.elf` flashes over the Debug Probe and
|
||||||
|
# streams logs/panics. Needed locally (not the uf2) because defmt decodes log strings from the ELF.
|
||||||
|
if [[ -f "$SRC_DIR/rust/pm-kit/pm-kit.elf" ]]; then
|
||||||
|
cp "$SRC_DIR/rust/pm-kit/pm-kit.elf" "$DEST_DIR/pm-kit.elf"
|
||||||
|
echo " pm-kit.elf ($(stat -c '%s' "$DEST_DIR/pm-kit.elf") bytes) # probe-rs flash + defmt RTT logging"
|
||||||
|
fi
|
||||||
cp "$DIST_DIR/pm_k1_circuitpy.zip" "$DEST_DIR/pm_k1_circuitpy.zip"; echo " pm_k1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_k1_circuitpy.zip") bytes)" # PM_K-1 CircuitPython bundle
|
cp "$DIST_DIR/pm_k1_circuitpy.zip" "$DEST_DIR/pm_k1_circuitpy.zip"; echo " pm_k1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_k1_circuitpy.zip") bytes)" # PM_K-1 CircuitPython bundle
|
||||||
cp "$DIST_DIR/pico-cp-app.py" "$DEST_DIR/pico-cp-app.py"; echo " pico-cp-app.py ($(stat -c '%s' "$DEST_DIR/pico-cp-app.py") bytes)" # served for version reading + reference
|
cp "$DIST_DIR/pico-cp-app.py" "$DEST_DIR/pico-cp-app.py"; echo " pico-cp-app.py ($(stat -c '%s' "$DEST_DIR/pico-cp-app.py") bytes)" # served for version reading + reference
|
||||||
cp "$DIST_DIR/pico-cp-app.mpy" "$DEST_DIR/pico-cp-app.mpy"; echo " pico-cp-app.mpy ($(stat -c '%s' "$DEST_DIR/pico-cp-app.mpy") bytes)" # precompiled firmware the editor pushes (base64)
|
cp "$DIST_DIR/pico-cp-app.mpy" "$DEST_DIR/pico-cp-app.mpy"; echo " pico-cp-app.mpy ($(stat -c '%s' "$DEST_DIR/pico-cp-app.mpy") bytes)" # precompiled firmware the editor pushes (base64)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
FROM docker.io/library/rust:1-slim
|
FROM docker.io/library/rust:1-slim
|
||||||
|
|
||||||
# Cortex-M33 target for the eventual RP2350 firmware. Harmless for the host tests.
|
# Cortex-M33 target for the eventual RP2350 firmware. Harmless for the host tests.
|
||||||
RUN rustup target add thumbv8m.main-none-eabihf
|
RUN rustup target add thumbv8m.main-none-eabihf \
|
||||||
|
&& rustup component add llvm-tools-preview \
|
||||||
|
&& cargo install flip-link # stack-overflow-safe linker (overflow faults instead of corrupting statics)
|
||||||
|
|
||||||
WORKDIR /work/rust
|
WORKDIR /work/rust
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,16 @@
|
||||||
target = "thumbv8m.main-none-eabihf"
|
target = "thumbv8m.main-none-eabihf"
|
||||||
|
|
||||||
[target.thumbv8m.main-none-eabihf]
|
[target.thumbv8m.main-none-eabihf]
|
||||||
|
# flip-link: places the stack BELOW the statics so a stack overflow hits a guard region
|
||||||
|
# and faults cleanly instead of silently corrupting .bss/.data (e.g. the SPI buffer → black screen).
|
||||||
|
linker = "flip-link"
|
||||||
|
# `cargo run` flashes over the Raspberry Pi Debug Probe and streams defmt logs/panics via RTT.
|
||||||
|
runner = "probe-rs run --chip RP235x"
|
||||||
rustflags = [
|
rustflags = [
|
||||||
"-C", "link-arg=--nmagic",
|
"-C", "link-arg=--nmagic",
|
||||||
"-C", "link-arg=-Tlink.x",
|
"-C", "link-arg=-Tlink.x",
|
||||||
|
"-C", "link-arg=-Tdefmt.x", # defmt's linker section for log-string interning
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
DEFMT_LOG = "debug"
|
||||||
|
|
|
||||||
1
rust/pm-kit/.gitignore
vendored
1
rust/pm-kit/.gitignore
vendored
|
|
@ -2,3 +2,4 @@ target/
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
*.uf2
|
*.uf2
|
||||||
*.bin
|
*.bin
|
||||||
|
*.elf
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ edition = "2021"
|
||||||
description = "PM_K-1 firmware (RP2350 / Pico 2). Stage 3 bring-up: boot blink → display → drivers + pm-core."
|
description = "PM_K-1 firmware (RP2350 / Pico 2). Stage 3 bring-up: boot blink → display → drivers + pm-core."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rp235x-hal = { version = "0.3", features = ["binary-info", "critical-section-impl", "rt"] }
|
rp235x-hal = { version = "0.3", features = ["binary-info", "critical-section-impl", "rt", "defmt"] }
|
||||||
cortex-m-rt = "0.7"
|
cortex-m-rt = "0.7"
|
||||||
panic-halt = "1"
|
defmt = "0.3"
|
||||||
|
defmt-rtt = "0.4"
|
||||||
|
panic-probe = { version = "0.3", features = ["print-defmt"] }
|
||||||
embedded-hal = "1"
|
embedded-hal = "1"
|
||||||
embedded-hal-0-2 = { package = "embedded-hal", version = "0.2.7" } # ADC OneShot trait (rp235x-hal ADC)
|
embedded-hal-0-2 = { package = "embedded-hal", version = "0.2.7" } # ADC OneShot trait (rp235x-hal ADC)
|
||||||
embedded-hal-bus = "0.2"
|
embedded-hal-bus = "0.2"
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ set -euo pipefail
|
||||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # rust/pm-kit
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # rust/pm-kit
|
||||||
REPO="$(cd "$DIR/../.." && pwd)" # repo root
|
REPO="$(cd "$DIR/../.." && pwd)" # repo root
|
||||||
RUNTIME="${RUNTIME:-podman}"
|
RUNTIME="${RUNTIME:-podman}"
|
||||||
IMG="pm-rust:1"
|
IMG="pm-rust:2"
|
||||||
|
|
||||||
"$RUNTIME" run --rm -v "$REPO":/work:Z -w /work/rust/pm-kit "$IMG" bash -c '
|
"$RUNTIME" run --rm -v "$REPO":/work:Z -w /work/rust/pm-kit "$IMG" bash -c '
|
||||||
set -e
|
set -e
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ use mipidsi::interface::{Interface, SpiInterface};
|
||||||
use mipidsi::models::ST7796;
|
use mipidsi::models::ST7796;
|
||||||
use mipidsi::options::{ColorInversion, ColorOrder, Orientation};
|
use mipidsi::options::{ColorInversion, ColorOrder, Orientation};
|
||||||
use mipidsi::Builder;
|
use mipidsi::Builder;
|
||||||
use panic_halt as _;
|
use defmt::info;
|
||||||
|
use defmt_rtt as _; // global defmt logger over RTT (read by `probe-rs run`)
|
||||||
|
use panic_probe as _; // panic handler that prints the message + location over defmt, then halts
|
||||||
use rp235x_hal as hal;
|
use rp235x_hal as hal;
|
||||||
use hal::fugit::RateExtU32;
|
use hal::fugit::RateExtU32;
|
||||||
use hal::Clock;
|
use hal::Clock;
|
||||||
|
|
@ -57,12 +59,13 @@ impl OutputPin for NoCs {
|
||||||
#[hal::entry]
|
#[hal::entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
// heap for track parsing (track-format uses alloc). RP2350 has 520 KB SRAM.
|
// heap for track parsing (track-format uses alloc). RP2350 has 520 KB SRAM.
|
||||||
|
const HEAP_SIZE: usize = 96 * 1024;
|
||||||
{
|
{
|
||||||
use core::mem::MaybeUninit;
|
use core::mem::MaybeUninit;
|
||||||
const HEAP_SIZE: usize = 16 * 1024;
|
|
||||||
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||||
unsafe { HEAP.init(core::ptr::addr_of_mut!(HEAP_MEM) as usize, HEAP_SIZE) }
|
unsafe { HEAP.init(core::ptr::addr_of_mut!(HEAP_MEM) as usize, HEAP_SIZE) }
|
||||||
}
|
}
|
||||||
|
info!("== pm-kit boot == heap {}KB, {} free", HEAP_SIZE / 1024, HEAP.free());
|
||||||
|
|
||||||
let mut pac = hal::pac::Peripherals::take().unwrap();
|
let mut pac = hal::pac::Peripherals::take().unwrap();
|
||||||
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
|
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
|
||||||
|
|
@ -149,24 +152,161 @@ fn main() -> ! {
|
||||||
.orientation(Orientation::new().flip_horizontal())
|
.orientation(Orientation::new().flip_horizontal())
|
||||||
.init(&mut timer)
|
.init(&mut timer)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
info!("display init ok, {} free", HEAP.free());
|
||||||
|
|
||||||
// ---- MINIMAL ISOLATION ----
|
// ---- inputs + speaker ----
|
||||||
// Heap is initialised above. Exercise the allocator once (parse), then just draw the
|
let mut btn_a = pins.gpio15.into_pull_up_input(); // A = play/stop
|
||||||
// confirmed-working pattern in a loop. No inputs/audio/clock. If the screen shows blue +
|
let mut btn_b = pins.gpio14.into_pull_up_input(); // B = grid/notation view
|
||||||
// corners → heap + parse + display are all fine and the bug is in the metronome loop logic.
|
let mut adc = hal::adc::Adc::new(pac.ADC, &mut pac.RESETS);
|
||||||
// If still black → the heap/parse path breaks the display.
|
let mut joy_x = hal::adc::AdcPin::new(pins.gpio26).unwrap();
|
||||||
// (parse removed for this test — heap init only)
|
let mut joy_y = hal::adc::AdcPin::new(pins.gpio27).unwrap();
|
||||||
|
let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
|
||||||
|
let mut spk = pwm_slices.pwm6; // GP13 = PWM slice 6, channel B
|
||||||
|
spk.set_div_int(125);
|
||||||
|
spk.set_top(600); // ~2 kHz click
|
||||||
|
spk.enable();
|
||||||
|
spk.channel_b.output_to(pins.gpio13);
|
||||||
|
|
||||||
|
// ---- built-in grooves ----
|
||||||
|
const GROOVES: [&str; 4] = [
|
||||||
|
"t120;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2=xxxxxxxx",
|
||||||
|
"t92;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x",
|
||||||
|
"t140;kick:4=X..x;snare:4=.X.X;hatClosed:4/4=xxxxxxxxxxxxxxxx",
|
||||||
|
"t100;kick:4=Xxxx;claves:5=Xxxxx~",
|
||||||
|
];
|
||||||
|
const NAMES: [&str; 4] = ["Four on the floor", "Half-time shuffle", "Driving 16ths", "5 over 4"];
|
||||||
|
|
||||||
|
let mut idx = 0usize;
|
||||||
|
let mut track = track_format::parse(GROOVES[idx]);
|
||||||
|
info!("parsed groove 0: bpm={} lanes={}, {} free", track.bpm, track.lanes.len(), HEAP.free());
|
||||||
|
let mut tempo: i64 = track.bpm;
|
||||||
|
let mut playing = true;
|
||||||
|
let mut notation = false;
|
||||||
|
|
||||||
|
let mut bar_start = timer.get_counter().ticks();
|
||||||
|
let mut last_step = usize::MAX;
|
||||||
|
let mut click_off_us: u64 = 0;
|
||||||
|
let (mut pa, mut pb) = (false, false);
|
||||||
|
let mut joy_zone = 0i8;
|
||||||
|
let mut full_redraw = true;
|
||||||
|
let mut last_draw_us = 0u64;
|
||||||
let mut hb = false;
|
let mut hb = false;
|
||||||
let mut hb_us = 0u64;
|
let mut hb_us = 0u64;
|
||||||
|
let mut last_draw_ok = false;
|
||||||
led.set_low().unwrap();
|
led.set_low().unwrap();
|
||||||
|
info!("entering main loop");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
pm_ui::draw_ui(&mut display).ok();
|
|
||||||
let now = timer.get_counter().ticks();
|
let now = timer.get_counter().ticks();
|
||||||
|
|
||||||
|
// ---- inputs ----
|
||||||
|
let a = btn_a.is_low().unwrap_or(false);
|
||||||
|
let b = btn_b.is_low().unwrap_or(false);
|
||||||
|
if a && !pa {
|
||||||
|
playing = !playing;
|
||||||
|
if playing {
|
||||||
|
bar_start = now;
|
||||||
|
last_step = usize::MAX;
|
||||||
|
}
|
||||||
|
full_redraw = true;
|
||||||
|
}
|
||||||
|
if b && !pb {
|
||||||
|
notation = !notation;
|
||||||
|
full_redraw = true;
|
||||||
|
}
|
||||||
|
pa = a;
|
||||||
|
pb = b;
|
||||||
|
|
||||||
|
let jx = adc.read(&mut joy_x).unwrap_or(2048);
|
||||||
|
let jy = adc.read(&mut joy_y).unwrap_or(2048);
|
||||||
|
// rotate the joystick reading 90° CCW for control
|
||||||
|
let rx = jy as i32;
|
||||||
|
let ry = 4095 - jx as i32;
|
||||||
|
let zone: i8 = if ry > 3200 { 1 } else if ry < 900 { 2 } else if rx > 3200 { 3 } else if rx < 900 { 4 } else { 0 };
|
||||||
|
if zone != 0 && joy_zone == 0 {
|
||||||
|
match zone {
|
||||||
|
1 => tempo = (tempo + 4).min(300), // up → tempo+
|
||||||
|
2 => tempo = (tempo - 4).max(30), // down → tempo-
|
||||||
|
3 => {
|
||||||
|
idx = (idx + 1) % GROOVES.len(); // right → next groove
|
||||||
|
track = track_format::parse(GROOVES[idx]);
|
||||||
|
tempo = track.bpm;
|
||||||
|
bar_start = now;
|
||||||
|
last_step = usize::MAX;
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
idx = (idx + GROOVES.len() - 1) % GROOVES.len(); // left → prev
|
||||||
|
track = track_format::parse(GROOVES[idx]);
|
||||||
|
tempo = track.bpm;
|
||||||
|
bar_start = now;
|
||||||
|
last_step = usize::MAX;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
full_redraw = true;
|
||||||
|
}
|
||||||
|
joy_zone = zone;
|
||||||
|
|
||||||
|
// ---- clock ----
|
||||||
|
let master = &track.lanes[0];
|
||||||
|
let msteps = master.levels.len().max(1) as u64;
|
||||||
|
let beats = master.groups.iter().map(|&g| g as u64).sum::<u64>().max(1);
|
||||||
|
let bar_us = 60_000_000u64 * beats / tempo.max(1) as u64;
|
||||||
|
let step_us = (bar_us / msteps).max(1);
|
||||||
|
let elapsed = now.wrapping_sub(bar_start) % bar_us;
|
||||||
|
let cur_step = (elapsed / step_us) as usize;
|
||||||
|
let phase = elapsed as f32 / bar_us as f32;
|
||||||
|
|
||||||
|
// ---- audio: click on a new step that has a hit ----
|
||||||
|
if playing && cur_step != last_step {
|
||||||
|
let lvl = master.levels[cur_step.min(msteps as usize - 1)];
|
||||||
|
if lvl > 0 {
|
||||||
|
let _ = spk.channel_b.set_duty_cycle(if lvl == 2 { 380 } else { 240 });
|
||||||
|
click_off_us = now + if lvl == 2 { 28_000 } else { 14_000 };
|
||||||
|
}
|
||||||
|
last_step = cur_step;
|
||||||
|
}
|
||||||
|
if now >= click_off_us {
|
||||||
|
let _ = spk.channel_b.set_duty_cycle(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- draw: on change, and periodically (so a draw lost right after init reappears, and
|
||||||
|
// the playhead advances). ~7 fps; partial/playhead-only redraw is the next optimization. ----
|
||||||
|
if full_redraw || now.wrapping_sub(last_draw_us) > 140_000 {
|
||||||
|
let lanes: Vec<pm_ui::LaneView> = track
|
||||||
|
.lanes
|
||||||
|
.iter()
|
||||||
|
.map(|l| pm_ui::LaneView {
|
||||||
|
name: &l.sound,
|
||||||
|
levels: &l.levels,
|
||||||
|
beats: l.groups.iter().map(|&g| g as u32).sum::<u32>().min(255) as u8,
|
||||||
|
poly: l.poly,
|
||||||
|
muted: l.mute,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let screen = pm_ui::Screen { name: NAMES[idx], bpm: tempo, playing, phase, lanes: &lanes };
|
||||||
|
last_draw_ok = if notation {
|
||||||
|
pm_ui::draw_notation(&mut display, &screen).is_ok()
|
||||||
|
} else {
|
||||||
|
pm_ui::draw_metronome(&mut display, &screen).is_ok()
|
||||||
|
};
|
||||||
|
full_redraw = false;
|
||||||
|
last_draw_us = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// heartbeat LED + log (~2 Hz) — confirms the loop is alive and the last draw returned Ok
|
||||||
if now.wrapping_sub(hb_us) > 500_000 {
|
if now.wrapping_sub(hb_us) > 500_000 {
|
||||||
hb = !hb;
|
hb = !hb;
|
||||||
let _ = if hb { led.set_high() } else { led.set_low() };
|
let _ = if hb { led.set_high() } else { led.set_low() };
|
||||||
|
info!("alive: idx={} step={} playing={} draw_ok={} free={}", idx, cur_step, playing, last_draw_ok, HEAP.free());
|
||||||
hb_us = now;
|
hb_us = now;
|
||||||
}
|
}
|
||||||
timer.delay_ms(50);
|
timer.delay_ms(8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// picotool metadata (visible via `picotool info`).
|
||||||
|
#[link_section = ".bi_entries"]
|
||||||
|
#[used]
|
||||||
|
pub static PICOTOOL_ENTRIES: [hal::binary_info::EntryAddr; 1] =
|
||||||
|
[hal::binary_info::rp_program_name!(c"pm-kit display")];
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
# Override the runtime with RUNTIME=docker ./run.sh ...
|
# Override the runtime with RUNTIME=docker ./run.sh ...
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
IMG="pm-rust:1"
|
IMG="pm-rust:2"
|
||||||
RUST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # rust/
|
RUST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # rust/
|
||||||
REPO_DIR="$(cd "$RUST_DIR/.." && pwd)" # repo root
|
REPO_DIR="$(cd "$RUST_DIR/.." && pwd)" # repo root
|
||||||
RUNTIME="${RUNTIME:-podman}"
|
RUNTIME="${RUNTIME:-podman}"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue