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}