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}