Skip to main content

oxstack_cardputer/
display.rs

1//! The 1.14" 240×135 ST7789V2 LCD, in landscape with the keyboard below.
2//!
3//! Drawing goes into a [`Frame`] in RAM; [`Display::flush`] sends only the
4//! rows that changed, by DMA straight from the framebuffer (no copy). A full
5//! frame is 64 800 bytes, about 13 ms at the 40 MHz SPI clock.
6
7use embassy_time::Delay;
8use embedded_hal_bus::spi::{DeviceError, ExclusiveDevice, NoDelay};
9use esp_hal::Async;
10use esp_hal::dma::{BurstConfig, DmaRxBuf, DmaTxBuf};
11use esp_hal::gpio::{Level, Output, OutputConfig};
12use esp_hal::peripherals::{DMA_CH0, GPIO33, GPIO34, GPIO35, GPIO36, GPIO37, SPI3};
13use esp_hal::spi::Mode;
14use esp_hal::spi::master::{Config, Spi, SpiDma};
15use esp_hal::time::Rate;
16use oxstack_drivers::st7789::{self, M5_ST7789V2_INIT, Panel, Rotation, St7789};
17use oxstack_gfx::Framebuffer;
18use static_cell::ConstStaticCell;
19
20/// Landscape width in pixels.
21pub const WIDTH: usize = 240;
22/// Landscape height in pixels.
23pub const HEIGHT: usize = 135;
24
25/// A full-screen framebuffer.
26pub type Frame = Framebuffer<WIDTH, HEIGHT>;
27
28/// How the panel sits in the ST7789's 240×320 memory, per M5GFX.
29pub const PANEL: Panel = Panel {
30    width: 135,
31    height: 240,
32    offset_x: 52,
33    offset_y: 40,
34    memory_width: 240,
35    memory_height: 320,
36    bgr: true,
37    invert: true,
38};
39
40/// Keyboard-below landscape orientation.
41pub const ROTATION: Rotation = Rotation::Deg90;
42
43/// SPI clock. GPIO35/36 are routed through the GPIO matrix, not IOMUX,
44/// which caps reliable output at 40 MHz (M5GFX uses the same).
45pub const SPI_FREQUENCY: Rate = Rate::from_mhz(40);
46
47/// The board's framebuffer, placed in `.bss` (64.8 KB RAM, zero flash).
48/// Returns `None` after the first call: take it once at start-up and share
49/// the `&'static mut` between apps.
50pub fn try_framebuffer() -> Option<&'static mut Frame> {
51    static FRAME: ConstStaticCell<Frame> = ConstStaticCell::new(Frame::new());
52    FRAME.try_take()
53}
54
55/// [`try_framebuffer`], panicking if it was already taken.
56pub fn framebuffer() -> &'static mut Frame {
57    try_framebuffer().expect("framebuffer already taken")
58}
59
60type Bus<'d> = ExclusiveDevice<SpiDma<'d, Async>, Output<'d>, NoDelay>;
61
62/// Display errors.
63pub type Error = st7789::Error<
64    DeviceError<esp_hal::spi::Error, core::convert::Infallible>,
65    core::convert::Infallible,
66>;
67
68/// SPI3, DMA channel 0, and the LCD's control pins.
69#[allow(missing_docs)]
70pub struct DisplayResources<'d> {
71    pub spi: SPI3<'d>,
72    pub dma: DMA_CH0<'d>,
73    pub sck: GPIO36<'d>,
74    pub mosi: GPIO35<'d>,
75    pub cs: GPIO37<'d>,
76    pub dc: GPIO34<'d>,
77    pub rst: GPIO33<'d>,
78}
79reborrow_group!(DisplayResources {
80    spi,
81    dma,
82    sck,
83    mosi,
84    cs,
85    dc,
86    rst
87});
88
89/// Bounce buffers for data DMA can't read in place (constants in flash).
90/// The framebuffer is in SRAM and goes direct. One static pair, reused by
91/// every `init`; the `&mut` borrow of the resources serialises them.
92fn bounce_buffers() -> (DmaRxBuf, DmaTxBuf) {
93    const CHUNK: usize = BurstConfig::DEFAULT.max_compatible_chunk_size();
94    let (rx, rx_desc) = static_dma_memory!(32, CHUNK);
95    let (tx, tx_desc) = static_dma_memory!(256, CHUNK);
96    (
97        DmaRxBuf::new(rx_desc, rx).expect("valid DMA buffer"),
98        DmaTxBuf::new(tx_desc, tx).expect("valid DMA buffer"),
99    )
100}
101
102impl<'d> DisplayResources<'d> {
103    /// Resets and initialises the panel. Frame memory holds garbage until the
104    /// first flush, and the backlight is a separate resource.
105    ///
106    /// The returned [`Display`] borrows the resources; drop it and `init`
107    /// can be called again.
108    pub async fn init(&mut self) -> Result<Display<'_>, Error> {
109        let (rx, tx) = bounce_buffers();
110        let spi = Spi::new(
111            self.spi.reborrow(),
112            Config::default()
113                .with_frequency(SPI_FREQUENCY)
114                .with_mode(Mode::_0),
115        )
116        .expect("40 MHz mode 0 is a valid SPI configuration")
117        .with_sck(self.sck.reborrow())
118        .with_mosi(self.mosi.reborrow())
119        .with_dma(self.dma.reborrow())
120        .with_buffers(rx, tx)
121        .into_async();
122
123        let out = OutputConfig::default();
124        let cs = Output::new(self.cs.reborrow(), Level::High, out);
125        let dc = Output::new(self.dc.reborrow(), Level::Low, out);
126        let mut rst = Output::new(self.rst.reborrow(), Level::High, out);
127
128        let mut delay = Delay;
129        let Ok(()) = st7789::hard_reset(&mut rst, &mut delay).await;
130
131        let Ok(bus) = ExclusiveDevice::new_no_delay(spi, cs);
132        let mut lcd = St7789::new(bus, dc, PANEL, ROTATION);
133        lcd.init(M5_ST7789V2_INIT, &mut delay).await?;
134        Ok(Display { lcd, _rst: rst })
135    }
136}
137
138/// The LCD driver.
139pub struct Display<'d> {
140    lcd: St7789<Bus<'d>, Output<'d>>,
141    /// Held so the reset line stays high.
142    _rst: Output<'d>,
143}
144
145impl<'d> Display<'d> {
146    /// Sends the framebuffer's changed rows; returns whether anything was sent.
147    ///
148    /// Cancel-safe in the sense that the panel is left consistent: a
149    /// cancelled flush may leave the dirty rows partly sent, and they are
150    /// no longer marked dirty. Call [`Frame::mark_all_dirty`] afterwards if
151    /// that matters.
152    pub async fn flush(&mut self, frame: &mut Frame) -> Result<bool, Error> {
153        let Some(dirty) = frame.take_dirty() else {
154            return Ok(false);
155        };
156        self.lcd.write_rows(dirty.y, dirty.bytes).await?;
157        Ok(true)
158    }
159
160    /// Turns the panel's output on or off; frame memory is retained.
161    pub async fn set_display_on(&mut self, on: bool) -> Result<(), Error> {
162        self.lcd.set_display_on(on).await
163    }
164
165    /// Enters or leaves panel sleep. Waits 120 ms.
166    pub async fn set_sleep(&mut self, sleep: bool) -> Result<(), Error> {
167        self.lcd.set_sleep(sleep, &mut Delay).await
168    }
169
170    /// The underlying controller driver, for raw commands and windows.
171    pub fn controller(&mut self) -> &mut St7789<Bus<'d>, Output<'d>> {
172        &mut self.lcd
173    }
174}