Skip to main content

core_s3/display/
panel.rs

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