//! 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::(); let mosi = pins.gpio3.into_function::(); 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")];