lcd_async/
lib.rs

1#![no_std]
2// associated re-typing not supported in rust yet
3#![allow(clippy::type_complexity)]
4#![warn(missing_docs)]
5
6//!
7//! # lcd-async
8//!
9//! An **async-first** display driver for TFT displays implementing the [MIPI Display Command Set](https://www.mipi.org/specifications/display-command-set).
10//!
11//! This crate is a fork of [mipidsi](https://github.com/almindor/mipidsi), redesigned for modern, async-native embedded Rust. It features a framebuffer-centric workflow, enabling efficient, non-blocking rendering and seamless integration with async runtimes like [embassy](https://embassy.dev/).
12//!
13//! ## Key Features
14//!
15//! - **Fully Asynchronous:** All display I/O is performed via async traits, making it ideal for async runtimes and DMA-driven workflows.
16//! - **Framebuffer-Based Drawing:** Draw your scene into an in-memory buffer using `embedded-graphics`, then send the entire frame to the display in one efficient async operation.
17//! - **Separation of Concerns:** Drawing is synchronous and CPU-bound; sending to the display is async and I/O-bound. This enables double buffering and advanced rendering patterns.
18//! - **Multiple Interface Support:**
19//!   - SPI ([`interface::SpiInterface`])
20//!   - 8080-style parallel via GPIO ([`interface::ParallelInterface`])
21//!
22//! ## Supported Models
23//!
24//! - GC9107
25//! - GC9A01
26//! - ILI9225
27//! - ILI9341
28//! - ILI9342C
29//! - ILI9486
30//! - ILI9488
31//! - RM67162
32//! - ST7735
33//! - ST7789
34//! - ST7796
35//!
36//! ## Example: Minimal Framebuffer Workflow
37//!
38//! ```rust
39//! use embedded_graphics::prelude::*;
40//! use embedded_graphics::pixelcolor::Rgb565;
41//! use embedded_graphics::primitives::{Circle, PrimitiveStyle};
42//! use lcd_async::raw_framebuf::RawFrameBuf;
43//!
44//! const WIDTH: usize = 240;
45//! const HEIGHT: usize = 240;
46//! let mut buffer = [0u8; WIDTH * HEIGHT * 2];
47//! let mut fbuf = RawFrameBuf::<Rgb565, _>::new(&mut buffer[..], WIDTH, HEIGHT);
48//! fbuf.clear(Rgb565::BLACK).unwrap();
49//! Circle::new(Point::new(120, 120), 80)
50//!     .into_styled(PrimitiveStyle::with_fill(Rgb565::GREEN))
51//!     .draw(&mut fbuf)
52//!     .unwrap();
53//! // See the examples/ directory for full async display usage
54//! ```
55//!
56//! ## Troubleshooting
57//!
58//! Refer to the [troubleshooting guide](_troubleshooting)
59//! if you experience problems like a blank screen or incorrect colors.
60//!
61//! ## License
62//!
63//! Licensed under MIT, same as the original mipidsi crate.
64
65use dcs::SetAddressMode;
66
67pub mod interface;
68
69use embedded_hal::digital::OutputPin;
70use embedded_hal_async::delay::DelayNs;
71
72pub mod options;
73use options::MemoryMapping;
74
75mod builder;
76pub use builder::*;
77
78pub mod dcs;
79
80pub mod models;
81pub mod raw_framebuf;
82use models::Model;
83
84mod graphics;
85
86mod test_image;
87pub use test_image::TestImage;
88
89pub mod _troubleshooting;
90
91///
92/// Display driver to connect to TFT displays.
93///
94pub struct Display<DI, MODEL, RST>
95where
96    DI: interface::Interface,
97    MODEL: Model,
98    RST: OutputPin,
99{
100    // DCS provider
101    di: DI,
102    // Model
103    model: MODEL,
104    // Reset pin
105    rst: Option<RST>,
106    // Model Options, includes current orientation
107    options: options::ModelOptions,
108    // Current MADCTL value copy for runtime updates
109    #[allow(dead_code)]
110    madctl: SetAddressMode,
111    // State monitor for sleeping TODO: refactor to a Model-connected state machine
112    sleeping: bool,
113}
114
115impl<DI, M, RST> Display<DI, M, RST>
116where
117    DI: interface::Interface,
118    M: Model,
119    RST: OutputPin,
120{
121    ///
122    /// Returns currently set [options::Orientation]
123    ///
124    pub fn orientation(&self) -> options::Orientation {
125        self.options.orientation
126    }
127
128    ///
129    /// Sets display [options::Orientation] with mirror image parameter
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use lcd_async::options::{Orientation, Rotation};
135    ///
136    /// # tokio_test::block_on(async {
137    /// # let mut display = lcd_async::_mock::new_mock_display().await;
138    /// display.set_orientation(Orientation::default().rotate(Rotation::Deg180)).await.unwrap();
139    /// # });
140    /// ```
141    pub async fn set_orientation(
142        &mut self,
143        orientation: options::Orientation,
144    ) -> Result<(), DI::Error> {
145        self.options.orientation = orientation;
146        self.model.update_options(&mut self.di, &self.options).await
147    }
148
149    /// Sends a raw pixel data slice to the specified rectangular region of the display.
150    pub async fn show_raw_data<DW>(
151        &mut self,
152        x: u16,
153        y: u16,
154        width: u16,
155        height: u16,
156        pixel_data: &[DW],
157    ) -> Result<(), DI::Error>
158    where
159        DI: interface::Interface<Word = DW>,
160        DW: Copy,
161    {
162        let ex = x + width - 1;
163        let ey = y + height - 1;
164
165        self.set_address_window(x, y, ex, ey).await?;
166        M::write_memory_start(&mut self.di).await?;
167        self.di.send_data_slice(pixel_data).await
168    }
169
170    /// Sets the vertical scroll region.
171    ///
172    /// The `top_fixed_area` and `bottom_fixed_area` arguments can be used to
173    /// define an area on the top and/or bottom of the display which won't be
174    /// affected by scrolling.
175    ///
176    /// Note that this method is not affected by the current display orientation
177    /// and will always scroll vertically relative to the default display
178    /// orientation.
179    ///
180    /// The combined height of the fixed area must not larger than the
181    /// height of the framebuffer height in the default orientation.
182    ///
183    /// After the scrolling region is defined the [`set_vertical_scroll_offset`](Self::set_vertical_scroll_offset) can be
184    /// used to scroll the display.
185    pub async fn set_vertical_scroll_region(
186        &mut self,
187        top_fixed_area: u16,
188        bottom_fixed_area: u16,
189    ) -> Result<(), DI::Error> {
190        M::set_vertical_scroll_region(&mut self.di, top_fixed_area, bottom_fixed_area).await
191    }
192
193    /// Sets the vertical scroll offset.
194    ///
195    /// Setting the vertical scroll offset shifts the vertical scroll region
196    /// upwards by `offset` pixels.
197    ///
198    /// Use [`set_vertical_scroll_region`](Self::set_vertical_scroll_region) to setup the scroll region, before
199    /// using this method.
200    pub async fn set_vertical_scroll_offset(&mut self, offset: u16) -> Result<(), DI::Error> {
201        M::set_vertical_scroll_offset(&mut self.di, offset).await
202    }
203
204    ///
205    /// Release resources allocated to this driver back.
206    /// This returns the display interface, reset pin and and the model deconstructing the driver.
207    ///
208    pub fn release(self) -> (DI, M, Option<RST>) {
209        (self.di, self.model, self.rst)
210    }
211
212    // Sets the address window for the display.
213    async fn set_address_window(
214        &mut self,
215        sx: u16,
216        sy: u16,
217        ex: u16,
218        ey: u16,
219    ) -> Result<(), DI::Error> {
220        // add clipping offsets if present
221        let mut offset = self.options.display_offset;
222        let mapping = MemoryMapping::from(self.options.orientation);
223        if mapping.reverse_columns {
224            offset.0 = M::FRAMEBUFFER_SIZE.0 - (self.options.display_size.0 + offset.0);
225        }
226        if mapping.reverse_rows {
227            offset.1 = M::FRAMEBUFFER_SIZE.1 - (self.options.display_size.1 + offset.1);
228        }
229        if mapping.swap_rows_and_columns {
230            offset = (offset.1, offset.0);
231        }
232
233        let (sx, sy, ex, ey) = (sx + offset.0, sy + offset.1, ex + offset.0, ey + offset.1);
234
235        M::update_address_window(
236            &mut self.di,
237            self.options.orientation.rotation,
238            sx,
239            sy,
240            ex,
241            ey,
242        )
243        .await
244    }
245
246    ///
247    /// Configures the tearing effect output.
248    ///
249    pub async fn set_tearing_effect(
250        &mut self,
251        tearing_effect: options::TearingEffect,
252    ) -> Result<(), DI::Error> {
253        M::set_tearing_effect(&mut self.di, tearing_effect, &self.options).await
254    }
255
256    ///
257    /// Returns `true` if display is currently set to sleep.
258    ///
259    pub fn is_sleeping(&self) -> bool {
260        self.sleeping
261    }
262
263    ///
264    /// Puts the display to sleep, reducing power consumption.
265    /// Need to call [Self::wake] before issuing other commands
266    ///
267    pub async fn sleep<D: DelayNs>(&mut self, delay: &mut D) -> Result<(), DI::Error> {
268        M::sleep(&mut self.di, delay).await?;
269        self.sleeping = true;
270        Ok(())
271    }
272
273    ///
274    /// Wakes the display after it's been set to sleep via [Self::sleep]
275    ///
276    pub async fn wake<D: DelayNs>(&mut self, delay: &mut D) -> Result<(), DI::Error> {
277        M::wake(&mut self.di, delay).await?;
278        self.sleeping = false;
279        Ok(())
280    }
281
282    /// Returns the DCS interface for sending raw commands.
283    ///
284    /// # Safety
285    ///
286    /// Sending raw commands to the controller can lead to undefined behaviour,
287    /// because the rest of the code isn't aware of any state changes that were caused by sending raw commands.
288    /// The user must ensure that the state of the controller isn't altered in a way that interferes with the normal
289    /// operation of this crate.
290    pub unsafe fn dcs(&mut self) -> &mut DI {
291        &mut self.di
292    }
293}
294
295/// Mock implementations of embedded-hal and interface traits for async architecture.
296///
297/// Do not use types in this module outside of doc tests.
298#[doc(hidden)]
299pub mod _mock {
300    use core::convert::Infallible;
301
302    use embedded_hal::{digital, spi};
303    use embedded_hal_async::delay::DelayNs;
304
305    use crate::{
306        interface::{Interface, InterfaceKind},
307        models::ILI9341Rgb565,
308        Builder, Display, NoResetPin,
309    };
310
311    pub async fn new_mock_display() -> Display<MockDisplayInterface, ILI9341Rgb565, NoResetPin> {
312        Builder::new(ILI9341Rgb565, MockDisplayInterface)
313            .init(&mut MockDelay)
314            .await
315            .unwrap()
316    }
317
318    pub struct MockOutputPin;
319
320    impl digital::OutputPin for MockOutputPin {
321        fn set_low(&mut self) -> Result<(), Self::Error> {
322            Ok(())
323        }
324
325        fn set_high(&mut self) -> Result<(), Self::Error> {
326            Ok(())
327        }
328    }
329
330    impl digital::ErrorType for MockOutputPin {
331        type Error = core::convert::Infallible;
332    }
333
334    pub struct MockSpi;
335
336    impl spi::SpiDevice for MockSpi {
337        fn transaction(
338            &mut self,
339            _operations: &mut [spi::Operation<'_, u8>],
340        ) -> Result<(), Self::Error> {
341            Ok(())
342        }
343    }
344
345    impl spi::ErrorType for MockSpi {
346        type Error = core::convert::Infallible;
347    }
348
349    pub struct MockDelay;
350
351    impl DelayNs for MockDelay {
352        async fn delay_ns(&mut self, _ns: u32) {}
353    }
354
355    pub struct MockDisplayInterface;
356
357    impl Interface for MockDisplayInterface {
358        type Word = u8;
359        type Error = Infallible;
360
361        const KIND: InterfaceKind = InterfaceKind::Serial4Line;
362
363        async fn send_command(&mut self, _command: u8, _args: &[u8]) -> Result<(), Self::Error> {
364            Ok(())
365        }
366
367        async fn send_data_slice(&mut self, _data: &[Self::Word]) -> Result<(), Self::Error> {
368            Ok(())
369        }
370    }
371}