1use embedded_graphics::{Pixel, pixelcolor::Rgb565, prelude::*, primitives::Rectangle};
2use embedded_hal::{
3 delay::DelayNs,
4 digital::{Error as DigitalError, OutputPin},
5 spi::{Error as SpiErrorTrait, SpiDevice},
6};
7
8const COLOR_STREAM_PIXELS: usize = 128;
9const PIXEL_STREAM_PIXELS: usize = 128;
10const DRAW_ITER_PIXELS: usize = 64;
11const RGB565_BE_WRITE_CHUNK_BYTES: usize = 4096;
12
13const CMD_SOFTWARE_RESET: u8 = 0x01;
14const CMD_SLEEP_OUT: u8 = 0x11;
15const CMD_DISPLAY_INVERSION_ON: u8 = 0x21;
16const CMD_DISPLAY_ON: u8 = 0x29;
17const CMD_IDLE_MODE_OFF: u8 = 0x38;
18const CMD_COLUMN_ADDRESS_SET: u8 = 0x2A;
19const CMD_ROW_ADDRESS_SET: u8 = 0x2B;
20const CMD_MEMORY_WRITE: u8 = 0x2C;
21const CMD_MEMORY_ACCESS_CONTROL: u8 = 0x36;
22const CMD_PIXEL_FORMAT_SET: u8 = 0x3A;
23const PIXEL_FORMAT_RGB565: u8 = 0x55;
24
25const SOFTWARE_RESET_DELAY_NS: u32 = 150_000_000;
26const SLEEP_OUT_DELAY_NS: u32 = 120_000_000;
27const DISPLAY_ON_DELAY_NS: u32 = 20_000_000;
28
29#[cfg_attr(feature = "defmt", derive(defmt::Format))]
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum DisplayError<SpiError, PinError> {
32 Spi(SpiError),
33 Pin(PinError),
34 PixelData(PixelDataError),
35 Text,
36}
37
38#[cfg_attr(feature = "defmt", derive(defmt::Format))]
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum PixelDataError {
42 OddByteLength,
43 LengthMismatch { expected: usize, actual: usize },
44 LengthOverflow,
45 OutOfBounds,
46 UnsupportedOrientation,
47}
48
49#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
51pub struct DisplayTransferStats {
52 pub logical_sessions: u32,
53 pub address_windows: u32,
54 pub spi_write_operations: u32,
55 pub pixel_bytes: usize,
56}
57
58pub trait LcdTransactionWriter {
60 type Error;
61
62 fn set_command_mode(&mut self) -> Result<(), Self::Error>;
63 fn set_data_mode(&mut self) -> Result<(), Self::Error>;
64 fn write(&mut self, bytes: &[u8]) -> Result<(), Self::Error>;
65}
66
67pub trait LcdTransactionDevice: SpiDevice {
70 fn with_lcd_transaction<R, E>(
71 &mut self,
72 operation: impl FnOnce(&mut dyn LcdTransactionWriter<Error = Self::Error>) -> Result<R, E>,
73 ) -> Result<R, LcdTransactionError<E, Self::Error>>;
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77pub enum LcdTransactionError<OperationError, DeviceError> {
78 Operation(OperationError),
79 Device(DeviceError),
80}
81
82#[cfg_attr(feature = "defmt", derive(defmt::Format))]
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum DisplayOrientation {
85 Portrait,
86 PortraitInverted,
87 Landscape,
88 LandscapeInverted,
89}
90
91#[cfg_attr(feature = "defmt", derive(defmt::Format))]
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub struct DisplayGeometry {
94 pub width: u16,
95 pub height: u16,
96 pub offset_x: u16,
97 pub offset_y: u16,
98}
99
100#[cfg_attr(feature = "defmt", derive(defmt::Format))]
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub struct PanelConfig {
103 pub geometry: DisplayGeometry,
104 pub invert_colors: bool,
105}
106
107#[cfg_attr(feature = "defmt", derive(defmt::Format))]
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct BusConfig {
110 pub write_hz: u32,
111}
112
113pub struct Display<SPI, DC, SDCS> {
114 spi: SPI,
115 dc: DC,
116 sd_cs_guard: SDCS,
117 bus: BusConfig,
118 panel: PanelConfig,
119 orientation: DisplayOrientation,
120}
121
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123struct PixelRun {
124 y: i32,
125 x_start: i32,
126 x_end: i32,
127 color: Rgb565,
128}
129
130impl PixelRun {
131 const fn new(point: Point, color: Rgb565) -> Self {
132 Self {
133 y: point.y,
134 x_start: point.x,
135 x_end: point.x,
136 color,
137 }
138 }
139
140 fn try_extend(&mut self, point: Point, color: Rgb565) -> bool {
141 if self.y == point.y && self.x_end.saturating_add(1) == point.x && self.color == color {
142 self.x_end = point.x;
143 true
144 } else {
145 false
146 }
147 }
148
149 fn rectangle(self) -> Rectangle {
150 Rectangle::new(
151 Point::new(self.x_start, self.y),
152 Size::new((self.x_end - self.x_start + 1) as u32, 1),
153 )
154 }
155}
156
157struct PixelRowBuffer {
158 start: Point,
159 colors: [Rgb565; DRAW_ITER_PIXELS],
160 len: usize,
161}
162
163impl PixelRowBuffer {
164 fn new(point: Point, color: Rgb565) -> Self {
165 let mut colors = [Rgb565::BLACK; DRAW_ITER_PIXELS];
166 colors[0] = color;
167 Self {
168 start: point,
169 colors,
170 len: 1,
171 }
172 }
173
174 fn try_push(&mut self, point: Point, color: Rgb565) -> bool {
175 if self.len == DRAW_ITER_PIXELS
176 || point.y != self.start.y
177 || point.x != self.start.x.saturating_add(self.len as i32)
178 {
179 return false;
180 }
181 self.colors[self.len] = color;
182 self.len += 1;
183 true
184 }
185
186 fn area(&self) -> Rectangle {
187 Rectangle::new(self.start, Size::new(self.len as u32, 1))
188 }
189
190 fn colors(&self) -> &[Rgb565] {
191 &self.colors[..self.len]
192 }
193}
194
195fn rgb565_be_byte_len(width: usize, height: usize) -> Result<usize, PixelDataError> {
196 width
197 .checked_mul(height)
198 .and_then(|pixels| pixels.checked_mul(2))
199 .ok_or(PixelDataError::LengthOverflow)
200}
201
202fn validate_rgb565_be_len(
203 width: usize,
204 height: usize,
205 actual: usize,
206) -> Result<usize, PixelDataError> {
207 if !actual.is_multiple_of(2) {
208 return Err(PixelDataError::OddByteLength);
209 }
210 let expected = rgb565_be_byte_len(width, height)?;
211 if actual != expected {
212 return Err(PixelDataError::LengthMismatch { expected, actual });
213 }
214 Ok(expected)
215}
216
217pub struct DisplayTransaction<'a, SpiError> {
219 writer: &'a mut dyn LcdTransactionWriter<Error = SpiError>,
220 geometry: DisplayGeometry,
221 stats: DisplayTransferStats,
222}
223
224#[derive(Clone, Copy, Debug, Eq, PartialEq)]
225pub enum DisplayTransactionError<SpiError> {
226 Spi(SpiError),
227 PixelData(PixelDataError),
228}
229
230impl<SpiError> DisplayTransaction<'_, SpiError> {
231 pub fn blit_rgb565_be(
233 &mut self,
234 area: Rectangle,
235 bytes: &[u8],
236 ) -> Result<(), DisplayTransactionError<SpiError>> {
237 let bounds = Rectangle::new(
238 Point::zero(),
239 Size::new(
240 u32::from(self.geometry.width),
241 u32::from(self.geometry.height),
242 ),
243 );
244 if area.is_zero_sized() {
245 validate_rgb565_be_len(0, 0, bytes.len())
246 .map_err(DisplayTransactionError::PixelData)?;
247 return Ok(());
248 }
249 if area.intersection(&bounds) != area {
250 return Err(DisplayTransactionError::PixelData(
251 PixelDataError::OutOfBounds,
252 ));
253 }
254 validate_rgb565_be_len(
255 area.size.width as usize,
256 area.size.height as usize,
257 bytes.len(),
258 )
259 .map_err(DisplayTransactionError::PixelData)?;
260 self.set_address_window(area)
261 .map_err(DisplayTransactionError::Spi)?;
262 self.write_rgb565_be(bytes)
263 .map_err(DisplayTransactionError::Spi)
264 }
265
266 fn set_address_window(&mut self, area: Rectangle) -> Result<(), SpiError> {
267 let x0 = self.geometry.offset_x + area.top_left.x as u16;
268 let y0 = self.geometry.offset_y + area.top_left.y as u16;
269 let x1 = x0 + area.size.width.saturating_sub(1) as u16;
270 let y1 = y0 + area.size.height.saturating_sub(1) as u16;
271
272 self.command(
273 CMD_COLUMN_ADDRESS_SET,
274 &[(x0 >> 8) as u8, x0 as u8, (x1 >> 8) as u8, x1 as u8],
275 )?;
276 self.command(
277 CMD_ROW_ADDRESS_SET,
278 &[(y0 >> 8) as u8, y0 as u8, (y1 >> 8) as u8, y1 as u8],
279 )?;
280 self.command(CMD_MEMORY_WRITE, &[])?;
281 self.writer.set_data_mode()?;
282 self.stats.address_windows += 1;
283 Ok(())
284 }
285
286 fn write_rgb565_be(&mut self, bytes: &[u8]) -> Result<(), SpiError> {
288 for chunk in bytes.chunks(RGB565_BE_WRITE_CHUNK_BYTES) {
289 self.writer.write(chunk)?;
290 self.stats.spi_write_operations += 1;
291 self.stats.pixel_bytes += chunk.len();
292 }
293 Ok(())
294 }
295
296 #[must_use]
297 pub const fn stats(&self) -> DisplayTransferStats {
298 self.stats
299 }
300
301 fn command(&mut self, command: u8, data: &[u8]) -> Result<(), SpiError> {
302 self.writer.set_command_mode()?;
303 self.writer.write(&[command])?;
304 self.stats.spi_write_operations += 1;
305 if !data.is_empty() {
306 self.writer.set_data_mode()?;
307 self.writer.write(data)?;
308 self.stats.spi_write_operations += 1;
309 }
310 Ok(())
311 }
312}
313
314impl<SPI, DC, SDCS> Display<SPI, DC, SDCS> {
315 #[must_use]
316 pub const fn new(
317 spi: SPI,
318 dc: DC,
319 sd_cs_guard: SDCS,
320 bus: BusConfig,
321 panel: PanelConfig,
322 ) -> Self {
323 Self {
324 spi,
325 dc,
326 sd_cs_guard,
327 bus,
328 panel,
329 orientation: DisplayOrientation::Landscape,
330 }
331 }
332
333 #[must_use]
334 pub const fn bus_config(&self) -> BusConfig {
335 self.bus
336 }
337
338 #[must_use]
339 pub const fn panel_config(&self) -> PanelConfig {
340 self.panel
341 }
342
343 #[must_use]
344 pub const fn geometry(&self) -> DisplayGeometry {
345 self.panel.geometry
346 }
347
348 #[must_use]
349 pub const fn orientation(&self) -> DisplayOrientation {
350 self.orientation
351 }
352
353 pub fn set_orientation(&mut self, orientation: DisplayOrientation) {
354 self.orientation = orientation;
355 }
356
357 pub fn clip_rectangle(&self, area: Rectangle) -> Rectangle {
358 area.intersection(&self.bounding_box())
359 }
360
361 pub fn map_logical_rectangle_to_native(&self, area: &Rectangle) -> Rectangle {
362 self.map_rectangle_to_native(area)
363 }
364
365 pub fn release(self) -> (SPI, DC, SDCS) {
366 (self.spi, self.dc, self.sd_cs_guard)
367 }
368
369 pub fn rgb565_be_byte_len(width: usize, height: usize) -> Result<usize, PixelDataError> {
371 rgb565_be_byte_len(width, height)
372 }
373
374 pub fn validate_rgb565_be_len(
376 width: usize,
377 height: usize,
378 actual: usize,
379 ) -> Result<usize, PixelDataError> {
380 validate_rgb565_be_len(width, height, actual)
381 }
382
383 fn logical_size(&self) -> Size {
384 let geometry = self.panel.geometry;
385 match self.orientation {
386 DisplayOrientation::Portrait | DisplayOrientation::PortraitInverted => {
387 Size::new(u32::from(geometry.height), u32::from(geometry.width))
388 }
389 DisplayOrientation::Landscape | DisplayOrientation::LandscapeInverted => {
390 Size::new(u32::from(geometry.width), u32::from(geometry.height))
391 }
392 }
393 }
394
395 fn map_rectangle_to_native(&self, area: &Rectangle) -> Rectangle {
396 let geometry = self.panel.geometry;
397 let native_width = i32::from(geometry.width);
398 let native_height = i32::from(geometry.height);
399 let x = area.top_left.x;
400 let y = area.top_left.y;
401 let width = area.size.width as i32;
402 let height = area.size.height as i32;
403
404 match self.orientation {
405 DisplayOrientation::Landscape => *area,
406 DisplayOrientation::LandscapeInverted => Rectangle::new(
407 Point::new(native_width - x - width, native_height - y - height),
408 area.size,
409 ),
410 DisplayOrientation::Portrait => Rectangle::new(
411 Point::new(y, native_height - x - width),
412 Size::new(area.size.height, area.size.width),
413 ),
414 DisplayOrientation::PortraitInverted => Rectangle::new(
415 Point::new(native_width - y - height, x),
416 Size::new(area.size.height, area.size.width),
417 ),
418 }
419 }
420}
421
422impl<SPI, DC, SDCS, SpiError, PinError> Display<SPI, DC, SDCS>
423where
424 SPI: SpiDevice<Error = SpiError>,
425 DC: OutputPin<Error = PinError>,
426 SDCS: OutputPin<Error = PinError>,
427 SpiError: SpiErrorTrait,
428 PinError: DigitalError,
429{
430 pub fn init(
431 &mut self,
432 delay: &mut impl DelayNs,
433 ) -> Result<(), DisplayError<SpiError, PinError>> {
434 self.sd_cs_guard.set_high().map_err(DisplayError::Pin)?;
435 self.command(CMD_SOFTWARE_RESET, &[])?;
436 delay.delay_ns(SOFTWARE_RESET_DELAY_NS);
437 self.init_ili9342_registers()?;
438 self.command(CMD_SLEEP_OUT, &[])?;
439 delay.delay_ns(SLEEP_OUT_DELAY_NS);
440 self.command(CMD_PIXEL_FORMAT_SET, &[PIXEL_FORMAT_RGB565])?;
441 self.write_orientation()?;
442 if self.panel.invert_colors {
443 self.command(CMD_DISPLAY_INVERSION_ON, &[])?;
444 }
445 self.command(CMD_IDLE_MODE_OFF, &[])?;
446 self.command(CMD_DISPLAY_ON, &[])?;
447 delay.delay_ns(DISPLAY_ON_DELAY_NS);
448 Ok(())
449 }
450
451 fn init_ili9342_registers(&mut self) -> Result<(), DisplayError<SpiError, PinError>> {
452 self.command(0xC8, &[0xFF, 0x93, 0x42])?;
453 self.command(0xC0, &[0x12, 0x12])?;
454 self.command(0xC1, &[0x03])?;
455 self.command(0xC5, &[0xF2])?;
456 self.command(0xB0, &[0xE0])?;
457 self.command(0xF6, &[0x01, 0x00, 0x00])?;
458 self.command(
459 0xE0,
460 &[
461 0x00, 0x0C, 0x11, 0x04, 0x11, 0x08, 0x37, 0x89, 0x4C, 0x06, 0x0C, 0x0A, 0x2E, 0x34,
462 0x0F,
463 ],
464 )?;
465 self.command(
466 0xE1,
467 &[
468 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x2E, 0x33,
469 0x0F,
470 ],
471 )?;
472 self.command(0xB6, &[0x08, 0x82, 0x1D, 0x04])
473 }
474
475 pub fn set_orientation_and_apply(
476 &mut self,
477 orientation: DisplayOrientation,
478 ) -> Result<(), DisplayError<SpiError, PinError>> {
479 self.orientation = orientation;
480 self.write_orientation()
481 }
482
483 fn write_orientation(&mut self) -> Result<(), DisplayError<SpiError, PinError>> {
484 self.command(
485 CMD_MEMORY_ACCESS_CONTROL,
486 &[madctl_for_orientation(self.orientation)],
487 )
488 }
489
490 pub fn command(
491 &mut self,
492 command: u8,
493 data: &[u8],
494 ) -> Result<(), DisplayError<SpiError, PinError>> {
495 self.dc.set_low().map_err(DisplayError::Pin)?;
496 self.spi.write(&[command]).map_err(DisplayError::Spi)?;
497 if !data.is_empty() {
498 self.dc.set_high().map_err(DisplayError::Pin)?;
499 self.spi.write(data).map_err(DisplayError::Spi)?;
500 }
501 Ok(())
502 }
503
504 pub fn clear(&mut self, color: Rgb565) -> Result<(), DisplayError<SpiError, PinError>> {
505 self.fill_solid(&Rectangle::new(Point::zero(), self.logical_size()), color)
506 }
507
508 pub fn blit_pixels<I>(
509 &mut self,
510 area: &Rectangle,
511 pixels: I,
512 ) -> Result<(), DisplayError<SpiError, PinError>>
513 where
514 I: IntoIterator<Item = Rgb565>,
515 {
516 let clipped = area.intersection(&self.bounding_box());
517 if clipped.is_zero_sized() {
518 return Ok(());
519 }
520 if self.orientation == DisplayOrientation::Landscape && clipped == *area {
521 self.set_address_window(&clipped)?;
522 self.write_color_stream(
523 pixels,
524 clipped.size.width.saturating_mul(clipped.size.height) as usize,
525 )
526 } else {
527 self.fill_contiguous(area, pixels)
528 }
529 }
530
531 fn flush_run(&mut self, run: PixelRun) -> Result<(), DisplayError<SpiError, PinError>> {
532 self.fill_solid(&run.rectangle(), run.color)
533 }
534
535 fn flush_pixel_row(
536 &mut self,
537 row: &PixelRowBuffer,
538 ) -> Result<(), DisplayError<SpiError, PinError>> {
539 self.blit_pixels(&row.area(), row.colors().iter().copied())
540 }
541
542 fn set_address_window(
543 &mut self,
544 area: &Rectangle,
545 ) -> Result<(), DisplayError<SpiError, PinError>> {
546 let geometry = self.panel.geometry;
547 let x0 = geometry.offset_x + area.top_left.x.max(0) as u16;
548 let y0 = geometry.offset_y + area.top_left.y.max(0) as u16;
549 let x1 = x0 + area.size.width.saturating_sub(1) as u16;
550 let y1 = y0 + area.size.height.saturating_sub(1) as u16;
551 self.command(
552 CMD_COLUMN_ADDRESS_SET,
553 &[(x0 >> 8) as u8, x0 as u8, (x1 >> 8) as u8, x1 as u8],
554 )?;
555 self.command(
556 CMD_ROW_ADDRESS_SET,
557 &[(y0 >> 8) as u8, y0 as u8, (y1 >> 8) as u8, y1 as u8],
558 )?;
559 self.dc.set_low().map_err(DisplayError::Pin)?;
560 self.spi
561 .write(&[CMD_MEMORY_WRITE])
562 .map_err(DisplayError::Spi)?;
563 self.dc.set_high().map_err(DisplayError::Pin)
564 }
565
566 fn write_repeated_color(
567 &mut self,
568 color: Rgb565,
569 pixels: usize,
570 ) -> Result<(), DisplayError<SpiError, PinError>> {
571 let raw = color.into_storage().to_be_bytes();
572 let mut chunk = [0u8; COLOR_STREAM_PIXELS * 2];
573 let mut offset = 0;
574 while offset < chunk.len() {
575 chunk[offset] = raw[0];
576 chunk[offset + 1] = raw[1];
577 offset += 2;
578 }
579 let mut remaining = pixels;
580 while remaining > 0 {
581 let write_pixels = remaining.min(COLOR_STREAM_PIXELS);
582 let write_len = write_pixels * 2;
583 self.spi
584 .write(&chunk[..write_len])
585 .map_err(DisplayError::Spi)?;
586 remaining -= write_pixels;
587 }
588 Ok(())
589 }
590
591 fn write_color_stream<I>(
592 &mut self,
593 pixels: I,
594 max_pixels: usize,
595 ) -> Result<(), DisplayError<SpiError, PinError>>
596 where
597 I: IntoIterator<Item = Rgb565>,
598 {
599 let mut chunk = [0u8; PIXEL_STREAM_PIXELS * 2];
600 let mut buffered_pixels = 0usize;
601
602 for (written_pixels, color) in pixels.into_iter().enumerate() {
603 if written_pixels >= max_pixels {
604 break;
605 }
606 let raw = color.into_storage().to_be_bytes();
607 let offset = buffered_pixels * 2;
608 chunk[offset] = raw[0];
609 chunk[offset + 1] = raw[1];
610 buffered_pixels += 1;
611
612 if buffered_pixels == PIXEL_STREAM_PIXELS {
613 self.spi.write(&chunk).map_err(DisplayError::Spi)?;
614 buffered_pixels = 0;
615 }
616 }
617
618 if buffered_pixels > 0 {
619 self.spi
620 .write(&chunk[..buffered_pixels * 2])
621 .map_err(DisplayError::Spi)?;
622 }
623 Ok(())
624 }
625}
626
627impl<SPI, DC, SDCS, SpiError, PinError> Display<SPI, DC, SDCS>
628where
629 SPI: LcdTransactionDevice<Error = SpiError>,
630 DC: OutputPin<Error = PinError>,
631 SDCS: OutputPin<Error = PinError>,
632 SpiError: SpiErrorTrait,
633 PinError: DigitalError,
634{
635 pub fn with_lcd_transaction<R>(
641 &mut self,
642 operation: impl FnOnce(
643 &mut DisplayTransaction<'_, SpiError>,
644 ) -> Result<R, DisplayError<SpiError, PinError>>,
645 ) -> Result<R, DisplayError<SpiError, PinError>> {
646 let geometry = self.panel.geometry;
647 self.spi
648 .with_lcd_transaction(|writer| {
649 let mut transaction = DisplayTransaction {
650 writer,
651 geometry,
652 stats: DisplayTransferStats {
653 logical_sessions: 1,
654 ..DisplayTransferStats::default()
655 },
656 };
657 operation(&mut transaction)
658 })
659 .map_err(|error| match error {
660 LcdTransactionError::Operation(error) => error,
661 LcdTransactionError::Device(error) => DisplayError::Spi(error),
662 })
663 }
664
665 pub fn blit_rgb565_be(
671 &mut self,
672 area: &Rectangle,
673 bytes: &[u8],
674 ) -> Result<(), DisplayError<SpiError, PinError>> {
675 if self.orientation != DisplayOrientation::Landscape {
676 return Err(DisplayError::PixelData(
677 PixelDataError::UnsupportedOrientation,
678 ));
679 }
680 if area.is_zero_sized() {
681 Self::validate_rgb565_be_len(0, 0, bytes.len()).map_err(DisplayError::PixelData)?;
682 return Ok(());
683 }
684 if area.intersection(&self.bounding_box()) != *area {
685 return Err(DisplayError::PixelData(PixelDataError::OutOfBounds));
686 }
687 Self::validate_rgb565_be_len(
688 area.size.width as usize,
689 area.size.height as usize,
690 bytes.len(),
691 )
692 .map_err(DisplayError::PixelData)?;
693
694 self.with_lcd_transaction(|transaction| {
695 transaction
696 .blit_rgb565_be(*area, bytes)
697 .map_err(|error| match error {
698 DisplayTransactionError::Spi(error) => DisplayError::Spi(error),
699 DisplayTransactionError::PixelData(error) => DisplayError::PixelData(error),
700 })
701 })
702 }
703}
704
705impl<SPI, DC, SDCS, SpiError, PinError> DrawTarget for Display<SPI, DC, SDCS>
706where
707 SPI: SpiDevice<Error = SpiError>,
708 DC: OutputPin<Error = PinError>,
709 SDCS: OutputPin<Error = PinError>,
710 SpiError: SpiErrorTrait,
711 PinError: DigitalError,
712{
713 type Color = Rgb565;
714 type Error = DisplayError<SpiError, PinError>;
715
716 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
717 where
718 I: IntoIterator<Item = Pixel<Self::Color>>,
719 {
720 let mut row = None;
721 for Pixel(point, color) in pixels {
722 if !self.bounding_box().contains(point) {
723 if let Some(current) = row.take() {
724 self.flush_pixel_row(¤t)?;
725 }
726 continue;
727 }
728 if let Some(mut current) = row.take() {
729 if current.try_push(point, color) {
730 row = Some(current);
731 } else {
732 self.flush_pixel_row(¤t)?;
733 row = Some(PixelRowBuffer::new(point, color));
734 }
735 } else {
736 row = Some(PixelRowBuffer::new(point, color));
737 }
738 }
739 if let Some(current) = row {
740 self.flush_pixel_row(¤t)?;
741 }
742 Ok(())
743 }
744
745 fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
746 let clipped = area.intersection(&self.bounding_box());
747 if clipped.is_zero_sized() {
748 return Ok(());
749 }
750 let native = self.map_rectangle_to_native(&clipped);
751 self.set_address_window(&native)?;
752 self.write_repeated_color(
753 color,
754 clipped.size.width.saturating_mul(clipped.size.height) as usize,
755 )
756 }
757
758 fn fill_contiguous<I>(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error>
759 where
760 I: IntoIterator<Item = Self::Color>,
761 {
762 let clipped = area.intersection(&self.bounding_box());
763 if clipped.is_zero_sized() {
764 return Ok(());
765 }
766 if self.orientation == DisplayOrientation::Landscape && clipped == *area {
767 self.set_address_window(&clipped)?;
768 return self.write_color_stream(
769 colors,
770 clipped.size.width.saturating_mul(clipped.size.height) as usize,
771 );
772 }
773
774 let area_width = area.size.width as i32;
775 let mut run = None;
776 for (index, color) in colors.into_iter().enumerate() {
777 let index = index as i32;
778 let point = Point::new(
779 area.top_left.x + index % area_width,
780 area.top_left.y + index / area_width,
781 );
782 if !clipped.contains(point) {
783 if let Some(current) = run.take() {
784 self.flush_run(current)?;
785 }
786 continue;
787 }
788 if let Some(mut current) = run.take() {
789 if current.try_extend(point, color) {
790 run = Some(current);
791 } else {
792 self.flush_run(current)?;
793 run = Some(PixelRun::new(point, color));
794 }
795 } else {
796 run = Some(PixelRun::new(point, color));
797 }
798 }
799 if let Some(current) = run {
800 self.flush_run(current)?;
801 }
802 Ok(())
803 }
804}
805
806const fn madctl_for_orientation(orientation: DisplayOrientation) -> u8 {
807 match orientation {
808 DisplayOrientation::Landscape => 0x08,
809 DisplayOrientation::LandscapeInverted => 0xC8,
810 DisplayOrientation::Portrait => 0x68,
811 DisplayOrientation::PortraitInverted => 0xA8,
812 }
813}
814
815impl<SPI, DC, SDCS> OriginDimensions for Display<SPI, DC, SDCS> {
816 fn size(&self) -> Size {
817 self.logical_size()
818 }
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824 use core::convert::Infallible;
825 use embedded_hal::{
826 digital::{ErrorType, OutputPin},
827 spi::{ErrorType as SpiErrorType, Operation},
828 };
829
830 #[derive(Default)]
831 struct RecordingSpi {
832 writes: std::vec::Vec<std::vec::Vec<u8>>,
833 command_mode: bool,
834 sessions: u32,
835 }
836
837 impl SpiErrorType for RecordingSpi {
838 type Error = Infallible;
839 }
840
841 impl SpiDevice for RecordingSpi {
842 fn transaction(&mut self, operations: &mut [Operation<'_, u8>]) -> Result<(), Self::Error> {
843 for operation in operations {
844 if let Operation::Write(bytes) = operation {
845 self.writes.push(bytes.to_vec());
846 }
847 }
848 Ok(())
849 }
850 }
851
852 impl LcdTransactionWriter for RecordingSpi {
853 type Error = Infallible;
854
855 fn set_command_mode(&mut self) -> Result<(), Self::Error> {
856 self.command_mode = true;
857 Ok(())
858 }
859
860 fn set_data_mode(&mut self) -> Result<(), Self::Error> {
861 self.command_mode = false;
862 Ok(())
863 }
864
865 fn write(&mut self, bytes: &[u8]) -> Result<(), Self::Error> {
866 self.writes.push(bytes.to_vec());
867 Ok(())
868 }
869 }
870
871 impl LcdTransactionDevice for RecordingSpi {
872 fn with_lcd_transaction<R, E>(
873 &mut self,
874 operation: impl FnOnce(&mut dyn LcdTransactionWriter<Error = Self::Error>) -> Result<R, E>,
875 ) -> Result<R, LcdTransactionError<E, Self::Error>> {
876 self.sessions += 1;
877 operation(self).map_err(LcdTransactionError::Operation)
878 }
879 }
880
881 #[derive(Default)]
882 struct DummyPin;
883
884 impl ErrorType for DummyPin {
885 type Error = Infallible;
886 }
887
888 impl OutputPin for DummyPin {
889 fn set_low(&mut self) -> Result<(), Self::Error> {
890 Ok(())
891 }
892
893 fn set_high(&mut self) -> Result<(), Self::Error> {
894 Ok(())
895 }
896 }
897
898 fn display(width: u16, height: u16) -> Display<RecordingSpi, DummyPin, DummyPin> {
899 Display::new(
900 RecordingSpi::default(),
901 DummyPin,
902 DummyPin,
903 BusConfig {
904 write_hz: 40_000_000,
905 },
906 PanelConfig {
907 geometry: DisplayGeometry {
908 width,
909 height,
910 offset_x: 0,
911 offset_y: 0,
912 },
913 invert_colors: true,
914 },
915 )
916 }
917
918 #[test]
919 fn rgb565_length_validation_rejects_malformed_data() {
920 type TestDisplay = Display<RecordingSpi, DummyPin, DummyPin>;
921 assert_eq!(TestDisplay::validate_rgb565_be_len(2, 2, 8), Ok(8));
922 assert_eq!(
923 TestDisplay::validate_rgb565_be_len(2, 2, 7),
924 Err(PixelDataError::OddByteLength)
925 );
926 assert_eq!(
927 TestDisplay::validate_rgb565_be_len(2, 2, 6),
928 Err(PixelDataError::LengthMismatch {
929 expected: 8,
930 actual: 6
931 })
932 );
933 assert_eq!(
934 TestDisplay::validate_rgb565_be_len(2, 2, 10),
935 Err(PixelDataError::LengthMismatch {
936 expected: 8,
937 actual: 10
938 })
939 );
940 assert_eq!(
941 TestDisplay::rgb565_be_byte_len(usize::MAX, 2),
942 Err(PixelDataError::LengthOverflow)
943 );
944 assert_eq!(TestDisplay::validate_rgb565_be_len(0, 0, 0), Ok(0));
945 }
946
947 #[test]
948 fn invalid_rgb565_blits_do_not_start_a_session() {
949 let mut display = display(4, 4);
950 let area = Rectangle::new(Point::new(3, 3), Size::new(2, 2));
951 assert_eq!(
952 display.blit_rgb565_be(&area, &[0; 8]),
953 Err(DisplayError::PixelData(PixelDataError::OutOfBounds))
954 );
955 let (spi, _, _) = display.release();
956 assert_eq!(spi.sessions, 0);
957 }
958
959 #[test]
960 fn rgb565_blit_uses_one_session_and_panel_ready_bytes() {
961 let mut display = display(4, 4);
962 let area = Rectangle::new(Point::new(1, 2), Size::new(2, 1));
963 display
964 .blit_rgb565_be(&area, &[0xF8, 0x00, 0x07, 0xE0])
965 .unwrap();
966
967 let (spi, _, _) = display.release();
968 assert_eq!(spi.sessions, 1);
969 assert_eq!(
970 spi.writes,
971 std::vec![
972 std::vec![CMD_COLUMN_ADDRESS_SET],
973 std::vec![0, 1, 0, 2],
974 std::vec![CMD_ROW_ADDRESS_SET],
975 std::vec![0, 2, 0, 2],
976 std::vec![CMD_MEMORY_WRITE],
977 std::vec![0xF8, 0x00, 0x07, 0xE0],
978 ]
979 );
980 }
981
982 #[test]
983 fn rgb565_blit_chunks_without_copying_or_extra_sessions() {
984 let bytes = std::vec![0x5A; RGB565_BE_WRITE_CHUNK_BYTES + 2];
985 let mut display = display(2049, 1);
986 let area = Rectangle::new(Point::zero(), Size::new(2049, 1));
987 display.blit_rgb565_be(&area, &bytes).unwrap();
988
989 let (spi, _, _) = display.release();
990 assert_eq!(spi.sessions, 1);
991 assert_eq!(spi.writes[5].len(), RGB565_BE_WRITE_CHUNK_BYTES);
992 assert_eq!(spi.writes[6], std::vec![0x5A; 2]);
993 }
994
995 #[test]
996 fn transaction_reports_reusable_transfer_statistics() {
997 let mut display = display(2, 1);
998 let stats = display
999 .with_lcd_transaction(|transaction| {
1000 transaction
1001 .blit_rgb565_be(
1002 Rectangle::new(Point::zero(), Size::new(2, 1)),
1003 &[0, 1, 2, 3],
1004 )
1005 .unwrap();
1006 Ok(transaction.stats())
1007 })
1008 .unwrap();
1009
1010 assert_eq!(stats.logical_sessions, 1);
1011 assert_eq!(stats.address_windows, 1);
1012 assert_eq!(stats.spi_write_operations, 6);
1013 assert_eq!(stats.pixel_bytes, 4);
1014 }
1015}