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;
11
12const CMD_SOFTWARE_RESET: u8 = 0x01;
13const CMD_SLEEP_OUT: u8 = 0x11;
14const CMD_DISPLAY_INVERSION_ON: u8 = 0x21;
15const CMD_DISPLAY_ON: u8 = 0x29;
16const CMD_IDLE_MODE_OFF: u8 = 0x38;
17const CMD_COLUMN_ADDRESS_SET: u8 = 0x2A;
18const CMD_ROW_ADDRESS_SET: u8 = 0x2B;
19const CMD_MEMORY_WRITE: u8 = 0x2C;
20const CMD_MEMORY_ACCESS_CONTROL: u8 = 0x36;
21const CMD_PIXEL_FORMAT_SET: u8 = 0x3A;
22const PIXEL_FORMAT_RGB565: u8 = 0x55;
23
24const SOFTWARE_RESET_DELAY_NS: u32 = 150_000_000;
25const SLEEP_OUT_DELAY_NS: u32 = 120_000_000;
26const DISPLAY_ON_DELAY_NS: u32 = 20_000_000;
27
28#[cfg_attr(feature = "defmt", derive(defmt::Format))]
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum DisplayError<SpiError, PinError> {
31    Spi(SpiError),
32    Pin(PinError),
33    Text,
34}
35
36#[cfg_attr(feature = "defmt", derive(defmt::Format))]
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum DisplayOrientation {
39    Portrait,
40    PortraitInverted,
41    Landscape,
42    LandscapeInverted,
43}
44
45#[cfg_attr(feature = "defmt", derive(defmt::Format))]
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct DisplayGeometry {
48    pub width: u16,
49    pub height: u16,
50    pub offset_x: u16,
51    pub offset_y: u16,
52}
53
54#[cfg_attr(feature = "defmt", derive(defmt::Format))]
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub struct PanelConfig {
57    pub geometry: DisplayGeometry,
58    pub invert_colors: bool,
59}
60
61#[cfg_attr(feature = "defmt", derive(defmt::Format))]
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub struct BusConfig {
64    pub write_hz: u32,
65}
66
67pub struct Display<SPI, DC, SDCS> {
68    spi: SPI,
69    dc: DC,
70    sd_cs_guard: SDCS,
71    bus: BusConfig,
72    panel: PanelConfig,
73    orientation: DisplayOrientation,
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77struct PixelRun {
78    y: i32,
79    x_start: i32,
80    x_end: i32,
81    color: Rgb565,
82}
83
84impl PixelRun {
85    const fn new(point: Point, color: Rgb565) -> Self {
86        Self {
87            y: point.y,
88            x_start: point.x,
89            x_end: point.x,
90            color,
91        }
92    }
93
94    fn try_extend(&mut self, point: Point, color: Rgb565) -> bool {
95        if self.y == point.y && self.x_end.saturating_add(1) == point.x && self.color == color {
96            self.x_end = point.x;
97            true
98        } else {
99            false
100        }
101    }
102
103    fn rectangle(self) -> Rectangle {
104        Rectangle::new(
105            Point::new(self.x_start, self.y),
106            Size::new((self.x_end - self.x_start + 1) as u32, 1),
107        )
108    }
109}
110
111struct PixelRowBuffer {
112    start: Point,
113    colors: [Rgb565; DRAW_ITER_PIXELS],
114    len: usize,
115}
116
117impl PixelRowBuffer {
118    fn new(point: Point, color: Rgb565) -> Self {
119        let mut colors = [Rgb565::BLACK; DRAW_ITER_PIXELS];
120        colors[0] = color;
121        Self {
122            start: point,
123            colors,
124            len: 1,
125        }
126    }
127
128    fn try_push(&mut self, point: Point, color: Rgb565) -> bool {
129        if self.len == DRAW_ITER_PIXELS
130            || point.y != self.start.y
131            || point.x != self.start.x.saturating_add(self.len as i32)
132        {
133            return false;
134        }
135        self.colors[self.len] = color;
136        self.len += 1;
137        true
138    }
139
140    fn area(&self) -> Rectangle {
141        Rectangle::new(self.start, Size::new(self.len as u32, 1))
142    }
143
144    fn colors(&self) -> &[Rgb565] {
145        &self.colors[..self.len]
146    }
147}
148
149impl<SPI, DC, SDCS> Display<SPI, DC, SDCS> {
150    #[must_use]
151    pub const fn new(
152        spi: SPI,
153        dc: DC,
154        sd_cs_guard: SDCS,
155        bus: BusConfig,
156        panel: PanelConfig,
157    ) -> Self {
158        Self {
159            spi,
160            dc,
161            sd_cs_guard,
162            bus,
163            panel,
164            orientation: DisplayOrientation::Landscape,
165        }
166    }
167
168    #[must_use]
169    pub const fn bus_config(&self) -> BusConfig {
170        self.bus
171    }
172
173    #[must_use]
174    pub const fn panel_config(&self) -> PanelConfig {
175        self.panel
176    }
177
178    #[must_use]
179    pub const fn geometry(&self) -> DisplayGeometry {
180        self.panel.geometry
181    }
182
183    #[must_use]
184    pub const fn orientation(&self) -> DisplayOrientation {
185        self.orientation
186    }
187
188    pub fn set_orientation(&mut self, orientation: DisplayOrientation) {
189        self.orientation = orientation;
190    }
191
192    pub fn clip_rectangle(&self, area: Rectangle) -> Rectangle {
193        area.intersection(&self.bounding_box())
194    }
195
196    pub fn map_logical_rectangle_to_native(&self, area: &Rectangle) -> Rectangle {
197        self.map_rectangle_to_native(area)
198    }
199
200    pub fn release(self) -> (SPI, DC, SDCS) {
201        (self.spi, self.dc, self.sd_cs_guard)
202    }
203
204    fn logical_size(&self) -> Size {
205        let geometry = self.panel.geometry;
206        match self.orientation {
207            DisplayOrientation::Portrait | DisplayOrientation::PortraitInverted => {
208                Size::new(u32::from(geometry.height), u32::from(geometry.width))
209            }
210            DisplayOrientation::Landscape | DisplayOrientation::LandscapeInverted => {
211                Size::new(u32::from(geometry.width), u32::from(geometry.height))
212            }
213        }
214    }
215
216    fn map_rectangle_to_native(&self, area: &Rectangle) -> Rectangle {
217        let geometry = self.panel.geometry;
218        let native_width = i32::from(geometry.width);
219        let native_height = i32::from(geometry.height);
220        let x = area.top_left.x;
221        let y = area.top_left.y;
222        let width = area.size.width as i32;
223        let height = area.size.height as i32;
224
225        match self.orientation {
226            DisplayOrientation::Landscape => *area,
227            DisplayOrientation::LandscapeInverted => Rectangle::new(
228                Point::new(native_width - x - width, native_height - y - height),
229                area.size,
230            ),
231            DisplayOrientation::Portrait => Rectangle::new(
232                Point::new(y, native_height - x - width),
233                Size::new(area.size.height, area.size.width),
234            ),
235            DisplayOrientation::PortraitInverted => Rectangle::new(
236                Point::new(native_width - y - height, x),
237                Size::new(area.size.height, area.size.width),
238            ),
239        }
240    }
241}
242
243impl<SPI, DC, SDCS, SpiError, PinError> Display<SPI, DC, SDCS>
244where
245    SPI: SpiDevice<Error = SpiError>,
246    DC: OutputPin<Error = PinError>,
247    SDCS: OutputPin<Error = PinError>,
248    SpiError: SpiErrorTrait,
249    PinError: DigitalError,
250{
251    pub fn init(
252        &mut self,
253        delay: &mut impl DelayNs,
254    ) -> Result<(), DisplayError<SpiError, PinError>> {
255        self.sd_cs_guard.set_high().map_err(DisplayError::Pin)?;
256        self.command(CMD_SOFTWARE_RESET, &[])?;
257        delay.delay_ns(SOFTWARE_RESET_DELAY_NS);
258        self.init_ili9342_registers()?;
259        self.command(CMD_SLEEP_OUT, &[])?;
260        delay.delay_ns(SLEEP_OUT_DELAY_NS);
261        self.command(CMD_PIXEL_FORMAT_SET, &[PIXEL_FORMAT_RGB565])?;
262        self.write_orientation()?;
263        if self.panel.invert_colors {
264            self.command(CMD_DISPLAY_INVERSION_ON, &[])?;
265        }
266        self.command(CMD_IDLE_MODE_OFF, &[])?;
267        self.command(CMD_DISPLAY_ON, &[])?;
268        delay.delay_ns(DISPLAY_ON_DELAY_NS);
269        Ok(())
270    }
271
272    fn init_ili9342_registers(&mut self) -> Result<(), DisplayError<SpiError, PinError>> {
273        self.command(0xC8, &[0xFF, 0x93, 0x42])?;
274        self.command(0xC0, &[0x12, 0x12])?;
275        self.command(0xC1, &[0x03])?;
276        self.command(0xC5, &[0xF2])?;
277        self.command(0xB0, &[0xE0])?;
278        self.command(0xF6, &[0x01, 0x00, 0x00])?;
279        self.command(
280            0xE0,
281            &[
282                0x00, 0x0C, 0x11, 0x04, 0x11, 0x08, 0x37, 0x89, 0x4C, 0x06, 0x0C, 0x0A, 0x2E, 0x34,
283                0x0F,
284            ],
285        )?;
286        self.command(
287            0xE1,
288            &[
289                0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x2E, 0x33,
290                0x0F,
291            ],
292        )?;
293        self.command(0xB6, &[0x08, 0x82, 0x1D, 0x04])
294    }
295
296    pub fn set_orientation_and_apply(
297        &mut self,
298        orientation: DisplayOrientation,
299    ) -> Result<(), DisplayError<SpiError, PinError>> {
300        self.orientation = orientation;
301        self.write_orientation()
302    }
303
304    fn write_orientation(&mut self) -> Result<(), DisplayError<SpiError, PinError>> {
305        self.command(
306            CMD_MEMORY_ACCESS_CONTROL,
307            &[madctl_for_orientation(self.orientation)],
308        )
309    }
310
311    pub fn command(
312        &mut self,
313        command: u8,
314        data: &[u8],
315    ) -> Result<(), DisplayError<SpiError, PinError>> {
316        self.dc.set_low().map_err(DisplayError::Pin)?;
317        self.spi.write(&[command]).map_err(DisplayError::Spi)?;
318        if !data.is_empty() {
319            self.dc.set_high().map_err(DisplayError::Pin)?;
320            self.spi.write(data).map_err(DisplayError::Spi)?;
321        }
322        Ok(())
323    }
324
325    pub fn clear(&mut self, color: Rgb565) -> Result<(), DisplayError<SpiError, PinError>> {
326        self.fill_solid(&Rectangle::new(Point::zero(), self.logical_size()), color)
327    }
328
329    pub fn blit_pixels<I>(
330        &mut self,
331        area: &Rectangle,
332        pixels: I,
333    ) -> Result<(), DisplayError<SpiError, PinError>>
334    where
335        I: IntoIterator<Item = Rgb565>,
336    {
337        let clipped = area.intersection(&self.bounding_box());
338        if clipped.is_zero_sized() {
339            return Ok(());
340        }
341        if self.orientation == DisplayOrientation::Landscape && clipped == *area {
342            self.set_address_window(&clipped)?;
343            self.write_color_stream(
344                pixels,
345                clipped.size.width.saturating_mul(clipped.size.height) as usize,
346            )
347        } else {
348            self.fill_contiguous(area, pixels)
349        }
350    }
351
352    fn flush_run(&mut self, run: PixelRun) -> Result<(), DisplayError<SpiError, PinError>> {
353        self.fill_solid(&run.rectangle(), run.color)
354    }
355
356    fn flush_pixel_row(
357        &mut self,
358        row: &PixelRowBuffer,
359    ) -> Result<(), DisplayError<SpiError, PinError>> {
360        self.blit_pixels(&row.area(), row.colors().iter().copied())
361    }
362
363    fn set_address_window(
364        &mut self,
365        area: &Rectangle,
366    ) -> Result<(), DisplayError<SpiError, PinError>> {
367        let geometry = self.panel.geometry;
368        let x0 = geometry.offset_x + area.top_left.x.max(0) as u16;
369        let y0 = geometry.offset_y + area.top_left.y.max(0) as u16;
370        let x1 = x0 + area.size.width.saturating_sub(1) as u16;
371        let y1 = y0 + area.size.height.saturating_sub(1) as u16;
372        self.command(
373            CMD_COLUMN_ADDRESS_SET,
374            &[(x0 >> 8) as u8, x0 as u8, (x1 >> 8) as u8, x1 as u8],
375        )?;
376        self.command(
377            CMD_ROW_ADDRESS_SET,
378            &[(y0 >> 8) as u8, y0 as u8, (y1 >> 8) as u8, y1 as u8],
379        )?;
380        self.dc.set_low().map_err(DisplayError::Pin)?;
381        self.spi
382            .write(&[CMD_MEMORY_WRITE])
383            .map_err(DisplayError::Spi)?;
384        self.dc.set_high().map_err(DisplayError::Pin)
385    }
386
387    fn write_repeated_color(
388        &mut self,
389        color: Rgb565,
390        pixels: usize,
391    ) -> Result<(), DisplayError<SpiError, PinError>> {
392        let raw = color.into_storage().to_be_bytes();
393        let mut chunk = [0u8; COLOR_STREAM_PIXELS * 2];
394        for pixel in chunk.chunks_exact_mut(2) {
395            pixel.copy_from_slice(&raw);
396        }
397        let mut remaining = pixels;
398        while remaining > 0 {
399            let write_pixels = remaining.min(COLOR_STREAM_PIXELS);
400            let write_len = write_pixels * 2;
401            self.spi
402                .write(&chunk[..write_len])
403                .map_err(DisplayError::Spi)?;
404            remaining -= write_pixels;
405        }
406        Ok(())
407    }
408
409    fn write_color_stream<I>(
410        &mut self,
411        pixels: I,
412        max_pixels: usize,
413    ) -> Result<(), DisplayError<SpiError, PinError>>
414    where
415        I: IntoIterator<Item = Rgb565>,
416    {
417        let mut chunk = [0u8; PIXEL_STREAM_PIXELS * 2];
418        let mut buffered_pixels = 0usize;
419
420        for (written_pixels, color) in pixels.into_iter().enumerate() {
421            if written_pixels >= max_pixels {
422                break;
423            }
424            let raw = color.into_storage().to_be_bytes();
425            let offset = buffered_pixels * 2;
426            chunk[offset] = raw[0];
427            chunk[offset + 1] = raw[1];
428            buffered_pixels += 1;
429
430            if buffered_pixels == PIXEL_STREAM_PIXELS {
431                self.spi.write(&chunk).map_err(DisplayError::Spi)?;
432                buffered_pixels = 0;
433            }
434        }
435
436        if buffered_pixels > 0 {
437            self.spi
438                .write(&chunk[..buffered_pixels * 2])
439                .map_err(DisplayError::Spi)?;
440        }
441        Ok(())
442    }
443}
444
445impl<SPI, DC, SDCS, SpiError, PinError> DrawTarget for Display<SPI, DC, SDCS>
446where
447    SPI: SpiDevice<Error = SpiError>,
448    DC: OutputPin<Error = PinError>,
449    SDCS: OutputPin<Error = PinError>,
450    SpiError: SpiErrorTrait,
451    PinError: DigitalError,
452{
453    type Color = Rgb565;
454    type Error = DisplayError<SpiError, PinError>;
455
456    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
457    where
458        I: IntoIterator<Item = Pixel<Self::Color>>,
459    {
460        let mut row = None;
461        for Pixel(point, color) in pixels {
462            if !self.bounding_box().contains(point) {
463                if let Some(current) = row.take() {
464                    self.flush_pixel_row(&current)?;
465                }
466                continue;
467            }
468            if let Some(mut current) = row.take() {
469                if current.try_push(point, color) {
470                    row = Some(current);
471                } else {
472                    self.flush_pixel_row(&current)?;
473                    row = Some(PixelRowBuffer::new(point, color));
474                }
475            } else {
476                row = Some(PixelRowBuffer::new(point, color));
477            }
478        }
479        if let Some(current) = row {
480            self.flush_pixel_row(&current)?;
481        }
482        Ok(())
483    }
484
485    fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
486        let clipped = area.intersection(&self.bounding_box());
487        if clipped.is_zero_sized() {
488            return Ok(());
489        }
490        let native = self.map_rectangle_to_native(&clipped);
491        self.set_address_window(&native)?;
492        self.write_repeated_color(
493            color,
494            clipped.size.width.saturating_mul(clipped.size.height) as usize,
495        )
496    }
497
498    fn fill_contiguous<I>(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error>
499    where
500        I: IntoIterator<Item = Self::Color>,
501    {
502        let clipped = area.intersection(&self.bounding_box());
503        if clipped.is_zero_sized() {
504            return Ok(());
505        }
506        if self.orientation == DisplayOrientation::Landscape && clipped == *area {
507            self.set_address_window(&clipped)?;
508            return self.write_color_stream(
509                colors,
510                clipped.size.width.saturating_mul(clipped.size.height) as usize,
511            );
512        }
513
514        let area_width = area.size.width as i32;
515        let mut run = None;
516        for (index, color) in colors.into_iter().enumerate() {
517            let index = index as i32;
518            let point = Point::new(
519                area.top_left.x + index % area_width,
520                area.top_left.y + index / area_width,
521            );
522            if !clipped.contains(point) {
523                if let Some(current) = run.take() {
524                    self.flush_run(current)?;
525                }
526                continue;
527            }
528            if let Some(mut current) = run.take() {
529                if current.try_extend(point, color) {
530                    run = Some(current);
531                } else {
532                    self.flush_run(current)?;
533                    run = Some(PixelRun::new(point, color));
534                }
535            } else {
536                run = Some(PixelRun::new(point, color));
537            }
538        }
539        if let Some(current) = run {
540            self.flush_run(current)?;
541        }
542        Ok(())
543    }
544}
545
546const fn madctl_for_orientation(orientation: DisplayOrientation) -> u8 {
547    match orientation {
548        DisplayOrientation::Landscape => 0x08,
549        DisplayOrientation::LandscapeInverted => 0xC8,
550        DisplayOrientation::Portrait => 0x68,
551        DisplayOrientation::PortraitInverted => 0xA8,
552    }
553}
554
555impl<SPI, DC, SDCS> OriginDimensions for Display<SPI, DC, SDCS> {
556    fn size(&self) -> Size {
557        self.logical_size()
558    }
559}