From 158a7ab8f391b4db042f67c9dadfe344c2b76066 Mon Sep 17 00:00:00 2001 From: s-ol Date: Sun, 30 Nov 2025 20:10:06 +0100 Subject: [PATCH 1/8] update for embedded-hal v1.0 --- Cargo.toml | 2 +- src/command.rs | 2 +- src/interface.rs | 44 +++++++++++++++++++++++++------------------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 945a9af..866d364 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ exclude = [ ] [dependencies] -embedded-hal = "0.2" +embedded-hal = "1.0" nb = "0.1" [dependencies.itertools] diff --git a/src/command.rs b/src/command.rs index ca9d6c7..2a0082c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -228,7 +228,7 @@ pub enum CommandError { BadTableLength, } -impl CommandError { +impl CommandError { /// Unwrap a `CommandError` that is assumed to be of the `InterfaceError` variant, or panic if /// it is any other variant. This is particularly used inside the region abstractions where we /// assume that non-interface-related errors are prevented by the correctness checks performed diff --git a/src/interface.rs b/src/interface.rs index 83f925c..f5ff5be 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -53,8 +53,8 @@ pub mod spi { impl SpiInterface where - SPI: hal::spi::FullDuplex, - DC: hal::digital::v2::OutputPin, + SPI: hal::spi::SpiDevice, + DC: hal::digital::OutputPin, { /// Create a new SPI interface to communicate with the display driver. `spi` is the SPI /// master device, and `dc` is the GPIO output pin connected to the D/C pin of the SSD1322. @@ -65,36 +65,40 @@ pub mod spi { impl DisplayInterface for SpiInterface where - SPI: hal::spi::FullDuplex, - DC: hal::digital::v2::OutputPin, + SPI: hal::spi::SpiDevice, + DC: hal::digital::OutputPin, { type Error = SpiInterfaceError< - ::Error, - >::Error, + ::Error, + ::Error, >; /// Send a command word to the display's command register. Synchronous. fn send_command(&mut self, cmd: u8) -> Result<(), Self::Error> { // The SPI device has FIFOs that we must ensure are drained before the bus will // quiesce. This must happen before asserting DC for a command. - while let Ok(_) = self.spi.read() { + let mut buf = [0 as u8; 1]; + while let Ok(_) = self.spi.read(&mut buf) { self.dc.set_high().map_err(Self::Error::from_dc)?; } self.dc.set_low().map_err(Self::Error::from_dc)?; - let bus_op = nb::block!(self.spi.send(cmd)) - .and_then(|_| nb::block!(self.spi.read())) - .map_err(Self::Error::from_spi) - .map(core::mem::drop); + let bus_op = self + .spi + .write(&[cmd]) + .and_then(|_| self.spi.read(&mut buf)) + .map_err(Self::Error::from_spi); self.dc.set_high().map_err(Self::Error::from_dc)?; bus_op } /// Send a sequence of data words to the display from a buffer. Synchronous. fn send_data(&mut self, buf: &[u8]) -> Result<(), Self::Error> { - for word in buf { - nb::block!(self.spi.send(word.clone())).map_err(Self::Error::from_spi)?; - nb::block!(self.spi.read()).map_err(Self::Error::from_spi)?; - } + self.spi.write(buf).map_err(Self::Error::from_spi)?; + // ,let mut buf = [0 as u8; 1]; + // ,for word in buf { + // , nb::block!(self.spi.write(word.clone())).map_err(Self::Error::from_spi)?; + // , nb::block!(self.spi.read(&mut buf)).map_err(Self::Error::from_spi)?; + // ,} Ok(()) } @@ -102,13 +106,15 @@ pub mod spi { /// the hardware FIFO is full, returns `WouldBlock` which means the word was not accepted /// and should be retried later. fn send_data_async(&mut self, word: u8) -> nb::Result<(), Self::Error> { - match self.spi.send(word) { + let mut buf = [0 as u8; 1]; + match self.spi.write(&[word]) { Ok(()) => { - let _ = self.spi.read(); + let _ = self.spi.read(&mut buf); Ok(()) } - Err(nb::Error::Other(e)) => Err(nb::Error::Other(Self::Error::from_spi(e))), - Err(nb::Error::WouldBlock) => Err(nb::Error::WouldBlock), + Err(e) => Err(nb::Error::Other(Self::Error::from_spi(e))), + // Err(nb::Error::Other(e)) => Err(nb::Error::Other(Self::Error::from_spi(e))), + // Err(nb::Error::WouldBlock) => Err(nb::Error::WouldBlock), } } } From 336c6f1dda4a4cc7b260e9a0d61f0bcf93b8d964 Mon Sep 17 00:00:00 2001 From: s-ol Date: Sat, 6 Dec 2025 19:55:28 +0100 Subject: [PATCH 2/8] Use display-interface crate to abstract data interface This uses display-interface to abstract away the communications interface, becoming fully independent (yet compatible with) embedded-hal. This also adds async support (behind the `async` feature). --- Cargo.toml | 8 +- src/command.rs | 43 +++++-- src/config.rs | 65 +++++++--- src/display/mod.rs | 111 ++++++++++++----- src/display/overscanned_region.rs | 32 +++-- src/display/region.rs | 92 +++++++------- src/interface.rs | 198 ------------------------------ src/lib.rs | 12 +- 8 files changed, 239 insertions(+), 322 deletions(-) delete mode 100644 src/interface.rs diff --git a/Cargo.toml b/Cargo.toml index 866d364..c144299 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,13 @@ exclude = [ ] [dependencies] -embedded-hal = "1.0" -nb = "0.1" +display-interface = "0.5" +maybe-async-cfg = "0.2" [dependencies.itertools] version = "0.7" default-features = false [features] -default = ["std"] -std = [] +default = ["async"] +async = [] diff --git a/src/command.rs b/src/command.rs index 2a0082c..cda3f74 100644 --- a/src/command.rs +++ b/src/command.rs @@ -6,7 +6,10 @@ //! there is a "column" address, these refer to horizontal groups of 2 bytes driving 4 pixels. use crate::command::consts::*; -use crate::interface::DisplayInterface; + +#[cfg(feature = "async")] +use display_interface::AsyncWriteOnlyDataCommand; +use display_interface::{DataFormat::U8, DisplayError, WriteOnlyDataCommand}; pub mod consts { //! Constants describing max supported display size and the display RAM layout. @@ -119,6 +122,7 @@ pub enum DisplayMode { /// Enumerates most of the valid commands that can be sent to the SSD1322 along with their /// parameter values. Commands which accept an array of similar "arguments" as a slice are encoded /// by `BufCommand` instead to avoid lifetime parameters on this enum. +#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async"))] #[derive(Clone, Copy)] pub enum Command { /// Enable the gray scale gamma table (see `BufCommand::SetGrayScaleTable`). @@ -203,6 +207,7 @@ pub enum Command { /// Enumerates commands that can be sent to the SSD1322 which accept a slice argument buffer. This /// is separated from `Command` so that the lifetime parameter of the argument buffer slice does /// not pervade code which never invokes these two commands. +#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async"))] pub enum BufCommand<'buf> { /// Set the gray scale gamma table. Each byte 0-14 can range from 0-180 and sets the pixel /// drive pulse width in DCLKs. Bytes 0->14 adjust the gamma setting for grayscale levels @@ -219,7 +224,7 @@ pub enum BufCommand<'buf> { /// Errors that can occur in commands. #[derive(Debug, PartialEq)] pub enum CommandError { - /// The underlying `DisplayInterface` gave an error while trying to issue the command to the + /// The underlying `WriteOnlyDataCommand` gave an error while trying to issue the command to the /// hardware. InterfaceError(IE), /// An argument to the command was outside of the valid range. @@ -257,11 +262,18 @@ macro_rules! ok_command { }}; } +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents(WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand")) + ) +)] impl Command { /// Transmit the command encoded by `self` to the display on interface `iface`. - pub fn send(self, iface: &mut DI) -> Result<(), CommandError> + pub async fn send(self, iface: &mut DI) -> Result<(), CommandError> where - DI: DisplayInterface, + DI: WriteOnlyDataCommand, { let mut arg_buf = [0u8; 2]; let (cmd, data) = match self { @@ -391,23 +403,32 @@ impl Command { } }?; iface - .send_command(cmd) + .send_commands(U8(&[cmd])) + .await .map_err(|e| CommandError::InterfaceError(e))?; if data.len() == 0 { Ok(()) } else { iface - .send_data(data) + .send_data(U8(data)) + .await .map_err(|e| CommandError::InterfaceError(e)) } } } +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents(WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand")) + ) +)] impl<'a> BufCommand<'a> { /// Transmit the command encoded by `self` to the display on interface `iface`. - pub fn send(self, iface: &mut DI) -> Result<(), CommandError> + pub async fn send(self, iface: &mut DI) -> Result<(), CommandError> where - DI: DisplayInterface, + DI: WriteOnlyDataCommand, { let (cmd, data) = match self { BufCommand::SetGrayScaleTable(table) => { @@ -432,13 +453,15 @@ impl<'a> BufCommand<'a> { BufCommand::WriteImageData(buf) => Ok((0x5C, buf)), }?; iface - .send_command(cmd) + .send_commands(U8(&[cmd])) + .await .map_err(|e| CommandError::InterfaceError(e))?; if data.len() == 0 { Ok(()) } else { iface - .send_data(data) + .send_data(U8(data)) + .await .map_err(|e| CommandError::InterfaceError(e)) } } diff --git a/src/config.rs b/src/config.rs index 79a1616..c272c4a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,28 +2,39 @@ //! relatively-static configuration. use crate::command::*; -use crate::interface; + +#[cfg(feature = "async")] +use display_interface::AsyncWriteOnlyDataCommand; +use display_interface::{DisplayError, WriteOnlyDataCommand}; /// The portion of the configuration which will persist inside the `Display` because it shares /// registers with functions that can be changed after initialization. This allows the rest of the /// `Config` struct to be thrown away to save RAM after `Display::init` finishes. +#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async"))] pub(crate) struct PersistentConfig { com_scan_direction: ComScanDirection, com_layout: ComLayout, } +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents(Command, WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand")) + ) +)] impl PersistentConfig { /// Transmit commands to the display at `iface` necessary to put that display into the /// configuration encoded in `self`. - pub(crate) fn send( + pub(crate) async fn send( &self, iface: &mut DI, increment_axis: IncrementAxis, column_remap: ColumnRemap, nibble_remap: NibbleRemap, - ) -> Result<(), CommandError> + ) -> Result<(), CommandError> where - DI: interface::DisplayInterface, + DI: WriteOnlyDataCommand, { Command::SetRemapping( increment_axis, @@ -33,11 +44,16 @@ impl PersistentConfig { self.com_layout, ) .send(iface) + .await } } /// A configuration for the display. Builder methods offer a declarative way to either sent a /// configuration command at init time, or to leave it at the chip's POR default. +#[maybe_async_cfg::maybe( + sync(keep_self), + async(feature = "async", idents(Command, PersistentConfig)) +)] pub struct Config { pub(crate) persistent_config: PersistentConfig, contrast_current_cmd: Option, @@ -49,6 +65,17 @@ pub struct Config { com_deselect_voltage_cmd: Option, } +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents( + Command, + PersistentConfig, + WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand") + ) + ) +)] impl Config { /// Create a new configuration. COM scan direction and COM layout are mandatory because the /// display will not function correctly unless they are set, so they must be provided in the @@ -138,23 +165,23 @@ impl Config { /// Transmit commands to the display at `iface` necessary to put that display into the /// configuration encoded in `self`. - pub(crate) fn send(&self, iface: &mut DI) -> Result<(), CommandError> + pub(crate) async fn send(&self, iface: &mut DI) -> Result<(), CommandError> where - DI: interface::DisplayInterface, + DI: WriteOnlyDataCommand, { - self.phase_lengths_cmd.map_or(Ok(()), |c| c.send(iface))?; - self.contrast_current_cmd - .map_or(Ok(()), |c| c.send(iface))?; - self.clock_fosc_divset_cmd - .map_or(Ok(()), |c| c.send(iface))?; - self.display_enhancements_cmd - .map_or(Ok(()), |c| c.send(iface))?; - self.second_precharge_period_cmd - .map_or(Ok(()), |c| c.send(iface))?; - self.precharge_voltage_cmd - .map_or(Ok(()), |c| c.send(iface))?; - self.com_deselect_voltage_cmd - .map_or(Ok(()), |c| c.send(iface))?; + for maybe_cmd in [ + self.phase_lengths_cmd, + self.contrast_current_cmd, + self.clock_fosc_divset_cmd, + self.display_enhancements_cmd, + self.second_precharge_period_cmd, + self.precharge_voltage_cmd, + self.com_deselect_voltage_cmd, + ] { + if let Some(cmd) = maybe_cmd { + cmd.send(iface).await? + } + } Ok(()) } } diff --git a/src/display/mod.rs b/src/display/mod.rs index 5f749d2..799b64a 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -19,10 +19,13 @@ pub mod region; use crate::command::consts::*; use crate::command::*; -use crate::config::{Config, PersistentConfig}; -use crate::display::overscanned_region::OverscannedRegion; -use crate::display::region::Region; -use crate::interface; +use crate::config::*; +use crate::display::overscanned_region::*; +use crate::display::region::*; + +#[cfg(feature = "async")] +use display_interface::AsyncWriteOnlyDataCommand; +use display_interface::{DisplayError, WriteOnlyDataCommand}; /// A pixel coordinate pair of `column` and `row`. `column` must be in the range [0, /// `consts::PIXEL_COL_MAX`], and `row` must be in the range [0, `consts::PIXEL_ROW_MAX`]. @@ -30,9 +33,19 @@ use crate::interface; pub struct PixelCoord(pub i16, pub i16); /// A driver for an SSD1322 display. +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents( + PersistentConfig, + WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"), + ) + ) +)] pub struct Display where - DI: interface::DisplayInterface, + DI: WriteOnlyDataCommand, { iface: DI, display_size: PixelCoord, @@ -40,9 +53,23 @@ where persistent_config: Option, } +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents( + Config, + Command, + BufCommand, + Region, + OverscannedRegion, + WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"), + ) + ) +)] impl Display where - DI: interface::DisplayInterface, + DI: WriteOnlyDataCommand, { /// Construct a new display driver for a display with viewable dimensions `display_size`, which /// is connected to the interface `iface`. @@ -76,37 +103,59 @@ where } /// Initialize the display with a config message. - pub fn init(&mut self, config: Config) -> Result<(), CommandError> { - self.sleep(true)?; - Command::SetDisplayMode(DisplayMode::BlankDark).send(&mut self.iface)?; - config.send(&mut self.iface)?; + pub async fn init(&mut self, config: Config) -> Result<(), CommandError> { + self.sleep(true).await?; + Command::SetDisplayMode(DisplayMode::BlankDark) + .send(&mut self.iface) + .await?; + Command::SetDefaultGrayScaleTable + .send(&mut self.iface) + .await?; + config.send(&mut self.iface).await?; self.persistent_config = Some(config.persistent_config); - Command::SetMuxRatio(self.display_size.1 as u8).send(&mut self.iface)?; - Command::SetDisplayOffset(self.display_offset.1 as u8).send(&mut self.iface)?; - Command::SetStartLine(0).send(&mut self.iface)?; - self.persistent_config.as_ref().unwrap().send( - &mut self.iface, - IncrementAxis::Horizontal, - ColumnRemap::Forward, - NibbleRemap::Forward, - )?; - self.sleep(false)?; - Command::SetDisplayMode(DisplayMode::Normal).send(&mut self.iface) + Command::SetMuxRatio(self.display_size.1 as u8) + .send(&mut self.iface) + .await?; + Command::SetDisplayOffset(self.display_offset.1 as u8) + .send(&mut self.iface) + .await?; + Command::SetStartLine(0).send(&mut self.iface).await?; + self.persistent_config + .as_ref() + .unwrap() + .send( + &mut self.iface, + IncrementAxis::Horizontal, + ColumnRemap::Forward, + NibbleRemap::Forward, + ) + .await?; + self.sleep(false).await?; + Command::SetDisplayMode(DisplayMode::Normal) + .send(&mut self.iface) + .await } /// Control sleep mode. - pub fn sleep(&mut self, enabled: bool) -> Result<(), CommandError> { - Command::SetSleepMode(enabled).send(&mut self.iface) + pub async fn sleep(&mut self, enabled: bool) -> Result<(), CommandError> { + Command::SetSleepMode(enabled).send(&mut self.iface).await } /// Control the master contrast. - pub fn contrast(&mut self, contrast: u8) -> Result<(), CommandError> { - Command::SetMasterContrast(contrast).send(&mut self.iface) + pub async fn contrast(&mut self, contrast: u8) -> Result<(), CommandError> { + Command::SetMasterContrast(contrast) + .send(&mut self.iface) + .await } /// Set the display brightness look-up table. - pub fn gray_scale_table(&mut self, table: &[u8]) -> Result<(), CommandError> { - BufCommand::SetGrayScaleTable(table).send(&mut self.iface) + pub async fn gray_scale_table( + &mut self, + table: &[u8], + ) -> Result<(), CommandError> { + BufCommand::SetGrayScaleTable(table) + .send(&mut self.iface) + .await } /// Set the vertical pan. @@ -114,8 +163,8 @@ where /// This uses the `Command::SetStartLine` feature to shift the display RAM row addresses /// relative to the active set of COM lines, allowing any display-height-sized window of the /// entire 128 rows of display RAM to be made visible. - pub fn vertical_pan(&mut self, offset: u8) -> Result<(), CommandError> { - Command::SetStartLine(offset).send(&mut self.iface) + pub async fn vertical_pan(&mut self, offset: u8) -> Result<(), CommandError> { + Command::SetStartLine(offset).send(&mut self.iface).await } /// Construct a rectangular region onto which to draw image data. @@ -131,7 +180,7 @@ where &'di mut self, upper_left: PixelCoord, lower_right: PixelCoord, - ) -> Result, CommandError> { + ) -> Result, CommandError> { // The row fields are bounds-checked against the chip's maximum supported row rather than // the display size, because the display supports vertical scrolling by adding an offset to // the memory address that corresponds to row 0 (`SetStartLine` command). This feature @@ -178,7 +227,7 @@ where &'di mut self, upper_left: PixelCoord, lower_right: PixelCoord, - ) -> Result, CommandError> { + ) -> Result, CommandError> { if false || upper_left.0 >= lower_right.0 || upper_left.1 >= lower_right.1 diff --git a/src/display/overscanned_region.rs b/src/display/overscanned_region.rs index cf0f041..46c06df 100644 --- a/src/display/overscanned_region.rs +++ b/src/display/overscanned_region.rs @@ -6,9 +6,12 @@ use itertools::iproduct; use crate::command::consts::*; -use crate::display::region::{Pack8to4, Region}; +use crate::display::region::*; use crate::display::PixelCoord; -use crate::interface; + +#[cfg(feature = "async")] +use display_interface::AsyncWriteOnlyDataCommand; +use display_interface::{DisplayError, WriteOnlyDataCommand}; /// A handle to a rectangular region which can be drawn into, but which is permitted to have /// portions that lie outside the viewable area of the display. Pixels that fall outside the @@ -21,9 +24,16 @@ use crate::interface; /// /// These are intended to be short-lived, and contain a mutable borrow of the display that issued /// them so clashing writes are prevented. +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents(Region, WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),) + ) +)] pub struct OverscannedRegion<'di, DI> where - DI: 'di + interface::DisplayInterface, + DI: 'di + WriteOnlyDataCommand, { viewable_region: Option>, upper_left: PixelCoord, @@ -44,9 +54,16 @@ fn in_range(x: T, lo: T, hi: T) -> bool { x >= lo && x < hi } +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents(Region, WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),) + ) +)] impl<'di, DI> OverscannedRegion<'di, DI> where - DI: 'di + interface::DisplayInterface, + DI: 'di + WriteOnlyDataCommand, { /// Construct a new region. This is only called by the factory method /// `Display::overscanned_region`, which checks the region coordinates are correctly ordered, @@ -87,7 +104,7 @@ where /// values of horizontally-adjacent pixels. Pixels are drawn left-to-right and top-to-bottom. /// The sequence of pixels is filtered such that only pixels which intersect the displayable /// area are transmitted to the hardware. - pub fn draw_packed(&mut self, iter: I) -> Result<(), DI::Error> + pub async fn draw_packed(&mut self, iter: I) -> Result<(), DisplayError> where I: Iterator, { @@ -109,17 +126,18 @@ where .as_mut() .unwrap() .draw_packed(only_viewable) + .await } /// Draw unpacked pixel image data into the region, where each byte independently represents a /// single pixel intensity value in the range [0, 15]. Pixels are drawn left-to-right and /// top-to-bottom. The sequence of pixels is filtered such that only pixels which intersect the /// displayable area are transmitted to the hardware. - pub fn draw(&mut self, iter: I) -> Result<(), DI::Error> + pub async fn draw(&mut self, iter: I) -> Result<(), DisplayError> where I: Iterator, { - self.draw_packed(Pack8to4(iter)) + self.draw_packed(Pack8to4(iter)).await } } diff --git a/src/display/region.rs b/src/display/region.rs index 1dc66b3..e753d5c 100644 --- a/src/display/region.rs +++ b/src/display/region.rs @@ -1,17 +1,27 @@ //! Region abstraction for drawing into rectangular regions of the display. -use nb; - use crate::command::{BufCommand, Command, CommandError}; +#[cfg(feature = "async")] +use crate::command::{BufCommandAsync, CommandAsync}; use crate::display::PixelCoord; -use crate::interface; + +#[cfg(feature = "async")] +use display_interface::AsyncWriteOnlyDataCommand; +use display_interface::{DataFormat::U8Iter, DisplayError, WriteOnlyDataCommand}; /// A handle to a rectangular region of a display which can be drawn into. These are intended to be /// short-lived, and contain a mutable borrow of the display that issued them so clashing writes /// are prevented. +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents(Command, WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),) + ) +)] pub struct Region<'di, DI> where - DI: 'di + interface::DisplayInterface, + DI: 'di + WriteOnlyDataCommand, { iface: &'di mut DI, top: u8, @@ -21,9 +31,20 @@ where pixel_cols: u16, } +#[maybe_async_cfg::maybe( + sync(keep_self), + async( + feature = "async", + idents( + Command, + BufCommand, + WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"), + ) + ) +)] impl<'di, DI> Region<'di, DI> where - DI: 'di + interface::DisplayInterface, + DI: 'di + WriteOnlyDataCommand, { /// Construct a new region. This is only called by the factory method `Display::region`, which /// checks that the region coordinates are within the viewable area and correctly ordered, and @@ -42,65 +63,40 @@ where /// Draw packed-pixel image data into the region, such that each byte is two 4-bit gray scale /// values of horizontally-adjacent pixels. Pixels are drawn left-to-right and top-to-bottom. - pub fn draw_packed(&mut self, mut iter: I) -> Result<(), DI::Error> + pub async fn draw_packed(&mut self, iter: I) -> Result<(), DisplayError> where I: Iterator, { // Set the row and column address registers and put the display in write mode. Unwrap all // of the CommandErrors in this scope as interface errors, as all bounds checking should be // done by the time we are here. - (|| { - Command::SetColumnAddress(self.buf_left, self.buf_left + self.buf_cols - 1) - .send(self.iface)?; - Command::SetRowAddress(self.top, self.top + self.rows - 1).send(self.iface)?; - BufCommand::WriteImageData(&[]).send(self.iface)?; - Ok(()) - })() - .map_err(CommandError::unwrap_interface)?; + Command::SetColumnAddress(self.buf_left, self.buf_left + self.buf_cols - 1) + .send(self.iface) + .await + .map_err(CommandError::unwrap_interface)?; + Command::SetRowAddress(self.top, self.top + self.rows - 1) + .send(self.iface) + .await + .map_err(CommandError::unwrap_interface)?; + BufCommand::WriteImageData(&[]) + .send(self.iface) + .await + .map_err(CommandError::unwrap_interface)?; - // Paint the region using asynchronous writes so that iter.next() may run concurrently with - // the SPI write cycle for a small throughput win. let region_total_bytes = self.pixel_cols as usize * self.rows as usize / 2; - let mut total_written = 0; - let mut next_byte: u8; - - loop { - // Break early if we have copied enough bytes to exactly fill the region. - if total_written >= region_total_bytes { - break; - } - - // Break early if the iterator runs out of bytes. - match iter.next() { - Some(pixels) => { - total_written += 1; - next_byte = pixels; - } - None => break, - } - - // Write the byte to the interface FIFO. If the FIFO is full then poll it until the - // send succeeds before continuing the outer loop to consume the next byte from the - // iterator. - loop { - match self.iface.send_data_async(next_byte) { - Ok(()) => break, - Err(nb::Error::WouldBlock) => {} - Err(nb::Error::Other(e)) => return Err(e), - } - } - } - Ok(()) + self.iface + .send_data(U8Iter(&mut iter.take(region_total_bytes))) + .await } /// Draw unpacked pixel image data into the region, where each byte independently represents a /// single pixel intensity value in the range [0, 15]. Pixels are drawn left-to-right and /// top-to-bottom. - pub fn draw(&mut self, iter: I) -> Result<(), DI::Error> + pub async fn draw(&mut self, iter: I) -> Result<(), DisplayError> where I: Iterator, { - self.draw_packed(Pack8to4(iter)) + self.draw_packed(Pack8to4(iter)).await } } diff --git a/src/interface.rs b/src/interface.rs deleted file mode 100644 index f5ff5be..0000000 --- a/src/interface.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! This module provides shims for the `embedded-hal` hardware corresponding to the SSD1322's -//! supported electrical/bus interfaces. It is a shim between `embedded-hal` implementations and -//! the display driver's command layer. - -use nb; - -/// An interface for the SSD1322 implements this trait, which provides the basic operations for -/// sending pre-encoded commands and data to the chip via the interface. -pub trait DisplayInterface { - type Error; - - fn send_command(&mut self, cmd: u8) -> Result<(), Self::Error>; - fn send_data(&mut self, buf: &[u8]) -> Result<(), Self::Error>; - fn send_data_async(&mut self, word: u8) -> nb::Result<(), Self::Error>; -} - -pub mod spi { - //! The SPI interface supports the "4-wire" interface of the driver, such that each word on the - //! SPI bus is 8 bits. The "3-wire" mode is not supported, as it replaces the D/C GPIO with a - //! 9th bit on each SPI word, and `embedded-hal` SPI traits do not currently support - //! non-byte-aligned SPI word lengths. - - use embedded_hal as hal; - - use super::DisplayInterface; - use nb; - - /// The union of all errors that may occur on the SPI interface. This consists of variants for - /// the error types of the D/C GPIO and the SPI bus. - #[derive(Debug)] - pub enum SpiInterfaceError { - DCError(DCE), - SPIError(SPIE), - } - - impl SpiInterfaceError { - fn from_dc(e: DCE) -> Self { - Self::DCError(e) - } - fn from_spi(e: SPIE) -> Self { - Self::SPIError(e) - } - } - - /// A configured `DisplayInterface` for controlling an SSD1322 via 4-wire SPI. - pub struct SpiInterface { - /// The SPI master device connected to the SSD1322. - spi: SPI, - /// A GPIO output pin connected to the D/C (data/command) pin of the SSD1322 (the fourth - /// "wire" of "4-wire" mode). - dc: DC, - } - - impl SpiInterface - where - SPI: hal::spi::SpiDevice, - DC: hal::digital::OutputPin, - { - /// Create a new SPI interface to communicate with the display driver. `spi` is the SPI - /// master device, and `dc` is the GPIO output pin connected to the D/C pin of the SSD1322. - pub fn new(spi: SPI, dc: DC) -> Self { - Self { spi: spi, dc: dc } - } - } - - impl DisplayInterface for SpiInterface - where - SPI: hal::spi::SpiDevice, - DC: hal::digital::OutputPin, - { - type Error = SpiInterfaceError< - ::Error, - ::Error, - >; - - /// Send a command word to the display's command register. Synchronous. - fn send_command(&mut self, cmd: u8) -> Result<(), Self::Error> { - // The SPI device has FIFOs that we must ensure are drained before the bus will - // quiesce. This must happen before asserting DC for a command. - let mut buf = [0 as u8; 1]; - while let Ok(_) = self.spi.read(&mut buf) { - self.dc.set_high().map_err(Self::Error::from_dc)?; - } - self.dc.set_low().map_err(Self::Error::from_dc)?; - let bus_op = self - .spi - .write(&[cmd]) - .and_then(|_| self.spi.read(&mut buf)) - .map_err(Self::Error::from_spi); - self.dc.set_high().map_err(Self::Error::from_dc)?; - bus_op - } - - /// Send a sequence of data words to the display from a buffer. Synchronous. - fn send_data(&mut self, buf: &[u8]) -> Result<(), Self::Error> { - self.spi.write(buf).map_err(Self::Error::from_spi)?; - // ,let mut buf = [0 as u8; 1]; - // ,for word in buf { - // , nb::block!(self.spi.write(word.clone())).map_err(Self::Error::from_spi)?; - // , nb::block!(self.spi.read(&mut buf)).map_err(Self::Error::from_spi)?; - // ,} - Ok(()) - } - - /// Send a data word to the display asynchronously, using `nb` style non-blocking send. If - /// the hardware FIFO is full, returns `WouldBlock` which means the word was not accepted - /// and should be retried later. - fn send_data_async(&mut self, word: u8) -> nb::Result<(), Self::Error> { - let mut buf = [0 as u8; 1]; - match self.spi.write(&[word]) { - Ok(()) => { - let _ = self.spi.read(&mut buf); - Ok(()) - } - Err(e) => Err(nb::Error::Other(Self::Error::from_spi(e))), - // Err(nb::Error::Other(e)) => Err(nb::Error::Other(Self::Error::from_spi(e))), - // Err(nb::Error::WouldBlock) => Err(nb::Error::WouldBlock), - } - } - } -} - -#[cfg(test)] -pub mod test_spy { - //! An interface for use in unit tests to spy on whatever was sent to it. - - use super::DisplayInterface; - use nb; - use std::cell::RefCell; - use std::rc::Rc; - - #[derive(Clone, Debug, PartialEq)] - pub enum Sent { - Cmd(u8), - Data(Vec), - } - - pub struct TestSpyInterface { - sent: Rc>>, - } - - impl TestSpyInterface { - pub fn new() -> Self { - TestSpyInterface { - sent: Rc::new(RefCell::new(Vec::new())), - } - } - pub fn split(&self) -> Self { - Self { - sent: self.sent.clone(), - } - } - pub fn check(&self, cmd: u8, data: &[u8]) { - let sent = self.sent.borrow(); - if data.len() == 0 { - assert_eq!(sent.len(), 1); - } else { - assert_eq!(sent.len(), 2); - assert_eq!(sent[1], Sent::Data(data.to_vec())); - } - assert_eq!(sent[0], Sent::Cmd(cmd)); - } - pub fn check_multi(&self, expect: &[Sent]) { - assert_eq!(*self.sent.borrow(), expect); - } - pub fn clear(&mut self) { - self.sent.borrow_mut().clear() - } - } - - impl DisplayInterface for TestSpyInterface { - type Error = core::convert::Infallible; - - fn send_command(&mut self, cmd: u8) -> Result<(), Self::Error> { - self.sent.borrow_mut().push(Sent::Cmd(cmd)); - Ok(()) - } - fn send_data(&mut self, data: &[u8]) -> Result<(), Self::Error> { - self.sent.borrow_mut().push(Sent::Data(data.to_vec())); - Ok(()) - } - fn send_data_async(&mut self, word: u8) -> nb::Result<(), Self::Error> { - let mut sent = self.sent.borrow_mut(); - { - let last_idx = sent.len() - 1; - match &mut sent[last_idx] { - Sent::Cmd(_) => {} - Sent::Data(ref mut d) => { - d.push(word); - return Ok(()); - } - }; - } - sent.push(Sent::Data(vec![word])); - Ok(()) - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 7a08285..7d49e64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,18 +35,20 @@ //! //! Example code is available in the `examples` folder. -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(feature = "std")] +#![no_std] extern crate core; pub mod command; pub mod config; pub mod display; -pub mod interface; // Re-exports for primary API. pub use crate::command::{consts, ComLayout, ComScanDirection}; pub use crate::config::Config; pub use crate::display::{Display, PixelCoord}; -pub use crate::interface::spi::SpiInterface; + +#[cfg(feature = "async")] +pub use crate::config::ConfigAsync; + +#[cfg(feature = "async")] +pub use crate::display::DisplayAsync; From d475d4b55a349e3679a548f66c3f055ae4b7c2a6 Mon Sep 17 00:00:00 2001 From: s-ol Date: Sat, 6 Dec 2025 20:05:59 +0100 Subject: [PATCH 3/8] Expose remapping in Config, remove PersistentConfig --- src/config.rs | 104 +++++++++++++++++++++------------------------ src/display/mod.rs | 18 +------- 2 files changed, 50 insertions(+), 72 deletions(-) diff --git a/src/config.rs b/src/config.rs index c272c4a..347457d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,55 +7,15 @@ use crate::command::*; use display_interface::AsyncWriteOnlyDataCommand; use display_interface::{DisplayError, WriteOnlyDataCommand}; -/// The portion of the configuration which will persist inside the `Display` because it shares -/// registers with functions that can be changed after initialization. This allows the rest of the -/// `Config` struct to be thrown away to save RAM after `Display::init` finishes. -#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async"))] -pub(crate) struct PersistentConfig { - com_scan_direction: ComScanDirection, - com_layout: ComLayout, -} - -#[maybe_async_cfg::maybe( - sync(keep_self), - async( - feature = "async", - idents(Command, WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand")) - ) -)] -impl PersistentConfig { - /// Transmit commands to the display at `iface` necessary to put that display into the - /// configuration encoded in `self`. - pub(crate) async fn send( - &self, - iface: &mut DI, - increment_axis: IncrementAxis, - column_remap: ColumnRemap, - nibble_remap: NibbleRemap, - ) -> Result<(), CommandError> - where - DI: WriteOnlyDataCommand, - { - Command::SetRemapping( - increment_axis, - column_remap, - nibble_remap, - self.com_scan_direction, - self.com_layout, - ) - .send(iface) - .await - } -} - /// A configuration for the display. Builder methods offer a declarative way to either sent a /// configuration command at init time, or to leave it at the chip's POR default. -#[maybe_async_cfg::maybe( - sync(keep_self), - async(feature = "async", idents(Command, PersistentConfig)) -)] +#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async", idents(Command)))] pub struct Config { - pub(crate) persistent_config: PersistentConfig, + com_scan_direction: ComScanDirection, + com_layout: ComLayout, + increment_axis: IncrementAxis, + column_remap: ColumnRemap, + nibble_remap: NibbleRemap, contrast_current_cmd: Option, phase_lengths_cmd: Option, clock_fosc_divset_cmd: Option, @@ -69,11 +29,7 @@ pub struct Config { sync(keep_self), async( feature = "async", - idents( - Command, - PersistentConfig, - WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand") - ) + idents(Command, WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand")) ) )] impl Config { @@ -83,10 +39,11 @@ impl Config { /// methods on `Config`. pub fn new(com_scan_direction: ComScanDirection, com_layout: ComLayout) -> Self { Config { - persistent_config: PersistentConfig { - com_scan_direction: com_scan_direction, - com_layout: com_layout, - }, + com_scan_direction: com_scan_direction, + com_layout: com_layout, + increment_axis: IncrementAxis::Horizontal, + column_remap: ColumnRemap::Forward, + nibble_remap: NibbleRemap::Forward, contrast_current_cmd: None, phase_lengths_cmd: None, clock_fosc_divset_cmd: None, @@ -97,6 +54,33 @@ impl Config { } } + /// Extend this `Config` to explicitly configure the increment axis. See + /// `Command::SetRemapping`. + pub fn increment_axis(self, increment_axis: IncrementAxis) -> Self { + Self { + increment_axis, + ..self + } + } + + /// Extend this `Config` to explicitly configure the column remapping. See + /// `Command::SetRemapping`. + pub fn column_remap(self, column_remap: ColumnRemap) -> Self { + Self { + column_remap, + ..self + } + } + + /// Extend this `Config` to explicitly configure the nibble remapping. See + /// `Command::SetRemapping`. + pub fn nibble_remap(self, nibble_remap: NibbleRemap) -> Self { + Self { + nibble_remap, + ..self + } + } + /// Extend this `Config` to explicitly configure display contrast current. See /// `Command::SetContrastCurrent`. pub fn contrast_current(self, current: u8) -> Self { @@ -169,6 +153,16 @@ impl Config { where DI: WriteOnlyDataCommand, { + Command::SetRemapping( + self.increment_axis, + self.column_remap, + self.nibble_remap, + self.com_scan_direction, + self.com_layout, + ) + .send(iface) + .await?; + for maybe_cmd in [ self.phase_lengths_cmd, self.contrast_current_cmd, diff --git a/src/display/mod.rs b/src/display/mod.rs index 799b64a..1db5ac4 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -37,10 +37,7 @@ pub struct PixelCoord(pub i16, pub i16); sync(keep_self), async( feature = "async", - idents( - PersistentConfig, - WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"), - ) + idents(WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand")) ) )] pub struct Display @@ -50,7 +47,6 @@ where iface: DI, display_size: PixelCoord, display_offset: PixelCoord, - persistent_config: Option, } #[maybe_async_cfg::maybe( @@ -98,7 +94,6 @@ where iface: iface, display_size: display_size, display_offset: display_offset, - persistent_config: None, } } @@ -112,7 +107,6 @@ where .send(&mut self.iface) .await?; config.send(&mut self.iface).await?; - self.persistent_config = Some(config.persistent_config); Command::SetMuxRatio(self.display_size.1 as u8) .send(&mut self.iface) .await?; @@ -120,16 +114,6 @@ where .send(&mut self.iface) .await?; Command::SetStartLine(0).send(&mut self.iface).await?; - self.persistent_config - .as_ref() - .unwrap() - .send( - &mut self.iface, - IncrementAxis::Horizontal, - ColumnRemap::Forward, - NibbleRemap::Forward, - ) - .await?; self.sleep(false).await?; Command::SetDisplayMode(DisplayMode::Normal) .send(&mut self.iface) From 41ff6d9d7aa40bd15f0f3c98da92b05c4435a8e2 Mon Sep 17 00:00:00 2001 From: s-ol Date: Sat, 13 Dec 2025 12:16:11 +0100 Subject: [PATCH 4/8] display-interface: update documentation --- README.md | 6 ++++-- src/config.rs | 2 +- src/lib.rs | 35 +++++++++++++++++++++-------------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d7ad2b8..bc763d7 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ ![Newhaven OLED display showing Ferris and the Rust logo](ferris-on-nhd.jpg) Pure Rust driver for the SSD1322 OLED display chip, for use with -[embedded-hal](https://crates.io/crates/embedded-hal). +[display-interface](https://crates.io/crates/display-interface). ## Description This driver is intended to work on embedded platforms using the `embedded-hal` trait library. It is `no_std`, contains no added `unsafe`, and does not require -an allocator. The initial release supports the 4-wire SPI interface. +an allocator. Both sync and `async` implementations are provided. Because the SSD1322 supports displays as large as 480x128 @ 4bpp, the primary API uses a `Region` abstraction to allow writing a stream of pixel data from an @@ -31,6 +31,8 @@ would consume a colossal (for a μC) 30kiB of RAM. inspiration. [japaric/embedded-hal](https://github.com/japaric/embedded-hal) for making dealing with embedded hardware easy, safe, and enjoyable. +[therealprof/display_interface](https://github.com/therealprof/display-interface) +for an abstract display interface. ## License diff --git a/src/config.rs b/src/config.rs index 347457d..504f1fd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,7 @@ use crate::command::*; use display_interface::AsyncWriteOnlyDataCommand; use display_interface::{DisplayError, WriteOnlyDataCommand}; -/// A configuration for the display. Builder methods offer a declarative way to either sent a +/// A configuration for the display. Builder methods offer a declarative way to either send a /// configuration command at init time, or to leave it at the chip's POR default. #[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async", idents(Command)))] pub struct Config { diff --git a/src/lib.rs b/src/lib.rs index 7d49e64..f89f944 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,37 +1,44 @@ //! Driver library for the Solomon Systech SSD1322 dot matrix OLED display driver. //! -//! This driver is intended to work on embedded platforms using any implementation of the -//! `embedded-hal` trait library. +//! This driver uses the traits from the `display-interface` crate to abstract away the physical +//! interface and HAL layer. //! //! Because the SSD1322 supports displays as large as 480x128 @ 4bpp, the primary API uses a //! `Region` abstraction to allow writing a stream of pixel data from an iterator onto a //! rectangular sub-region of the display area. This avoids the requirement to buffer the entire //! display RAM in the host, since such a buffer would consume a colossal (for a μC) 30kiB of RAM. //! +//! If keeping a framebuffer is acceptable, `embedded_graphics::framebuffer::Framebuffer` can be +//! used to store the data and gain `embedded_graphics` drawing support. +//! +//! When the `async` feature is enabled, a separate async API is available with all structs in this +//! crate renamed by appending `Async`: compare [`Display`] and [`DisplayAsync`] etc. +//! //! To use the driver: //! -//! - Use your platform's `embedded-hal` implementation to obtain the necessary I/Os where your -//! SSD1322 display is connected. For example, in 4-wire SPI mode, you will need a configured SPI +//! - Use your platform's hardware implementation to obtain the necessary I/Os where your SSD1322 +//! display is connected. For example, in 4-wire SPI mode, you will need a configured SPI //! master device and one GPIO push-pull output pin device. //! -//! - Construct a `DisplayInterface`, for example an `SpiInterface`, which will take ownership of -//! the I/Os you just obtained. +//! - Construct a [`display_interface::WriteOnlyDataCommand`] implementation, for example +//! `display_interface_spi::SPIInterface`, which will take ownership of the I/Os you just +//! obtained. //! -//! - Construct a `Display`, which will take ownership of the `DisplayInterface` along with the +//! - Construct a [`Display`], which will take ownership of the display interface along with the //! display resolution and offset parameters. //! //! - Referring to your display module's datasheet, create a `Config` to set the various parameters //! in the chip appropriately for the OLEDs in your display module, and send it to the display -//! with `Display::init`. +//! with [`Display::init`]. //! -//! - To draw, call `Display::region` or `Display::overscanned_region` to obtain a region instance -//! for the rectangular area where you want to write image information. Use the `draw_packed` or -//! `draw` methods of the region to write image data supplied by an iterator. The region is -//! intended to be short-lived and will mutably borrow the display, so the compiler will prevent -//! accidental clashing writes. +//! - To draw, call [`Display::region`] or [`Display::overscanned_region`] to obtain a region +//! instance for the rectangular area where you want to write image information. Use the +//! `draw_packed` or `draw` methods of the region to write image data supplied by an iterator. +//! The region is intended to be short-lived and will mutably borrow the display, so the compiler +//! will prevent accidental clashing writes. //! //! - Other functions of the device, such as sleep mode, vertical pan, and contrast control, are -//! available via methods on `Display`. +//! available via methods on [`Display`]. //! //! Example code is available in the `examples` folder. From f096ba70b628b69ee51767d98a7ba61f1fba421c Mon Sep 17 00:00:00 2001 From: s-ol Date: Sat, 13 Dec 2025 13:04:57 +0100 Subject: [PATCH 5/8] display-interface: add nrf52-graphics-embassy example --- .gitignore | 1 + embedded-examples/init_stm32f30x.rs | 117 ---------------- .../nrf52-graphics-embassy/.cargo/config.toml | 6 + .../nrf52-graphics-embassy/Cargo.toml | 28 ++++ .../nrf52-graphics-embassy/build.rs | 34 +++++ .../nrf52-graphics-embassy/memory.x | 11 ++ .../nrf52-graphics-embassy/src/main.rs | 130 ++++++++++++++++++ 7 files changed, 210 insertions(+), 117 deletions(-) delete mode 100644 embedded-examples/init_stm32f30x.rs create mode 100644 embedded-examples/nrf52-graphics-embassy/.cargo/config.toml create mode 100644 embedded-examples/nrf52-graphics-embassy/Cargo.toml create mode 100644 embedded-examples/nrf52-graphics-embassy/build.rs create mode 100644 embedded-examples/nrf52-graphics-embassy/memory.x create mode 100644 embedded-examples/nrf52-graphics-embassy/src/main.rs diff --git a/.gitignore b/.gitignore index 6a6ed9a..ad62af7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +embedded-examples/*/target **/*.rs.bk Cargo.lock diff --git a/embedded-examples/init_stm32f30x.rs b/embedded-examples/init_stm32f30x.rs deleted file mode 100644 index c20237e..0000000 --- a/embedded-examples/init_stm32f30x.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Full example code for setting up an SSD1322 display. This runs on an STM32F303RE, using a -//! Newhaven Displays NHD-3.12-25664UCY2 connected to SPI1, PA8 for C/S, and PA9 for /RESET. - -#![deny(unsafe_code)] -#![no_main] -#![no_std] - -extern crate cortex_m; -extern crate embedded_hal as hal_api; -extern crate stm32f30x; -extern crate stm32f30x_hal as hal; -#[macro_use] -extern crate cortex_m_rt; -extern crate panic_abort; -extern crate ssd1322; - -use core::iter; -use cortex_m::asm; -use cortex_m_rt::ExceptionFrame; -use hal::prelude::*; -use hal::spi; -use ssd1322 as oled; - -entry!(main); - -exception!(*, default_handler); -exception!(HardFault, hard_fault); - -fn hard_fault(_ef: &ExceptionFrame) -> ! { - asm::bkpt(); - loop {} -} - -fn default_handler(_irqn: i16) { - loop {} -} - -fn main() -> ! { - // Get peripherals and set up RCC. - let cp = cortex_m::Peripherals::take().unwrap(); - let dp = stm32f30x::Peripherals::take().unwrap(); - - let mut flash = dp.FLASH.constrain(); - let mut rcc = dp.RCC.constrain(); - let clocks = rcc.cfgr.freeze(&mut flash.acr); - let mut delay = hal::delay::Delay::new(cp.SYST, clocks); - - // Get GPIO A where the display is connected. - let mut gpioa = dp.GPIOA.split(&mut rcc.ahb); - - // Set up SPI1, which is Alternate Function 5 for GPIOs PA5,6,7. - let disp_sck = gpioa.pa5.into_af5(&mut gpioa.moder, &mut gpioa.afrl); - let disp_miso = gpioa.pa6.into_af5(&mut gpioa.moder, &mut gpioa.afrl); - let disp_mosi = gpioa.pa7.into_af5(&mut gpioa.moder, &mut gpioa.afrl); - - let disp_spi = spi::Spi::spi1( - dp.SPI1, - (disp_sck, disp_miso, disp_mosi), - hal_api::spi::Mode { - polarity: hal_api::spi::Polarity::IdleLow, - phase: hal_api::spi::Phase::CaptureOnFirstTransition, - }, - 8.mhz(), - clocks, - &mut rcc.apb2, - ); - - // PA8 will be the D/C push-pull output for the 4th wire. - let disp_dc = gpioa - .pa8 - .into_push_pull_output(&mut gpioa.moder, &mut gpioa.otyper); - - // PA9 is the display's /RESET pin. The ssd1322 library does not control this pin; we will - // assert reset separately. - let mut disp_rst = gpioa - .pa9 - .into_push_pull_output(&mut gpioa.moder, &mut gpioa.otyper); - - // Create the SpiInterface and Display. - let mut disp = oled::Display::new( - oled::SpiInterface::new(disp_spi, disp_dc), - oled::PixelCoord(256, 64), - oled::PixelCoord(112, 0), - ); - - // Assert the display's /RESET for 10ms. - disp_rst.set_low(); - delay.delay_ms(10_u16); - disp_rst.set_high(); - - // Initialize the display. These parameters are taken from the Newhaven datasheet for the - // NHD-3.12-25664UCY2. - disp.init( - oled::Config::new( - oled::ComScanDirection::RowZeroLast, - oled::ComLayout::DualProgressive, - ).clock_fosc_divset(9, 1) - .display_enhancements(true, true) - .contrast_current(159) - .phase_lengths(5, 14) - .precharge_voltage(31) - .second_precharge_period(8) - .com_deselect_voltage(7), - ).unwrap(); - - // Get a region covering the entire display area, and clear it by writing all zeros. - { - let mut region = disp - .region(oled::PixelCoord(0, 0), oled::PixelCoord(256, 128)) - .unwrap(); - region.draw_packed(iter::repeat(0)).unwrap(); - } - - loop { - asm::wfi(); - } -} diff --git a/embedded-examples/nrf52-graphics-embassy/.cargo/config.toml b/embedded-examples/nrf52-graphics-embassy/.cargo/config.toml new file mode 100644 index 0000000..ed9e304 --- /dev/null +++ b/embedded-examples/nrf52-graphics-embassy/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +# replace nRF82810_xxAA with your chip as listed in `probe-rs chip list` +runner = "probe-rs run --chip nRF52840_xxAA" + +[build] +target = "thumbv7em-none-eabi" diff --git a/embedded-examples/nrf52-graphics-embassy/Cargo.toml b/embedded-examples/nrf52-graphics-embassy/Cargo.toml new file mode 100644 index 0000000..b649468 --- /dev/null +++ b/embedded-examples/nrf52-graphics-embassy/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ssd1322-examples" +license = "MIT OR Apache-2.0" +edition = "2018" +publish = false + +[dependencies] +embassy-sync = "0.7.2" +embassy-time = "0.5" +embassy-executor = { version = "0.9.0", features = ["arch-cortex-m", "executor-thread", "executor-interrupt"] } +embassy-nrf = { version = "0.8.0", features = ["nrf52840", "time-driver-rtc1", "time", "nfc-pins-as-gpio"] } + +ssd1322 = { path = "../.." } + +embedded-hal = "1.0" +embedded-hal-bus = { version = "0.3", features = ["async"] } +embedded-graphics = "0.8" + +cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] } +cortex-m-rt = "0.7.0" +panic-probe = { version = "1.0.0" } +display-interface-spi = "0.5.0" + +[profile.release] +debug = 2 + +[package.metadata.embassy] +build = [{ target = "thumbv7em-none-eabi" }] diff --git a/embedded-examples/nrf52-graphics-embassy/build.rs b/embedded-examples/nrf52-graphics-embassy/build.rs new file mode 100644 index 0000000..cd1a264 --- /dev/null +++ b/embedded-examples/nrf52-graphics-embassy/build.rs @@ -0,0 +1,34 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); + + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); +} diff --git a/embedded-examples/nrf52-graphics-embassy/memory.x b/embedded-examples/nrf52-graphics-embassy/memory.x new file mode 100644 index 0000000..51bc398 --- /dev/null +++ b/embedded-examples/nrf52-graphics-embassy/memory.x @@ -0,0 +1,11 @@ +MEMORY +{ + + FLASH (rx) : ORIGIN = 0x26000 , LENGTH = 0xED000 - 0x26000 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 + + /* + FLASH : ORIGIN = 0x00000000, LENGTH = 256K + RAM : ORIGIN = 0x20000000, LENGTH = 24K + */ +} diff --git a/embedded-examples/nrf52-graphics-embassy/src/main.rs b/embedded-examples/nrf52-graphics-embassy/src/main.rs new file mode 100644 index 0000000..c45c6aa --- /dev/null +++ b/embedded-examples/nrf52-graphics-embassy/src/main.rs @@ -0,0 +1,130 @@ +//! Full example code for setting up an SSD1322 display and drawing to it using +//! `embedded_graphics`. This runs on an NRF52840, using a Newhaven Displays NHD-2.7-12864WD*. + +#![no_std] +#![no_main] + +use embassy_executor::Spawner; +use embassy_nrf::gpio::{Level, Output, OutputDrive}; +use embassy_nrf::{bind_interrupts, peripherals, spim}; +use embassy_time::Timer; +use embedded_hal_bus::spi as spi_bus; + +use display_interface_spi::SPIInterface; +use embedded_graphics::{ + framebuffer::{buffer_size, Framebuffer}, + pixelcolor::{raw::LittleEndian, Gray4}, + prelude::*, + primitives::{Line, PrimitiveStyle}, +}; +use ssd1322 as oled; + +use panic_probe as _; + +bind_interrupts!(struct Irqs { + SPIM3 => spim::InterruptHandler; +}); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let p = embassy_nrf::init(Default::default()); + + let pin_rst = p.P0_05; + let pin_sck = p.P1_12; + let pin_sdo = p.P1_14; + let pin_cs = p.P1_15; + let pin_dc = p.P1_13; + let spi_instance = p.SPI3; + + // My dev board has a switchable power regulator, turn that on + Output::new(p.P0_21, Level::High, OutputDrive::Standard).persist(); + + // Assert the display's /RESET. + let mut out_rst = Output::new(pin_rst, Level::Low, OutputDrive::Standard); + Timer::after_millis(2).await; + out_rst.set_high(); + Timer::after_millis(2).await; + + // Set up the push-pull outputs for CS and D/C signals. + let out_cs = Output::new(pin_cs, Level::High, OutputDrive::Standard); + let out_dc = Output::new(pin_dc, Level::High, OutputDrive::Standard); + + // Set up the SPI master interface. + let mut spi_config = spim::Config::default(); + spi_config.frequency = spim::Frequency::M8; + let spi_bus = spim::Spim::new_txonly(spi_instance, Irqs, pin_sck, pin_sdo, spi_config); + let spi_dev = spi_bus::ExclusiveDevice::new(spi_bus, out_cs, embassy_time::Delay).unwrap(); + + // Wrap all I/O in the `display-interface` SPI implementation. + // This is what the `ssd1322` crate interacts with. + let spi_iface = SPIInterface::new(spi_dev, out_dc); + + // Create the Display instance. + let mut display = oled::DisplayAsync::new( + spi_iface, + oled::PixelCoord(256, 64), // double width because of duplicate pixels, see below + oled::PixelCoord(56 * 2, 0), + ); + + display + .init( + oled::ConfigAsync::new( + oled::ComScanDirection::RowZeroLast, + oled::ComLayout::Progressive, + ) + .column_remap(oled::command::ColumnRemap::Reverse) + .clock_fosc_divset(9, 1) + .display_enhancements(true, true) + .contrast_current(0x7f) + .phase_lengths(5, 15) + .precharge_voltage(0x1f) + .com_deselect_voltage(0x04), + ) + .await + .unwrap(); + + let mut fb = + Framebuffer::(128, 64) }>::new(); + + let mut i = 0; + let mut shade = 0; + loop { + // fb.clear(Gray4::BLACK).unwrap(); + + if i > 128 + 64 { + i = (i + 1) % 8; + shade = (shade + 1) % 16; + } + + let point = if i < 128 { + Point::new(i, 0) + } else { + Point::new(127, i - 128) + }; + + Line::new(Point::new(0, 64), point) + .into_styled(PrimitiveStyle::with_stroke(Gray4::new(16 - shade), 1)) + .draw(&mut fb) + .unwrap(); + + i += 8; + + // the NHD-2.7-12864WD is a little odd in that each visible pixel is driven as two + // consecutive virtual pixels. This duplicates each nibble to account for that. + let pixels = fb.data().iter().flat_map(|n| { + let upper = n & 0xf0; + let lower = n & 0x0f; + [upper | (upper >> 4), lower | (lower << 4)] + }); + + // send framebuffer to display: get a region and send the packed pixel data. + display + .region(oled::PixelCoord(0, 0), oled::PixelCoord(256, 64)) + .unwrap() + .draw_packed(pixels) + .await + .unwrap(); + + Timer::after_millis(1).await; + } +} From f3ccfddde2f31d8f442b3d1f5482ec51f2b67e64 Mon Sep 17 00:00:00 2001 From: s-ol Date: Mon, 15 Dec 2025 10:53:13 +0100 Subject: [PATCH 6/8] Align imports - Single use statement per crate/cfg --- src/display/mod.rs | 14 +++++++++----- src/display/overscanned_region.rs | 4 +--- src/display/region.rs | 6 ++++-- src/lib.rs | 13 ++++++------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/display/mod.rs b/src/display/mod.rs index 1db5ac4..a4c6bf4 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -17,11 +17,15 @@ pub mod testing { pub mod overscanned_region; pub mod region; -use crate::command::consts::*; -use crate::command::*; -use crate::config::*; -use crate::display::overscanned_region::*; -use crate::display::region::*; +use crate::{ + command::consts::*, command::*, config::Config, display::overscanned_region::OverscannedRegion, + display::region::Region, +}; +#[cfg(feature = "async")] +use crate::{ + config::ConfigAsync, display::overscanned_region::OverscannedRegionAsync, + display::region::RegionAsync, +}; #[cfg(feature = "async")] use display_interface::AsyncWriteOnlyDataCommand; diff --git a/src/display/overscanned_region.rs b/src/display/overscanned_region.rs index 46c06df..77cdea1 100644 --- a/src/display/overscanned_region.rs +++ b/src/display/overscanned_region.rs @@ -5,9 +5,7 @@ use itertools::iproduct; -use crate::command::consts::*; -use crate::display::region::*; -use crate::display::PixelCoord; +use crate::{command::consts::*, display::region::*, display::PixelCoord}; #[cfg(feature = "async")] use display_interface::AsyncWriteOnlyDataCommand; diff --git a/src/display/region.rs b/src/display/region.rs index e753d5c..a6b53de 100644 --- a/src/display/region.rs +++ b/src/display/region.rs @@ -1,9 +1,11 @@ //! Region abstraction for drawing into rectangular regions of the display. -use crate::command::{BufCommand, Command, CommandError}; #[cfg(feature = "async")] use crate::command::{BufCommandAsync, CommandAsync}; -use crate::display::PixelCoord; +use crate::{ + command::{BufCommand, Command, CommandError}, + display::PixelCoord, +}; #[cfg(feature = "async")] use display_interface::AsyncWriteOnlyDataCommand; diff --git a/src/lib.rs b/src/lib.rs index f89f944..c38d713 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,12 +50,11 @@ pub mod config; pub mod display; // Re-exports for primary API. -pub use crate::command::{consts, ComLayout, ComScanDirection}; -pub use crate::config::Config; -pub use crate::display::{Display, PixelCoord}; +pub use crate::{ + command::{consts, ComLayout, ComScanDirection}, + config::Config, + display::{Display, PixelCoord}, +}; #[cfg(feature = "async")] -pub use crate::config::ConfigAsync; - -#[cfg(feature = "async")] -pub use crate::display::DisplayAsync; +pub use crate::{config::ConfigAsync, display::DisplayAsync}; From e05d0542ce1d0ac8af5ff14492d3bb4d739f194f Mon Sep 17 00:00:00 2001 From: s-ol Date: Sat, 28 Mar 2026 17:12:01 +0100 Subject: [PATCH 7/8] display-interface: add back TestSpyInterface --- Cargo.toml | 2 +- src/command.rs | 3 +- src/display/mod.rs | 16 +------ src/display/overscanned_region.rs | 3 +- src/display/region.rs | 3 +- src/lib.rs | 6 +++ src/testing.rs | 80 +++++++++++++++++++++++++++++++ 7 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 src/testing.rs diff --git a/Cargo.toml b/Cargo.toml index c144299..524d3ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ exclude = [ ] [dependencies] -display-interface = "0.5" +display-interface = { git = "https://github.com/therealprof/display-interface.git", commit = "9b915f93e8665f56b47f11d4fe28c92c1db514bb" } # 0.5 maybe-async-cfg = "0.2" [dependencies.itertools] diff --git a/src/command.rs b/src/command.rs index cda3f74..649779c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -469,8 +469,9 @@ impl<'a> BufCommand<'a> { #[cfg(test)] mod tests { + extern crate std; use super::*; - use crate::interface::test_spy::TestSpyInterface; + use crate::testing::TestSpyInterface; use std::vec::Vec; #[test] diff --git a/src/display/mod.rs b/src/display/mod.rs index a4c6bf4..e177bf8 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -1,19 +1,6 @@ //! The main API to the display driver. It provides a builder API to configure the display, and //! methods for obtaining `Region` instances which can be used to write image data to the display. -// This has to be here in order to be usable by mods declared afterwards. -#[cfg(test)] -#[macro_use] -pub mod testing { - macro_rules! send { - ([$($d:tt),*]) => {Sent::Data(vec![$($d,)*])}; - ($c:tt) => {Sent::Cmd($c)}; - } - macro_rules! sends { - ($($e:tt),*) => {&[$(send!($e),)*]}; - } -} - pub mod overscanned_region; pub mod region; @@ -238,7 +225,8 @@ where #[cfg(test)] mod tests { use super::{PixelCoord as Px, *}; - use interface::test_spy::{Sent, TestSpyInterface}; + use crate::sends; + use crate::testing::TestSpyInterface; #[test] fn init_defaults() { diff --git a/src/display/overscanned_region.rs b/src/display/overscanned_region.rs index 77cdea1..7195a28 100644 --- a/src/display/overscanned_region.rs +++ b/src/display/overscanned_region.rs @@ -144,7 +144,8 @@ mod tests { use crate::command::{ComLayout, ComScanDirection}; use crate::config::Config; use crate::display::{Display, PixelCoord as Px}; - use crate::interface::test_spy::{Sent, TestSpyInterface}; + use crate::sends; + use crate::testing::TestSpyInterface; #[test] fn draw_packed_interior() { diff --git a/src/display/region.rs b/src/display/region.rs index a6b53de..d1df724 100644 --- a/src/display/region.rs +++ b/src/display/region.rs @@ -128,7 +128,8 @@ mod tests { use crate::command::{ComLayout, ComScanDirection}; use crate::config::Config; use crate::display::{Display, PixelCoord as Px}; - use crate::interface::test_spy::{Sent, TestSpyInterface}; + use crate::sends; + use crate::testing::TestSpyInterface; #[test] fn draw_packed() { diff --git a/src/lib.rs b/src/lib.rs index c38d713..82732d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,12 @@ pub mod command; pub mod config; pub mod display; +#[cfg(test)] +extern crate std; + +#[cfg(test)] +pub mod testing; + // Re-exports for primary API. pub use crate::{ command::{consts, ComLayout, ComScanDirection}, diff --git a/src/testing.rs b/src/testing.rs new file mode 100644 index 0000000..be38476 --- /dev/null +++ b/src/testing.rs @@ -0,0 +1,80 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::vec::Vec; + +use display_interface::{DataFormat, DisplayError, WriteOnlyDataCommand}; + +#[macro_export] +macro_rules! send { + ([$($d:tt),*]) => {$crate::testing::Sent::Data(std::vec![$($d,)*])}; + ($c:tt) => {$crate::testing::Sent::Cmd($c)}; + } + +#[macro_export] +macro_rules! sends { + ($($e:tt),*) => {&[$($crate::send!($e),)*]}; + } + +#[derive(Clone, Debug, PartialEq)] +pub enum Sent { + Cmd(u8), + Data(Vec), +} + +pub struct TestSpyInterface { + sent: Rc>>, +} + +impl TestSpyInterface { + pub fn new() -> Self { + TestSpyInterface { + sent: Rc::new(RefCell::new(Vec::new())), + } + } + + pub fn split(&self) -> Self { + Self { + sent: self.sent.clone(), + } + } + + pub fn check(&self, cmd: u8, data: &[u8]) { + let sent = self.sent.borrow(); + if data.len() == 0 { + assert_eq!(sent.len(), 1); + } else { + assert_eq!(sent.len(), 2); + assert_eq!(sent[1], Sent::Data(data.to_vec())); + } + assert_eq!(sent[0], Sent::Cmd(cmd)); + } + + pub fn check_multi(&self, expect: &[Sent]) { + assert_eq!(*self.sent.borrow(), expect); + } + + pub fn clear(&mut self) { + self.sent.borrow_mut().clear() + } +} + +impl WriteOnlyDataCommand for TestSpyInterface { + fn send_commands(&mut self, cmd: DataFormat<'_>) -> Result<(), DisplayError> { + let mut commands = match cmd { + DataFormat::U8(data) => data.iter().map(|c| Sent::Cmd(*c)).collect(), + DataFormat::U8Iter(data) => data.map(Sent::Cmd).collect(), + _ => todo!(), + }; + self.sent.borrow_mut().append(&mut commands); + Ok(()) + } + + fn send_data(&mut self, buf: DataFormat<'_>) -> Result<(), DisplayError> { + self.sent.borrow_mut().push(match buf { + DataFormat::U8(data) => Sent::Data(data.to_vec()), + DataFormat::U8Iter(data) => Sent::Data(data.collect()), + _ => todo!(), + }); + Ok(()) + } +} From 1e9561fa484348c16d1d0bf66f26bd8c5ae49412 Mon Sep 17 00:00:00 2001 From: s-ol Date: Sat, 28 Mar 2026 17:19:12 +0100 Subject: [PATCH 8/8] update tests (init command order, grayscale table) --- src/display/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/display/mod.rs b/src/display/mod.rs index e177bf8..fcea3d9 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -238,10 +238,11 @@ mod tests { di.check_multi(sends!( 0xAE, // sleep enable 0xA4, // display blank + 0xB9, // default grayscale table + 0xA0, [0b00010100, 0b00010001], // remapping 0xCA, [63], // mux ratio 64 lines 0xA2, [0], // display offset 0 0xA1, [0], // start line 0 - 0xA0, [0b00010100, 0b00010001], // remapping 0xAF, // sleep disable 0xA6 // display normal )); @@ -264,6 +265,8 @@ mod tests { di.check_multi(sends!( 0xAE, // sleep enable 0xA4, // display blank + 0xB9, // default grayscale table + 0xA0, [0b00010100, 0b00010001], // remapping 0xB1, [0xE2], // phase lengths 0xC1, [160], // contrast current 0xB3, [0x70], // clock @@ -274,7 +277,6 @@ mod tests { 0xCA, [127], // mux ratio 128 lines 0xA2, [0], // display offset 0 0xA1, [0], // start line 0 - 0xA0, [0b00010100, 0b00010001], // remapping 0xAF, // sleep disable 0xA6 // display normal )); @@ -290,10 +292,11 @@ mod tests { di.check_multi(sends!( 0xAE, // sleep enable 0xA4, // display blank + 0xB9, // default grayscale table + 0xA0, [0b00010100, 0b00010001], // remapping 0xCA, [63], // mux ratio 64 lines 0xA2, [32], // display offset 32 0xA1, [0], // start line 0 - 0xA0, [0b00010100, 0b00010001], // remapping 0xAF, // sleep disable 0xA6 // display normal ));