diff --git a/Cargo.toml b/Cargo.toml index 474e4c2..fb127c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ edition = "2018" [dependencies] embedded-graphics = { version = "0.6.1", optional = true} embedded-hal = {version = "0.2.3", features = ["unproven"]} +bit_field = "0.10.1" [dev-dependencies] linux-embedded-hal = "0.3" diff --git a/examples/epd2in13_v2.rs b/examples/epd2in13_v2.rs new file mode 100644 index 0000000..ac509c3 --- /dev/null +++ b/examples/epd2in13_v2.rs @@ -0,0 +1,167 @@ +#![deny(warnings)] + +use embedded_graphics::{ + fonts::{Font12x16, Font6x8, Text}, + prelude::*, + primitives::{Circle, Line}, + style::PrimitiveStyle, + text_style, +}; +use embedded_hal::prelude::*; +use epd_waveshare::{ + color::*, + epd2in13_v2::{Display2in13, EPD2in13}, + graphics::{Display, DisplayRotation}, + prelude::*, +}; +use linux_embedded_hal::{ + spidev::{self, SpidevOptions}, + sysfs_gpio::Direction, + Delay, Pin, Spidev, +}; + +// activate spi, gpio in raspi-config +// needs to be run with sudo because of some sysfs_gpio permission problems and follow-up timing problems +// see https://github.com/rust-embedded/rust-sysfs-gpio/issues/5 and follow-up issues + +fn main() -> Result<(), std::io::Error> { + // Configure SPI + // Settings are taken from + let mut spi = Spidev::open("/dev/spidev0.0").expect("spidev directory"); + let options = SpidevOptions::new() + .bits_per_word(8) + .max_speed_hz(4_000_000) + .mode(spidev::SpiModeFlags::SPI_MODE_0) + .build(); + spi.configure(&options).expect("spi configuration"); + + // Configure Digital I/O Pin to be used as Chip Select for SPI + let cs = Pin::new(26); //BCM7 CE0 + cs.export().expect("cs export"); + while !cs.is_exported() {} + cs.set_direction(Direction::Out).expect("CS Direction"); + cs.set_value(1).expect("CS Value set to 1"); + + let busy = Pin::new(5); //pin 29 + busy.export().expect("busy export"); + while !busy.is_exported() {} + busy.set_direction(Direction::In).expect("busy Direction"); + //busy.set_value(1).expect("busy Value set to 1"); + + let dc = Pin::new(6); //pin 31 //bcm6 + dc.export().expect("dc export"); + while !dc.is_exported() {} + dc.set_direction(Direction::Out).expect("dc Direction"); + dc.set_value(1).expect("dc Value set to 1"); + + let rst = Pin::new(16); //pin 36 //bcm16 + rst.export().expect("rst export"); + while !rst.is_exported() {} + rst.set_direction(Direction::Out).expect("rst Direction"); + rst.set_value(1).expect("rst Value set to 1"); + + let mut delay = Delay {}; + + let mut epd2in13 = + EPD2in13::new(&mut spi, cs, busy, dc, rst, &mut delay).expect("eink initalize error"); + + //println!("Test all the rotations"); + let mut display = Display2in13::default(); + + display.set_rotation(DisplayRotation::Rotate0); + draw_text(&mut display, "Rotate 0!", 5, 50); + + display.set_rotation(DisplayRotation::Rotate90); + draw_text(&mut display, "Rotate 90!", 5, 50); + + display.set_rotation(DisplayRotation::Rotate180); + draw_text(&mut display, "Rotate 180!", 5, 50); + + display.set_rotation(DisplayRotation::Rotate270); + draw_text(&mut display, "Rotate 270!", 5, 50); + + epd2in13.update_frame(&mut spi, &display.buffer())?; + epd2in13 + .display_frame(&mut spi) + .expect("display frame new graphics"); + delay.delay_ms(5000u16); + + //println!("Now test new graphics with default rotation and some special stuff:"); + display.clear_buffer(Color::White); + + // draw a analog clock + let _ = Circle::new(Point::new(64, 64), 40) + .into_styled(PrimitiveStyle::with_stroke(Black, 1)) + .draw(&mut display); + let _ = Line::new(Point::new(64, 64), Point::new(30, 40)) + .into_styled(PrimitiveStyle::with_stroke(Black, 4)) + .draw(&mut display); + let _ = Line::new(Point::new(64, 64), Point::new(80, 40)) + .into_styled(PrimitiveStyle::with_stroke(Black, 1)) + .draw(&mut display); + + // draw white on black background + let _ = Text::new("It's working-WoB!", Point::new(90, 10)) + .into_styled(text_style!( + font = Font6x8, + text_color = White, + background_color = Black + )) + .draw(&mut display); + + // use bigger/different font + let _ = Text::new("It's working-WoB!", Point::new(90, 40)) + .into_styled(text_style!( + font = Font12x16, + text_color = White, + background_color = Black + )) + .draw(&mut display); + + // Demonstrating how to use the partial refresh feature of the screen. + // Real animations can be used. + epd2in13 + .set_refresh(&mut spi, &mut delay, RefreshLUT::QUICK) + .unwrap(); + epd2in13.clear_frame(&mut spi).unwrap(); + + // a moving `Hello World!` + let limit = 10; + for i in 0..limit { + draw_text(&mut display, " Hello World! ", 5 + i * 12, 50); + + epd2in13 + .update_and_display_frame(&mut spi, &display.buffer()) + .expect("display frame new graphics"); + delay.delay_ms(1_000u16); + } + + // Show a spinning bar without any delay between frames. Shows how «fast» + // the screen can refresh for this kind of change (small single character) + display.clear_buffer(Color::White); + epd2in13 + .update_and_display_frame(&mut spi, &display.buffer()) + .unwrap(); + + let spinner = ["|", "/", "-", "\\"]; + for i in 0..10 { + display.clear_buffer(Color::White); + draw_text(&mut display, spinner[i % spinner.len()], 10, 100); + epd2in13 + .update_and_display_frame(&mut spi, &display.buffer()) + .unwrap(); + } + + println!("Finished tests - going to sleep"); + epd2in13.sleep(&mut spi) +} + +fn draw_text(display: &mut Display2in13, text: &str, x: i32, y: i32) { + let _ = Text::new(text, Point::new(x, y)) + .into_styled(text_style!( + font = Font6x8, + text_color = Black, + background_color = White + )) + .draw(display); +} diff --git a/src/epd2in13_v2/command.rs b/src/epd2in13_v2/command.rs new file mode 100644 index 0000000..b7e1986 --- /dev/null +++ b/src/epd2in13_v2/command.rs @@ -0,0 +1,287 @@ +//! SPI Commands for the Waveshare 2.13" v2 + +use crate::traits; + +extern crate bit_field; +use bit_field::BitField; + +/// EPD2in13 v2 +/// +/// For more infos about the addresses and what they are doing look into the pdfs +#[allow(dead_code)] +#[allow(non_camel_case_types)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + DRIVER_OUTPUT_CONTROL = 0x01, + GATE_DRIVING_VOLTAGE_CTRL = 0x03, + SOURCE_DRIVING_VOLTAGE_CTRL = 0x04, + BOOSTER_SOFT_START_CONTROL = 0x0C, + GATE_SCAN_START_POSITION = 0x0F, + DEEP_SLEEP_MODE = 0x10, + DATA_ENTRY_MODE_SETTING = 0x11, + SW_RESET = 0x12, + HV_READY_DETECTION = 0x14, + VCI_DETECTION = 0x15, + TEMPERATURE_SENSOR_CONTROL_WRITE = 0x1A, + TEMPERATURE_SENSOR_CONTROL_READ = 0x1B, + TEMPERATURE_SENSOR_EXT_CONTROL_WRITE = 0x1C, + MASTER_ACTIVATION = 0x20, + DISPLAY_UPDATE_CONTROL_1 = 0x21, + DISPLAY_UPDATE_CONTROL_2 = 0x22, + WRITE_RAM = 0x24, + WRITE_RAM_RED = 0x26, + READ_RAM = 0x27, + VCOM_SENSE = 0x28, + VCOM_SENSE_DURATION = 0x29, + PROGRAM_VCOM_OPT = 0x2A, + WRITE_VCOM_REGISTER = 0x2C, + OTP_REGISTER_READ = 0x2D, + STATUS_BIT_READ = 0x2F, + PROGRAM_WS_OTP = 0x30, + LOAD_WS_OTP = 0x31, + WRITE_LUT_REGISTER = 0x32, + PROGRAM_OTP_SELECTION = 0x36, + WRITE_OTP_SELECTION = 0x37, + SET_DUMMY_LINE_PERIOD = 0x3A, + SET_GATE_LINE_WIDTH = 0x3B, + BORDER_WAVEFORM_CONTROL = 0x3C, + READ_RAM_OPTION = 0x41, + SET_RAM_X_ADDRESS_START_END_POSITION = 0x44, + SET_RAM_Y_ADDRESS_START_END_POSITION = 0x45, + AUTO_WRITE_RED_RAM_REGULAR_PATTERN = 0x46, + AUTO_WRITE_BW_RAM_REGULAR_PATTERN = 0x47, + SET_RAM_X_ADDRESS_COUNTER = 0x4E, + SET_RAM_Y_ADDRESS_COUNTER = 0x4F, + SET_ANALOG_BLOCK_CONTROL = 0x74, + SET_DIGITAL_BLOCK_CONTROL = 0x7E, + + NOP = 0x7F, +} + +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), + ] + } +} + +/// These are not directly documented, but the bitfield is easily reversed from +/// documentation and sample code +/// [7|6|5|4|3|2|1|0] +/// | | | | | | | `--- disable clock +/// | | | | | | `----- disable analog +/// | | | | | `------- display +/// | | | | `--------- undocumented and unknown use, +/// | | | | but used in waveshare reference code +/// | | | `----------- load LUT +/// | | `------------- load temp +/// | `--------------- enable clock +/// `----------------- enable analog + +pub(crate) struct DisplayUpdateControl2(pub u8); +#[allow(dead_code)] +impl DisplayUpdateControl2 { + pub fn new() -> DisplayUpdateControl2 { + DisplayUpdateControl2(0x00) + } + + pub fn disable_clock(mut self) -> Self { + self.0.set_bit(0, true); + self + } + + pub fn disable_analog(mut self) -> Self { + self.0.set_bit(1, true); + self + } + + pub fn display(mut self) -> Self { + self.0.set_bit(2, true); + self + } + + pub fn load_lut(mut self) -> Self { + self.0.set_bit(4, true); + self + } + + pub fn load_temp(mut self) -> Self { + self.0.set_bit(5, true); + self + } + + pub fn enable_clock(mut self) -> Self { + self.0.set_bit(6, true); + self + } + + pub fn enable_analog(mut self) -> Self { + self.0.set_bit(7, true); + self + } +} + +#[allow(dead_code)] +#[allow(non_camel_case_types)] +pub(crate) enum DataEntryModeIncr { + X_DECR_Y_DECR = 0x0, + X_INCR_Y_DECR = 0x1, + X_DECR_Y_INCR = 0x2, + X_INCR_Y_INCR = 0x3, +} + +#[allow(dead_code)] +#[allow(non_camel_case_types)] +pub(crate) enum DataEntryModeDir { + X_DIR = 0x0, + Y_DIR = 0x4, +} + +#[allow(dead_code)] +#[allow(non_camel_case_types)] +#[derive(Copy, Clone)] +pub(crate) enum BorderWaveFormVBD { + GS = 0x0, + FIX_LEVEL = 0x1, + VCOM = 0x2, +} + +#[allow(dead_code)] +#[allow(non_camel_case_types)] +#[derive(Copy, Clone)] +pub(crate) enum BorderWaveFormFixLevel { + VSS = 0x0, + VSH1 = 0x1, + VSL = 0x2, + VSH2 = 0x3, +} + +#[allow(dead_code)] +#[allow(non_camel_case_types)] +#[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)] +#[allow(non_camel_case_types)] +#[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 + MODE_1 = 0x01, + + // Same as MODE_1 but RAM content is not kept + MODE_2 = 0x11, +} + +pub(crate) struct GateDrivingVoltage(pub u8); +pub(crate) struct SourceDrivingVoltage(pub u8); +pub(crate) struct VCOM(pub u8); + +pub(crate) trait I32Ext { + fn vcom(self) -> VCOM; + fn gate_driving_decivolt(self) -> GateDrivingVoltage; + fn source_driving_decivolt(self) -> SourceDrivingVoltage; +} + +impl I32Ext for i32 { + // This is really not very nice. Until I find something better, this will be + // a placeholder. + fn vcom(self) -> VCOM { + assert!(self >= -30 && self <= -2); + let u = match -self { + 2 => 0x08, + 3 => 0x0B, + 4 => 0x10, + 5 => 0x14, + 6 => 0x17, + 7 => 0x1B, + 8 => 0x20, + 9 => 0x24, + 10 => 0x28, + 11 => 0x2C, + 12 => 0x2F, + 13 => 0x34, + 14 => 0x37, + 15 => 0x3C, + 16 => 0x40, + 17 => 0x44, + 18 => 0x48, + 19 => 0x4B, + 20 => 0x50, + 21 => 0x54, + 22 => 0x58, + 23 => 0x5B, + 24 => 0x5F, + 25 => 0x64, + 26 => 0x68, + 27 => 0x6C, + 28 => 0x6F, + 29 => 0x73, + 30 => 0x78, + _ => 0, + }; + VCOM(u) + } + + fn gate_driving_decivolt(self) -> GateDrivingVoltage { + assert!(self >= 100 && self <= 210 && self % 5 == 0); + GateDrivingVoltage(((self - 100) / 5 + 0x03) as u8) + } + + fn source_driving_decivolt(self) -> SourceDrivingVoltage { + assert!( + (self >= 24 && self <= 88) + || (self >= 90 && self <= 180 && self % 5 == 0) + || (self >= -180 && self <= -90 && self % 5 == 0) + ); + + if self >= 24 && self <= 88 { + SourceDrivingVoltage(((self - 24) + 0x8E) as u8) + } else if self >= 90 && self <= 180 { + SourceDrivingVoltage(((self - 90) / 2 + 0x23) as u8) + } else { + SourceDrivingVoltage((((-self - 90) / 5) * 2 + 0x1A) as u8) + } + } +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} diff --git a/src/epd2in13_v2/constants.rs b/src/epd2in13_v2/constants.rs new file mode 100644 index 0000000..8c5ef37 --- /dev/null +++ b/src/epd2in13_v2/constants.rs @@ -0,0 +1,35 @@ +#[rustfmt::skip] + +// Original Waveforms from Waveshare +pub(crate) const LUT_FULL_UPDATE: [u8; 70] =[ + 0x80,0x60,0x40,0x00,0x00,0x00,0x00, // LUT0: BB: VS 0 ~7 + 0x10,0x60,0x20,0x00,0x00,0x00,0x00, // LUT1: BW: VS 0 ~7 + 0x80,0x60,0x40,0x00,0x00,0x00,0x00, // LUT2: WB: VS 0 ~7 + 0x10,0x60,0x20,0x00,0x00,0x00,0x00, // LUT3: WW: VS 0 ~7 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00, // LUT4: VCOM: VS 0 ~7 + + 0x03,0x03,0x00,0x00,0x02, // TP0 A~D RP0 + 0x09,0x09,0x00,0x00,0x02, // TP1 A~D RP1 + 0x03,0x03,0x00,0x00,0x02, // TP2 A~D RP2 + 0x00,0x00,0x00,0x00,0x00, // TP3 A~D RP3 + 0x00,0x00,0x00,0x00,0x00, // TP4 A~D RP4 + 0x00,0x00,0x00,0x00,0x00, // TP5 A~D RP5 + 0x00,0x00,0x00,0x00,0x00, // TP6 A~D RP6 +]; + +#[rustfmt::skip] +pub(crate) const LUT_PARTIAL_UPDATE: [u8; 70] =[ + 0x00,0x00,0x00,0x00,0x00,0x00,0x00, // LUT0: BB: VS 0 ~7 + 0x80,0x00,0x00,0x00,0x00,0x00,0x00, // LUT1: BW: VS 0 ~7 + 0x40,0x00,0x00,0x00,0x00,0x00,0x00, // LUT2: WB: VS 0 ~7 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00, // LUT3: WW: VS 0 ~7 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00, // LUT4: VCOM: VS 0 ~7 + + 0x0A,0x00,0x00,0x00,0x00, // TP0 A~D RP0 + 0x00,0x00,0x00,0x00,0x00, // TP1 A~D RP1 + 0x00,0x00,0x00,0x00,0x00, // TP2 A~D RP2 + 0x00,0x00,0x00,0x00,0x00, // TP3 A~D RP3 + 0x00,0x00,0x00,0x00,0x00, // TP4 A~D RP4 + 0x00,0x00,0x00,0x00,0x00, // TP5 A~D RP5 + 0x00,0x00,0x00,0x00,0x00, // TP6 A~D RP6 +]; diff --git a/src/epd2in13_v2/graphics.rs b/src/epd2in13_v2/graphics.rs new file mode 100644 index 0000000..1c77a3e --- /dev/null +++ b/src/epd2in13_v2/graphics.rs @@ -0,0 +1,159 @@ +use crate::buffer_len; +use crate::epd2in13_v2::{DEFAULT_BACKGROUND_COLOR, HEIGHT, WIDTH}; +use crate::graphics::{Display, DisplayRotation}; +use embedded_graphics::pixelcolor::BinaryColor; +use embedded_graphics::prelude::*; + +/// Full size buffer for use with the 2in13 v2 EPD +/// +/// Can also be manually constructed: +/// `buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); WIDTH / 8 * HEIGHT]` +pub struct Display2in13 { + buffer: [u8; buffer_len(WIDTH as usize, HEIGHT as usize)], + rotation: DisplayRotation, +} + +impl Default for Display2in13 { + fn default() -> Self { + Display2in13 { + buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); + buffer_len(WIDTH as usize, HEIGHT as usize)], + rotation: DisplayRotation::default(), + } + } +} + +impl DrawTarget for Display2in13 { + type Error = core::convert::Infallible; + + fn draw_pixel(&mut self, pixel: Pixel) -> Result<(), Self::Error> { + self.draw_helper(WIDTH, HEIGHT, pixel) + } + + fn size(&self) -> Size { + Size::new(WIDTH, HEIGHT) + } +} + +impl Display for Display2in13 { + fn buffer(&self) -> &[u8] { + &self.buffer + } + + fn get_mut_buffer(&mut self) -> &mut [u8] { + &mut self.buffer + } + + fn set_rotation(&mut self, rotation: DisplayRotation) { + self.rotation = rotation; + } + + fn rotation(&self) -> DisplayRotation { + self.rotation + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::color::{Black, Color}; + use crate::epd2in13_v2; + use crate::graphics::{Display, DisplayRotation}; + use embedded_graphics::{primitives::Line, style::PrimitiveStyle}; + + // test buffer length + #[test] + fn graphics_size() { + let display = Display2in13::default(); + assert_eq!(display.buffer().len(), 4000); + } + + // test default background color on all bytes + #[test] + fn graphics_default() { + let display = Display2in13::default(); + for &byte in display.buffer() { + assert_eq!(byte, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } + + #[test] + fn graphics_rotation_0() { + let mut display = Display2in13::default(); + + let _ = Line::new(Point::new(0, 0), Point::new(7, 0)) + .into_styled(PrimitiveStyle::with_stroke(Black, 1)) + .draw(&mut display); + + let buffer = display.buffer(); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } + + #[test] + fn graphics_rotation_90() { + let mut display = Display2in13::default(); + display.set_rotation(DisplayRotation::Rotate90); + + let _ = Line::new( + Point::new(0, (WIDTH - 8) as i32), + Point::new(0, (WIDTH - 1) as i32), + ) + .into_styled(PrimitiveStyle::with_stroke(Black, 1)) + .draw(&mut display); + + let buffer = display.buffer(); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } + + #[test] + fn graphics_rotation_180() { + let mut display = Display2in13::default(); + display.set_rotation(DisplayRotation::Rotate180); + + let _ = Line::new( + Point::new((WIDTH - 8) as i32, (HEIGHT - 1) as i32), + Point::new((WIDTH - 1) as i32, (HEIGHT - 1) as i32), + ) + .into_styled(PrimitiveStyle::with_stroke(Black, 1)) + .draw(&mut display); + + let buffer = display.buffer(); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } + + #[test] + fn graphics_rotation_270() { + let mut display = Display2in13::default(); + display.set_rotation(DisplayRotation::Rotate270); + + let _ = Line::new( + Point::new((HEIGHT - 1) as i32, 0), + Point::new((HEIGHT - 1) as i32, 7), + ) + .into_styled(PrimitiveStyle::with_stroke(Black, 1)) + .draw(&mut display); + + let buffer = display.buffer(); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } +} diff --git a/src/epd2in13_v2/mod.rs b/src/epd2in13_v2/mod.rs new file mode 100644 index 0000000..b0b53f8 --- /dev/null +++ b/src/epd2in13_v2/mod.rs @@ -0,0 +1,575 @@ +//! A Driver for the Waveshare 2.13" E-Ink Display (V2) via SPI +//! +//! # References +//! +//! - [Waveshare product page](https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT) +//! - [Waveshare C driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/c/lib/e-Paper/EPD_2in13_V2.c) +//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd2in13_V2.py) +//! - [Controller Datasheet SS1780](http://www.e-paper-display.com/download_detail/downloadsId=682.html) +//! + +use embedded_hal::{ + blocking::{delay::*, spi::Write}, + digital::v2::{InputPin, OutputPin}, +}; + +use crate::buffer_len; +use crate::color::Color; +use crate::interface::DisplayInterface; +use crate::traits::{InternalWiAdditions, RefreshLUT, WaveshareDisplay}; + +pub(crate) mod command; +use self::command::{ + BorderWaveForm, BorderWaveFormFixLevel, BorderWaveFormGS, BorderWaveFormVBD, Command, + DataEntryModeDir, DataEntryModeIncr, DeepSleepMode, DisplayUpdateControl2, DriverOutput, + GateDrivingVoltage, I32Ext, SourceDrivingVoltage, VCOM, +}; + +pub(crate) mod constants; +use self::constants::{LUT_FULL_UPDATE, LUT_PARTIAL_UPDATE}; + +#[cfg(feature = "graphics")] +mod graphics; +#[cfg(feature = "graphics")] +pub use self::graphics::Display2in13; + +/// 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: Color = Color::White; +const IS_BUSY_LOW: bool = false; + +/// EPD2in13 (V2) driver +/// +pub struct EPD2in13 { + /// Connection Interface + interface: DisplayInterface, + + sleep_mode: DeepSleepMode, + + /// Background Color + background_color: Color, + refresh: RefreshLUT, +} + +impl InternalWiAdditions + for EPD2in13 +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, +{ + fn init>( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + // HW reset + self.interface.reset(delay); + + if self.refresh == RefreshLUT::QUICK { + self.set_vcom_register(spi, (-9).vcom())?; + self.wait_until_idle(); + + self.set_lut(spi, Some(self.refresh))?; + + // Python code does this, not sure why + // self.cmd_with_data(spi, Command::WRITE_OTP_SELECTION, &[0, 0, 0, 0, 0x40, 0, 0])?; + + // During partial update, clock/analog are not disabled between 2 + // updates. + self.set_display_update_control_2( + spi, + DisplayUpdateControl2::new().enable_analog().enable_clock(), + )?; + self.command(spi, Command::MASTER_ACTIVATION)?; + self.wait_until_idle(); + + self.set_border_waveform( + spi, + BorderWaveForm { + vbd: BorderWaveFormVBD::GS, + fix_level: BorderWaveFormFixLevel::VSS, + gs_trans: BorderWaveFormGS::LUT1, + }, + )?; + } else { + self.wait_until_idle(); + self.command(spi, Command::SW_RESET)?; + self.wait_until_idle(); + + self.set_driver_output( + spi, + DriverOutput { + scan_is_linear: true, + scan_g0_is_first: true, + scan_dir_incr: true, + width: (HEIGHT - 1) as u16, + }, + )?; + + // These 2 are the reset values + self.set_dummy_line_period(spi, 0x30)?; + self.set_gate_scan_start_position(spi, 0)?; + + self.set_data_entry_mode( + spi, + DataEntryModeIncr::X_INCR_Y_INCR, + DataEntryModeDir::X_DIR, + )?; + + // Use simple X/Y auto increase + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + self.set_ram_address_counters(spi, 0, 0)?; + + self.set_border_waveform( + spi, + BorderWaveForm { + vbd: BorderWaveFormVBD::GS, + fix_level: BorderWaveFormFixLevel::VSS, + gs_trans: BorderWaveFormGS::LUT3, + }, + )?; + + self.set_vcom_register(spi, (-21).vcom())?; + + self.set_gate_driving_voltage(spi, 190.gate_driving_decivolt())?; + self.set_source_driving_voltage( + spi, + 150.source_driving_decivolt(), + 50.source_driving_decivolt(), + (-150).source_driving_decivolt(), + )?; + + self.set_gate_line_width(spi, 10)?; + + self.set_lut(spi, Some(self.refresh))?; + } + + self.wait_until_idle(); + Ok(()) + } +} + +impl WaveshareDisplay + for EPD2in13 +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, +{ + fn new>( + spi: &mut SPI, + cs: CS, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + ) -> Result { + let mut epd = EPD2in13 { + interface: DisplayInterface::new(cs, busy, dc, rst), + sleep_mode: DeepSleepMode::MODE_1, + background_color: DEFAULT_BACKGROUND_COLOR, + refresh: RefreshLUT::FULL, + }; + + 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) -> Result<(), SPI::Error> { + self.wait_until_idle(); + + // All sample code enables and disables analog/clocks... + self.set_display_update_control_2( + spi, + DisplayUpdateControl2::new() + .enable_analog() + .enable_clock() + .disable_analog() + .disable_clock(), + )?; + self.command(spi, Command::MASTER_ACTIVATION)?; + + self.set_sleep_mode(spi, self.sleep_mode)?; + Ok(()) + } + + fn update_frame(&mut self, spi: &mut SPI, buffer: &[u8]) -> Result<(), SPI::Error> { + assert!(buffer.len() == buffer_len(WIDTH as usize, HEIGHT as usize)); + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + self.set_ram_address_counters(spi, 0, 0)?; + + self.cmd_with_data(spi, Command::WRITE_RAM, buffer)?; + + if self.refresh == RefreshLUT::FULL { + // Always keep the base buffer equal to current if not doing partial refresh. + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + self.set_ram_address_counters(spi, 0, 0)?; + + self.cmd_with_data(spi, Command::WRITE_RAM_RED, buffer)?; + } + Ok(()) + } + + /// Updating only a part of the frame is not supported when using the + /// partial refresh feature. The function will panic if called when set to + /// use partial refresh. + fn update_partial_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + assert!((width * height / 8) as usize == buffer.len()); + + // This should not be used when doing partial refresh. The RAM_RED must + // be updated with the last buffer having been displayed. Doing partial + // update directly in RAM makes this update impossible (we can't read + // RAM content). Using this function will most probably make the actual + // display incorrect as the controler will compare with something + // incorrect. + assert!(self.refresh == RefreshLUT::FULL); + + self.set_ram_area(spi, x, y, x + width, y + height)?; + self.set_ram_address_counters(spi, x, y)?; + + self.cmd_with_data(spi, Command::WRITE_RAM, buffer)?; + + if self.refresh == RefreshLUT::FULL { + // Always keep the base buffer equals to current if not doing partial refresh. + self.set_ram_area(spi, x, y, x + width, y + height)?; + self.set_ram_address_counters(spi, x, y)?; + + self.cmd_with_data(spi, Command::WRITE_RAM_RED, buffer)?; + } + + Ok(()) + } + + /// Never use directly this function when using partial refresh, or also + /// keep the base buffer in syncd using `set_partial_base_buffer` function. + fn display_frame(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + if self.refresh == RefreshLUT::FULL { + self.set_display_update_control_2( + spi, + DisplayUpdateControl2::new() + .enable_clock() + .enable_analog() + .display() + .disable_analog() + .disable_clock(), + )?; + } else { + self.set_display_update_control_2(spi, DisplayUpdateControl2::new().display())?; + } + self.command(spi, Command::MASTER_ACTIVATION)?; + self.wait_until_idle(); + + Ok(()) + } + + fn update_and_display_frame(&mut self, spi: &mut SPI, buffer: &[u8]) -> Result<(), SPI::Error> { + self.update_frame(spi, buffer)?; + self.display_frame(spi)?; + + if self.refresh == RefreshLUT::QUICK { + self.set_partial_base_buffer(spi, buffer)?; + } + Ok(()) + } + + fn clear_frame(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let color = self.background_color.get_byte_value(); + + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + self.set_ram_address_counters(spi, 0, 0)?; + + self.command(spi, Command::WRITE_RAM)?; + self.interface.data_x_times( + spi, + color, + buffer_len(WIDTH as usize, HEIGHT as usize) as u32, + )?; + + // Always keep the base buffer equals to current if not doing partial refresh. + if self.refresh == RefreshLUT::FULL { + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + self.set_ram_address_counters(spi, 0, 0)?; + + self.command(spi, Command::WRITE_RAM_RED)?; + self.interface.data_x_times( + spi, + color, + buffer_len(WIDTH as usize, HEIGHT as usize) as u32, + )?; + } + Ok(()) + } + + fn set_background_color(&mut self, background_color: Color) { + self.background_color = background_color; + } + + fn background_color(&self) -> &Color { + &self.background_color + } + + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + fn set_lut( + &mut self, + spi: &mut SPI, + refresh_rate: Option, + ) -> Result<(), SPI::Error> { + let buffer = match refresh_rate { + Some(RefreshLUT::FULL) | None => &LUT_FULL_UPDATE, + Some(RefreshLUT::QUICK) => &LUT_PARTIAL_UPDATE, + }; + + self.cmd_with_data(spi, Command::WRITE_LUT_REGISTER, buffer) + } + + fn is_busy(&self) -> bool { + self.interface.is_busy(IS_BUSY_LOW) + } +} + +impl EPD2in13 +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, +{ + /// When using partial refresh, the controller uses the provided buffer for + /// comparison with new buffer. + pub fn set_partial_base_buffer( + &mut self, + spi: &mut SPI, + buffer: &[u8], + ) -> Result<(), SPI::Error> { + assert!(buffer_len(WIDTH as usize, HEIGHT as usize) == buffer.len()); + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + self.set_ram_address_counters(spi, 0, 0)?; + + self.cmd_with_data(spi, Command::WRITE_RAM_RED, buffer)?; + Ok(()) + } + + /// Selects which sleep mode will be used when triggering the deep sleep. + pub fn set_deep_sleep_mode(&mut self, mode: DeepSleepMode) { + self.sleep_mode = mode; + } + + /// Sets the refresh mode. When changing mode, the screen will be + /// re-initialized accordingly. + pub fn set_refresh>( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + refresh: RefreshLUT, + ) -> Result<(), SPI::Error> { + if self.refresh != refresh { + self.refresh = refresh; + self.init(spi, delay)?; + } + Ok(()) + } + + fn set_gate_scan_start_position( + &mut self, + spi: &mut SPI, + start: u16, + ) -> Result<(), SPI::Error> { + assert!(start <= 295); + self.cmd_with_data( + spi, + Command::GATE_SCAN_START_POSITION, + &[(start & 0xFF) as u8, ((start >> 8) & 0x1) as u8], + ) + } + + fn set_border_waveform( + &mut self, + spi: &mut SPI, + borderwaveform: BorderWaveForm, + ) -> Result<(), SPI::Error> { + self.cmd_with_data( + spi, + Command::BORDER_WAVEFORM_CONTROL, + &[borderwaveform.to_u8()], + ) + } + + fn set_vcom_register(&mut self, spi: &mut SPI, vcom: VCOM) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::WRITE_VCOM_REGISTER, &[vcom.0]) + } + + fn set_gate_driving_voltage( + &mut self, + spi: &mut SPI, + voltage: GateDrivingVoltage, + ) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::GATE_DRIVING_VOLTAGE_CTRL, &[voltage.0]) + } + + fn set_dummy_line_period( + &mut self, + spi: &mut SPI, + number_of_lines: u8, + ) -> Result<(), SPI::Error> { + assert!(number_of_lines <= 127); + self.cmd_with_data(spi, Command::SET_DUMMY_LINE_PERIOD, &[number_of_lines]) + } + + fn set_gate_line_width(&mut self, spi: &mut SPI, width: u8) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::SET_GATE_LINE_WIDTH, &[width & 0x0F]) + } + + /// Sets the source driving voltage value + fn set_source_driving_voltage( + &mut self, + spi: &mut SPI, + vsh1: SourceDrivingVoltage, + vsh2: SourceDrivingVoltage, + vsl: SourceDrivingVoltage, + ) -> Result<(), SPI::Error> { + self.cmd_with_data( + spi, + Command::SOURCE_DRIVING_VOLTAGE_CTRL, + &[vsh1.0, vsh2.0, vsl.0], + ) + } + + /// Prepare the actions that the next master activation command will + /// trigger. + fn set_display_update_control_2( + &mut self, + spi: &mut SPI, + value: DisplayUpdateControl2, + ) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::DISPLAY_UPDATE_CONTROL_2, &[value.0]) + } + + /// 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::DEEP_SLEEP_MODE, &[mode as u8]) + } + + fn set_driver_output(&mut self, spi: &mut SPI, output: DriverOutput) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::DRIVER_OUTPUT_CONTROL, &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::DATA_ENTRY_MODE_SETTING, &[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::SET_RAM_X_ADDRESS_START_END_POSITION, + &[(start_x >> 3) as u8, (end_x >> 3) as u8], + )?; + + self.cmd_with_data( + spi, + Command::SET_RAM_Y_ADDRESS_START_END_POSITION, + &[ + 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, + x: u32, + y: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(); + self.cmd_with_data(spi, Command::SET_RAM_X_ADDRESS_COUNTER, &[(x >> 3) as u8])?; + + self.cmd_with_data( + spi, + Command::SET_RAM_Y_ADDRESS_COUNTER, + &[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 wait_until_idle(&mut self) { + self.interface.wait_until_idle(IS_BUSY_LOW) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 122); + assert_eq!(HEIGHT, 250); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/src/lib.rs b/src/lib.rs index f8e1c77..6c4b94b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,6 +74,7 @@ mod interface; pub mod epd1in54; pub mod epd1in54b; +pub mod epd2in13_v2; pub mod epd2in9; pub mod epd2in9bc; pub mod epd4in2; diff --git a/src/traits.rs b/src/traits.rs index 52a7337..360e63c 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -12,7 +12,7 @@ pub(crate) trait Command { } /// Seperates the different LUT for the Display Refresh process -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum RefreshLUT { /// The "normal" full Lookuptable for the Refresh-Sequence FULL,