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::digital::{InputPin, OutputPin};
8use embedded_hal_async::{delay::DelayNs, digital::Wait, spi::SpiDevice};
9
10use crate::{
11    buffer::{binary_buffer_length, BinaryBuffer},
12    log::{debug, trace},
13    Epd, EpdHw,
14};
15
16/// LUT for a full refresh. This should be used occasionally for best display results.
17///
18/// See [RECOMMENDED_MIN_FULL_REFRESH_INTERVAL] and [RECOMMENDED_MAX_FULL_REFRESH_INTERVAL].
19const LUT_FULL_UPDATE: [u8; 30] = [
20    0x50, 0xAA, 0x55, 0xAA, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
21    0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
22];
23/// LUT for a partial refresh. This should be used for frequent updates, but it's recommended to
24/// perform a full refresh occasionally.
25///
26/// See [RECOMMENDED_MIN_FULL_REFRESH_INTERVAL] and [RECOMMENDED_MAX_FULL_REFRESH_INTERVAL].
27const LUT_PARTIAL_UPDATE: [u8; 30] = [
28    0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
29    0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
30];
31
32#[cfg_attr(feature = "defmt", derive(defmt::Format))]
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34/// The refresh mode for the display.
35pub enum RefreshMode {
36    /// Use the full update LUT. This is slower, but should be done occasionally to avoid ghosting.
37    ///
38    /// It's recommended to avoid full refreshes less than [RECOMMENDED_MIN_FULL_REFRESH_INTERVAL] apart,
39    /// but to do a full refresh at least every [RECOMMENDED_MAX_FULL_REFRESH_INTERVAL].
40    Full,
41    /// Use the partial update LUT for fast refresh. A full refresh should be done occasionally to
42    /// avoid ghosting, see [RECOMMENDED_MAX_FULL_REFRESH_INTERVAL].
43    Partial,
44}
45
46impl RefreshMode {
47    /// Returns the LUT to use for this refresh mode.
48    pub fn lut(&self) -> &[u8; 30] {
49        match self {
50            RefreshMode::Full => &LUT_FULL_UPDATE,
51            RefreshMode::Partial => &LUT_PARTIAL_UPDATE,
52        }
53    }
54}
55
56/// The height of the display (portrait orientation).
57pub const DISPLAY_HEIGHT: u16 = 296;
58/// The width of the display (portrait orientation).
59pub const DISPLAY_WIDTH: u16 = 128;
60/// It's recommended to avoid doing a full refresh more often than this (at least on a regular basis).
61pub const RECOMMENDED_MIN_FULL_REFRESH_INTERVAL: Duration = Duration::from_secs(180);
62/// It's recommended to do a full refresh at least this often.
63pub const RECOMMENDED_MAX_FULL_REFRESH_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
64
65#[cfg_attr(feature = "defmt", derive(defmt::Format))]
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Command {
68    /// Used to initialise the display.
69    DriverOutputControl = 0x01,
70    /// Used to configure the on chip voltage booster and regulator.
71    BoosterSoftStartControl = 0x0C,
72    /// Used to enter deep sleep mode. Requires a hardware reset and reinitialisation to wake up.
73    DeepSleepMode = 0x10,
74    /// Changes the auto-increment behaviour of the address counter.
75    DataEntryModeSetting = 0x11,
76    /// Resets all commands and parameters to default values (except deep sleep mode).
77    SwReset = 0x12,
78    /// Writes to the temperature register.
79    TemperatureSensorControl = 0x1A,
80    /// Activates the display update sequence. This must be set beforehand using [DisplayUpdateControl2].
81    /// This operation must not be interrupted.
82    MasterActivation = 0x20,
83    /// Can be used to bypass the RAM content and directly read 1 or 0 for all pixels.
84    DisplayUpdateControl1 = 0x21,
85    /// Configures the display update sequence for use with [MasterActivation].
86    DisplayUpdateControl2 = 0x22,
87    /// Writes data to RAM, autoincrementing the address counter.
88    WriteRam = 0x24,
89    /// Writes to the VCOM register.
90    WriteVcom = 0x2C,
91    /// Writes the LUT register (30 bytes, exclude the VSH/VSL and dummy bits).
92    WriteLut = 0x32,
93    /// ? Part of magic config.
94    SetDummyLinePeriod = 0x3A,
95    /// ? Part of magic config.
96    SetGateLineWidth = 0x3B,
97    /// Register to configure the behaviour of the border.
98    /// This can be set directly to a fixed colour, or managed via the VCOM register.
99    BorderWaveformControl = 0x3C,
100    /// Sets the start and end positions of the X axis.
101    SetRamXStartEnd = 0x44,
102    /// Sets the start and end positions of the Y axis.
103    SetRamYStartEnd = 0x45,
104    /// Sets the current x coordinate of the address counter.
105    SetRamX = 0x4E,
106    /// Sets the current y coordinate of the address counter.
107    SetRamY = 0x4F,
108    /// Does nothing, but can be used to terminate other commands such as [WriteRam]
109    Noop = 0xFF,
110}
111
112impl Command {
113    fn register(&self) -> u8 {
114        *self as u8
115    }
116}
117
118/// This should be sent with [Command::DriverOutputControl] during initialisation.
119///
120/// From the sample code, the bytes mean the following:
121///
122/// * low byte of display long edge
123/// * high byte of display long edge
124/// * GD = 0, SM = 0, TB = 0 (unclear what this means)
125const DRIVER_OUTPUT_INIT_DATA: [u8; 3] = [0x27, 0x01, 0x00];
126/// This should be sent with [Command::BoosterSoftStartControl] during initialisation.
127/// Note that there are two versions of this command, one in the datasheet, and one in the sample code.
128const BOOSTER_SOFT_START_INIT_DATA: [u8; 3] = [0xD7, 0xD6, 0x9D];
129// Sample code: ^
130// Datasheet:
131// const BOOSTER_SOFT_START_INIT_DATA: [u8; 3] = [0xCF, 0xCE, 0x8D];
132
133/// Controls v1 of the 2.9" Waveshare e-paper display.
134///
135/// * [datasheet](https://files.waveshare.com/upload/e/e6/2.9inch_e-Paper_Datasheet.pdf)
136/// * [sample code](https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in9.py)
137///
138/// The display has a portrait orientation. This uses [BinaryColor], where `Off` is black and `On` is white.
139pub struct Epd2in9<HW>
140where
141    HW: EpdHw,
142{
143    hw: HW,
144}
145
146impl<HW> Epd2in9<HW>
147where
148    HW: EpdHw,
149{
150    pub fn new(hw: HW) -> Self {
151        Epd2in9 { hw }
152    }
153
154    /// Sets the border to the specified colour. You need to call [Epd::update_display] using
155    /// [RefreshMode::Full] afterwards to apply this change.
156    pub async fn set_border(
157        &mut self,
158        spi: &mut HW::Spi,
159        color: BinaryColor,
160    ) -> Result<(), HW::Error> {
161        let border_setting: u8 = match color {
162            BinaryColor::Off => 0x00,
163            BinaryColor::On => 0x01,
164        };
165        self.send(spi, Command::BorderWaveformControl, &[border_setting])
166            .await
167    }
168}
169
170impl<HW> Epd<HW> for Epd2in9<HW>
171where
172    HW: EpdHw,
173{
174    type Command = Command;
175    type RefreshMode = RefreshMode;
176    type Buffer = BinaryBuffer<
177        { binary_buffer_length(Size::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32)) },
178    >;
179
180    fn new_buffer(&self) -> Self::Buffer {
181        BinaryBuffer::new(Size::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32))
182    }
183
184    fn width(&self) -> u32 {
185        DISPLAY_WIDTH as u32
186    }
187
188    fn height(&self) -> u32 {
189        DISPLAY_HEIGHT as u32
190    }
191
192    async fn init(&mut self, spi: &mut HW::Spi, mode: RefreshMode) -> Result<(), HW::Error> {
193        // Ensure reset is high before toggling it low.
194        self.reset().await?;
195
196        // Reset all configurations to default.
197        self.send(spi, Command::SwReset, &[]).await?;
198
199        self.send(spi, Command::DriverOutputControl, &DRIVER_OUTPUT_INIT_DATA)
200            .await?;
201        self.send(
202            spi,
203            Command::BoosterSoftStartControl,
204            &BOOSTER_SOFT_START_INIT_DATA,
205        )
206        .await?;
207        // Auto-increment X and Y, moving in the X direction first.
208        self.send(spi, Command::DataEntryModeSetting, &[0b11])
209            .await?;
210
211        // Apply more magical config settings from the sample code.
212        // Potentially: configure VCOM for 7 degrees celsius?
213        self.send(spi, Command::WriteVcom, &[0xA8]).await?;
214        // Configure 4 dummy lines per gate.
215        self.send(spi, Command::SetDummyLinePeriod, &[0x1A]).await?;
216        // 2us per line.
217        self.send(spi, Command::SetGateLineWidth, &[0x08]).await?;
218
219        self.send(spi, Command::WriteLut, mode.lut()).await?;
220
221        Ok(())
222    }
223
224    async fn set_refresh_mode(
225        &mut self,
226        spi: &mut <HW as EpdHw>::Spi,
227        mode: Self::RefreshMode,
228    ) -> Result<(), <HW as EpdHw>::Error> {
229        debug!("Changing refresh mode to {:?}", mode);
230        self.send(spi, Command::WriteLut, mode.lut()).await
231    }
232
233    async fn reset(&mut self) -> Result<(), HW::Error> {
234        debug!("Resetting EPD");
235        // Assume reset is already high.
236        self.hw.reset().set_low()?;
237        self.hw.delay().delay_ms(10).await;
238        self.hw.reset().set_high()?;
239        self.hw.delay().delay_ms(10).await;
240        Ok(())
241    }
242
243    async fn sleep(&mut self, spi: &mut HW::Spi) -> Result<(), <HW as EpdHw>::Error> {
244        debug!("Sleeping EPD");
245        self.send(spi, Command::DeepSleepMode, &[0x01]).await
246    }
247
248    async fn wake(&mut self, _spi: &mut HW::Spi) -> Result<(), <HW as EpdHw>::Error> {
249        debug!("Waking EPD");
250        self.reset().await
251
252        // Confirmed with a physical screen that init is not required after waking.
253    }
254
255    async fn display_buffer(
256        &mut self,
257        spi: &mut HW::Spi,
258        buffer: &Self::Buffer,
259    ) -> Result<(), <HW as EpdHw>::Error> {
260        debug!("Displaying buffer");
261        let buffer_bounds = buffer.bounding_box();
262        self.set_window(spi, buffer_bounds).await?;
263        self.set_cursor(spi, buffer_bounds.top_left).await?;
264        self.write_image(spi, buffer.data()).await?;
265
266        self.update_display(spi).await?;
267
268        Ok(())
269    }
270
271    /// Sets the window to which the next image data will be written.
272    ///
273    /// The x-axis only supports multiples of 8; values outside this result in an [Error::InvalidArgument] error.
274    async fn set_window(
275        &mut self,
276        spi: &mut HW::Spi,
277        shape: Rectangle,
278    ) -> Result<(), <HW as EpdHw>::Error> {
279        // Use a debug assert as this is a soft failure in production; it will just lead to
280        // slightly misaligned display content.
281        let x_start = shape.top_left.x;
282        let x_end = x_start + shape.size.width as i32 - 1;
283        debug_assert!(x_start % 8 == 0 && x_end % 8 == 7, "window's top_left.x and width must be 8-bit aligned");
284        let x_start_byte = ((x_start >> 3) & 0xFF) as u8;
285        let x_end_byte = ((x_end >> 3) & 0xFF) as u8;
286        self.send(spi, Command::SetRamXStartEnd, &[x_start_byte, x_end_byte])
287            .await?;
288
289        let (y_start_low, y_start_high) = split_low_and_high(shape.top_left.y as u16);
290        let (y_end_low, y_end_high) =
291            split_low_and_high((shape.top_left.y + shape.size.height as i32 - 1) as u16);
292        self.send(
293            spi,
294            Command::SetRamYStartEnd,
295            &[y_start_low, y_start_high, y_end_low, y_end_high],
296        )
297        .await?;
298
299        Ok(())
300    }
301
302    /// Sets the cursor position to write the next data to.
303    ///
304    /// The x-axis only supports multiples of 8; values outside this will result in [Error::InvalidArgument].
305    async fn set_cursor(
306        &mut self,
307        spi: &mut HW::Spi,
308        position: Point,
309    ) -> Result<(), <HW as EpdHw>::Error> {
310        // Use a debug assert as this is a soft failure in production; it will just lead to
311        // slightly misaligned display content.
312        debug_assert_eq!(position.x % 8, 0, "position.x must be 8-bit aligned");
313        self.send(spi, Command::SetRamX, &[(position.x >> 3) as u8])
314            .await?;
315        let (y_low, y_high) = split_low_and_high(position.y as u16);
316        self.send(spi, Command::SetRamY, &[y_low, y_high]).await?;
317        Ok(())
318    }
319
320    async fn update_display(&mut self, spi: &mut HW::Spi) -> Result<(), <HW as EpdHw>::Error> {
321        // Enable the clock and CP (?), and then display the data from the RAM. Note that there are
322        // two RAM buffers, so this will swap the active buffer. Calling this function twice in a row
323        // without writing further to RAM therefore results in displaying the previous image.
324
325        // Experimentation:
326        // * Sending just 0x04 doesn't work, it hangs in busy state. The clocks are needed.
327        // * Sending 0xC8 (INITIAL_DISPLAY) results in a black screen.
328        // * Sending 0xCD (INITIAL_DISPLAY + PATTERN_DISPLAY) results in seemingly broken, semi-random behaviour.
329        // The INIITIAL_DISPLAY settings potentially relate to the "bypass" settings in
330        // [Command::DisplayUpdateControl1], but the precise mode is unclear.
331
332        self.send(spi, Command::DisplayUpdateControl2, &[0xC4])
333            .await?;
334        self.send(spi, Command::MasterActivation, &[]).await?;
335        self.send(spi, Command::Noop, &[]).await?;
336        Ok(())
337    }
338
339    async fn write_image(
340        &mut self,
341        spi: &mut HW::Spi,
342        image: &[u8],
343    ) -> Result<(), <HW as EpdHw>::Error> {
344        self.send(spi, Command::WriteRam, image).await
345    }
346
347    async fn send(
348        &mut self,
349        spi: &mut HW::Spi,
350        command: Command,
351        data: &[u8],
352    ) -> Result<(), HW::Error> {
353        trace!("Sending EPD command: {:?}", command);
354        self.wait_if_busy().await?;
355
356        self.hw.dc().set_low()?;
357        spi.write(&[command.register()]).await?;
358
359        if !data.is_empty() {
360            self.hw.dc().set_high()?;
361            spi.write(data).await?;
362        }
363
364        Ok(())
365    }
366
367    async fn wait_if_busy(&mut self) -> Result<(), HW::Error> {
368        let busy = self.hw.busy();
369        // Note: the datasheet states that busy pin is active low, i.e. we should wait for it when
370        // it's low, but this is incorrect. The sample code treats it as active high, which works.
371        if busy.is_high().unwrap() {
372            trace!("Waiting for busy EPD");
373            busy.wait_for_low().await?;
374        }
375        Ok(())
376    }
377}
378
379#[inline(always)]
380/// Splits a 16-bit value into the two 8-bit values representing the low and high bytes.
381fn split_low_and_high(value: u16) -> (u8, u8) {
382    let low = (value & 0xFF) as u8;
383    let high = ((value >> 8) & 0xFF) as u8;
384    (low, high)
385}