From 8fc19ae69aa7b1243953935dd8b6c076950e6ee7 Mon Sep 17 00:00:00 2001 From: m5r Date: Sat, 25 Apr 2026 21:40:29 +0200 Subject: [PATCH] fix(epd7in5_v2): match waveshare reference framebuffer polarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `update_frame` was sending the buffer verbatim to DTM2 (0x13), causing every framebuffer to render with bit polarity inverted on the actual panel: `Color::White` (bit 1) showed as black, `Color::Black` (bit 0) showed as white. Symmetric test patterns hide this; any image with asymmetric content does not. Waveshare's reference C demo `RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_7in5_V2.c::EPD_7IN5_V2_Display` sends the user buffer raw to DTM1 (0x10) and bitwise-NOT to DTM2 (0x13). The Python driver `epd7in5_V2.py::display` does the same on the wire, just with the opposite user-facing buffer convention (it pre-inverts in `getbuffer` per its own `e-paper world 0=white, 1=black` comment). The panel's DTM2 register expects bit 0 = white per the UC8179 datasheet §22, KW mode with NEW/OLD, DDX=00: `{NEW=0, OLD=0} -> LUTWW`. This patch: - adds `DisplayInterface::data_inverted` that streams `~data` via a 256- byte stack chunk, avoiding heap allocation for the 48 KB framebuffer. - changes `Epd7in5::update_frame` to write DTM1 raw + DTM2 inverted, matching the C demo. Writing both forces a full LUTKW/LUTWK transition for every pixel, producing strong contrast. - changes `Epd7in5::clear_frame` to write DTM1=0xFF + DTM2=0x00, matching `EPD_7IN5_V2_Clear`. Previously both were 0x00 which goes through LUTWW (stays white) — works for a freshly-powered panel but doesn't force a clean transition from a prior image. Verified on real hardware: ESP32-S3 driving a Waveshare 7.5" e-Paper V2 (G) panel via the Universal Driver HAT Rev 2.3. Before the patch, the smoke binary's white-bg + black-text framebuffer rendered as black-bg with white text. After the patch, it renders correctly without any app-side color swap. --- src/epd7in5_v2/mod.rs | 16 ++++++++++++++-- src/interface.rs | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/epd7in5_v2/mod.rs b/src/epd7in5_v2/mod.rs index 8d0d45f3..5cc8f05c 100644 --- a/src/epd7in5_v2/mod.rs +++ b/src/epd7in5_v2/mod.rs @@ -131,7 +131,16 @@ where delay: &mut DELAY, ) -> Result<(), SPI::Error> { self.wait_until_idle(spi, delay)?; - self.cmd_with_data(spi, Command::DataStartTransmission2, buffer)?; + // Waveshare's reference C demo (EPD_7in5_V2.c::EPD_7IN5_V2_Display) + // sends the framebuffer to DTM1 (0x10) raw and to DTM2 (0x13) bitwise- + // inverted. The user-facing convention is bit 1 = white; the panel's + // DTM2 register expects bit 0 = white (datasheet §22, KW mode with + // NEW/OLD, DDX=00). Writing both DTM1 and DTM2 with opposite polarity + // forces a full LUTKW/LUTWK transition for every pixel, producing + // strong contrast. Without this, every framebuffer renders inverted. + self.cmd_with_data(spi, Command::DataStartTransmission1, buffer)?; + self.command(spi, Command::DataStartTransmission2)?; + self.interface.data_inverted(spi, buffer)?; Ok(()) } @@ -169,8 +178,11 @@ where self.wait_until_idle(spi, delay)?; self.send_resolution(spi)?; + // Match Waveshare's `EPD_7IN5_V2_Clear` (DTM1=0xFF, DTM2=0x00) so + // every pixel transitions black->white via LUTKW. See `update_frame` + // for the polarity rationale. self.command(spi, Command::DataStartTransmission1)?; - self.interface.data_x_times(spi, 0x00, WIDTH / 8 * HEIGHT)?; + self.interface.data_x_times(spi, 0xFF, WIDTH / 8 * HEIGHT)?; self.command(spi, Command::DataStartTransmission2)?; self.interface.data_x_times(spi, 0x00, WIDTH / 8 * HEIGHT)?; diff --git a/src/interface.rs b/src/interface.rs index f3a9a3d7..ed12d9c6 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -89,6 +89,23 @@ where self.data(spi, data) } + /// Sends data with bytewise bitwise-NOT applied. Streams via a stack chunk + /// to avoid heap allocation. Required for displays whose DTM2 register + /// expects bit polarity inverted from the user-facing framebuffer (e.g. + /// 7.5" V2, where Waveshare's reference C demo + /// `EPD_7in5_V2.c::EPD_7IN5_V2_Display` applies the same `~` before 0x13). + pub(crate) fn data_inverted(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> { + let _ = self.dc.set_high(); + let mut chunk = [0u8; 256]; + for source in data.chunks(chunk.len()) { + for (index, &byte) in source.iter().enumerate() { + chunk[index] = !byte; + } + self.write(spi, &chunk[..source.len()])?; + } + Ok(()) + } + /// Basic function for sending the same byte of data (one u8) multiple times over spi /// /// Enables direct interaction with the device with the help of [command()](ConnectionInterface::command())