pm-kit: hold CS low — fix ST7796 1/4 screen (mipidsi toggles CS mid-command)

Reading mipidsi's interface/spi.rs: send_command writes the command byte and its
parameters as TWO separate SpiDevice transactions, so a normal SpiDevice de-asserts
CS between them. The ST7796 needs CS continuous across command+parameters, so
MADCTL/COLMOD/B6 args never loaded → default scan/orientation → 1/4 + rotated
(parameter-less commands and the pixel stream still worked, which is why it lit up).

CircuitPython's FourWire holds CS low for the whole command; replicate that: drive
the real CS (GP5) low for the session and give ExclusiveDevice a no-op CS. DC alone
selects command vs data.

Diagnosed entirely on the host: panelsim (new) decodes mipidsi's actual command/
pixel stream into a PNG and rendered perfectly, proving the protocol was right and
the bug was in the physical SPI/CS layer — then the driver source confirmed it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-31 23:01:04 -05:00
parent 026c20523d
commit b154ccf493
2 changed files with 150 additions and 2 deletions

View file

@ -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::<hal::gpio::FunctionSpi>();
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);

View file

@ -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<Rgb565>,
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<const N: usize>(&mut self, pixels: impl IntoIterator<Item = [u8; N]>) -> 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<const N: usize>(&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");
}