diff --git a/README.md b/README.md index 79c9fec3..15c2f90b 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ epd.update_and_display_frame( & mut spi, & display.buffer()) ?; | [2.9 Inch B/W (A)](https://www.waveshare.com/product/2.9inch-e-paper-module.htm) | Black, White | ✕ | ✔ | ✔ | ✔ | | [2.9 Inch B/W V2 (A)](https://www.waveshare.com/product/2.9inch-e-paper-module.htm) | Black, White | ✕ | ✔ | ✔ | ✔ | | [2.7 Inch 3 Color (B)](https://www.waveshare.com/2.7inch-e-paper-b.htm) | Black, White, Red | ✕ | ✔ | ✔ | ✔ | +| [2.66 Inch 3 Color (B)](https://www.waveshare.com/wiki/Pico-ePaper-2.66-B) | Black, White, Red | ✕ | ✕ | ✔ | ✔ | | [1.54 Inch B/W/Y (C) (Discontinued)](https://www.waveshare.com/1.54inch-e-paper-module-c.htm) | Black, White, Yellow | ✕ | ✕ | ✔ | ✔ | | [1.54 Inch B/W/R (B)](https://www.waveshare.com/1.54inch-e-Paper-B.htm) | Black, White, Red | ✕ | ✕ | ✔ | ✔ | | [1.54 Inch B/W (A)](https://www.waveshare.com/1.54inch-e-Paper-Module.htm) | Black, White | ✕ | ✔ | ✔ | ✔ | diff --git a/src/epd2in66b/command.rs b/src/epd2in66b/command.rs new file mode 100644 index 00000000..1a50918a --- /dev/null +++ b/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/src/epd2in66b/mod.rs b/src/epd2in66b/mod.rs new file mode 100644 index 00000000..092fce3f --- /dev/null +++ b/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/src/lib.rs b/src/lib.rs index a994e03b..b948814d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,7 @@ 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;