diff --git a/rust/pm-kit/src/main.rs b/rust/pm-kit/src/main.rs index c324f50..5082c7d 100644 --- a/rust/pm-kit/src/main.rs +++ b/rust/pm-kit/src/main.rs @@ -28,6 +28,23 @@ 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(); @@ -56,7 +73,8 @@ fn main() -> ! { let mosi = pins.gpio3.into_function::(); let dc = pins.gpio6.into_push_pull_output(); let mut rst = pins.gpio7.into_push_pull_output(); - let cs = pins.gpio5.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( @@ -65,7 +83,7 @@ fn main() -> ! { 16.MHz(), embedded_hal::spi::MODE_0, ); - let spi_device = ExclusiveDevice::new_no_delay(spi, cs).unwrap(); + 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); diff --git a/rust/uisim/src/bin/panelsim.rs b/rust/uisim/src/bin/panelsim.rs new file mode 100644 index 0000000..bbc620a --- /dev/null +++ b/rust/uisim/src/bin/panelsim.rs @@ -0,0 +1,130 @@ +//! Panel emulator: a host mipidsi `Interface` that decodes the REAL command/pixel stream +//! (CASET/RASET/RAMWR + send_pixels/send_repeated_pixel) into a framebuffer, then renders +//! pm-ui *through mipidsi* and saves a PNG. Unlike the plain uisim (which draws to its own +//! buffer and never exercises mipidsi's pixel path), this reproduces addressing/stride/count +//! bugs — the kind causing the 1/4 + stripes on hardware. `cargo run --bin panelsim`. + +use core::convert::Infallible; +use embedded_graphics::pixelcolor::raw::RawU16; +use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::prelude::*; +use embedded_hal::delay::DelayNs; +use embedded_hal::digital::{ErrorType, OutputPin}; +use mipidsi::interface::Interface; +use mipidsi::models::ST7796; +use mipidsi::options::{ColorInversion, ColorOrder, Orientation}; +use mipidsi::Builder; + +const W: usize = 320; +const H: usize = 480; + +struct Panel { + fb: Vec, + xs: usize, + xe: usize, + ys: usize, + ye: usize, + cx: usize, + cy: usize, + pixels_written: u64, +} +impl Panel { + fn new() -> Self { + Panel { fb: vec![Rgb565::BLACK; W * H], xs: 0, xe: W - 1, ys: 0, ye: H - 1, cx: 0, cy: 0, pixels_written: 0 } + } + fn put(&mut self, c: Rgb565) { + if self.cx < W && self.cy < H { + self.fb[self.cy * W + self.cx] = c; + } + self.pixels_written += 1; + self.cx += 1; + if self.cx > self.xe { + self.cx = self.xs; + self.cy += 1; + } + } +} +fn be(a: &[u8]) -> usize { + ((a[0] as usize) << 8) | a[1] as usize +} +impl Interface for Panel { + type Word = u8; + type Error = Infallible; + fn send_command(&mut self, cmd: u8, args: &[u8]) -> Result<(), Infallible> { + match cmd { + 0x2A if args.len() >= 4 => { + self.xs = be(&args[0..2]); + self.xe = be(&args[2..4]); + } + 0x2B if args.len() >= 4 => { + self.ys = be(&args[0..2]); + self.ye = be(&args[2..4]); + } + 0x2C => { + self.cx = self.xs; + self.cy = self.ys; + } + _ => {} + } + Ok(()) + } + fn send_pixels(&mut self, pixels: impl IntoIterator) -> Result<(), Infallible> { + for px in pixels { + let v = ((px[0] as u16) << 8) | px[1] as u16; + self.put(Rgb565::from(RawU16::new(v))); + } + Ok(()) + } + fn send_repeated_pixel(&mut self, pixel: [u8; N], count: u32) -> Result<(), Infallible> { + let v = ((pixel[0] as u16) << 8) | pixel[1] as u16; + let c = Rgb565::from(RawU16::new(v)); + for _ in 0..count { + self.put(c); + } + Ok(()) + } +} + +struct NoPin; +impl ErrorType for NoPin { + type Error = Infallible; +} +impl OutputPin for NoPin { + fn set_low(&mut self) -> Result<(), Infallible> { + Ok(()) + } + fn set_high(&mut self) -> Result<(), Infallible> { + Ok(()) + } +} +struct NoDelay; +impl DelayNs for NoDelay { + fn delay_ns(&mut self, _: u32) {} +} + +fn main() { + let mut delay = NoDelay; + let mut display = Builder::new(ST7796, Panel::new()) + .reset_pin(NoPin) + .display_size(W as u16, H as u16) + .color_order(ColorOrder::Bgr) + .invert_colors(ColorInversion::Inverted) + .orientation(Orientation::new().flip_horizontal()) + .init(&mut delay) + .unwrap(); + + pm_ui::draw_ui(&mut display).unwrap(); + + let (panel, _m, _r) = display.release(); + println!("pixels written by mipidsi: {} (full screen = {})", panel.pixels_written, W * H); + + let img = image::RgbImage::from_fn(W as u32, H as u32, |x, y| { + let c = panel.fb[(y as usize) * W + x as usize]; + let r = (c.r() << 3) | (c.r() >> 2); + let g = (c.g() << 2) | (c.g() >> 4); + let b = (c.b() << 3) | (c.b() >> 2); + image::Rgb([r, g, b]) + }); + img.save("panelsim.png").unwrap(); + println!("wrote panelsim.png"); +}