diff --git a/Cargo.toml b/Cargo.toml index 08f3123..677ac97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,8 @@ edition = "2018" # `branch` is optional; default is `master` travis-ci = { repository = "caemor/epd-waveshare" } - [features] -default = ["epd1in54", "epd1in54b", "epd2in9", "epd4in2", "epd7in5", "graphics"] +default = ["epd1in54", "epd1in54b", "epd2in9", "epd4in2", "epd7in5", "epd7in5_v2", "graphics"] graphics = ["embedded-graphics"] epd1in54 = [] @@ -27,6 +26,7 @@ epd1in54b = [] epd2in9 = [] epd4in2 = [] epd7in5 = [] +epd7in5_v2 = [] # offers an alternative fast full lut for type_a displays, but the refresh isnt as clean looking type_a_alternative_faster_lut = [] diff --git a/src/epd7in5_v2/command.rs b/src/epd7in5_v2/command.rs new file mode 100644 index 0000000..121f8b5 --- /dev/null +++ b/src/epd7in5_v2/command.rs @@ -0,0 +1,155 @@ +//! SPI Commands for the Waveshare 7.5" E-Ink Display + +use crate::traits; + +/// EPD7in5 commands +/// +/// Should rarely (never?) be needed directly. +/// +/// 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 { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift + /// direction, booster switch, soft reset. + PANEL_SETTING = 0x00, + + /// Selecting internal and external power + POWER_SETTING = 0x01, + + /// After the Power Off command, the driver will power off following the Power Off + /// Sequence; BUSY signal will become "0". This command will turn off charge pump, + /// T-con, source driver, gate driver, VCOM, and temperature sensor, but register + /// data will be kept until VDD becomes OFF. Source Driver output and Vcom will remain + /// as previous condition, which may have 2 conditions: 0V or floating. + POWER_OFF = 0x02, + + /// Setting Power OFF sequence + POWER_OFF_SEQUENCE_SETTING = 0x03, + + /// Turning On the Power + /// + /// After the Power ON command, the driver will power on following the Power ON + /// sequence. Once complete, the BUSY signal will become "1". + POWER_ON = 0x04, + + /// Starting data transmission + BOOSTER_SOFT_START = 0x06, + + /// This command makes the chip enter the deep-sleep mode to save power. + /// + /// The deep sleep mode would return to stand-by by hardware reset. + /// + /// The only one parameter is a check code, the command would be excuted if check code = 0xA5. + DEEP_SLEEP = 0x07, + + /// This command starts transmitting data and write them into SRAM. To complete data + /// transmission, command DSP (Data Stop) must be issued. Then the chip will start to + /// send data/VCOM for panel. + /// + /// BLACK/WHITE or OLD_DATA + DATA_START_TRANSMISSION_1 = 0x10, + + /// To stop data transmission, this command must be issued to check the `data_flag`. + /// + /// After this command, BUSY signal will become "0" until the display update is + /// finished. + DATA_STOP = 0x11, + + /// After this command is issued, driver will refresh display (data/VCOM) according to + /// SRAM data and LUT. + /// + /// After Display Refresh command, BUSY signal will become "0" until the display + /// update is finished. + DISPLAY_REFRESH = 0x12, + + /// RED or NEW_DATA + DATA_START_TRANSMISSION_2 = 0x13, + + /// Dual SPI - what for? + DUAL_SPI = 0x15, + + /// This command builds the VCOM Look-Up Table (LUTC). + LUT_FOR_VCOM = 0x20, + /// This command builds the Black Look-Up Table (LUTB). + LUT_BLACK = 0x21, + /// This command builds the White Look-Up Table (LUTW). + LUT_WHITE = 0x22, + /// This command builds the Gray1 Look-Up Table (LUTG1). + LUT_GRAY_1 = 0x23, + /// This command builds the Gray2 Look-Up Table (LUTG2). + LUT_GRAY_2 = 0x24, + /// This command builds the Red0 Look-Up Table (LUTR0). + LUT_RED_0 = 0x25, + /// This command builds the Red1 Look-Up Table (LUTR1). + LUT_RED_1 = 0x26, + /// This command builds the Red2 Look-Up Table (LUTR2). + LUT_RED_2 = 0x27, + /// This command builds the Red3 Look-Up Table (LUTR3). + LUT_RED_3 = 0x28, + /// This command builds the XON Look-Up Table (LUTXON). + LUT_XON = 0x29, + + /// The command controls the PLL clock frequency. + PLL_CONTROL = 0x30, + + /// This command reads the temperature sensed by the temperature sensor. + TEMPERATURE_SENSOR_COMMAND = 0x40, + /// This command selects the Internal or External temperature sensor. + TEMPERATURE_CALIBRATION = 0x41, + /// This command could write data to the external temperature sensor. + TEMPERATURE_SENSOR_WRITE = 0x42, + /// This command could read data from the external temperature sensor. + TEMPERATURE_SENSOR_READ = 0x43, + + /// This command indicates the interval of Vcom and data output. When setting the + /// vertical back porch, the total blanking will be kept (20 Hsync). + VCOM_AND_DATA_INTERVAL_SETTING = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn + /// the battery condition. + LOW_POWER_DETECTION = 0x51, + + /// This command defines non-overlap period of Gate and Source. + TCON_SETTING = 0x60, + /// This command defines alternative resolution and this setting is of higher priority + /// than the RES\[1:0\] in R00H (PSR). + TCON_RESOLUTION = 0x61, + /// This command defines MCU host direct access external memory mode. + SPI_FLASH_CONTROL = 0x65, + + /// The LUT_REV / Chip Revision is read from OTP address = 25001 and 25000. + REVISION = 0x70, + /// This command reads the IC status. + GET_STATUS = 0x71, + + /// This command implements related VCOM sensing setting. + AUTO_MEASUREMENT_VCOM = 0x80, + /// This command gets the VCOM value. + READ_VCOM_VALUE = 0x81, + /// This command sets `VCOM_DC` value. + VCM_DC_SETTING = 0x82, + + // /// This is in all the Waveshare controllers for EPD7in5, but it's not documented + // /// anywhere in the datasheet `¯\_(ツ)_/¯` + // FLASH_MODE = 0xE5, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Command as CommandTrait; + + #[test] + fn command_addr() { + assert_eq!(Command::PANEL_SETTING.address(), 0x00); + assert_eq!(Command::DISPLAY_REFRESH.address(), 0x12); + } +} diff --git a/src/epd7in5_v2/graphics.rs b/src/epd7in5_v2/graphics.rs new file mode 100644 index 0000000..aa6f464 --- /dev/null +++ b/src/epd7in5_v2/graphics.rs @@ -0,0 +1,151 @@ +use crate::epd7in5_v2::{DEFAULT_BACKGROUND_COLOR, HEIGHT, WIDTH}; +use crate::graphics::{Display, DisplayRotation}; +use crate::prelude::*; +use embedded_graphics::prelude::*; + +/// Full size buffer for use with the 7in5 EPD +/// +/// Can also be manually constructed: +/// `buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); WIDTH / 8 * HEIGHT]` +pub struct Display7in5 { + buffer: [u8; WIDTH as usize * HEIGHT as usize / 8], + rotation: DisplayRotation, +} + +impl Default for Display7in5 { + fn default() -> Self { + Display7in5 { + buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); + WIDTH as usize * HEIGHT as usize / 8], + rotation: DisplayRotation::default(), + } + } +} + +impl Drawing for Display7in5 { + fn draw(&mut self, item_pixels: T) + where + T: IntoIterator>, + { + self.draw_helper(WIDTH, HEIGHT, item_pixels); + } +} + +impl Display for Display7in5 { + 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::Color; + use crate::epd7in5_v2; + use crate::graphics::{Display, DisplayRotation}; + use embedded_graphics::coord::Coord; + use embedded_graphics::primitives::Line; + + // test buffer length + #[test] + fn graphics_size() { + let display = Display7in5::default(); + assert_eq!(display.buffer().len(), 48000); + } + + // test default background color on all bytes + #[test] + fn graphics_default() { + let display = Display7in5::default(); + for &byte in display.buffer() { + assert_eq!(byte, epd7in5_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } + + #[test] + fn graphics_rotation_0() { + let mut display = Display7in5::default(); + display.draw( + Line::new(Coord::new(0, 0), Coord::new(7, 0)) + .stroke(Some(Color::Black)) + .into_iter(), + ); + + let buffer = display.buffer(); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, epd7in5_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } + + #[test] + fn graphics_rotation_90() { + let mut display = Display7in5::default(); + display.set_rotation(DisplayRotation::Rotate90); + display.draw( + Line::new(Coord::new(0, 792), Coord::new(0, 799)) + .stroke(Some(Color::Black)) + .into_iter(), + ); + + let buffer = display.buffer(); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, epd7in5_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } + + #[test] + fn graphics_rotation_180() { + let mut display = Display7in5::default(); + display.set_rotation(DisplayRotation::Rotate180); + display.draw( + Line::new(Coord::new(792, 479), Coord::new(799, 479)) + .stroke(Some(Color::Black)) + .into_iter(), + ); + + let buffer = display.buffer(); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, epd7in5_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } + + #[test] + fn graphics_rotation_270() { + let mut display = Display7in5::default(); + display.set_rotation(DisplayRotation::Rotate270); + display.draw( + Line::new(Coord::new(479, 0), Coord::new(479, 7)) + .stroke(Some(Color::Black)) + .into_iter(), + ); + + let buffer = display.buffer(); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, epd7in5_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value()); + } + } +} diff --git a/src/epd7in5_v2/mod.rs b/src/epd7in5_v2/mod.rs new file mode 100644 index 0000000..ba42a72 --- /dev/null +++ b/src/epd7in5_v2/mod.rs @@ -0,0 +1,278 @@ +//! A simple Driver for the Waveshare 7.5" E-Ink Display (V2) via SPI +//! +//! # References +//! +//! - [Datasheet](https://www.waveshare.com/wiki/7.5inch_e-Paper_HAT) +//! - [Waveshare C driver](https://github.com/waveshare/e-Paper/blob/702def0/RaspberryPi%26JetsonNano/c/lib/e-Paper/EPD_7in5_V2.c) +//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/702def0/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd7in5_V2.py) +//! +//! Important note for V2: +//! Revision V2 has been released on 2019.11, the resolution is upgraded to 800×480, from 640×384 of V1. +//! The hardware and interface of V2 are compatible with V1, however, the related software should be updated. + +use embedded_hal::{ + blocking::{delay::*, spi::Write}, + digital::v2::{InputPin, OutputPin}, +}; + +use crate::color::Color; +use crate::interface::DisplayInterface; +use crate::traits::{InternalWiAdditions, RefreshLUT, WaveshareDisplay, WaveshareDisplayExt}; + +pub(crate) mod command; +use self::command::Command; + +#[cfg(feature = "graphics")] +mod graphics; +#[cfg(feature = "graphics")] +pub use self::graphics::Display7in5; + +pub const WIDTH: u32 = 800; +pub const HEIGHT: u32 = 480; +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = true; + +/// EPD7in5 (V2) driver +/// +pub struct EPD7in5 { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: Color, +} + +impl InternalWiAdditions + for EPD7in5 +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, +{ + fn init>( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + // Reset the device + self.interface.reset(delay); + + // V2 procedure as described here: + // https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd7in5bc_V2.py + // and as per specs: + // https://www.waveshare.com/w/upload/6/60/7.5inch_e-Paper_V2_Specification.pdf + + self.cmd_with_data(spi, Command::BOOSTER_SOFT_START, &[0x17, 0x17, 0x27, 0x17])?; + self.cmd_with_data(spi, Command::POWER_SETTING, &[0x07, 0x17, 0x3F, 0x3F])?; + self.command(spi, Command::POWER_ON)?; + self.wait_until_idle(); + self.cmd_with_data(spi, Command::PANEL_SETTING, &[0x1F])?; + self.cmd_with_data(spi, Command::PLL_CONTROL, &[0x06])?; + self.cmd_with_data(spi, Command::TCON_RESOLUTION, &[0x03, 0x20, 0x01, 0xE0])?; + self.cmd_with_data(spi, Command::DUAL_SPI, &[0x00])?; + self.cmd_with_data(spi, Command::TCON_SETTING, &[0x22])?; + self.cmd_with_data(spi, Command::VCOM_AND_DATA_INTERVAL_SETTING, &[0x10, 0x07])?; + self.wait_until_idle(); + Ok(()) + } +} + +impl WaveshareDisplay + for EPD7in5 +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, +{ + /// Creates a new driver from a SPI peripheral, CS Pin, Busy InputPin, DC + /// + /// This already initialises the device. That means [init()] isn't needed + /// directly afterwards. + /// + /// [init()]: InternalWiAdditions::init + /// + /// # Example + /// + /// ```rust,ignore + /// //buffer = some image data; + /// + /// let mut epd7in5 = EPD7in5::new(spi, cs, busy, dc, rst, delay); + /// + /// epd7in5.update_and_display_frame(&mut spi, &buffer); + /// + /// epd7in5.sleep(); + /// ``` + fn new>( + spi: &mut SPI, + cs: CS, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + ) -> Result { + let interface = DisplayInterface::new(cs, busy, dc, rst); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = EPD7in5 { interface, 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) -> Result<(), SPI::Error> { + self.command(spi, Command::POWER_OFF)?; + self.wait_until_idle(); + self.cmd_with_data(spi, Command::DEEP_SLEEP, &[0xA5])?; + Ok(()) + } + + fn update_frame(&mut self, spi: &mut SPI, buffer: &[u8]) -> Result<(), SPI::Error> { + self.command(spi, Command::DATA_START_TRANSMISSION_2)?; + self.send_data(spi, buffer)?; + self.wait_until_idle(); + Ok(()) + } + + fn update_partial_frame( + &mut self, + _spi: &mut SPI, + _buffer: &[u8], + _x: u32, + _y: u32, + _width: u32, + _height: u32, + ) -> Result<(), SPI::Error> { + unimplemented!(); + } + + fn display_frame(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + self.command(spi, Command::DISPLAY_REFRESH)?; + self.wait_until_idle(); + Ok(()) + } + + fn clear_frame(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + self.send_resolution(spi)?; + + self.command(spi, Command::DATA_START_TRANSMISSION_1)?; + self.interface.data_x_times(spi, 0x00, WIDTH * HEIGHT / 8)?; + + self.command(spi, Command::DATA_START_TRANSMISSION_2)?; + self.interface.data_x_times(spi, 0x00, WIDTH * HEIGHT / 8)?; + + self.command(spi, Command::DISPLAY_REFRESH)?; + self.wait_until_idle(); + Ok(()) + } + + fn set_background_color(&mut self, color: Color) { + self.color = color; + } + + fn background_color(&self) -> &Color { + &self.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> { + unimplemented!(); + } + + fn is_busy(&self) -> bool { + self.interface.is_busy(IS_BUSY_LOW) + } +} + +impl WaveshareDisplayExt + for EPD7in5 +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, +{ + fn update_and_display_frame(&mut self, spi: &mut SPI, buffer: &[u8]) -> Result<(), SPI::Error> { + self.command(spi, Command::DATA_START_TRANSMISSION_2)?; + for b in buffer { self.send_data(spi, &[255 - b])?; } + self.command(spi, Command::DISPLAY_REFRESH)?; + self.wait_until_idle(); + Ok(()) + } +} + +impl EPD7in5 +where + SPI: Write, + CS: OutputPin, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, +{ + fn command(&mut self, spi: &mut SPI, command: Command) -> Result<(), SPI::Error> { + self.interface.cmd(spi, command) + } + + fn send_data(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> { + self.interface.data(spi, data) + } + + 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) + } + + fn send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::TCON_RESOLUTION)?; + self.send_data(spi, &[(w >> 8) as u8])?; + self.send_data(spi, &[w as u8])?; + self.send_data(spi, &[(h >> 8) as u8])?; + self.send_data(spi, &[h as u8]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 800); + assert_eq!(HEIGHT, 480); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/src/lib.rs b/src/lib.rs index 931e4dc..00e8183 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,8 @@ mod interface; #[cfg(feature = "epd7in5")] pub mod epd7in5; +#[cfg(feature = "epd7in5_v2")] +pub mod epd7in5_v2; #[cfg(feature = "epd4in2")] pub mod epd4in2; @@ -85,6 +87,10 @@ pub(crate) mod type_a; pub mod prelude { pub use crate::color::Color; pub use crate::traits::{RefreshLUT, WaveshareDisplay, WaveshareThreeColorDisplay}; + + #[cfg(feature = "epd7in5_v2")] + pub use crate::traits::WaveshareDisplayExt; + pub use crate::SPI_MODE; #[cfg(feature = "graphics")]