Skip to main content

oxstack_drivers/
st7789.rs

1//! Sitronix ST7789 TFT controller (4-wire SPI, RGB565).
2//!
3//! Small ST7789 panels expose only a window of the controller's 240×320
4//! frame memory, so every panel has a native size, a memory offset and a
5//! rotation-dependent `MADCTL`. [`Panel`] captures that once, and
6//! [`Panel::geometry`] derives the window for any [`Rotation`] at compile
7//! time.
8//!
9//! The driver streams pixel bytes as given. It does no conversion and keeps
10//! no buffer, so a framebuffer that already stores big-endian RGB565 can be
11//! sent with zero copies (and, on ESP32 targets, straight from RAM via DMA).
12
13use embedded_hal::digital::OutputPin;
14use embedded_hal_async::delay::DelayNs;
15use embedded_hal_async::spi::SpiDevice;
16
17/// Command opcodes used by the driver.
18pub mod cmd {
19    #![allow(missing_docs)]
20    pub const SWRESET: u8 = 0x01;
21    pub const SLPIN: u8 = 0x10;
22    pub const SLPOUT: u8 = 0x11;
23    pub const NORON: u8 = 0x13;
24    pub const INVOFF: u8 = 0x20;
25    pub const INVON: u8 = 0x21;
26    pub const DISPOFF: u8 = 0x28;
27    pub const DISPON: u8 = 0x29;
28    pub const CASET: u8 = 0x2A;
29    pub const RASET: u8 = 0x2B;
30    pub const RAMWR: u8 = 0x2C;
31    pub const MADCTL: u8 = 0x36;
32    pub const IDMOFF: u8 = 0x38;
33    pub const COLMOD: u8 = 0x3A;
34}
35
36/// `MADCTL` bits.
37pub mod madctl {
38    #![allow(missing_docs)]
39    pub const MY: u8 = 0x80;
40    pub const MX: u8 = 0x40;
41    pub const MV: u8 = 0x20;
42    pub const ML: u8 = 0x10;
43    pub const BGR: u8 = 0x08;
44    pub const MH: u8 = 0x04;
45}
46
47/// `COLMOD` value for 16 bits per pixel (RGB565) on both interfaces.
48const COLMOD_RGB565: u8 = 0x55;
49
50/// One step of a controller initialisation script.
51#[derive(Clone, Copy, Debug)]
52pub struct InitStep {
53    /// Command opcode.
54    pub cmd: u8,
55    /// Parameter bytes sent with `DC` high.
56    pub params: &'static [u8],
57    /// Time to wait after the command, in milliseconds.
58    pub delay_ms: u16,
59}
60
61impl InitStep {
62    const fn new(cmd: u8, params: &'static [u8]) -> Self {
63        Self {
64            cmd,
65            params,
66            delay_ms: 0,
67        }
68    }
69}
70
71/// Power, voltage and gamma settings for the ST7789V2 panels M5Stack ships
72/// (Cardputer, StickC Plus family). Values are from M5GFX's `Panel_ST7789`
73/// (MIT, © M5Stack); porch and frame-rate registers are left at reset
74/// defaults, as M5GFX does, because they differ between panel lots.
75pub const M5_ST7789V2_INIT: &[InitStep] = &[
76    InitStep::new(0xB7, &[0x35]),       // GCTRL: gate voltages
77    InitStep::new(0xBB, &[0x28]),       // VCOMS
78    InitStep::new(0xC0, &[0x0C]),       // LCMCTRL
79    InitStep::new(0xC2, &[0x01, 0xFF]), // VDVVRHEN
80    InitStep::new(0xC3, &[0x10]),       // VRHS
81    InitStep::new(0xC4, &[0x20]),       // VDVSET
82    InitStep::new(0xD0, &[0xA4, 0xA1]), // PWCTRL1
83    InitStep::new(0xB0, &[0x00, 0xC0]), // RAMCTRL: MSB-first, 65K→262K expansion
84    InitStep::new(
85        0xE0, // PVGAMCTRL
86        &[
87            0xD0, 0x00, 0x02, 0x07, 0x0A, 0x28, 0x32, 0x44, 0x42, 0x06, 0x0E, 0x12, 0x14, 0x17,
88        ],
89    ),
90    InitStep::new(
91        0xE1, // NVGAMCTRL
92        &[
93            0xD0, 0x00, 0x02, 0x07, 0x0A, 0x28, 0x31, 0x54, 0x47, 0x0E, 0x1C, 0x17, 0x1B, 0x1E,
94        ],
95    ),
96    InitStep {
97        cmd: cmd::SLPOUT,
98        params: &[],
99        delay_ms: 130,
100    },
101    InitStep::new(cmd::IDMOFF, &[]),
102];
103
104/// Screen rotation, clockwise from the panel's native portrait orientation.
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106#[cfg_attr(feature = "defmt", derive(defmt::Format))]
107pub enum Rotation {
108    /// Native orientation.
109    Deg0,
110    /// Quarter turn.
111    Deg90,
112    /// Half turn.
113    Deg180,
114    /// Three-quarter turn.
115    Deg270,
116}
117
118/// How a physical panel sits inside the controller's frame memory.
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120pub struct Panel {
121    /// Visible width in native (portrait) orientation.
122    pub width: u16,
123    /// Visible height in native orientation.
124    pub height: u16,
125    /// Column of the first visible pixel in frame memory.
126    pub offset_x: u16,
127    /// Row of the first visible pixel in frame memory.
128    pub offset_y: u16,
129    /// Frame memory width (240 for ST7789).
130    pub memory_width: u16,
131    /// Frame memory height (320 for ST7789).
132    pub memory_height: u16,
133    /// Panel wires sub-pixels in BGR order.
134    pub bgr: bool,
135    /// Panel needs colour inversion (`INVON`) for correct colours.
136    pub invert: bool,
137}
138
139/// The drawable window for one panel + rotation.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141#[cfg_attr(feature = "defmt", derive(defmt::Format))]
142pub struct Geometry {
143    /// Width in the rotated orientation.
144    pub width: u16,
145    /// Height in the rotated orientation.
146    pub height: u16,
147    /// Frame-memory column of logical x = 0.
148    pub col_start: u16,
149    /// Frame-memory row of logical y = 0.
150    pub row_start: u16,
151    /// `MADCTL` register value.
152    pub madctl: u8,
153}
154
155impl Panel {
156    /// Window and `MADCTL` for `rotation`.
157    ///
158    /// When the scan direction is mirrored on an axis, the visible window
159    /// moves to the far end of frame memory on that axis. That is why a
160    /// panel offset of 52 can become 53 once rotated.
161    pub const fn geometry(&self, rotation: Rotation) -> Geometry {
162        use madctl::*;
163        let (swap, mirror_col, mirror_row, mut bits) = match rotation {
164            Rotation::Deg0 => (false, false, false, 0),
165            Rotation::Deg90 => (true, false, true, MV | MX | MH),
166            Rotation::Deg180 => (false, true, true, MX | MH | MY | ML),
167            Rotation::Deg270 => (true, true, false, MV | MY | ML),
168        };
169        if self.bgr {
170            bits |= BGR;
171        }
172        let (w, h, ox, oy, mw, mh) = if swap {
173            (
174                self.height,
175                self.width,
176                self.offset_y,
177                self.offset_x,
178                self.memory_height,
179                self.memory_width,
180            )
181        } else {
182            (
183                self.width,
184                self.height,
185                self.offset_x,
186                self.offset_y,
187                self.memory_width,
188                self.memory_height,
189            )
190        };
191        Geometry {
192            width: w,
193            height: h,
194            col_start: if mirror_col { mw - (w + ox) } else { ox },
195            row_start: if mirror_row { mh - (h + oy) } else { oy },
196            madctl: bits,
197        }
198    }
199}
200
201/// Driver error: either the SPI device or the `DC` pin failed.
202#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203#[cfg_attr(feature = "defmt", derive(defmt::Format))]
204pub enum Error<S, P> {
205    /// SPI transfer failed.
206    Spi(S),
207    /// Driving the data/command pin failed.
208    Pin(P),
209}
210
211/// Pulses a hardware reset line (active low) and waits for the controller.
212pub async fn hard_reset<P: OutputPin, D: DelayNs>(
213    rst: &mut P,
214    delay: &mut D,
215) -> Result<(), P::Error> {
216    rst.set_low()?;
217    delay.delay_us(20).await;
218    rst.set_high()?;
219    // Datasheet: up to 120 ms before the controller accepts SLPOUT.
220    delay.delay_ms(120).await;
221    Ok(())
222}
223
224/// ST7789 driver.
225pub struct St7789<SPI, DC> {
226    spi: SPI,
227    dc: DC,
228    panel: Panel,
229    geometry: Geometry,
230}
231
232type LcdResult<T, SPI, DC> = core::result::Result<
233    T,
234    Error<
235        <SPI as embedded_hal_async::spi::ErrorType>::Error,
236        <DC as embedded_hal::digital::ErrorType>::Error,
237    >,
238>;
239
240impl<SPI: SpiDevice, DC: OutputPin> St7789<SPI, DC> {
241    /// Wraps an SPI device and the `DC` pin. Call [`init`](Self::init) next.
242    pub const fn new(spi: SPI, dc: DC, panel: Panel, rotation: Rotation) -> Self {
243        Self {
244            spi,
245            dc,
246            panel,
247            geometry: panel.geometry(rotation),
248        }
249    }
250
251    /// Runs `script`, then sets RGB565, rotation, inversion, and turns the
252    /// display on.
253    pub async fn init(
254        &mut self,
255        script: &[InitStep],
256        delay: &mut impl DelayNs,
257    ) -> LcdResult<(), SPI, DC> {
258        for step in script {
259            self.command(step.cmd, step.params).await?;
260            if step.delay_ms > 0 {
261                delay.delay_ms(u32::from(step.delay_ms)).await;
262            }
263        }
264        self.command(cmd::COLMOD, &[COLMOD_RGB565]).await?;
265        self.command(cmd::MADCTL, &[self.geometry.madctl]).await?;
266        let inv = if self.panel.invert {
267            cmd::INVON
268        } else {
269            cmd::INVOFF
270        };
271        self.command(inv, &[]).await?;
272        self.command(cmd::NORON, &[]).await?;
273        self.command(cmd::DISPON, &[]).await
274    }
275
276    /// Current drawable window.
277    pub const fn geometry(&self) -> Geometry {
278        self.geometry
279    }
280
281    /// Changes rotation. Frame memory is not redrawn.
282    pub async fn set_rotation(&mut self, rotation: Rotation) -> LcdResult<(), SPI, DC> {
283        self.geometry = self.panel.geometry(rotation);
284        self.command(cmd::MADCTL, &[self.geometry.madctl]).await
285    }
286
287    /// Sends a command followed by its parameters.
288    pub async fn command(&mut self, cmd: u8, params: &[u8]) -> LcdResult<(), SPI, DC> {
289        self.dc.set_low().map_err(Error::Pin)?;
290        self.spi.write(&[cmd]).await.map_err(Error::Spi)?;
291        if !params.is_empty() {
292            self.dc.set_high().map_err(Error::Pin)?;
293            self.spi.write(params).await.map_err(Error::Spi)?;
294        }
295        Ok(())
296    }
297
298    /// Selects the inclusive window `(x0, y0)..=(x1, y1)` in logical
299    /// coordinates. Coordinates beyond the visible area are the caller's
300    /// responsibility.
301    pub async fn set_window(
302        &mut self,
303        x0: u16,
304        y0: u16,
305        x1: u16,
306        y1: u16,
307    ) -> LcdResult<(), SPI, DC> {
308        let g = self.geometry;
309        let [a, b] = (x0 + g.col_start).to_be_bytes();
310        let [c, d] = (x1 + g.col_start).to_be_bytes();
311        self.command(cmd::CASET, &[a, b, c, d]).await?;
312        let [a, b] = (y0 + g.row_start).to_be_bytes();
313        let [c, d] = (y1 + g.row_start).to_be_bytes();
314        self.command(cmd::RASET, &[a, b, c, d]).await
315    }
316
317    /// Streams big-endian RGB565 bytes into the current window.
318    pub async fn write_pixels(&mut self, bytes: &[u8]) -> LcdResult<(), SPI, DC> {
319        self.command(cmd::RAMWR, &[]).await?;
320        self.dc.set_high().map_err(Error::Pin)?;
321        self.spi.write(bytes).await.map_err(Error::Spi)
322    }
323
324    /// Writes full-width rows starting at row `y`. `bytes` must hold whole
325    /// rows; it is sent as one contiguous transfer.
326    pub async fn write_rows(&mut self, y: u16, bytes: &[u8]) -> LcdResult<(), SPI, DC> {
327        let row_bytes = usize::from(self.geometry.width) * 2;
328        debug_assert!(row_bytes > 0 && bytes.len().is_multiple_of(row_bytes));
329        let rows = (bytes.len() / row_bytes) as u16;
330        if rows == 0 {
331            return Ok(());
332        }
333        self.set_window(0, y, self.geometry.width - 1, y + rows - 1)
334            .await?;
335        self.write_pixels(bytes).await
336    }
337
338    /// Display on/off. Frame memory is kept while off.
339    pub async fn set_display_on(&mut self, on: bool) -> LcdResult<(), SPI, DC> {
340        self.command(if on { cmd::DISPON } else { cmd::DISPOFF }, &[])
341            .await
342    }
343
344    /// Enters (`true`) or leaves sleep mode. The controller needs 120 ms
345    /// between the two; this call waits that long after the command.
346    pub async fn set_sleep(
347        &mut self,
348        sleep: bool,
349        delay: &mut impl DelayNs,
350    ) -> LcdResult<(), SPI, DC> {
351        self.command(if sleep { cmd::SLPIN } else { cmd::SLPOUT }, &[])
352            .await?;
353        delay.delay_ms(120).await;
354        Ok(())
355    }
356
357    /// Software reset; prefer [`hard_reset`] when a reset pin is wired.
358    pub async fn soft_reset(&mut self, delay: &mut impl DelayNs) -> LcdResult<(), SPI, DC> {
359        self.command(cmd::SWRESET, &[]).await?;
360        delay.delay_ms(150).await;
361        Ok(())
362    }
363
364    /// Returns the SPI device and `DC` pin.
365    pub fn release(self) -> (SPI, DC) {
366        (self.spi, self.dc)
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    extern crate std;
373    use super::*;
374    use crate::test_util::block_on;
375    use core::cell::RefCell;
376    use core::convert::Infallible;
377    use embedded_hal_async::spi::Operation;
378    use std::rc::Rc;
379    use std::vec::Vec;
380
381    /// The 1.14" panel on the M5Stack Cardputer, as configured by M5GFX.
382    const CARDPUTER: Panel = Panel {
383        width: 135,
384        height: 240,
385        offset_x: 52,
386        offset_y: 40,
387        memory_width: 240,
388        memory_height: 320,
389        bgr: true,
390        invert: true,
391    };
392
393    #[test]
394    fn cardputer_geometry_per_rotation() {
395        let table = [
396            (Rotation::Deg0, (135, 240, 52, 40, 0x08)),
397            (Rotation::Deg90, (240, 135, 40, 53, 0x6C)),
398            (Rotation::Deg180, (135, 240, 53, 40, 0xDC)),
399            (Rotation::Deg270, (240, 135, 40, 52, 0xB8)),
400        ];
401        for (r, expected) in table {
402            let g = CARDPUTER.geometry(r);
403            assert_eq!(
404                (g.width, g.height, g.col_start, g.row_start, g.madctl),
405                expected,
406                "{r:?}"
407            );
408            let (mw, mh) = if g.madctl & madctl::MV != 0 {
409                (320, 240)
410            } else {
411                (240, 320)
412            };
413            assert!(g.col_start + g.width <= mw, "{r:?} overflows columns");
414            assert!(g.row_start + g.height <= mh, "{r:?} overflows rows");
415        }
416    }
417
418    /// `(dc_level, bytes)` for each SPI write.
419    type WriteLog = Rc<RefCell<Vec<(bool, Vec<u8>)>>>;
420
421    /// Records each SPI write together with the `DC` level at that moment.
422    #[derive(Clone, Default)]
423    struct Bus {
424        dc: Rc<RefCell<bool>>,
425        log: WriteLog,
426    }
427    struct Dc(Rc<RefCell<bool>>);
428
429    impl embedded_hal::digital::ErrorType for Dc {
430        type Error = Infallible;
431    }
432    impl OutputPin for Dc {
433        fn set_low(&mut self) -> Result<(), Infallible> {
434            *self.0.borrow_mut() = false;
435            Ok(())
436        }
437        fn set_high(&mut self) -> Result<(), Infallible> {
438            *self.0.borrow_mut() = true;
439            Ok(())
440        }
441    }
442    impl embedded_hal_async::spi::ErrorType for Bus {
443        type Error = Infallible;
444    }
445    impl SpiDevice for Bus {
446        async fn transaction(&mut self, ops: &mut [Operation<'_, u8>]) -> Result<(), Infallible> {
447            for op in ops {
448                if let Operation::Write(bytes) = op {
449                    self.log
450                        .borrow_mut()
451                        .push((*self.dc.borrow(), bytes.to_vec()));
452                }
453            }
454            Ok(())
455        }
456    }
457
458    #[test]
459    fn write_rows_sets_offset_window_then_streams_data() {
460        let bus = Bus::default();
461        let dc = Dc(bus.dc.clone());
462        let mut lcd = St7789::new(bus.clone(), dc, CARDPUTER, Rotation::Deg90);
463        let rows = [0xAB; 240 * 2 * 2];
464        block_on(lcd.write_rows(10, &rows)).unwrap();
465
466        let log = bus.log.borrow();
467        let expected: [(bool, &[u8]); 6] = [
468            (false, &[cmd::CASET]),
469            (true, &[0, 40, 1, 23]), // 40..=279
470            (false, &[cmd::RASET]),
471            (true, &[0, 63, 0, 64]), // 53+10 ..= 53+11
472            (false, &[cmd::RAMWR]),
473            (true, &rows),
474        ];
475        assert_eq!(log.len(), expected.len());
476        for (got, want) in log.iter().zip(expected) {
477            assert_eq!((got.0, got.1.as_slice()), want);
478        }
479    }
480}