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:
parent
0ea442d68d
commit
04350f9d09
4 changed files with 135 additions and 24 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
41
rust/uisim/src/bin/periphsim.rs
Normal file
41
rust/uisim/src/bin/periphsim.rs
Normal 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");
|
||||
}
|
||||
Loading…
Reference in a new issue