diff --git a/src/epd12in48b_v2/command.rs b/src/epd12in48b_v2/command.rs
new file mode 100644
index 00000000..6b8f8ba9
--- /dev/null
+++ b/src/epd12in48b_v2/command.rs
@@ -0,0 +1,43 @@
+//! SPI Commands for the Waveshare 12.48"(B) V2 Ink Display
+
+use crate::traits;
+
+/// Epd12in48 commands
+///
+#[allow(unused, non_camel_case_types)]
+#[derive(Clone, Copy)]
+pub enum Command {
+ PanelSetting = 0x00,
+ PowerOff = 0x02,
+ PowerOn = 0x04,
+ BoosterSoftStart = 0x06,
+ DeepSleep = 0x07,
+ DataStartTransmission1 = 0x10,
+ DisplayRefresh = 0x12,
+ DataStartTransmission2 = 0x13,
+ DualSPI = 0x15,
+ LUTC = 0x20,
+ LUTWW = 0x21,
+ LUTKW_LUTR = 0x22,
+ LUTWK_LUTW = 0x23,
+ LUTKK_LUTK = 0x24,
+ LUTBD = 0x25,
+ KWLUTOption = 0x2B,
+ VcomAndDataIntervalSetting = 0x50,
+ TconSetting = 0x60,
+ TconResolution = 0x61,
+ GetStatus = 0x71,
+ PartialWindow = 0x90,
+ PartialIn = 0x91,
+ PartialOut = 0x92,
+ CascadeSetting = 0xE0,
+ PowerSaving = 0xE3,
+ ForceTemperature = 0xE5,
+}
+
+impl traits::Command for Command {
+ /// Returns the address of the command
+ fn address(self) -> u8 {
+ self as u8
+ }
+}
diff --git a/src/epd12in48b_v2/config.rs b/src/epd12in48b_v2/config.rs
new file mode 100644
index 00000000..ff6422a3
--- /dev/null
+++ b/src/epd12in48b_v2/config.rs
@@ -0,0 +1,42 @@
+#[derive(Copy, Clone, Debug)]
+/// EPD Configuration
+pub struct Config {
+ /// Specifies how data1 bits are mapped to colors:
+ /// - `false`: 0 => black, 1 => white
+ /// - `true`: 0 => white, 1 => black
+ pub inverted_kw: bool,
+ /// Specifies how data2 bits are mapped to colors:
+ /// - `false`: 0 => red not active, 1 => red active
+ /// - `true`: 0 => red active, 1 => red not active
+ ///
+ /// Note that whenever the red channel is active, the black/white channel is ignored.
+ pub inverted_r: bool,
+ /// Lookup table to use for the screen border
+ pub border_lut: BorderLUT,
+ /// Whether to use the lookup tables loaded via `set_lut...` methods, or the built-in ones.
+ pub external_lut: bool,
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ inverted_kw: false,
+ inverted_r: false,
+ border_lut: BorderLUT::LUTBD,
+ external_lut: false,
+ }
+ }
+}
+
+/// Screen border lookup table variants
+#[derive(Copy, Clone, Debug)]
+pub enum BorderLUT {
+ /// Use LUTBD
+ LUTBD,
+ /// Use LUTK
+ LUTK,
+ /// Use LUTW
+ LUTW,
+ /// Use LUTR
+ LUTR,
+}
diff --git a/src/epd12in48b_v2/mod.rs b/src/epd12in48b_v2/mod.rs
new file mode 100644
index 00000000..10586a13
--- /dev/null
+++ b/src/epd12in48b_v2/mod.rs
@@ -0,0 +1,679 @@
+//! A driver for the Waveshare 12.48"(B) E-Ink Display (V2) via SPI
+//! (also known as [GDEY1248Z51](https://www.good-display.com/product/422.html))
+//!
+//! # References
+//!
+//! - [Datasheet](https://files.waveshare.com/upload/b/b4/12.48inch_e-Paper_B_V2_Specification.pdf)
+//! - [Wiki](https://www.waveshare.com/wiki/12.48inch_e-Paper_Module_(B))
+//! - [Waveshare C drivers](https://github.com/waveshareteam/12.48inch-e-paper/)
+//!
+
+mod command;
+mod config;
+
+use embedded_hal::{
+ delay::DelayNs,
+ digital::{InputPin, OutputPin, PinState},
+ spi::SpiBus,
+};
+
+pub use crate::rect::Rect;
+use command::Command;
+pub use config::*;
+
+/// A collection of peripherals controlling the EPD
+///
+/// The display is composed of 4 sub-displays arranged like so:
+/// ```md
+/// 0 648 1304
+/// 0 +--------+--------+
+/// | S2 | M2 |
+/// 492 +--------+--------+
+/// | M1 | S1 |
+/// 984 +--------+--------+
+/// ```
+/// Resolution of `S2` and `M1` is 648 x 492,
+/// resolution of `S1` and `M2` is 656 x 492.
+///
+pub struct Peripherals
+where
+ INPUT: InputPin,
+ OUTPUT: OutputPin,
+ SPI: SpiBus,
+{
+ /// SPI bus shared by all sub-displays.
+ pub spi: SPI,
+ /// Chip select signal for `M1`.
+ pub m1_cs: OUTPUT,
+ /// Chip select signal for `S1`.
+ pub s1_cs: OUTPUT,
+ /// Chip select signal for `M2`.
+ pub m2_cs: OUTPUT,
+ /// Chip select signal for `S2`.
+ pub s2_cs: OUTPUT,
+ /// Shared "command/data" signal for `M1` and `S1`.
+ pub m1s1_dc: OUTPUT,
+ /// Shared "command/data" signal for `M2` and `S2`.
+ pub m2s2_dc: OUTPUT,
+ /// Shared reset signal for `M1` and `S1`.
+ pub m1s1_rst: OUTPUT,
+ /// Shared reset signal for `M2` and `S2`.
+ pub m2s2_rst: OUTPUT,
+ /// "Busy" signal from `M1`.
+ pub m1_busy: INPUT,
+ /// "Busy" signal from `S1`.
+ pub s1_busy: INPUT,
+ /// "Busy" signal from `M2`.
+ pub m2_busy: INPUT,
+ /// "Busy" signal from `S2`.
+ pub s2_busy: INPUT,
+}
+
+/// EPD width
+pub const WIDTH: u32 = 1304;
+/// EPD height
+pub const HEIGHT: u32 = 984;
+
+const S2_WIDTH: u32 = 648;
+const S2_HEIGHT: u32 = 492;
+
+const FULL_RECT: Rect = Rect {
+ x: 0,
+ y: 0,
+ w: WIDTH,
+ h: HEIGHT,
+};
+
+const S2_RECT: Rect = Rect {
+ x: 0,
+ y: 0,
+ w: S2_WIDTH,
+ h: S2_HEIGHT,
+};
+
+const M2_RECT: Rect = Rect {
+ x: S2_WIDTH,
+ y: 0,
+ w: WIDTH - S2_WIDTH,
+ h: S2_HEIGHT,
+};
+
+const M1_RECT: Rect = Rect {
+ x: 0,
+ y: S2_HEIGHT,
+ w: S2_WIDTH,
+ h: HEIGHT - S2_HEIGHT,
+};
+
+const S1_RECT: Rect = Rect {
+ x: S2_WIDTH,
+ y: S2_HEIGHT,
+ w: WIDTH - S2_WIDTH,
+ h: HEIGHT - S2_HEIGHT,
+};
+
+type CS = u8;
+const CS_M1: CS = 0b0001;
+const CS_S1: CS = 0b0010;
+const CS_M2: CS = 0b0100;
+const CS_S2: CS = 0b1000;
+const CS_ALL: CS = CS_M1 | CS_S1 | CS_M2 | CS_S2;
+const CS_DATA: CS = 0b10000;
+
+/// Waveshare 12.48"(B)
+pub struct EpdDriver
+where
+ INPUT: InputPin,
+ OUTPUT: OutputPin,
+ SPI: SpiBus,
+ DELAY: DelayNs,
+{
+ peris: Peripherals,
+ delay: DELAY,
+ control_state: CS,
+}
+
+impl EpdDriver
+where
+ INPUT: InputPin,
+ INPUT::Error: core::fmt::Debug,
+ OUTPUT: OutputPin,
+ OUTPUT::Error: core::fmt::Debug,
+ SPI: SpiBus,
+ SPI::Error: core::fmt::Debug,
+ DELAY: DelayNs,
+{
+ /// Constructs a new instance of the EpdDriver.
+ /// Normally should be followd by calls to [`reset()`](EpdDriver::reset) and [`init()`](EpdDriver::init)
+ /// to wake up the display and initialize its registers.
+ pub fn new(peris: Peripherals, delay: DELAY) -> Self {
+ EpdDriver {
+ peris,
+ delay,
+ control_state: 0,
+ }
+ }
+
+ /// Consumes EpdDriver, releasing peripherals to the caller.
+ pub fn into_peripherals(self) -> Peripherals {
+ self.peris
+ }
+
+ /// Reset the display, potentially waking it up from deep sleep.
+ /// Normally should be followed by a call to [`init()`](EpdDriver::init).
+ pub fn reset(&mut self) -> Result<(), OUTPUT::Error> {
+ drop(self.peris.m1_cs.set_high());
+ drop(self.peris.s1_cs.set_high());
+ drop(self.peris.m2_cs.set_high());
+ drop(self.peris.s2_cs.set_high());
+ drop(self.peris.m1s1_dc.set_low());
+ drop(self.peris.m2s2_dc.set_low());
+ self.control_state = 0;
+
+ self.peris.m1s1_rst.set_high()?;
+ self.peris.m2s2_rst.set_high()?;
+ self.delay.delay_ms(1);
+
+ self.peris.m1s1_rst.set_low()?;
+ self.delay.delay_us(100); // min RST low = 50us
+ self.peris.m1s1_rst.set_high()?;
+ self.delay.delay_ms(100); // min wait after RST = 10ms
+
+ self.peris.m2s2_rst.set_low()?;
+ self.delay.delay_us(100);
+ self.peris.m2s2_rst.set_high()?;
+ self.delay.delay_ms(100);
+
+ Ok(())
+ }
+
+ /// Initialize display registers.
+ pub fn init(&mut self, config: &Config) -> Result<(), SPI::Error> {
+ // booster soft start
+ self.cmd_with_data(CS_ALL, Command::BoosterSoftStart, &[0x17, 0x17, 0x39, 0x17])?;
+
+ // resolution setting
+ fn resolution_data(rect: Rect) -> [u8; 4] {
+ [
+ (rect.w / 256) as u8,
+ (rect.w % 256) as u8,
+ (rect.h / 256) as u8,
+ (rect.h % 256) as u8,
+ ]
+ }
+ self.cmd_with_data(CS_M1, Command::TconResolution, &resolution_data(M1_RECT))?;
+ self.cmd_with_data(CS_S1, Command::TconResolution, &resolution_data(S1_RECT))?;
+ self.cmd_with_data(CS_M2, Command::TconResolution, &resolution_data(M2_RECT))?;
+ self.cmd_with_data(CS_S2, Command::TconResolution, &resolution_data(S2_RECT))?;
+
+ self.cmd_with_data(CS_ALL, Command::DualSPI, &[0x20])?;
+ self.cmd_with_data(CS_ALL, Command::TconSetting, &[0x22])?;
+ self.cmd_with_data(CS_ALL, Command::PowerSaving, &[0x00])?;
+ self.cmd_with_data(CS_ALL, Command::CascadeSetting, &[0x03])?;
+ self.cmd_with_data(CS_ALL, Command::ForceTemperature, &[25])?;
+
+ self.set_mode(config)?;
+
+ self.flush()
+ }
+
+ /// Set data "polarity", waveform lookup table mode, etc, without re-initializing anything else.
+ pub fn set_mode(&mut self, config: &Config) -> Result<(), SPI::Error> {
+ let ddx = match (config.inverted_r, config.inverted_kw) {
+ (false, true) => 0b00,
+ (false, false) => 0b01,
+ (true, true) => 0b10,
+ (true, false) => 0b11,
+ };
+ let ddx0 = ddx & 1 == 1;
+ let bdv = match (ddx0, config.border_lut) {
+ (false, BorderLUT::LUTBD) => 0b00,
+ (false, BorderLUT::LUTR) => 0b01,
+ (false, BorderLUT::LUTW) => 0b10,
+ (false, BorderLUT::LUTK) => 0b11,
+ (true, BorderLUT::LUTK) => 0b00,
+ (true, BorderLUT::LUTW) => 0b01,
+ (true, BorderLUT::LUTR) => 0b10,
+ (true, BorderLUT::LUTBD) => 0b11,
+ };
+
+ let reg = (config.external_lut as u8) << 5;
+ self.cmd_with_data(CS_M1, Command::PanelSetting, &[reg | 0x0F])?;
+ self.cmd_with_data(CS_S1, Command::PanelSetting, &[reg | 0x0F])?;
+ self.cmd_with_data(CS_M2, Command::PanelSetting, &[reg | 0x03])?;
+ self.cmd_with_data(CS_S2, Command::PanelSetting, &[reg | 0x03])?;
+
+ let bdv = bdv << 4;
+ self.cmd_with_data(
+ CS_ALL,
+ Command::VcomAndDataIntervalSetting,
+ &[bdv | ddx, 0x07],
+ )?;
+
+ self.flush()
+ }
+
+ /// Fill data1 buffer with pixels:
+ /// - data1 containes the black/white image channel,
+ /// - data2 contains the red/not red channel.
+ ///
+ /// `pixels` may contain a lesser number of rows than the window being written,
+ /// in which case it will be treated as circular.
+ pub fn write_data1(&mut self, pixels: &[u8]) -> Result<(), SPI::Error> {
+ self.write_window_data(Command::DataStartTransmission1, FULL_RECT, pixels)?;
+ self.flush()
+ }
+
+ /// Fill data2 buffer with pixels.
+ /// See also [`write_data1`](EpdDriver::write_data1).
+ pub fn write_data2(&mut self, pixels: &[u8]) -> Result<(), SPI::Error> {
+ self.write_window_data(Command::DataStartTransmission2, FULL_RECT, pixels)?;
+ self.flush()
+ }
+
+ /// Fill a window in the data1 buffer with pixels.
+ /// See also [`write_data1`](EpdDriver::write_data1).
+ pub fn write_data1_partial(&mut self, window: Rect, pixels: &[u8]) -> Result<(), SPI::Error> {
+ self.write_partial(Command::DataStartTransmission1, window, pixels)?;
+ self.flush()
+ }
+
+ /// Fill a window in the data2 buffer with pixels.
+ /// See also [`write_data1`](EpdDriver::write_data1).
+ pub fn write_data2_partial(&mut self, window: Rect, pixels: &[u8]) -> Result<(), SPI::Error> {
+ self.write_partial(Command::DataStartTransmission2, window, pixels)?;
+ self.flush()
+ }
+
+ /// Store VCOM Look-Up Table.
+ ///
+ /// If LUT data is shorter than expected, the rest is filled with zeroes.
+ /// Note that stored lookup tables need to be activated by setting
+ /// [`Config::external_lut`](config::Config::external_lut)`=true`.
+ pub fn set_lutc(&mut self, data: &[u8]) -> Result<(), SPI::Error> {
+ self.set_lut(Command::LUTC, data, 60)
+ }
+
+ /// Store White-to-White Look-Up Table.
+ /// See also [`write_data1`](EpdDriver::set_lutc).
+ pub fn set_lutww(&mut self, data: &[u8]) -> Result<(), SPI::Error> {
+ self.set_lut(Command::LUTWW, data, 42)
+ }
+
+ /// Store Black-to-White (KW mode) / Red (KWR mode) Look-Up Table.
+ /// See also [`write_data1`](EpdDriver::set_lutc).
+ pub fn set_lutkw_lutr(&mut self, data: &[u8]) -> Result<(), SPI::Error> {
+ self.set_lut(Command::LUTKW_LUTR, data, 60)
+ }
+
+ /// Store White-to-Black (KW mode) / White (KWR mode) Look-Up Table.
+ /// See also [`write_data1`](EpdDriver::set_lutc).
+ pub fn set_lutwk_lutw(&mut self, data: &[u8]) -> Result<(), SPI::Error> {
+ self.set_lut(Command::LUTWK_LUTW, data, 60)
+ }
+
+ /// Store Black-to-Black (KW mode) / Black (KWR mode) Look-Up Table.
+ /// See also [`write_data1`](EpdDriver::set_lutc).
+ pub fn set_lutkk_lutk(&mut self, data: &[u8]) -> Result<(), SPI::Error> {
+ self.set_lut(Command::LUTKK_LUTK, data, 60)
+ }
+
+ /// Store Border Look-Up Table.
+ /// See also [`write_data1`](EpdDriver::set_lutc).
+ pub fn set_lutbd(&mut self, data: &[u8]) -> Result<(), SPI::Error> {
+ self.set_lut(Command::LUTBD, data, 42)
+ }
+
+ fn set_lut(&mut self, cmd: Command, data: &[u8], reqd_len: usize) -> Result<(), SPI::Error> {
+ self.cmd_with_data(CS_ALL, cmd, data)?;
+ if data.len() < reqd_len {
+ let zeroes = [0; 60];
+ self.spi_write(CS_ALL | CS_DATA, &zeroes[..reqd_len - data.len()])?;
+ }
+ self.flush()
+ }
+
+ /// Refresh the entire display.
+ pub fn refresh_display(&mut self) -> Result<(), SPI::Error> {
+ self.begin_refresh_display()?;
+ drop(self.wait_ready(CS_ALL));
+ Ok(())
+ }
+
+ /// Asynchronous version of [`refresh_display`](EpdDriver::refresh_display).
+ /// Use [`is_busy`](EpdDriver::is_busy) to poll for completion.
+ pub fn begin_refresh_display(&mut self) -> Result<(), SPI::Error> {
+ self.cmd(CS_ALL, Command::PowerOn)?;
+ drop(self.wait_ready(CS_ALL));
+ // Appears to be required to reliably trigger display refresh after a power-on.
+ self.delay.delay_ms(100);
+
+ self.cmd(CS_ALL, Command::DisplayRefresh)?;
+
+ self.flush()
+ }
+
+ /// Refresh the specified sub-window of the display.
+ ///
+ /// Technically, this works, however, after 2+ partial updates, the rest of the displayed image becomes visibly degraded.
+ pub fn refresh_display_partial(&mut self, window: Rect) -> Result<(), SPI::Error> {
+ self.begin_refresh_display_partial(window)?;
+
+ drop(self.wait_ready(CS_ALL));
+ Ok(())
+ }
+
+ /// Asynchronous version of [`refresh_display_partial`](EpdDriver::refresh_display_partial).
+ /// Use [`is_busy`](EpdDriver::is_busy) to poll for completion.
+ pub fn begin_refresh_display_partial(&mut self, window: Rect) -> Result<(), SPI::Error> {
+ self.setup_partial_windows(window)?;
+
+ self.cmd(CS_ALL, Command::PowerOn)?;
+ drop(self.wait_ready(CS_ALL));
+ self.delay.delay_ms(100);
+
+ self.cmd(CS_ALL, Command::PartialIn)?;
+ self.cmd(CS_ALL, Command::DisplayRefresh)?;
+ self.cmd(CS_ALL, Command::PartialOut)?;
+
+ self.flush()
+ }
+
+ /// Turn off booster, controller, source driver, gate driver, VCOM, and temperature sensor.
+ /// However, the contents of the data memory buffers will be retained.
+ pub fn power_off(&mut self) -> Result<(), SPI::Error> {
+ self.cmd(CS_ALL, Command::PowerOff)?;
+ drop(self.wait_ready(CS_ALL));
+
+ self.flush()
+ }
+
+ /// Put display into deep sleep. Only [`reset()`](EpdDriver::reset) can bring it out of this state.
+ /// The contents of the data memory buffers will be lost.
+ pub fn hibernate(&mut self) -> Result<(), SPI::Error> {
+ self.cmd(CS_ALL, Command::PowerOff)?;
+ drop(self.wait_ready(CS_ALL));
+
+ self.cmd_with_data(CS_ALL, Command::DeepSleep, &[0xA5])?;
+
+ self.flush()
+ }
+
+ fn setup_partial_windows(&mut self, window: Rect) -> Result<(), SPI::Error> {
+ let s2_part = window.intersect(S2_RECT).sub_offset(S2_RECT.x, S2_RECT.y);
+ let m2_part = window.intersect(M2_RECT).sub_offset(M2_RECT.x, M2_RECT.y);
+ let m1_part = window.intersect(M1_RECT).sub_offset(M1_RECT.x, M1_RECT.y);
+ let s1_part = window.intersect(S1_RECT).sub_offset(S1_RECT.x, S1_RECT.y);
+
+ fn partial_window_data(window: Rect, reverse_scan: Option) -> [u8; 9] {
+ if window.is_empty() {
+ [0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x01]
+ } else {
+ let start_x = match reverse_scan {
+ Some(width) => width - window.x - window.w,
+ None => window.x,
+ };
+ let end_x = start_x + window.w - 1;
+ let start_y = window.y;
+ let end_y = start_y + window.h - 1;
+ [
+ (start_x / 256) as u8,
+ (start_x % 256) as u8,
+ (end_x / 256) as u8,
+ (end_x % 256) as u8,
+ (start_y / 256) as u8,
+ (start_y % 256) as u8,
+ (end_y / 256) as u8,
+ (end_y % 256) as u8,
+ 0x01,
+ ]
+ }
+ }
+
+ self.cmd_with_data(
+ CS_S2,
+ Command::PartialWindow,
+ &partial_window_data(s2_part, Some(S2_RECT.w)),
+ )?;
+ self.cmd_with_data(
+ CS_M2,
+ Command::PartialWindow,
+ &partial_window_data(m2_part, Some(M2_RECT.w)),
+ )?;
+ self.cmd_with_data(
+ CS_M1,
+ Command::PartialWindow,
+ &partial_window_data(m1_part, None),
+ )?;
+ self.cmd_with_data(
+ CS_S1,
+ Command::PartialWindow,
+ &partial_window_data(s1_part, None),
+ )?;
+
+ Ok(())
+ }
+
+ fn write_partial(
+ &mut self,
+ transmission_cmd: Command,
+ window: Rect,
+ pixels: &[u8],
+ ) -> Result<(), SPI::Error> {
+ if window.x % 8 != 0 || window.w % 8 != 0 {
+ panic!("Window is not 8-aligned horizontally");
+ }
+
+ self.cmd(CS_ALL, Command::PartialIn)?;
+
+ self.setup_partial_windows(window)?;
+ self.write_window_data(transmission_cmd, window, pixels)?;
+
+ self.cmd(CS_ALL, Command::PartialOut)
+ }
+
+ // Send data to each sub-display for the window area that overlaps with it.
+ fn write_window_data(
+ &mut self,
+ transmission_cmd: Command,
+ window: Rect,
+ pixels: &[u8],
+ ) -> Result<(), SPI::Error> {
+ assert!(!pixels.is_empty());
+
+ let s2_part = window.intersect(S2_RECT);
+ let s1_part = window.intersect(S1_RECT);
+
+ let top_rows = s2_part.h as usize;
+ let bottom_rows = s1_part.h as usize;
+ let left_bytes = (s2_part.w / 8) as usize;
+ let right_bytes = (s1_part.w / 8) as usize;
+
+ let row_offset = |row| {
+ let offset = row * (left_bytes + right_bytes);
+ if offset < pixels.len() {
+ offset
+ } else {
+ // Wrap around
+ offset % pixels.len()
+ }
+ };
+
+ if top_rows > 0 {
+ if left_bytes > 0 {
+ self.cmd(CS_S2, transmission_cmd)?;
+ for y in 0..top_rows {
+ let begin = row_offset(y);
+ let end = begin + left_bytes;
+ self.spi_write(CS_S2 | CS_DATA, &pixels[begin..end])?;
+ }
+ }
+
+ if right_bytes > 0 {
+ self.cmd(CS_M2, transmission_cmd)?;
+ for y in 0..top_rows {
+ let begin = row_offset(y) + left_bytes;
+ let end = begin + right_bytes;
+ self.spi_write(CS_M2 | CS_DATA, &pixels[begin..end])?;
+ }
+ }
+ }
+
+ if bottom_rows > 0 {
+ if left_bytes > 0 {
+ self.cmd(CS_M1, transmission_cmd)?;
+ for y in 0..bottom_rows {
+ let begin = row_offset(top_rows + y);
+ let end = begin + left_bytes;
+ self.spi_write(CS_M1 | CS_DATA, &pixels[begin..end])?;
+ }
+ }
+
+ if right_bytes > 0 {
+ self.cmd(CS_S1, transmission_cmd)?;
+ for y in 0..bottom_rows {
+ let begin = row_offset(top_rows + y) + left_bytes;
+ let end = begin + right_bytes;
+ self.spi_write(CS_S1 | CS_DATA, &pixels[begin..end])?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ fn cmd(&mut self, chips: CS, command: Command) -> Result<(), SPI::Error> {
+ self.spi_write(chips, &[command as u8])
+ }
+
+ fn cmd_with_data(
+ &mut self,
+ chips: CS,
+ command: Command,
+ data: &[u8],
+ ) -> Result<(), SPI::Error> {
+ self.spi_write(chips, &[command as u8])?;
+ self.spi_write(chips | CS_DATA, data)
+ }
+
+ // Set control pins to the specified state, then send data via SPI.
+ fn spi_write(&mut self, control: CS, data: &[u8]) -> Result<(), SPI::Error> {
+ if self.control_state != control {
+ fn pin_state(high: bool) -> PinState {
+ if high {
+ PinState::High
+ } else {
+ PinState::Low
+ }
+ }
+
+ self.peris.spi.flush()?;
+ self.delay.delay_ns(100); // Tscc = 20ns, Tchw = 40ns
+
+ // CS is active low
+ drop(self.peris.m1_cs.set_state(pin_state(control & CS_M1 == 0)));
+ drop(self.peris.s1_cs.set_state(pin_state(control & CS_S1 == 0)));
+ drop(self.peris.m2_cs.set_state(pin_state(control & CS_M2 == 0)));
+ drop(self.peris.s2_cs.set_state(pin_state(control & CS_S2 == 0)));
+
+ // DC is active high
+ let dc = pin_state(control & CS_DATA != 0);
+ drop(self.peris.m1s1_dc.set_state(dc));
+ drop(self.peris.m2s2_dc.set_state(dc));
+
+ self.delay.delay_ns(100); // Tcss = 60ns, Tsds = 30ns
+ self.control_state = control;
+ }
+
+ self.peris.spi.write(data)
+ }
+
+ // Flush SPI, reset control pins to the default state.
+ fn flush(&mut self) -> Result<(), SPI::Error> {
+ self.peris.spi.flush()?;
+ drop(self.peris.m1_cs.set_high());
+ drop(self.peris.s1_cs.set_high());
+ drop(self.peris.m2_cs.set_high());
+ drop(self.peris.s2_cs.set_high());
+ drop(self.peris.m1s1_dc.set_low());
+ drop(self.peris.m2s2_dc.set_low());
+ self.control_state = 0;
+ Ok(())
+ }
+
+ fn wait_ready(&mut self, chips: CS) -> Result<(), INPUT::Error> {
+ while self.busy_chips(chips)? != 0 {
+ self.delay.delay_ms(200);
+ }
+ Ok(())
+ }
+
+ fn busy_chips(&mut self, chips: CS) -> Result {
+ let mut busy = 0;
+ if chips & CS_M1 != 0 {
+ if self.peris.m1_busy.is_low()? {
+ busy |= CS_M1;
+ }
+ }
+ if chips & CS_S1 != 0 {
+ if self.peris.s1_busy.is_low()? {
+ busy |= CS_S1;
+ }
+ }
+ if chips & CS_M2 != 0 {
+ if self.peris.m2_busy.is_low()? {
+ busy |= CS_M2;
+ }
+ }
+ if chips & CS_S2 != 0 {
+ if self.peris.s2_busy.is_low()? {
+ busy |= CS_S2;
+ }
+ }
+ Ok(busy)
+ }
+
+ /// Poll readiness status of all sub-displays and return a bit mask of the busy ones.
+ pub fn get_busy(&mut self) -> u8 {
+ self.busy_chips(CS_ALL).unwrap()
+ }
+
+ /// Check if any of the sub-displays is busy.
+ pub fn is_busy(&mut self) -> bool {
+ self.busy_chips(CS_ALL).unwrap() != 0
+ }
+
+ /// Query and return the status byte of each sub-display.
+ /// Order: \[M1, S1, M2, S2\].
+ pub fn get_status(&mut self) -> Result<[u8; 4], SPI::Error> {
+ self.control_state = 0xFF;
+ let mut status = [0u8; 4];
+ for i in 0..4 {
+ let (cs, dc) = match i {
+ 0 => (&mut self.peris.m1_cs, &mut self.peris.m1s1_dc),
+ 1 => (&mut self.peris.s1_cs, &mut self.peris.m1s1_dc),
+ 2 => (&mut self.peris.m2_cs, &mut self.peris.m2s2_dc),
+ _ => (&mut self.peris.s2_cs, &mut self.peris.m2s2_dc),
+ };
+ // Request status
+ drop(cs.set_low());
+ drop(dc.set_low());
+ self.delay.delay_ns(100); // Tcss = 60ns
+ self.peris.spi.write(&[Command::GetStatus as u8])?;
+ self.peris.spi.flush()?;
+ self.delay.delay_ns(100); // Tsds = 30ns
+
+ // Read status
+ drop(dc.set_high());
+ self.delay.delay_ns(100); // Tsdh = 30ns
+ self.peris.spi.read(&mut status[i..i + 1])?;
+ self.delay.delay_ns(100); // Tscc = 20ns
+ drop(dc.set_low());
+
+ drop(cs.set_high());
+ self.delay.delay_ns(100); // Tchw = 40ns
+ }
+ self.control_state = 0;
+ Ok(status)
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 5ff4754c..ce50f7a4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -70,6 +70,8 @@ mod traits;
pub mod color;
+pub mod rect;
+
/// Interface for the physical connection between display and the controlling device
mod interface;
@@ -100,6 +102,7 @@ pub mod epd7in5_hd;
pub mod epd7in5_v2;
pub mod epd7in5b_v2;
pub use epd7in5b_v2 as epd7in5b_v3;
+pub mod epd12in48b_v2;
pub(crate) mod type_a;
diff --git a/src/rect.rs b/src/rect.rs
new file mode 100644
index 00000000..4fa2d02e
--- /dev/null
+++ b/src/rect.rs
@@ -0,0 +1,87 @@
+//! Rectangle operations for bigger displays with multiple _windows_
+use core::cmp;
+
+/// A rectangle
+#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
+pub struct Rect {
+ /// Origin X
+ pub x: u32,
+ /// Origin Y
+ pub y: u32,
+ /// Width
+ pub w: u32,
+ /// Height
+ pub h: u32,
+}
+
+impl Rect {
+ /// Construct a new rectangle
+ pub const fn new(x: u32, y: u32, w: u32, h: u32) -> Rect {
+ Rect { x, y, w, h }
+ }
+ /// Compute intersection with another rectangle
+ pub fn intersect(&self, other: Rect) -> Rect {
+ let x = cmp::max(self.x, other.x);
+ let y = cmp::max(self.y, other.y);
+ let w = cmp::min(self.x + self.w, other.x + other.w).saturating_sub(x);
+ let h = cmp::min(self.y + self.h, other.y + other.h).saturating_sub(y);
+ Rect { x, y, w, h }
+ }
+ /// Move rectangle by (-dx,-dy)
+ pub fn sub_offset(&self, dx: u32, dy: u32) -> Rect {
+ Rect {
+ x: self.x - dx,
+ y: self.y - dy,
+ w: self.w,
+ h: self.h,
+ }
+ }
+ /// Test whether the rectangle is empty.
+ pub fn is_empty(&self) -> bool {
+ self.w == 0 || self.h == 0
+ }
+}
+
+#[test]
+fn test_intersect() {
+ let r1 = Rect::new(0, 0, 10, 10);
+ let r2 = Rect::new(6, 3, 10, 10);
+ let r3 = r1.intersect(r2);
+ assert!(matches!(
+ r3,
+ Rect {
+ x: 6,
+ y: 3,
+ w: 4,
+ h: 7
+ }
+ ));
+
+ let r1 = Rect::new(0, 0, 10, 10);
+ let r2 = Rect::new(10, 11, 10, 10);
+ let r3 = r1.intersect(r2);
+ assert!(matches!(
+ r3,
+ Rect {
+ x: _,
+ y: _,
+ w: 0,
+ h: 0
+ }
+ ));
+}
+
+#[test]
+fn sub_offset() {
+ let r1 = Rect::new(10, 10, 10, 10);
+ let r2 = r1.sub_offset(10, 5);
+ assert!(matches!(
+ r2,
+ Rect {
+ x: 0,
+ y: 5,
+ w: 10,
+ h: 10
+ }
+ ));
+}