pm-kit: peripheral self-test — buttons + joystick (ADC) + speaker (PWM)

Honest answer to 'do the inputs/speaker work?': they had NO Rust code. Add the
drivers and a live self-test: buttons GP15/GP14 (pull-up), joystick GP26/GP27 via
ADC, speaker GP13 via PWM (~2 kHz click on button press). draw_peripheral_test
(pm-ui) shows button states, joystick dot + X/Y values, and beep activity; layout
verified in the simulator (uisim --bin periphsim) before flashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-31 23:56:13 -05:00
parent 0ea442d68d
commit 04350f9d09
4 changed files with 135 additions and 24 deletions

View file

@ -9,6 +9,7 @@ rp235x-hal = { version = "0.3", features = ["binary-info", "critical-section-imp
cortex-m-rt = "0.7"
panic-halt = "1"
embedded-hal = "1"
embedded-hal-0-2 = { package = "embedded-hal", version = "0.2.7" } # ADC OneShot trait (rp235x-hal ADC)
embedded-hal-bus = "0.2"
mipidsi = "0.9"
embedded-graphics = "0.8"

View file

@ -8,7 +8,9 @@
#![no_main]
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;
use embedded_hal::digital::{InputPin, OutputPin};
use embedded_hal::pwm::SetDutyCycle;
use embedded_hal_0_2::adc::OneShot;
use embedded_hal_bus::spi::ExclusiveDevice;
use mipidsi::interface::{Interface, SpiInterface};
use mipidsi::models::ST7796;
@ -133,31 +135,36 @@ fn main() -> ! {
.init(&mut timer)
.unwrap();
// The real metronome screen the host simulator renders (rust/uisim → PNG). Static sample data
// for now (no allocator needed — draw_metronome takes borrowed slices); the live track + a
// moving playhead come once pm-core is linked in.
let kick = [2u8, 1, 1, 1];
let snare = [0u8, 1, 0, 1];
let hat = [2u8, 1, 1, 1, 1, 1, 1, 1];
let ride = [2u8, 0, 1, 0, 1, 0, 1, 0];
let cow = [2u8, 1, 1];
let lanes = [
pm_ui::LaneView { name: "kick", levels: &kick, beats: 4, poly: false, muted: false },
pm_ui::LaneView { name: "snare", levels: &snare, beats: 4, poly: false, muted: false },
pm_ui::LaneView { name: "hatClosed", levels: &hat, beats: 4, poly: false, muted: false },
pm_ui::LaneView { name: "ride", levels: &ride, beats: 4, poly: false, muted: false },
pm_ui::LaneView { name: "cowbell", levels: &cow, beats: 3, poly: true, muted: false },
];
let screen = pm_ui::Screen { name: "Four-on-the-floor", bpm: 128, playing: true, phase: 0.30, lanes: &lanes };
pm_ui::draw_metronome(&mut display, &screen).unwrap();
// ---- Peripheral self-test: buttons (GP15/GP14), joystick (GP26/GP27 ADC), speaker (GP13 PWM).
// Verifies the inputs + speaker actually work on the device; reads live and draws state.
let mut btn_a = pins.gpio15.into_pull_up_input();
let mut btn_b = pins.gpio14.into_pull_up_input();
// Reached the loop → display init + draw completed. Slow 1 Hz blink (vs the solid-ON during
// init above) so "hung in init" / "running" / "reset loop" are distinguishable on the LED.
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_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 tone
spk.enable();
spk.channel_b.output_to(pins.gpio13);
let mut hb = false;
loop {
led.set_high().unwrap();
timer.delay_ms(500);
led.set_low().unwrap();
timer.delay_ms(500);
let a = btn_a.is_low().unwrap();
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();
hb = !hb; // LED heartbeat
let _ = if hb { led.set_high() } else { led.set_low() };
timer.delay_ms(40);
}
}

View file

@ -182,6 +182,68 @@ where
Ok(())
}
/// Live state for the peripheral self-test.
pub struct PeriphState {
pub a: bool,
pub b: bool,
pub x: u16, // joystick X, 0..4095
pub y: u16, // joystick Y, 0..4095
pub beep: bool,
}
/// Peripheral self-test screen: button states, joystick position + values, speaker activity.
pub fn draw_peripheral_test<D>(d: &mut D, s: &PeriphState) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565>,
{
let bb = d.bounding_box();
let w = bb.size.width as i32;
d.clear(BG)?;
Text::new("PERIPHERAL TEST", Point::new(12, 22), MonoTextStyle::new(&FONT_10X20, TXT)).draw(d)?;
// buttons
let lab = MonoTextStyle::new(&FONT_10X20, Rgb565::BLACK);
for (i, (name, on)) in [("A", s.a), ("B", s.b)].iter().enumerate() {
let bx = 16 + i as i32 * 96;
Rectangle::new(Point::new(bx, 48), Size::new(84, 84))
.into_styled(PrimitiveStyle::with_fill(if *on { GREEN } else { PANEL }))
.draw(d)?;
Text::with_alignment(name, Point::new(bx + 42, 98), lab, Alignment::Center).draw(d)?;
}
// speaker / beep
let beep_sty = MonoTextStyle::new(&FONT_9X18_BOLD, if s.beep { AMBER } else { MUTE });
Text::new("SPK", Point::new(216, 78), beep_sty).draw(d)?;
if s.beep {
Text::new("BEEP", Point::new(216, 100), MonoTextStyle::new(&FONT_6X10, AMBER)).draw(d)?;
}
// joystick box + dot
let jb_x = 40;
let jb_y = 170;
let jb = 200;
Rectangle::new(Point::new(jb_x, jb_y), Size::new(jb as u32, jb as u32))
.into_styled(PrimitiveStyleBuilder::new().stroke_color(GRID).stroke_width(2).fill_color(PANEL).build())
.draw(d)?;
Line::new(Point::new(jb_x + jb / 2, jb_y), Point::new(jb_x + jb / 2, jb_y + jb)).into_styled(PrimitiveStyle::with_stroke(GRID, 1)).draw(d)?;
Line::new(Point::new(jb_x, jb_y + jb / 2), Point::new(jb_x + jb, jb_y + jb / 2)).into_styled(PrimitiveStyle::with_stroke(GRID, 1)).draw(d)?;
let dx = jb_x + (s.x as i32 * (jb - 12) / 4095) + 6;
let dy = jb_y + (s.y as i32 * (jb - 12) / 4095) + 6;
Rectangle::new(Point::new(dx - 6, dy - 6), Size::new(12, 12)).into_styled(PrimitiveStyle::with_fill(CYAN)).draw(d)?;
// numeric values
let vs = MonoTextStyle::new(&FONT_6X10, TXT);
let mut nb = [0u8; 12];
Text::new("X", Point::new(jb_x, jb_y + jb + 18), vs).draw(d)?;
Text::new(fmt_u32(s.x as u32, &mut nb), Point::new(jb_x + 16, jb_y + jb + 18), vs).draw(d)?;
let mut nb2 = [0u8; 12];
Text::new("Y", Point::new(jb_x + 90, jb_y + jb + 18), vs).draw(d)?;
Text::new(fmt_u32(s.y as u32, &mut nb2), Point::new(jb_x + 106, jb_y + jb + 18), vs).draw(d)?;
Text::with_alignment("press A/B = click, move joystick", Point::new(w / 2, jb_y + jb + 44), vs, Alignment::Center).draw(d)?;
Ok(())
}
/// Bring-up diagnostic pattern (kept for hardware bring-up / fallback).
pub fn draw_ui<D>(d: &mut D) -> Result<(), D::Error>
where

View file

@ -0,0 +1,41 @@
//! Render the peripheral-test screen (pm-ui) with mock input state → PNG, to check the layout
//! before flashing. `cargo run --bin periphsim`.
use embedded_graphics::{pixelcolor::Rgb565, prelude::*};
use pm_ui::PeriphState;
const W: u32 = 320;
const H: u32 = 480;
struct Fb {
px: Vec<Rgb565>,
}
impl DrawTarget for Fb {
type Color = Rgb565;
type Error = core::convert::Infallible;
fn draw_iter<I: IntoIterator<Item = Pixel<Rgb565>>>(&mut self, pixels: I) -> Result<(), Self::Error> {
for Pixel(p, c) in pixels {
if p.x >= 0 && p.y >= 0 && (p.x as u32) < W && (p.y as u32) < H {
self.px[(p.y as u32 * W + p.x as u32) as usize] = c;
}
}
Ok(())
}
}
impl OriginDimensions for Fb {
fn size(&self) -> Size {
Size::new(W, H)
}
}
fn main() {
let mut fb = Fb { px: vec![Rgb565::BLACK; (W * H) as usize] };
let st = PeriphState { a: true, b: false, x: 2900, y: 1100, beep: true };
pm_ui::draw_peripheral_test(&mut fb, &st).unwrap();
let img = image::RgbImage::from_fn(W, H, |x, y| {
let c = fb.px[(y * W + x) as usize];
image::Rgb([(c.r() << 3) | (c.r() >> 2), (c.g() << 2) | (c.g() >> 4), (c.b() << 3) | (c.b() >> 2)])
});
img.save("periph.png").unwrap();
println!("wrote periph.png");
}