From 85408f0d03cf678a6dbf5c4b439b3fe15c34bcf4 Mon Sep 17 00:00:00 2001 From: Tommy Gilligan <7865781+tommy-gilligan@users.noreply.github.com> Date: Fri, 9 Feb 2024 00:08:40 +1100 Subject: [PATCH] Add support for EPD 2in13b V4 Amalgamation of waveshare/e-Paper code, datasheet PDF reference code and existing 2in13v2 driver --- src/epd2in13b_v4/command.rs | 148 +++++++++++ src/epd2in13b_v4/mod.rs | 517 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 666 insertions(+) create mode 100644 src/epd2in13b_v4/command.rs create mode 100644 src/epd2in13b_v4/mod.rs diff --git a/src/epd2in13b_v4/command.rs b/src/epd2in13b_v4/command.rs new file mode 100644 index 00000000..0c879de1 --- /dev/null +++ b/src/epd2in13b_v4/command.rs @@ -0,0 +1,148 @@ +//! SPI Commands for the Waveshare 2.13"B V4 E-Ink Display + +use crate::traits; + +extern crate bit_field; +use bit_field::BitField; + +/// Epd2in13 v4 +/// +/// For more infos about the addresses and what they are doing look into the pdfs +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + DriverOutputControl = 0x01, + GateDrivingVoltageCtrl = 0x03, + SourceDrivingVoltageCtrl = 0x04, + DeepSleepMode = 0x10, + DataEntryModeSetting = 0x11, + SwReset = 0x12, + TemperatureSensorRead = 0x18, + MasterActivation = 0x20, + DisplayUpdateControl1 = 0x21, + DisplayUpdateControl2 = 0x22, + WriteRam = 0x24, + WriteRamRed = 0x26, + WriteVcomRegister = 0x2C, + StatusBitRead = 0x2F, + WriteLutRegister = 0x32, + BorderWaveformControl = 0x3C, + SetRamXAddressStartEndPosition = 0x44, + SetRamYAddressStartEndPosition = 0x45, + SetRamXAddressCounter = 0x4E, + SetRamYAddressCounter = 0x4F, +} + +pub(crate) struct DriverOutput { + pub scan_is_linear: bool, + pub scan_g0_is_first: bool, + pub scan_dir_incr: bool, + pub width: u16, +} + +impl DriverOutput { + pub fn to_bytes(&self) -> [u8; 3] { + [ + self.width as u8, + (self.width >> 8) as u8, + *0u8.set_bit(0, !self.scan_dir_incr) + .set_bit(1, !self.scan_g0_is_first) + .set_bit(2, !self.scan_is_linear), + ] + } +} + +#[allow(dead_code, clippy::enum_variant_names)] +#[derive(Copy, Clone)] +pub(crate) enum RamOption { + Normal = 0x0, + BypassAs0 = 0x2, + Inverse = 0x4, +} + +pub(crate) struct DisplayUpdateControl { + pub red_ram_option: RamOption, + pub bw_ram_option: RamOption, + pub source_output_mode: bool, +} + +impl DisplayUpdateControl { + pub fn to_bytes(&self) -> [u8; 2] { + [ + ((self.red_ram_option as u8) << 4) | (self.bw_ram_option as u8), + if self.source_output_mode { 128 } else { 0 }, + ] + } +} + +#[allow(dead_code, clippy::enum_variant_names)] +pub(crate) enum DataEntryModeIncr { + XDecrYDecr = 0x0, + XIncrYDecr = 0x1, + XDecrYIncr = 0x2, + XIncrYIncr = 0x3, +} + +#[allow(dead_code)] +pub(crate) enum DataEntryModeDir { + XDir = 0x0, + YDir = 0x4, +} + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum BorderWaveFormVbd { + Gs = 0x0, + FixLevel = 0x1, + Vcom = 0x2, +} + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum BorderWaveFormFixLevel { + Vss = 0x0, + Vsh1 = 0x1, + Vsl = 0x2, + Vsh2 = 0x3, +} + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum BorderWaveFormGs { + Lut0 = 0x0, + Lut1 = 0x1, + Lut2 = 0x2, + Lut3 = 0x3, +} + +pub(crate) struct BorderWaveForm { + pub vbd: BorderWaveFormVbd, + pub fix_level: BorderWaveFormFixLevel, + pub gs_trans: BorderWaveFormGs, +} + +impl BorderWaveForm { + pub fn to_u8(&self) -> u8 { + *0u8.set_bits(6..8, self.vbd as u8) + .set_bits(4..6, self.fix_level as u8) + .set_bits(0..2, self.gs_trans as u8) + } +} + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub enum DeepSleepMode { + // Sleeps and keeps access to RAM and controller + Normal = 0x00, + // Sleeps without access to RAM/controller but keeps RAM content + Mode1 = 0x01, + // Same as MODE_1 but RAM content is not kept + Mode2 = 0x11, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} diff --git a/src/epd2in13b_v4/mod.rs b/src/epd2in13b_v4/mod.rs new file mode 100644 index 00000000..f9513848 --- /dev/null +++ b/src/epd2in13b_v4/mod.rs @@ -0,0 +1,517 @@ +//! A simple Drlever for the Waveshare 2.13" B V4 E-Ink Display via SPI +//! More information on this display can be found at the [Waveshare Wiki](https://www.waveshare.com/wiki/Pico-ePaper-2.13-B) +//! This driver was build and tested for 250x122, 2.13inch E-Ink display HAT for Raspberry Pi, three-color, SPI interface +//! +//! # Example for the 2.13" B V4 E-Ink Display +//! +//!```rust, no_run +//!# use embedded_hal_mock::*; +//!# fn main() -> Result<(), MockError> { +//!use embedded_graphics::{prelude::*, primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder}}; +//!use epd_waveshare::{epd2in13b_v4::*, prelude::*}; +//!# +//!# let expectations = []; +//!# let mut spi = spi::Mock::new(&expectations); +//!# let expectations = []; +//!# let cs_pin = pin::Mock::new(&expectations); +//!# let busy_in = pin::Mock::new(&expectations); +//!# let dc = pin::Mock::new(&expectations); +//!# let rst = pin::Mock::new(&expectations); +//!# let mut delay = delay::MockNoop::new(); +//! +//!// Setup EPD +//!let mut epd = Epd2in13b::new(&mut spi, cs_pin, busy_in, dc, rst, &mut delay, None)?; +//! +//!// Use display graphics from embedded-graphics +//!// This display is for the black/white/chromatic pixels +//!let mut tricolor_display = Display2in13b::default(); +//! +//!// Use embedded graphics for drawing a black line +//!let _ = Line::new(Point::new(0, 120), Point::new(0, 200)) +//! .into_styled(PrimitiveStyle::with_stroke(TriColor::Black, 1)) +//! .draw(&mut tricolor_display); +//! +//!// We use `chromatic` but it will be shown as red/yellow +//!let _ = Line::new(Point::new(15, 120), Point::new(15, 200)) +//! .into_styled(PrimitiveStyle::with_stroke(TriColor::Chromatic, 1)) +//! .draw(&mut tricolor_display); +//! +//!// Display updated frame +//!epd.update_color_frame( +//! &mut spi, +//! &mut delay, +//! &tricolor_display.bw_buffer(), +//! &tricolor_display.chromatic_buffer() +//!)?; +//!epd.display_frame(&mut spi, &mut delay)?; +//! +//!// Set the EPD to sleep +//!epd.sleep(&mut spi, &mut delay)?; +//!# Ok(()) +//!# } +//!``` +// Original Waveforms from Waveshare +use embedded_hal::{ + blocking::{delay::*, spi::Write}, + digital::v2::{InputPin, OutputPin}, +}; + +use crate::buffer_len; +use crate::color::TriColor; +use crate::interface::DisplayInterface; +use crate::traits::{ + InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, +}; + +pub(crate) mod command; +use self::command::{ + BorderWaveForm, BorderWaveFormFixLevel, BorderWaveFormGs, BorderWaveFormVbd, Command, + DataEntryModeDir, DataEntryModeIncr, DeepSleepMode, DisplayUpdateControl, DriverOutput, + RamOption, +}; + +/// Full size buffer for use with the 2.13" v4 EPD +#[cfg(feature = "graphics")] +pub type Display2in13b = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) * 2 }, + TriColor, +>; + +/// Width of the display. +pub const WIDTH: u32 = 122; + +/// Height of the display +pub const HEIGHT: u32 = 250; + +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: TriColor = TriColor::White; +const IS_BUSY_LOW: bool = false; + +/// Epd2in13b (V4) driver +pub struct Epd2in13b { + /// Connection Interface + interface: DisplayInterface, + + /// Background Color + background_color: TriColor, +} + +impl InternalWiAdditions + for Epd2in13b +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayUs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // HW reset + self.interface.reset(delay, 10_000, 10_000); + + self.wait_until_idle(spi, delay)?; + self.interface.cmd(spi, Command::SwReset)?; + self.wait_until_idle(spi, delay)?; + + self.set_driver_output( + spi, + DriverOutput { + scan_is_linear: true, + scan_g0_is_first: true, + scan_dir_incr: true, + width: (HEIGHT - 1) as u16, + }, + )?; + + self.set_data_entry_mode(spi, DataEntryModeIncr::XIncrYIncr, DataEntryModeDir::XDir)?; + + // Use simple X/Y auto increase + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + self.set_ram_address_counters(spi, delay, 0, 0)?; + + self.set_border_waveform( + spi, + command::BorderWaveForm { + vbd: BorderWaveFormVbd::Gs, + fix_level: BorderWaveFormFixLevel::Vss, + gs_trans: BorderWaveFormGs::Lut3, + }, + )?; + + self.cmd_with_data(spi, Command::WriteVcomRegister, &[0x36])?; + self.cmd_with_data(spi, Command::GateDrivingVoltageCtrl, &[0x17])?; + self.cmd_with_data(spi, Command::SourceDrivingVoltageCtrl, &[0x41, 0x00, 0x32])?; + + self.set_display_update_control( + spi, + command::DisplayUpdateControl { + red_ram_option: RamOption::Normal, + bw_ram_option: RamOption::Normal, + source_output_mode: false, + }, + )?; + + self.wait_until_idle(spi, delay)?; + + Ok(()) + } +} + +impl WaveshareThreeColorDisplay + for Epd2in13b +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayUs, +{ + 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) + } + + fn update_achromatic_frame( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + black: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::WriteRam)?; + self.interface.data(spi, black)?; + Ok(()) + } + + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::WriteRamRed)?; + self.interface.data(spi, chromatic)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd2in13b +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayUs, +{ + type DisplayColor = TriColor; + fn new( + spi: &mut SPI, + cs: CS, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let mut epd = Epd2in13b { + interface: DisplayInterface::new(cs, busy, dc, rst, delay_us), + background_color: DEFAULT_BACKGROUND_COLOR, + }; + + epd.init(spi, delay)?; + Ok(epd) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + fn sleep(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { + self.set_sleep_mode(spi, DeepSleepMode::Normal)?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + _delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + assert!(buffer.len() == buffer_len(WIDTH as usize, HEIGHT as usize)); + self.cmd_with_data(spi, Command::WriteRam, buffer)?; + + self.command(spi, Command::WriteRamRed)?; + self.interface.data_x_times( + spi, + TriColor::Black.get_byte_value(), + buffer_len(WIDTH as usize, HEIGHT as usize) as u32, + )?; + Ok(()) + } + + 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!(); + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.command(spi, Command::MasterActivation)?; + 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.clear_achromatic_frame(spi)?; + self.clear_chromatic_frame(spi) + } + + fn set_background_color(&mut self, background_color: TriColor) { + self.background_color = background_color; + } + + fn background_color(&self) -> &TriColor { + &self.background_color + } + + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + fn set_lut( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + _refresh_rate: Option, + ) -> Result<(), SPI::Error> { + unimplemented!() + } + + 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 Epd2in13b +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayUs, +{ + fn set_display_update_control( + &mut self, + spi: &mut SPI, + display_update_control: DisplayUpdateControl, + ) -> Result<(), SPI::Error> { + self.cmd_with_data( + spi, + Command::DisplayUpdateControl1, + &display_update_control.to_bytes(), + ) + } + + fn set_border_waveform( + &mut self, + spi: &mut SPI, + borderwaveform: BorderWaveForm, + ) -> Result<(), SPI::Error> { + self.cmd_with_data( + spi, + Command::BorderWaveformControl, + &[borderwaveform.to_u8()], + ) + } + + /// Triggers the deep sleep mode + fn set_sleep_mode(&mut self, spi: &mut SPI, mode: DeepSleepMode) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::DeepSleepMode, &[mode as u8]) + } + + fn set_driver_output(&mut self, spi: &mut SPI, output: DriverOutput) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::DriverOutputControl, &output.to_bytes()) + } + + /// Sets the data entry mode (ie. how X and Y positions changes when writing + /// data to RAM) + fn set_data_entry_mode( + &mut self, + spi: &mut SPI, + counter_incr_mode: DataEntryModeIncr, + counter_direction: DataEntryModeDir, + ) -> Result<(), SPI::Error> { + let mode = counter_incr_mode as u8 | counter_direction as u8; + self.cmd_with_data(spi, Command::DataEntryModeSetting, &[mode]) + } + + /// Sets both X and Y pixels ranges + fn set_ram_area( + &mut self, + spi: &mut SPI, + start_x: u32, + start_y: u32, + end_x: u32, + end_y: u32, + ) -> Result<(), SPI::Error> { + self.cmd_with_data( + spi, + Command::SetRamXAddressStartEndPosition, + &[(start_x >> 3) as u8, (end_x >> 3) as u8], + )?; + + self.cmd_with_data( + spi, + Command::SetRamYAddressStartEndPosition, + &[ + start_y as u8, + (start_y >> 8) as u8, + end_y as u8, + (end_y >> 8) as u8, + ], + ) + } + + /// Sets both X and Y pixels counters when writing data to RAM + fn set_ram_address_counters( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + x: u32, + y: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::SetRamXAddressCounter, &[(x >> 3) as u8])?; + + self.cmd_with_data( + spi, + Command::SetRamYAddressCounter, + &[y as u8, (y >> 8) as u8], + )?; + Ok(()) + } + + 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) + } + + fn clear_achromatic_frame(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + match self.background_color { + TriColor::White => { + self.command(spi, Command::WriteRam)?; + self.interface.data_x_times( + spi, + 0xFF, + buffer_len(WIDTH as usize, HEIGHT as usize) as u32, + )?; + } + TriColor::Chromatic => { + self.command(spi, Command::WriteRam)?; + self.interface.data_x_times( + spi, + 0xFF, + buffer_len(WIDTH as usize, HEIGHT as usize) as u32, + )?; + } + TriColor::Black => { + self.command(spi, Command::WriteRam)?; + self.interface.data_x_times( + spi, + 0x00, + buffer_len(WIDTH as usize, HEIGHT as usize) as u32, + )?; + } + } + + Ok(()) + } + + fn clear_chromatic_frame(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + match self.background_color { + TriColor::White => { + self.command(spi, Command::WriteRam)?; + self.interface.data_x_times( + spi, + 0x00, + buffer_len(WIDTH as usize, HEIGHT as usize) as u32, + )?; + } + TriColor::Chromatic => { + self.command(spi, Command::WriteRam)?; + self.interface.data_x_times( + spi, + 0xFF, + buffer_len(WIDTH as usize, HEIGHT as usize) as u32, + )?; + } + TriColor::Black => { + self.command(spi, Command::WriteRam)?; + self.interface.data_x_times( + spi, + 0x00, + buffer_len(WIDTH as usize, HEIGHT as usize) as u32, + )?; + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 122); + assert_eq!(HEIGHT, 250); + assert_eq!(DEFAULT_BACKGROUND_COLOR, TriColor::White); + } +} diff --git a/src/lib.rs b/src/lib.rs index ce50f7a4..3ef7291a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,6 +81,7 @@ pub mod epd1in54_v2; pub mod epd1in54b; pub mod epd1in54c; pub mod epd2in13_v2; +pub mod epd2in13b_v4; pub mod epd2in13bc; pub mod epd2in66b; pub mod epd2in7;