Skip to main content

core_s3/display/
panel.rs

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/// Validation error for caller-owned panel-ready pixel data.
39#[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/// Transfer counters for one scoped LCD session.
50#[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
58/// Low-level writer available only while a device-owned LCD session is active.
59pub 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
67/// Optional capability for devices that can keep LCD CS and bus ownership
68/// active across multiple command/data writes.
69pub 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
217/// Scoped high-level writer used by [`Display::with_lcd_transaction`].
218pub 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    /// Validates and streams one landscape panel region in big-endian RGB565.
232    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    /// Streams panel-ready big-endian RGB565 bytes without conversion or copying.
287    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    /// Returns the exact byte length for a tightly packed RGB565 frame.
370    pub fn rgb565_be_byte_len(width: usize, height: usize) -> Result<usize, PixelDataError> {
371        rgb565_be_byte_len(width, height)
372    }
373
374    /// Validates a tightly packed big-endian RGB565 byte slice.
375    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    /// Runs several LCD command/data writes in one device-owned bus session.
636    ///
637    /// CoreS3's shared LCD device keeps LCD CS asserted and GPIO35 in D/C mode
638    /// for this closure, then restores both chip selects high, GPIO35 to SD MISO,
639    /// and SPI2 to SD-safe full-duplex mode before returning.
640    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    /// Zero-copy blit of tightly packed, big-endian RGB565 bytes.
666    ///
667    /// `area` must be fully inside the landscape display bounds. The byte slice
668    /// must contain exactly two bytes per pixel in row-major, big-endian RGB565
669    /// order. Validation completes before the shared LCD transaction begins.
670    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(&current)?;
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(&current)?;
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(&current)?;
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}