metronome/rust/pm-kit/src/main.rs
Me Here 0ea442d68d pm-kit: draw the real metronome screen on the panel (static sample data)
Switch the firmware from the bring-up diagnostic to pm_ui::draw_metronome with
static borrowed lane data (no allocator yet). Shows the actual metronome UI on the
device; live track + moving playhead come when pm-core is linked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:48:50 -05:00

168 lines
7.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! PM_K-1 firmware — Stage 3 bring-up.
//! Milestone 1 (done): blink GP25 → proved boot/flash.
//! Milestone 2 (this): init the ST7796 320×480 display over SPI0 and draw to it.
//! Pins (from the CircuitPython firmware): SCK=GP2, MOSI=GP3, CS=GP5, DC=GP6, RST=GP7;
//! BGR panel, colours inverted. LED on GP25 keeps blinking as a heartbeat.
#![no_std]
#![no_main]
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;
use embedded_hal_bus::spi::ExclusiveDevice;
use mipidsi::interface::{Interface, SpiInterface};
use mipidsi::models::ST7796;
use mipidsi::options::{ColorInversion, ColorOrder, Orientation};
use mipidsi::Builder;
use panic_halt as _;
use rp235x_hal as hal;
use hal::fugit::RateExtU32;
use hal::Clock;
/// Image definition block — the RP2350 bootrom looks for this to boot the image.
#[link_section = ".start_block"]
#[used]
pub static IMAGE_DEF: hal::block::ImageDef = hal::block::ImageDef::secure_exe();
const XTAL_FREQ_HZ: u32 = 12_000_000;
const WIDTH: u16 = 320;
const HEIGHT: u16 = 480;
/// No-op chip-select for the SPI wrapper. The real CS (GP5) is held low for the whole session,
/// because mipidsi's SpiInterface sends a command and its parameters as SEPARATE SpiDevice
/// transactions — a normal SpiDevice would toggle CS between them, and the ST7796 needs CS
/// continuous across command+parameters (so MADCTL/COLMOD/B6 args load). DC selects cmd vs data.
struct NoCs;
impl embedded_hal::digital::ErrorType for NoCs {
type Error = core::convert::Infallible;
}
impl OutputPin for NoCs {
fn set_low(&mut self) -> Result<(), core::convert::Infallible> {
Ok(())
}
fn set_high(&mut self) -> Result<(), core::convert::Infallible> {
Ok(())
}
}
#[hal::entry]
fn main() -> ! {
let mut pac = hal::pac::Peripherals::take().unwrap();
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
let clocks = hal::clocks::init_clocks_and_plls(
XTAL_FREQ_HZ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let mut timer = hal::Timer::new_timer0(pac.TIMER0, &mut pac.RESETS, &clocks);
let sio = hal::Sio::new(pac.SIO);
let pins = hal::gpio::Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
let mut led = pins.gpio25.into_push_pull_output();
led.set_high().unwrap(); // solid ON during init: if it stays solid → hung in init; slow blink → reached the loop
// --- ST7796 over SPI0 ---
let sclk = pins.gpio2.into_function::<hal::gpio::FunctionSpi>();
let mosi = pins.gpio3.into_function::<hal::gpio::FunctionSpi>();
let dc = pins.gpio6.into_push_pull_output();
let mut rst = pins.gpio7.into_push_pull_output();
let mut cs = pins.gpio5.into_push_pull_output();
cs.set_low().unwrap(); // hold CS low for the whole session (see NoCs) — the fix for the 1/4 panel
let spi = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (mosi, sclk));
let spi = spi.init(
&mut pac.RESETS,
clocks.peripheral_clock.freq(),
16.MHz(),
embedded_hal::spi::MODE_0,
);
let spi_device = ExclusiveDevice::new_no_delay(spi, NoCs).unwrap();
let mut buffer = [0u8; 512];
let mut di = SpiInterface::new(spi_device, dc, &mut buffer);
// Hardware reset, then the FULL known-good CircuitPython st7796_init as the PRIMARY bring-up
// (right after reset, the only time the panel accepts the extension setup cleanly). Confirmed
// necessary: mipidsi's address window + MADCTL already match CircuitPython (host initdump), so
// the only remaining difference was this extension init not running as the primary init.
rst.set_high().unwrap();
timer.delay_ms(5);
rst.set_low().unwrap();
timer.delay_ms(20);
rst.set_high().unwrap();
timer.delay_ms(150);
di.send_command(0x01, &[]).unwrap(); // SWRESET
timer.delay_ms(120);
di.send_command(0x11, &[]).unwrap(); // SLPOUT
timer.delay_ms(120);
di.send_command(0xF0, &[0xC3]).unwrap(); // unlock extension command set
di.send_command(0xF0, &[0x96]).unwrap();
di.send_command(0x36, &[0x48]).unwrap(); // MADCTL
di.send_command(0x3A, &[0x55]).unwrap(); // COLMOD 16bpp
di.send_command(0xB4, &[0x01]).unwrap();
di.send_command(0xB6, &[0x80, 0x02, 0x3B]).unwrap(); // display function control: 480 lines
di.send_command(0xE8, &[0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33]).unwrap();
di.send_command(0xC1, &[0x06]).unwrap();
di.send_command(0xC2, &[0xA7]).unwrap();
di.send_command(0xC5, &[0x18]).unwrap();
timer.delay_ms(120);
di.send_command(0xE0, &[0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B]).unwrap();
di.send_command(0xE1, &[0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B]).unwrap();
di.send_command(0xF0, &[0x3C]).unwrap(); // lock
di.send_command(0xF0, &[0x69]).unwrap();
timer.delay_ms(120);
di.send_command(0x21, &[]).unwrap(); // INVON (INVERT_COLORS = true)
di.send_command(0x29, &[]).unwrap(); // DISPON
timer.delay_ms(50);
// Build mipidsi for DRAWING only — NO reset_pin (already reset), so it just re-asserts the
// basics (SLPOUT/MADCTL=0x48/INVON/COLMOD/NORON/DISPON) without touching the extension setup.
let mut display = Builder::new(ST7796, di)
.display_size(WIDTH, HEIGHT)
.color_order(ColorOrder::Bgr)
.invert_colors(ColorInversion::Inverted)
.orientation(Orientation::new().flip_horizontal())
.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();
// 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.
loop {
led.set_high().unwrap();
timer.delay_ms(500);
led.set_low().unwrap();
timer.delay_ms(500);
}
}
/// 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")];