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 + } + )); +}