metronome/rust/pm-kit/src/main.rs
Me Here 7faca6d0d7 pm-kit: isolation step 2 — heap init only (16KB), no parse, draw_ui
If blue → the 96KB heap memory was colliding (stack/buffer). If still black → the
allocator's presence itself. Narrowing the heap/display interaction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 08:16:46 -05:00

172 lines
6.9 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]
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::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;
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() -> ! {
// heap for track parsing (track-format uses alloc). RP2350 has 520 KB SRAM.
{
use core::mem::MaybeUninit;
const HEAP_SIZE: usize = 16 * 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 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();
// ---- MINIMAL ISOLATION ----
// Heap is initialised above. Exercise the allocator once (parse), then just draw the
// confirmed-working pattern in a loop. No inputs/audio/clock. If the screen shows blue +
// corners → heap + parse + display are all fine and the bug is in the metronome loop logic.
// If still black → the heap/parse path breaks the display.
// (parse removed for this test — heap init only)
let mut hb = false;
let mut hb_us = 0u64;
led.set_low().unwrap();
loop {
pm_ui::draw_ui(&mut display).ok();
let now = timer.get_counter().ticks();
if now.wrapping_sub(hb_us) > 500_000 {
hb = !hb;
let _ = if hb { led.set_high() } else { led.set_low() };
hb_us = now;
}
timer.delay_ms(50);
}
}