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