From ab79f590093826d51fc25db45309a75946389d84 Mon Sep 17 00:00:00 2001 From: Scotty Waggoner Date: Tue, 25 Aug 2026 15:48:19 -0700 Subject: [PATCH] feat: Add support for the 4.2in 3 color (B/C) display Adds a driver for the original-generation 4.2in 400x300 tri-color panel, covering both the B (black/white/red) and C (black/white/yellow) pigments. Waveshare ships a single driver for both; the later 4in2b_V2 revision is a different controller and is red-only. The panel runs from its OTP waveform, selected by PanelSetting 0x0F, so the whole initialisation is three commands: BoosterSoftStart, PowerOn and PanelSetting. It has no partial refresh, so update_partial_frame and set_lut are unimplemented and QuickRefresh is not implemented at all. The chromatic plane is inverted on send. Together with BWRBIT=false this produces the polarity the panel documents: a set bit is white on both planes, and a cleared bit on the chromatic plane is the pigment. Tested against a 4.2in (C) yellow panel on a Waveshare ESP32 driver board. Supersedes #144 and #231, which target a graphics.rs architecture that has since been replaced by the BWRBIT const parameter. --- CHANGELOG.md | 6 + README.md | 1 + src/epd4in2bc/command.rs | 183 +++++++++++++ src/epd4in2bc/mod.rs | 547 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 5 files changed, 738 insertions(+) create mode 100644 src/epd4in2bc/command.rs create mode 100644 src/epd4in2bc/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 71bd6e9d..46f4c818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Add support for the 4.2 inch 3 color (B/C) display, in both the red (B) and yellow (C) pigments + ## [v0.6.0] - 2024-10-28 ### Added diff --git a/README.md b/README.md index 7e075b8d..1d2136f2 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ epd4in2.sleep(&mut spi, &mut delay) | [7.3 Inch HAT (F)](https://www.waveshare.com/product/7.3inch-e-paper-hat-f.htm) | Black, White, Red, Green, Blue, Yellow, Orange | ✕ | ✕ | ✔ | ✔ | | [5.83 Inch B/W/R (b)](https://www.waveshare.com/5.83inch-e-Paper-B.htm) | Black, White, Red | ✕ | Not officially | ✔ | ✔ | | [5.65 Inch 7 Color (F)](https://www.waveshare.com/5.65inch-e-paper-module-f.htm) | Black, White, Red, Green, Blue, Yellow, Orange | ✕ | ✕ | ✔ | ✔ | +| [4.2 Inch 3 Color (B/C)](https://www.waveshare.com/4.2inch-e-paper-module-c.htm) | Black, White, Red or Yellow | ✕ | ✕ | ✔ | ✔ | | [4.2 Inch B/W (A)](https://www.waveshare.com/product/4.2inch-e-paper-module.htm) | Black, White | ✕ | Not officially [[2](#2-42-inch-e-ink-blackwhite---partial-refresh)] | ✔ | ✔ | | [2.13 Inch B/W (A) V2](https://www.waveshare.com/product/2.13inch-e-paper-hat.htm) | Black, White | ✕ | ✔ | ✔ | ✔ | | [2.13 Inch B/W/R (B/C) V2](https://www.waveshare.com/product/raspberry-pi/displays/e-paper/2.13inch-e-paper-hat-b.htm) | Black, White, Red | ✕ | ✕ | ✔ | ✔ | diff --git a/src/epd4in2bc/command.rs b/src/epd4in2bc/command.rs new file mode 100644 index 00000000..f28cc328 --- /dev/null +++ b/src/epd4in2bc/command.rs @@ -0,0 +1,183 @@ +//! SPI Commands for the Waveshare 4.2" (B/C) tri-color E-Ink Display +use crate::traits; +/// EPD4IN2BC commands +/// +/// Should rarely (never?) be needed directly. +/// +/// For more infos about the addresses and what they are doing look into the pdfs +/// +/// The description of the single commands is mostly taken from IL0398.pdf +/// +/// This is the same UC8176-class command set as the black/white [`crate::epd4in2`]. It is +/// duplicated rather than shared, matching how the other tri-color variants of existing displays +/// are structured (see [`crate::epd5in83b_v2`] and [`crate::epd7in5b_v2`]). +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift direction, booster switch, soft reset + /// One Byte of Data: + /// 0x0F Red Mode, LUT from OTP + /// 0x1F B/W Mode, LUT from OTP + /// 0x2F Red Mode, LUT set by registers + /// 0x3F B/W Mode, LUT set by registers + /// + /// This driver uses 0x0F: tri-color, waveform from OTP. + PanelSetting = 0x00, + /// selecting internal and external power + /// self.send_data(0x03)?; //VDS_EN, VDG_EN + /// self.send_data(0x00)?; //VCOM_HV, VGHL_LV[1], VGHL_LV[0] + /// self.send_data(0x2b)?; //VDH + /// self.send_data(0x2b)?; //VDL + /// self.send_data(0xff)?; //VDHR + PowerSetting = 0x01, + /// After the Power Off command, the driver will power off following the Power Off Sequence. This command will turn off charge + /// pump, T-con, source driver, gate driver, VCOM, and temperature sensor, but register data will be kept until VDD becomes OFF. + /// Source Driver output and Vcom will remain as previous condition, which may have 2 conditions: floating. + PowerOff = 0x02, + /// Setting Power OFF sequence + PowerOffSequenceSetting = 0x03, + /// Turning On the Power + PowerOn = 0x04, + /// This command enables the internal bandgap, which will be cleared by the next POF. + PowerOnMeasure = 0x05, + /// Starting data transmission + /// 3-times: self.send_data(0x17)?; //07 0f 17 1f 27 2F 37 2f + BoosterSoftStart = 0x06, + /// After this command is transmitted, the chip would enter the deep-sleep mode to save power. + /// + /// The deep sleep mode would return to standby by hardware reset. + /// + /// The only one parameter is a check code, the command would be excuted if check code = 0xA5. + DeepSleep = 0x07, + /// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data + /// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel. + /// + /// - In B/W mode, this command writes “OLD” data to SRAM. + /// - In B/W/Red mode, this command writes “B/W” data to SRAM. + /// - In Program mode, this command writes “OTP” data to SRAM for programming. + DataStartTransmission1 = 0x10, + /// Stopping data transmission + DataStop = 0x11, + /// While user sent this command, driver will refresh display (data/VCOM) according to SRAM data and LUT. + /// + /// After Display Refresh command, BUSY_N signal will become “0” and the refreshing of panel starts. + DisplayRefresh = 0x12, + /// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data + /// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel. + /// - In B/W mode, this command writes “NEW” data to SRAM. + /// - In B/W/Red mode, this command writes “RED” data to SRAM. + DataStartTransmission2 = 0x13, + + /// This command stores VCOM Look-Up Table with 7 groups of data. Each group contains information for one state and is stored + /// with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutForVcom = 0x20, + /// This command stores White-to-White Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutWhiteToWhite = 0x21, + /// This command stores Black-to-White Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutBlackToWhite = 0x22, + /// This command stores White-to-Black Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutWhiteToBlack = 0x23, + /// This command stores Black-to-Black Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutBlackToBlack = 0x24, + /// The command controls the PLL clock frequency. + PllControl = 0x30, + /// This command reads the temperature sensed by the temperature sensor. + /// + /// Doesn't work! Waveshare doesn't connect the read pin + TemperatureSensor = 0x40, + /// Selects the Internal or External temperature sensor and offset + TemperatureSensorSelection = 0x41, + /// Write External Temperature Sensor + TemperatureSensorWrite = 0x42, + /// Read External Temperature Sensor + /// + /// Doesn't work! Waveshare doesn't connect the read pin + TemperatureSensorRead = 0x43, + /// This command indicates the interval of Vcom and data output. When setting the vertical back porch, the total blanking will be kept (20 Hsync) + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn the battery condition. + LowPowerDetection = 0x51, + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority than the RES\[1:0\] in R00H (PSR). + ResolutionSetting = 0x61, + /// This command defines the Fist Active Gate and First Active Source of active channels. + GsstSetting = 0x65, + /// The LUT_REV / Chip Revision is read from OTP address = 0x001. + /// + /// Doesn't work! Waveshare doesn't connect the read pin + Revision = 0x70, + /// Read Flags. This command reads the IC status + /// PTL, I2C_ERR, I2C_BUSY, DATA, PON, POF, BUSY + /// + /// Doesn't work! Waveshare doesn't connect the read pin + GetStatus = 0x71, + /// Automatically measure VCOM. This command reads the IC status + AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value + /// + /// Doesn't work! Waveshare doesn't connect the read pin + ReadVcomValue = 0x81, + /// Set VCM_DC + VcmDcSetting = 0x82, + /// This command sets partial window + PartialWindow = 0x90, + /// This command makes the display enter partial mode + PartialIn = 0x91, + /// This command makes the display exit partial mode and enter normal mode + PartialOut = 0x92, + /// After this command is issued, the chip would enter the program mode. + /// + /// After the programming procedure completed, a hardware reset is necessary for leaving program mode. + /// + /// The only one parameter is a check code, the command would be excuted if check code = 0xA5. + ProgramMode = 0xA0, + /// After this command is transmitted, the programming state machine would be activated. + /// + /// The BUSY flag would fall to 0 until the programming is completed. + ActiveProgramming = 0xA1, + /// The command is used for reading the content of OTP for checking the data of programming. + /// + /// The value of (n) is depending on the amount of programmed data, tha max address = 0xFFF. + ReadOtp = 0xA2, + /// This command is set for saving power during fresh period. If the output voltage of VCOM / Source is from negative to positive or + /// from positive to negative, the power saving mechanism will be activated. The active period width is defined by the following two + /// parameters. + PowerSaving = 0xE3, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Command as CommandTrait; + + #[test] + fn command_addr() { + assert_eq!(Command::PowerSaving.address(), 0xE3); + + assert_eq!(Command::PanelSetting.address(), 0x00); + + assert_eq!(Command::DisplayRefresh.address(), 0x12); + } +} diff --git a/src/epd4in2bc/mod.rs b/src/epd4in2bc/mod.rs new file mode 100644 index 00000000..a364cd1f --- /dev/null +++ b/src/epd4in2bc/mod.rs @@ -0,0 +1,547 @@ +//! A simple Driver for the Waveshare 4.2" (B/C) tri-color E-Ink Display via SPI +//! +//! This covers the original-generation 4.2" 400x300 three-color raw panels, in both the +//! B (black/white/**red**) and C (black/white/**yellow**) pigments. Waveshare ships a single +//! `epd4in2bc` driver for both; there is no separate `epd4in2c`, and the later `epd4in2b_V2` +//! revision is a different controller and is red-only. +//! +//! The panel uses waveforms programmed into OTP, selected by `PanelSetting = 0x0F`. It therefore +//! takes no LUT upload, no power setting, no PLL setting and no resolution setting -- the whole +//! initialisation is three commands. +//! +//! # References +//! +//! - [Product page](https://www.waveshare.com/4.2inch-e-paper-module-c.htm) +//! - [Manual](https://www.waveshare.com/wiki/4.2inch_e-Paper_Module_(C)_Manual) +//! - [Waveshare Python driver](https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd4in2bc.py) +//! +//! # Cautions +//! +//! From the manual, and worth honouring in application code: +//! +//! - A full refresh takes roughly **25 seconds**, and the recommended minimum interval between +//! refreshes is **180 seconds**. +//! - This panel has **no partial refresh**. [`WaveshareDisplay::update_partial_frame`] is +//! deliberately unimplemented, and [`crate::traits::QuickRefresh`] is not implemented at all. +//! - **Always call [`WaveshareDisplay::sleep`] once the refresh is done.** Leaving the panel +//! powered holds it in a high-voltage state, which permanently damages it. +//! +//! # Examples +//! +//!```rust, no_run +//!# use embedded_hal_mock::eh1::*; +//!# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +//!use embedded_graphics::{ +//! prelude::*, primitives::{Line, PrimitiveStyle}, +//!}; +//!use epd_waveshare::{epd4in2bc::*, prelude::*}; +//!# +//!# let expectations = []; +//!# let mut spi = spi::Mock::new(&expectations); +//!# let expectations = []; +//!# let cs_pin = digital::Mock::new(&expectations); +//!# let busy_in = digital::Mock::new(&expectations); +//!# let dc = digital::Mock::new(&expectations); +//!# let rst = digital::Mock::new(&expectations); +//!# let mut delay = delay::NoopDelay::new(); +//! +//!// Setup EPD +//!let mut epd = Epd4in2bc::new(&mut spi, busy_in, dc, rst, &mut delay, None)?; +//! +//!// Use display graphics from embedded-graphics +//!let mut display = Display4in2bc::default(); +//! +//!// Draw a chromatic (red or yellow, depending on the panel) line +//!let _ = Line::new(Point::new(0, 120), Point::new(0, 295)) +//! .into_styled(PrimitiveStyle::with_stroke(TriColor::Chromatic, 1)) +//! .draw(&mut display); +//! +//!// Display updated frame +//!epd.update_and_display_frame(&mut spi, display.buffer(), &mut delay)?; +//! +//!// Set the EPD to sleep -- do not skip this +//!epd.sleep(&mut spi, &mut delay)?; +//!# Ok(()) +//!# } +//!``` + +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::color::TriColor; +use crate::interface::DisplayInterface; +use crate::traits::{ + InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, +}; + +pub(crate) mod command; +use self::command::Command; + +#[cfg(feature = "graphics")] +use crate::buffer_len; + +/// Width of the display +pub const WIDTH: u32 = 400; +/// Height of the display +pub const HEIGHT: u32 = 300; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: TriColor = TriColor::White; + +const IS_BUSY_LOW: bool = true; +// Bulk SPI writes. The 4.2" b/w driver toggles CS per byte, but this panel does not require it -- +// the tri-color `epd2in9b_v4` is bulk too -- and at 30_000 bytes per frame the per-byte transaction +// overhead is worth avoiding. +const SINGLE_BYTE_WRITE: bool = false; +/// Bytes per colour plane. The [`Display4in2bc`] buffer holds two of these back to back. +const NUM_DISPLAY_BITS: u32 = WIDTH / 8 * HEIGHT; + +/// Full size buffer for use with the 4in2bc EPD +/// +/// `BWRBIT` is `false`, which combined with the chromatic-plane inversion this driver applies on +/// send produces the polarity the panel documents: on both planes a set bit is white, and on the +/// chromatic plane a cleared bit is the chromatic pigment. +#[cfg(feature = "graphics")] +pub type Display4in2bc = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize * 2) }, + TriColor, +>; + +/// Epd4in2bc driver +pub struct Epd4in2bc { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: TriColor, +} + +impl InternalWiAdditions + for Epd4in2bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Vendor reset: 200ms high, 2ms low, 200ms high. + self.interface.reset(delay, 200_000, 2_000); + + self.cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x17])?; + + self.command(spi, Command::PowerOn)?; + self.wait_until_idle(spi, delay)?; + + // 0x0F selects the OTP waveform, so no LUT upload follows. + self.cmd_with_data(spi, Command::PanelSetting, &[0x0F])?; + + Ok(()) + } +} + +impl WaveshareThreeColorDisplay + for Epd4in2bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn update_color_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + black: &[u8], + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.update_achromatic_frame(spi, delay, black)?; + self.update_chromatic_frame(spi, delay, chromatic) + } + + /// Update only the black/white data of the display. + /// + /// Finish by calling `update_chromatic_frame`. + fn update_achromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + black: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DataStartTransmission1, black)?; + Ok(()) + } + + /// Update only chromatic data of the display. + /// + /// This data takes precedence over the black/white data. The bytes are inverted on the way + /// out, so that a set bit in the caller's buffer means the chromatic pigment -- see + /// [`Display4in2bc`]. + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::DataStartTransmission2)?; + self.send_inverted(spi, chromatic)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd4in2bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = TriColor; + + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd4in2bc { interface, color }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.command(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + delay.delay_us(2_000_000); + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + fn set_background_color(&mut self, color: TriColor) { + self.color = color; + } + + fn background_color(&self) -> &TriColor { + &self.color + } + + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + /// Send a full dual-plane buffer, as produced by [`Display4in2bc`]. + /// + /// The first half is the black/white plane and the second half is the chromatic plane; the + /// latter is inverted on the way out. + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + let split = NUM_DISPLAY_BITS as usize; + debug_assert_eq!( + buffer.len(), + 2 * split, + "expected a dual-plane buffer; a single-plane buffer would leave the chromatic plane \ + short and desync the panel" + ); + + self.update_achromatic_frame(spi, delay, &buffer[..split])?; + self.update_chromatic_frame(spi, delay, &buffer[split..])?; + Ok(()) + } + + /// Not supported: this panel has no partial refresh mode. + fn update_partial_frame( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + _buffer: &[u8], + _x: u32, + _y: u32, + _width: u32, + _height: u32, + ) -> Result<(), SPI::Error> { + unimplemented!("the 4.2in tri-color panel does not support partial refresh") + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.command(spi, Command::DisplayRefresh)?; + self.wait_until_idle(spi, delay)?; + Ok(()) + } + + fn update_and_display_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.update_frame(spi, buffer, delay)?; + self.display_frame(spi, delay)?; + Ok(()) + } + + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + + // A set bit is white on both planes, so 0xFF twice clears to white. + self.command(spi, Command::DataStartTransmission1)?; + self.interface.data_x_times(spi, 0xFF, NUM_DISPLAY_BITS)?; + + self.command(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, 0xFF, NUM_DISPLAY_BITS)?; + + Ok(()) + } + + /// Not supported: the panel runs from its OTP waveform, selected by `PanelSetting = 0x0F`, + /// and ignores LUTs written to the LUT registers. + fn set_lut( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + _refresh_rate: Option, + ) -> Result<(), SPI::Error> { + unimplemented!("the 4.2in tri-color panel uses its OTP waveform and has no settable LUT") + } + + fn wait_until_idle(&mut self, _spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.wait_until_idle(delay, IS_BUSY_LOW); + Ok(()) + } +} + +impl Epd4in2bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn command(&mut self, spi: &mut SPI, command: Command) -> Result<(), SPI::Error> { + self.interface.cmd(spi, command) + } + + fn cmd_with_data( + &mut self, + spi: &mut SPI, + command: Command, + data: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd_with_data(spi, command, data) + } + + /// Send `data` with every byte bitwise inverted, without allocating. + fn send_inverted(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> { + let mut chunk = [0u8; 64]; + for block in data.chunks(chunk.len()) { + let n = block.len(); + for (dst, src) in chunk[..n].iter_mut().zip(block) { + *dst = !*src; + } + self.interface.data(spi, &chunk[..n])?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + extern crate std; + use std::vec::Vec; + + /// Records every byte handed to the bus, tagged with the DC level at the time, so a test can + /// assert the exact command/data stream the panel would see. + #[derive(Default)] + struct Recorder { + written: Vec, + } + + impl embedded_hal::spi::ErrorType for Recorder { + type Error = embedded_hal::spi::ErrorKind; + } + + impl embedded_hal::spi::SpiDevice for Recorder { + fn transaction( + &mut self, + operations: &mut [embedded_hal::spi::Operation<'_, u8>], + ) -> Result<(), Self::Error> { + for op in operations { + if let embedded_hal::spi::Operation::Write(buf) = op { + self.written.extend_from_slice(buf); + } + } + Ok(()) + } + } + + struct FakePin; + impl embedded_hal::digital::ErrorType for FakePin { + type Error = core::convert::Infallible; + } + impl embedded_hal::digital::OutputPin for FakePin { + fn set_low(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + fn set_high(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + } + /// Never busy, so `wait_until_idle` returns immediately. + impl embedded_hal::digital::InputPin for FakePin { + fn is_high(&mut self) -> Result { + Ok(true) + } + fn is_low(&mut self) -> Result { + Ok(false) + } + } + + struct NoDelay; + impl embedded_hal::delay::DelayNs for NoDelay { + fn delay_ns(&mut self, _ns: u32) {} + } + + fn build() -> ( + Recorder, + Epd4in2bc, + ) { + let mut spi = Recorder::default(); + let mut delay = NoDelay; + let epd = Epd4in2bc::new(&mut spi, FakePin, FakePin, FakePin, &mut delay, None).unwrap(); + spi.written.clear(); // drop the init stream; tests below assert their own calls + (spi, epd) + } + + #[test] + fn init_matches_vendor_sequence() { + let mut spi = Recorder::default(); + let mut delay = NoDelay; + let _ = Epd4in2bc::new(&mut spi, FakePin, FakePin, FakePin, &mut delay, None).unwrap(); + + // BoosterSoftStart 17 17 17, PowerOn, PanelSetting 0F -- and nothing else. + assert_eq!(spi.written, [0x06, 0x17, 0x17, 0x17, 0x04, 0x00, 0x0F]); + } + + #[test] + fn update_frame_splits_planes_and_inverts_chromatic() { + let (mut spi, mut epd) = build(); + let mut delay = NoDelay; + + let n = NUM_DISPLAY_BITS as usize; + let mut buffer = Vec::new(); + buffer.extend(core::iter::repeat(0xAAu8).take(n)); // b/w plane + buffer.extend(core::iter::repeat(0x0Fu8).take(n)); // chromatic plane + + epd.update_frame(&mut spi, &buffer, &mut delay).unwrap(); + + // 0x10 + b/w plane verbatim, then 0x13 + chromatic plane inverted. + assert_eq!(spi.written.len(), 2 + 2 * n); + assert_eq!(spi.written[0], 0x10); + assert!(spi.written[1..=n].iter().all(|&b| b == 0xAA)); + assert_eq!(spi.written[n + 1], 0x13); + assert!(spi.written[n + 2..].iter().all(|&b| b == 0xF0)); + } + + #[test] + fn clear_frame_writes_white_to_both_planes() { + let (mut spi, mut epd) = build(); + let mut delay = NoDelay; + + epd.clear_frame(&mut spi, &mut delay).unwrap(); + + let n = NUM_DISPLAY_BITS as usize; + assert_eq!(spi.written.len(), 2 + 2 * n); + assert_eq!(spi.written[0], 0x10); + assert_eq!(spi.written[n + 1], 0x13); + // A set bit is white on both planes. + assert!(spi.written[1..=n].iter().all(|&b| b == 0xFF)); + assert!(spi.written[n + 2..].iter().all(|&b| b == 0xFF)); + } + + /// End-to-end polarity check: draw through `Display4in2bc` and assert the bytes that reach the + /// panel carry the polarity the manual documents -- on both planes a set bit is white, and on + /// the chromatic plane a cleared bit is the chromatic pigment. + #[cfg(feature = "graphics")] + #[test] + fn drawn_colors_reach_panel_with_vendor_polarity() { + use embedded_graphics::prelude::*; + + let (mut spi, mut epd) = build(); + let mut delay = NoDelay; + + let mut display = Display4in2bc::default(); + display.clear(TriColor::White).unwrap(); + // Row 0: black at x=0, white at x=1 (from the clear), chromatic at x=2. + display.set_pixel(Pixel(Point::new(0, 0), TriColor::Black)); + display.set_pixel(Pixel(Point::new(2, 0), TriColor::Chromatic)); + + epd.update_frame(&mut spi, display.buffer(), &mut delay) + .unwrap(); + + let n = NUM_DISPLAY_BITS as usize; + let bw_byte0 = spi.written[1]; + let chromatic_byte0 = spi.written[n + 2]; + + // b/w plane: bit7 (x=0) cleared = black, every other pixel left white. + assert_eq!(bw_byte0, 0b0111_1111, "b/w plane polarity"); + // chromatic plane as sent: bit5 (x=2) cleared = chromatic, everything else white. + assert_eq!(chromatic_byte0, 0b1101_1111, "chromatic plane polarity"); + } + + #[test] + fn sleep_powers_off_then_deep_sleeps() { + let (mut spi, mut epd) = build(); + let mut delay = NoDelay; + + epd.sleep(&mut spi, &mut delay).unwrap(); + + assert_eq!(spi.written, [0x02, 0x07, 0xA5]); + } + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 400); + assert_eq!(HEIGHT, 300); + assert_eq!(DEFAULT_BACKGROUND_COLOR, TriColor::White); + } + + #[test] + fn buffer_is_dual_plane() { + assert_eq!(NUM_DISPLAY_BITS, 15_000); + assert_eq!( + buffer_len(WIDTH as usize, HEIGHT as usize * 2), + 2 * NUM_DISPLAY_BITS as usize + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 3ef7291a..1d366caf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,6 +94,7 @@ pub mod epd2in9bc; pub mod epd2in9d; pub mod epd3in7; pub mod epd4in2; +pub mod epd4in2bc; pub mod epd5in65f; pub mod epd5in83_v2; pub mod epd5in83b_v2;