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(¤t)?;
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(¤t)?;
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(¤t)?;
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}