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>
172 lines
6.9 KiB
Rust
172 lines
6.9 KiB
Rust
//! 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);
|
||
}
|
||
}
|