ssd1331_async/
lib.rs

1//! Async driver for SSD1331-based displays with SPI interface.
2
3#![no_std]
4
5use command::Command;
6use embedded_graphics_core::pixelcolor::raw::ToBytes;
7use embedded_graphics_core::prelude::{Dimensions, OriginDimensions, PixelColor, Point, Size};
8use embedded_graphics_core::primitives::Rectangle;
9use embedded_hal::digital::OutputPin;
10use embedded_hal_async::delay::DelayNs;
11use embedded_hal_async::spi::SpiDevice;
12use heapless::Vec;
13
14mod command;
15mod framebuffer;
16mod rgb332;
17
18pub use framebuffer::Framebuffer;
19pub use rgb332::Rgb332;
20
21pub const DISPLAY_WIDTH: u32 = 96;
22pub const DISPLAY_HEIGHT: u32 = 64;
23
24/// Number of bits per pixel in a data transfer.
25///
26/// The display internally supports BGR order and alternative 16-bit color
27/// mode, but this driver does not, so effectively 8-bit is Rgb332 and 16-bit
28/// is Rgb565. The built-in display RAM always uses 16 bits per pixel. When
29/// sending 8-bit data, the display controller fills in the lower bits. 16-bit
30/// pixels are always sent in big-endian order.
31#[derive(Clone, Copy, PartialEq, Eq)]
32#[repr(u8)]
33pub enum BitDepth {
34    Eight = 0x00,
35    Sixteen = 0x40, // Default after reset.
36}
37
38impl BitDepth {
39    pub fn bytes(&self) -> usize {
40        match self {
41            Self::Eight => 1,
42            Self::Sixteen => 2,
43        }
44    }
45}
46
47/// Row- or column-major order of pixels for a data transfer.
48///
49/// This can be changed before any transfer, but this driver just sets it on
50/// init matching the display orientation (portrait or landscape).
51#[derive(Clone, Copy, PartialEq, Eq)]
52#[repr(u8)]
53pub enum PixelOrder {
54    RowMajor = 0x00, // Default after reset.
55    ColumnMajor = 0x01,
56}
57
58/// Order in which a data transfer populates a given RAM row.
59///
60/// The display controller docs make it sound like this bit sets the mapping
61/// between RAM and display pixels, but it really doesn't: if you flip the
62/// flag after the transfer, the display will not change. Maybe there's a
63/// clever use case for setting this per transfer, but this driver just sets
64/// it once on init.
65#[derive(Clone, Copy, PartialEq, Eq)]
66#[repr(u8)]
67pub enum ColumnDirection {
68    LeftToRight = 0x00, // Default after reset.
69    RightToLeft = 0x02,
70}
71
72/// Mapping between RAM rows and physical display rows.
73///
74/// Changing this flips the displayed pixels vertically without modifying RAM
75/// contents.
76#[derive(Clone, Copy, PartialEq, Eq)]
77#[repr(u8)]
78pub enum RowDirection {
79    Normal = 0x00,
80    Reversed = 0x10,
81}
82
83/// Whether the physical display rows are interleaved compared to the RAM
84/// rows.
85///
86/// Most displays based on SSD1331 controller seem to interleave the pins, so
87/// all pre-configured data mappings set this.
88#[derive(Clone, Copy, PartialEq, Eq)]
89#[repr(u8)]
90pub enum RowInterleave {
91    Disabled = 0x00, // Default after reset.
92    Enabled = 0x20,
93}
94
95/// Describes the mapping between the display memory and the physical pixels.
96#[derive(Clone, Copy, PartialEq, Eq)]
97pub struct Config {
98    pub pixel_order: PixelOrder,
99    pub column_direction: ColumnDirection,
100    pub row_direction: RowDirection,
101    pub row_interleave: RowInterleave,
102}
103
104impl Default for Config {
105    /// Creates a configuration for the default orientation.
106    ///
107    /// This is the same settings as the display assumes after reset, except
108    /// the row interleave is enabled. On my model, this is portrait mode with
109    /// the pins below the screen.
110    fn default() -> Self {
111        Self {
112            pixel_order: PixelOrder::RowMajor,
113            column_direction: ColumnDirection::LeftToRight,
114            row_direction: RowDirection::Normal,
115            row_interleave: RowInterleave::Enabled,
116        }
117    }
118}
119
120impl Config {
121    /// For orientation rotated 90 degrees counter-clockwise from the default.
122    pub fn ccw90() -> Self {
123        Self {
124            pixel_order: PixelOrder::ColumnMajor,
125            column_direction: ColumnDirection::LeftToRight,
126            row_direction: RowDirection::Reversed,
127            row_interleave: RowInterleave::Enabled,
128        }
129    }
130
131    /// For orientation rotated 180 degrees from the default.
132    pub fn ccw180() -> Self {
133        Self {
134            pixel_order: PixelOrder::RowMajor,
135            column_direction: ColumnDirection::RightToLeft,
136            row_direction: RowDirection::Reversed,
137            row_interleave: RowInterleave::Enabled,
138        }
139    }
140
141    /// For orientation rotated 270 degrees counter-clockwise from the default.
142    pub fn ccw270() -> Self {
143        Self {
144            pixel_order: PixelOrder::ColumnMajor,
145            column_direction: ColumnDirection::RightToLeft,
146            row_direction: RowDirection::Normal,
147            row_interleave: RowInterleave::Enabled,
148        }
149    }
150}
151
152/// Error type for this driver.
153///
154/// Currently only used to propagate errors from the HAL.
155#[derive(Debug)]
156pub enum Error<PinE, SpiE> {
157    Pin(PinE),
158    Spi(SpiE),
159}
160
161/// The implementation of the driver.
162///
163/// Can be used with [`embedded-graphics`] crate in async frameworks (e.g.
164/// Embassy). Since the `embedded-graphics` API is synchronous, the driver
165/// assumes use of a framebuffer, and provides an async method to transfer its
166/// contents to the display. Full-size framebuffer requires ~12Kb (6Kb for
167/// 8-bit color mode). The driver allows using a smaller buffer and addressing
168/// a sub-area of the display; for example, it's possible to draw monospaced
169/// text one character at a time, or mix text and graphics areas.
170///
171/// The driver dutifully propagates all errors from the HAL, but the display
172/// controller is stateful and the driver doesn't attempt to return it to a
173/// known good state after an error. You can call `init()` to hard-reset the
174/// display and reinitialize the driver after an error.
175///
176/// [`embedded-graphics`]: https://crates.io/crates/embedded-graphics
177pub struct Ssd1331<RST, DC, SPI> {
178    data_mapping: Config,
179
180    rst: RST,
181    dc: DC,
182    spi: SPI,
183
184    bit_depth: BitDepth,
185    area: Rectangle,
186
187    command_buf: Vec<u8, 16>,
188}
189
190impl<RST, DC, SPI> OriginDimensions for Ssd1331<RST, DC, SPI> {
191    fn size(&self) -> Size {
192        if self.data_mapping.pixel_order == PixelOrder::RowMajor {
193            Size::new(DISPLAY_WIDTH, DISPLAY_HEIGHT)
194        } else {
195            Size::new(DISPLAY_HEIGHT, DISPLAY_WIDTH)
196        }
197    }
198}
199
200impl<RST, DC, SPI, PinE, SpiE> Ssd1331<RST, DC, SPI>
201where
202    RST: OutputPin<Error = PinE>,
203    DC: OutputPin<Error = PinE>,
204    SPI: SpiDevice<Error = SpiE>,
205{
206    /// Creates a new driver instance and initializes the display.
207    ///
208    /// Requires GPIO output pins connected to RST and DC pins on the display,
209    /// and a SPI device with SDO and SCK outputs connected to the display.
210    /// The CS (chip select) pin of the display can be controlled by the SPI
211    /// device, or you can simply tie it low, and pass a DummyPin to the SPI
212    /// device. SPI bus should be configured to MODE_0, MSB first (usually the
213    /// default). Frequencies up to 50 MHz seem to work fine, even though the
214    /// display datasheet specifies ~6 MHz max.
215    pub async fn new(
216        data_mapping: Config,
217        rst: RST,
218        dc: DC,
219        spi: SPI,
220        delay: &mut impl DelayNs,
221    ) -> Result<Self, Error<PinE, SpiE>> {
222        let mut d = Self {
223            rst,
224            dc,
225            spi,
226            data_mapping,
227            bit_depth: BitDepth::Sixteen,
228            area: Rectangle::zero(), // Just until init().
229            command_buf: Vec::new(),
230        };
231
232        d.init(delay).await?;
233
234        Ok(d)
235    }
236
237    /// Hard-resets and re-initializes the display.
238    ///
239    /// Also clears the display RAM. This will take a few milliseconds.
240    /// Instances returned by [Self::new] are already initialized.
241    pub async fn init(&mut self, delay: &mut impl DelayNs) -> Result<(), Error<PinE, SpiE>> {
242        // Hold the display in reset for 1ms. Note that this does not seem to
243        // clear the onboard RAM. The RST pin behaves as NRST (low level resets
244        // the display).
245        self.rst.set_low().map_err(Error::Pin)?;
246        delay.delay_ms(1).await;
247        self.rst.set_high().map_err(Error::Pin)?;
248        delay.delay_ms(1).await;
249
250        self.area = Rectangle::new(Point::zero(), Size::new(DISPLAY_WIDTH, DISPLAY_HEIGHT));
251        self.bit_depth = BitDepth::Sixteen;
252
253        self.command_buf.clear();
254
255        self.send_commands(&[
256            Command::RemapAndBitDepth(self.data_mapping, self.bit_depth),
257            // Default is 15, results in grays saturating at about 50%.
258            Command::MasterCurrent(5),
259            // Default is 0x80 for all. Lowering the G channel seems to result
260            // in a better color balance on my display. This should be a user
261            // setting.
262            Command::Contrast(0x80, 0x50, 0x80),
263            Command::ClearWindow(self.area),
264            Command::DisplayOn(true),
265        ])
266        .await?;
267
268        // ClearWindow needs time to write to RAM.
269        delay.delay_ms(1).await;
270
271        Ok(())
272    }
273
274    /// Consumes the driver and returns the peripherals to you.
275    pub fn release(self) -> (RST, DC, SPI) {
276        (self.rst, self.dc, self.spi)
277    }
278
279    /// Sends the data to the given area of the display's frame buffer.
280    ///
281    /// The `area` is in your logical display coordinates; e.g if you use
282    /// [Config::ccw90], the logical size is (64, 96) and the (0, 0) is the
283    /// top-right corner of the un-rotated physical screen.
284    ///
285    /// You can fill the area using a smaller buffer by repeatedly calling
286    /// this method and passing the same `area`. Sending more data than fits
287    /// in the area will wrap around and overwrite the beginning of the area.
288    ///
289    /// # Panics
290    ///
291    /// If the area is empty or not completely contained within the display
292    /// bounds.
293    pub async fn write_pixels(
294        &mut self,
295        data: &[u8],
296        bit_depth: BitDepth,
297        area: Rectangle,
298    ) -> Result<(), Error<PinE, SpiE>> {
299        assert!(self.bounding_box().contains(area.top_left));
300        assert!(self.bounding_box().contains(area.bottom_right().unwrap()));
301        assert!(self.command_buf.is_empty());
302        if self.bit_depth != bit_depth {
303            self.bit_depth = bit_depth;
304            assert!(Command::RemapAndBitDepth(self.data_mapping, self.bit_depth)
305                .push(&mut self.command_buf));
306        }
307        let ram_area = self.ram_area(area);
308        if self.area != ram_area {
309            self.area = ram_area;
310            assert!(Command::AddressRectangle(self.area).push(&mut self.command_buf));
311        }
312        self.flush_commands().await?;
313        self.dc.set_high().map_err(Error::Pin)?;
314        self.spi.write(data).await.map_err(Error::Spi)?;
315
316        Ok(())
317    }
318
319    // Returns display RAM rectangle for the given rectangle on the logical
320    // display. The display controller takes into account the X/Y mirroring
321    // settings, but the axis remain X and Y regardless of the pixel order.
322    fn ram_area(&self, area: Rectangle) -> Rectangle {
323        if self.data_mapping.pixel_order == PixelOrder::RowMajor {
324            area
325        } else {
326            Rectangle::new(
327                Point::new(area.top_left.y, area.top_left.x),
328                Size::new(area.size.height, area.size.width),
329            )
330        }
331    }
332
333    async fn send_commands(&mut self, commands: &[Command]) -> Result<(), Error<PinE, SpiE>> {
334        for command in commands {
335            if command.push(&mut self.command_buf) {
336                continue;
337            }
338            self.flush_commands().await?;
339            assert!(command.push(&mut self.command_buf));
340        }
341        self.flush_commands().await?;
342        Ok(())
343    }
344
345    async fn flush_commands(&mut self) -> Result<(), Error<PinE, SpiE>> {
346        if !self.command_buf.is_empty() {
347            self.dc.set_low().map_err(Error::Pin)?;
348            self.spi
349                .write(&self.command_buf)
350                .await
351                .map_err(Error::Spi)?;
352            self.command_buf.clear();
353        }
354        Ok(())
355    }
356}
357
358/// Convenience trait to hide details of the driver type.
359///
360/// Once the display driver is created, only the error type depends on the HAL
361/// types used for the implementation. For the use cases where panic on error
362/// is acceptable, we can ignore the type parameters.
363#[allow(async_fn_in_trait)]
364pub trait WritePixels {
365    /// See [Ssd1331::write_pixels].
366    async fn write_pixels(&mut self, data: &[u8], bit_depth: BitDepth, area: Rectangle);
367
368    /// Transfers the contents of the framebuffer to the display.
369    async fn flush<C>(&mut self, fb: &Framebuffer<'_, C>, top_left: Point)
370    where
371        C: PixelColor + ToBytes,
372    {
373        self.write_pixels(
374            fb.data(),
375            fb.bit_depth(),
376            Rectangle::new(top_left, fb.size()),
377        )
378        .await
379    }
380}
381
382impl<RST, DC, SPI, PinE, SpiE> WritePixels for Ssd1331<RST, DC, SPI>
383where
384    RST: OutputPin<Error = PinE>,
385    DC: OutputPin<Error = PinE>,
386    SPI: SpiDevice<Error = SpiE>,
387{
388    async fn write_pixels(&mut self, data: &[u8], bit_depth: BitDepth, area: Rectangle) {
389        self.write_pixels(data, bit_depth, area)
390            .await
391            .unwrap_or_else(|_| panic!("write failed"))
392    }
393}