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