pm-kit: live metronome — real tracks, clock, audio clicks, controls, view toggle
The firmware is now an actual metronome (not a static screen): - embedded-alloc heap → parses tracks with track-format on-device. - 4 built-in grooves; clock-driven from the Timer; audio clicks on the master lane's hits via the GP13 PWM (accent louder/longer), short edge-triggered pulses. - Controls: A = play/stop, B = grid/notation view; joystick (rotated 90° CCW) up/down = tempo +/-, left/right = prev/next groove. - Renders draw_metronome or draw_notation; a cheap draw_progress strip animates the bar position every frame (full redraw only on change → no flicker). - Robust: all input reads use unwrap_or (no panics in the loop) — addresses the self-test crash (likely an ADC unwrap on WouldBlock) and the continuous-buzzer. Compile + simulator verified (grid renders all 4 grooves incl. triplets/polymeter). NEEDS ON-DEVICE CHECK: audio timing, joystick directions, and that the crash is gone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ec29fb7284
commit
0c788b1153
3 changed files with 164 additions and 17 deletions
|
|
@ -14,6 +14,8 @@ embedded-hal-bus = "0.2"
|
||||||
mipidsi = "0.9"
|
mipidsi = "0.9"
|
||||||
embedded-graphics = "0.8"
|
embedded-graphics = "0.8"
|
||||||
pm-ui = { path = "../pm-ui" }
|
pm-ui = { path = "../pm-ui" }
|
||||||
|
track-format = { path = "../track-format" }
|
||||||
|
embedded-alloc = "0.6"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,13 @@
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use embedded_alloc::LlffHeap as Heap;
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static HEAP: Heap = Heap::empty();
|
||||||
|
|
||||||
use embedded_hal::delay::DelayNs;
|
use embedded_hal::delay::DelayNs;
|
||||||
use embedded_hal::digital::{InputPin, OutputPin};
|
use embedded_hal::digital::{InputPin, OutputPin};
|
||||||
use embedded_hal::pwm::SetDutyCycle;
|
use embedded_hal::pwm::SetDutyCycle;
|
||||||
|
|
@ -49,6 +56,14 @@ 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.
|
||||||
|
{
|
||||||
|
use core::mem::MaybeUninit;
|
||||||
|
const HEAP_SIZE: usize = 96 * 1024;
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
let clocks = hal::clocks::init_clocks_and_plls(
|
let clocks = hal::clocks::init_clocks_and_plls(
|
||||||
|
|
@ -135,36 +150,148 @@ fn main() -> ! {
|
||||||
.init(&mut timer)
|
.init(&mut timer)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// ---- Peripheral self-test: buttons (GP15/GP14), joystick (GP26/GP27 ADC), speaker (GP13 PWM).
|
// ---- inputs + speaker ----
|
||||||
// Verifies the inputs + speaker actually work on the device; reads live and draws state.
|
let mut btn_a = pins.gpio15.into_pull_up_input(); // A = play/stop
|
||||||
let mut btn_a = pins.gpio15.into_pull_up_input();
|
let mut btn_b = pins.gpio14.into_pull_up_input(); // B = grid/notation view
|
||||||
let mut btn_b = pins.gpio14.into_pull_up_input();
|
|
||||||
|
|
||||||
let mut adc = hal::adc::Adc::new(pac.ADC, &mut pac.RESETS);
|
let mut adc = hal::adc::Adc::new(pac.ADC, &mut pac.RESETS);
|
||||||
let mut joy_x = hal::adc::AdcPin::new(pins.gpio26).unwrap();
|
let mut joy_x = hal::adc::AdcPin::new(pins.gpio26).unwrap();
|
||||||
let mut joy_y = hal::adc::AdcPin::new(pins.gpio27).unwrap();
|
let mut joy_y = hal::adc::AdcPin::new(pins.gpio27).unwrap();
|
||||||
|
|
||||||
let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
|
let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
|
||||||
let mut spk = pwm_slices.pwm6; // GP13 = PWM slice 6, channel B
|
let mut spk = pwm_slices.pwm6; // GP13 = PWM slice 6, channel B
|
||||||
spk.set_div_int(125);
|
spk.set_div_int(125);
|
||||||
spk.set_top(600); // ~2 kHz click tone
|
spk.set_top(600); // ~2 kHz click
|
||||||
spk.enable();
|
spk.enable();
|
||||||
spk.channel_b.output_to(pins.gpio13);
|
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]);
|
||||||
|
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 hb = false;
|
let mut hb = false;
|
||||||
|
let mut hb_us = 0u64;
|
||||||
|
led.set_low().unwrap();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let a = btn_a.is_low().unwrap();
|
let now = timer.get_counter().ticks();
|
||||||
let b = btn_b.is_low().unwrap();
|
|
||||||
let x: u16 = adc.read(&mut joy_x).unwrap();
|
|
||||||
let y: u16 = adc.read(&mut joy_y).unwrap();
|
|
||||||
let beep = a || b;
|
|
||||||
let _ = spk.channel_b.set_duty_cycle(if beep { 300 } else { 0 });
|
|
||||||
|
|
||||||
pm_ui::draw_peripheral_test(&mut display, &pm_ui::PeriphState { a, b, x, y, beep }).ok();
|
// ---- 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;
|
||||||
|
|
||||||
hb = !hb; // LED heartbeat
|
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: full screen only on change; the bar-position strip every frame ----
|
||||||
|
if full_redraw {
|
||||||
|
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 };
|
||||||
|
if notation {
|
||||||
|
pm_ui::draw_notation(&mut display, &screen).ok();
|
||||||
|
} else {
|
||||||
|
pm_ui::draw_metronome(&mut display, &screen).ok();
|
||||||
|
}
|
||||||
|
full_redraw = false;
|
||||||
|
}
|
||||||
|
let _ = pm_ui::draw_progress(&mut display, phase, playing, 62);
|
||||||
|
|
||||||
|
// heartbeat LED (~1 Hz)
|
||||||
|
if now.wrapping_sub(hb_us) > 500_000 {
|
||||||
|
hb = !hb;
|
||||||
let _ = if hb { led.set_high() } else { led.set_low() };
|
let _ = if hb { led.set_high() } else { led.set_low() };
|
||||||
timer.delay_ms(40);
|
hb_us = now;
|
||||||
|
}
|
||||||
|
timer.delay_ms(8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,24 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Thin bar-position strip the firmware can update cheaply every frame (no full redraw → no
|
||||||
|
/// flicker). `phase` 0..1 through the bar. Draw it at `top` (draw_metronome leaves y≈60 free).
|
||||||
|
pub fn draw_progress<D>(d: &mut D, phase: f32, playing: bool, top: i32) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Rgb565>,
|
||||||
|
{
|
||||||
|
let bb = d.bounding_box();
|
||||||
|
let w = bb.size.width as i32;
|
||||||
|
const M: i32 = 12;
|
||||||
|
let bw = w - 2 * M;
|
||||||
|
Rectangle::new(Point::new(M, top), Size::new(bw as u32, 5)).into_styled(PrimitiveStyle::with_fill(PANEL)).draw(d)?;
|
||||||
|
if playing {
|
||||||
|
let fw = ((bw as f32) * phase.clamp(0.0, 1.0)) as u32;
|
||||||
|
Rectangle::new(Point::new(M, top), Size::new(fw, 5)).into_styled(PrimitiveStyle::with_fill(GREEN)).draw(d)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Live state for the peripheral self-test.
|
/// Live state for the peripheral self-test.
|
||||||
pub struct PeriphState {
|
pub struct PeriphState {
|
||||||
pub a: bool,
|
pub a: bool,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue