diff --git a/epd-waveshare/.gitignore b/epd-waveshare/.gitignore new file mode 100644 index 0000000..1f41f71 --- /dev/null +++ b/epd-waveshare/.gitignore @@ -0,0 +1,16 @@ +# Generated files +**/target +**/*.rs.bk + + +# The library shouldn't decide about the exact versions of +# its dependencies, but let the downstream crate decide. +Cargo.lock + + + +# vscode +.vscode/* + +# intellij/clion +.idea/ diff --git a/epd-waveshare/Cargo.toml b/epd-waveshare/Cargo.toml new file mode 100644 index 0000000..55d682a --- /dev/null +++ b/epd-waveshare/Cargo.toml @@ -0,0 +1,62 @@ +[package] +authors = ["Christoph Groß "] +categories = ["embedded", "hardware-support", "no-std"] +description = "An embedded-hal based driver for ePaper displays from Waveshare formerly published as eink-waveshare-rs" +documentation = "https://docs.rs/epd-waveshare" +homepage = "https://github.com/caemor/epd-waveshare" +keywords = ["ePaper", "Display", "epd", "eink"] +license = "ISC" +name = "epd-waveshare" +readme = "README.md" +repository = "https://github.com/Caemor/epd-waveshare.git" +version = "0.5.0" +edition = "2021" + +[badges] +# travis-ci = { repository = "caemor/epd-waveshare" } + +[dependencies] +embedded-graphics-core = { version = "0.4", optional = true } +embedded-hal = "1.0.0" +bit_field = "0.10.1" + +[dev-dependencies] +embedded-graphics = "0.8" +embedded-hal-mock = { version = "=0.10.0", default-features = false, features = [ + "eh1", +] } + +[target.'cfg(unix)'.dev-dependencies] +linux-embedded-hal = "0.4.0" + +[[example]] +name = "epd1in54_no_graphics" +required-features = ["linux-dev"] + +[[example]] +name = "epd2in13_v2" +required-features = ["linux-dev"] + +[[example]] +name = "epd2in13bc" +required-features = ["linux-dev"] + +[[example]] +name = "epd4in2_variable_size" +required-features = ["linux-dev"] + +[[example]] +name = "epd4in2" +required-features = ["linux-dev"] + +[features] +# Remove the linux-dev feature to build the tests on non unix systems +default = ["graphics", "linux-dev", "epd2in13_v3"] + +graphics = ["embedded-graphics-core"] +epd2in13_v2 = [] +epd2in13_v3 = [] +linux-dev = [] + +# Offers an alternative fast full lut for type_a displays, but the refreshed screen isnt as clean looking +type_a_alternative_faster_lut = [] diff --git a/epd-waveshare/License.md b/epd-waveshare/License.md new file mode 100644 index 0000000..362429a --- /dev/null +++ b/epd-waveshare/License.md @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2018, Christoph Groß, morituri2@mailbox.org + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/epd-waveshare/src/color.rs b/epd-waveshare/src/color.rs new file mode 100644 index 0000000..74823d3 --- /dev/null +++ b/epd-waveshare/src/color.rs @@ -0,0 +1,436 @@ +//! B/W Color for EPDs +//! +//! EPD representation of multicolor with separate buffers +//! for each bit makes it hard to properly represent colors here + +#[cfg(feature = "graphics")] +use embedded_graphics_core::pixelcolor::BinaryColor; +#[cfg(feature = "graphics")] +use embedded_graphics_core::pixelcolor::PixelColor; + +/// When trying to parse u8 to one of the color types +#[derive(Debug, PartialEq, Eq)] +pub struct OutOfColorRangeParseError(u8); +impl core::fmt::Display for OutOfColorRangeParseError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Outside of possible Color Range: {}", self.0) + } +} + +impl OutOfColorRangeParseError { + fn _new(size: u8) -> OutOfColorRangeParseError { + OutOfColorRangeParseError(size) + } +} + +/// Only for the Black/White-Displays +// TODO : 'color' is not a good name for black and white, rename it to BiColor/BWColor ? +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Color { + /// Black color + Black, + /// White color + White, +} + +/// Only for the Black/White/Color-Displays +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum TriColor { + /// Black color + Black, + /// White color + White, + /// Chromatic color + Chromatic, +} + +/// For the 7 Color Displays +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum OctColor { + /// Black Color + Black = 0x00, + /// White Color + White = 0x01, + /// Green Color + Green = 0x02, + /// Blue Color + Blue = 0x03, + /// Red Color + Red = 0x04, + /// Yellow Color + Yellow = 0x05, + /// Orange Color + Orange = 0x06, + /// HiZ / Clean Color + HiZ = 0x07, +} + +/// Color trait for use in `Display`s +pub trait ColorType { + /// Number of bit used to represent this color type in a single buffer. + /// To get the real number of bits per pixel you should multiply this by `BUFFER_COUNT` + const BITS_PER_PIXEL_PER_BUFFER: usize; + + /// Number of buffer used to represent this color type + /// splitted buffer like tricolo is 2, otherwise this should be 1. + const BUFFER_COUNT: usize; + + /// Return the data used to set a pixel color + /// + /// * bwrbit is used to tell the value of the unused bit when a chromatic + /// color is set (TriColor only as for now) + /// * pos is the pixel position in the line, used to know which pixels must be set + /// + /// Return values are : + /// * .0 is the mask used to exclude this pixel from the byte (eg: 0x7F in BiColor) + /// * .1 are the bits used to set the color in the byte (eg: 0x80 in BiColor) + /// this is u16 because we set 2 bytes in case of split buffer + fn bitmask(&self, bwrbit: bool, pos: u32) -> (u8, u16); +} + +impl ColorType for Color { + const BITS_PER_PIXEL_PER_BUFFER: usize = 1; + const BUFFER_COUNT: usize = 1; + fn bitmask(&self, _bwrbit: bool, pos: u32) -> (u8, u16) { + let bit = 0x80 >> (pos % 8); + match self { + Color::Black => (!bit, 0u16), + Color::White => (!bit, bit as u16), + } + } +} + +impl ColorType for TriColor { + const BITS_PER_PIXEL_PER_BUFFER: usize = 1; + const BUFFER_COUNT: usize = 2; + fn bitmask(&self, bwrbit: bool, pos: u32) -> (u8, u16) { + let bit = 0x80 >> (pos % 8); + match self { + TriColor::Black => (!bit, 0u16), + TriColor::White => (!bit, bit as u16), + TriColor::Chromatic => ( + !bit, + if bwrbit { + (bit as u16) << 8 + } else { + (bit as u16) << 8 | bit as u16 + }, + ), + } + } +} + +impl ColorType for OctColor { + const BITS_PER_PIXEL_PER_BUFFER: usize = 4; + const BUFFER_COUNT: usize = 1; + fn bitmask(&self, _bwrbit: bool, pos: u32) -> (u8, u16) { + let mask = !(0xF0 >> (pos % 2)); + let bits = self.get_nibble() as u16; + (mask, if pos % 2 == 1 { bits } else { bits << 4 }) + } +} + +#[cfg(feature = "graphics")] +impl From for OctColor { + fn from(b: BinaryColor) -> OctColor { + match b { + BinaryColor::On => OctColor::Black, + BinaryColor::Off => OctColor::White, + } + } +} + +#[cfg(feature = "graphics")] +impl From for embedded_graphics_core::pixelcolor::Rgb888 { + fn from(b: OctColor) -> Self { + let (r, g, b) = b.rgb(); + Self::new(r, g, b) + } +} + +#[cfg(feature = "graphics")] +impl From for OctColor { + fn from(p: embedded_graphics_core::pixelcolor::Rgb888) -> OctColor { + use embedded_graphics_core::prelude::RgbColor; + let colors = [ + OctColor::Black, + OctColor::White, + OctColor::Green, + OctColor::Blue, + OctColor::Red, + OctColor::Yellow, + OctColor::Orange, + OctColor::HiZ, + ]; + // if the user has already mapped to the right color space, it will just be in the list + if let Some(found) = colors.iter().find(|c| c.rgb() == (p.r(), p.g(), p.b())) { + return *found; + } + + // This is not ideal but just pick the nearest color + *colors + .iter() + .map(|c| (c, c.rgb())) + .map(|(c, (r, g, b))| { + let dist = (i32::from(r) - i32::from(p.r())).pow(2) + + (i32::from(g) - i32::from(p.g())).pow(2) + + (i32::from(b) - i32::from(p.b())).pow(2); + (c, dist) + }) + .min_by_key(|(_c, dist)| *dist) + .map(|(c, _)| c) + .unwrap_or(&OctColor::White) + } +} + +#[cfg(feature = "graphics")] +impl From for OctColor { + fn from(b: embedded_graphics_core::pixelcolor::raw::RawU4) -> Self { + use embedded_graphics_core::prelude::RawData; + OctColor::from_nibble(b.into_inner()).unwrap() + } +} + +#[cfg(feature = "graphics")] +impl PixelColor for OctColor { + type Raw = embedded_graphics_core::pixelcolor::raw::RawU4; +} + +impl OctColor { + /// Gets the Nibble representation of the Color as needed by the display + pub fn get_nibble(self) -> u8 { + self as u8 + } + /// Converts two colors into a single byte for the Display + pub fn colors_byte(a: OctColor, b: OctColor) -> u8 { + a.get_nibble() << 4 | b.get_nibble() + } + + ///Take the nibble (lower 4 bits) and convert to an OctColor if possible + pub fn from_nibble(nibble: u8) -> Result { + match nibble & 0xf { + 0x00 => Ok(OctColor::Black), + 0x01 => Ok(OctColor::White), + 0x02 => Ok(OctColor::Green), + 0x03 => Ok(OctColor::Blue), + 0x04 => Ok(OctColor::Red), + 0x05 => Ok(OctColor::Yellow), + 0x06 => Ok(OctColor::Orange), + 0x07 => Ok(OctColor::HiZ), + e => Err(OutOfColorRangeParseError(e)), + } + } + ///Split the nibbles of a single byte and convert both to an OctColor if possible + pub fn split_byte(byte: u8) -> Result<(OctColor, OctColor), OutOfColorRangeParseError> { + let low = OctColor::from_nibble(byte & 0xf)?; + let high = OctColor::from_nibble((byte >> 4) & 0xf)?; + Ok((high, low)) + } + /// Converts to limited range of RGB values. + pub fn rgb(self) -> (u8, u8, u8) { + match self { + OctColor::White => (0xff, 0xff, 0xff), + OctColor::Black => (0x00, 0x00, 0x00), + OctColor::Green => (0x00, 0xff, 0x00), + OctColor::Blue => (0x00, 0x00, 0xff), + OctColor::Red => (0xff, 0x00, 0x00), + OctColor::Yellow => (0xff, 0xff, 0x00), + OctColor::Orange => (0xff, 0x80, 0x00), + OctColor::HiZ => (0x80, 0x80, 0x80), /* looks greyish */ + } + } +} +//TODO: Rename get_bit_value to bit() and get_byte_value to byte() ? + +impl Color { + /// Get the color encoding of the color for one bit + pub fn get_bit_value(self) -> u8 { + match self { + Color::White => 1u8, + Color::Black => 0u8, + } + } + + /// Gets a full byte of black or white pixels + pub fn get_byte_value(self) -> u8 { + match self { + Color::White => 0xff, + Color::Black => 0x00, + } + } + + /// Parses from u8 to Color + fn from_u8(val: u8) -> Self { + match val { + 0 => Color::Black, + 1 => Color::White, + e => panic!( + "DisplayColor only parses 0 and 1 (Black and White) and not `{}`", + e + ), + } + } + + /// Returns the inverse of the given color. + /// + /// Black returns White and White returns Black + pub fn inverse(self) -> Color { + match self { + Color::White => Color::Black, + Color::Black => Color::White, + } + } +} + +impl From for Color { + fn from(value: u8) -> Self { + Color::from_u8(value) + } +} + +#[cfg(feature = "graphics")] +impl PixelColor for Color { + type Raw = (); +} + +#[cfg(feature = "graphics")] +impl From for Color { + fn from(b: BinaryColor) -> Color { + match b { + BinaryColor::On => Color::Black, + BinaryColor::Off => Color::White, + } + } +} + +#[cfg(feature = "graphics")] +impl From for Color { + fn from(rgb: embedded_graphics_core::pixelcolor::Rgb888) -> Self { + use embedded_graphics_core::pixelcolor::RgbColor; + if rgb == RgbColor::BLACK { + Color::Black + } else if rgb == RgbColor::WHITE { + Color::White + } else { + // choose closest color + if (rgb.r() as u16 + rgb.g() as u16 + rgb.b() as u16) > 255 * 3 / 2 { + Color::White + } else { + Color::Black + } + } + } +} + +#[cfg(feature = "graphics")] +impl From for embedded_graphics_core::pixelcolor::Rgb888 { + fn from(color: Color) -> Self { + use embedded_graphics_core::pixelcolor::RgbColor; + match color { + Color::Black => embedded_graphics_core::pixelcolor::Rgb888::BLACK, + Color::White => embedded_graphics_core::pixelcolor::Rgb888::WHITE, + } + } +} + +impl TriColor { + /// Get the color encoding of the color for one bit + pub fn get_bit_value(self) -> u8 { + match self { + TriColor::White => 1u8, + TriColor::Black | TriColor::Chromatic => 0u8, + } + } + + /// Gets a full byte of black or white pixels + pub fn get_byte_value(self) -> u8 { + match self { + TriColor::White => 0xff, + TriColor::Black | TriColor::Chromatic => 0x00, + } + } +} + +#[cfg(feature = "graphics")] +impl PixelColor for TriColor { + type Raw = (); +} + +#[cfg(feature = "graphics")] +impl From for TriColor { + fn from(b: BinaryColor) -> TriColor { + match b { + BinaryColor::On => TriColor::Black, + BinaryColor::Off => TriColor::White, + } + } +} +#[cfg(feature = "graphics")] +impl From for TriColor { + fn from(rgb: embedded_graphics_core::pixelcolor::Rgb888) -> Self { + use embedded_graphics_core::pixelcolor::RgbColor; + if rgb == RgbColor::BLACK { + TriColor::Black + } else if rgb == RgbColor::WHITE { + TriColor::White + } else { + // there is no good approximation here since we don't know which color is 'chromatic' + TriColor::Chromatic + } + } +} +#[cfg(feature = "graphics")] +impl From for embedded_graphics_core::pixelcolor::Rgb888 { + fn from(tri_color: TriColor) -> Self { + use embedded_graphics_core::pixelcolor::RgbColor; + match tri_color { + TriColor::Black => embedded_graphics_core::pixelcolor::Rgb888::BLACK, + TriColor::White => embedded_graphics_core::pixelcolor::Rgb888::WHITE, + // assume chromatic is red + TriColor::Chromatic => embedded_graphics_core::pixelcolor::Rgb888::new(255, 0, 0), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_u8() { + assert_eq!(Color::Black, Color::from(0u8)); + assert_eq!(Color::White, Color::from(1u8)); + } + + // test all values aside from 0 and 1 which all should panic + #[test] + fn from_u8_panic() { + for val in 2..=u8::max_value() { + extern crate std; + let result = std::panic::catch_unwind(|| Color::from(val)); + assert!(result.is_err()); + } + } + + #[test] + fn u8_conversion_black() { + assert_eq!(Color::from(Color::Black.get_bit_value()), Color::Black); + assert_eq!(Color::from(0u8).get_bit_value(), 0u8); + } + + #[test] + fn u8_conversion_white() { + assert_eq!(Color::from(Color::White.get_bit_value()), Color::White); + assert_eq!(Color::from(1u8).get_bit_value(), 1u8); + } + + #[test] + fn test_oct() { + let left = OctColor::Red; + let right = OctColor::Green; + assert_eq!( + OctColor::split_byte(OctColor::colors_byte(left, right)), + Ok((left, right)) + ); + } +} diff --git a/epd-waveshare/src/epd1in54/mod.rs b/epd-waveshare/src/epd1in54/mod.rs new file mode 100644 index 0000000..4a4514e --- /dev/null +++ b/epd-waveshare/src/epd1in54/mod.rs @@ -0,0 +1,399 @@ +//! A simple Driver for the Waveshare 1.54" E-Ink Display via SPI +//! +//! # Example for the 1.54 in E-Ink Display +//! +//!```rust, no_run +//!# use embedded_hal_mock::eh1::*; +//!# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +//!use embedded_graphics::{ +//! pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyleBuilder}, +//!}; +//!use epd_waveshare::{epd1in54::*, 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::NoopDelay::new(); +//! +//!// Setup EPD +//!let mut epd = Epd1in54::new(&mut spi, busy_in, dc, rst, &mut delay, None)?; +//! +//!// Use display graphics from embedded-graphics +//!let mut display = Display1in54::default(); +//! +//!// Use embedded graphics for drawing a line +//!let style = PrimitiveStyleBuilder::new() +//! .stroke_color(Color::Black) +//! .stroke_width(1) +//! .build(); +//!let _ = Line::new(Point::new(0, 120), Point::new(0, 295)) +//! .into_styled(style) +//! .draw(&mut display); +//! +//!// Display updated frame +//!epd.update_frame(&mut spi, &display.buffer(), &mut delay)?; +//!epd.display_frame(&mut spi, &mut delay)?; +//! +//!// Set the EPD to sleep +//!epd.sleep(&mut spi, &mut delay)?; +//!# Ok(()) +//!# } +//!``` + +/// Width of the display +pub const WIDTH: u32 = 200; +/// Height of the display +pub const HEIGHT: u32 = 200; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +//const DPI: u16 = 184; +const IS_BUSY_LOW: bool = false; +const SINGLE_BYTE_WRITE: bool = true; + +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::type_a::{ + command::Command, + constants::{LUT_FULL_UPDATE, LUT_PARTIAL_UPDATE}, +}; + +use crate::color::Color; + +use crate::traits::{RefreshLut, WaveshareDisplay}; + +use crate::buffer_len; +use crate::interface::DisplayInterface; + +/// Full size buffer for use with the 1in54b EPD +#[cfg(feature = "graphics")] +pub type Display1in54 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Epd1in54 driver +pub struct Epd1in54 { + /// SPI + interface: DisplayInterface, + /// Color + background_color: Color, + /// Refresh LUT + refresh: RefreshLut, +} + +impl Epd1in54 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.reset(delay, 10_000, 10_000); + + // 3 Databytes: + // A[7:0] + // 0.. A[8] + // 0.. B[2:0] + // Default Values: A = Height of Screen (0x127), B = 0x00 (GD, SM and TB=0?) + self.interface.cmd_with_data( + spi, + Command::DriverOutputControl, + &[HEIGHT as u8, (HEIGHT >> 8) as u8, 0x00], + )?; + + // 3 Databytes: (and default values from datasheet and arduino) + // 1 .. A[6:0] = 0xCF | 0xD7 + // 1 .. B[6:0] = 0xCE | 0xD6 + // 1 .. C[6:0] = 0x8D | 0x9D + //TODO: test + self.interface + .cmd_with_data(spi, Command::BoosterSoftStartControl, &[0xD7, 0xD6, 0x9D])?; + + // One Databyte with value 0xA8 for 7V VCOM + self.interface + .cmd_with_data(spi, Command::WriteVcomRegister, &[0xA8])?; + + // One Databyte with default value 0x1A for 4 dummy lines per gate + self.interface + .cmd_with_data(spi, Command::SetDummyLinePeriod, &[0x1A])?; + + // One Databyte with default value 0x08 for 2us per line + self.interface + .cmd_with_data(spi, Command::SetGateLineWidth, &[0x08])?; + + // One Databyte with default value 0x03 + // -> address: x increment, y increment, address counter is updated in x direction + self.interface + .cmd_with_data(spi, Command::DataEntryModeSetting, &[0x03])?; + + self.set_lut(spi, delay, None)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd1in54 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + + let mut epd = Epd1in54 { + interface, + 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, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // 0x00 for Normal mode (Power on Reset), 0x01 for Deep Sleep Mode + //TODO: is 0x00 needed here or would 0x01 be even more efficient? + self.interface + .cmd_with_data(spi, Command::DeepSleepMode, &[0x00])?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.use_full_frame(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::WriteRam, buffer)?; + Ok(()) + } + + //TODO: update description: last 3 bits will be ignored for width and x_pos + 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> { + self.wait_until_idle(spi, delay)?; + self.set_ram_area(spi, delay, x, y, x + width, y + height)?; + self.set_ram_counter(spi, delay, x, y)?; + + self.interface + .cmd_with_data(spi, Command::WriteRam, buffer)?; + Ok(()) + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // enable clock signal, enable cp, display pattern -> 0xC4 (tested with the arduino version) + //TODO: test control_1 or control_2 with default value 0xFF (from the datasheet) + self.interface + .cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xC4])?; + + self.interface.cmd(spi, Command::MasterActivation)?; + // MASTER Activation should not be interupted to avoid currption of panel images + // therefore a terminate command is send + self.interface.cmd(spi, Command::Nop)?; + 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.wait_until_idle(spi, delay)?; + self.use_full_frame(spi, delay)?; + + // clear the ram with the background color + let color = self.background_color.get_byte_value(); + + self.interface.cmd(spi, Command::WriteRam)?; + self.interface + .data_x_times(spi, color, WIDTH / 8 * HEIGHT)?; + Ok(()) + } + + fn set_background_color(&mut self, background_color: Color) { + self.background_color = background_color; + } + + fn background_color(&self) -> &Color { + &self.background_color + } + + fn set_lut( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + refresh_rate: Option, + ) -> Result<(), SPI::Error> { + if let Some(refresh_lut) = refresh_rate { + self.refresh = refresh_lut; + } + match self.refresh { + RefreshLut::Full => self.set_lut_helper(spi, delay, &LUT_FULL_UPDATE), + RefreshLut::Quick => self.set_lut_helper(spi, delay, &LUT_PARTIAL_UPDATE), + } + } + + 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 Epd1in54 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + pub(crate) fn use_full_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + // choose full frame/ram + self.set_ram_area(spi, delay, 0, 0, WIDTH - 1, HEIGHT - 1)?; + + // start from the beginning + self.set_ram_counter(spi, delay, 0, 0) + } + + pub(crate) fn set_ram_area( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + start_x: u32, + start_y: u32, + end_x: u32, + end_y: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + assert!(start_x < end_x); + assert!(start_y < end_y); + + // x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram + // aren't relevant + self.interface.cmd_with_data( + spi, + Command::SetRamXAddressStartEndPosition, + &[(start_x >> 3) as u8, (end_x >> 3) as u8], + )?; + + // 2 Databytes: A[7:0] & 0..A[8] for each - start and end + self.interface.cmd_with_data( + spi, + Command::SetRamYAddressStartEndPosition, + &[ + start_y as u8, + (start_y >> 8) as u8, + end_y as u8, + (end_y >> 8) as u8, + ], + )?; + Ok(()) + } + + pub(crate) fn set_ram_counter( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + x: u32, + y: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram + // aren't relevant + self.interface + .cmd_with_data(spi, Command::SetRamXAddressCounter, &[(x >> 3) as u8])?; + + // 2 Databytes: A[7:0] & 0..A[8] + self.interface.cmd_with_data( + spi, + Command::SetRamYAddressCounter, + &[y as u8, (y >> 8) as u8], + )?; + Ok(()) + } + + fn set_lut_helper( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + assert!(buffer.len() == 30); + + self.interface + .cmd_with_data(spi, Command::WriteLutRegister, buffer)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 200); + assert_eq!(HEIGHT, 200); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd1in54_v2/constants.rs b/epd-waveshare/src/epd1in54_v2/constants.rs new file mode 100644 index 0000000..578534c --- /dev/null +++ b/epd-waveshare/src/epd1in54_v2/constants.rs @@ -0,0 +1,46 @@ +#[rustfmt::skip] +// Original Waveforms from Waveshare for 1in54_V2 +pub(crate) const LUT_FULL_UPDATE: [u8; 159] =[ + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x8, 0x1, 0x0, 0x8, 0x1, 0x0, 0x2, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x22, 0x17, 0x41, 0x0, 0x32, 0x20 +]; + +#[rustfmt::skip] +pub(crate) const LUT_PARTIAL_UPDATE: [u8; 159] =[ + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x02, 0x17, 0x41, 0xB0, 0x32, 0x28, +]; diff --git a/epd-waveshare/src/epd1in54_v2/mod.rs b/epd-waveshare/src/epd1in54_v2/mod.rs new file mode 100644 index 0000000..80bf584 --- /dev/null +++ b/epd-waveshare/src/epd1in54_v2/mod.rs @@ -0,0 +1,383 @@ +//! A simple Driver for the Waveshare 1.54" E-Ink Display via SPI +//! +//! GDEH0154D67 + +/// Width of the display +pub const WIDTH: u32 = 200; +/// Height of the display +pub const HEIGHT: u32 = 200; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = false; +const SINGLE_BYTE_WRITE: bool = true; + +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::type_a::command::Command; + +mod constants; +use crate::epd1in54_v2::constants::{LUT_FULL_UPDATE, LUT_PARTIAL_UPDATE}; + +use crate::color::Color; + +use crate::traits::{RefreshLut, WaveshareDisplay}; + +use crate::interface::DisplayInterface; + +#[cfg(feature = "graphics")] +pub use crate::epd1in54::Display1in54; + +/// Epd1in54 driver +pub struct Epd1in54 { + /// SPI + interface: DisplayInterface, + /// Color + background_color: Color, + + /// Refresh LUT + refresh: RefreshLut, +} + +impl Epd1in54 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + 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)?; + + // 3 Databytes: + // A[7:0] + // 0.. A[8] + // 0.. B[2:0] + // Default Values: A = Height of Screen (0x127), B = 0x00 (GD, SM and TB=0?) + self.interface.cmd_with_data( + spi, + Command::DriverOutputControl, + &[(HEIGHT - 1) as u8, 0x0, 0x00], + )?; + + self.interface + .cmd_with_data(spi, Command::DataEntryModeSetting, &[0x3])?; + + self.set_ram_area(spi, delay, 0, 0, WIDTH - 1, HEIGHT - 1)?; + + self.interface.cmd_with_data( + spi, + Command::TemperatureSensorSelection, + &[0x80], // 0x80: internal temperature sensor + )?; + + self.interface + .cmd_with_data(spi, Command::TemperatureSensorControl, &[0xB1, 0x20])?; + + self.set_ram_counter(spi, delay, 0, 0)?; + + //Initialize the lookup table with a refresh waveform + self.set_lut(spi, delay, None)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd1in54 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + + let mut epd = Epd1in54 { + interface, + 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, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::DeepSleepMode, &[0x01])?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.use_full_frame(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::WriteRam, buffer)?; + Ok(()) + } + + //TODO: update description: last 3 bits will be ignored for width and x_pos + 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> { + self.wait_until_idle(spi, delay)?; + self.set_ram_area(spi, delay, x, y, x + width, y + height)?; + self.set_ram_counter(spi, delay, x, y)?; + + self.interface + .cmd_with_data(spi, Command::WriteRam, buffer)?; + Ok(()) + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + if self.refresh == RefreshLut::Full { + self.interface + .cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xC7])?; + } else if self.refresh == RefreshLut::Quick { + self.interface + .cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xCF])?; + } + + self.interface.cmd(spi, Command::MasterActivation)?; + // MASTER Activation should not be interupted to avoid currption of panel images + // therefore a terminate command is send + self.interface.cmd(spi, Command::Nop)?; + 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.wait_until_idle(spi, delay)?; + self.use_full_frame(spi, delay)?; + + // clear the ram with the background color + let color = self.background_color.get_byte_value(); + + self.interface.cmd(spi, Command::WriteRam)?; + self.interface + .data_x_times(spi, color, WIDTH / 8 * HEIGHT)?; + self.interface.cmd(spi, Command::WriteRam2)?; + self.interface + .data_x_times(spi, color, WIDTH / 8 * HEIGHT)?; + Ok(()) + } + + fn set_background_color(&mut self, background_color: Color) { + self.background_color = background_color; + } + + fn background_color(&self) -> &Color { + &self.background_color + } + + fn set_lut( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + refresh_rate: Option, + ) -> Result<(), SPI::Error> { + if let Some(refresh_lut) = refresh_rate { + self.refresh = refresh_lut; + } + match self.refresh { + RefreshLut::Full => self.set_lut_helper(spi, delay, &LUT_FULL_UPDATE), + RefreshLut::Quick => self.set_lut_helper(spi, delay, &LUT_PARTIAL_UPDATE), + }?; + + // Additional configuration required only for partial updates + if self.refresh == RefreshLut::Quick { + self.interface.cmd_with_data( + spi, + Command::WriteOtpSelection, + &[0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0], + )?; + self.interface + .cmd_with_data(spi, Command::BorderWaveformControl, &[0x80])?; + self.interface + .cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xc0])?; + self.interface.cmd(spi, Command::MasterActivation)?; + // MASTER Activation should not be interupted to avoid currption of panel images + // therefore a terminate command is send + self.interface.cmd(spi, Command::Nop)?; + } + Ok(()) + } + + 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 Epd1in54 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + pub(crate) fn use_full_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + // choose full frame/ram + self.set_ram_area(spi, delay, 0, 0, WIDTH - 1, HEIGHT - 1)?; + + // start from the beginning + self.set_ram_counter(spi, delay, 0, 0) + } + + pub(crate) fn set_ram_area( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + start_x: u32, + start_y: u32, + end_x: u32, + end_y: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + assert!(start_x < end_x); + assert!(start_y < end_y); + + // x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram + // aren't relevant + self.interface.cmd_with_data( + spi, + Command::SetRamXAddressStartEndPosition, + &[(start_x >> 3) as u8, (end_x >> 3) as u8], + )?; + + // 2 Databytes: A[7:0] & 0..A[8] for each - start and end + self.interface.cmd_with_data( + spi, + Command::SetRamYAddressStartEndPosition, + &[ + start_y as u8, + (start_y >> 8) as u8, + end_y as u8, + (end_y >> 8) as u8, + ], + )?; + Ok(()) + } + + pub(crate) fn set_ram_counter( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + x: u32, + y: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram + // aren't relevant + self.interface + .cmd_with_data(spi, Command::SetRamXAddressCounter, &[(x >> 3) as u8])?; + + // 2 Databytes: A[7:0] & 0..A[8] + self.interface.cmd_with_data( + spi, + Command::SetRamYAddressCounter, + &[y as u8, (y >> 8) as u8], + )?; + Ok(()) + } + + fn set_lut_helper( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + assert!(buffer.len() == 159); + + self.interface + .cmd_with_data(spi, Command::WriteLutRegister, &buffer[0..153])?; + + self.interface + .cmd_with_data(spi, Command::WriteLutRegisterEnd, &[buffer[153]])?; + + self.wait_until_idle(spi, delay)?; + + self.interface + .cmd_with_data(spi, Command::GateDrivingVoltage, &[buffer[154]])?; + + self.interface.cmd_with_data( + spi, + Command::SourceDrivingVoltage, + &[buffer[155], buffer[156], buffer[157]], + )?; + self.interface + .cmd_with_data(spi, Command::WriteVcomRegister, &[buffer[158]])?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 200); + assert_eq!(HEIGHT, 200); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd1in54b/command.rs b/epd-waveshare/src/epd1in54b/command.rs new file mode 100644 index 0000000..fd1e35d --- /dev/null +++ b/epd-waveshare/src/epd1in54b/command.rs @@ -0,0 +1,40 @@ +//! SPI Commands for the Waveshare 1.54" red E-Ink Display +use crate::traits; + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + PanelSetting = 0x00, + + PowerSetting = 0x01, + PowerOff = 0x02, + PowerOn = 0x04, + BoosterSoftStart = 0x06, + DataStartTransmission1 = 0x10, + DisplayRefresh = 0x12, + DataStartTransmission2 = 0x13, + + LutForVcom = 0x20, + LutWhiteToWhite = 0x21, + LutBlackToWhite = 0x22, + LutG0 = 0x23, + LutG1 = 0x24, + LutRedVcom = 0x25, + LutRed0 = 0x26, + LutRed1 = 0x27, + + PllControl = 0x30, + TemperatureSensor = 0x40, + TemperatureSensorSelection = 0x41, + VcomAndDataIntervalSetting = 0x50, + ResolutionSetting = 0x61, + VcmDcSetting = 0x82, + PowerSaving = 0xE3, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} diff --git a/epd-waveshare/src/epd1in54b/constants.rs b/epd-waveshare/src/epd1in54b/constants.rs new file mode 100644 index 0000000..a08c0fc --- /dev/null +++ b/epd-waveshare/src/epd1in54b/constants.rs @@ -0,0 +1,31 @@ +pub(crate) const LUT_VCOM0: &[u8] = &[ + 0x0E, 0x14, 0x01, 0x0A, 0x06, 0x04, 0x0A, 0x0A, 0x0F, 0x03, 0x03, 0x0C, 0x06, 0x0A, 0x00, +]; + +pub(crate) const LUT_WHITE_TO_WHITE: &[u8] = &[ + 0x0E, 0x14, 0x01, 0x0A, 0x46, 0x04, 0x8A, 0x4A, 0x0F, 0x83, 0x43, 0x0C, 0x86, 0x0A, 0x04, +]; + +pub(crate) const LUT_BLACK_TO_WHITE: &[u8] = &[ + 0x0E, 0x14, 0x01, 0x8A, 0x06, 0x04, 0x8A, 0x4A, 0x0F, 0x83, 0x43, 0x0C, 0x06, 0x4A, 0x04, +]; + +pub(crate) const LUT_G1: &[u8] = &[ + 0x8E, 0x94, 0x01, 0x8A, 0x06, 0x04, 0x8A, 0x4A, 0x0F, 0x83, 0x43, 0x0C, 0x06, 0x0A, 0x04, +]; + +pub(crate) const LUT_G2: &[u8] = &[ + 0x8E, 0x94, 0x01, 0x8A, 0x06, 0x04, 0x8A, 0x4A, 0x0F, 0x83, 0x43, 0x0C, 0x06, 0x0A, 0x04, +]; + +pub(crate) const LUT_RED_VCOM: &[u8] = &[ + 0x03, 0x1D, 0x01, 0x01, 0x08, 0x23, 0x37, 0x37, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub(crate) const LUT_RED0: &[u8] = &[ + 0x83, 0x5D, 0x01, 0x81, 0x48, 0x23, 0x77, 0x77, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +pub(crate) const LUT_RED1: &[u8] = &[ + 0x03, 0x1D, 0x01, 0x01, 0x08, 0x23, 0x37, 0x37, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; diff --git a/epd-waveshare/src/epd1in54b/mod.rs b/epd-waveshare/src/epd1in54b/mod.rs new file mode 100644 index 0000000..08aae37 --- /dev/null +++ b/epd-waveshare/src/epd1in54b/mod.rs @@ -0,0 +1,379 @@ +//! A simple Driver for the Waveshare 1.54" (B) E-Ink Display via SPI + +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::interface::DisplayInterface; +use crate::traits::{ + InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, +}; + +//The Lookup Tables for the Display +mod constants; +use crate::epd1in54b::constants::*; + +/// Width of epd1in54 in pixels +pub const WIDTH: u32 = 200; +/// Height of epd1in54 in pixels +pub const HEIGHT: u32 = 200; +/// Default Background Color (white) +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = true; +const SINGLE_BYTE_WRITE: bool = true; + +use crate::color::Color; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 1in54b EPD +/// TODO this should be a TriColor, but let's keep it as is at first +#[cfg(feature = "graphics")] +pub type Display1in54b = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Epd1in54b driver +pub struct Epd1in54b { + interface: DisplayInterface, + color: Color, +} + +impl InternalWiAdditions + for Epd1in54b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.reset(delay, 10_000, 10_000); + + // set the power settings + self.interface + .cmd_with_data(spi, Command::PowerSetting, &[0x07, 0x00, 0x08, 0x00])?; + + // start the booster + self.interface + .cmd_with_data(spi, Command::BoosterSoftStart, &[0x07, 0x07, 0x07])?; + + // power on + self.command(spi, Command::PowerOn)?; + delay.delay_us(5000); + self.wait_until_idle(spi, delay)?; + + // set the panel settings + self.cmd_with_data(spi, Command::PanelSetting, &[0xCF])?; + + self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x37])?; + + // PLL + self.cmd_with_data(spi, Command::PllControl, &[0x39])?; + + // set resolution + self.send_resolution(spi)?; + + self.cmd_with_data(spi, Command::VcmDcSetting, &[0x0E])?; + + self.set_lut(spi, delay, None)?; + + self.wait_until_idle(spi, delay)?; + + Ok(()) + } +} + +impl WaveshareThreeColorDisplay + for Epd1in54b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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.wait_until_idle(spi, delay)?; + self.send_resolution(spi)?; + + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + for b in black { + let expanded = expand_bits(*b); + self.interface.data(spi, &expanded)?; + } + Ok(()) + } + + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data(spi, chromatic)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd1in54b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd1in54b { interface, color }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x17])?; //border floating + + self.interface + .cmd_with_data(spi, Command::VcmDcSetting, &[0x00])?; // Vcom to 0V + + self.interface + .cmd_with_data(spi, Command::PowerSetting, &[0x02, 0x00, 0x00, 0x00])?; //VG&VS to 0V fast + + self.wait_until_idle(spi, delay)?; + + //NOTE: The example code has a 1s delay here + + self.command(spi, Command::PowerOff)?; + + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + 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 update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.send_resolution(spi)?; + + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + for b in buffer { + // Two bits per pixel + let expanded = expand_bits(*b); + self.interface.data(spi, &expanded)?; + } + + //NOTE: Example code has a delay here + + // Clear the read layer + let color = self.color.get_byte_value(); + let nbits = WIDTH * (HEIGHT / 8); + + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, color, nbits)?; + + //NOTE: Example code has a delay here + Ok(()) + } + + #[allow(unused)] + 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.wait_until_idle(spi, delay)?; + self.command(spi, Command::DisplayRefresh)?; + 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.wait_until_idle(spi, delay)?; + self.send_resolution(spi)?; + + let color = DEFAULT_BACKGROUND_COLOR.get_byte_value(); + + // Clear the black + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + // Uses 2 bits per pixel + self.interface + .data_x_times(spi, color, 2 * (WIDTH / 8 * HEIGHT))?; + + // Clear the red + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface + .data_x_times(spi, color, WIDTH / 8 * HEIGHT)?; + Ok(()) + } + + fn set_lut( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + _refresh_rate: Option, + ) -> Result<(), SPI::Error> { + self.interface + .cmd_with_data(spi, Command::LutForVcom, LUT_VCOM0)?; + self.interface + .cmd_with_data(spi, Command::LutWhiteToWhite, LUT_WHITE_TO_WHITE)?; + self.interface + .cmd_with_data(spi, Command::LutBlackToWhite, LUT_BLACK_TO_WHITE)?; + self.interface.cmd_with_data(spi, Command::LutG0, LUT_G1)?; + self.interface.cmd_with_data(spi, Command::LutG1, LUT_G2)?; + self.interface + .cmd_with_data(spi, Command::LutRedVcom, LUT_RED_VCOM)?; + self.interface + .cmd_with_data(spi, Command::LutRed0, LUT_RED0)?; + self.interface + .cmd_with_data(spi, Command::LutRed1, LUT_RED1)?; + + Ok(()) + } + + 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 Epd1in54b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::ResolutionSetting)?; + + self.send_data(spi, &[w as u8])?; + self.send_data(spi, &[(h >> 8) as u8])?; + self.send_data(spi, &[h as u8]) + } +} + +fn expand_bits(bits: u8) -> [u8; 2] { + let mut x = bits as u16; + + x = (x | (x << 4)) & 0x0F0F; + x = (x | (x << 2)) & 0x3333; + x = (x | (x << 1)) & 0x5555; + x = x | (x << 1); + + [(x >> 8) as u8, (x & 0xFF) as u8] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 200); + assert_eq!(HEIGHT, 200); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd1in54c/command.rs b/epd-waveshare/src/epd1in54c/command.rs new file mode 100644 index 0000000..5913986 --- /dev/null +++ b/epd-waveshare/src/epd1in54c/command.rs @@ -0,0 +1,38 @@ +//! SPI Commands for the Waveshare 1.54" C yellow E-Ink Display +use crate::traits; + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + PanelSetting = 0x00, + + PowerSetting = 0x01, + PowerOff = 0x02, + PowerOn = 0x04, + BoosterSoftStart = 0x06, + DeepSleep = 0x07, + DataStartTransmission1 = 0x10, + DisplayRefresh = 0x12, + DataStartTransmission2 = 0x13, + + LutForVcom = 0x20, + LutWhiteToWhite = 0x21, + LutBlackToWhite = 0x22, + LutWhiteToBlack = 0x23, + LutBlackToBlack = 0x24, + + PllControl = 0x30, + TemperatureSensor = 0x40, + TemperatureSensorSelection = 0x41, + VcomAndDataIntervalSetting = 0x50, + ResolutionSetting = 0x61, + VcmDcSetting = 0x82, + PowerSaving = 0xE3, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} diff --git a/epd-waveshare/src/epd1in54c/mod.rs b/epd-waveshare/src/epd1in54c/mod.rs new file mode 100644 index 0000000..bbe2730 --- /dev/null +++ b/epd-waveshare/src/epd1in54c/mod.rs @@ -0,0 +1,306 @@ +//! A simple Driver for the Waveshare 1.54" (C) E-Ink Display via SPI + +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::interface::DisplayInterface; +use crate::traits::{ + InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, +}; + +/// Width of epd1in54 in pixels +pub const WIDTH: u32 = 152; +/// Height of epd1in54 in pixels +pub const HEIGHT: u32 = 152; +/// Default Background Color (white) +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = true; +const NUM_DISPLAY_BITS: u32 = WIDTH / 8 * HEIGHT; +const SINGLE_BYTE_WRITE: bool = true; + +use crate::color::Color; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 1in54c EPD +/// TODO this should be a TriColor, but let's keep it as is at first +#[cfg(feature = "graphics")] +pub type Display1in54c = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Epd1in54c driver +pub struct Epd1in54c { + interface: DisplayInterface, + color: Color, +} + +impl InternalWiAdditions + for Epd1in54c +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Based on Reference Program Code from: + // https://www.waveshare.com/w/upload/a/ac/1.54inch_e-Paper_Module_C_Specification.pdf + // and: + // https://github.com/waveshare/e-Paper/blob/master/STM32/STM32-F103ZET6/User/e-Paper/EPD_1in54c.c + self.interface.reset(delay, 10_000, 2_000); + + // start the booster + self.cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x17])?; + + // power on + self.command(spi, Command::PowerOn)?; + delay.delay_us(5000); + self.wait_until_idle(spi, delay)?; + + // set the panel settings + self.cmd_with_data(spi, Command::PanelSetting, &[0x0f, 0x0d])?; + + // set resolution + self.send_resolution(spi)?; + + self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x77])?; + + Ok(()) + } +} + +impl WaveshareThreeColorDisplay + for Epd1in54c +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DataStartTransmission1, black)?; + + Ok(()) + } + + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DataStartTransmission2, chromatic)?; + + Ok(()) + } +} + +impl WaveshareDisplay + for Epd1in54c +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd1in54c { interface, color }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + + self.command(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DeepSleep, &[0xa5])?; + + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + 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 update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.update_achromatic_frame(spi, delay, buffer)?; + + // Clear the chromatic layer + let color = self.color.get_byte_value(); + + self.command(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + + Ok(()) + } + + #[allow(unused)] + 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::DisplayRefresh)?; + 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.wait_until_idle(spi, delay)?; + let color = DEFAULT_BACKGROUND_COLOR.get_byte_value(); + + // Clear the black + self.command(spi, Command::DataStartTransmission1)?; + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + + // Clear the chromatic + self.command(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + + Ok(()) + } + + fn set_lut( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + _refresh_rate: Option, + ) -> Result<(), SPI::Error> { + Ok(()) + } + + 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 Epd1in54c +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::ResolutionSetting)?; + + // | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + // | HRES[7:3] | 0 | 0 | 0 | + self.send_data(spi, &[(w as u8) & 0b1111_1000])?; + // | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + // | - | - | - | - | - | - | - | VRES[8] | + self.send_data(spi, &[(w >> 8) as u8])?; + // | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | + // | VRES[7:0] | + // Specification shows C/D is zero while sending the last byte, + // but upstream code does not implement it like that. So for now + // we follow upstream code. + self.send_data(spi, &[h as u8]) + } +} diff --git a/epd-waveshare/src/epd2in13_v2/command.rs b/epd-waveshare/src/epd2in13_v2/command.rs new file mode 100644 index 0000000..55e6d04 --- /dev/null +++ b/epd-waveshare/src/epd2in13_v2/command.rs @@ -0,0 +1,276 @@ +//! 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)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + DriverOutputControl = 0x01, + GateDrivingVoltageCtrl = 0x03, + SourceDrivingVoltageCtrl = 0x04, + BoosterSoftStartControl = 0x0C, + GateScanStartPosition = 0x0F, + DeepSleepMode = 0x10, + DataEntryModeSetting = 0x11, + SwReset = 0x12, + HvReadyDetection = 0x14, + VciDetection = 0x15, + TemperatureSensorControlWrite = 0x1A, + TemperatureSensorControlRead = 0x1B, + TemperatureSensorExtControlWrite = 0x1C, + MasterActivation = 0x20, + DisplayUpdateControl1 = 0x21, + DisplayUpdateControl2 = 0x22, + WriteRam = 0x24, + WriteRamRed = 0x26, + ReadRam = 0x27, + VcomSense = 0x28, + VcomSenseDuration = 0x29, + ProgramVcomOpt = 0x2A, + WriteVcomRegister = 0x2C, + OtpRegisterRead = 0x2D, + StatusBitRead = 0x2F, + ProgramWsOtp = 0x30, + LoadWsOtp = 0x31, + WriteLutRegister = 0x32, + ProgramOtpSelection = 0x36, + WriteOtpSelection = 0x37, + SetDummyLinePeriod = 0x3A, + SetGateLineWidth = 0x3B, + BorderWaveformControl = 0x3C, + ReadRamOption = 0x41, + SetRamXAddressStartEndPosition = 0x44, + SetRamYAddressStartEndPosition = 0x45, + AutoWriteRedRamRegularPattern = 0x46, + AutoWriteBwRamRegularPattern = 0x47, + SetRamXAddressCounter = 0x4E, + SetRamYAddressCounter = 0x4F, + SetAnalogBlockControl = 0x74, + SetDigitalBlockControl = 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, 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, +} + +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!((-30..=-2).contains(&self)); + 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!((100..=210).contains(&self) && self % 5 == 0); + GateDrivingVoltage(((self - 100) / 5 + 0x03) as u8) + } + + fn source_driving_decivolt(self) -> SourceDrivingVoltage { + assert!((24..=88).contains(&self) || (self % 5 == 0 && (90..=180).contains(&self.abs()))); + + if (24..=88).contains(&self) { + SourceDrivingVoltage(((self - 24) + 0x8E) as u8) + } else if (90..=180).contains(&self) { + 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/epd-waveshare/src/epd2in13_v2/constants.rs b/epd-waveshare/src/epd2in13_v2/constants.rs new file mode 100644 index 0000000..6895959 --- /dev/null +++ b/epd-waveshare/src/epd2in13_v2/constants.rs @@ -0,0 +1,86 @@ +#[rustfmt::skip] + +#[cfg(feature = "epd2in13_v2")] +// 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 +]; + +#[cfg(feature = "epd2in13_v2")] +#[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 +]; + +#[cfg(feature = "epd2in13_v3")] +#[rustfmt::skip] +// Original Waveforms from Waveshare +pub(crate) const LUT_PARTIAL_UPDATE: [u8; 159] =[ + 0x0,0x40,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x80,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x40,0x40,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x14,0x0,0x0,0x0,0x0,0x0,0x0, + 0x1,0x0,0x0,0x0,0x0,0x0,0x0, + 0x1,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x22,0x22,0x22,0x22,0x22,0x22,0x0,0x0,0x0, + 0x22,0x17,0x41,0x00,0x32,0x36, +]; + +#[cfg(feature = "epd2in13_v3")] +#[rustfmt::skip] +pub(crate) const LUT_FULL_UPDATE: [u8; 159] =[ + 0x80,0x4A,0x40,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x40,0x4A,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x80,0x4A,0x40,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x40,0x4A,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0xF,0x0,0x0,0x0,0x0,0x0,0x0, + 0xF,0x0,0x0,0xF,0x0,0x0,0x2, + 0xF,0x0,0x0,0x0,0x0,0x0,0x0, + 0x1,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x22,0x22,0x22,0x22,0x22,0x22,0x0,0x0,0x0, + 0x22,0x17,0x41,0x0,0x32,0x36 +]; diff --git a/epd-waveshare/src/epd2in13_v2/mod.rs b/epd-waveshare/src/epd2in13_v2/mod.rs new file mode 100644 index 0000000..5d8d4e0 --- /dev/null +++ b/epd-waveshare/src/epd2in13_v2/mod.rs @@ -0,0 +1,599 @@ +//! A Driver for the Waveshare 2.13" E-Ink Display (V2 and V3) via SPI +//! +//! # References V2 +//! +//! - [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) +//! +//! # References V3 +//! +//! - [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_JetsonNano/c/lib/e-Paper/EPD_2in13_V3.c) +//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in9b_V3.py) +//! - [Controller Datasheet SS1780](http://www.e-paper-display.com/download_detail/downloadsId=682.html) +//! + +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +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(all(feature = "epd2in13_v2", feature = "epd2in13_v3"))] +compile_error!( + "feature \"epd2in13_v2\" and feature \"epd2in13_v3\" cannot be enabled at the same time" +); +#[cfg(not(any(feature = "epd2in13_v2", feature = "epd2in13_v3")))] +compile_error!( + "One of feature \"epd2in13_v2\" and feature \"epd2in13_v3\" needs to be enabled as a feature" +); + +/// Full size buffer for use with the 2in13 v2 and v3 EPD +#[cfg(feature = "graphics")] +pub type Display2in13 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// 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; +const SINGLE_BYTE_WRITE: bool = true; + +/// Epd2in13 (V2 & V3) driver +/// +/// To use this driver for V2 of the display, feature \"epd2in13_v3\" needs to be disabled and feature \"epd2in13_v2\" enabled. +pub struct Epd2in13 { + /// Connection Interface + interface: DisplayInterface, + + sleep_mode: DeepSleepMode, + + /// Background Color + background_color: Color, + refresh: RefreshLut, +} + +impl InternalWiAdditions + for Epd2in13 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // HW reset + self.interface.reset(delay, 10_000, 10_000); + + if self.refresh == RefreshLut::Quick { + self.set_vcom_register(spi, (-9).vcom())?; + self.wait_until_idle(spi, delay)?; + + self.set_lut(spi, delay, Some(self.refresh))?; + + // Python code does this, not sure why + // self.cmd_with_data(spi, Command::WriteOtpSelection, &[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::MasterActivation)?; + self.wait_until_idle(spi, delay)?; + + self.set_border_waveform( + spi, + BorderWaveForm { + vbd: BorderWaveFormVbd::Gs, + fix_level: BorderWaveFormFixLevel::Vss, + gs_trans: BorderWaveFormGs::Lut1, + }, + )?; + } else { + self.wait_until_idle(spi, delay)?; + self.command(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, + }, + )?; + + // 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::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, + 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, delay, Some(self.refresh))?; + } + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd2in13 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let mut epd = Epd2in13 { + interface: DisplayInterface::new(busy, dc, rst, delay_us), + sleep_mode: DeepSleepMode::Mode1, + 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, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + + // 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::MasterActivation)?; + + self.set_sleep_mode(spi, self.sleep_mode)?; + 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.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + self.set_ram_address_counters(spi, delay, 0, 0)?; + + self.cmd_with_data(spi, Command::WriteRam, 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, delay, 0, 0)?; + + self.cmd_with_data(spi, Command::WriteRamRed, 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, + delay: &mut DELAY, + 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, delay, x, y)?; + + self.cmd_with_data(spi, Command::WriteRam, 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, delay, x, y)?; + + self.cmd_with_data(spi, Command::WriteRamRed, 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, delay: &mut DELAY) -> 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::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)?; + + if self.refresh == RefreshLut::Quick { + self.set_partial_base_buffer(spi, delay, buffer)?; + } + Ok(()) + } + + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> 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, delay, 0, 0)?; + + self.command(spi, Command::WriteRam)?; + 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, delay, 0, 0)?; + + self.command(spi, Command::WriteRamRed)?; + 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, + _delay: &mut DELAY, + 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::WriteLutRegister, buffer) + } + + 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 Epd2in13 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// 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, + delay: &mut DELAY, + 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, delay, 0, 0)?; + + self.cmd_with_data(spi, Command::WriteRamRed, 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::GateScanStartPosition, + &[(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::BorderWaveformControl, + &[borderwaveform.to_u8()], + ) + } + + fn set_vcom_register(&mut self, spi: &mut SPI, vcom: Vcom) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::WriteVcomRegister, &[vcom.0]) + } + + fn set_gate_driving_voltage( + &mut self, + spi: &mut SPI, + voltage: GateDrivingVoltage, + ) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::GateDrivingVoltageCtrl, &[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::SetDummyLinePeriod, &[number_of_lines]) + } + + fn set_gate_line_width(&mut self, spi: &mut SPI, width: u8) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::SetGateLineWidth, &[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::SourceDrivingVoltageCtrl, + &[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::DisplayUpdateControl2, &[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::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) + } +} + +#[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/epd-waveshare/src/epd2in13bc/command.rs b/epd-waveshare/src/epd2in13bc/command.rs new file mode 100644 index 0000000..48c7541 --- /dev/null +++ b/epd-waveshare/src/epd2in13bc/command.rs @@ -0,0 +1,38 @@ +//! SPI Commands for the Waveshare 2.13" (B/C) E-Ink Display +use crate::traits; + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + PanelSetting = 0x00, + + PowerSetting = 0x01, + PowerOff = 0x02, + PowerOn = 0x04, + BoosterSoftStart = 0x06, + DeepSleep = 0x07, + DataStartTransmission1 = 0x10, + DisplayRefresh = 0x12, + DataStartTransmission2 = 0x13, + + LutForVcom = 0x20, + LutWhiteToWhite = 0x21, + LutBlackToWhite = 0x22, + LutWhiteToBlack = 0x23, + LutBlackToBlack = 0x24, + + PllControl = 0x30, + TemperatureSensor = 0x40, + TemperatureSensorSelection = 0x41, + VcomAndDataIntervalSetting = 0x50, + ResolutionSetting = 0x61, + VcmDcSetting = 0x82, + PowerSaving = 0xE3, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} diff --git a/epd-waveshare/src/epd2in13bc/mod.rs b/epd-waveshare/src/epd2in13bc/mod.rs new file mode 100644 index 0000000..9d1f0d8 --- /dev/null +++ b/epd-waveshare/src/epd2in13bc/mod.rs @@ -0,0 +1,393 @@ +//! A simple Driver for the Waveshare 2.13" (B/C) E-Ink Display via SPI +//! More information on this display can be found at the [Waveshare Wiki](https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT_(B)) +//! This driver was build and tested for 212x104, 2.13inch E-Ink display HAT for Raspberry Pi, three-color, SPI interface +//! +//! # Example for the 2.13" E-Ink Display +//! +//!```rust, no_run +//!# use embedded_hal_mock::eh1::*; +//!# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +//!use embedded_graphics::{prelude::*, primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder}}; +//!use epd_waveshare::{epd2in13bc::*, 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::NoopDelay::new(); +//! +//!// Setup EPD +//!let mut epd = Epd2in13bc::new(&mut spi, 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 = Display2in13bc::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(()) +//!# } +//!``` +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::interface::DisplayInterface; +use crate::traits::{ + InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, +}; + +/// Width of epd2in13bc in pixels +pub const WIDTH: u32 = 104; +/// Height of epd2in13bc in pixels +pub const HEIGHT: u32 = 212; +/// Default background color (white) of epd2in13bc display +pub const DEFAULT_BACKGROUND_COLOR: TriColor = TriColor::White; + +/// Number of bits for b/w buffer and same for chromatic buffer +const NUM_DISPLAY_BITS: u32 = WIDTH / 8 * HEIGHT; + +const IS_BUSY_LOW: bool = true; +const VCOM_DATA_INTERVAL: u8 = 0x07; +const WHITE_BORDER: u8 = 0x70; +const BLACK_BORDER: u8 = 0x30; +const CHROMATIC_BORDER: u8 = 0xb0; +const FLOATING_BORDER: u8 = 0xF0; +const SINGLE_BYTE_WRITE: bool = true; + +use crate::color::TriColor; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 2.13" b/c EPD +#[cfg(feature = "graphics")] +pub type Display2in13bc = crate::graphics::Display< + WIDTH, + HEIGHT, + true, + { buffer_len(WIDTH as usize, HEIGHT as usize * 2) }, + TriColor, +>; + +/// Epd2in13bc driver +pub struct Epd2in13bc { + interface: DisplayInterface, + color: TriColor, +} + +impl InternalWiAdditions + for Epd2in13bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Values taken from datasheet and sample code + + self.interface.reset(delay, 10_000, 10_000); + + // start the booster + self.interface + .cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x17])?; + + // power on + self.command(spi, Command::PowerOn)?; + delay.delay_us(5000); + self.wait_until_idle(spi, delay)?; + + // set the panel settings + self.cmd_with_data(spi, Command::PanelSetting, &[0x8F])?; + + self.cmd_with_data( + spi, + Command::VcomAndDataIntervalSetting, + &[WHITE_BORDER | VCOM_DATA_INTERVAL], + )?; + + // set resolution + self.send_resolution(spi)?; + + self.cmd_with_data(spi, Command::VcmDcSetting, &[0x0A])?; + + self.wait_until_idle(spi, delay)?; + + Ok(()) + } +} + +impl WaveshareThreeColorDisplay + for Epd2in13bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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) + } + + /// Update only the black/white data of the display. + /// + /// Finish by calling `update_chromatic_frame`. + fn update_achromatic_frame( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + black: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface.data(spi, black)?; + Ok(()) + } + + /// Update only chromatic data of the display. + /// + /// This data takes precedence over the black/white data. + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data(spi, chromatic)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd2in13bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = TriColor; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd2in13bc { interface, color }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Section 8.2 from datasheet + self.interface.cmd_with_data( + spi, + Command::VcomAndDataIntervalSetting, + &[FLOATING_BORDER | VCOM_DATA_INTERVAL], + )?; + + self.command(spi, Command::PowerOff)?; + // The example STM code from Github has a wait after PowerOff + self.wait_until_idle(spi, delay)?; + + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + fn set_background_color(&mut self, color: TriColor) { + self.color = color; + } + + fn background_color(&self) -> &TriColor { + &self.color + } + + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + self.interface.data(spi, buffer)?; + + // Clear the chromatic layer + let color = self.color.get_byte_value(); + + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } + + #[allow(unused)] + 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> { + Ok(()) + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.command(spi, Command::DisplayRefresh)?; + + 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.send_resolution(spi)?; + + let color = DEFAULT_BACKGROUND_COLOR.get_byte_value(); + + // Clear the black + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + + // Clear the chromatic + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } + + fn set_lut( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + _refresh_rate: Option, + ) -> Result<(), SPI::Error> { + Ok(()) + } + + 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 Epd2in13bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::ResolutionSetting)?; + + self.send_data(spi, &[w as u8])?; + self.send_data(spi, &[(h >> 8) as u8])?; + self.send_data(spi, &[h as u8]) + } + + /// Set the outer border of the display to the chosen color. + pub fn set_border_color(&mut self, spi: &mut SPI, color: TriColor) -> Result<(), SPI::Error> { + let border = match color { + TriColor::Black => BLACK_BORDER, + TriColor::White => WHITE_BORDER, + TriColor::Chromatic => CHROMATIC_BORDER, + }; + self.cmd_with_data( + spi, + Command::VcomAndDataIntervalSetting, + &[border | VCOM_DATA_INTERVAL], + ) + } +} diff --git a/epd-waveshare/src/epd2in66b/command.rs b/epd-waveshare/src/epd2in66b/command.rs new file mode 100644 index 0000000..1a50918 --- /dev/null +++ b/epd-waveshare/src/epd2in66b/command.rs @@ -0,0 +1,117 @@ +#![allow(dead_code)] +//! SPI Commands for the SSD1675B driver chip + +use crate::traits; + +#[derive(Copy, Clone)] +pub(crate) enum Command { + DriverOutputControl = 0x01, + GateDrivingVoltageControl = 0x02, + SourceDrivingVoltageControl = 0x04, + ProgramOTPInitialCodeSetting = 0x08, + WriteRegisterForInitialCodeSetting = 0x09, + ReadRegisterForInitiaslCodeSetting = 0x0a, + BoosterSoftstartControl = 0x0c, + GateScanStartPosition = 0x0f, + DeepSleepMode = 0x10, + DataEntryMode = 0x11, + Reset = 0x12, + HVReadyDetection = 0x14, + VCIDetection = 0x15, + TemperatureSensorSelection = 0x18, + WriteTemperatureRegister = 0x1a, + ReadTemperatureRegister = 0x1b, + ExternalTemperatureSensorWrite = 0x1c, + MasterActivation = 0x20, + DisplayUpdateControl1 = 0x21, + DisplayUpdateControl2 = 0x22, + WriteBlackWhiteRAM = 0x24, + WriteRedRAM = 0x26, + ReadRAM = 0x27, + SenseVCOM = 0x28, + VCOMSenseDuration = 0x29, + ProgramOTPVCOM = 0x2a, + WriteRegisterForVCOMControl = 0x2b, + WriteVCOMRegister = 0x2c, + ReadOTPDisplayOptions = 0x2d, + ReadOTPUserId = 0x2e, + ReadStatusBits = 0x2f, + ProgramOTPWaveformSetting = 0x30, + LoadOTPWaveformSetting = 0x31, + WriteLUTRegister = 0x32, + CalculateCRC = 0x34, + ReadCRC = 0x35, + ProgramOTPSelection = 0x36, + WriteRegisterForDisplayOption = 0x37, + WriteRegisterForUserID = 0x38, + OTPProgramMode = 0x39, + SetDummyLinePeriod = 0x3a, + SetGateLineWidth = 0x3b, + BorderWaveformControl = 0x3c, + RAMReadOption = 0x41, + SetXAddressRange = 0x44, + SetYAddressRange = 0x45, + RedRAMTestPattern = 0x46, + BlackWhiteRAMTestPattern = 0x47, + SetXAddressCounter = 0x4e, + SetYAddressCounter = 0x4f, + SetAnalogBlockControl = 0x74, + SetDigitalBlockControl = 0x7e, + Nop = 0x7f, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} + +pub(crate) enum DataEntrySign { + DecYDecX = 0b00, + DecYIncX = 0b01, + IncYDecX = 0b10, + IncYIncX = 0b11, +} +pub(crate) enum DataEntryRow { + XMinor = 0b000, + YMinor = 0b100, +} + +pub(crate) enum WriteMode { + Normal = 0b0000, + ForceZero = 0b0100, + Invert = 0b1000, +} +pub(crate) enum OutputSource { + S0ToS175 = 0x00, + S8ToS167 = 0x80, +} + +pub(crate) enum DeepSleep { + Awake = 0b00, + SleepKeepingRAM = 0b01, + SleepLosingRAM = 0b11, +} + +pub(crate) enum PatH { + H8 = 0b000_0000, + H16 = 0b001_0000, + H32 = 0b010_0000, + H64 = 0b011_0000, + H128 = 0b100_0000, + H256 = 0b101_0000, + H296 = 0b110_0000, +} +pub(crate) enum PatW { + W8 = 0b000, + W16 = 0b001, + W32 = 0b010, + W64 = 0b011, + W128 = 0b100, + W160 = 0b101, +} +pub(crate) enum StartWith { + Zero = 0x00, + One = 0x80, +} diff --git a/epd-waveshare/src/epd2in66b/mod.rs b/epd-waveshare/src/epd2in66b/mod.rs new file mode 100644 index 0000000..092fce3 --- /dev/null +++ b/epd-waveshare/src/epd2in66b/mod.rs @@ -0,0 +1,517 @@ +//! A driver for the Waveshare three-color E-ink Pi Pico hat 'Pico-ePaper-2.66-B' B/W/R. +//! +//! +//! This driver was built and tested for this 296x152, 2.66inch three-color E-Ink display hat for the Pi Pico, it is expected to work for +//! other boards too, but that might depend on how the OTP memory in the display is programmed by the factory. +//! +//! The driver embedded in the display of this board is the SSD1675B, [documented by cursedhardware](https://cursedhardware.github.io/epd-driver-ic/SSD1675B.pdf). +//! +//! The pin assigments are shown on the Waveshare wiki [schematic](https://www.waveshare.com/w/upload/8/8d/Pico-ePaper-2.66.pdf). +//! +//! Information on this display/hat can be found at the [Waveshare Wiki](https://www.waveshare.com/wiki/Pico-ePaper-2.66-B). +//! Do read this documentation, in particular to understand how often this display both should and should not be updated. +//! +//! # Example for the 'Pico-ePaper-2.66-B' B/W/R Pi Pico Hat E-Ink Display +//! This example was created in an environment using the [Knurling](https://github.com/knurling-rs) ```flip-link```, ```defmt``` and ```probe-run``` tools - you will +//! need to adjust for your preferred setup. +//!```ignore +//!#![no_std] +//!#![no_main] +//!use epd_waveshare::{epd2in66b::*, prelude::*}; +//! +//!use cortex_m_rt::entry; +//!//use defmt::*; +//!use defmt_rtt as _; +//!use panic_probe as _; +//! +//!// Use embedded-graphics to create a bitmap to show +//!fn drawing() -> Display2in66b { +//! use embedded_graphics::{ +//! mono_font::{ascii::FONT_10X20, MonoTextStyle}, +//! prelude::*, +//! primitives::PrimitiveStyle, +//! text::{Alignment, Text}, +//! }; +//! +//! // Create a Display buffer to draw on, specific for this ePaper +//! let mut display = Display2in66b::default(); +//! +//! // Landscape mode, USB plug to the right +//! display.set_rotation(DisplayRotation::Rotate270); +//! +//! // Change the background from the default black to white +//! let _ = display +//! .bounding_box() +//! .into_styled(PrimitiveStyle::with_fill(TriColor::White)) +//! .draw(&mut display); +//! +//! // Draw some text on the buffer +//! let text = "Pico-ePaper-2.66 B/W/R"; +//! Text::with_alignment( +//! text, +//! display.bounding_box().center() + Point::new(1, 0), +//! MonoTextStyle::new(&FONT_10X20, TriColor::Black), +//! Alignment::Center, +//! ) +//! .draw(&mut display) +//! .unwrap(); +//! Text::with_alignment( +//! text, +//! display.bounding_box().center() + Point::new(0, 1), +//! MonoTextStyle::new(&FONT_10X20, TriColor::Chromatic), +//! Alignment::Center, +//! ) +//! .draw(&mut display) +//! .unwrap(); +//! +//! display +//!} +//! +//!#[entry] +//!fn main() -> ! { +//! use fugit::RateExtU32; +//! use rp_pico::hal::{ +//! self, +//! clocks::{init_clocks_and_plls, Clock}, +//! gpio::{FunctionSpi, PinState, Pins}, +//! pac, +//! sio::Sio, +//! watchdog::Watchdog, +//! }; +//! +//! // Boilerplate to access the peripherals +//! let mut pac = pac::Peripherals::take().unwrap(); +//! let core = pac::CorePeripherals::take().unwrap(); +//! let mut watchdog = Watchdog::new(pac.WATCHDOG); +//! let external_xtal_freq_hz = 12_000_000u32; +//! let clocks = init_clocks_and_plls( +//! external_xtal_freq_hz, +//! pac.XOSC, +//! pac.CLOCKS, +//! pac.PLL_SYS, +//! pac.PLL_USB, +//! &mut pac.RESETS, +//! &mut watchdog, +//! ) +//! .ok() +//! .unwrap(); +//! let sio = Sio::new(pac.SIO); +//! let pins = Pins::new( +//! pac.IO_BANK0, +//! pac.PADS_BANK0, +//! sio.gpio_bank0, +//! &mut pac.RESETS, +//! ); +//! +//! // Pin assignments of the Pi Pico-ePaper-2.66 Hat +//! let _ = pins.gpio10.into_mode::(); +//! let _ = pins.gpio11.into_mode::(); +//! let chip_select_pin = pins.gpio9.into_push_pull_output_in_state(PinState::High); +//! let is_busy_pin = pins.gpio13.into_floating_input(); +//! let data_or_command_pin = pins.gpio8.into_push_pull_output_in_state(PinState::High); +//! let reset_pin = pins.gpio12.into_push_pull_output_in_state(PinState::High); +//! +//! // SPI +//! let spi = hal::Spi::<_, _, 8>::new(pac.SPI1); +//! let mut spi = spi.init( +//! &mut pac.RESETS, +//! clocks.peripheral_clock.freq(), +//! 20_000_000u32.Hz(), // The SSD1675B docs say 20MHz max +//! &SPI_MODE, +//! ); +//! +//! // Delay +//! let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); +//! +//! // Setup the EPD driver +//! let mut e_paper = Epd2in66b::new( +//! &mut spi, +//! chip_select_pin, +//! is_busy_pin, +//! data_or_command_pin, +//! reset_pin, +//! &mut delay, +//! None, +//! ) +//! .unwrap(); +//! +//! // Create and fill a Display buffer +//! let display = drawing(); +//! +//! // Send the Display buffer to the ePaper RAM +//! e_paper +//! .update_color_frame( +//! &mut spi, +//! &mut delay, +//! &display.bw_buffer(), +//! &display.chromatic_buffer(), +//! ) +//! .unwrap(); +//! +//! // Render the ePaper RAM - takes time. +//! e_paper.display_frame(&mut spi, &mut delay).unwrap(); +//! +//! // Always turn off your EPD as much as possible - ePaper wears out while powered on. +//! e_paper.sleep(&mut spi, &mut delay).unwrap(); +//! +//! delay.delay_ms(60 * 1000); +//! +//! // Set the display all-white before storing your ePaper long-term. +//! e_paper.wake_up(&mut spi, &mut delay).unwrap(); +//! e_paper.clear_frame(&mut spi, &mut delay).unwrap(); +//! e_paper.display_frame(&mut spi, &mut delay).unwrap(); +//! e_paper.sleep(&mut spi, &mut delay).unwrap(); +//! +//! loop {} +//!} +//!``` + +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::color::TriColor; +use crate::interface::DisplayInterface; +use crate::traits::{ + InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, +}; + +pub(crate) mod command; +use self::command::*; +use crate::buffer_len; + +/// Display height in pixels. +pub const WIDTH: u32 = 152; +/// Display width in pixels +pub const HEIGHT: u32 = 296; + +const SINGLE_BYTE_WRITE: bool = true; + +/// White, display this during long-term storage +pub const DEFAULT_BACKGROUND_COLOR: TriColor = TriColor::White; + +/// A Display buffer configured with our extent and color depth. +#[cfg(feature = "graphics")] +pub type Display2in66b = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) * 2 }, + TriColor, +>; + +/// The EPD 2in66-B driver. +pub struct Epd2in66b { + interface: DisplayInterface, + background: TriColor, +} + +impl InternalWiAdditions + for Epd2in66b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // We follow the sequence of the Pi-Pico hat example code. + self.hw_reset(delay)?; + self.sw_reset(spi, delay)?; + self.data_entry_mode(spi, DataEntryRow::XMinor, DataEntrySign::IncYIncX)?; + self.set_display_window(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + self.update_control1( + spi, + WriteMode::Normal, + WriteMode::Normal, + OutputSource::S8ToS167, + )?; + self.set_cursor(spi, 0, 0)?; + + Ok(()) + } +} + +impl WaveshareThreeColorDisplay + for Epd2in66b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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.set_cursor(spi, 0, 0)?; + self.interface.cmd(spi, Command::WriteBlackWhiteRAM)?; + self.interface.data(spi, black) + } + + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.set_cursor(spi, 0, 0)?; + self.interface.cmd(spi, Command::WriteRedRAM)?; + self.interface.data(spi, chromatic) + } +} + +impl WaveshareDisplay + for Epd2in66b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = TriColor; + + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result + where + Self: Sized, + { + let mut epd = Self { + interface: DisplayInterface::new(busy, dc, rst, delay_us), + background: DEFAULT_BACKGROUND_COLOR, + }; + epd.init(spi, delay)?; + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.cmd_with_data( + spi, + Command::DeepSleepMode, + &[DeepSleep::SleepLosingRAM as u8], + ) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + fn set_background_color(&mut self, color: Self::DisplayColor) { + self.background = color; + } + + fn background_color(&self) -> &Self::DisplayColor { + &self.background + } + + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.set_cursor(spi, 0, 0)?; + self.update_achromatic_frame(spi, delay, buffer)?; + self.red_pattern(spi, delay, PatW::W160, PatH::H296, StartWith::Zero) // do NOT consider background here since red overrides other colors + } + + 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> { + self.set_display_window(spi, x, y, x + width, y + height)?; + self.set_cursor(spi, x, y)?; + self.update_achromatic_frame(spi, delay, buffer)?; + self.set_display_window(spi, 0, 0, WIDTH, HEIGHT) + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::MasterActivation)?; + self.wait_until_idle(delay) + } + + 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) + } + + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + let (white, red) = match self.background { + TriColor::Black => (StartWith::Zero, StartWith::Zero), + TriColor::White => (StartWith::One, StartWith::Zero), + TriColor::Chromatic => (StartWith::Zero, StartWith::One), + }; + self.black_white_pattern(spi, delay, PatW::W160, PatH::H296, white)?; + self.red_pattern(spi, delay, PatW::W160, PatH::H296, red) + } + + fn set_lut( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + _refresh_rate: Option, + ) -> Result<(), SPI::Error> { + Ok(()) + } + + fn wait_until_idle(&mut self, _spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(delay) + } +} + +// Helper functions that enforce some type and value constraints. Meant to help with code readability. They caught some of my silly errors -> yay rust!. +impl Epd2in66b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn wait_until_idle(&mut self, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.wait_until_idle(delay, false); + Ok(()) + } + fn hw_reset(&mut self, delay: &mut DELAY) -> Result<(), SPI::Error> { + // The initial delay is taken from other code here, the 2 ms comes from the SSD1675B datasheet. + self.interface.reset(delay, 20_000, 2_000); + self.wait_until_idle(delay) + } + fn sw_reset(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::Reset)?; + self.wait_until_idle(delay) + } + fn data_entry_mode( + &mut self, + spi: &mut SPI, + row: DataEntryRow, + sign: DataEntrySign, + ) -> Result<(), SPI::Error> { + self.interface + .cmd_with_data(spi, Command::DataEntryMode, &[row as u8 | sign as u8]) + } + fn set_display_window( + &mut self, + spi: &mut SPI, + xstart: u32, + ystart: u32, + xend: u32, + yend: u32, + ) -> Result<(), SPI::Error> { + self.interface.cmd_with_data( + spi, + Command::SetXAddressRange, + &[(((xstart >> 3) & 0x1f) as u8), (((xend >> 3) & 0x1f) as u8)], + )?; + self.interface.cmd_with_data( + spi, + Command::SetYAddressRange, + &[ + ((ystart & 0xff) as u8), + (((ystart >> 8) & 0x01) as u8), + ((yend & 0xff) as u8), + (((yend >> 8) & 0x01) as u8), + ], + ) + } + fn update_control1( + &mut self, + spi: &mut SPI, + red_mode: WriteMode, + bw_mode: WriteMode, + source: OutputSource, + ) -> Result<(), SPI::Error> { + self.interface.cmd_with_data( + spi, + Command::DisplayUpdateControl1, + &[((red_mode as u8) << 4 | bw_mode as u8), (source as u8)], + ) + } + + fn set_cursor(&mut self, spi: &mut SPI, x: u32, y: u32) -> Result<(), SPI::Error> { + self.interface.cmd_with_data( + spi, + Command::SetXAddressCounter, + &[((x >> 3) & 0x1f) as u8], + )?; + self.interface.cmd_with_data( + spi, + Command::SetYAddressCounter, + &[((y & 0xff) as u8), (((y >> 8) & 0x01) as u8)], + ) + } + + fn black_white_pattern( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + w: PatW, + h: PatH, + phase: StartWith, + ) -> Result<(), SPI::Error> { + self.interface.cmd_with_data( + spi, + Command::BlackWhiteRAMTestPattern, + &[phase as u8 | h as u8 | w as u8], + )?; + self.wait_until_idle(delay) + } + fn red_pattern( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + w: PatW, + h: PatH, + phase: StartWith, + ) -> Result<(), SPI::Error> { + self.interface.cmd_with_data( + spi, + Command::RedRAMTestPattern, + &[phase as u8 | h as u8 | w as u8], + )?; + self.wait_until_idle(delay) + } +} diff --git a/epd-waveshare/src/epd2in7b/command.rs b/epd-waveshare/src/epd2in7b/command.rs new file mode 100644 index 0000000..e32365c --- /dev/null +++ b/epd-waveshare/src/epd2in7b/command.rs @@ -0,0 +1,137 @@ +//! SPI Commands for the Waveshare 2.7" B 3 color E-Ink Display +use crate::traits; + +/// EPD2IN7B commands +/// +/// More information can be found in the [specification](https://www.waveshare.com/w/upload/d/d8/2.7inch-e-paper-b-specification.pdf) +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift direction, booster switch, soft reset + PanelSetting = 0x00, + /// Selecting internal and external power + PowerSetting = 0x01, + PowerOff = 0x02, + /// Setting Power OFF sequence + PowerOffSequenceSetting = 0x03, + PowerOn = 0x04, + /// This command enables the internal bandgap, which will be cleared by the next POF. + PowerOnMeasure = 0x05, + /// Starting data transmission + /// + /// ```ignore + /// self.send_data(&[0x07, 0x07, 0x17])?; + /// ``` + BoosterSoftStart = 0x06, + /// After this command is transmitted, the chip would enter the deep-sleep mode to save power. + /// + /// The deep sleep mode would return to standby by hardware reset. + /// + /// The only one parameter is a check code, the command would be excuted if check code = 0xA5. + DeepSleep = 0x07, + /// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data + /// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel. + /// + /// - In B/W mode, this command writes “OLD” data to SRAM. + /// - In B/W/Red mode, this command writes “B/W” data to SRAM. + DataStartTransmission1 = 0x10, + /// Stopping data transmission + DataStop = 0x11, + /// After this command is issued, driver will refresh display (data/VCOM) according to SRAM data and LUT. + DisplayRefresh = 0x12, + /// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data + /// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel. + /// - In B/W mode, this command writes “NEW” data to SRAM. + /// - In B/W/Red mode, this command writes “RED” data to SRAM. + DataStartTransmission2 = 0x13, + /// The command define as follows: The register is indicates that user start to transmit data, then write to SRAM. While data transmission + /// complete, user must send command DSP (Data transmission Stop). Then chip will start to send data/VCOM for panel. + /// + /// - In B/W mode, this command writes “OLD” data to SRAM. + /// - In B/W/Red mode, this command writes “B/W” data to SRAM. + PartialDataStartTransmission1 = 0x14, + /// The command define as follows: The register is indicates that user start to transmit data, then write to SRAM. While data transmission + /// complete, user must send command DSP (Data transmission Stop). Then chip will start to send data/VCOM for panel. + /// + /// - In B/W mode, this command writes “NEW” data to SRAM. + /// - In B/W/Red mode, this command writes “RED” data to SRAM. + PartialDataStartTransmission2 = 0x15, + /// While user sent this command, driver will refresh display (data/VCOM) base on SRAM data and LUT. + /// + /// Only the area (X,Y, W, L) would update, the others pixel output would follow VCOM LUT + PartialDisplayRefresh = 0x16, + /// This command builds the Look-up table for VCOM + LutForVcom = 0x20, + LutWhiteToWhite = 0x21, + LutBlackToWhite = 0x22, + LutWhiteToBlack = 0x23, + LutBlackToBlack = 0x24, + /// The command controls the PLL clock frequency. + PllControl = 0x30, + /// This command reads the temperature sensed by the temperature sensor. + /// + /// Doesn't work! Waveshare doesn't connect the read pin + TemperatureSensor = 0x40, + /// This command selects Internal or External temperature sensor. + TemperatureSensorCalibration = 0x41, + /// Write External Temperature Sensor + TemperatureSensorWrite = 0x42, + /// Read External Temperature Sensor + /// + /// Doesn't work! Waveshare doesn't connect the read pin + TemperatureSensorRead = 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) + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn the battery condition. + LowPowerDetection = 0x51, + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority than the RES\[1:0\] in R00H (PSR). + ResolutionSetting = 0x61, + SourceAndGateSetting = 0x62, + /// This command reads the IC status. + /// + /// Doesn't work! Waveshare doesn't connect the read pin + GetStatus = 0x71, + /// Automatically measure VCOM. This command reads the IC status + AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value + /// + /// Doesn't work! Waveshare doesn't connect the read pin + ReadVcomValue = 0x81, + /// This command sets VCOM_DC value. + VcmDcSetting = 0x82, + /// After this command is issued, the chip would enter the program mode. + /// + /// After the programming procedure completed, a hardware reset is necessary for leaving program mode. + /// + /// The only one parameter is a check code, the command would be excuted if check code = 0xA5. + ProgramMode = 0xA0, + /// After this command is issued, the chip would enter the program mode. + ActiveProgramming = 0xA1, + /// The command is used for reading the content of OTP for checking the data of programming. + /// + /// The value of (n) is depending on the amount of programmed data, tha max address = 0xFFF. + ReadOtp = 0xA2, + /// Not shown in commands table, but used in init sequence + PowerOptimization = 0xf8, +} + +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::PanelSetting.address(), 0x00); + assert_eq!(Command::DisplayRefresh.address(), 0x12); + } +} diff --git a/epd-waveshare/src/epd2in7b/constants.rs b/epd-waveshare/src/epd2in7b/constants.rs new file mode 100644 index 0000000..f850f97 --- /dev/null +++ b/epd-waveshare/src/epd2in7b/constants.rs @@ -0,0 +1,55 @@ +#[rustfmt::skip] +pub(crate) const LUT_VCOM_DC: [u8; 44] = [ +0x00, 0x00, +0x00, 0x1A, 0x1A, 0x00, 0x00, 0x01, +0x00, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0x00, 0x0E, 0x01, 0x0E, 0x01, 0x10, +0x00, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0x00, 0x04, 0x10, 0x00, 0x00, 0x05, +0x00, 0x03, 0x0E, 0x00, 0x00, 0x0A, +0x00, 0x23, 0x00, 0x00, 0x00, 0x01, +]; + +#[rustfmt::skip] +pub(crate) const LUT_WW: [u8; 42] =[ +0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, +0x40, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, +0x80, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0x00, 0x04, 0x10, 0x00, 0x00, 0x05, +0x00, 0x03, 0x0E, 0x00, 0x00, 0x0A, +0x00, 0x23, 0x00, 0x00, 0x00, 0x01, +]; + +#[rustfmt::skip] +pub(crate) const LUT_BW: [u8; 42] =[ +0xA0, 0x1A, 0x1A, 0x00, 0x00, 0x01, +0x00, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, +0x90, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0xB0, 0x04, 0x10, 0x00, 0x00, 0x05, +0xB0, 0x03, 0x0E, 0x00, 0x00, 0x0A, +0xC0, 0x23, 0x00, 0x00, 0x00, 0x01, +]; + +#[rustfmt::skip] +pub(crate) const LUT_BB: [u8; 42] =[ +0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, +0x40, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, +0x80, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0x00, 0x04, 0x10, 0x00, 0x00, 0x05, +0x00, 0x03, 0x0E, 0x00, 0x00, 0x0A, +0x00, 0x23, 0x00, 0x00, 0x00, 0x01, +]; + +#[rustfmt::skip] +pub(crate) const LUT_WB: [u8; 42] =[ +0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, +0x20, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, +0x10, 0x0A, 0x0A, 0x00, 0x00, 0x08, +0x00, 0x04, 0x10, 0x00, 0x00, 0x05, +0x00, 0x03, 0x0E, 0x00, 0x00, 0x0A, +0x00, 0x23, 0x00, 0x00, 0x00, 0x01, +]; diff --git a/epd-waveshare/src/epd2in7b/mod.rs b/epd-waveshare/src/epd2in7b/mod.rs new file mode 100644 index 0000000..42f25f1 --- /dev/null +++ b/epd-waveshare/src/epd2in7b/mod.rs @@ -0,0 +1,464 @@ +//! A simple Driver for the Waveshare 2.7" B Tri-Color E-Ink Display via SPI +//! +//! [Documentation](https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT_(B)) + +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::interface::DisplayInterface; +use crate::traits::{ + InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, +}; + +// The Lookup Tables for the Display +mod constants; +use crate::epd2in7b::constants::*; + +/// Width of the display +pub const WIDTH: u32 = 176; +/// Height of the display +pub const HEIGHT: u32 = 264; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = true; +const SINGLE_BYTE_WRITE: bool = true; + +use crate::color::Color; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 2in7B EPD +/// TODO this should be a TriColor, but let's keep it as is at first +#[cfg(feature = "graphics")] +pub type Display2in7b = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Epd2in7b driver +pub struct Epd2in7b { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: Color, +} + +impl InternalWiAdditions + for Epd2in7b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // reset the device + self.interface.reset(delay, 10_000, 2_000); + + // power on + self.command(spi, Command::PowerOn)?; + delay.delay_us(5000); + self.wait_until_idle(spi, delay)?; + + // set panel settings, 0xbf is bw, 0xaf is multi-color + self.interface + .cmd_with_data(spi, Command::PanelSetting, &[0xaf])?; + + // pll control + self.interface + .cmd_with_data(spi, Command::PllControl, &[0x3a])?; + + // set the power settings + self.interface.cmd_with_data( + spi, + Command::PowerSetting, + &[0x03, 0x00, 0x2b, 0x2b, 0x09], + )?; + + // start the booster + self.interface + .cmd_with_data(spi, Command::BoosterSoftStart, &[0x07, 0x07, 0x17])?; + + // power optimization + self.interface + .cmd_with_data(spi, Command::PowerOptimization, &[0x60, 0xa5])?; + self.interface + .cmd_with_data(spi, Command::PowerOptimization, &[0x89, 0xa5])?; + self.interface + .cmd_with_data(spi, Command::PowerOptimization, &[0x90, 0x00])?; + self.interface + .cmd_with_data(spi, Command::PowerOptimization, &[0x93, 0x2a])?; + self.interface + .cmd_with_data(spi, Command::PowerOptimization, &[0x73, 0x41])?; + + self.interface + .cmd_with_data(spi, Command::VcmDcSetting, &[0x12])?; + + self.interface + .cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x87])?; + + self.set_lut(spi, delay, None)?; + + self.interface + .cmd_with_data(spi, Command::PartialDisplayRefresh, &[0x00])?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd2in7b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd2in7b { 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, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0xf7])?; + + self.command(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + _delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.send_buffer_helper(spi, buffer)?; + + // Clear chromatic layer since we won't be using it here + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface + .data_x_times(spi, !self.color.get_byte_value(), WIDTH / 8 * HEIGHT)?; + + self.interface.cmd(spi, Command::DataStop)?; + 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> { + self.interface + .cmd(spi, Command::PartialDataStartTransmission1)?; + + self.send_data(spi, &[(x >> 8) as u8])?; + self.send_data(spi, &[(x & 0xf8) as u8])?; + self.send_data(spi, &[(y >> 8) as u8])?; + self.send_data(spi, &[(y & 0xff) as u8])?; + self.send_data(spi, &[(width >> 8) as u8])?; + self.send_data(spi, &[(width & 0xf8) as u8])?; + self.send_data(spi, &[(height >> 8) as u8])?; + self.send_data(spi, &[(height & 0xff) as u8])?; + self.wait_until_idle(spi, delay)?; + + self.send_buffer_helper(spi, buffer)?; + + self.interface.cmd(spi, Command::DataStop) + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.command(spi, Command::DisplayRefresh)?; + 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.command(spi, Command::DisplayRefresh)?; + Ok(()) + } + + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + + let color_value = self.color.get_byte_value(); + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface + .data_x_times(spi, color_value, WIDTH / 8 * HEIGHT)?; + + self.interface.cmd(spi, Command::DataStop)?; + + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface + .data_x_times(spi, color_value, WIDTH / 8 * HEIGHT)?; + self.interface.cmd(spi, Command::DataStop)?; + 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, + delay: &mut DELAY, + _refresh_rate: Option, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::LutForVcom, &LUT_VCOM_DC)?; + self.cmd_with_data(spi, Command::LutWhiteToWhite, &LUT_WW)?; + self.cmd_with_data(spi, Command::LutBlackToWhite, &LUT_BW)?; + self.cmd_with_data(spi, Command::LutWhiteToBlack, &LUT_WB)?; + self.cmd_with_data(spi, Command::LutBlackToBlack, &LUT_BB)?; + Ok(()) + } + + 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 WaveshareThreeColorDisplay + for Epd2in7b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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) + } + + /// Update only the black/white data of the display. + /// + /// Finish by calling `update_chromatic_frame`. + fn update_achromatic_frame( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + achromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + self.send_buffer_helper(spi, achromatic)?; + + self.interface.cmd(spi, Command::DataStop) + } + + /// Update only chromatic data of the display. + /// + /// This data takes precedence over the black/white data. + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission2)?; + + self.send_buffer_helper(spi, chromatic)?; + + self.interface.cmd(spi, Command::DataStop)?; + self.wait_until_idle(spi, delay)?; + + Ok(()) + } +} + +impl Epd2in7b +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_buffer_helper(&mut self, spi: &mut SPI, buffer: &[u8]) -> Result<(), SPI::Error> { + // Based on the waveshare implementation, all data for color values is flipped. This helper + // method makes that transmission easier + for b in buffer.iter() { + self.send_data(spi, &[!b])?; + } + Ok(()) + } + + fn cmd_with_data( + &mut self, + spi: &mut SPI, + command: Command, + data: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd_with_data(spi, command, data) + } + + /// Refresh display for partial frame + pub fn display_partial_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + self.command(spi, Command::PartialDisplayRefresh)?; + self.send_data(spi, &[(x >> 8) as u8])?; + self.send_data(spi, &[(x & 0xf8) as u8])?; + self.send_data(spi, &[(y >> 8) as u8])?; + self.send_data(spi, &[(y & 0xff) as u8])?; + self.send_data(spi, &[(width >> 8) as u8])?; + self.send_data(spi, &[(width & 0xf8) as u8])?; + self.send_data(spi, &[(height >> 8) as u8])?; + self.send_data(spi, &[(height & 0xff) as u8])?; + self.wait_until_idle(spi, delay)?; + Ok(()) + } + + /// Update black/achromatic frame + #[allow(clippy::too_many_arguments)] + pub fn update_partial_achromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + achromatic: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + self.interface + .cmd(spi, Command::PartialDataStartTransmission1)?; + self.send_data(spi, &[(x >> 8) as u8])?; + self.send_data(spi, &[(x & 0xf8) as u8])?; + self.send_data(spi, &[(y >> 8) as u8])?; + self.send_data(spi, &[(y & 0xff) as u8])?; + self.send_data(spi, &[(width >> 8) as u8])?; + self.send_data(spi, &[(width & 0xf8) as u8])?; + self.send_data(spi, &[(height >> 8) as u8])?; + self.send_data(spi, &[(height & 0xff) as u8])?; + self.wait_until_idle(spi, delay)?; + + for b in achromatic.iter() { + // Flipping based on waveshare implementation + self.send_data(spi, &[!b])?; + } + + Ok(()) + } + + /// Update partial chromatic/red frame + #[allow(clippy::too_many_arguments)] + pub fn update_partial_chromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + chromatic: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + self.interface + .cmd(spi, Command::PartialDataStartTransmission2)?; + self.send_data(spi, &[(x >> 8) as u8])?; + self.send_data(spi, &[(x & 0xf8) as u8])?; + self.send_data(spi, &[(y >> 8) as u8])?; + self.send_data(spi, &[(y & 0xff) as u8])?; + self.send_data(spi, &[(width >> 8) as u8])?; + self.send_data(spi, &[(width & 0xf8) as u8])?; + self.send_data(spi, &[(height >> 8) as u8])?; + self.send_data(spi, &[(height & 0xff) as u8])?; + self.wait_until_idle(spi, delay)?; + + for b in chromatic.iter() { + // Flipping based on waveshare implementation + self.send_data(spi, &[!b])?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 176); + assert_eq!(HEIGHT, 264); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd2in9/mod.rs b/epd-waveshare/src/epd2in9/mod.rs new file mode 100644 index 0000000..48c5bdc --- /dev/null +++ b/epd-waveshare/src/epd2in9/mod.rs @@ -0,0 +1,388 @@ +//! A simple Driver for the Waveshare 2.9" E-Ink Display via SPI +//! +//! +//! # Example for the 2.9 in E-Ink Display +//! +//!```rust, no_run +//!# use embedded_hal_mock::eh1::*; +//!# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +//!use embedded_graphics::{ +//! pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle}, +//!}; +//!use epd_waveshare::{epd2in9::*, 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::NoopDelay::new(); +//! +//!// Setup EPD +//!let mut epd = Epd2in9::new(&mut spi, busy_in, dc, rst, &mut delay, None)?; +//! +//!// Use display graphics from embedded-graphics +//!let mut display = Display2in9::default(); +//! +//!// Use embedded graphics for drawing a line +//!let _ = Line::new(Point::new(0, 120), Point::new(0, 295)) +//! .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1)) +//! .draw(&mut display); +//! +//! // Display updated frame +//!epd.update_frame(&mut spi, &display.buffer(), &mut delay)?; +//!epd.display_frame(&mut spi, &mut delay)?; +//! +//!// Set the EPD to sleep +//!epd.sleep(&mut spi, &mut delay)?; +//!# Ok(()) +//!# } +//!``` + +/// Width of epd2in9 in pixels +pub const WIDTH: u32 = 128; +/// Height of epd2in9 in pixels +pub const HEIGHT: u32 = 296; +/// Default Background Color (white) +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = false; +const SINGLE_BYTE_WRITE: bool = true; + +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::type_a::{ + command::Command, + constants::{LUT_FULL_UPDATE, LUT_PARTIAL_UPDATE}, +}; + +use crate::color::Color; + +use crate::traits::*; + +use crate::buffer_len; +use crate::interface::DisplayInterface; + +/// Display with Fullsize buffer for use with the 2in9 EPD +#[cfg(feature = "graphics")] +pub type Display2in9 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Epd2in9 driver +/// +pub struct Epd2in9 { + /// SPI + interface: DisplayInterface, + /// Color + background_color: Color, + /// Refresh LUT + refresh: RefreshLut, +} + +impl Epd2in9 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.reset(delay, 10_000, 10_000); + + self.wait_until_idle(spi, delay)?; + + // 3 Databytes: + // A[7:0] + // 0.. A[8] + // 0.. B[2:0] + // Default Values: A = Height of Screen (0x127), B = 0x00 (GD, SM and TB=0?) + self.interface + .cmd_with_data(spi, Command::DriverOutputControl, &[0x27, 0x01, 0x00])?; + + // 3 Databytes: (and default values from datasheet and arduino) + // 1 .. A[6:0] = 0xCF | 0xD7 + // 1 .. B[6:0] = 0xCE | 0xD6 + // 1 .. C[6:0] = 0x8D | 0x9D + //TODO: test + self.interface + .cmd_with_data(spi, Command::BoosterSoftStartControl, &[0xD7, 0xD6, 0x9D])?; + + // One Databyte with value 0xA8 for 7V VCOM + self.interface + .cmd_with_data(spi, Command::WriteVcomRegister, &[0xA8])?; + + // One Databyte with default value 0x1A for 4 dummy lines per gate + self.interface + .cmd_with_data(spi, Command::SetDummyLinePeriod, &[0x1A])?; + + // One Databyte with default value 0x08 for 2us per line + self.interface + .cmd_with_data(spi, Command::SetGateLineWidth, &[0x08])?; + + // One Databyte with default value 0x03 + // -> address: x increment, y increment, address counter is updated in x direction + self.interface + .cmd_with_data(spi, Command::DataEntryModeSetting, &[0x03])?; + + self.set_lut(spi, delay, None) + } +} + +impl WaveshareDisplay + for Epd2in9 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + + let mut epd = Epd2in9 { + interface, + background_color: DEFAULT_BACKGROUND_COLOR, + refresh: RefreshLut::Full, + }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // 0x00 for Normal mode (Power on Reset), 0x01 for Deep Sleep Mode + //TODO: is 0x00 needed here? (see also epd1in54) + self.interface + .cmd_with_data(spi, Command::DeepSleepMode, &[0x00])?; + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.init(spi, delay)?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.use_full_frame(spi, delay)?; + + self.interface + .cmd_with_data(spi, Command::WriteRam, buffer)?; + Ok(()) + } + + //TODO: update description: last 3 bits will be ignored for width and x_pos + 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> { + self.wait_until_idle(spi, delay)?; + self.set_ram_area(spi, x, y, x + width, y + height)?; + self.set_ram_counter(spi, delay, x, y)?; + + self.interface + .cmd_with_data(spi, Command::WriteRam, buffer)?; + Ok(()) + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // enable clock signal, enable cp, display pattern -> 0xC4 (tested with the arduino version) + //TODO: test control_1 or control_2 with default value 0xFF (from the datasheet) + self.interface + .cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xC4])?; + + self.interface.cmd(spi, Command::MasterActivation)?; + // MASTER Activation should not be interupted to avoid currption of panel images + // therefore a terminate command is send + self.interface.cmd(spi, Command::Nop)?; + 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.wait_until_idle(spi, delay)?; + self.use_full_frame(spi, delay)?; + + // clear the ram with the background color + let color = self.background_color.get_byte_value(); + + self.interface.cmd(spi, Command::WriteRam)?; + self.interface + .data_x_times(spi, color, WIDTH / 8 * HEIGHT)?; + Ok(()) + } + + fn set_background_color(&mut self, background_color: Color) { + self.background_color = background_color; + } + + fn background_color(&self) -> &Color { + &self.background_color + } + + fn set_lut( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + refresh_rate: Option, + ) -> Result<(), SPI::Error> { + if let Some(refresh_lut) = refresh_rate { + self.refresh = refresh_lut; + } + match self.refresh { + RefreshLut::Full => self.set_lut_helper(spi, delay, &LUT_FULL_UPDATE), + RefreshLut::Quick => self.set_lut_helper(spi, delay, &LUT_PARTIAL_UPDATE), + } + } + + 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 Epd2in9 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn use_full_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // choose full frame/ram + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + + // start from the beginning + self.set_ram_counter(spi, delay, 0, 0) + } + + fn set_ram_area( + &mut self, + spi: &mut SPI, + start_x: u32, + start_y: u32, + end_x: u32, + end_y: u32, + ) -> Result<(), SPI::Error> { + assert!(start_x < end_x); + assert!(start_y < end_y); + + // x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram + // aren't relevant + self.interface.cmd_with_data( + spi, + Command::SetRamXAddressStartEndPosition, + &[(start_x >> 3) as u8, (end_x >> 3) as u8], + )?; + + // 2 Databytes: A[7:0] & 0..A[8] for each - start and end + self.interface.cmd_with_data( + spi, + Command::SetRamYAddressStartEndPosition, + &[ + start_y as u8, + (start_y >> 8) as u8, + end_y as u8, + (end_y >> 8) as u8, + ], + ) + } + + fn set_ram_counter( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + x: u32, + y: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram + // aren't relevant + self.interface + .cmd_with_data(spi, Command::SetRamXAddressCounter, &[(x >> 3) as u8])?; + + // 2 Databytes: A[7:0] & 0..A[8] + self.interface.cmd_with_data( + spi, + Command::SetRamYAddressCounter, + &[y as u8, (y >> 8) as u8], + )?; + Ok(()) + } + + /// Set your own LUT, this function is also used internally for set_lut + fn set_lut_helper( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + assert!(buffer.len() == 30); + self.interface + .cmd_with_data(spi, Command::WriteLutRegister, buffer)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 128); + assert_eq!(HEIGHT, 296); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd2in9_v2/mod.rs b/epd-waveshare/src/epd2in9_v2/mod.rs new file mode 100644 index 0000000..3a36fdf --- /dev/null +++ b/epd-waveshare/src/epd2in9_v2/mod.rs @@ -0,0 +1,535 @@ +//! A simple Driver for the Waveshare 2.9" E-Ink Display V2 via SPI +//! +//! Specification: +//! +//! # Example for the 2.9 in E-Ink Display V2 +//! +//!```rust, no_run +//!# use embedded_hal_mock::eh1::*; +//!# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +//!use embedded_graphics::{ +//! pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle}, +//!}; +//!use epd_waveshare::{epd2in9_v2::*, 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::NoopDelay::new(); +//! +//!// Setup EPD +//!let mut epd = Epd2in9::new(&mut spi, busy_in, dc, rst, &mut delay, None)?; +//! +//!// Use display graphics from embedded-graphics +//!let mut display = Display2in9::default(); +//! +//!// Use embedded graphics for drawing a line +//!let _ = Line::new(Point::new(0, 120), Point::new(0, 295)) +//! .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1)) +//! .draw(&mut display); +//! +//!// Display updated frame +//!epd.update_frame(&mut spi, &display.buffer(), &mut delay)?; +//!epd.display_frame(&mut spi, &mut delay)?; +//! +//!// Draw something new here +//! +//!// Display new image as a base image for further quick refreshes +//!epd.update_old_frame(&mut spi, &display.buffer(), &mut delay)?; +//!epd.display_frame(&mut spi, &mut delay)?; +//! +//!// Update image here +//! +//!// quick refresh of updated pixels +//!epd.update_new_frame(&mut spi, &display.buffer(), &mut delay)?; +//!epd.display_new_frame(&mut spi, &mut delay)?; +//! +//!// Set the EPD to sleep +//!epd.sleep(&mut spi, &mut delay)?; +//!# Ok(()) +//!# } +//!``` + +/// Width of epd2in9 in pixels +pub const WIDTH: u32 = 128; +/// Height of epd2in9 in pixels +pub const HEIGHT: u32 = 296; +/// Default Background Color (white) +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = false; +const SINGLE_BYTE_WRITE: bool = true; + +const LUT_PARTIAL_2IN9: [u8; 159] = [ + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x0, 0x0, 0x0, 0x22, 0x17, 0x41, 0xB0, 0x32, 0x36, +]; + +const WS_20_30: [u8; 159] = [ + 0x80, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x10, 0x66, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x80, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, + 0x10, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x8, 0x0, 0x0, 0x0, 0x0, 0x1, 0xA, 0xA, 0x0, 0xA, 0xA, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x8, 0x0, 0x1, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x44, 0x44, 0x44, 0x44, + 0x44, 0x44, 0x0, 0x0, 0x0, 0x22, 0x17, 0x41, 0x0, 0x32, 0x36, +]; + +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::type_a::command::Command; + +use crate::color::Color; + +use crate::traits::*; + +use crate::buffer_len; +use crate::interface::DisplayInterface; +use crate::traits::QuickRefresh; + +/// Display with Fullsize buffer for use with the 2in9 EPD V2 +#[cfg(feature = "graphics")] +pub type Display2in9 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Epd2in9 driver +/// +pub struct Epd2in9 { + /// SPI + interface: DisplayInterface, + /// Color + background_color: Color, + /// Refresh LUT + refresh: RefreshLut, +} + +impl Epd2in9 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.reset(delay, 10_000, 2_000); + + self.wait_until_idle(spi, delay)?; + self.interface.cmd(spi, Command::SwReset)?; + self.wait_until_idle(spi, delay)?; + + // 3 Databytes: + // A[7:0] + // 0.. A[8] + // 0.. B[2:0] + // Default Values: A = Height of Screen (0x127), B = 0x00 (GD, SM and TB=0?) + self.interface + .cmd_with_data(spi, Command::DriverOutputControl, &[0x27, 0x01, 0x00])?; + + // One Databyte with default value 0x03 + // -> address: x increment, y increment, address counter is updated in x direction + self.interface + .cmd_with_data(spi, Command::DataEntryModeSetting, &[0x03])?; + + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + + self.interface + .cmd_with_data(spi, Command::DisplayUpdateControl1, &[0x00, 0x80])?; + + self.set_ram_counter(spi, delay, 0, 0)?; + + self.wait_until_idle(spi, delay)?; + + // set LUT by host + self.set_lut_helper(spi, delay, &WS_20_30[0..153])?; + self.interface + .cmd_with_data(spi, Command::WriteLutRegisterEnd, &WS_20_30[153..154])?; + self.interface + .cmd_with_data(spi, Command::GateDrivingVoltage, &WS_20_30[154..155])?; + self.interface + .cmd_with_data(spi, Command::SourceDrivingVoltage, &WS_20_30[155..158])?; + self.interface + .cmd_with_data(spi, Command::WriteVcomRegister, &WS_20_30[158..159])?; + + Ok(()) + } +} + +impl WaveshareDisplay + for Epd2in9 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + + let mut epd = Epd2in9 { + interface, + background_color: DEFAULT_BACKGROUND_COLOR, + refresh: RefreshLut::Full, + }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // 0x00 for Normal mode (Power on Reset), 0x01 for Deep Sleep Mode + self.interface + .cmd_with_data(spi, Command::DeepSleepMode, &[0x01])?; + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay)?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.interface.cmd_with_data(spi, Command::WriteRam, buffer) + } + + 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> { + //TODO This is copied from epd2in9 but it seems not working. Partial refresh supported by version 2? + self.wait_until_idle(spi, delay)?; + self.set_ram_area(spi, x, y, x + width, y + height)?; + self.set_ram_counter(spi, delay, x, y)?; + + self.interface + .cmd_with_data(spi, Command::WriteRam, buffer)?; + Ok(()) + } + + /// actually is the "Turn on Display" sequence + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // Enable clock signal, Enable Analog, Load temperature value, DISPLAY with DISPLAY Mode 1, Disable Analog, Disable OSC + self.interface + .cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xC7])?; + self.interface.cmd(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.wait_until_idle(spi, delay)?; + + // clear the ram with the background color + let color = self.background_color.get_byte_value(); + + self.interface.cmd(spi, Command::WriteRam)?; + self.interface + .data_x_times(spi, color, WIDTH / 8 * HEIGHT)?; + self.interface.cmd(spi, Command::WriteRam2)?; + self.interface.data_x_times(spi, color, WIDTH / 8 * HEIGHT) + } + + fn set_background_color(&mut self, background_color: Color) { + self.background_color = background_color; + } + + fn background_color(&self) -> &Color { + &self.background_color + } + + fn set_lut( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + refresh_rate: Option, + ) -> Result<(), SPI::Error> { + if let Some(refresh_lut) = refresh_rate { + self.refresh = refresh_lut; + } + Ok(()) + } + + 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 Epd2in9 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn use_full_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // choose full frame/ram + self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; + + // start from the beginning + self.set_ram_counter(spi, delay, 0, 0) + } + + fn set_ram_area( + &mut self, + spi: &mut SPI, + start_x: u32, + start_y: u32, + end_x: u32, + end_y: u32, + ) -> Result<(), SPI::Error> { + assert!(start_x < end_x); + assert!(start_y < end_y); + + // x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram + // aren't relevant + self.interface.cmd_with_data( + spi, + Command::SetRamXAddressStartEndPosition, + &[(start_x >> 3) as u8, (end_x >> 3) as u8], + )?; + + // 2 Databytes: A[7:0] & 0..A[8] for each - start and end + self.interface.cmd_with_data( + spi, + Command::SetRamYAddressStartEndPosition, + &[ + start_y as u8, + (start_y >> 8) as u8, + end_y as u8, + (end_y >> 8) as u8, + ], + ) + } + + fn set_ram_counter( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + x: u32, + y: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram + // aren't relevant + self.interface + .cmd_with_data(spi, Command::SetRamXAddressCounter, &[x as u8])?; + + // 2 Databytes: A[7:0] & 0..A[8] + self.interface.cmd_with_data( + spi, + Command::SetRamYAddressCounter, + &[y as u8, (y >> 8) as u8], + )?; + Ok(()) + } + + /// Set your own LUT, this function is also used internally for set_lut + fn set_lut_helper( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::WriteLutRegister, buffer)?; + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl QuickRefresh + for Epd2in9 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// To be followed immediately by `update_new_frame`. + fn update_old_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::WriteRam2, buffer) + } + + /// To be used immediately after `update_old_frame`. + fn update_new_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.interface.reset(delay, 10_000, 2_000); + + self.set_lut_helper(spi, delay, &LUT_PARTIAL_2IN9)?; + self.interface.cmd_with_data( + spi, + Command::WriteOtpSelection, + &[0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00], + )?; + self.interface + .cmd_with_data(spi, Command::BorderWaveformControl, &[0x80])?; + self.interface + .cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xC0])?; + self.interface.cmd(spi, Command::MasterActivation)?; + + self.wait_until_idle(spi, delay)?; + + self.use_full_frame(spi, delay)?; + + self.interface + .cmd_with_data(spi, Command::WriteRam, buffer)?; + Ok(()) + } + + /// For a quick refresh of the new updated frame. To be used immediately after `update_new_frame` + fn display_new_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::DisplayUpdateControl2, &[0x0F])?; + self.interface.cmd(spi, Command::MasterActivation)?; + self.wait_until_idle(spi, delay)?; + Ok(()) + } + + /// Updates and displays the new frame. + fn update_and_display_new_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.update_new_frame(spi, buffer, delay)?; + self.display_new_frame(spi, delay)?; + Ok(()) + } + + /// Partial quick refresh not supported yet + #[allow(unused)] + fn update_partial_old_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + //TODO supported by display? + unimplemented!() + } + + /// Partial quick refresh not supported yet + #[allow(unused)] + fn update_partial_new_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + //TODO supported by display? + unimplemented!() + } + + /// Partial quick refresh not supported yet + #[allow(unused)] + fn clear_partial_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + //TODO supported by display? + unimplemented!() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 128); + assert_eq!(HEIGHT, 296); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd2in9bc/command.rs b/epd-waveshare/src/epd2in9bc/command.rs new file mode 100644 index 0000000..7121c50 --- /dev/null +++ b/epd-waveshare/src/epd2in9bc/command.rs @@ -0,0 +1,38 @@ +//! SPI Commands for the Waveshare 2.9" (B/C) E-Ink Display +use crate::traits; + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + PanelSetting = 0x00, + + PowerSetting = 0x01, + PowerOff = 0x02, + PowerOn = 0x04, + BoosterSoftStart = 0x06, + DeepSleep = 0x07, + DataStartTransmission1 = 0x10, + DisplayRefresh = 0x12, + DataStartTransmission2 = 0x13, + + LutForVcom = 0x20, + LutWhiteToWhite = 0x21, + LutBlackToWhite = 0x22, + LutWhiteToBlack = 0x23, + LutBlackToBlack = 0x24, + + PllControl = 0x30, + TemperatureSensor = 0x40, + TemperatureSensorSelection = 0x41, + VcomAndDataIntervalSetting = 0x50, + ResolutionSetting = 0x61, + VcmDcSetting = 0x82, + PowerSaving = 0xE3, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} diff --git a/epd-waveshare/src/epd2in9bc/mod.rs b/epd-waveshare/src/epd2in9bc/mod.rs new file mode 100644 index 0000000..0f6d52b --- /dev/null +++ b/epd-waveshare/src/epd2in9bc/mod.rs @@ -0,0 +1,397 @@ +//! A simple Driver for the Waveshare 2.9" (B/C) E-Ink Display via SPI +//! +//! # Example for the 2.9" E-Ink Display +//! +//!```rust, no_run +//!# use embedded_hal_mock::eh1::*; +//!# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +//!use embedded_graphics::{ +//! pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle}, +//!}; +//!use epd_waveshare::{epd2in9bc::*, 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::NoopDelay::new(); +//! +//!// Setup EPD +//!let mut epd = Epd2in9bc::new(&mut spi, busy_in, dc, rst, &mut delay, None)?; +//! +//!// Use display graphics from embedded-graphics +//!// This display is for the black/white pixels +//!let mut mono_display = Display2in9bc::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(Color::Black, 1)) +//! .draw(&mut mono_display); +//! +//!// Use a second display for red/yellow +//!let mut chromatic_display = Display2in9bc::default(); +//! +//!// We use `Black` but it will be shown as red/yellow +//!let _ = Line::new(Point::new(15, 120), Point::new(15, 200)) +//! .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1)) +//! .draw(&mut chromatic_display); +//! +//!// Display updated frame +//!epd.update_color_frame( +//! &mut spi, +//! &mut delay, +//! &mono_display.buffer(), +//! &chromatic_display.buffer() +//!)?; +//!epd.display_frame(&mut spi, &mut delay)?; +//! +//!// Set the EPD to sleep +//!epd.sleep(&mut spi, &mut delay)?; +//!# Ok(()) +//!# } +//!``` +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::interface::DisplayInterface; +use crate::traits::{ + InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, +}; + +/// Width of epd2in9bc in pixels +pub const WIDTH: u32 = 128; +/// Height of epd2in9bc in pixels +pub const HEIGHT: u32 = 296; +/// Default background color (white) of epd2in9bc display +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; + +const NUM_DISPLAY_BITS: u32 = WIDTH / 8 * HEIGHT; + +const IS_BUSY_LOW: bool = true; +const VCOM_DATA_INTERVAL: u8 = 0x07; +const WHITE_BORDER: u8 = 0x70; +const BLACK_BORDER: u8 = 0x30; +const CHROMATIC_BORDER: u8 = 0xb0; +const FLOATING_BORDER: u8 = 0xF0; +const SINGLE_BYTE_WRITE: bool = true; + +use crate::color::{Color, TriColor}; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 2in9b/c EPD +/// TODO this should be a TriColor, but let's keep it as is at first +#[cfg(feature = "graphics")] +pub type Display2in9bc = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Epd2in9bc driver +pub struct Epd2in9bc { + interface: DisplayInterface, + color: Color, +} + +impl InternalWiAdditions + for Epd2in9bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Values taken from datasheet and sample code + + self.interface.reset(delay, 10_000, 10_000); + + // start the booster + self.interface + .cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x17])?; + + // power on + self.command(spi, Command::PowerOn)?; + delay.delay_us(5000); + self.wait_until_idle(spi, delay)?; + + // set the panel settings + self.cmd_with_data(spi, Command::PanelSetting, &[0x8F])?; + + self.cmd_with_data( + spi, + Command::VcomAndDataIntervalSetting, + &[WHITE_BORDER | VCOM_DATA_INTERVAL], + )?; + + // set resolution + self.send_resolution(spi)?; + + self.cmd_with_data(spi, Command::VcmDcSetting, &[0x0A])?; + + self.wait_until_idle(spi, delay)?; + + Ok(()) + } +} + +impl WaveshareThreeColorDisplay + for Epd2in9bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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) + } + + /// Update only the black/white data of the display. + /// + /// Finish by calling `update_chromatic_frame`. + fn update_achromatic_frame( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + black: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface.data(spi, black)?; + Ok(()) + } + + /// Update only chromatic data of the display. + /// + /// This data takes precedence over the black/white data. + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data(spi, chromatic)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd2in9bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd2in9bc { interface, color }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Section 8.2 from datasheet + self.interface.cmd_with_data( + spi, + Command::VcomAndDataIntervalSetting, + &[FLOATING_BORDER | VCOM_DATA_INTERVAL], + )?; + + self.command(spi, Command::PowerOff)?; + // The example STM code from Github has a wait after PowerOff + self.wait_until_idle(spi, delay)?; + + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + 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 update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + self.interface.data(spi, buffer)?; + + // Clear the chromatic layer + let color = self.color.get_byte_value(); + + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } + + #[allow(unused)] + 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> { + Ok(()) + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.command(spi, Command::DisplayRefresh)?; + + 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.send_resolution(spi)?; + + let color = DEFAULT_BACKGROUND_COLOR.get_byte_value(); + + // Clear the black + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + + // Clear the chromatic + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } + + fn set_lut( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + _refresh_rate: Option, + ) -> Result<(), SPI::Error> { + Ok(()) + } + + 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 Epd2in9bc +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::ResolutionSetting)?; + + self.send_data(spi, &[w as u8])?; + self.send_data(spi, &[(h >> 8) as u8])?; + self.send_data(spi, &[h as u8]) + } + + /// Set the outer border of the display to the chosen color. + pub fn set_border_color(&mut self, spi: &mut SPI, color: TriColor) -> Result<(), SPI::Error> { + let border = match color { + TriColor::Black => BLACK_BORDER, + TriColor::White => WHITE_BORDER, + TriColor::Chromatic => CHROMATIC_BORDER, + }; + self.cmd_with_data( + spi, + Command::VcomAndDataIntervalSetting, + &[border | VCOM_DATA_INTERVAL], + ) + } +} diff --git a/epd-waveshare/src/epd2in9d/command.rs b/epd-waveshare/src/epd2in9d/command.rs new file mode 100644 index 0000000..c7abc24 --- /dev/null +++ b/epd-waveshare/src/epd2in9d/command.rs @@ -0,0 +1,150 @@ +//! SPI Commands for the Waveshare 2.9" FLEXIBLE E-PAPER DISPLAY +use crate::traits; + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + PanelSetting = 0x00, + /// selecting internal and external power + /// self.send_data(0x03)?; //VDS_EN, VDG_EN + /// self.send_data(0x00)?; //VCOM_HV, VGHL_LV[1], VGHL_LV[0] + /// self.send_data(0x2b)?; //VDH + /// self.send_data(0x2b)?; //VDL + /// self.send_data(0xff)?; //VDHR + PowerSetting = 0x01, + /// After the Power Off command, the driver will power off following the Power Off Sequence. 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: floating. + PowerOff = 0x02, + /// Setting Power OFF sequence + PowerOffSequenceSetting = 0x03, + /// Turning On the Power + PowerOn = 0x04, + /// This command enables the internal bandgap, which will be cleared by the next POF. + PowerOnMeasure = 0x05, + /// Starting data transmission + /// 3-times: self.send_data(0x17)?; //07 0f 17 1f 27 2F 37 2f + BoosterSoftStart = 0x06, + /// After this command is transmitted, the chip would enter the deep-sleep mode to save power. + /// + /// The deep sleep mode would return to standby by hardware reset. + /// + /// The only one parameter is a check code, the command would be excuted if check code = 0xA5. + DeepSleep = 0x07, + /// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data + /// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel. + /// + /// - In B/W mode, this command writes “OLD” data to SRAM. + /// - In B/W/Red mode, this command writes “B/W” data to SRAM. + /// - In Program mode, this command writes “OTP” data to SRAM for programming. + DataStartTransmission1 = 0x10, + /// Stopping data transmission + DataStop = 0x11, + /// While user sent this command, driver will refresh display (data/VCOM) according to SRAM data and LUT. + /// + /// After Display Refresh command, BUSY_N signal will become “0” and the refreshing of panel starts. + DisplayRefresh = 0x12, + /// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data + /// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel. + /// - In B/W mode, this command writes “NEW” data to SRAM. + /// - In B/W/Red mode, this command writes “RED” data to SRAM. + DataStartTransmission2 = 0x13, + + /// This command stores VCOM Look-Up Table with 7 groups of data. Each group contains information for one state and is stored + /// with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutForVcom = 0x20, + /// This command stores White-to-White Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutWhiteToWhite = 0x21, + /// This command stores Black-to-White Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutBlackToWhite = 0x22, + /// This command stores White-to-Black Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutWhiteToBlack = 0x23, + /// This command stores Black-to-Black Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutBlackToBlack = 0x24, + /// The command controls the PLL clock frequency. + PllControl = 0x30, + /// This command reads the temperature sensed by the temperature sensor. + /// + /// Doesn't work! Waveshare doesn't connect the read pin + TemperatureSensor = 0x40, + /// Selects the Internal or External temperature sensor and offset + TemperatureSensorSelection = 0x41, + /// Write External Temperature Sensor + TemperatureSensorWrite = 0x42, + /// Read External Temperature Sensor + /// + /// Doesn't work! Waveshare doesn't connect the read pin + TemperatureSensorRead = 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) + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn the battery condition. + LowPowerDetection = 0x51, + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority than the RES\[1:0\] in R00H (PSR). + ResolutionSetting = 0x61, + /// This command defines the Fist Active Gate and First Active Source of active channels. + // GsstSetting = 0x65, + /// The LUT_REV / Chip Revision is read from OTP address = 0x001. + /// + /// Doesn't work! Waveshare doesn't connect the read pin + // Revision = 0x70, + /// Read Flags. This command reads the IC status + /// PTL, I2C_ERR, I2C_BUSY, DATA, PON, POF, BUSY + /// + /// Doesn't work! Waveshare doesn't connect the read pin + GetStatus = 0x71, + /// Automatically measure VCOM. This command reads the IC status + AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value + /// + /// Doesn't work! Waveshare doesn't connect the read pin + ReadVcomValue = 0x81, + /// Set VCM_DC + VcmDcSetting = 0x82, + /// This command sets partial window + PartialWindow = 0x90, + /// This command makes the display enter partial mode + PartialIn = 0x91, + /// This command makes the display exit partial mode and enter normal mode + PartialOut = 0x92, + /// After this command is issued, the chip would enter the program mode. + /// + /// After the programming procedure completed, a hardware reset is necessary for leaving program mode. + /// + /// The only one parameter is a check code, the command would be excuted if check code = 0xA5. + ProgramMode = 0xA0, + /// After this command is transmitted, the programming state machine would be activated. + /// + /// The BUSY flag would fall to 0 until the programming is completed. + ActiveProgramming = 0xA1, + /// The command is used for reading the content of OTP for checking the data of programming. + /// + /// The value of (n) is depending on the amount of programmed data, tha max address = 0xFFF. + ReadOtp = 0xA2, + /// This command is set for saving power during fresh period. If the output voltage of VCOM / Source is from negative to positive or + /// from positive to negative, the power saving mechanism will be activated. The active period width is defined by the following two + /// parameters. + PowerSaving = 0xE3, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} diff --git a/epd-waveshare/src/epd2in9d/constants.rs b/epd-waveshare/src/epd2in9d/constants.rs new file mode 100644 index 0000000..6a56d2e --- /dev/null +++ b/epd-waveshare/src/epd2in9d/constants.rs @@ -0,0 +1,61 @@ +//! This file contains look-up-tables used to set voltages used during +//! various categories of pixel refreshes. + +/** + * partial screen update LUT +**/ +#[rustfmt::skip] +pub(crate) const LUT_VCOM1: [u8; 44] = [ + 0x00, 0x19, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_WW1: [u8; 42] =[ + 0x00, 0x19, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_BW1: [u8; 42] =[ + 0x80, 0x19, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_BB1: [u8; 42] =[ + 0x00, 0x19, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_WB1: [u8; 42] =[ + 0x40, 0x19, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; diff --git a/epd-waveshare/src/epd2in9d/mod.rs b/epd-waveshare/src/epd2in9d/mod.rs new file mode 100644 index 0000000..95d7239 --- /dev/null +++ b/epd-waveshare/src/epd2in9d/mod.rs @@ -0,0 +1,407 @@ +//! A simple Driver for the Waveshare 2.9" D E-Ink Display via SPI +//! +//! +//! 参考[Waveshare](https://www.waveshare.net/wiki/2.9inch_e-Paper_HAT_%28D%29)的文档/例程进行构建 +//! +//! Specification: https://www.waveshare.net/w/upload/b/b5/2.9inch_e-Paper_%28D%29_Specification.pdf + +use core::slice::from_raw_parts; + +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::interface::DisplayInterface; +use crate::traits::{InternalWiAdditions, RefreshLut, WaveshareDisplay}; + +//The Lookup Tables for the Display +mod constants; +use crate::epd2in9d::constants::*; + +/// Width of Epd2in9d in pixels +pub const WIDTH: u32 = 128; +/// Height of Epd2in9d in pixels +pub const HEIGHT: u32 = 296; +/// EPD_ARRAY of Epd2in9d in pixels +/// WIDTH / 8 * HEIGHT +pub const EPD_ARRAY: u32 = 4736; +/// Default Background Color (white) +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::Black; +const IS_BUSY_LOW: bool = false; +const SINGLE_BYTE_WRITE: bool = true; + +use crate::color::Color; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Display with Fullsize buffer for use with the 2in9 EPD D +#[cfg(feature = "graphics")] +pub type Display2in9d = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Epd2in9d driver +/// +pub struct Epd2in9d<'a, SPI, BUSY, DC, RST, DELAY> { + /// SPI + interface: DisplayInterface, + /// Color + // background_color: Color, + color: Color, + /// Refresh LUT + refresh: RefreshLut, + // Storing old data for partial refreshes + old_data: &'a [u8], + // 标记是否局刷的状态 + is_partial_refresh: bool, +} + +impl InternalWiAdditions + for Epd2in9d<'_, SPI, BUSY, DC, RST, DELAY> +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.reset(delay, 10_000, 2_000); + + //panel setting + //LUT from OTP,KW-BF KWR-AF BWROTP 0f BWOTP 1f + self.interface + .cmd_with_data(spi, Command::PanelSetting, &[0x1f, 0x0D])?; + + //resolution setting + self.interface + .cmd_with_data(spi, Command::ResolutionSetting, &[0x80, 0x01, 0x28])?; + + self.interface.cmd(spi, Command::PowerOn)?; + self.wait_until_idle(spi, delay)?; + + //VCOM AND DATA INTERVAL SETTING + self.interface + .cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x97])?; + + Ok(()) + } +} + +impl WaveshareDisplay + for Epd2in9d<'_, SPI, BUSY, DC, RST, DELAY> +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + let old_data: &[u8] = &[]; + let is_partial_refresh = false; + + let mut epd = Epd2in9d { + interface, + color, + refresh: RefreshLut::Full, + old_data, + is_partial_refresh, + }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.is_partial_refresh = false; + self.interface + .cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0xf7])?; + self.interface.cmd(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + delay.delay_us(100_000); + self.interface + .cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay)?; + Ok(()) + } + + fn set_background_color(&mut self, background_color: Color) { + self.color = background_color; + } + + fn background_color(&self) -> &Color { + &self.color + } + + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + // Corresponds to the Display function. + // Used to write the data to be displayed to the screen SRAM. + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + if self.is_partial_refresh { + // Modify local refresh status if full refresh is performed. + self.is_partial_refresh = false; + } + self.wait_until_idle(spi, delay)?; + + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface.data_x_times(spi, 0xFF, EPD_ARRAY)?; + + self.interface + .cmd_with_data(spi, Command::DataStartTransmission2, buffer)?; + self.old_data = unsafe { from_raw_parts(buffer.as_ptr(), buffer.len()) }; + Ok(()) + } + + // 这个是DisplayPart + // Partial refresh write address and data + 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> { + if !self.is_partial_refresh { + // Initialize only on first call + self.set_part_reg(spi, delay)?; + self.is_partial_refresh = true; + } + self.interface.cmd(spi, Command::PartialIn)?; + + self.interface.cmd(spi, Command::PartialWindow)?; + self.interface.data(spi, &[(x - x % 8) as u8])?; + self.interface + .data(spi, &[(((x - x % 8) + width - 1) - 1) as u8])?; + self.interface.data(spi, &[(y / 256) as u8])?; + self.interface.data(spi, &[(y % 256) as u8])?; + self.interface + .data(spi, &[((y + height - 1) / 256) as u8])?; + self.interface + .data(spi, &[((y + height - 1) % 256 - 1) as u8])?; + self.interface.data(spi, &[0x28])?; + + self.interface + .cmd_with_data(spi, Command::DataStartTransmission1, self.old_data)?; + + self.interface + .cmd_with_data(spi, Command::DataStartTransmission2, buffer)?; + self.old_data = unsafe { from_raw_parts(buffer.as_ptr(), buffer.len()) }; + + Ok(()) + } + + /// actually is the "Turn on Display" sequence + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DisplayRefresh)?; + delay.delay_us(1_000); + 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.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface.data_x_times(spi, 0x00, EPD_ARRAY)?; + + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, 0xFF, EPD_ARRAY)?; + + self.display_frame(spi, delay)?; + + Ok(()) + } + + fn set_lut( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + refresh_rate: Option, + ) -> Result<(), SPI::Error> { + if let Some(refresh_lut) = refresh_rate { + self.refresh = refresh_lut; + } + self.set_lut_helper( + spi, delay, &LUT_VCOM1, &LUT_WW1, &LUT_BW1, &LUT_WB1, &LUT_BB1, + )?; + + Ok(()) + } + + 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 Epd2in9d<'_, SPI, BUSY, DC, RST, DELAY> +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// Wake Up Screen + /// + /// After the screen sleeps, it enters deep sleep mode. If you need to refresh the screen while in deep sleep mode, you must first execute awaken(). + /// Wake the screen. + // fn awaken(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // // reset the device + // self.interface.reset(delay, 20_000, 2_000); + // self.wait_until_idle(spi, delay)?; + + // // panel setting + // // LUT from OTP,KW-BF KWR-AF BWROTP 0f BWOTP 1f + // self.interface + // .cmd_with_data(spi, Command::PanelSetting, &[0x1f])?; + + // // resolution setting + // self.interface + // .cmd_with_data(spi, Command::ResolutionSetting, &[0x80, 0x01, 0x28])?; + + // // VCOM AND DATA INTERVAL SETTING + // self.interface + // .cmd(spi, Command::VcomAndDataIntervalSetting)?; + // Ok(()) + // } + + fn set_part_reg(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Reset the EPD driver circuit + //TODO: 这里在微雪的例程中反复刷新了3次,后面有显示问题再进行修改 + self.interface.reset(delay, 10_000, 2_000); + + // Power settings + //TODO: The data in the document is [0x03,0x00,0x2b,0x2b,0x09]. + self.interface.cmd_with_data( + spi, + Command::PowerSetting, + &[0x03, 0x00, 0x2b, 0x2b, 0x03], + )?; + + // Soft start + self.interface + .cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x17])?; + + // Panel settings + self.interface + .cmd_with_data(spi, Command::PanelSetting, &[0xbf, 0x0D])?; + + // Setting the refresh rate + // 3a 100HZ | 29 150Hz | 39 200HZ | 31 171HZ + // 3a is used in the example + self.interface + .cmd_with_data(spi, Command::PllControl, &[0x3C])?; + + // Resolution Settings + self.interface + .cmd_with_data(spi, Command::ResolutionSetting, &[0x80, 0x01, 0x28])?; + + // vcom_DC settings + self.interface + .cmd_with_data(spi, Command::VcmDcSetting, &[0x12])?; + + self.set_lut(spi, delay, None)?; + + // Power on + // self.interface.cmd_with_data( + // spi, + // Command::PowerOn, + // &[0x04], + // ); + self.interface.cmd(spi, Command::PowerOn)?; + + // Get the BUSY level, high to continue, low to wait for the screen to respond. + //TODO: This is the recommended step in the documentation, but I've ignored it since I've seen other screens that don't wait. + self.wait_until_idle(spi, delay)?; + + // vcom and data interval settings + // self.interface + // .cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x97])?; + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn set_lut_helper( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + lut_vcom: &[u8], + lut_ww: &[u8], + lut_bw: &[u8], + lut_wb: &[u8], + lut_bb: &[u8], + ) -> Result<(), SPI::Error> { + let _ = delay; + // LUT VCOM + self.interface + .cmd_with_data(spi, Command::LutForVcom, lut_vcom)?; + + // LUT WHITE to WHITE + self.interface + .cmd_with_data(spi, Command::LutWhiteToWhite, lut_ww)?; + + // LUT BLACK to WHITE + self.interface + .cmd_with_data(spi, Command::LutBlackToWhite, lut_bw)?; + + // LUT WHITE to BLACK + self.interface + .cmd_with_data(spi, Command::LutWhiteToBlack, lut_wb)?; + + // LUT BLACK to BLACK + self.interface + .cmd_with_data(spi, Command::LutBlackToBlack, lut_bb)?; + Ok(()) + } +} diff --git a/epd-waveshare/src/epd3in7/command.rs b/epd-waveshare/src/epd3in7/command.rs new file mode 100644 index 0000000..5aba926 --- /dev/null +++ b/epd-waveshare/src/epd3in7/command.rs @@ -0,0 +1,88 @@ +//! SPI Commands for the Waveshare 3.7" E-Ink Display + +use crate::traits; + +/// EPD3IN7 commands +/// +/// Should rarely (never?) be needed directly. +/// +/// For more infos about the addresses and what they are doing look into the pdfs +/// +/// The description of the single commands is mostly taken from EDP3IN7 specification +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// + GateSetting = 0x01, + /// + PowerOff = 0x02, + /// + Sleep2 = 0x07, + /// + GateVoltage = 0x03, + /// + GateVoltageSource = 0x04, + /// + BoosterSoftStartControl = 0x0C, + /// After this command initiated, the chip will enter Deep Sleep Mode, + /// BUSY pad will keep output high. + /// + /// Note: To exit Deep Sleep Mode, User required to send HWRESET to the driver. + DeepSleep = 0x10, + /// + DataEntrySequence = 0x11, + /// This command resets commands and parameters to their S/W Reset default values, + /// except Deep Sleep Mode. + /// During this operation BUSY pad will keep output high. + /// + /// Note: RAM is unaffected by this command. + SwReset = 0x12, + /// This command selects the Internal or External temperature sensor and offset + TemperatureSensorSelection = 0x18, + /// Write to temperature register + TemperatureSensorWrite = 0x1A, + /// Read from temperature register + TemperatureSensorRead = 0x1B, + /// This command activates Display Update sequence. + /// The Display Update sequence option is located at R22h. + /// + /// Note: BUSY pad will output high during operation. User **should not** interrupt this operation + /// to avoid corruption of panel images. + DisplayUpdateSequence = 0x20, + /// This command sets a Display Update Sequence option. + DisplayUpdateSequenceSetting = 0x22, + /// This command will transfer its data to B/W RAM, until another command is written + WriteRam = 0x24, + /// This command writes VCOM register from MCU interface + WriteVcomRegister = 0x2C, + /// This command writes LUT register from MCU interface (105 bytes), + /// which contains the content of VS [nx-LUT], TP #[nX], RP #[n] + WriteLutRegister = 0x32, + /// + DisplayOption = 0x37, + /// + BorderWaveformControl = 0x3C, + /// This command specifies the start/end positions of the window address in the X direction, + /// by an address unit of RAM. + SetRamXAddressStartEndPosition = 0x44, + /// This command specifies the start/end positions of the window address in the Y direction, + /// by an address unit of RAM. + SetRamYAddressStartEndPosition = 0x45, + /// + AutoWriteRedRamRegularPattern = 0x46, + /// + AutoWriteBwRamRegularPattern = 0x47, + /// This command makes the initial settings for the RAM X address in the address counter (AC) + SetRamXAddressCounter = 0x4E, + /// This command makes the initial settings for the RAM Y address in the address counter (AC) + SetRamYAddressCounter = 0x4F, + /// + Sleep = 0x50, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} diff --git a/epd-waveshare/src/epd3in7/constants.rs b/epd-waveshare/src/epd3in7/constants.rs new file mode 100644 index 0000000..bbfbf0c --- /dev/null +++ b/epd-waveshare/src/epd3in7/constants.rs @@ -0,0 +1,29 @@ +// This LUT clears the whole display during updates. +pub(crate) const LUT_1GRAY_GC: [u8; 105] = [ + 0x2A, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //1 + 0x05, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //2 + 0x2A, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //3 + 0x05, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //4 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //5 + 0x00, 0x02, 0x03, 0x0A, 0x00, 0x02, 0x06, 0x0A, 0x05, 0x00, //6 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //9 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //10 + 0x22, 0x22, 0x22, 0x22, 0x22, +]; + +// This LUT updates only the pixels that have changed. +pub(crate) const LUT_1GRAY_DU: [u8; 105] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //1 + 0x01, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //2 + 0x0A, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //3 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //4 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //5 + 0x00, 0x00, 0x05, 0x05, 0x00, 0x05, 0x03, 0x05, 0x05, 0x00, //6 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //9 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //10 + 0x22, 0x22, 0x22, 0x22, 0x22, +]; diff --git a/epd-waveshare/src/epd3in7/mod.rs b/epd-waveshare/src/epd3in7/mod.rs new file mode 100644 index 0000000..c01c49f --- /dev/null +++ b/epd-waveshare/src/epd3in7/mod.rs @@ -0,0 +1,266 @@ +//! A simple Driver for the Waveshare 3.7" E-Ink Display via SPI +//! +//! +//! Build with the help of documentation/code from [Waveshare](https://www.waveshare.com/wiki/3.7inch_e-Paper_HAT), +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +pub(crate) mod command; +mod constants; + +use self::command::Command; +use self::constants::*; + +use crate::buffer_len; +use crate::color::Color; +use crate::interface::DisplayInterface; +use crate::traits::{InternalWiAdditions, RefreshLut, WaveshareDisplay}; + +/// Width of the display. +pub const WIDTH: u32 = 280; + +/// Height of the display +pub const HEIGHT: u32 = 480; + +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; + +const IS_BUSY_LOW: bool = false; + +const SINGLE_BYTE_WRITE: bool = true; + +/// Display with Fullsize buffer for use with the 3in7 EPD +#[cfg(feature = "graphics")] +pub type Display3in7 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// EPD3in7 driver +pub struct EPD3in7 { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + background_color: Color, +} + +impl InternalWiAdditions + for EPD3in7 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // reset the device + self.interface.reset(delay, 30, 10); + + self.interface.cmd(spi, Command::SwReset)?; + delay.delay_us(300000u32); + + self.interface + .cmd_with_data(spi, Command::AutoWriteRedRamRegularPattern, &[0xF7])?; + self.interface.wait_until_idle(delay, IS_BUSY_LOW); + self.interface + .cmd_with_data(spi, Command::AutoWriteBwRamRegularPattern, &[0xF7])?; + self.interface.wait_until_idle(delay, IS_BUSY_LOW); + + self.interface + .cmd_with_data(spi, Command::GateSetting, &[0xDF, 0x01, 0x00])?; + self.interface + .cmd_with_data(spi, Command::GateVoltage, &[0x00])?; + self.interface + .cmd_with_data(spi, Command::GateVoltageSource, &[0x41, 0xA8, 0x32])?; + + self.interface + .cmd_with_data(spi, Command::DataEntrySequence, &[0x03])?; + + self.interface + .cmd_with_data(spi, Command::BorderWaveformControl, &[0x03])?; + + self.interface.cmd_with_data( + spi, + Command::BoosterSoftStartControl, + &[0xAE, 0xC7, 0xC3, 0xC0, 0xC0], + )?; + + self.interface + .cmd_with_data(spi, Command::TemperatureSensorSelection, &[0x80])?; + + self.interface + .cmd_with_data(spi, Command::WriteVcomRegister, &[0x44])?; + + self.interface.cmd_with_data( + spi, + Command::DisplayOption, + &[0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x4F, 0xFF, 0xFF, 0xFF, 0xFF], + )?; + + self.interface.cmd_with_data( + spi, + Command::SetRamXAddressStartEndPosition, + &[0x00, 0x00, 0x17, 0x01], + )?; + self.interface.cmd_with_data( + spi, + Command::SetRamYAddressStartEndPosition, + &[0x00, 0x00, 0xDF, 0x01], + )?; + + self.interface + .cmd_with_data(spi, Command::DisplayUpdateSequenceSetting, &[0xCF])?; + + self.set_lut(spi, delay, Some(RefreshLut::Full))?; + Ok(()) + } +} + +impl WaveshareDisplay + for EPD3in7 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let mut epd = EPD3in7 { + interface: DisplayInterface::new(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.interface.cmd_with_data(spi, Command::Sleep, &[0xF7])?; + self.interface.cmd(spi, Command::PowerOff)?; + self.interface + .cmd_with_data(spi, Command::Sleep2, &[0xA5])?; + Ok(()) + } + + fn set_background_color(&mut self, color: Self::DisplayColor) { + self.background_color = color; + } + + fn background_color(&self) -> &Self::DisplayColor { + &self.background_color + } + + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + 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.interface + .cmd_with_data(spi, Command::SetRamXAddressCounter, &[0x00, 0x00])?; + self.interface + .cmd_with_data(spi, Command::SetRamYAddressCounter, &[0x00, 0x00])?; + + self.interface + .cmd_with_data(spi, Command::WriteRam, buffer)?; + + Ok(()) + } + + #[allow(unused)] + 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> { + todo!() + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + //self.interface + // .cmd_with_data(spi, Command::WRITE_LUT_REGISTER, &LUT_1GRAY_GC)?; + self.interface.cmd(spi, Command::DisplayUpdateSequence)?; + self.interface.wait_until_idle(delay, IS_BUSY_LOW); + 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.interface + .cmd_with_data(spi, Command::SetRamXAddressCounter, &[0x00, 0x00])?; + self.interface + .cmd_with_data(spi, Command::SetRamYAddressCounter, &[0x00, 0x00])?; + + let color = self.background_color.get_byte_value(); + self.interface.cmd(spi, Command::WriteRam)?; + self.interface.data_x_times(spi, color, WIDTH * HEIGHT)?; + + Ok(()) + } + + fn set_lut( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + refresh_rate: Option, + ) -> Result<(), SPI::Error> { + let buffer = match refresh_rate { + Some(RefreshLut::Full) | None => &LUT_1GRAY_GC, + Some(RefreshLut::Quick) => &LUT_1GRAY_DU, + }; + + self.interface + .cmd_with_data(spi, Command::WriteLutRegister, buffer)?; + Ok(()) + } + + 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(()) + } +} diff --git a/epd-waveshare/src/epd4in2/command.rs b/epd-waveshare/src/epd4in2/command.rs new file mode 100644 index 0000000..4d3a9fe --- /dev/null +++ b/epd-waveshare/src/epd4in2/command.rs @@ -0,0 +1,177 @@ +//! SPI Commands for the Waveshare 4.2" E-Ink Display +use crate::traits; +/// EPD4IN2 commands +/// +/// Should rarely (never?) be needed directly. +/// +/// For more infos about the addresses and what they are doing look into the pdfs +/// +/// The description of the single commands is mostly taken from IL0398.pdf +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift direction, booster switch, soft reset + /// One Byte of Data: + /// 0x0F Red Mode, LUT from OTP + /// 0x1F B/W Mode, LUT from OTP + /// 0x2F Red Mode, LUT set by registers + /// 0x3F B/W Mode, LUT set by registers + PanelSetting = 0x00, + /// selecting internal and external power + /// self.send_data(0x03)?; //VDS_EN, VDG_EN + /// self.send_data(0x00)?; //VCOM_HV, VGHL_LV[1], VGHL_LV[0] + /// self.send_data(0x2b)?; //VDH + /// self.send_data(0x2b)?; //VDL + /// self.send_data(0xff)?; //VDHR + PowerSetting = 0x01, + /// After the Power Off command, the driver will power off following the Power Off Sequence. 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: floating. + PowerOff = 0x02, + /// Setting Power OFF sequence + PowerOffSequenceSetting = 0x03, + /// Turning On the Power + PowerOn = 0x04, + /// This command enables the internal bandgap, which will be cleared by the next POF. + PowerOnMeasure = 0x05, + /// Starting data transmission + /// 3-times: self.send_data(0x17)?; //07 0f 17 1f 27 2F 37 2f + BoosterSoftStart = 0x06, + /// After this command is transmitted, the chip would enter the deep-sleep mode to save power. + /// + /// The deep sleep mode would return to standby by hardware reset. + /// + /// The only one parameter is a check code, the command would be excuted if check code = 0xA5. + DeepSleep = 0x07, + /// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data + /// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel. + /// + /// - In B/W mode, this command writes “OLD” data to SRAM. + /// - In B/W/Red mode, this command writes “B/W” data to SRAM. + /// - In Program mode, this command writes “OTP” data to SRAM for programming. + DataStartTransmission1 = 0x10, + /// Stopping data transmission + DataStop = 0x11, + /// While user sent this command, driver will refresh display (data/VCOM) according to SRAM data and LUT. + /// + /// After Display Refresh command, BUSY_N signal will become “0” and the refreshing of panel starts. + DisplayRefresh = 0x12, + /// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data + /// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel. + /// - In B/W mode, this command writes “NEW” data to SRAM. + /// - In B/W/Red mode, this command writes “RED” data to SRAM. + DataStartTransmission2 = 0x13, + + /// This command stores VCOM Look-Up Table with 7 groups of data. Each group contains information for one state and is stored + /// with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutForVcom = 0x20, + /// This command stores White-to-White Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutWhiteToWhite = 0x21, + /// This command stores Black-to-White Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutBlackToWhite = 0x22, + /// This command stores White-to-Black Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutWhiteToBlack = 0x23, + /// This command stores Black-to-Black Look-Up Table with 7 groups of data. Each group contains information for one state and is + /// stored with 6 bytes, while the sixth byte indicates how many times that phase will repeat. + /// + /// from IL0373 + LutBlackToBlack = 0x24, + /// The command controls the PLL clock frequency. + PllControl = 0x30, + /// This command reads the temperature sensed by the temperature sensor. + /// + /// Doesn't work! Waveshare doesn't connect the read pin + TemperatureSensor = 0x40, + /// Selects the Internal or External temperature sensor and offset + TemperatureSensorSelection = 0x41, + /// Write External Temperature Sensor + TemperatureSensorWrite = 0x42, + /// Read External Temperature Sensor + /// + /// Doesn't work! Waveshare doesn't connect the read pin + TemperatureSensorRead = 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) + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn the battery condition. + LowPowerDetection = 0x51, + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority than the RES\[1:0\] in R00H (PSR). + ResolutionSetting = 0x61, + /// This command defines the Fist Active Gate and First Active Source of active channels. + GsstSetting = 0x65, + /// The LUT_REV / Chip Revision is read from OTP address = 0x001. + /// + /// Doesn't work! Waveshare doesn't connect the read pin + Revision = 0x70, + /// Read Flags. This command reads the IC status + /// PTL, I2C_ERR, I2C_BUSY, DATA, PON, POF, BUSY + /// + /// Doesn't work! Waveshare doesn't connect the read pin + GetStatus = 0x71, + /// Automatically measure VCOM. This command reads the IC status + AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value + /// + /// Doesn't work! Waveshare doesn't connect the read pin + ReadVcomValue = 0x81, + /// Set VCM_DC + VcmDcSetting = 0x82, + /// This command sets partial window + PartialWindow = 0x90, + /// This command makes the display enter partial mode + PartialIn = 0x91, + /// This command makes the display exit partial mode and enter normal mode + PartialOut = 0x92, + /// After this command is issued, the chip would enter the program mode. + /// + /// After the programming procedure completed, a hardware reset is necessary for leaving program mode. + /// + /// The only one parameter is a check code, the command would be excuted if check code = 0xA5. + ProgramMode = 0xA0, + /// After this command is transmitted, the programming state machine would be activated. + /// + /// The BUSY flag would fall to 0 until the programming is completed. + ActiveProgramming = 0xA1, + /// The command is used for reading the content of OTP for checking the data of programming. + /// + /// The value of (n) is depending on the amount of programmed data, tha max address = 0xFFF. + ReadOtp = 0xA2, + /// This command is set for saving power during fresh period. If the output voltage of VCOM / Source is from negative to positive or + /// from positive to negative, the power saving mechanism will be activated. The active period width is defined by the following two + /// parameters. + PowerSaving = 0xE3, +} + +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::PowerSaving.address(), 0xE3); + + assert_eq!(Command::PanelSetting.address(), 0x00); + + assert_eq!(Command::DisplayRefresh.address(), 0x12); + } +} diff --git a/epd-waveshare/src/epd4in2/constants.rs b/epd-waveshare/src/epd4in2/constants.rs new file mode 100644 index 0000000..a4c8037 --- /dev/null +++ b/epd-waveshare/src/epd4in2/constants.rs @@ -0,0 +1,115 @@ +//! This file contains look-up-tables used to set voltages used during +//! various categories of pixel refreshes. + +#[rustfmt::skip] +pub(crate) const LUT_VCOM0: [u8; 44] = [ +// The commented-out line below was used in a Ben Krasnow video explaining +// partial refreshes. +// 0x40, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x00, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_VCOM0_QUICK: [u8; 44] = [ + 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_WW: [u8; 42] =[ + 0x40, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x40, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_WW_QUICK: [u8; 42] =[ + 0xA0, 0x0E, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_BW: [u8; 42] =[ + 0x40, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x40, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_BW_QUICK: [u8; 42] =[ + 0xA0, 0x0E, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_BB: [u8; 42] =[ + 0x80, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x80, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_BB_QUICK: [u8; 42] =[ + 0x50, 0x0E, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_WB: [u8; 42] =[ + 0x80, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x80, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +pub(crate) const LUT_WB_QUICK: [u8; 42] =[ + 0x50, 0x0E, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; diff --git a/epd-waveshare/src/epd4in2/mod.rs b/epd-waveshare/src/epd4in2/mod.rs new file mode 100644 index 0000000..a1c2a0d --- /dev/null +++ b/epd-waveshare/src/epd4in2/mod.rs @@ -0,0 +1,606 @@ +//! A simple Driver for the Waveshare 4.2" E-Ink Display via SPI +//! +//! +//! Build with the help of documentation/code from [Waveshare](https://www.waveshare.com/wiki/4.2inch_e-Paper_Module), +//! [Ben Krasnows partial Refresh tips](https://benkrasnow.blogspot.de/2017/10/fast-partial-refresh-on-42-e-paper.html) and +//! the driver documents in the `pdfs`-folder as orientation. +//! +//! # Examples +//! +//!```rust, no_run +//!# use embedded_hal_mock::eh1::*; +//!# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +//!use embedded_graphics::{ +//! pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle}, +//!}; +//!use epd_waveshare::{epd4in2::*, 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::NoopDelay::new(); +//! +//!// Setup EPD +//!let mut epd = Epd4in2::new(&mut spi, busy_in, dc, rst, &mut delay, None)?; +//! +//!// Use display graphics from embedded-graphics +//!let mut display = Display4in2::default(); +//! +//!// Use embedded graphics for drawing a line +//!let _ = Line::new(Point::new(0, 120), Point::new(0, 295)) +//! .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1)) +//! .draw(&mut display); +//! +//! // Display updated frame +//!epd.update_frame(&mut spi, &display.buffer(), &mut delay)?; +//!epd.display_frame(&mut spi, &mut delay)?; +//! +//!// Set the EPD to sleep +//!epd.sleep(&mut spi, &mut delay)?; +//!# Ok(()) +//!# } +//!``` +//! +//! +//! +//! BE CAREFUL! The screen can get ghosting/burn-ins through the Partial Fast Update Drawing. + +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +use crate::interface::DisplayInterface; +use crate::traits::{InternalWiAdditions, QuickRefresh, RefreshLut, WaveshareDisplay}; + +//The Lookup Tables for the Display +mod constants; +use crate::epd4in2::constants::*; + +/// Width of the display +pub const WIDTH: u32 = 400; +/// Height of the display +pub const HEIGHT: u32 = 300; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = true; +const SINGLE_BYTE_WRITE: bool = true; + +use crate::color::Color; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 4in2 EPD +#[cfg(feature = "graphics")] +pub type Display4in2 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Epd4in2 driver +/// +pub struct Epd4in2 { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: Color, + /// Refresh LUT + refresh: RefreshLut, +} + +impl InternalWiAdditions + for Epd4in2 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // reset the device + self.interface.reset(delay, 10_000, 10_000); + + // set the power settings + self.interface.cmd_with_data( + spi, + Command::PowerSetting, + &[0x03, 0x00, 0x2b, 0x2b, 0xff], + )?; + + // start the booster + self.interface + .cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x17])?; + + // power on + self.command(spi, Command::PowerOn)?; + delay.delay_us(5000); + self.wait_until_idle(spi, delay)?; + + // set the panel settings + self.cmd_with_data(spi, Command::PanelSetting, &[0x3F])?; + + // Set Frequency, 200 Hz didn't work on my board + // 150Hz and 171Hz wasn't tested yet + // TODO: Test these other frequencies + // 3A 100HZ 29 150Hz 39 200HZ 31 171HZ DEFAULT: 3c 50Hz + self.cmd_with_data(spi, Command::PllControl, &[0x3A])?; + + self.send_resolution(spi)?; + + self.interface + .cmd_with_data(spi, Command::VcmDcSetting, &[0x12])?; + + //VBDF 17|D7 VBDW 97 VBDB 57 VBDF F7 VBDW 77 VBDB 37 VBDR B7 + self.interface + .cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x97])?; + + self.set_lut(spi, delay, None)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd4in2 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd4in2 { + interface, + color, + refresh: RefreshLut::Full, + }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x17])?; //border floating + self.command(spi, Command::VcmDcSetting)?; // VCOM to 0V + self.command(spi, Command::PanelSetting)?; + + self.command(spi, Command::PowerSetting)?; //VG&VS to 0V fast + for _ in 0..4 { + self.send_data(spi, &[0x00])?; + } + + self.command(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + self.interface + .cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + 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 update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + let color_value = self.color.get_byte_value(); + + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface + .data_x_times(spi, color_value, WIDTH / 8 * HEIGHT)?; + + self.interface + .cmd_with_data(spi, Command::DataStartTransmission2, buffer)?; + 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> { + self.wait_until_idle(spi, delay)?; + if buffer.len() as u32 != width / 8 * height { + //TODO: panic!! or sth like that + //return Err("Wrong buffersize"); + } + + self.command(spi, Command::PartialIn)?; + self.command(spi, Command::PartialWindow)?; + self.send_data(spi, &[(x >> 8) as u8])?; + let tmp = x & 0xf8; + self.send_data(spi, &[tmp as u8])?; // x should be the multiple of 8, the last 3 bit will always be ignored + let tmp = tmp + width - 1; + self.send_data(spi, &[(tmp >> 8) as u8])?; + self.send_data(spi, &[(tmp | 0x07) as u8])?; + + self.send_data(spi, &[(y >> 8) as u8])?; + self.send_data(spi, &[y as u8])?; + + self.send_data(spi, &[((y + height - 1) >> 8) as u8])?; + self.send_data(spi, &[(y + height - 1) as u8])?; + + self.send_data(spi, &[0x01])?; // Gates scan both inside and outside of the partial window. (default) + + //TODO: handle dtm somehow + let is_dtm1 = false; + if is_dtm1 { + self.command(spi, Command::DataStartTransmission1)? //TODO: check if data_start transmission 1 also needs "old"/background data here + } else { + self.command(spi, Command::DataStartTransmission2)? + } + + self.send_data(spi, buffer)?; + + self.command(spi, Command::PartialOut)?; + Ok(()) + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::DisplayRefresh)?; + 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.command(spi, Command::DisplayRefresh)?; + Ok(()) + } + + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.send_resolution(spi)?; + + let color_value = self.color.get_byte_value(); + + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface + .data_x_times(spi, color_value, WIDTH / 8 * HEIGHT)?; + + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface + .data_x_times(spi, color_value, WIDTH / 8 * HEIGHT)?; + Ok(()) + } + + fn set_lut( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + refresh_rate: Option, + ) -> Result<(), SPI::Error> { + if let Some(refresh_lut) = refresh_rate { + self.refresh = refresh_lut; + } + match self.refresh { + RefreshLut::Full => { + self.set_lut_helper(spi, delay, &LUT_VCOM0, &LUT_WW, &LUT_BW, &LUT_WB, &LUT_BB) + } + RefreshLut::Quick => self.set_lut_helper( + spi, + delay, + &LUT_VCOM0_QUICK, + &LUT_WW_QUICK, + &LUT_BW_QUICK, + &LUT_WB_QUICK, + &LUT_BB_QUICK, + ), + } + } + + 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 Epd4in2 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::ResolutionSetting)?; + 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]) + } + + #[allow(clippy::too_many_arguments)] + fn set_lut_helper( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + lut_vcom: &[u8], + lut_ww: &[u8], + lut_bw: &[u8], + lut_wb: &[u8], + lut_bb: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // LUT VCOM + self.cmd_with_data(spi, Command::LutForVcom, lut_vcom)?; + + // LUT WHITE to WHITE + self.cmd_with_data(spi, Command::LutWhiteToWhite, lut_ww)?; + + // LUT BLACK to WHITE + self.cmd_with_data(spi, Command::LutBlackToWhite, lut_bw)?; + + // LUT WHITE to BLACK + self.cmd_with_data(spi, Command::LutWhiteToBlack, lut_wb)?; + + // LUT BLACK to BLACK + self.cmd_with_data(spi, Command::LutBlackToBlack, lut_bb)?; + Ok(()) + } + + /// Helper function. Sets up the display to send pixel data to a custom + /// starting point. + pub fn shift_display( + &mut self, + spi: &mut SPI, + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + self.send_data(spi, &[(x >> 8) as u8])?; + let tmp = x & 0xf8; + self.send_data(spi, &[tmp as u8])?; // x should be the multiple of 8, the last 3 bit will always be ignored + let tmp = tmp + width - 1; + self.send_data(spi, &[(tmp >> 8) as u8])?; + self.send_data(spi, &[(tmp | 0x07) as u8])?; + + self.send_data(spi, &[(y >> 8) as u8])?; + self.send_data(spi, &[y as u8])?; + + self.send_data(spi, &[((y + height - 1) >> 8) as u8])?; + self.send_data(spi, &[(y + height - 1) as u8])?; + + self.send_data(spi, &[0x01])?; // Gates scan both inside and outside of the partial window. (default) + + Ok(()) + } +} + +impl QuickRefresh + for Epd4in2 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// To be followed immediately after by `update_old_frame`. + fn update_old_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + self.interface.data(spi, buffer)?; + + Ok(()) + } + + /// To be used immediately after `update_old_frame`. + fn update_new_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // self.send_resolution(spi)?; + + self.interface.cmd(spi, Command::DataStartTransmission2)?; + + self.interface.data(spi, buffer)?; + + Ok(()) + } + + /// This is a wrapper around `display_frame` for using this device as a true + /// `QuickRefresh` device. + fn display_new_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.display_frame(spi, delay) + } + + /// This is wrapper around `update_new_frame` and `display_frame` for using + /// this device as a true `QuickRefresh` device. + /// + /// To be used immediately after `update_old_frame`. + fn update_and_display_new_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.update_new_frame(spi, buffer, delay)?; + self.display_frame(spi, delay) + } + + fn update_partial_old_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + + if buffer.len() as u32 != width / 8 * height { + //TODO: panic!! or sth like that + //return Err("Wrong buffersize"); + } + + self.interface.cmd(spi, Command::PartialIn)?; + self.interface.cmd(spi, Command::PartialWindow)?; + + self.shift_display(spi, x, y, width, height)?; + + self.interface.cmd(spi, Command::DataStartTransmission1)?; + + self.interface.data(spi, buffer)?; + + Ok(()) + } + + /// Always call `update_partial_old_frame` before this, with buffer-updating code + /// between the calls. + fn update_partial_new_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + if buffer.len() as u32 != width / 8 * height { + //TODO: panic!! or sth like that + //return Err("Wrong buffersize"); + } + + self.shift_display(spi, x, y, width, height)?; + + self.interface.cmd(spi, Command::DataStartTransmission2)?; + + self.interface.data(spi, buffer)?; + + self.interface.cmd(spi, Command::PartialOut)?; + Ok(()) + } + + fn clear_partial_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.send_resolution(spi)?; + + let color_value = self.color.get_byte_value(); + + self.interface.cmd(spi, Command::PartialIn)?; + self.interface.cmd(spi, Command::PartialWindow)?; + + self.shift_display(spi, x, y, width, height)?; + + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface + .data_x_times(spi, color_value, width / 8 * height)?; + + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface + .data_x_times(spi, color_value, width / 8 * height)?; + + self.interface.cmd(spi, Command::PartialOut)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 400); + assert_eq!(HEIGHT, 300); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd5in65f/command.rs b/epd-waveshare/src/epd5in65f/command.rs new file mode 100644 index 0000000..23d2246 --- /dev/null +++ b/epd-waveshare/src/epd5in65f/command.rs @@ -0,0 +1,150 @@ +//! SPI Commands for the Waveshare 7.5" E-Ink Display + +use crate::traits; + +/// EPD6in65f 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)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift + /// direction, booster switch, soft reset. + PanelSetting = 0x00, + + /// Selecting internal and external power + PowerSetting = 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. + PowerOff = 0x02, + + /// Setting Power OFF sequence + PowerOffSequenceSetting = 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". + PowerOn = 0x04, + + /// Starting data transmission + BoosterSoftStart = 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. + DeepSleep = 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 + DataStartTransmission1 = 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. + DataStop = 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. + DisplayRefresh = 0x12, + + /// Image Process Command + ImageProcess = 0x13, + + /// This command builds the VCOM Look-Up Table (LUTC). + LutForVcom = 0x20, + /// This command builds the Black Look-Up Table (LUTB). + LutBlack = 0x21, + /// This command builds the White Look-Up Table (LUTW). + LutWhite = 0x22, + /// This command builds the Gray1 Look-Up Table (LUTG1). + LutGray1 = 0x23, + /// This command builds the Gray2 Look-Up Table (LUTG2). + LutGray2 = 0x24, + /// This command builds the Red0 Look-Up Table (LUTR0). + LutRed0 = 0x25, + /// This command builds the Red1 Look-Up Table (LUTR1). + LutRed1 = 0x26, + /// This command builds the Red2 Look-Up Table (LUTR2). + LutRed2 = 0x27, + /// This command builds the Red3 Look-Up Table (LUTR3). + LutRed3 = 0x28, + /// This command builds the XON Look-Up Table (LUTXON). + LutXon = 0x29, + + /// The command controls the PLL clock frequency. + PllControl = 0x30, + + /// This command reads the temperature sensed by the temperature sensor. + TemperatureSensor = 0x40, + /// This command selects the Internal or External temperature sensor. + TemperatureCalibration = 0x41, + /// This command could write data to the external temperature sensor. + TemperatureSensorWrite = 0x42, + /// This command could read data from the external temperature sensor. + TemperatureSensorRead = 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). + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn + /// the battery condition. + LowPowerDetection = 0x51, + + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority + /// than the RES\[1:0\] in R00H (PSR). + TconResolution = 0x61, + /// This command defines MCU host direct access external memory mode. + //SpiFlashControl = 0x65, + + /// The LUT_REV / Chip Revision is read from OTP address = 25001 and 25000. + //Revision = 0x70, + /// This command reads the IC status. + GetStatus = 0x71, + + /// This command implements related VCOM sensing setting. + //AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value. + ReadVcomValue = 0x81, + /// This command sets `VCOM_DC` value. + VcmDcSetting = 0x82, + // /// This is in all the Waveshare controllers for EPD6in65f, but it's not documented + // /// anywhere in the datasheet `¯\_(ツ)_/¯` + FlashMode = 0xE3, +} + +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::PanelSetting.address(), 0x00); + assert_eq!(Command::DisplayRefresh.address(), 0x12); + } +} diff --git a/epd-waveshare/src/epd5in65f/mod.rs b/epd-waveshare/src/epd5in65f/mod.rs new file mode 100644 index 0000000..1a70b78 --- /dev/null +++ b/epd-waveshare/src/epd5in65f/mod.rs @@ -0,0 +1,266 @@ +//! A simple Driver for the Waveshare 6.65 inch (F) E-Ink Display via SPI +//! +//! # References +//! +//! - [Datasheet](https://www.waveshare.com/wiki/5.65inch_e-Paper_Module_(F)) +//! - [Waveshare C driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/c/lib/e-Paper/EPD_5in65f.c) +//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd5in65f.py) + +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::color::OctColor; +use crate::interface::DisplayInterface; +use crate::traits::{InternalWiAdditions, RefreshLut, WaveshareDisplay}; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 5in65f EPD +#[cfg(feature = "graphics")] +pub type Display5in65f = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize * 4) }, + OctColor, +>; + +/// Width of the display +pub const WIDTH: u32 = 600; +/// Height of the display +pub const HEIGHT: u32 = 448; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: OctColor = OctColor::White; +/// Default mode of writing data (single byte vs blockwise) +const SINGLE_BYTE_WRITE: bool = true; + +/// Epd5in65f driver +/// +pub struct Epd5in65f { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: OctColor, +} + +impl InternalWiAdditions + for Epd5in65f +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Reset the device + self.interface.reset(delay, 10_000, 2_000); + + self.cmd_with_data(spi, Command::PanelSetting, &[0xEF, 0x08])?; + self.cmd_with_data(spi, Command::PowerSetting, &[0x37, 0x00, 0x23, 0x23])?; + self.cmd_with_data(spi, Command::PowerOffSequenceSetting, &[0x00])?; + self.cmd_with_data(spi, Command::BoosterSoftStart, &[0xC7, 0xC7, 0x1D])?; + self.cmd_with_data(spi, Command::PllControl, &[0x3C])?; + self.cmd_with_data(spi, Command::TemperatureSensor, &[0x00])?; + self.update_vcom(spi)?; + self.cmd_with_data(spi, Command::TconSetting, &[0x22])?; + self.send_resolution(spi)?; + + self.cmd_with_data(spi, Command::FlashMode, &[0xAA])?; + + delay.delay_us(100_000); + + self.update_vcom(spi)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd5in65f +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = OctColor; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd5in65f { 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, _delay: &mut DELAY) -> Result<(), SPI::Error> { + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.update_vcom(spi)?; + self.send_resolution(spi)?; + self.cmd_with_data(spi, Command::DataStartTransmission1, buffer)?; + 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.wait_until_idle(spi, delay)?; + self.command(spi, Command::PowerOn)?; + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::DisplayRefresh)?; + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::PowerOff)?; + self.wait_busy_low(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> { + let bg = OctColor::colors_byte(self.color, self.color); + self.wait_until_idle(spi, delay)?; + self.update_vcom(spi)?; + self.send_resolution(spi)?; + self.command(spi, Command::DataStartTransmission1)?; + self.interface.data_x_times(spi, bg, WIDTH * HEIGHT / 2)?; + self.display_frame(spi, delay)?; + Ok(()) + } + + fn set_background_color(&mut self, color: OctColor) { + self.color = color; + } + + fn background_color(&self) -> &OctColor { + &self.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, true); + Ok(()) + } +} + +impl Epd5in65f +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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_busy_low(&mut self, delay: &mut DELAY) { + self.interface.wait_until_idle(delay, false); + } + fn send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::TconResolution)?; + 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]) + } + + fn update_vcom(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let bg_color = (self.color.get_nibble() & 0b111) << 5; + self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x17 | bg_color])?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 600); + assert_eq!(HEIGHT, 448); + assert_eq!(DEFAULT_BACKGROUND_COLOR, OctColor::White); + } +} diff --git a/epd-waveshare/src/epd5in83_v2/command.rs b/epd-waveshare/src/epd5in83_v2/command.rs new file mode 100644 index 0000000..46b6b64 --- /dev/null +++ b/epd-waveshare/src/epd5in83_v2/command.rs @@ -0,0 +1,134 @@ +//! SPI Commands for the Waveshare 5.83" E-Ink Display + +use crate::traits; + +/// Epd5in83 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)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift + /// direction, booster switch, soft reset. + PanelSetting = 0x00, + + /// Selecting internal and external power + PowerSetting = 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. + PowerOff = 0x02, + + /// Setting Power OFF sequence + PowerOffSequenceSetting = 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". + PowerOn = 0x04, + + /// Starting data transmission + BoosterSoftStart = 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. + DeepSleep = 0x07, + + /// This command starts transmitting B/W data and write them into SRAM. To complete data + /// transmission, commands Display Refresh or Data Start Transmission2 must be issued. Then the chip will start to + /// send data/VCOM for panel. + DataStartTransmission1 = 0x10, + + /// This command starts transmitting RED data and write them into SRAM. To complete data + /// transmission, command Display refresh must be issued. Then the chip will start to + /// send data/VCOM for panel. + DataStartTransmission2 = 0x13, + + /// 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. + DataStop = 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. + DisplayRefresh = 0x12, + + /// Enables or disables Dual SPI mode + DualSPI = 0x15, + + /// The command controls the PLL clock frequency. + PllControl = 0x30, + + /// This command reads the temperature sensed by the temperature sensor. + TemperatureSensorCalibration = 0x40, + /// This command selects the Internal or External temperature sensor. + TemperatureSensorSelection = 0x41, + /// This command could write data to the external temperature sensor. + TemperatureSensorWrite = 0x42, + /// This command could read data from the external temperature sensor. + TemperatureSensorRead = 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). + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn + /// the battery condition. + LowPowerDetection = 0x51, + + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority + /// than the RES\[1:0\] in R00H (PSR). + TconResolution = 0x61, + + /// The LUT_REV / Chip Revision is read from OTP address = 25001 and 25000. + Revision = 0x70, + /// This command reads the IC status. + GetStatus = 0x71, + + /// This command implements related VCOM sensing setting. + AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value. + ReadVcomValue = 0x81, + /// This command sets `VCOM_DC` value. + VcmDcSetting = 0x82, + + /// Sets window size for the partial update + PartialWindow = 0x90, + /// Sets chip into partial update mode + PartialIn = 0x91, + /// Quits partial update mode + PartialOut = 0x92, +} + +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::PanelSetting.address(), 0x00); + assert_eq!(Command::DisplayRefresh.address(), 0x12); + } +} diff --git a/epd-waveshare/src/epd5in83_v2/mod.rs b/epd-waveshare/src/epd5in83_v2/mod.rs new file mode 100644 index 0000000..58909b2 --- /dev/null +++ b/epd-waveshare/src/epd5in83_v2/mod.rs @@ -0,0 +1,272 @@ +//! A simple Driver for the Waveshare 5.83" v2 E-Ink Display via SPI +//! +//! # References +//! +//! - [Datasheet](https://www.waveshare.com/5.83inch-e-paper-hat.htm) +//! - [Waveshare C driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_5in83_V2.c) +//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd5in83_V2.py) + +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::color::Color; +use crate::interface::DisplayInterface; +use crate::prelude::WaveshareDisplay; +use crate::traits::{InternalWiAdditions, RefreshLut}; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 5in83 v2 EPD +#[cfg(feature = "graphics")] +pub type Display5in83 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Width of the display +pub const WIDTH: u32 = 648; +/// Height of the display +pub const HEIGHT: u32 = 480; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = true; +const NUM_DISPLAY_BITS: u32 = WIDTH * HEIGHT / 8; +const SINGLE_BYTE_WRITE: bool = true; + +/// Epd5in83 driver +/// +pub struct Epd5in83 { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: Color, +} + +impl InternalWiAdditions + for Epd5in83 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Reset the device + self.interface.reset(delay, 2000, 50); + + // Set the power settings: VGH=20V,VGL=-20V,VDH=15V,VDL=-15V + self.cmd_with_data(spi, Command::PowerSetting, &[0x07, 0x07, 0x3F, 0x3F])?; + + // Power on + self.command(spi, Command::PowerOn)?; + delay.delay_us(5000); + self.wait_until_idle(spi, delay)?; + + // Set the panel settings: BWOTP + self.cmd_with_data(spi, Command::PanelSetting, &[0x1F])?; + + // Set the real resolution + self.send_resolution(spi)?; + + // Disable dual SPI + self.cmd_with_data(spi, Command::DualSPI, &[0x00])?; + + // Set Vcom and data interval + self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x10, 0x07])?; + + // Set S2G and G2S non-overlap periods to 12 (default) + self.cmd_with_data(spi, Command::TconSetting, &[0x22])?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd5in83 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd5in83 { interface, color }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + 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 update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + let color_value = self.color.get_byte_value(); + + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface + .data_x_times(spi, color_value, WIDTH / 8 * HEIGHT)?; + + self.interface + .cmd_with_data(spi, Command::DataStartTransmission2, buffer)?; + 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::DisplayRefresh)?; + 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.wait_until_idle(spi, delay)?; + + self.command(spi, Command::DataStartTransmission1)?; + self.interface.data_x_times(spi, 0xFF, NUM_DISPLAY_BITS)?; + + self.command(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, 0x00, NUM_DISPLAY_BITS)?; + + Ok(()) + } + + 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 Epd5in83 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::TconResolution)?; + 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, 648); + assert_eq!(HEIGHT, 480); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd5in83b_v2/command.rs b/epd-waveshare/src/epd5in83b_v2/command.rs new file mode 100644 index 0000000..46b6b64 --- /dev/null +++ b/epd-waveshare/src/epd5in83b_v2/command.rs @@ -0,0 +1,134 @@ +//! SPI Commands for the Waveshare 5.83" E-Ink Display + +use crate::traits; + +/// Epd5in83 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)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift + /// direction, booster switch, soft reset. + PanelSetting = 0x00, + + /// Selecting internal and external power + PowerSetting = 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. + PowerOff = 0x02, + + /// Setting Power OFF sequence + PowerOffSequenceSetting = 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". + PowerOn = 0x04, + + /// Starting data transmission + BoosterSoftStart = 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. + DeepSleep = 0x07, + + /// This command starts transmitting B/W data and write them into SRAM. To complete data + /// transmission, commands Display Refresh or Data Start Transmission2 must be issued. Then the chip will start to + /// send data/VCOM for panel. + DataStartTransmission1 = 0x10, + + /// This command starts transmitting RED data and write them into SRAM. To complete data + /// transmission, command Display refresh must be issued. Then the chip will start to + /// send data/VCOM for panel. + DataStartTransmission2 = 0x13, + + /// 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. + DataStop = 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. + DisplayRefresh = 0x12, + + /// Enables or disables Dual SPI mode + DualSPI = 0x15, + + /// The command controls the PLL clock frequency. + PllControl = 0x30, + + /// This command reads the temperature sensed by the temperature sensor. + TemperatureSensorCalibration = 0x40, + /// This command selects the Internal or External temperature sensor. + TemperatureSensorSelection = 0x41, + /// This command could write data to the external temperature sensor. + TemperatureSensorWrite = 0x42, + /// This command could read data from the external temperature sensor. + TemperatureSensorRead = 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). + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn + /// the battery condition. + LowPowerDetection = 0x51, + + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority + /// than the RES\[1:0\] in R00H (PSR). + TconResolution = 0x61, + + /// The LUT_REV / Chip Revision is read from OTP address = 25001 and 25000. + Revision = 0x70, + /// This command reads the IC status. + GetStatus = 0x71, + + /// This command implements related VCOM sensing setting. + AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value. + ReadVcomValue = 0x81, + /// This command sets `VCOM_DC` value. + VcmDcSetting = 0x82, + + /// Sets window size for the partial update + PartialWindow = 0x90, + /// Sets chip into partial update mode + PartialIn = 0x91, + /// Quits partial update mode + PartialOut = 0x92, +} + +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::PanelSetting.address(), 0x00); + assert_eq!(Command::DisplayRefresh.address(), 0x12); + } +} diff --git a/epd-waveshare/src/epd5in83b_v2/mod.rs b/epd-waveshare/src/epd5in83b_v2/mod.rs new file mode 100644 index 0000000..625b1e7 --- /dev/null +++ b/epd-waveshare/src/epd5in83b_v2/mod.rs @@ -0,0 +1,352 @@ +//! A simple Driver for the Waveshare 5.83" (B) v2 E-Ink Display via SPI +//! +//! # References +//! +//! - [Datasheet](https://www.waveshare.com/5.83inch-e-Paper-B.htm) +//! - [Waveshare C driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_5in83b_V2.c) +//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd5in83b_V2.py) + +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::color::Color; +use crate::interface::DisplayInterface; +use crate::prelude::{TriColor, WaveshareDisplay, WaveshareThreeColorDisplay}; +use crate::traits::{InternalWiAdditions, RefreshLut}; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 5in83b v2 EPD +#[cfg(feature = "graphics")] +pub type Display5in83 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize * 2) }, + TriColor, +>; + +/// Width of the display +pub const WIDTH: u32 = 648; +/// Height of the display +pub const HEIGHT: u32 = 480; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = true; +const NUM_DISPLAY_BITS: u32 = WIDTH / 8 * HEIGHT; +const SINGLE_BYTE_WRITE: bool = true; + +/// Epd7in5 driver +/// +pub struct Epd5in83 { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: Color, +} + +impl InternalWiAdditions + for Epd5in83 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Reset the device + self.interface.reset(delay, 10_000, 10_000); + + // Start the booster + self.cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x1e, 0x17])?; + + // Set the power settings: VGH=20V,VGL=-20V,VDH=15V,VDL=-15V + self.cmd_with_data(spi, Command::PowerSetting, &[0x07, 0x07, 0x3F, 0x3F])?; + + // Power on + self.command(spi, Command::PowerOn)?; + delay.delay_us(5000); + self.wait_until_idle(spi, delay)?; + + // Set the panel settings: BWROTP + self.cmd_with_data(spi, Command::PanelSetting, &[0x0F])?; + + // Set the real resolution + self.send_resolution(spi)?; + + // Disable dual SPI + self.cmd_with_data(spi, Command::DualSPI, &[0x00])?; + + // Set Vcom and data interval + self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x11, 0x07])?; + + // Set S2G and G2S non-overlap periods to 12 (default) + self.cmd_with_data(spi, Command::TconSetting, &[0x22])?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareThreeColorDisplay + for Epd5in83 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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)?; + Ok(()) + } + + fn update_achromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + black: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DataStartTransmission1, black)?; + Ok(()) + } + + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DataStartTransmission2, chromatic)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd5in83 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd5in83 { interface, color }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + 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 update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.update_achromatic_frame(spi, delay, buffer)?; + let color = self.color.get_byte_value(); + self.command(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; + 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> { + self.wait_until_idle(spi, delay)?; + if buffer.len() as u32 != width / 8 * height { + //TODO panic or error + } + + let hrst_upper = (x / 8) as u8 >> 6; + let hrst_lower = ((x / 8) << 3) as u8; + let hred_upper = ((x + width) / 8) as u8 >> 6; + let hred_lower = (((x + width) / 8) << 3) as u8 & 0b111; + let vrst_upper = (y >> 8) as u8; + let vrst_lower = y as u8; + let vred_upper = ((y + height) >> 8) as u8; + let vred_lower = (y + height) as u8; + let pt_scan = 0x01; // Gates scan both inside and outside of the partial window. (default) + + self.command(spi, Command::PartialIn)?; + self.command(spi, Command::PartialWindow)?; + self.send_data( + spi, + &[ + hrst_upper, hrst_lower, hred_upper, hred_lower, vrst_upper, vrst_lower, vred_upper, + vred_lower, pt_scan, + ], + )?; + self.command(spi, Command::DataStartTransmission1)?; + self.send_data(spi, buffer)?; + + let color = TriColor::Black.get_byte_value(); //We need it black, so red channel will be rendered transparent + self.command(spi, Command::DataStartTransmission2)?; + self.interface + .data_x_times(spi, color, width * height / 8)?; + + self.command(spi, Command::DisplayRefresh)?; + self.wait_until_idle(spi, delay)?; + + self.command(spi, Command::PartialOut)?; + Ok(()) + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.command(spi, Command::DisplayRefresh)?; + 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.wait_until_idle(spi, delay)?; + + // The Waveshare controllers all implement clear using 0x33 + self.command(spi, Command::DataStartTransmission1)?; + self.interface.data_x_times(spi, 0xFF, NUM_DISPLAY_BITS)?; + + self.command(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, 0x00, NUM_DISPLAY_BITS)?; + + Ok(()) + } + + 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 Epd5in83 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::TconResolution)?; + 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, 648); + assert_eq!(HEIGHT, 480); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd7in3f/command.rs b/epd-waveshare/src/epd7in3f/command.rs new file mode 100644 index 0000000..0122ac7 --- /dev/null +++ b/epd-waveshare/src/epd7in3f/command.rs @@ -0,0 +1,55 @@ +use crate::traits; + +#[allow(dead_code, clippy::upper_case_acronyms)] +#[derive(Clone, Copy, Debug)] +pub(crate) enum Command { + Ox00 = 0x00, + Ox01 = 0x01, + + PowerOff = 0x02, + + Ox03 = 0x03, + + PowerOn = 0x04, + + Ox05 = 0x05, + Ox06 = 0x06, + + DeepSleep = 0x07, + + Ox08 = 0x08, + + DataStartTransmission = 0x10, + + DataFresh = 0x12, + + IPC = 0x13, + + Ox30 = 0x30, + + TSE = 0x41, + + Ox50 = 0x50, + Ox60 = 0x60, + Ox61 = 0x61, + + Ox82 = 0x82, + Ox84 = 0x84, + + CMDH = 0xAA, + + AGID = 0x86, + + CCSET = 0xE0, + + OxE3 = 0xE3, + + TSSET = 0xE6, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} diff --git a/epd-waveshare/src/epd7in3f/mod.rs b/epd-waveshare/src/epd7in3f/mod.rs new file mode 100644 index 0000000..11a733d --- /dev/null +++ b/epd-waveshare/src/epd7in3f/mod.rs @@ -0,0 +1,275 @@ +//! A simple Driver for the Waveshare 7.3inch e-Paper HAT (F) Display via SPI +//! +//! # References +//! +//! - [Datasheet](https://www.waveshare.com/wiki/7.3inch_e-Paper_HAT_(F)) +//! - [Waveshare C driver](https://github.com/waveshareteam/e-Paper/blob/8be47b27f1a6808fd82ea9ceeac04c172e4ee9a8/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_7in3f.c) +//! - [Waveshare Python driver](https://github.com/waveshareteam/e-Paper/blob/8be47b27f1a6808fd82ea9ceeac04c172e4ee9a8/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in3f.py) + +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::{ + buffer_len, + color::OctColor, + interface::DisplayInterface, + traits::{InternalWiAdditions, WaveshareDisplay}, +}; + +use self::command::Command; + +mod command; + +/// Full size buffer for use with the 7in3f EPD +#[cfg(feature = "graphics")] +pub type Display7in3f = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize * 4) }, + OctColor, +>; + +/// Width of the display +pub const WIDTH: u32 = 800; +/// Height of the display +pub const HEIGHT: u32 = 480; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: OctColor = OctColor::White; +/// Default mode of writing data (single byte vs blockwise) +const SINGLE_BYTE_WRITE: bool = true; + +/// Epd57n3f driver +pub struct Epd7in3f { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: OctColor, +} + +impl InternalWiAdditions + for Epd7in3f +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), ::Error> { + self.interface.reset(delay, 20_000, 2_000); + self.wait_busy_low(delay); + delay.delay_ms(30); + + self.cmd_with_data(spi, Command::CMDH, &[0x49, 0x55, 0x20, 0x08, 0x09, 0x18])?; + self.cmd_with_data(spi, Command::Ox01, &[0x3F, 0x00, 0x32, 0x2A, 0x0E, 0x2A])?; + self.cmd_with_data(spi, Command::Ox00, &[0x5F, 0x69])?; + self.cmd_with_data(spi, Command::Ox03, &[0x00, 0x54, 0x00, 0x44])?; + self.cmd_with_data(spi, Command::Ox05, &[0x40, 0x1F, 0x1F, 0x2C])?; + self.cmd_with_data(spi, Command::Ox06, &[0x6F, 0x1F, 0x1F, 0x22])?; + self.cmd_with_data(spi, Command::Ox08, &[0x6F, 0x1F, 0x1F, 0x22])?; + self.cmd_with_data(spi, Command::IPC, &[0x00, 0x04])?; + self.cmd_with_data(spi, Command::Ox30, &[0x3C])?; + self.cmd_with_data(spi, Command::TSE, &[0x00])?; + self.cmd_with_data(spi, Command::Ox50, &[0x3F])?; + self.cmd_with_data(spi, Command::Ox60, &[0x02, 0x00])?; + self.cmd_with_data(spi, Command::Ox61, &[0x03, 0x20, 0x01, 0xE0])?; + self.cmd_with_data(spi, Command::Ox82, &[0x1E])?; + self.cmd_with_data(spi, Command::Ox84, &[0x00])?; + self.cmd_with_data(spi, Command::AGID, &[0x00])?; + self.cmd_with_data(spi, Command::OxE3, &[0x2F])?; + self.cmd_with_data(spi, Command::CCSET, &[0x00])?; + self.cmd_with_data(spi, Command::TSSET, &[0x00]) + } +} + +impl WaveshareDisplay + for Epd7in3f +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = OctColor; + + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result::Error> + where + Self: Sized, + { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd7in3f { interface, color }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), ::Error> { + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5]) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), ::Error> { + self.init(spi, delay) + } + + fn set_background_color(&mut self, color: Self::DisplayColor) { + self.color = color; + } + + fn background_color(&self) -> &Self::DisplayColor { + &self.color + } + + fn width(&self) -> u32 { + WIDTH + } + + fn height(&self) -> u32 { + HEIGHT + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), ::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DataStartTransmission, buffer) + } + + fn update_partial_frame( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + _buffer: &[u8], + _x: u32, + _y: u32, + _width: u32, + _height: u32, + ) -> Result<(), ::Error> { + unimplemented!() + } + + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), ::Error> { + self.command(spi, Command::PowerOn)?; + self.wait_busy_low(delay); + + self.cmd_with_data(spi, Command::DataFresh, &[0x00])?; + self.wait_busy_low(delay); + + self.cmd_with_data(spi, Command::PowerOff, &[0x00])?; + self.wait_busy_low(delay); + + Ok(()) + } + + fn update_and_display_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), ::Error> { + self.update_frame(spi, buffer, delay)?; + self.display_frame(spi, delay) + } + + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), ::Error> { + let bg = OctColor::colors_byte(self.color, self.color); + + self.wait_busy_low(delay); + self.command(spi, Command::DataStartTransmission)?; + self.interface.data_x_times(spi, bg, WIDTH * HEIGHT / 2)?; + + self.display_frame(spi, delay) + } + + fn set_lut( + &mut self, + _spi: &mut SPI, + _delay: &mut DELAY, + _refresh_rate: Option, + ) -> Result<(), ::Error> { + unimplemented!() + } + + fn wait_until_idle(&mut self, _spi: &mut SPI, delay: &mut DELAY) -> Result<(), ::Error> { + self.wait_busy_low(delay); + Ok(()) + } +} + +impl Epd7in3f +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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_busy_low(&mut self, delay: &mut DELAY) { + self.interface.wait_until_idle(delay, true); + } + + /// Show 7 blocks of color, used for quick testing + pub fn show_7block(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + let color_7 = [ + OctColor::Black, + OctColor::White, + OctColor::Green, + OctColor::Blue, + OctColor::Red, + OctColor::Yellow, + OctColor::Orange, + OctColor::White, + ]; + + self.command(spi, Command::DataStartTransmission)?; + for _ in 0..240 { + for color in color_7.iter().take(4) { + for _ in 0..100 { + self.interface + .data(spi, &[OctColor::colors_byte(*color, *color)])?; + } + } + } + + for _ in 0..240 { + for color in color_7.iter().skip(4) { + for _ in 0..100 { + self.interface + .data(spi, &[OctColor::colors_byte(*color, *color)])?; + } + } + } + + self.display_frame(spi, delay) + } +} diff --git a/epd-waveshare/src/epd7in5/command.rs b/epd-waveshare/src/epd7in5/command.rs new file mode 100644 index 0000000..8615269 --- /dev/null +++ b/epd-waveshare/src/epd7in5/command.rs @@ -0,0 +1,153 @@ +//! 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)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift + /// direction, booster switch, soft reset. + PanelSetting = 0x00, + + /// Selecting internal and external power + PowerSetting = 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. + PowerOff = 0x02, + + /// Setting Power OFF sequence + PowerOffSequenceSetting = 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". + PowerOn = 0x04, + + /// Starting data transmission + BoosterSoftStart = 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. + DeepSleep = 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. + DataStartTransmission1 = 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. + DataStop = 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. + DisplayRefresh = 0x12, + + /// After this command is issued, image process engine will find thin lines/pixels + /// from frame SRAM and update the frame SRAM for applying new gray level waveform. + /// + /// After "Image Process Command", BUSY_N signal will become "0" until image process + /// is finished. + ImageProcess = 0x13, + + /// This command builds the VCOM Look-Up Table (LUTC). + LutForVcom = 0x20, + /// This command builds the Black Look-Up Table (LUTB). + LutBlack = 0x21, + /// This command builds the White Look-Up Table (LUTW). + LutWhite = 0x22, + /// This command builds the Gray1 Look-Up Table (LUTG1). + LutGray1 = 0x23, + /// This command builds the Gray2 Look-Up Table (LUTG2). + LutGray2 = 0x24, + /// This command builds the Red0 Look-Up Table (LUTR0). + LutRed0 = 0x25, + /// This command builds the Red1 Look-Up Table (LUTR1). + LutRed1 = 0x26, + /// This command builds the Red2 Look-Up Table (LUTR2). + LutRed2 = 0x27, + /// This command builds the Red3 Look-Up Table (LUTR3). + LutRed3 = 0x28, + /// This command builds the XON Look-Up Table (LUTXON). + LutXon = 0x29, + + /// The command controls the PLL clock frequency. + PllControl = 0x30, + + /// This command reads the temperature sensed by the temperature sensor. + TemperatureSensor = 0x40, + /// This command selects the Internal or External temperature sensor. + TemperatureCalibration = 0x41, + /// This command could write data to the external temperature sensor. + TemperatureSensorWrite = 0x42, + /// This command could read data from the external temperature sensor. + TemperatureSensorRead = 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). + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn + /// the battery condition. + LowPowerDetection = 0x51, + + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority + /// than the RES\[1:0\] in R00H (PSR). + TconResolution = 0x61, + /// This command defines MCU host direct access external memory mode. + SpiFlashControl = 0x65, + + /// The LUT_REV / Chip Revision is read from OTP address = 25001 and 25000. + Revision = 0x70, + /// This command reads the IC status. + GetStatus = 0x71, + + /// This command implements related VCOM sensing setting. + AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value. + ReadVcomValue = 0x81, + /// This command sets `VCOM_DC` value. + VcmDcSetting = 0x82, + + /// This is in all the Waveshare controllers for Epd7in5, but it's not documented + /// anywhere in the datasheet `¯\_(ツ)_/¯` + FlashMode = 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::PanelSetting.address(), 0x00); + assert_eq!(Command::DisplayRefresh.address(), 0x12); + } +} diff --git a/epd-waveshare/src/epd7in5/mod.rs b/epd-waveshare/src/epd7in5/mod.rs new file mode 100644 index 0000000..121b780 --- /dev/null +++ b/epd-waveshare/src/epd7in5/mod.rs @@ -0,0 +1,287 @@ +//! A simple Driver for the Waveshare 7.5" E-Ink Display via SPI +//! +//! # References +//! +//! - [Datasheet](https://www.waveshare.com/wiki/7.5inch_e-Paper_HAT) +//! - [Waveshare C driver](https://github.com/waveshare/e-Paper/blob/702def06bcb75983c98b0f9d25d43c552c248eb0/RaspberryPi%26JetsonNano/c/lib/e-Paper/EPD_7in5.c) +//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/702def06bcb75983c98b0f9d25d43c552c248eb0/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd7in5.py) + +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::color::Color; +use crate::interface::DisplayInterface; +use crate::traits::{InternalWiAdditions, RefreshLut, WaveshareDisplay}; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 7in5 EPD +#[cfg(feature = "graphics")] +pub type Display7in5 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Width of the display +pub const WIDTH: u32 = 640; +/// Height of the display +pub const HEIGHT: u32 = 384; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = true; +const SINGLE_BYTE_WRITE: bool = false; + +/// Epd7in5 driver +/// +pub struct Epd7in5 { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: Color, +} + +impl InternalWiAdditions + for Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Reset the device + self.interface.reset(delay, 10_000, 10_000); + + // Set the power settings + self.cmd_with_data(spi, Command::PowerSetting, &[0x37, 0x00])?; + + // Set the panel settings: + // - 600 x 448 + // - Using LUT from external flash + self.cmd_with_data(spi, Command::PanelSetting, &[0xCF, 0x08])?; + + // Start the booster + self.cmd_with_data(spi, Command::BoosterSoftStart, &[0xC7, 0xCC, 0x28])?; + + // Power on + self.command(spi, Command::PowerOn)?; + delay.delay_us(5000); + self.wait_until_idle(spi, delay)?; + + // Set the clock frequency to 50Hz (default) + self.cmd_with_data(spi, Command::PllControl, &[0x3C])?; + + // Select internal temperature sensor (default) + self.cmd_with_data(spi, Command::TemperatureCalibration, &[0x00])?; + + // Set Vcom and data interval to 10 (default), border output to white + self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x77])?; + + // Set S2G and G2S non-overlap periods to 12 (default) + self.cmd_with_data(spi, Command::TconSetting, &[0x22])?; + + // Set the real resolution + self.send_resolution(spi)?; + + // Set VCOM_DC to -1.5V + self.cmd_with_data(spi, Command::VcmDcSetting, &[0x1E])?; + + // This is in all the Waveshare controllers for Epd7in5 + self.cmd_with_data(spi, Command::FlashMode, &[0x03])?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + let color = DEFAULT_BACKGROUND_COLOR; + + let mut epd = Epd7in5 { interface, color }; + + epd.init(spi, delay)?; + + Ok(epd) + } + + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + Ok(()) + } + + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.init(spi, delay) + } + + 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 update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::DataStartTransmission1)?; + for byte in buffer { + let mut temp = *byte; + for _ in 0..4 { + let mut data = if temp & 0x80 == 0 { 0x00 } else { 0x03 }; + data <<= 4; + temp <<= 1; + data |= if temp & 0x80 == 0 { 0x00 } else { 0x03 }; + temp <<= 1; + self.send_data(spi, &[data])?; + } + } + 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.wait_until_idle(spi, delay)?; + self.command(spi, Command::DisplayRefresh)?; + 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.command(spi, Command::DisplayRefresh)?; + Ok(()) + } + + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.send_resolution(spi)?; + + // The Waveshare controllers all implement clear using 0x33 + self.command(spi, Command::DataStartTransmission1)?; + self.interface + .data_x_times(spi, 0x33, WIDTH / 8 * HEIGHT * 4)?; + Ok(()) + } + + 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 Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::TconResolution)?; + 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, 640); + assert_eq!(HEIGHT, 384); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd7in5_hd/command.rs b/epd-waveshare/src/epd7in5_hd/command.rs new file mode 100644 index 0000000..946ace8 --- /dev/null +++ b/epd-waveshare/src/epd7in5_hd/command.rs @@ -0,0 +1,159 @@ +//! 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 { + DriverOutputControl = 0x01, + + /// Set gate driving voltage + GateDrivingVoltageControl = 0x03, + + /// Set source driving voltage + SourceDrivingVoltageControl = 0x04, + + SoftStart = 0x0C, + + /// Set the scanning start position of the gate driver. + /// The valid range is from 0 to 679. + GateScanStartPosition = 0x0F, + + /// Deep sleep mode control + DeepSleep = 0x10, + + /// Define data entry sequence + DataEntry = 0x11, + + /// resets the commands and parameters to their S/W Reset default values except R10h-Deep Sleep Mode. + /// During operation, BUSY pad will output high. + /// Note: RAM are unaffected by this command. + SwReset = 0x12, + + /// After this command initiated, HV Ready detection starts. + /// BUSY pad will output high during detection. + /// The detection result can be read from the Status Bit Read (Command 0x2F). + HvReadyDetection = 0x14, + + /// After this command initiated, VCI detection starts. + /// BUSY pad will output high during detection. + /// The detection result can be read from the Status Bit Read (Command 0x2F). + VciDetection = 0x15, + + /// Temperature Sensor Selection + TemperatureSensorControl = 0x18, + + /// Write to temperature register + TemperatureSensorWrite = 0x1A, + + /// Read from temperature register + TemperatureSensorRead = 0x1B, + + /// Write Command to External temperature sensor. + TemperatureSensorWriteExternal = 0x1C, + + /// Activate Display Update Sequence + MasterActivation = 0x20, + + /// RAM content option for Display Update + DisplayUpdateControl1 = 0x21, + + /// Display Update Sequence Option + DisplayUpdateControl2 = 0x22, + + /// After this command, data entries will be written into the BW RAM until another command is written + WriteRamBw = 0x24, + + /// After this command, data entries will be written into the RED RAM until another command is written + WriteRamRed = 0x26, + + /// Fetch data from RAM + ReadRam = 0x27, + + /// Enter VCOM sensing conditions + VcomSense = 0x28, + + /// Enter VCOM sensing conditions + VcomSenseDuration = 0x29, + + /// Program VCOM register into OTP + VcomProgramOtp = 0x2A, + + /// Reduces a glitch when ACVCOM is toggled + VcomControl = 0x2B, + + /// Write VCOM register from MCU interface + VcomWrite = 0x2C, + + /// Read Register for Display Option + OtpRead = 0x2D, + + /// CRC calculation command for OTP content validation + CrcCalculation = 0x34, + + /// CRC Status Read + CrcRead = 0x35, + + /// Program OTP Selection according to the OTP Selection Control + ProgramSelection = 0x36, + + /// Write Register for Display Option + DisplayOptionWrite = 0x37, + + /// Write register for User ID + UserIdWrite = 0x38, + + /// Select border waveform for VBD + VbdControl = 0x3C, + + /// Read RAM Option + ReadRamOption = 0x41, + + /// Specify the start/end positions of the window address in the X direction by an address unit for RAM + SetRamXStartEnd = 0x44, + + /// Specify the start/end positions of the window address in the Y direction by an address unit for RAM + SetRamYStartEnd = 0x45, + + /// Auto write RED RAM for regular pattern + AutoWriteRed = 0x46, + + /// Auto write B/W RAM for regular pattern + AutoWriteBw = 0x47, + + /// Make initial settings for the RAM X address in the address counter (AC) + SetRamXAc = 0x4E, + + /// Make initial settings for the RAM Y address in the address counter (AC) + SetRamYAc = 0x4F, + + /// This command is an empty command; it does not have any effect on the display module. + /// However, it can be used to terminate Frame Memory Write or Read Commands. + Nop = 0x7F, +} + +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::MasterActivation.address(), 0x20); + assert_eq!(Command::SwReset.address(), 0x12); + assert_eq!(Command::DisplayUpdateControl2.address(), 0x22); + } +} diff --git a/epd-waveshare/src/epd7in5_hd/mod.rs b/epd-waveshare/src/epd7in5_hd/mod.rs new file mode 100644 index 0000000..f78df1f --- /dev/null +++ b/epd-waveshare/src/epd7in5_hd/mod.rs @@ -0,0 +1,269 @@ +//! A simple Driver for the Waveshare 7.5" E-Ink Display (HD) via SPI +//! +//! Color values for this driver are inverted compared to the [EPD 7in5 V2 driver](crate::epd7in5_v2) +//! *EPD 7in5 HD:* White = 1/0xFF, Black = 0/0x00 +//! *EPD 7in5 V2:* White = 0/0x00, Black = 1/0xFF +//! +//! # References +//! +//! - [Datasheet](https://www.waveshare.com/w/upload/2/27/7inch_HD_e-Paper_Specification.pdf) +//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in5_HD.py) +//! +use embedded_hal::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::color::Color; +use crate::interface::DisplayInterface; +use crate::traits::{InternalWiAdditions, RefreshLut, WaveshareDisplay}; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 7in5 HD EPD +#[cfg(feature = "graphics")] +pub type Display7in5 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Width of the display +pub const WIDTH: u32 = 880; +/// Height of the display +pub const HEIGHT: u32 = 528; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; // Inverted for HD as compared to 7in5 v2 (HD: 0xFF = White) +const IS_BUSY_LOW: bool = false; +const SINGLE_BYTE_WRITE: bool = false; + +/// EPD7in5 (HD) driver +/// +pub struct Epd7in5 { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: Color, +} + +impl InternalWiAdditions + for Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Reset the device + self.interface.reset(delay, 10_000, 2_000); + + // HD procedure as described here: + // https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in5_HD.py + // and as per specs: + // https://www.waveshare.com/w/upload/2/27/7inch_HD_e-Paper_Specification.pdf + + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::SwReset)?; + self.wait_until_idle(spi, delay)?; + + self.cmd_with_data(spi, Command::AutoWriteRed, &[0xF7])?; + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::AutoWriteBw, &[0xF7])?; + self.wait_until_idle(spi, delay)?; + + self.cmd_with_data(spi, Command::SoftStart, &[0xAE, 0xC7, 0xC3, 0xC0, 0x40])?; + + self.cmd_with_data(spi, Command::DriverOutputControl, &[0xAF, 0x02, 0x01])?; + + self.cmd_with_data(spi, Command::DataEntry, &[0x01])?; + + self.cmd_with_data(spi, Command::SetRamXStartEnd, &[0x00, 0x00, 0x6F, 0x03])?; + self.cmd_with_data(spi, Command::SetRamYStartEnd, &[0xAF, 0x02, 0x00, 0x00])?; + + self.cmd_with_data(spi, Command::VbdControl, &[0x05])?; + + self.cmd_with_data(spi, Command::TemperatureSensorControl, &[0x80])?; + + self.cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xB1])?; + + self.command(spi, Command::MasterActivation)?; + self.wait_until_idle(spi, delay)?; + + self.cmd_with_data(spi, Command::SetRamXAc, &[0x00, 0x00])?; + self.cmd_with_data(spi, Command::SetRamYAc, &[0x00, 0x00])?; + + Ok(()) + } +} + +impl WaveshareDisplay + for Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + 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, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DeepSleep, &[0x01])?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::SetRamYAc, &[0x00, 0x00])?; + self.cmd_with_data(spi, Command::WriteRamBw, buffer)?; + self.cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xF7])?; + 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> { + let pixel_count = WIDTH / 8 * HEIGHT; + let background_color_byte = self.color.get_byte_value(); + + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::SetRamYAc, &[0x00, 0x00])?; + + for cmd in &[Command::WriteRamBw, Command::WriteRamRed] { + self.command(spi, *cmd)?; + self.interface + .data_x_times(spi, background_color_byte, pixel_count)?; + } + + self.cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xF7])?; + self.command(spi, Command::MasterActivation)?; + self.wait_until_idle(spi, delay)?; + 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, + _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 Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epd_size() { + assert_eq!(WIDTH, 880); + assert_eq!(HEIGHT, 528); + assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); + } +} diff --git a/epd-waveshare/src/epd7in5_v2/command.rs b/epd-waveshare/src/epd7in5_v2/command.rs new file mode 100644 index 0000000..95292d3 --- /dev/null +++ b/epd-waveshare/src/epd7in5_v2/command.rs @@ -0,0 +1,153 @@ +//! 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)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift + /// direction, booster switch, soft reset. + PanelSetting = 0x00, + + /// Selecting internal and external power + PowerSetting = 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. + PowerOff = 0x02, + + /// Setting Power OFF sequence + PowerOffSequenceSetting = 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". + PowerOn = 0x04, + + /// Starting data transmission + BoosterSoftStart = 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. + DeepSleep = 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 + DataStartTransmission1 = 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. + DataStop = 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. + DisplayRefresh = 0x12, + + /// RED or NEW_DATA + DataStartTransmission2 = 0x13, + + /// Dual SPI - what for? + DualSpi = 0x15, + + /// This command builds the VCOM Look-Up Table (LUTC). + LutForVcom = 0x20, + /// This command builds the Black Look-Up Table (LUTB). + LutBlack = 0x21, + /// This command builds the White Look-Up Table (LUTW). + LutWhite = 0x22, + /// This command builds the Gray1 Look-Up Table (LUTG1). + LutGray1 = 0x23, + /// This command builds the Gray2 Look-Up Table (LUTG2). + LutGray2 = 0x24, + /// This command builds the Red0 Look-Up Table (LUTR0). + LutRed0 = 0x25, + /// This command builds the Red1 Look-Up Table (LUTR1). + LutRed1 = 0x26, + /// This command builds the Red2 Look-Up Table (LUTR2). + LutRed2 = 0x27, + /// This command builds the Red3 Look-Up Table (LUTR3). + LutRed3 = 0x28, + /// This command builds the XON Look-Up Table (LUTXON). + LutXon = 0x29, + + /// The command controls the PLL clock frequency. + PllControl = 0x30, + + /// This command reads the temperature sensed by the temperature sensor. + TemperatureSensor = 0x40, + /// This command selects the Internal or External temperature sensor. + TemperatureCalibration = 0x41, + /// This command could write data to the external temperature sensor. + TemperatureSensorWrite = 0x42, + /// This command could read data from the external temperature sensor. + TemperatureSensorRead = 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). + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn + /// the battery condition. + LowPowerDetection = 0x51, + + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority + /// than the RES\[1:0\] in R00H (PSR). + TconResolution = 0x61, + /// This command defines MCU host direct access external memory mode. + SpiFlashControl = 0x65, + + /// The LUT_REV / Chip Revision is read from OTP address = 25001 and 25000. + Revision = 0x70, + /// This command reads the IC status. + GetStatus = 0x71, + + /// This command implements related VCOM sensing setting. + AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value. + ReadVcomValue = 0x81, + /// This command sets `VCOM_DC` value. + VcmDcSetting = 0x82, + // /// This is in all the Waveshare controllers for Epd7in5, but it's not documented + // /// anywhere in the datasheet `¯\_(ツ)_/¯` + // FlashMode = 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::PanelSetting.address(), 0x00); + assert_eq!(Command::DisplayRefresh.address(), 0x12); + } +} diff --git a/epd-waveshare/src/epd7in5_v2/mod.rs b/epd-waveshare/src/epd7in5_v2/mod.rs new file mode 100644 index 0000000..7f09a25 --- /dev/null +++ b/epd-waveshare/src/epd7in5_v2/mod.rs @@ -0,0 +1,261 @@ +//! 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::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::color::Color; +use crate::interface::DisplayInterface; +use crate::traits::{InternalWiAdditions, RefreshLut, WaveshareDisplay}; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 7in5 v2 EPD +#[cfg(feature = "graphics")] +pub type Display7in5 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize) }, + Color, +>; + +/// Width of the display +pub const WIDTH: u32 = 800; +/// Height of the display +pub const HEIGHT: u32 = 480; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; +const IS_BUSY_LOW: bool = true; +const SINGLE_BYTE_WRITE: bool = false; + +/// Epd7in5 (V2) driver +/// +pub struct Epd7in5 { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: Color, +} + +impl InternalWiAdditions + for Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Reset the device + self.interface.reset(delay, 10_000, 2_000); + + // 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::BoosterSoftStart, &[0x17, 0x17, 0x27, 0x17])?; + self.cmd_with_data(spi, Command::PowerSetting, &[0x07, 0x17, 0x3F, 0x3F])?; + self.command(spi, Command::PowerOn)?; + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::PanelSetting, &[0x1F])?; + self.cmd_with_data(spi, Command::PllControl, &[0x06])?; + self.cmd_with_data(spi, Command::TconResolution, &[0x03, 0x20, 0x01, 0xE0])?; + self.cmd_with_data(spi, Command::DualSpi, &[0x00])?; + self.cmd_with_data(spi, Command::TconSetting, &[0x22])?; + self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x10, 0x07])?; + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = Color; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + 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, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DataStartTransmission2, buffer)?; + 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.wait_until_idle(spi, delay)?; + self.command(spi, Command::DisplayRefresh)?; + 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.command(spi, Command::DisplayRefresh)?; + Ok(()) + } + + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.send_resolution(spi)?; + + self.command(spi, Command::DataStartTransmission1)?; + self.interface.data_x_times(spi, 0x00, WIDTH / 8 * HEIGHT)?; + + self.command(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, 0x00, WIDTH / 8 * HEIGHT)?; + + self.command(spi, Command::DisplayRefresh)?; + 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, + _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_with_cmd(spi, delay, IS_BUSY_LOW, Command::GetStatus) + } +} + +impl Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::TconResolution)?; + 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/epd-waveshare/src/epd7in5b_v2/command.rs b/epd-waveshare/src/epd7in5b_v2/command.rs new file mode 100644 index 0000000..fd2d70c --- /dev/null +++ b/epd-waveshare/src/epd7in5b_v2/command.rs @@ -0,0 +1,166 @@ +//! SPI Commands for the Waveshare 7.5"(B) V2 and V3 -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)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift + /// direction, booster switch, soft reset. + PanelSetting = 0x00, + + /// Selecting internal and external power + PowerSetting = 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. + PowerOff = 0x02, + + /// Setting Power OFF sequence + PowerOffSequenceSetting = 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". + PowerOn = 0x04, + + /// Starting data transmission + BoosterSoftStart = 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. + DeepSleep = 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 + DataStartTransmission1 = 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. + DataStop = 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. + DisplayRefresh = 0x12, + + /// RED or NEW_DATA + DataStartTransmission2 = 0x13, + + /// Dual SPI - what for? + DualSpi = 0x15, + + /// This command builds the VCOM Look-Up Table (LUTC). + LutForVcom = 0x20, + /// This command builds the Black Look-Up Table (LUTB). + LutBlack = 0x21, + /// This command builds the White Look-Up Table (LUTW). + LutWhite = 0x22, + /// This command builds the Gray1 Look-Up Table (LUTG1). + LutGray1 = 0x23, + /// This command builds the Gray2 Look-Up Table (LUTG2). + LutGray2 = 0x24, + /// This command builds the Red0 Look-Up Table (LUTR0). + LutRed0 = 0x25, + /// This command builds the Red1 Look-Up Table (LUTR1). + LutRed1 = 0x26, + /// This command builds the Red2 Look-Up Table (LUTR2). + LutRed2 = 0x27, + /// This command builds the Red3 Look-Up Table (LUTR3). + LutRed3 = 0x28, + /// This command builds the XON Look-Up Table (LUTXON). + LutXon = 0x29, + + /// LUT Option + LutOpt = 0x2A, + /// KW LUT Option + KWLutOpt = 0x2B, + + /// The command controls the PLL clock frequency. + PllControl = 0x30, + + /// This command reads the temperature sensed by the temperature sensor. + TemperatureSensor = 0x40, + /// This command selects the Internal or External temperature sensor. + TemperatureCalibration = 0x41, + /// This command could write data to the external temperature sensor. + TemperatureSensorWrite = 0x42, + /// This command could read data from the external temperature sensor. + TemperatureSensorRead = 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). + VcomAndDataIntervalSetting = 0x50, + /// This command indicates the input power condition. Host can read this flag to learn + /// the battery condition. + LowPowerDetection = 0x51, + + /// This command defines non-overlap period of Gate and Source. + TconSetting = 0x60, + /// This command defines alternative resolution and this setting is of higher priority + /// than the RES\[1:0\] in R00H (PSR). + TconResolution = 0x61, + /// This command defines MCU host direct access external memory mode. + SpiFlashControl = 0x65, + + /// The LUT_REV / Chip Revision is read from OTP address = 25001 and 25000. + Revision = 0x70, + /// This command reads the IC status. + GetStatus = 0x71, + + /// This command implements related VCOM sensing setting. + AutoMeasurementVcom = 0x80, + /// This command gets the VCOM value. + ReadVcomValue = 0x81, + /// This command sets `VCOM_DC` value. + VcmDcSetting = 0x82, + // /// This is in all the Waveshare controllers for Epd7in5, but it's not documented + // /// anywhere in the datasheet `¯\_(ツ)_/¯` + // FlashMode = 0xE5, + /// Sets window size for the partial update + PartialWindow = 0x90, + /// Sets chip into partial update mode + PartialIn = 0x91, + /// Quits partial update mode + PartialOut = 0x92, + /// Read OTP + ReadOTP = 0xA2, +} + +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::PanelSetting.address(), 0x00); + assert_eq!(Command::DisplayRefresh.address(), 0x12); + } +} diff --git a/epd-waveshare/src/epd7in5b_v2/mod.rs b/epd-waveshare/src/epd7in5b_v2/mod.rs new file mode 100644 index 0000000..708dd45 --- /dev/null +++ b/epd-waveshare/src/epd7in5b_v2/mod.rs @@ -0,0 +1,397 @@ +//! A simple Driver for the Waveshare 7.5" (B) 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::{ + delay::DelayNs, + digital::{InputPin, OutputPin}, + spi::SpiDevice, +}; + +use crate::color::TriColor; +use crate::interface::DisplayInterface; +use crate::traits::{ + InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, +}; + +pub(crate) mod command; +use self::command::Command; +use crate::buffer_len; + +/// Full size buffer for use with the 7in5b v2 EPD +#[cfg(feature = "graphics")] +pub type Display7in5 = crate::graphics::Display< + WIDTH, + HEIGHT, + false, + { buffer_len(WIDTH as usize, HEIGHT as usize * 2) }, + TriColor, +>; + +/// Width of the display +pub const WIDTH: u32 = 800; +/// Height of the display +pub const HEIGHT: u32 = 480; +/// Default Background Color +pub const DEFAULT_BACKGROUND_COLOR: TriColor = TriColor::White; + +/// Number of bytes for b/w buffer and same for chromatic buffer bits +const NUM_DISPLAY_BITS: usize = WIDTH as usize / 8 * HEIGHT as usize; +const IS_BUSY_LOW: bool = true; +const SINGLE_BYTE_WRITE: bool = false; + +/// Epd7in5 (V2) driver +/// +pub struct Epd7in5 { + /// Connection Interface + interface: DisplayInterface, + /// Background Color + color: TriColor, +} + +impl InternalWiAdditions + for Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + // Reset the device + // C driver does 200/2 original rust driver does 10/2 + self.interface.reset(delay, 200_000, 2_000); + + // 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::PowerSetting, &[0x07, 0x07, 0x3F, 0x3F])?; + self.command(spi, Command::PowerOn)?; + // C driver adds a static 100ms delay here + self.wait_until_idle(spi, delay)?; + // Done, but this is also the default + // 0x1F = B/W mode ? doesnt seem to work + self.cmd_with_data(spi, Command::PanelSetting, &[0x0F])?; + // Not done in C driver, this is the default + //self.cmd_with_data(spi, Command::PllControl, &[0x06])?; + self.cmd_with_data(spi, Command::TconResolution, &[0x03, 0x20, 0x01, 0xE0])?; + // Documentation removed in v3 but done in v2 and works in v3 + self.cmd_with_data(spi, Command::DualSpi, &[0x00])?; + // 0x10 in BW mode (Work ?) V + // 0x12 in BW mode to disable new/old thing + // 0x01 -> Black border + // 0x11 -> White norder + // 0x21 -> Red border + // 0x31 -> don't touch border + // the second nibble can change polarity (may be easier for default + // display initialization) V + self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x11, 0x07])?; + // This is the default + self.cmd_with_data(spi, Command::TconSetting, &[0x22])?; + self.cmd_with_data(spi, Command::SpiFlashControl, &[0x00, 0x00, 0x00, 0x00])?; + // Not in C driver + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareThreeColorDisplay + for Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + 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) + } + + /// Update only the black/white data of the display. + /// + /// Finish by calling `update_chromatic_frame`. + fn update_achromatic_frame( + &mut self, + spi: &mut SPI, + _delay: &mut DELAY, + black: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission1)?; + self.interface.data(spi, black)?; + self.interface.cmd(spi, Command::DataStop)?; + Ok(()) + } + + /// Update only chromatic data of the display. + /// + /// This data takes precedence over the black/white data. + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error> { + self.interface.cmd(spi, Command::DataStartTransmission2)?; + self.interface.data(spi, chromatic)?; + self.interface.cmd(spi, Command::DataStop)?; + + self.wait_until_idle(spi, delay)?; + Ok(()) + } +} + +impl WaveshareDisplay + for Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + type DisplayColor = TriColor; + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result { + let interface = DisplayInterface::new(busy, dc, rst, delay_us); + 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, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.command(spi, Command::PowerOff)?; + self.wait_until_idle(spi, delay)?; + self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; + Ok(()) + } + + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + // (B) version sends one buffer for black and one for red + self.cmd_with_data( + spi, + Command::DataStartTransmission1, + &buffer[..NUM_DISPLAY_BITS], + )?; + self.cmd_with_data( + spi, + Command::DataStartTransmission2, + &buffer[NUM_DISPLAY_BITS..], + )?; + self.interface.cmd(spi, Command::DataStop)?; + 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.wait_until_idle(spi, delay)?; + self.command(spi, Command::DisplayRefresh)?; + 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.command(spi, Command::DisplayRefresh)?; + Ok(()) + } + + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + self.send_resolution(spi)?; + + self.command(spi, Command::DataStartTransmission1)?; + self.interface.data_x_times(spi, 0xFF, WIDTH / 8 * HEIGHT)?; + + self.command(spi, Command::DataStartTransmission2)?; + self.interface.data_x_times(spi, 0x00, WIDTH / 8 * HEIGHT)?; + + self.interface.cmd(spi, Command::DataStop)?; + + self.command(spi, Command::DisplayRefresh)?; + + Ok(()) + } + + fn set_background_color(&mut self, color: Self::DisplayColor) { + self.color = color; + } + + fn background_color(&self) -> &Self::DisplayColor { + &self.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!(); + } + + /// wait + fn wait_until_idle(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { + self.interface + .wait_until_idle_with_cmd(spi, delay, IS_BUSY_LOW, Command::GetStatus) + } +} + +impl Epd7in5 +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// temporary replacement for missing delay in the trait to call wait_until_idle + #[allow(clippy::too_many_arguments)] + pub fn update_partial_frame2( + &mut self, + spi: &mut SPI, + buffer: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + delay: &mut DELAY, + ) -> Result<(), SPI::Error> { + self.wait_until_idle(spi, delay)?; + if buffer.len() as u32 != width / 8 * height { + //TODO panic or error + } + + let hrst_upper = (x / 8) as u8 >> 5; + let hrst_lower = ((x / 8) << 3) as u8; + let hred_upper = ((x + width) / 8 - 1) as u8 >> 5; + let hred_lower = (((x + width) / 8 - 1) << 3) as u8 | 0b111; + let vrst_upper = (y >> 8) as u8; + let vrst_lower = y as u8; + let vred_upper = ((y + height - 1) >> 8) as u8; + let vred_lower = (y + height - 1) as u8; + let pt_scan = 0x01; // Gates scan both inside and outside of the partial window. (default) + + self.command(spi, Command::PartialIn)?; + self.cmd_with_data( + spi, + Command::PartialWindow, + &[ + hrst_upper, hrst_lower, hred_upper, hred_lower, vrst_upper, vrst_lower, vred_upper, + vred_lower, pt_scan, + ], + )?; + let half = buffer.len() / 2; + self.cmd_with_data(spi, Command::DataStartTransmission1, &buffer[..half])?; + self.cmd_with_data(spi, Command::DataStartTransmission2, &buffer[half..])?; + + self.command(spi, Command::DisplayRefresh)?; + self.wait_until_idle(spi, delay)?; + + self.command(spi, Command::PartialOut)?; + Ok(()) + } + + 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 send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { + let w = self.width(); + let h = self.height(); + + self.command(spi, Command::TconResolution)?; + 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, TriColor::White); + } +} diff --git a/epd-waveshare/src/graphics.rs b/epd-waveshare/src/graphics.rs new file mode 100644 index 0000000..51ddda5 --- /dev/null +++ b/epd-waveshare/src/graphics.rs @@ -0,0 +1,452 @@ +//! Graphics Support for EPDs + +use crate::color::{ColorType, TriColor}; +use core::marker::PhantomData; +use embedded_graphics_core::prelude::*; + +/// Display rotation, only 90° increments supported +#[derive(Clone, Copy, Default)] +pub enum DisplayRotation { + /// No rotation + #[default] + Rotate0, + /// Rotate by 90 degrees clockwise + Rotate90, + /// Rotate by 180 degrees clockwise + Rotate180, + /// Rotate 270 degrees clockwise + Rotate270, +} + +/// count the number of bytes per line knowing that it may contains padding bits +const fn line_bytes(width: u32, bits_per_pixel: usize) -> usize { + // round to upper 8 bit count + (width as usize * bits_per_pixel + 7) / 8 +} + +/// Display bffer used for drawing with embedded graphics +/// This can be rendered on EPD using ... +/// +/// - WIDTH: width in pixel when display is not rotated +/// - HEIGHT: height in pixel when display is not rotated +/// - BWRBIT: mandatory value of the B/W when chromatic bit is set, can be any value for non +/// tricolor epd +/// - COLOR: color type used by the target display +/// - BYTECOUNT: This is redundant with prvious data and should be removed when const generic +/// expressions are stabilized +/// +/// More on BWRBIT: +/// +/// Different chromatic displays differently treat the bits in chromatic color planes. +/// Some of them ([crate::epd2in13bc]) will render a color pixel if bit is set for that pixel, +/// which is a [DisplayColorRendering::Positive] mode. +/// +/// Other displays, like [crate::epd5in83b_v2] in opposite, will draw color pixel if bit is +/// cleared for that pixel, which is a [DisplayColorRendering::Negative] mode. +/// +/// BWRBIT=true: chromatic doesn't override white, white bit cleared for black, white bit set for white, both bits set for chromatic +/// BWRBIT=false: chromatic does override white, both bits cleared for black, white bit set for white, red bit set for black +pub struct Display< + const WIDTH: u32, + const HEIGHT: u32, + const BWRBIT: bool, + const BYTECOUNT: usize, + COLOR: ColorType + PixelColor, +> { + buffer: [u8; BYTECOUNT], + rotation: DisplayRotation, + _color: PhantomData, +} + +impl< + const WIDTH: u32, + const HEIGHT: u32, + const BWRBIT: bool, + const BYTECOUNT: usize, + COLOR: ColorType + PixelColor, + > Default for Display +{ + /// Initialize display with the color '0', which may not be the same on all device. + /// Many devices have a bit parameter polarity that should be changed if this is not the right + /// one. + /// However, every device driver should implement a DEFAULT_COLOR constant to indicate which + /// color this represents (TODO) + /// + /// If you want a specific default color, you can still call clear() to set one. + // inline is necessary here to allow heap allocation via Box on stack limited programs + #[inline(always)] + fn default() -> Self { + Self { + // default color must be 0 for every bit in a pixel to make this work everywere + buffer: [0u8; BYTECOUNT], + rotation: DisplayRotation::default(), + _color: PhantomData, + } + } +} + +/// For use with embedded_grahics +impl< + const WIDTH: u32, + const HEIGHT: u32, + const BWRBIT: bool, + const BYTECOUNT: usize, + COLOR: ColorType + PixelColor, + > DrawTarget for Display +{ + type Color = COLOR; + type Error = core::convert::Infallible; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + for pixel in pixels { + self.set_pixel(pixel); + } + Ok(()) + } +} + +/// For use with embedded_grahics +impl< + const WIDTH: u32, + const HEIGHT: u32, + const BWRBIT: bool, + const BYTECOUNT: usize, + COLOR: ColorType + PixelColor, + > OriginDimensions for Display +{ + fn size(&self) -> Size { + match self.rotation { + DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => Size::new(WIDTH, HEIGHT), + DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => Size::new(HEIGHT, WIDTH), + } + } +} + +impl< + const WIDTH: u32, + const HEIGHT: u32, + const BWRBIT: bool, + const BYTECOUNT: usize, + COLOR: ColorType + PixelColor, + > Display +{ + /// get internal buffer to use it (to draw in epd) + pub fn buffer(&self) -> &[u8] { + &self.buffer + } + + /// Set the display rotation. + /// + /// This only concerns future drawing made to it. Anything aready drawn + /// stays as it is in the buffer. + pub fn set_rotation(&mut self, rotation: DisplayRotation) { + self.rotation = rotation; + } + + /// Get current rotation + pub fn rotation(&self) -> DisplayRotation { + self.rotation + } + + /// Set a specific pixel color on this display + pub fn set_pixel(&mut self, pixel: Pixel) { + set_pixel( + &mut self.buffer, + WIDTH, + HEIGHT, + self.rotation, + BWRBIT, + pixel, + ); + } +} + +/// Some Tricolor specifics +impl + Display +{ + /// get black/white internal buffer to use it (to draw in epd) + pub fn bw_buffer(&self) -> &[u8] { + &self.buffer[..self.buffer.len() / 2] + } + + /// get chromatic internal buffer to use it (to draw in epd) + pub fn chromatic_buffer(&self) -> &[u8] { + &self.buffer[self.buffer.len() / 2..] + } +} + +/// Same as `Display`, except that its characteristics are defined at runtime. +/// See display for documentation as everything is the same except that default +/// is replaced by a `new` method. +pub struct VarDisplay<'a, COLOR: ColorType + PixelColor> { + width: u32, + height: u32, + bwrbit: bool, + buffer: &'a mut [u8], + rotation: DisplayRotation, + _color: PhantomData, +} + +/// For use with embedded_grahics +impl<'a, COLOR: ColorType + PixelColor> DrawTarget for VarDisplay<'a, COLOR> { + type Color = COLOR; + type Error = core::convert::Infallible; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + for pixel in pixels { + self.set_pixel(pixel); + } + Ok(()) + } +} + +/// For use with embedded_grahics +impl<'a, COLOR: ColorType + PixelColor> OriginDimensions for VarDisplay<'a, COLOR> { + fn size(&self) -> Size { + match self.rotation { + DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => { + Size::new(self.width, self.height) + } + DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => { + Size::new(self.height, self.width) + } + } + } +} + +/// Error found during usage of VarDisplay +#[derive(Debug)] +pub enum VarDisplayError { + /// The provided buffer was too small + BufferTooSmall, +} + +impl<'a, COLOR: ColorType + PixelColor> VarDisplay<'a, COLOR> { + /// You must allocate the buffer by yourself, it must be large enough to contain all pixels. + /// + /// Parameters are documented in `Display` as they are the same as the const generics there. + /// bwrbit should be false for non tricolor displays + pub fn new( + width: u32, + height: u32, + buffer: &'a mut [u8], + bwrbit: bool, + ) -> Result { + let myself = Self { + width, + height, + bwrbit, + buffer, + rotation: DisplayRotation::default(), + _color: PhantomData, + }; + // enfore some constraints dynamicly + if myself.buffer_size() > myself.buffer.len() { + return Err(VarDisplayError::BufferTooSmall); + } + Ok(myself) + } + + /// get the number of used bytes in the buffer + fn buffer_size(&self) -> usize { + self.height as usize + * line_bytes( + self.width, + COLOR::BITS_PER_PIXEL_PER_BUFFER * COLOR::BUFFER_COUNT, + ) + } + + /// get internal buffer to use it (to draw in epd) + pub fn buffer(&self) -> &[u8] { + &self.buffer[..self.buffer_size()] + } + + /// Set the display rotation. + /// + /// This only concerns future drawing made to it. Anything aready drawn + /// stays as it is in the buffer. + pub fn set_rotation(&mut self, rotation: DisplayRotation) { + self.rotation = rotation; + } + + /// Get current rotation + pub fn rotation(&self) -> DisplayRotation { + self.rotation + } + + /// Set a specific pixel color on this display + pub fn set_pixel(&mut self, pixel: Pixel) { + let size = self.buffer_size(); + set_pixel( + &mut self.buffer[..size], + self.width, + self.height, + self.rotation, + self.bwrbit, + pixel, + ); + } +} + +/// Some Tricolor specifics +impl<'a> VarDisplay<'a, TriColor> { + /// get black/white internal buffer to use it (to draw in epd) + pub fn bw_buffer(&self) -> &[u8] { + &self.buffer[..self.buffer_size() / 2] + } + + /// get chromatic internal buffer to use it (to draw in epd) + pub fn chromatic_buffer(&self) -> &[u8] { + &self.buffer[self.buffer_size() / 2..self.buffer_size()] + } +} + +// This is a function to share code between `Display` and `VarDisplay` +// It sets a specific pixel in a buffer to a given color. +// The big number of parameters is due to the fact that it is an internal function to both +// strctures. +fn set_pixel( + buffer: &mut [u8], + width: u32, + height: u32, + rotation: DisplayRotation, + bwrbit: bool, + pixel: Pixel, +) { + let Pixel(point, color) = pixel; + + // final coordinates + let (x, y) = match rotation { + // as i32 = never use more than 2 billion pixel per line or per column + DisplayRotation::Rotate0 => (point.x, point.y), + DisplayRotation::Rotate90 => (width as i32 - 1 - point.y, point.x), + DisplayRotation::Rotate180 => (width as i32 - 1 - point.x, height as i32 - 1 - point.y), + DisplayRotation::Rotate270 => (point.y, height as i32 - 1 - point.x), + }; + + // Out of range check + if (x < 0) || (x >= width as i32) || (y < 0) || (y >= height as i32) { + // don't do anything in case of out of range + return; + } + + let index = x as usize * COLOR::BITS_PER_PIXEL_PER_BUFFER / 8 + + y as usize * line_bytes(width, COLOR::BITS_PER_PIXEL_PER_BUFFER); + let (mask, bits) = color.bitmask(bwrbit, x as u32); + + if COLOR::BUFFER_COUNT == 2 { + // split buffer is for tricolor displays that use 2 buffer for 2 bits per pixel + buffer[index] = buffer[index] & mask | (bits & 0xFF) as u8; + let index = index + buffer.len() / 2; + buffer[index] = buffer[index] & mask | (bits >> 8) as u8; + } else { + buffer[index] = buffer[index] & mask | bits as u8; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::color::*; + use embedded_graphics::{ + prelude::*, + primitives::{Line, PrimitiveStyle}, + }; + + // test buffer length + #[test] + fn graphics_size() { + // example definition taken from epd1in54 + let display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default(); + assert_eq!(display.buffer().len(), 5000); + } + + // test default background color on all bytes + #[test] + fn graphics_default() { + let display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default(); + for &byte in display.buffer() { + assert_eq!(byte, 0); + } + } + + #[test] + fn graphics_rotation_0() { + let mut display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default(); + let _ = Line::new(Point::new(0, 0), Point::new(7, 0)) + .into_styled(PrimitiveStyle::with_stroke(Color::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, 0); + } + } + + #[test] + fn graphics_rotation_90() { + let mut display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default(); + display.set_rotation(DisplayRotation::Rotate90); + let _ = Line::new(Point::new(0, 192), Point::new(0, 199)) + .into_styled(PrimitiveStyle::with_stroke(Color::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, 0); + } + } + + #[test] + fn graphics_rotation_180() { + let mut display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default(); + display.set_rotation(DisplayRotation::Rotate180); + let _ = Line::new(Point::new(192, 199), Point::new(199, 199)) + .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1)) + .draw(&mut display); + + let buffer = display.buffer(); + + extern crate std; + std::println!("{:?}", buffer); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, 0); + } + } + + #[test] + fn graphics_rotation_270() { + let mut display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default(); + display.set_rotation(DisplayRotation::Rotate270); + let _ = Line::new(Point::new(199, 0), Point::new(199, 7)) + .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1)) + .draw(&mut display); + + let buffer = display.buffer(); + + extern crate std; + std::println!("{:?}", buffer); + + assert_eq!(buffer[0], Color::Black.get_byte_value()); + + for &byte in buffer.iter().skip(1) { + assert_eq!(byte, 0); + } + } +} diff --git a/epd-waveshare/src/interface.rs b/epd-waveshare/src/interface.rs new file mode 100644 index 0000000..f3a9a3d --- /dev/null +++ b/epd-waveshare/src/interface.rs @@ -0,0 +1,208 @@ +use crate::traits::Command; +use core::marker::PhantomData; +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +/// The Connection Interface of all (?) Waveshare EPD-Devices +/// +/// SINGLE_BYTE_WRITE defines if a data block is written bytewise +/// or blockwise to the spi device +pub(crate) struct DisplayInterface { + /// SPI + _spi: PhantomData, + /// DELAY + _delay: PhantomData, + /// Low for busy, Wait until display is ready! + busy: BUSY, + /// Data/Command Control Pin (High for data, Low for command) + dc: DC, + /// Pin for Resetting + rst: RST, + /// number of ms the idle loop should sleep on + delay_us: u32, +} + +impl + DisplayInterface +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// Creates a new `DisplayInterface` struct + /// + /// If no delay is given, a default delay of 10ms is used. + pub fn new(busy: BUSY, dc: DC, rst: RST, delay_us: Option) -> Self { + // default delay of 10ms + let delay_us = delay_us.unwrap_or(10_000); + DisplayInterface { + _spi: PhantomData, + _delay: PhantomData, + busy, + dc, + rst, + delay_us, + } + } + + /// Basic function for sending [Commands](Command). + /// + /// Enables direct interaction with the device with the help of [data()](DisplayInterface::data()) + pub(crate) fn cmd(&mut self, spi: &mut SPI, command: T) -> Result<(), SPI::Error> { + // low for commands + let _ = self.dc.set_low(); + + // Transfer the command over spi + self.write(spi, &[command.address()]) + } + + /// Basic function for sending an array of u8-values of data over spi + /// + /// Enables direct interaction with the device with the help of [command()](Epd4in2::command()) + pub(crate) fn data(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> { + // high for data + let _ = self.dc.set_high(); + + if SINGLE_BYTE_WRITE { + for val in data.iter().copied() { + // Transfer data one u8 at a time over spi + self.write(spi, &[val])?; + } + } else { + self.write(spi, data)?; + } + + Ok(()) + } + + /// Basic function for sending [Commands](Command) and the data belonging to it. + /// + /// TODO: directly use ::write? cs wouldn't needed to be changed twice than + pub(crate) fn cmd_with_data( + &mut self, + spi: &mut SPI, + command: T, + data: &[u8], + ) -> Result<(), SPI::Error> { + self.cmd(spi, command)?; + self.data(spi, data) + } + + /// Basic function for sending the same byte of data (one u8) multiple times over spi + /// + /// Enables direct interaction with the device with the help of [command()](ConnectionInterface::command()) + pub(crate) fn data_x_times( + &mut self, + spi: &mut SPI, + val: u8, + repetitions: u32, + ) -> Result<(), SPI::Error> { + // high for data + let _ = self.dc.set_high(); + // Transfer data (u8) over spi + for _ in 0..repetitions { + self.write(spi, &[val])?; + } + Ok(()) + } + + // spi write helper/abstraction function + fn write(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> { + // transfer spi data + // Be careful!! Linux has a default limit of 4096 bytes per spi transfer + // see https://raspberrypi.stackexchange.com/questions/65595/spi-transfer-fails-with-buffer-size-greater-than-4096 + if cfg!(target_os = "linux") { + for data_chunk in data.chunks(4096) { + spi.write(data_chunk)?; + } + Ok(()) + } else { + spi.write(data) + } + } + + /// Waits until device isn't busy anymore (busy == HIGH) + /// + /// This is normally handled by the more complicated commands themselves, + /// but in the case you send data and commands directly you might need to check + /// if the device is still busy + /// + /// is_busy_low + /// + /// - TRUE for epd4in2, epd2in13, epd2in7, epd5in83, epd7in5 + /// - FALSE for epd2in9, epd1in54 (for all Display Type A ones?) + /// + /// Most likely there was a mistake with the 2in9 busy connection + pub(crate) fn wait_until_idle(&mut self, delay: &mut DELAY, is_busy_low: bool) { + while self.is_busy(is_busy_low) { + // This has been removed and added many time : + // - it is faster to not have it + // - it is complicated to pass the delay everywhere all the time + // - busy waiting can consume more power that delaying + // - delay waiting enables task switching on realtime OS + // -> keep it and leave the decision to the user + if self.delay_us > 0 { + delay.delay_us(self.delay_us); + } + } + } + + /// Same as `wait_until_idle` for device needing a command to probe Busy pin + pub(crate) fn wait_until_idle_with_cmd( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + is_busy_low: bool, + status_command: T, + ) -> Result<(), SPI::Error> { + self.cmd(spi, status_command)?; + if self.delay_us > 0 { + delay.delay_us(self.delay_us); + } + while self.is_busy(is_busy_low) { + self.cmd(spi, status_command)?; + if self.delay_us > 0 { + delay.delay_us(self.delay_us); + } + } + Ok(()) + } + + /// Checks if device is still busy + /// + /// This is normally handled by the more complicated commands themselves, + /// but in the case you send data and commands directly you might need to check + /// if the device is still busy + /// + /// is_busy_low + /// + /// - TRUE for epd4in2, epd2in13, epd2in7, epd5in83, epd7in5 + /// - FALSE for epd2in9, epd1in54 (for all Display Type A ones?) + /// + /// Most likely there was a mistake with the 2in9 busy connection + /// //TODO: use the #cfg feature to make this compile the right way for the certain types + pub(crate) fn is_busy(&mut self, is_busy_low: bool) -> bool { + (is_busy_low && self.busy.is_low().unwrap_or(false)) + || (!is_busy_low && self.busy.is_high().unwrap_or(false)) + } + + /// Resets the device. + /// + /// Often used to awake the module from deep sleep. See [Epd4in2::sleep()](Epd4in2::sleep()) + /// + /// The timing of keeping the reset pin low seems to be important and different per device. + /// Most displays seem to require keeping it low for 10ms, but the 7in5_v2 only seems to reset + /// properly with 2ms + pub(crate) fn reset(&mut self, delay: &mut DELAY, initial_delay: u32, duration: u32) { + let _ = self.rst.set_high(); + delay.delay_us(initial_delay); + + let _ = self.rst.set_low(); + delay.delay_us(duration); + let _ = self.rst.set_high(); + //TODO: the upstream libraries always sleep for 200ms here + // 10ms works fine with just for the 7in5_v2 but this needs to be validated for other devices + delay.delay_us(200_000); + } +} diff --git a/epd-waveshare/src/lib.rs b/epd-waveshare/src/lib.rs new file mode 100644 index 0000000..9b39352 --- /dev/null +++ b/epd-waveshare/src/lib.rs @@ -0,0 +1,135 @@ +//! A simple Driver for the [Waveshare](https://github.com/waveshare/e-Paper) E-Ink Displays via SPI +//! +//! - Built using [`embedded-hal`] traits. +//! - Graphics support is added through [`embedded-graphics`] +//! +//! [`embedded-graphics`]: https://docs.rs/embedded-graphics/ +//! [`embedded-hal`]: https://docs.rs/embedded-hal +//! + +//! +//! # Example +//! +//!```rust, no_run +//!# use embedded_hal_mock::eh1::*; +//!# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +//!use embedded_graphics::{ +//! pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle}, +//!}; +//!use epd_waveshare::{epd1in54::*, 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::NoopDelay::new(); +//! +//!// Setup EPD +//!let mut epd = Epd1in54::new(&mut spi, busy_in, dc, rst, &mut delay, None)?; +//! +//!// Use display graphics from embedded-graphics +//!let mut display = Display1in54::default(); +//! +//!// Use embedded graphics for drawing a line +//! +//!let _ = Line::new(Point::new(0, 120), Point::new(0, 295)) +//! .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1)) +//! .draw(&mut display); +//! +//! // Display updated frame +//!epd.update_frame(&mut spi, &display.buffer(), &mut delay)?; +//!epd.display_frame(&mut spi, &mut delay)?; +//! +//!// Set the EPD to sleep +//!epd.sleep(&mut spi, &mut delay)?; +//!# Ok(()) +//!# } +//!``` +//! +//! # Other information and requirements +//! +//! - Buffersize: Wherever a buffer is used it always needs to be of the size: `width / 8 * length`, +//! where width and length being either the full e-ink size or the partial update window size +//! +//! ### SPI +//! +//! MISO is not connected/available. SPI_MODE_0 is used (CPHL = 0, CPOL = 0) with 8 bits per word, MSB first. +//! +//! Maximum speed tested by myself was 8Mhz but more should be possible (Ben Krasnow used 18Mhz with his implemenation) +//! +#![no_std] +#![deny(missing_docs)] + +#[cfg(feature = "graphics")] +pub mod graphics; + +mod traits; + +pub mod color; + +/// Interface for the physical connection between display and the controlling device +mod interface; + +pub mod epd1in54; +pub mod epd1in54_v2; +pub mod epd1in54b; +pub mod epd1in54c; +pub mod epd2in13_v2; +pub mod epd2in13bc; +pub mod epd2in66b; +pub mod epd2in7b; +pub mod epd2in9; +pub mod epd2in9_v2; +pub mod epd2in9bc; +pub mod epd2in9d; +pub mod epd3in7; +pub mod epd4in2; +pub mod epd5in65f; +pub mod epd5in83_v2; +pub mod epd5in83b_v2; +pub mod epd7in3f; +pub mod epd7in5; +pub mod epd7in5_hd; +pub mod epd7in5_v2; +pub mod epd7in5b_v2; +pub use epd7in5b_v2 as epd7in5b_v3; + +pub(crate) mod type_a; + +/// Includes everything important besides the chosen Display +pub mod prelude { + pub use crate::color::{Color, OctColor, TriColor}; + pub use crate::traits::{ + QuickRefresh, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, + }; + + pub use crate::SPI_MODE; + + #[cfg(feature = "graphics")] + pub use crate::graphics::{Display, DisplayRotation}; +} + +/// Computes the needed buffer length. Takes care of rounding up in case width +/// is not divisible by 8. +/// +/// unused +/// bits width +/// <----><------------------------> +/// \[XXXXX210\]\[76543210\]...\[76543210\] ^ +/// \[XXXXX210\]\[76543210\]...\[76543210\] | height +/// \[XXXXX210\]\[76543210\]...\[76543210\] v +pub const fn buffer_len(width: usize, height: usize) -> usize { + (width + 7) / 8 * height +} + +use embedded_hal::spi::{Mode, Phase, Polarity}; + +/// SPI mode - +/// For more infos see [Requirements: SPI](index.html#spi) +pub const SPI_MODE: Mode = Mode { + phase: Phase::CaptureOnFirstTransition, + polarity: Polarity::IdleLow, +}; diff --git a/epd-waveshare/src/traits.rs b/epd-waveshare/src/traits.rs new file mode 100644 index 0000000..b1f3f73 --- /dev/null +++ b/epd-waveshare/src/traits.rs @@ -0,0 +1,360 @@ +use core::marker::Sized; +use embedded_hal::{delay::*, digital::*, spi::SpiDevice}; + +/// All commands need to have this trait which gives the address of the command +/// which needs to be send via SPI with activated CommandsPin (Data/Command Pin in CommandMode) +pub(crate) trait Command: Copy { + fn address(self) -> u8; +} + +/// Seperates the different LUT for the Display Refresh process +#[derive(Debug, Clone, PartialEq, Eq, Copy, Default)] +pub enum RefreshLut { + /// The "normal" full Lookuptable for the Refresh-Sequence + #[default] + Full, + /// The quick LUT where not the full refresh sequence is followed. + /// This might lead to some + Quick, +} + +pub(crate) trait InternalWiAdditions +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// This initialises the EPD and powers it up + /// + /// This function is already called from + /// - [new()](WaveshareDisplay::new()) + /// - [`wake_up`] + /// + /// + /// This function calls [reset](WaveshareDisplay::reset), + /// so you don't need to call reset your self when trying to wake your device up + /// after setting it to sleep. + fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>; +} + +/// Functions to interact with three color panels +pub trait WaveshareThreeColorDisplay: + WaveshareDisplay +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// Transmit data to the SRAM of the EPD + /// + /// Updates both the black and the secondary color layers + fn update_color_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + black: &[u8], + chromatic: &[u8], + ) -> Result<(), SPI::Error>; + + /// Update only the black/white data of the display. + /// + /// This must be finished by calling `update_chromatic_frame`. + fn update_achromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + black: &[u8], + ) -> Result<(), SPI::Error>; + + /// Update only the chromatic data of the display. + /// + /// This should be preceded by a call to `update_achromatic_frame`. + /// This data takes precedence over the black/white data. + fn update_chromatic_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + chromatic: &[u8], + ) -> Result<(), SPI::Error>; +} + +/// All the functions to interact with the EPDs +/// +/// This trait includes all public functions to use the EPDs +/// +/// # Example +/// +///```rust, no_run +///# use embedded_hal_mock::eh1::*; +///# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +///use embedded_graphics::{ +/// pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle}, +///}; +///use epd_waveshare::{epd4in2::*, 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::NoopDelay::new(); +/// +///// Setup EPD +///let mut epd = Epd4in2::new(&mut spi, busy_in, dc, rst, &mut delay, None)?; +/// +///// Use display graphics from embedded-graphics +///let mut display = Display4in2::default(); +/// +///// Use embedded graphics for drawing a line +/// +///let _ = Line::new(Point::new(0, 120), Point::new(0, 295)) +/// .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1)) +/// .draw(&mut display); +/// +/// // Display updated frame +///epd.update_frame(&mut spi, &display.buffer(), &mut delay)?; +///epd.display_frame(&mut spi, &mut delay)?; +/// +///// Set the EPD to sleep +///epd.sleep(&mut spi, &mut delay)?; +///# Ok(()) +///# } +///``` +pub trait WaveshareDisplay +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// The Color Type used by the Display + type DisplayColor; + /// Creates a new driver from a SPI peripheral, CS Pin, Busy InputPin, DC + /// + /// `delay_us` is the number of us the idle loop should sleep on. + /// Setting it to 0 implies busy waiting. + /// Setting it to None means a default value is used. + /// + /// This already initialises the device. + fn new( + spi: &mut SPI, + busy: BUSY, + dc: DC, + rst: RST, + delay: &mut DELAY, + delay_us: Option, + ) -> Result + where + Self: Sized; + + /// Let the device enter deep-sleep mode to save power. + /// + /// The deep sleep mode returns to standby with a hardware reset. + fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>; + + /// Wakes the device up from sleep + /// + /// Also reintialises the device if necessary. + fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>; + + /// Sets the backgroundcolor for various commands like [clear_frame](WaveshareDisplay::clear_frame) + fn set_background_color(&mut self, color: Self::DisplayColor); + + /// Get current background color + fn background_color(&self) -> &Self::DisplayColor; + + /// Get the width of the display + fn width(&self) -> u32; + + /// Get the height of the display + fn height(&self) -> u32; + + /// Transmit a full frame to the SRAM of the EPD + fn update_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error>; + + /// Transmits partial data to the SRAM of the EPD + /// + /// (x,y) is the top left corner + /// + /// BUFFER needs to be of size: width / 8 * height ! + #[allow(clippy::too_many_arguments)] + 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>; + + /// Displays the frame data from SRAM + /// + /// This function waits until the device isn`t busy anymore + fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>; + + /// Provide a combined update&display and save some time (skipping a busy check in between) + fn update_and_display_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error>; + + /// Clears the frame buffer on the EPD with the declared background color + /// + /// The background color can be changed with [`WaveshareDisplay::set_background_color`] + fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>; + + /// Trait for using various Waveforms from different LUTs + /// E.g. for partial refreshes + /// + /// A full refresh is needed after a certain amount of quick refreshes! + /// + /// WARNING: Quick Refresh might lead to ghosting-effects/problems with your display. Especially for the 4.2in Display! + /// + /// If None is used the old value will be loaded on the LUTs once more + fn set_lut( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + refresh_rate: Option, + ) -> Result<(), SPI::Error>; + + /// Wait until the display has stopped processing data + /// + /// You can call this to make sure a frame is displayed before goin further + fn wait_until_idle(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>; +} + +/// Allows quick refresh support for displays that support it; lets you send both +/// old and new frame data to support this. +/// +/// When using the quick refresh look-up table, the display must receive separate display +/// buffer data marked as old, and new. This is used to determine which pixels need to change, +/// and how they will change. This isn't required when using full refreshes. +/// +/// (todo: Example ommitted due to CI failures.) +/// Example: +///```rust, no_run +///# use embedded_hal_mock::eh1::*; +///# fn main() -> Result<(), embedded_hal::spi::ErrorKind> { +///# use embedded_graphics::{ +///# pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle}, +///# }; +///# use epd_waveshare::{epd4in2::*, prelude::*}; +///# use epd_waveshare::graphics::VarDisplay; +///# +///# 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::NoopDelay::new(); +///# +///# // Setup EPD +///# let mut epd = Epd4in2::new(&mut spi, busy_in, dc, rst, &mut delay, None)?; +///let (x, y, frame_width, frame_height) = (20, 40, 80,80); +/// +///let mut buffer = [DEFAULT_BACKGROUND_COLOR.get_byte_value(); 80 / 8 * 80]; +///let mut display = VarDisplay::new(frame_width, frame_height, &mut buffer,false).unwrap(); +/// +///epd.update_partial_old_frame(&mut spi, &mut delay, display.buffer(), x, y, frame_width, frame_height) +/// .ok(); +/// +///display.clear(Color::White).ok(); +///// Execute drawing commands here. +/// +///epd.update_partial_new_frame(&mut spi, &mut delay, display.buffer(), x, y, frame_width, frame_height) +/// .ok(); +///# Ok(()) +///# } +///``` +pub trait QuickRefresh +where + SPI: SpiDevice, + BUSY: InputPin, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, +{ + /// Updates the old frame. + fn update_old_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error>; + + /// Updates the new frame. + fn update_new_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error>; + + /// Displays the new frame + fn display_new_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error>; + + /// Updates and displays the new frame. + fn update_and_display_new_frame( + &mut self, + spi: &mut SPI, + buffer: &[u8], + delay: &mut DELAY, + ) -> Result<(), SPI::Error>; + + /// Updates the old frame for a portion of the display. + #[allow(clippy::too_many_arguments)] + fn update_partial_old_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error>; + + /// Updates the new frame for a portion of the display. + #[allow(clippy::too_many_arguments)] + fn update_partial_new_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + buffer: &[u8], + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error>; + + /// Clears the partial frame buffer on the EPD with the declared background color + /// The background color can be changed with [`WaveshareDisplay::set_background_color`] + fn clear_partial_frame( + &mut self, + spi: &mut SPI, + delay: &mut DELAY, + x: u32, + y: u32, + width: u32, + height: u32, + ) -> Result<(), SPI::Error>; +} diff --git a/epd-waveshare/src/type_a/command.rs b/epd-waveshare/src/type_a/command.rs new file mode 100644 index 0000000..170156e --- /dev/null +++ b/epd-waveshare/src/type_a/command.rs @@ -0,0 +1,103 @@ +//! SPI Commands for the Waveshare 2.9" and 1.54" E-Ink Display + +use crate::traits; + +/// Epd1in54 and EPD2IN9 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)] +#[derive(Copy, Clone)] +pub(crate) enum Command { + /// Driver Output control + /// 3 Databytes: + /// A[7:0] + /// 0.. A[8] + /// 0.. B[2:0] + /// Default: Set A[8:0] = 0x127 and B[2:0] = 0x0 + DriverOutputControl = 0x01, + GateDrivingVoltage = 0x03, + SourceDrivingVoltage = 0x04, + /// Booster Soft start control + /// 3 Databytes: + /// 1.. A[6:0] + /// 1.. B[6:0] + /// 1.. C[6:0] + /// Default: A[7:0] = 0xCF, B[7:0] = 0xCE, C[7:0] = 0x8D + BoosterSoftStartControl = 0x0C, + GateScanStartPosition = 0x0F, + //TODO: useful? + // GateScanStartPosition = 0x0F, + /// Deep Sleep Mode Control + /// 1 Databyte: + /// 0.. A[0] + /// Values: + /// A[0] = 0: Normal Mode (POR) + /// A[0] = 1: Enter Deep Sleep Mode + DeepSleepMode = 0x10, + // /// Data Entry mode setting + DataEntryModeSetting = 0x11, + + SwReset = 0x12, + + TemperatureSensorSelection = 0x18, + + TemperatureSensorControl = 0x1A, + + MasterActivation = 0x20, + + DisplayUpdateControl1 = 0x21, + + DisplayUpdateControl2 = 0x22, + + WriteRam = 0x24, + + WriteRam2 = 0x26, + + WriteVcomRegister = 0x2C, + + WriteLutRegister = 0x32, + + WriteOtpSelection = 0x37, + + SetDummyLinePeriod = 0x3A, + + SetGateLineWidth = 0x3B, + + BorderWaveformControl = 0x3C, + + WriteLutRegisterEnd = 0x3f, + + SetRamXAddressStartEndPosition = 0x44, + + SetRamYAddressStartEndPosition = 0x45, + + SetRamXAddressCounter = 0x4E, + + SetRamYAddressCounter = 0x4F, + + Nop = 0xFF, +} + +impl traits::Command for Command { + /// Returns the address of the command + fn address(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::Command; + use crate::traits::Command as CommandTrait; + + #[test] + fn command_addr() { + assert_eq!(Command::DriverOutputControl.address(), 0x01); + + assert_eq!(Command::SetRamXAddressCounter.address(), 0x4E); + + assert_eq!(Command::Nop.address(), 0xFF); + } +} diff --git a/epd-waveshare/src/type_a/constants.rs b/epd-waveshare/src/type_a/constants.rs new file mode 100644 index 0000000..69116e8 --- /dev/null +++ b/epd-waveshare/src/type_a/constants.rs @@ -0,0 +1,25 @@ +#[cfg(not(any(feature = "type_a_alternative_faster_lut")))] +#[rustfmt::skip] +// Original Waveforms from Waveshare +pub(crate) const LUT_FULL_UPDATE: [u8; 30] =[ + 0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, + 0x66, 0x69, 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, + 0x00, 0x00, 0x00, 0x00, 0xF8, 0xB4, 0x13, 0x51, + 0x35, 0x51, 0x51, 0x19, 0x01, 0x00 +]; + +#[rustfmt::skip] +pub(crate) const LUT_PARTIAL_UPDATE: [u8; 30] =[ + 0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +]; + +#[cfg(feature = "type_a_alternative_faster_lut")] +#[rustfmt::skip] +// Waveform from TeXiToi/il3820 +pub(crate) const LUT_FULL_UPDATE: [u8; 30] =[ + 0x50, 0xAA, 0x55, 0xAA, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; diff --git a/epd-waveshare/src/type_a/mod.rs b/epd-waveshare/src/type_a/mod.rs new file mode 100644 index 0000000..84c4f1c --- /dev/null +++ b/epd-waveshare/src/type_a/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod command; +pub(crate) mod constants;