pm-kit: full st7796 init as PRIMARY bring-up, then mipidsi for drawing

Host initdump proved mipidsi's MADCTL (0x48), COLMOD, and address window
(CASET 0..319 / RASET 0..479) already match CircuitPython exactly — so the 1/4
+ rotation wasn't an addressing bug. The missing piece was the ST7796 extension
init (B6/power/gamma) running as the PRIMARY bring-up right after reset (grafting
it onto mipidsi's already-DISPON'd panel blanked or under-configured it).

Now: manual hw reset + full CircuitPython st7796_init via the raw interface, THEN
Builder without reset_pin (re-asserts only the basics, extension setup persists).
initdump extended to also dump CASET/RASET draw windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-31 22:50:51 -05:00
parent b2ea27f506
commit 026c20523d
2 changed files with 64 additions and 24 deletions

View file

@ -55,7 +55,7 @@ fn main() -> ! {
let sclk = pins.gpio2.into_function::<hal::gpio::FunctionSpi>(); let sclk = pins.gpio2.into_function::<hal::gpio::FunctionSpi>();
let mosi = pins.gpio3.into_function::<hal::gpio::FunctionSpi>(); let mosi = pins.gpio3.into_function::<hal::gpio::FunctionSpi>();
let dc = pins.gpio6.into_push_pull_output(); let dc = pins.gpio6.into_push_pull_output();
let rst = pins.gpio7.into_push_pull_output(); let mut rst = pins.gpio7.into_push_pull_output();
let cs = pins.gpio5.into_push_pull_output(); let cs = pins.gpio5.into_push_pull_output();
let spi = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (mosi, sclk)); let spi = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (mosi, sclk));
@ -68,33 +68,54 @@ fn main() -> ! {
let spi_device = ExclusiveDevice::new_no_delay(spi, cs).unwrap(); let spi_device = ExclusiveDevice::new_no_delay(spi, cs).unwrap();
let mut buffer = [0u8; 512]; let mut buffer = [0u8; 512];
let di = SpiInterface::new(spi_device, dc, &mut buffer); 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) let mut display = Builder::new(ST7796, di)
.reset_pin(rst)
.display_size(WIDTH, HEIGHT) .display_size(WIDTH, HEIGHT)
.color_order(ColorOrder::Bgr) .color_order(ColorOrder::Bgr)
.invert_colors(ColorInversion::Inverted) .invert_colors(ColorInversion::Inverted)
.orientation(Orientation::new().flip_horizontal()) // panel wants MX set (matches CircuitPython MADCTL 0x48) .orientation(Orientation::new().flip_horizontal())
.init(&mut timer) .init(&mut timer)
.unwrap(); .unwrap();
// Minimal: mipidsi's plain init already lit the panel (milestone 2 showed content, just in a // Same UI code the host simulator renders (rust/uisim → PNG).
// sub-region because the gate scan wasn't set to 480 lines). 0xB6 (Display Function Control)
// fixes the line count and is a BASIC command — it does NOT need the 0xF0 extension unlock
// (the unlock gates gamma/power, and was the likely blanker). Bracket it with DISP off/on so
// the scan is reconfigured while the display is off, the way CircuitPython does it.
{
let di = unsafe { display.dcs() };
di.send_command(0x28, &[]).unwrap(); // DISPOFF
di.send_command(0xB6, &[0x80, 0x02, 0x3B]).unwrap(); // display function control: 480 lines
di.send_command(0x29, &[]).unwrap(); // DISPON
}
timer.delay_ms(50);
// Same UI code the host simulator renders (rust/uisim → PNG). If this is wrong, the sim
// shows it without the bench; if the sim is right but the panel is wrong, it's a controller
// issue (init/MADCTL/0xB6), not a draw bug.
pm_ui::draw_ui(&mut display).unwrap(); pm_ui::draw_ui(&mut display).unwrap();
// Reached the loop → display init + draw completed. Slow 1 Hz blink (vs the solid-ON during // Reached the loop → display init + draw completed. Slow 1 Hz blink (vs the solid-ON during

View file

@ -3,6 +3,7 @@
//! guessing at the panel bring-up. `cargo run --bin initdump`. //! guessing at the panel bring-up. `cargo run --bin initdump`.
use core::convert::Infallible; use core::convert::Infallible;
use embedded_graphics::{pixelcolor::Rgb565, prelude::*, primitives::{PrimitiveStyle, Rectangle}};
use embedded_hal::delay::DelayNs; use embedded_hal::delay::DelayNs;
use embedded_hal::digital::{ErrorType, OutputPin}; use embedded_hal::digital::{ErrorType, OutputPin};
use mipidsi::interface::Interface; use mipidsi::interface::Interface;
@ -57,7 +58,7 @@ impl DelayNs for NoDelay {
fn main() { fn main() {
let mut delay = NoDelay; let mut delay = NoDelay;
let display = Builder::new(ST7796, Rec::default()) let mut display = Builder::new(ST7796, Rec::default())
.reset_pin(NoPin) .reset_pin(NoPin)
.display_size(320, 480) .display_size(320, 480)
.color_order(ColorOrder::Bgr) .color_order(ColorOrder::Bgr)
@ -65,11 +66,29 @@ fn main() {
.orientation(Orientation::new().flip_horizontal()) .orientation(Orientation::new().flip_horizontal())
.init(&mut delay) .init(&mut delay)
.unwrap(); .unwrap();
// boundary between init and draw commands (raw access to the recording interface)
let init_len = unsafe { display.dcs() }.log.len();
// Full-screen fill, then a single pixel at the far corner → reveals the address window.
Rectangle::new(Point::new(0, 0), Size::new(320, 480))
.into_styled(PrimitiveStyle::with_fill(Rgb565::BLUE))
.draw(&mut display)
.unwrap();
Pixel(Point::new(319, 479), Rgb565::RED).draw(&mut display).unwrap();
let (rec, _model, _rst) = display.release(); let (rec, _model, _rst) = display.release();
println!("mipidsi ST7796 init sequence ({} commands):", rec.log.len()); println!("init: {init_len} commands; full log {} commands", rec.log.len());
for (c, a) in &rec.log { for (i, (c, a)) in rec.log.iter().enumerate() {
let args = a.iter().map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" "); let args = a.iter().map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" ");
println!(" 0x{c:02X} {args}"); let tag = match c {
0x2A => " <- CASET (col x0..x1)",
0x2B => " <- RASET (row y0..y1)",
0x2C => " <- RAMWR",
_ => "",
};
let phase = if i < init_len { "init" } else { "draw" };
println!(" [{phase}] 0x{c:02X} {args}{tag}");
} }
} }