epd_waveshare_async/
epd2in9.rs

1use core::time::Duration;
2use embedded_graphics::{
3    pixelcolor::BinaryColor,
4    prelude::{Dimensions, Point, Size},
5    primitives::Rectangle,
6};
7use embedded_hal::{
8    digital::{InputPin, OutputPin},
9    spi::{Phase, Polarity},
10};
11use embedded_hal_async::{delay::DelayNs, digital::Wait, spi::SpiDevice};
12
13use crate::{
14    buffer::{binary_buffer_length, BinaryBuffer},
15    log::{debug, trace},
16    Epd, EpdHw,
17};
18
19/// LUT for a full refresh. This should be used occasionally for best display results.
20///
21/// See [RECOMMENDED_MIN_FULL_REFRESH_INTERVAL] and [RECOMMENDED_MAX_FULL_REFRESH_INTERVAL].
22const LUT_FULL_UPDATE: [u8; 30] = [
23    0x50, 0xAA, 0x55, 0xAA, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
24    0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
25];
26/// LUT for a partial refresh. This should be used for frequent updates, but it's recommended to
27/// perform a full refresh occasionally.
28///
29/// See [RECOMMENDED_MIN_FULL_REFRESH_INTERVAL] and [RECOMMENDED_MAX_FULL_REFRESH_INTERVAL].
30const LUT_PARTIAL_UPDATE: [u8; 30] = [
31    0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
32    0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
33];
34
35#[cfg_attr(feature = "defmt", derive(defmt::Format))]
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37/// The refresh mode for the display.
38pub enum RefreshMode {
39    /// Use the full update LUT. This is slower, but should be done occasionally to avoid ghosting.
40    ///
41    /// It's recommended to avoid full refreshes less than [RECOMMENDED_MIN_FULL_REFRESH_INTERVAL] apart,
42    /// but to do a full refresh at least every [RECOMMENDED_MAX_FULL_REFRESH_INTERVAL].
43    Full,
44    /// Uses the partial update LUT for fast refresh. A full refresh should be done occasionally to
45    /// avoid ghosting, see [RECOMMENDED_MAX_FULL_REFRESH_INTERVAL].
46    ///
47    /// This is the standard "fast" update. It diffs the current framebuffer against the
48    /// previous framebuffer, and just updates the pixels that differ.
49    Partial,
50    /// Uses the partial update LUT for a fast refresh, but only updates black (`BinaryColor::Off`)
51    /// pixels from the current framebuffer. The previous framebuffer is ignored.
52    PartialBlackBypass,
53    /// Uses the partial update LUT for a fast refresh, but only updates white (`BinaryColor::On`)
54    /// pixels from the current framebuffer. The previous framebuffer is ignored.
55    PartialWhiteBypass,
56}
57
58impl RefreshMode {
59    /// Returns the LUT to use for this refresh mode.
60    pub fn lut(&self) -> &[u8; 30] {
61        match self {
62            RefreshMode::Full => &LUT_FULL_UPDATE,
63            _ => &LUT_PARTIAL_UPDATE,
64        }
65    }
66}
67
68/// The height of the display (portrait orientation).
69pub const DISPLAY_HEIGHT: u16 = 296;
70/// The width of the display (portrait orientation).
71pub const DISPLAY_WIDTH: u16 = 128;
72/// It's recommended to avoid doing a full refresh more often than this (at least on a regular basis).
73pub const RECOMMENDED_MIN_FULL_REFRESH_INTERVAL: Duration = Duration::from_secs(180);
74/// It's recommended to do a full refresh at least this often.
75pub const RECOMMENDED_MAX_FULL_REFRESH_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
76pub const RECOMMENDED_SPI_HZ: u32 = 4_000_000; // 4 MHz
77/// Use this phase in conjunction with [RECOMMENDED_SPI_POLARITY] so that the EPD can capture data
78/// on the rising edge.
79pub const RECOMMENDED_SPI_PHASE: Phase = Phase::CaptureOnFirstTransition;
80/// Use this polarity in conjunction with [RECOMMENDED_SPI_PHASE] so that the EPD can capture data
81/// on the rising edge.
82pub const RECOMMENDED_SPI_POLARITY: Polarity = Polarity::IdleLow;
83
84/// Low-level commands for the Epd2In9. You probably want to use the other methods exposed on the
85/// [Epd2In9] for most operations, but can send commands directly with [Epd2In9::send] for low-level
86/// control or experimentation.
87#[cfg_attr(feature = "defmt", derive(defmt::Format))]
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum Command {
90    /// Used to initialise the display.
91    DriverOutputControl = 0x01,
92    /// Used to configure the on chip voltage booster and regulator.
93    BoosterSoftStartControl = 0x0C,
94    /// Used to enter deep sleep mode. Requires a hardware reset and reinitialisation to wake up.
95    DeepSleepMode = 0x10,
96    /// Changes the auto-increment behaviour of the address counter.
97    DataEntryModeSetting = 0x11,
98    /// Resets all commands and parameters to default values (except deep sleep mode).
99    SwReset = 0x12,
100    /// Writes to the temperature register.
101    TemperatureSensorControl = 0x1A,
102    /// Activates the display update sequence. This must be set beforehand using [Command::DisplayUpdateControl2].
103    /// This operation must not be interrupted.
104    MasterActivation = 0x20,
105    /// Used for a RAM "bypass" mode when using [RefreshMode::Partial]. This is poorly explained in the docs,
106    /// but essentially we have three options:
107    ///
108    /// 1. `0x00` (default): just update the pixels that have changed **between the two internal
109    ///    frame buffers**. This normally does what you expect. You can hack it a bit to do
110    ///    interesting things by writing to both the old and new frame buffers.
111    /// 2. `0x80`: just update the white (`BinaryColor::On`) pixels in the current frame buffer. It
112    ///    doesn't matter what is in the old frame buffer.
113    /// 3. `0x90`: just update the black (`BinaryColor::Off`) pixels in the current frame buffer.
114    ///    It doesn't matter what is in the old frame buffer.
115    ///
116    /// Options 2 and 3 are what the datasheet calls "bypass" mode.
117    DisplayUpdateControl1 = 0x21,
118    /// Configures the display update sequence for use with [Command::MasterActivation].
119    DisplayUpdateControl2 = 0x22,
120    /// Writes data to the current frame buffer, auto-incrementing the address counter.
121    WriteRam = 0x24,
122    /// Writes data to the old frame buffer, auto-incrementing the address counter.
123    WriteOldRam = 0x26,
124    /// Writes to the VCOM register.
125    WriteVcom = 0x2C,
126    /// Writes the LUT register (30 bytes, exclude the VSH/VSL and dummy bits).
127    WriteLut = 0x32,
128    /// ? Part of magic config.
129    SetDummyLinePeriod = 0x3A,
130    /// ? Part of magic config.
131    SetGateLineWidth = 0x3B,
132    /// Register to configure the behaviour of the border.
133    BorderWaveformControl = 0x3C,
134    /// Sets the start and end positions of the X axis for the auto-incrementing address counter.
135    /// Note that the x position can only be configured as a multiple of 8.
136    SetRamXStartEnd = 0x44,
137    /// Sets the start and end positions of the Y axis for the auto-incrementing address counter.
138    SetRamYStartEnd = 0x45,
139    /// Sets the current x coordinate of the address counter.
140    /// Note that the x position can only be configured as a multiple of 8.
141    SetRamX = 0x4E,
142    /// Sets the current y coordinate of the address counter.
143    SetRamY = 0x4F,
144    /// Does nothing, but can be used to terminate other commands such as [Command::WriteRam]
145    Noop = 0xFF,
146}
147
148impl Command {
149    /// Returns the register address for this command.
150    fn register(&self) -> u8 {
151        *self as u8
152    }
153}
154
155/// The buffer type used by [Epd2In9].
156pub type Epd2In9Buffer =
157    BinaryBuffer<{ binary_buffer_length(Size::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32)) }>;
158
159/// This should be sent with [Command::DriverOutputControl] during initialisation.
160///
161/// From the sample code, the bytes mean the following:
162///
163/// * low byte of display long edge
164/// * high byte of display long edge
165/// * GD = 0, SM = 0, TB = 0 (unclear what this means)
166const DRIVER_OUTPUT_INIT_DATA: [u8; 3] = [0x27, 0x01, 0x00];
167/// This should be sent with [Command::BoosterSoftStartControl] during initialisation.
168/// Note that there are two versions of this command, one in the datasheet, and one in the sample code.
169const BOOSTER_SOFT_START_INIT_DATA: [u8; 3] = [0xD7, 0xD6, 0x9D];
170// Sample code: ^
171// Datasheet:
172// const BOOSTER_SOFT_START_INIT_DATA: [u8; 3] = [0xCF, 0xCE, 0x8D];
173
174/// Controls v1 of the 2.9" Waveshare e-paper display.
175///
176/// * [datasheet](https://files.waveshare.com/upload/e/e6/2.9inch_e-Paper_Datasheet.pdf)
177/// * [sample code](https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in9.py)
178///
179/// The display has a portrait orientation. This uses [BinaryColor], where `Off` is black and `On` is white.
180pub struct Epd2In9<HW>
181where
182    HW: EpdHw,
183{
184    hw: HW,
185    refresh_mode: Option<RefreshMode>,
186}
187
188impl<HW> Epd2In9<HW>
189where
190    HW: EpdHw,
191{
192    pub fn new(hw: HW) -> Self {
193        Epd2In9 {
194            hw,
195            refresh_mode: None,
196        }
197    }
198
199    /// Sets the border to the specified colour. You need to call [Epd::update_display] using
200    /// [RefreshMode::Full] afterwards to apply this change.
201    ///
202    /// Note: on my board, the white setting fades to grey fairly quickly. I have not found a way
203    /// to avoid this.
204    pub async fn set_border(
205        &mut self,
206        spi: &mut HW::Spi,
207        color: BinaryColor,
208    ) -> Result<(), HW::Error> {
209        let border_setting: u8 = match color {
210            BinaryColor::Off => 0x00,
211            BinaryColor::On => 0x01,
212        };
213        self.send(spi, Command::BorderWaveformControl, &[border_setting])
214            .await
215    }
216
217    /// Writes buffer data into the old internal framebuffer. This can be useful either:
218    ///
219    /// * to prep the next frame before the current one has been displayed (since the old buffer
220    ///   becomes the current buffer after the next call to [Self::update_display()]).
221    /// * to modify the "diff" that is displayed if in [RefreshMode::Partial]. Also see [Command::DisplayUpdateControl1].
222    pub async fn write_old_framebuffer(
223        &mut self,
224        spi: &mut HW::Spi,
225        buffer: &Epd2In9Buffer,
226    ) -> Result<(), <HW as EpdHw>::Error> {
227        let buffer_bounds = buffer.bounding_box();
228        self.set_window(spi, buffer_bounds).await?;
229        self.set_cursor(spi, buffer_bounds.top_left).await?;
230        self.send(spi, Command::WriteOldRam, buffer.data()).await
231    }
232
233    /// Send the following command and data to the display. Waits until the display is no longer busy before sending.
234    pub async fn send(
235        &mut self,
236        spi: &mut HW::Spi,
237        command: Command,
238        data: &[u8],
239    ) -> Result<(), HW::Error> {
240        trace!("Sending EPD command: {:?}", command);
241        self.wait_if_busy().await?;
242
243        self.hw.dc().set_low()?;
244        spi.write(&[command.register()]).await?;
245
246        if !data.is_empty() {
247            self.hw.dc().set_high()?;
248            spi.write(data).await?;
249        }
250
251        Ok(())
252    }
253
254    /// Waits for the current operation to complete if the display is busy.
255    ///
256    /// Note that this will wait forever if the display is asleep.
257    async fn wait_if_busy(&mut self) -> Result<(), HW::Error> {
258        let busy = self.hw.busy();
259        // Note: the datasheet states that busy pin is active low, i.e. we should wait for it when
260        // it's low, but this is incorrect. The sample code treats it as active high, which works.
261        if busy.is_high().unwrap() {
262            trace!("Waiting for busy EPD");
263            busy.wait_for_low().await?;
264        }
265        Ok(())
266    }
267}
268
269impl<HW> Epd<HW> for Epd2In9<HW>
270where
271    HW: EpdHw,
272{
273    type RefreshMode = RefreshMode;
274    type Buffer = Epd2In9Buffer;
275
276    fn new_buffer(&self) -> Self::Buffer {
277        BinaryBuffer::new(Size::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32))
278    }
279
280    fn width(&self) -> u32 {
281        DISPLAY_WIDTH as u32
282    }
283
284    fn height(&self) -> u32 {
285        DISPLAY_HEIGHT as u32
286    }
287
288    async fn init(&mut self, spi: &mut HW::Spi, mode: RefreshMode) -> Result<(), HW::Error> {
289        debug!("Initialising display");
290        self.reset().await?;
291
292        // Reset all configurations to default.
293        self.send(spi, Command::SwReset, &[]).await?;
294
295        self.send(spi, Command::DriverOutputControl, &DRIVER_OUTPUT_INIT_DATA)
296            .await?;
297        self.send(
298            spi,
299            Command::BoosterSoftStartControl,
300            &BOOSTER_SOFT_START_INIT_DATA,
301        )
302        .await?;
303        // Auto-increment X and Y, moving in the X direction first.
304        self.send(spi, Command::DataEntryModeSetting, &[0b11])
305            .await?;
306
307        // Apply more magical config settings from the sample code.
308        // Potentially: configure VCOM for 7 degrees celsius?
309        self.send(spi, Command::WriteVcom, &[0xA8]).await?;
310        // Configure 4 dummy lines per gate.
311        self.send(spi, Command::SetDummyLinePeriod, &[0x1A]).await?;
312        // 2us per line.
313        self.send(spi, Command::SetGateLineWidth, &[0x08]).await?;
314
315        self.set_refresh_mode(spi, mode).await
316    }
317
318    async fn set_refresh_mode(
319        &mut self,
320        spi: &mut <HW as EpdHw>::Spi,
321        mode: Self::RefreshMode,
322    ) -> Result<(), <HW as EpdHw>::Error> {
323        // Update the LUT only if needed.
324        match self.refresh_mode {
325            Some(old_mode) if old_mode == mode => return Ok(()),
326            Some(old_mode) if old_mode.lut() != mode.lut() => {
327                self.send(spi, Command::WriteLut, mode.lut()).await?;
328            }
329            None => {
330                self.send(spi, Command::WriteLut, mode.lut()).await?;
331            }
332            _ => {}
333        }
334
335        debug!("Changing refresh mode to {:?}", mode);
336        self.refresh_mode = Some(mode);
337
338        // Update bypass if needed.
339        match mode {
340            RefreshMode::Partial => {
341                self.send(spi, Command::DisplayUpdateControl1, &[0x00])
342                    .await
343            }
344            RefreshMode::PartialBlackBypass => {
345                self.send(spi, Command::DisplayUpdateControl1, &[0x90])
346                    .await
347            }
348            RefreshMode::PartialWhiteBypass => {
349                self.send(spi, Command::DisplayUpdateControl1, &[0x80])
350                    .await
351            }
352            _ => Ok(()),
353        }
354    }
355
356    async fn reset(&mut self) -> Result<(), HW::Error> {
357        debug!("Resetting EPD");
358        // Assume reset is already high.
359        self.hw.reset().set_low()?;
360        self.hw.delay().delay_ms(10).await;
361        self.hw.reset().set_high()?;
362        self.hw.delay().delay_ms(10).await;
363        Ok(())
364    }
365
366    async fn sleep(&mut self, spi: &mut HW::Spi) -> Result<(), <HW as EpdHw>::Error> {
367        debug!("Sleeping EPD");
368        self.send(spi, Command::DeepSleepMode, &[0x01]).await
369    }
370
371    async fn wake(&mut self, _spi: &mut HW::Spi) -> Result<(), <HW as EpdHw>::Error> {
372        debug!("Waking EPD");
373        self.reset().await
374
375        // Confirmed with a physical screen that init is not required after waking.
376    }
377
378    async fn display_buffer(
379        &mut self,
380        spi: &mut HW::Spi,
381        buffer: &Self::Buffer,
382    ) -> Result<(), <HW as EpdHw>::Error> {
383        self.write_framebuffer(spi, buffer).await?;
384
385        self.update_display(spi).await
386    }
387
388    async fn write_framebuffer(
389        &mut self,
390        spi: &mut HW::Spi,
391        buffer: &Self::Buffer,
392    ) -> Result<(), <HW as EpdHw>::Error> {
393        let buffer_bounds = buffer.bounding_box();
394        self.set_window(spi, buffer_bounds).await?;
395        self.set_cursor(spi, buffer_bounds.top_left).await?;
396        self.write_image(spi, buffer.data()).await
397    }
398
399    /// Sets the window to which the next image data will be written.
400    ///
401    /// The x-axis only supports multiples of 8; values outside this result in a debug-mode panic,
402    /// or potentially misaligned content when debug assertions are disabled.
403    async fn set_window(
404        &mut self,
405        spi: &mut HW::Spi,
406        shape: Rectangle,
407    ) -> Result<(), <HW as EpdHw>::Error> {
408        // Use a debug assert as this is a soft failure in production; it will just lead to
409        // slightly misaligned display content.
410        let x_start = shape.top_left.x;
411        let x_end = x_start + shape.size.width as i32 - 1;
412        #[cfg(feature = "defmt")]
413        defmt::debug_assert!(
414            x_start % 8 == 0 && x_end % 8 == 7,
415            "window's top_left.x and width must be 8-bit aligned"
416        );
417        #[cfg(not(feature = "defmt"))]
418        debug_assert!(
419            x_start % 8 == 0 && x_end % 8 == 7,
420            "window's top_left.x and width must be 8-bit aligned"
421        );
422        let x_start_byte = ((x_start >> 3) & 0xFF) as u8;
423        let x_end_byte = ((x_end >> 3) & 0xFF) as u8;
424        self.send(spi, Command::SetRamXStartEnd, &[x_start_byte, x_end_byte])
425            .await?;
426
427        let (y_start_low, y_start_high) = split_low_and_high(shape.top_left.y as u16);
428        let (y_end_low, y_end_high) =
429            split_low_and_high((shape.top_left.y + shape.size.height as i32 - 1) as u16);
430        self.send(
431            spi,
432            Command::SetRamYStartEnd,
433            &[y_start_low, y_start_high, y_end_low, y_end_high],
434        )
435        .await?;
436
437        Ok(())
438    }
439
440    /// Sets the cursor position to write the next data to.
441    ///
442    /// The x-axis only supports multiples of 8; values outside this will result in a panic in
443    /// debug mode, or potentially misaligned content if debug assertions are disabled.
444    async fn set_cursor(
445        &mut self,
446        spi: &mut HW::Spi,
447        position: Point,
448    ) -> Result<(), <HW as EpdHw>::Error> {
449        // Use a debug assert as this is a soft failure in production; it will just lead to
450        // slightly misaligned display content.
451        #[cfg(feature = "defmt")]
452        defmt::debug_assert_eq!(position.x % 8, 0, "position.x must be 8-bit aligned");
453        #[cfg(not(feature = "defmt"))]
454        debug_assert_eq!(position.x % 8, 0, "position.x must be 8-bit aligned");
455
456        self.send(spi, Command::SetRamX, &[(position.x >> 3) as u8])
457            .await?;
458        let (y_low, y_high) = split_low_and_high(position.y as u16);
459        self.send(spi, Command::SetRamY, &[y_low, y_high]).await?;
460        Ok(())
461    }
462
463    async fn update_display(&mut self, spi: &mut HW::Spi) -> Result<(), <HW as EpdHw>::Error> {
464        // Enable the clock and CP (?), and then display the data from the RAM. Note that there are
465        // two RAM buffers, so this will swap the active buffer. Calling this function twice in a row
466        // without writing further to RAM therefore results in displaying the previous image.
467
468        // Experimentation:
469        // * Sending just 0x04 doesn't work, it hangs in busy state. The clocks are needed.
470        // * Sending 0xC8 (INITIAL_DISPLAY) results in a black screen.
471        // * Sending 0xCD (INITIAL_DISPLAY + PATTERN_DISPLAY) results in seemingly broken, semi-random behaviour.
472        // The INIITIAL_DISPLAY settings potentially relate to the "bypass" settings in
473        // [Command::DisplayUpdateControl1], but the precise mode is unclear.
474        debug!("Updating display");
475
476        self.send(spi, Command::DisplayUpdateControl2, &[0xC4])
477            .await?;
478        self.send(spi, Command::MasterActivation, &[]).await?;
479        self.send(spi, Command::Noop, &[]).await?;
480        Ok(())
481    }
482
483    async fn write_image(
484        &mut self,
485        spi: &mut HW::Spi,
486        image: &[u8],
487    ) -> Result<(), <HW as EpdHw>::Error> {
488        self.send(spi, Command::WriteRam, image).await
489    }
490}
491
492#[inline(always)]
493/// Splits a 16-bit value into the two 8-bit values representing the low and high bytes.
494fn split_low_and_high(value: u16) -> (u8, u8) {
495    let low = (value & 0xFF) as u8;
496    let high = ((value >> 8) & 0xFF) as u8;
497    (low, high)
498}