diff --git a/Cargo.toml b/Cargo.toml index ddf48f4..30685a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Wesley Moore "] [dependencies] +libm = "0.1.2" [dependencies.embedded-hal] features = ["unproven"] @@ -13,10 +14,19 @@ version = "0.2.2" optional = true version = "0.4.4" -[dev-dependencies] -linux-embedded-hal = "0.2.1" # for examples -profont = "0.1" +[dependencies.linux-embedded-hal] +optional = true +version = "0.2.1" + +[dependencies.profont] +optional = true +version = "0.1" [features] default = ["graphics"] graphics = ["embedded-graphics"] +examples = ["linux-embedded-hal", "profont"] + +[[example]] +name = "raspberry_pi_inky_phat" +required-features = ["examples"] diff --git a/README.md b/README.md index ac44482..717bcf4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SSD1675 EPD display driver +# SSD1675 ePaper Display Driver Rust driver for the [Solomon Systech SSD1675][SSD1675] e-Paper display (EPD) controller, for use with [embedded-hal]. @@ -8,7 +8,6 @@ controller, for use with [embedded-hal]. Photo of Inky pHAT ePaper display on Raspberry Pi Zero W - ## Description This driver is intended to work on embedded platforms using the `embedded-hal` @@ -21,6 +20,27 @@ The library has been tested and confirmed working on these devices: * Red/Black/White [Inky pHAT] version 2 on Raspberry Pi Zero (pictured above) +## Examples + +**Note:** To build the examples the `examples` feature needs to be enabled. E.g. + + cargo build --release --examples --features examples + +### Raspberry Pi with Inky pHAT + +The [Raspberry Pi Inky pHAT +example](https://github.com/wezm/ssd1675/blob/master/examples/raspberry_pi_inky_phat.rs), +shows how to display information on an [Inky pHAT] using this crate. The photo +at the top of the page shows this example in action. To avoid the need to +compile on the Raspberry Pi itself I recommend cross-compiling with the [cross] +tool. With `cross` installed build the example as follows: + + cross build --target=arm-unknown-linux-gnueabi --release --example raspberry_pi_inky_phat --features examples + +After it is built copy +`target/arm-unknown-linux-gnueabi/release/examples/raspberry_pi_inky_phat` to +the Raspberry Pi. + ## Credits * [Waveshare EPD driver](https://github.com/caemor/epd-waveshare) @@ -36,9 +56,10 @@ The library has been tested and confirmed working on these devices: http://www.apache.org/licenses/LICENSE-2.0) - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) -[SSD1675]: http://www.solomon-systech.com/en/product/advanced-display/bistable-display-driver-ic/SSD1675/ -[embedded-hal]: https://crates.io/crates/embedded-hal -[Inky pHat]: https://shop.pimoroni.com/products/inky-phat [crate-docs]: https://docs.rs/ssd1675 -[LICENSE-MIT]: https://github.com/wezm/ssd1675/blob/master/LICENSE-MIT +[cross]: https://github.com/rust-embedded/cross +[embedded-hal]: https://crates.io/crates/embedded-hal +[Inky pHAT]: https://shop.pimoroni.com/products/inky-phat [LICENSE-APACHE]: https://github.com/wezm/ssd1675/blob/master/LICENSE-APACHE +[LICENSE-MIT]: https://github.com/wezm/ssd1675/blob/master/LICENSE-MIT +[SSD1675]: http://www.solomon-systech.com/en/product/advanced-display/bistable-display-driver-ic/SSD1675/ diff --git a/src/color.rs b/src/color.rs index 70451d1..83d13c2 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,3 +1,4 @@ +/// Represents the state of a pixel in the display #[derive(Clone, Copy, PartialEq, Debug)] pub enum Color { Black, diff --git a/src/command.rs b/src/command.rs index 4e99b2d..155cb51 100644 --- a/src/command.rs +++ b/src/command.rs @@ -53,6 +53,7 @@ pub enum DeepSleepMode { DiscardRAM, } +/// A command that can be issued to the controller. #[derive(Clone, Copy)] pub enum Command { /// Set the MUX of gate lines, scanning sequence and direction @@ -125,7 +126,7 @@ pub enum Command { // WriteDisplayOption, // WriteUserId, // OTPProgramMode, - /// Set the number dummy line period in terms of gate line width (TGate) + /// Set the number of dummy line period in terms of gate line width (TGate) DummyLinePeriod(u8), /// Set the gate line width (TGate) GateLineWidth(u8), @@ -211,7 +212,8 @@ macro_rules! pack { } impl Command { - pub(crate) fn execute(&self, interface: &mut I) -> Result<(), I::Error> { + /// Execute the command, transmitting any associated data as well. + pub fn execute(&self, interface: &mut I) -> Result<(), I::Error> { use self::Command::*; let mut buf = [0u8; 4]; @@ -304,7 +306,8 @@ impl Command { } impl<'buf> BufCommand<'buf> { - pub(crate) fn execute(&self, interface: &mut I) -> Result<(), I::Error> { + /// Execute the command, transmitting the associated buffer as well. + pub fn execute(&self, interface: &mut I) -> Result<(), I::Error> { use self::BufCommand::*; let (command, data) = match self { diff --git a/src/config.rs b/src/config.rs index 0979b94..85fb7df 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,25 @@ use command::{BufCommand, Command, DataEntryMode, IncrementAxis}; -use display::{Dimensions, Rotation}; +use display::{self, Dimensions, Rotation}; +/// Builder for constructing a display Config. +/// +/// Dimensions must supplied, all other settings will use a default value if not supplied. However +/// it's likely that LUT values will need to be supplied to successfully use a display. +/// +/// ### Example +/// +/// ``` +/// use ssd1675::{Builder, Dimensions, Rotation}; +/// +/// let config = Builder::new() +/// .dimensions(Dimensions { +/// rows: 212, +/// cols: 104, +/// }) +/// .rotation(Rotation::Rotate270) +/// .build() +/// .expect("invalid configuration"); +/// ``` pub struct Builder<'a> { dummy_line_period: Command, gate_line_width: Command, @@ -11,9 +30,15 @@ pub struct Builder<'a> { rotation: Rotation, } +/// Error returned if Builder configuration is invalid. +/// +/// Currently only returned if a configuration is built without dimensions. #[derive(Debug)] pub struct BuilderError {} +/// Display configuration. +/// +/// Passed to Display::new. Use `Builder` to construct a `Config`. pub struct Config<'a> { pub(crate) dummy_line_period: Command, pub(crate) gate_line_width: Command, @@ -42,10 +67,14 @@ impl<'a> Default for Builder<'a> { } impl<'a> Builder<'a> { + /// Create a new Builder. pub fn new() -> Self { Self::default() } + /// Set the number of dummy line period in terms of gate line width (TGate). + /// + /// Defaults to 0x07. Corresponds to command 0x3A. pub fn dummy_line_period(self, dummy_line_period: u8) -> Self { Self { dummy_line_period: Command::DummyLinePeriod(dummy_line_period), @@ -53,6 +82,9 @@ impl<'a> Builder<'a> { } } + /// Set the gate line width (TGate). + /// + /// Defaults to 0x04. Corresponds to command 0x3B. pub fn gate_line_width(self, gate_line_width: u8) -> Self { Self { gate_line_width: Command::GateLineWidth(gate_line_width), @@ -60,6 +92,9 @@ impl<'a> Builder<'a> { } } + /// Set VCOM register value. + /// + /// Defaults to 0x3C. Corresponds to command 0x2C. pub fn vcom(self, value: u8) -> Self { Self { write_vcom: Command::WriteVCOM(value), @@ -67,6 +102,13 @@ impl<'a> Builder<'a> { } } + /// Set lookup table (70 bytes). + /// + /// **Note:** The supplied slice must be exactly 70 bytes long. + /// + /// There is no default for the lookup table. Corresponds to command 0x32. If not supplied then + /// the default in the controller is used. Apparently the display manufacturer will normally + /// supply the LUT values for a particular display batch. pub fn lut(self, lut: &'a [u8]) -> Self { Self { write_lut: Some(BufCommand::WriteLUT(lut)), @@ -74,6 +116,10 @@ impl<'a> Builder<'a> { } } + /// Define data entry sequence. + /// + /// Defaults to DataEntryMode::IncrementAxis, IncrementAxis::Horizontal. Corresponds to command + /// 0x11. pub fn data_entry_mode( self, data_entry_mode: DataEntryMode, @@ -85,17 +131,41 @@ impl<'a> Builder<'a> { } } + /// Set the display dimensions. + /// + /// There is no default for this setting. The dimensions must be set for the builder to + /// successfully build a Config. pub fn dimensions(self, dimensions: Dimensions) -> Self { + assert!( + dimensions.cols % 8 == 0, + "columns must be evenly divisible by 8" + ); + assert!( + dimensions.rows <= display::MAX_GATE_OUTPUTS, + "rows must be less than MAX_GATE_OUTPUTS" + ); + assert!( + dimensions.cols <= display::MAX_SOURCE_OUTPUTS, + "cols must be less than MAX_SOURCE_OUTPUTS" + ); + Self { dimensions: Some(dimensions), ..self } } + /// Set the display rotation. + /// + /// Defaults to no rotation (`Rotation::Rotate0`). Use this to translate between the physical + /// rotation of the display and how the data is displayed on the display. pub fn rotation(self, rotation: Rotation) -> Self { Self { rotation, ..self } } + /// Build the display Config. + /// + /// Will fail if dimensions are not set. pub fn build(self) -> Result, BuilderError> { Ok(Config { dummy_line_period: self.dummy_line_period, diff --git a/src/display.rs b/src/display.rs index 0c3eab9..500362e 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,22 +1,38 @@ +extern crate libm; + use hal; -use command::{BufCommand, Command, DataEntryMode, DeepSleepMode, IncrementAxis}; +use command::{BufCommand, Command, DeepSleepMode}; use config::Config; use interface::DisplayInterface; // Max display resolution is 160x296 -const MAX_SOURCE_OUTPUTS: usize = 160; -const MAX_GATE_OUTPUTS: usize = 296; +/// The maximum number of rows supported by the controller +pub const MAX_GATE_OUTPUTS: u16 = 296; +/// The maximum number of columns supported by the controller +pub const MAX_SOURCE_OUTPUTS: u8 = 160; // Magic numbers from the data sheet const ANALOG_BLOCK_CONTROL_MAGIC: u8 = 0x54; const DIGITAL_BLOCK_CONTROL_MAGIC: u8 = 0x3B; +/// Represents the dimensions of the display. pub struct Dimensions { + /// The number of rows the display has. + /// + /// Must be less than or equal to MAX_GATE_OUTPUTS. pub rows: u16, + /// The number of columns the display has. + /// + /// Must be less than or equal to MAX_SOURCE_OUTPUTS. pub cols: u8, } +/// Represents the physical rotation of the display relative to the native orientation. +/// +/// For example the native orientation of the Inky pHAT display is a tall (portrait) 104x212 +/// display. `Rotate270` can be used to make it the right way up when attached to a Raspberry Pi +/// Zero with the ports on the top. #[derive(Clone, Copy)] pub enum Rotation { Rotate0, @@ -26,11 +42,13 @@ pub enum Rotation { } impl Default for Rotation { + /// Default is no rotation (`Rotate0`). fn default() -> Self { Rotation::Rotate0 } } +/// A configured display with a hardware interface. pub struct Display<'a, I> where I: DisplayInterface, @@ -43,12 +61,16 @@ impl<'a, I> Display<'a, I> where I: DisplayInterface, { + /// Create a new display instance from a DisplayInterface and Config. + /// + /// The `Config` is typically created with `config::Builder`. pub fn new(interface: I, config: Config<'a>) -> Self { - // TODO: Assert dimensions are evenly divisible by 8 Self { interface, config } } - /// Perform a hardware reset followed by software reset + /// Perform a hardware reset followed by software reset. + /// + /// This will wake a controller that has previously entered deep sleep. pub fn reset>( &mut self, delay: &mut D, @@ -79,7 +101,6 @@ where // POR is HiZ. Need pull from config // Command::BorderWaveform(u8).execute(&mut self.interface)?; - // BufCommand::WriteLUT(&LUT_RED).execute(&mut self.interface)?; if let Some(ref write_lut) = self.config.write_lut { write_lut.execute(&mut self.interface)?; } @@ -93,6 +114,10 @@ where Ok(()) } + /// Update the display by writing the supplied B/W and Red buffers to the controller. + /// + /// This method will write the two buffers to the controller then initiate the update + /// display command. Currently it will busy wait until the update has completed. pub fn update>( &mut self, black: &[u8], @@ -100,7 +125,7 @@ where delay: &mut D, ) -> Result<(), I::Error> { // Write the B/W RAM - let buf_limit = ((self.rows() * self.cols() as u16) as f32 / 8.).ceil() as usize; + let buf_limit = libm::ceilf((self.rows() * self.cols() as u16) as f32 / 8.) as usize; Command::XAddress(0).execute(&mut self.interface)?; Command::YAddress(0).execute(&mut self.interface)?; BufCommand::WriteBlackData(&black[..buf_limit]).execute(&mut self.interface)?; @@ -123,18 +148,25 @@ where Ok(()) } + /// Enter deep sleep mode. + /// + /// This puts the display controller into a low power mode. `reset` must be called to wake it + /// from sleep. pub fn deep_sleep(&mut self) -> Result<(), I::Error> { Command::DeepSleepMode(DeepSleepMode::PreserveRAM).execute(&mut self.interface) } + /// Returns the number of rows the display has. pub fn rows(&self) -> u16 { self.config.dimensions.rows } + /// Returns the number of columns the display has. pub fn cols(&self) -> u8 { self.config.dimensions.cols } + /// Returns the rotation the display was configured with. pub fn rotation(&self) -> Rotation { self.config.rotation } diff --git a/src/graphics.rs b/src/graphics.rs index e5de38c..7b1a84e 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -4,6 +4,11 @@ use display::{Display, Rotation}; use hal; use interface::DisplayInterface; +/// A display that holds buffers for drawing into and updating the display from. +/// +/// When the `graphics` feature is enabled `GraphicDisplay` implements the `Draw` trait from +/// [embedded-graphics](https://crates.io/crates/embedded-graphics). This allows basic shapes and +/// text to be drawn on the display. pub struct GraphicDisplay<'a, I> where I: DisplayInterface, @@ -17,6 +22,10 @@ impl<'a, I> GraphicDisplay<'a, I> where I: DisplayInterface, { + /// Promote a `Display` to a `GraphicDisplay`. + /// + /// B/W and Red buffers for drawing into must be supplied. These should be `rows` * `cols` in + /// length. pub fn new( display: Display<'a, I>, black_buffer: &'a mut [u8], @@ -29,6 +38,7 @@ where } } + /// Update the display by writing the buffers to the controller. pub fn update>( &mut self, delay: &mut D, @@ -37,6 +47,7 @@ where .update(self.black_buffer, self.red_buffer, delay) } + /// Clear the buffers, filling them a single color. pub fn clear(&mut self, color: Color) { let (black, red) = match color { Color::White => (0xFF, 0x00), diff --git a/src/interface.rs b/src/interface.rs index beae4b2..ba29acc 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -5,25 +5,91 @@ const RESET_DELAY_MS: u8 = 10; const MAX_SPI_SPEED_HZ: u32 = 20_000_000; +/// Trait implemented by displays to provide implemenation of core functionality. pub trait DisplayInterface { type Error; + /// Send a command to the controller. + /// + /// Prefer calling `execute` on a [Commmand](../command/enum.Command.html) over calling this + /// directly. fn send_command(&mut self, command: u8) -> Result<(), Self::Error>; + + /// Send data for a command. fn send_data(&mut self, data: &[u8]) -> Result<(), Self::Error>; + + /// Reset the controller. fn reset>(&mut self, delay: &mut D); + + /// Wait for the controller to indicate it is not busy. fn busy_wait(&self); } +/// The hardware interface to a display. +/// +/// ### Example +/// +/// This example uses the Linux implementation of the embedded HAL traits to build a display +/// interface. For a complete example see [the Raspberry Pi Inky pHAT example](https://github.com/wezm/ssd1675/blob/master/examples/raspberry_pi_inky_phat.rs). +/// +/// ```ignore +/// extern crate linux_embedded_hal; +/// use linux_embedded_hal::spidev::{self, SpidevOptions}; +/// use linux_embedded_hal::sysfs_gpio::Direction; +/// use linux_embedded_hal::Delay; +/// use linux_embedded_hal::{Pin, Spidev}; +/// +/// extern crate ssd1675; +/// use ssd1675::{Builder, Color, Dimensions, Display, GraphicDisplay, Rotation}; +/// +/// // Configure SPI +/// let mut spi = Spidev::open("/dev/spidev0.0").expect("SPI device"); +/// let options = SpidevOptions::new() +/// .bits_per_word(8) +/// .max_speed_hz(4_000_000) +/// .mode(spidev::SPI_MODE_0) +/// .build(); +/// spi.configure(&options).expect("SPI configuration"); +/// +/// // https://pinout.xyz/pinout/inky_phat +/// // Configure Digital I/O Pins +/// let cs = Pin::new(8); // BCM8 +/// cs.export().expect("cs export"); +/// while !cs.is_exported() {} +/// cs.set_direction(Direction::Out).expect("CS Direction"); +/// cs.set_value(1).expect("CS Value set to 1"); +/// +/// let busy = Pin::new(17); // BCM17 +/// busy.export().expect("busy export"); +/// while !busy.is_exported() {} +/// busy.set_direction(Direction::In).expect("busy Direction"); +/// +/// let dc = Pin::new(22); // BCM22 +/// dc.export().expect("dc export"); +/// while !dc.is_exported() {} +/// dc.set_direction(Direction::Out).expect("dc Direction"); +/// dc.set_value(1).expect("dc Value set to 1"); +/// +/// let reset = Pin::new(27); // BCM27 +/// reset.export().expect("reset export"); +/// while !reset.is_exported() {} +/// reset +/// .set_direction(Direction::Out) +/// .expect("reset Direction"); +/// reset.set_value(1).expect("reset Value set to 1"); +/// +/// // Build the interface from the pins and SPI device +/// let controller = ssd1675::Interface::new(spi, cs, busy, dc, reset); pub struct Interface { - /// SPI + /// SPI interface spi: SPI, - /// CS for SPI + /// CS (chip select) for SPI (output) cs: CS, - /// Low for busy, Wait until display is ready! + /// Active low busy pin (input) busy: BUSY, - /// Data/Command Control Pin (High for data, Low for command) + /// Data/Command Control Pin (High for data, Low for command) (output) dc: DC, - /// Pin for Reseting + /// Pin for reseting the controller (output) reset: RESET, } @@ -35,6 +101,7 @@ where DC: hal::digital::OutputPin, RESET: hal::digital::OutputPin, { + /// Create a new Interface from embedded hal traits. pub fn new(spi: SPI, cs: CS, busy: BUSY, dc: DC, reset: RESET) -> Self { Self { spi, diff --git a/src/lib.rs b/src/lib.rs index 6d31d7a..f366c05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,42 @@ #![no_std] +//! SSD1675 ePaper Display Driver +//! +//! For a complete example see +//! [the Raspberry Pi Inky pHAT example](https://github.com/wezm/ssd1675/blob/master/examples/raspberry_pi_inky_phat.rs). +//! +//! ### Usage +//! +//! To control a display you will need: +//! +//! * An [Interface] to the controller +//! * A [display configuration][Config] +//! * A [Display] +//! +//! The [Interface] captures the details of the hardware connection to the SSD1675 controller. This +//! includes an SPI device and some GPIO pins. The SSD1675 can control many different displays that +//! vary in dimensions, rotation, and driving characteristics. The [Config] captures these details. +//! To aid in constructing the [Config] there is a [Builder] interface. Finally when you have an +//! interface and a [Config] a [Display] instance can be created. +//! +//! Optionally the [Display] can be promoted to a [GraphicDisplay], which allows it to use the +//! functionality from the [embedded-graphics crate][embedded-graphics]. The plain display only +//! provides the ability to update the display by passing black/white and red buffers. +//! +//! To update the display you will typically follow this flow: +//! +//! 1. [reset](display/struct.Display.html#method.reset) +//! 1. [clear](graphics/struct.GraphicDisplay.html#method.clear) +//! 1. [update](graphics/struct.GraphicDisplay.html#method.update) +//! 1. [sleep](display/struct.Display.html#method.deep_sleep) +//! +//! [Interface]: interface/struct.Interface.html +//! [Display]: display/struct.Display.html +//! [GraphicDisplay]: display/struct.GraphicDisplay.html +//! [Config]: config/struct.Config.html +//! [Builder]: config/struct.Builder.html +//! [embedded-graphics]: https://crates.io/crates/embedded-graphics + extern crate embedded_hal as hal; #[cfg(test)] @@ -7,11 +44,11 @@ extern crate embedded_hal as hal; extern crate std; mod color; -mod command; -mod config; -mod display; -mod graphics; -mod interface; +pub mod command; +pub mod config; +pub mod display; +pub mod graphics; +pub mod interface; pub use color::Color; pub use config::Builder;