60 changed files with 5776 additions and 1213 deletions
@ -0,0 +1,8 @@ |
|||||||
|
# If enabled, auto-assigns users when a new issue is created |
||||||
|
# Defaults to true, allows you to install the app globally, and disable on a per-repo basis |
||||||
|
addAssignees: true |
||||||
|
|
||||||
|
# The list of users to assign to new issues. |
||||||
|
# If empty or not provided, the repository owner is assigned |
||||||
|
assignees: |
||||||
|
- caemor |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
# Set to true to add reviewers to pull requests |
||||||
|
addReviewers: true |
||||||
|
|
||||||
|
# Set to true to add assignees to pull requests |
||||||
|
addAssignees: true |
||||||
|
|
||||||
|
# A list of reviewers to be added to pull requests (GitHub user name) |
||||||
|
reviewers: |
||||||
|
- caemor |
||||||
|
|
||||||
|
# A list of keywords to be skipped the process that add reviewers if pull requests include it |
||||||
|
#skipKeywords: |
||||||
|
# - wip |
||||||
|
|
||||||
|
# A number of reviewers added to the pull request |
||||||
|
# Set 0 to add all the reviewers (default: 0) |
||||||
|
numberOfReviewers: 0 |
||||||
@ -1,49 +0,0 @@ |
|||||||
language: rust |
|
||||||
rust: |
|
||||||
- stable |
|
||||||
sudo: required |
|
||||||
env: TARGET=x86_64-unknown-linux-gnu |
|
||||||
|
|
||||||
before_install: |
|
||||||
- set -e |
|
||||||
- rustup self update |
|
||||||
|
|
||||||
# install: |
|
||||||
# - cargo install cargo-update || echo "cargo-update already installed" |
|
||||||
# - cargo install-update -a # update outdated cached binaries |
|
||||||
# - cargo install cross || echo "cross already installed" |
|
||||||
|
|
||||||
#TODO: remove -A clippy::new_ret_no_self when new version of clippy gets released! |
|
||||||
script: |
|
||||||
- cargo check --all-features |
|
||||||
- cargo test --all-features |
|
||||||
|
|
||||||
cache: cargo |
|
||||||
before_cache: |
|
||||||
# Travis can't cache files that are not readable by "others" |
|
||||||
- chmod -R a+r $HOME/.cargo |
|
||||||
- | |
|
||||||
if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then |
|
||||||
cargo install cargo-tarpaulin |
|
||||||
fi |
|
||||||
|
|
||||||
after_success: | |
|
||||||
if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then |
|
||||||
# Uncomment the following line for coveralls.io |
|
||||||
# cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID |
|
||||||
|
|
||||||
# Uncomment the following two lines create and upload a report for codecov.io |
|
||||||
cargo tarpaulin --all-features --out Xml |
|
||||||
bash <(curl -s https://codecov.io/bash) -t "$CODECOV_TOKEN" |
|
||||||
fi |
|
||||||
|
|
||||||
|
|
||||||
branches: |
|
||||||
only: |
|
||||||
# release tags |
|
||||||
- /^v\d+\.\d+\.\d+.*$/ |
|
||||||
- main |
|
||||||
|
|
||||||
notifications: |
|
||||||
email: |
|
||||||
on_success: never |
|
||||||
@ -0,0 +1,167 @@ |
|||||||
|
#![deny(warnings)] |
||||||
|
|
||||||
|
use embedded_graphics::{ |
||||||
|
mono_font::MonoTextStyleBuilder, |
||||||
|
prelude::*, |
||||||
|
primitives::{Circle, Line, PrimitiveStyle}, |
||||||
|
text::{Baseline, Text, TextStyleBuilder}, |
||||||
|
}; |
||||||
|
use embedded_hal::prelude::*; |
||||||
|
use epd_waveshare::{ |
||||||
|
color::*, |
||||||
|
epd2in13bc::{Display2in13bc, Epd2in13bc}, |
||||||
|
graphics::{DisplayRotation, TriDisplay}, |
||||||
|
prelude::*, |
||||||
|
}; |
||||||
|
use linux_embedded_hal::{ |
||||||
|
spidev::{self, SpidevOptions}, |
||||||
|
sysfs_gpio::Direction, |
||||||
|
Delay, Pin, Spidev, |
||||||
|
}; |
||||||
|
|
||||||
|
// activate spi, gpio in raspi-config
|
||||||
|
// needs to be run with sudo because of some sysfs_gpio permission problems and follow-up timing problems
|
||||||
|
// see https://github.com/rust-embedded/rust-sysfs-gpio/issues/5 and follow-up issues
|
||||||
|
//
|
||||||
|
// This example first setups SPI communication using the pin layout found
|
||||||
|
// at https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT_(B). This example uses the layout for the
|
||||||
|
// Raspberry Pi Zero (RPI Zero). The Chip Select (CS) was taken from the ep2in9 example since CE0 (GPIO8) did
|
||||||
|
// not seem to work on RPI Zero with 2.13" HAT
|
||||||
|
//
|
||||||
|
// The first frame is filled with four texts at different rotations (black on white)
|
||||||
|
// The second frame uses a buffer for black/white and a seperate buffer for chromatic/white (i.e. red or yellow)
|
||||||
|
// This example draws a sample clock in black on white and two texts using white on red.
|
||||||
|
//
|
||||||
|
// after finishing, put the display to sleep
|
||||||
|
|
||||||
|
fn main() -> Result<(), std::io::Error> { |
||||||
|
let busy = Pin::new(24); // GPIO 24, board J-18
|
||||||
|
busy.export().expect("busy export"); |
||||||
|
while !busy.is_exported() {} |
||||||
|
busy.set_direction(Direction::In).expect("busy Direction"); |
||||||
|
|
||||||
|
let dc = Pin::new(25); // GPIO 25, board J-22
|
||||||
|
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 rst = Pin::new(17); // GPIO 17, board J-11
|
||||||
|
rst.export().expect("rst export"); |
||||||
|
while !rst.is_exported() {} |
||||||
|
rst.set_direction(Direction::Out).expect("rst Direction"); |
||||||
|
// rst.set_value(1).expect("rst Value set to 1");
|
||||||
|
|
||||||
|
// Configure Digital I/O Pin to be used as Chip Select for SPI
|
||||||
|
let cs = Pin::new(26); // CE0, board J-24, GPIO 8 -> doesn work. use this from 2in19 example which works
|
||||||
|
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"); |
||||||
|
|
||||||
|
// Configure SPI
|
||||||
|
// Settings are taken from
|
||||||
|
let mut spi = Spidev::open("/dev/spidev0.0").expect("spidev directory"); |
||||||
|
let options = SpidevOptions::new() |
||||||
|
.bits_per_word(8) |
||||||
|
.max_speed_hz(10_000_000) |
||||||
|
.mode(spidev::SpiModeFlags::SPI_MODE_0) |
||||||
|
.build(); |
||||||
|
spi.configure(&options).expect("spi configuration"); |
||||||
|
|
||||||
|
let mut delay = Delay {}; |
||||||
|
|
||||||
|
let mut epd2in13 = |
||||||
|
Epd2in13bc::new(&mut spi, cs, busy, dc, rst, &mut delay).expect("eink initalize error"); |
||||||
|
|
||||||
|
println!("Test all the rotations"); |
||||||
|
let mut display = Display2in13bc::default(); |
||||||
|
display.clear_buffer(TriColor::White); |
||||||
|
|
||||||
|
display.set_rotation(DisplayRotation::Rotate0); |
||||||
|
draw_text(&mut display, "Rotation 0!", 5, 50); |
||||||
|
|
||||||
|
display.set_rotation(DisplayRotation::Rotate90); |
||||||
|
draw_text(&mut display, "Rotation 90!", 5, 50); |
||||||
|
|
||||||
|
display.set_rotation(DisplayRotation::Rotate180); |
||||||
|
draw_text(&mut display, "Rotation 180!", 5, 50); |
||||||
|
|
||||||
|
display.set_rotation(DisplayRotation::Rotate270); |
||||||
|
draw_text(&mut display, "Rotation 270!", 5, 50); |
||||||
|
|
||||||
|
// Since we only used black and white, we can resort to updating only
|
||||||
|
// the bw-buffer of this tri-color screen
|
||||||
|
|
||||||
|
epd2in13 |
||||||
|
.update_and_display_frame(&mut spi, display.bw_buffer(), &mut delay) |
||||||
|
.expect("display frame new graphics"); |
||||||
|
|
||||||
|
println!("First frame done. Waiting 5s"); |
||||||
|
delay.delay_ms(5000u16); |
||||||
|
|
||||||
|
println!("Now test new graphics with default rotation and three colors:"); |
||||||
|
display.clear_buffer(TriColor::White); |
||||||
|
|
||||||
|
// draw a analog clock
|
||||||
|
let _ = Circle::with_center(Point::new(64, 64), 80) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(TriColor::Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
let _ = Line::new(Point::new(64, 64), Point::new(30, 40)) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(TriColor::Black, 4)) |
||||||
|
.draw(&mut display); |
||||||
|
let _ = Line::new(Point::new(64, 64), Point::new(80, 40)) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(TriColor::Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
// draw text white on Red background by using the chromatic buffer
|
||||||
|
let style = MonoTextStyleBuilder::new() |
||||||
|
.font(&embedded_graphics::mono_font::ascii::FONT_6X10) |
||||||
|
.text_color(TriColor::White) |
||||||
|
.background_color(TriColor::Chromatic) |
||||||
|
.build(); |
||||||
|
let text_style = TextStyleBuilder::new().baseline(Baseline::Top).build(); |
||||||
|
|
||||||
|
let _ = Text::with_text_style("It's working-WoB!", Point::new(90, 10), style, text_style) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
// use bigger/different font
|
||||||
|
let style = MonoTextStyleBuilder::new() |
||||||
|
.font(&embedded_graphics::mono_font::ascii::FONT_10X20) |
||||||
|
.text_color(TriColor::White) |
||||||
|
.background_color(TriColor::Chromatic) |
||||||
|
.build(); |
||||||
|
|
||||||
|
let _ = Text::with_text_style("It's working\nWoB!", Point::new(90, 40), style, text_style) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
// we used three colors, so we need to update both bw-buffer and chromatic-buffer
|
||||||
|
|
||||||
|
epd2in13.update_color_frame(&mut spi, display.bw_buffer(), display.chromatic_buffer())?; |
||||||
|
epd2in13 |
||||||
|
.display_frame(&mut spi, &mut delay) |
||||||
|
.expect("display frame new graphics"); |
||||||
|
|
||||||
|
println!("Second frame done. Waiting 5s"); |
||||||
|
delay.delay_ms(5000u16); |
||||||
|
|
||||||
|
// clear both bw buffer and chromatic buffer
|
||||||
|
display.clear_buffer(TriColor::White); |
||||||
|
epd2in13.update_color_frame(&mut spi, display.bw_buffer(), display.chromatic_buffer())?; |
||||||
|
epd2in13.display_frame(&mut spi, &mut delay)?; |
||||||
|
|
||||||
|
println!("Finished tests - going to sleep"); |
||||||
|
epd2in13.sleep(&mut spi, &mut delay) |
||||||
|
} |
||||||
|
|
||||||
|
fn draw_text(display: &mut Display2in13bc, text: &str, x: i32, y: i32) { |
||||||
|
let style = MonoTextStyleBuilder::new() |
||||||
|
.font(&embedded_graphics::mono_font::ascii::FONT_6X10) |
||||||
|
.text_color(TriColor::White) |
||||||
|
.background_color(TriColor::Black) |
||||||
|
.build(); |
||||||
|
|
||||||
|
let text_style = TextStyleBuilder::new().baseline(Baseline::Top).build(); |
||||||
|
|
||||||
|
let _ = Text::with_text_style(text, Point::new(x, y), style, text_style).draw(display); |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
//! SPI Commands for the Waveshare 1.54" C yellow E-Ink Display
|
||||||
|
use crate::traits; |
||||||
|
|
||||||
|
#[allow(dead_code)] |
||||||
|
#[derive(Copy, Clone)] |
||||||
|
pub(crate) enum Command { |
||||||
|
PanelSetting = 0x00, |
||||||
|
|
||||||
|
PowerSetting = 0x01, |
||||||
|
PowerOff = 0x02, |
||||||
|
PowerOn = 0x04, |
||||||
|
BoosterSoftStart = 0x06, |
||||||
|
DeepSleep = 0x07, |
||||||
|
DataStartTransmission1 = 0x10, |
||||||
|
DisplayRefresh = 0x12, |
||||||
|
DataStartTransmission2 = 0x13, |
||||||
|
|
||||||
|
LutForVcom = 0x20, |
||||||
|
LutWhiteToWhite = 0x21, |
||||||
|
LutBlackToWhite = 0x22, |
||||||
|
LutWhiteToBlack = 0x23, |
||||||
|
LutBlackToBlack = 0x24, |
||||||
|
|
||||||
|
PllControl = 0x30, |
||||||
|
TemperatureSensor = 0x40, |
||||||
|
TemperatureSensorSelection = 0x41, |
||||||
|
VcomAndDataIntervalSetting = 0x50, |
||||||
|
ResolutionSetting = 0x61, |
||||||
|
VcmDcSetting = 0x82, |
||||||
|
PowerSaving = 0xE3, |
||||||
|
} |
||||||
|
|
||||||
|
impl traits::Command for Command { |
||||||
|
/// Returns the address of the command
|
||||||
|
fn address(self) -> u8 { |
||||||
|
self as u8 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
use crate::epd1in54c::{DEFAULT_BACKGROUND_COLOR, HEIGHT, NUM_DISPLAY_BITS, WIDTH}; |
||||||
|
use crate::graphics::{Display, DisplayRotation}; |
||||||
|
use embedded_graphics_core::pixelcolor::BinaryColor; |
||||||
|
use embedded_graphics_core::prelude::*; |
||||||
|
|
||||||
|
/// Full size buffer for use with the 1in54c EPD
|
||||||
|
///
|
||||||
|
/// Can also be manually constructed and be used together with VarDisplay
|
||||||
|
pub struct Display1in54c { |
||||||
|
buffer: [u8; NUM_DISPLAY_BITS as usize], |
||||||
|
rotation: DisplayRotation, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for Display1in54c { |
||||||
|
fn default() -> Self { |
||||||
|
Display1in54c { |
||||||
|
buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); NUM_DISPLAY_BITS as usize], |
||||||
|
rotation: DisplayRotation::default(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl DrawTarget for Display1in54c { |
||||||
|
type Color = BinaryColor; |
||||||
|
type Error = core::convert::Infallible; |
||||||
|
|
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error> |
||||||
|
where |
||||||
|
I: IntoIterator<Item = Pixel<Self::Color>>, |
||||||
|
{ |
||||||
|
for pixel in pixels { |
||||||
|
self.draw_helper(WIDTH, HEIGHT, pixel)?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OriginDimensions for Display1in54c { |
||||||
|
fn size(&self) -> Size { |
||||||
|
Size::new(WIDTH, HEIGHT) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Display for Display1in54c { |
||||||
|
fn buffer(&self) -> &[u8] { |
||||||
|
&self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn get_mut_buffer(&mut self) -> &mut [u8] { |
||||||
|
&mut self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn set_rotation(&mut self, rotation: DisplayRotation) { |
||||||
|
self.rotation = rotation; |
||||||
|
} |
||||||
|
|
||||||
|
fn rotation(&self) -> DisplayRotation { |
||||||
|
self.rotation |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,300 @@ |
|||||||
|
//! A simple Driver for the Waveshare 1.54" (C) E-Ink Display via SPI
|
||||||
|
|
||||||
|
use embedded_hal::{ |
||||||
|
blocking::{delay::*, spi::Write}, |
||||||
|
digital::v2::*, |
||||||
|
}; |
||||||
|
|
||||||
|
use crate::interface::DisplayInterface; |
||||||
|
use crate::traits::{ |
||||||
|
InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, |
||||||
|
}; |
||||||
|
|
||||||
|
/// Width of epd1in54 in pixels
|
||||||
|
pub const WIDTH: u32 = 152; |
||||||
|
/// Height of epd1in54 in pixels
|
||||||
|
pub const HEIGHT: u32 = 152; |
||||||
|
/// Default Background Color (white)
|
||||||
|
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; |
||||||
|
const IS_BUSY_LOW: bool = true; |
||||||
|
const NUM_DISPLAY_BITS: u32 = WIDTH * HEIGHT / 8; |
||||||
|
|
||||||
|
use crate::color::Color; |
||||||
|
|
||||||
|
pub(crate) mod command; |
||||||
|
use self::command::Command; |
||||||
|
|
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
mod graphics; |
||||||
|
|
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
pub use self::graphics::Display1in54c; |
||||||
|
|
||||||
|
/// Epd1in54c driver
|
||||||
|
pub struct Epd1in54c<SPI, CS, BUSY, DC, RST, DELAY> { |
||||||
|
interface: DisplayInterface<SPI, CS, BUSY, DC, RST, DELAY>, |
||||||
|
color: Color, |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> InternalWiAdditions<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd1in54c<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
// Based on Reference Program Code from:
|
||||||
|
// https://www.waveshare.com/w/upload/a/ac/1.54inch_e-Paper_Module_C_Specification.pdf
|
||||||
|
// and:
|
||||||
|
// https://github.com/waveshare/e-Paper/blob/master/STM32/STM32-F103ZET6/User/e-Paper/EPD_1in54c.c
|
||||||
|
self.interface.reset(delay, 2); |
||||||
|
|
||||||
|
// start the booster
|
||||||
|
self.cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x17])?; |
||||||
|
|
||||||
|
// power on
|
||||||
|
self.command(spi, Command::PowerOn)?; |
||||||
|
delay.delay_ms(5); |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
// set the panel settings
|
||||||
|
self.cmd_with_data(spi, Command::PanelSetting, &[0x0f, 0x0d])?; |
||||||
|
|
||||||
|
// set resolution
|
||||||
|
self.send_resolution(spi)?; |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x77])?; |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> WaveshareThreeColorDisplay<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd1in54c<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn update_color_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
black: &[u8], |
||||||
|
chromatic: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_achromatic_frame(spi, black)?; |
||||||
|
self.update_chromatic_frame(spi, chromatic) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_achromatic_frame(&mut self, spi: &mut SPI, black: &[u8]) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.cmd_with_data(spi, Command::DataStartTransmission1, black)?; |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_chromatic_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
chromatic: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.cmd_with_data(spi, Command::DataStartTransmission2, chromatic)?; |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> WaveshareDisplay<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd1in54c<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
type DisplayColor = Color; |
||||||
|
fn new( |
||||||
|
spi: &mut SPI, |
||||||
|
cs: CS, |
||||||
|
busy: BUSY, |
||||||
|
dc: DC, |
||||||
|
rst: RST, |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<Self, SPI::Error> { |
||||||
|
let interface = DisplayInterface::new(cs, busy, dc, rst); |
||||||
|
let color = DEFAULT_BACKGROUND_COLOR; |
||||||
|
|
||||||
|
let mut epd = Epd1in54c { interface, color }; |
||||||
|
|
||||||
|
epd.init(spi, delay)?; |
||||||
|
|
||||||
|
Ok(epd) |
||||||
|
} |
||||||
|
|
||||||
|
fn sleep(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
self.command(spi, Command::PowerOff)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
self.cmd_with_data(spi, Command::DeepSleep, &[0xa5])?; |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.init(spi, delay) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_background_color(&mut self, color: Color) { |
||||||
|
self.color = color; |
||||||
|
} |
||||||
|
|
||||||
|
fn background_color(&self) -> &Color { |
||||||
|
&self.color |
||||||
|
} |
||||||
|
|
||||||
|
fn width(&self) -> u32 { |
||||||
|
WIDTH |
||||||
|
} |
||||||
|
|
||||||
|
fn height(&self) -> u32 { |
||||||
|
HEIGHT |
||||||
|
} |
||||||
|
|
||||||
|
fn update_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
_delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_achromatic_frame(spi, buffer)?; |
||||||
|
|
||||||
|
// Clear the chromatic layer
|
||||||
|
let color = self.color.get_byte_value(); |
||||||
|
|
||||||
|
self.command(spi, Command::DataStartTransmission2)?; |
||||||
|
self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[allow(unused)] |
||||||
|
fn update_partial_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
unimplemented!() |
||||||
|
} |
||||||
|
|
||||||
|
fn display_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.command(spi, Command::DisplayRefresh)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_and_display_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_frame(spi, buffer, delay)?; |
||||||
|
self.display_frame(spi, delay)?; |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn clear_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
let color = DEFAULT_BACKGROUND_COLOR.get_byte_value(); |
||||||
|
|
||||||
|
// Clear the black
|
||||||
|
self.command(spi, Command::DataStartTransmission1)?; |
||||||
|
self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; |
||||||
|
|
||||||
|
// Clear the chromatic
|
||||||
|
self.command(spi, Command::DataStartTransmission2)?; |
||||||
|
self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_lut( |
||||||
|
&mut self, |
||||||
|
_spi: &mut SPI, |
||||||
|
_refresh_rate: Option<RefreshLut>, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn is_busy(&self) -> bool { |
||||||
|
self.interface.is_busy(IS_BUSY_LOW) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> Epd1in54c<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn command(&mut self, spi: &mut SPI, command: Command) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, command) |
||||||
|
} |
||||||
|
|
||||||
|
fn send_data(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> { |
||||||
|
self.interface.data(spi, data) |
||||||
|
} |
||||||
|
|
||||||
|
fn cmd_with_data( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
command: Command, |
||||||
|
data: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd_with_data(spi, command, data) |
||||||
|
} |
||||||
|
|
||||||
|
fn wait_until_idle(&mut self) { |
||||||
|
let _ = self.interface.wait_until_idle(IS_BUSY_LOW); |
||||||
|
} |
||||||
|
|
||||||
|
fn send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { |
||||||
|
let w = self.width(); |
||||||
|
let h = self.height(); |
||||||
|
|
||||||
|
self.command(spi, Command::ResolutionSetting)?; |
||||||
|
|
||||||
|
// | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
|
||||||
|
// | HRES[7:3] | 0 | 0 | 0 |
|
||||||
|
self.send_data(spi, &[(w as u8) & 0b1111_1000])?; |
||||||
|
// | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
|
||||||
|
// | - | - | - | - | - | - | - | VRES[8] |
|
||||||
|
self.send_data(spi, &[(w >> 8) as u8])?; |
||||||
|
// | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
|
||||||
|
// | VRES[7:0] |
|
||||||
|
// Specification shows C/D is zero while sending the last byte,
|
||||||
|
// but upstream code does not implement it like that. So for now
|
||||||
|
// we follow upstream code.
|
||||||
|
self.send_data(spi, &[h as u8]) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
//! SPI Commands for the Waveshare 2.13" (B/C) E-Ink Display
|
||||||
|
use crate::traits; |
||||||
|
|
||||||
|
#[allow(dead_code)] |
||||||
|
#[derive(Copy, Clone)] |
||||||
|
pub(crate) enum Command { |
||||||
|
PanelSetting = 0x00, |
||||||
|
|
||||||
|
PowerSetting = 0x01, |
||||||
|
PowerOff = 0x02, |
||||||
|
PowerOn = 0x04, |
||||||
|
BoosterSoftStart = 0x06, |
||||||
|
DeepSleep = 0x07, |
||||||
|
DataStartTransmission1 = 0x10, |
||||||
|
DisplayRefresh = 0x12, |
||||||
|
DataStartTransmission2 = 0x13, |
||||||
|
|
||||||
|
LutForVcom = 0x20, |
||||||
|
LutWhiteToWhite = 0x21, |
||||||
|
LutBlackToWhite = 0x22, |
||||||
|
LutWhiteToBlack = 0x23, |
||||||
|
LutBlackToBlack = 0x24, |
||||||
|
|
||||||
|
PllControl = 0x30, |
||||||
|
TemperatureSensor = 0x40, |
||||||
|
TemperatureSensorSelection = 0x41, |
||||||
|
VcomAndDataIntervalSetting = 0x50, |
||||||
|
ResolutionSetting = 0x61, |
||||||
|
VcmDcSetting = 0x82, |
||||||
|
PowerSaving = 0xE3, |
||||||
|
} |
||||||
|
|
||||||
|
impl traits::Command for Command { |
||||||
|
/// Returns the address of the command
|
||||||
|
fn address(self) -> u8 { |
||||||
|
self as u8 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
use crate::color::TriColor; |
||||||
|
use crate::epd2in13bc::{DEFAULT_BACKGROUND_COLOR, HEIGHT, NUM_DISPLAY_BITS, WIDTH}; |
||||||
|
use crate::graphics::{DisplayRotation, TriDisplay}; |
||||||
|
use embedded_graphics_core::prelude::*; |
||||||
|
|
||||||
|
/// Full size buffer for use with the 2.13" b/c EPD
|
||||||
|
///
|
||||||
|
/// Can also be manually constructed and be used together with VarDisplay
|
||||||
|
pub struct Display2in13bc { |
||||||
|
// one buffer for both b/w and for chromatic:
|
||||||
|
// * &buffer[0..NUM_DISPLAY_BITS] for b/w buffer and
|
||||||
|
// * &buffer[NUM_DISPLAY_BITS..2*NUM_DISPLAY_BITS] for chromatic buffer
|
||||||
|
buffer: [u8; 2 * NUM_DISPLAY_BITS as usize], |
||||||
|
rotation: DisplayRotation, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for Display2in13bc { |
||||||
|
fn default() -> Self { |
||||||
|
Display2in13bc { |
||||||
|
buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); 2 * NUM_DISPLAY_BITS as usize], |
||||||
|
rotation: DisplayRotation::default(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl DrawTarget for Display2in13bc { |
||||||
|
type Color = TriColor; |
||||||
|
type Error = core::convert::Infallible; |
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error> |
||||||
|
where |
||||||
|
I: IntoIterator<Item = Pixel<Self::Color>>, |
||||||
|
{ |
||||||
|
for pixel in pixels { |
||||||
|
self.draw_helper_tri(WIDTH, HEIGHT, pixel)?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OriginDimensions for Display2in13bc { |
||||||
|
fn size(&self) -> Size { |
||||||
|
Size::new(WIDTH, HEIGHT) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TriDisplay for Display2in13bc { |
||||||
|
fn buffer(&self) -> &[u8] { |
||||||
|
&self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn get_mut_buffer(&mut self) -> &mut [u8] { |
||||||
|
&mut self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn set_rotation(&mut self, rotation: DisplayRotation) { |
||||||
|
self.rotation = rotation; |
||||||
|
} |
||||||
|
|
||||||
|
fn rotation(&self) -> DisplayRotation { |
||||||
|
self.rotation |
||||||
|
} |
||||||
|
|
||||||
|
fn chromatic_offset(&self) -> usize { |
||||||
|
NUM_DISPLAY_BITS as usize |
||||||
|
} |
||||||
|
|
||||||
|
fn bw_buffer(&self) -> &[u8] { |
||||||
|
&self.buffer[0..self.chromatic_offset()] |
||||||
|
} |
||||||
|
|
||||||
|
fn chromatic_buffer(&self) -> &[u8] { |
||||||
|
&self.buffer[self.chromatic_offset()..] |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,386 @@ |
|||||||
|
//! A simple Driver for the Waveshare 2.13" (B/C) E-Ink Display via SPI
|
||||||
|
//! More information on this display can be found at the [Waveshare Wiki](https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT_(B))
|
||||||
|
//! This driver was build and tested for 212x104, 2.13inch E-Ink display HAT for Raspberry Pi, three-color, SPI interface
|
||||||
|
//!
|
||||||
|
//! # Example for the 2.13" E-Ink Display
|
||||||
|
//!
|
||||||
|
//!```rust, no_run
|
||||||
|
//!# use embedded_hal_mock::*;
|
||||||
|
//!# fn main() -> Result<(), MockError> {
|
||||||
|
//!use embedded_graphics::{prelude::*, primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder}};
|
||||||
|
//!use epd_waveshare::{epd2in13bc::*, prelude::*};
|
||||||
|
//!#
|
||||||
|
//!# let expectations = [];
|
||||||
|
//!# let mut spi = spi::Mock::new(&expectations);
|
||||||
|
//!# let expectations = [];
|
||||||
|
//!# let cs_pin = pin::Mock::new(&expectations);
|
||||||
|
//!# let busy_in = pin::Mock::new(&expectations);
|
||||||
|
//!# let dc = pin::Mock::new(&expectations);
|
||||||
|
//!# let rst = pin::Mock::new(&expectations);
|
||||||
|
//!# let mut delay = delay::MockNoop::new();
|
||||||
|
//!
|
||||||
|
//!// Setup EPD
|
||||||
|
//!let mut epd = Epd2in13bc::new(&mut spi, cs_pin, busy_in, dc, rst, &mut delay)?;
|
||||||
|
//!
|
||||||
|
//!// Use display graphics from embedded-graphics
|
||||||
|
//!// This display is for the black/white/chromatic pixels
|
||||||
|
//!let mut tricolor_display = Display2in13bc::default();
|
||||||
|
//!
|
||||||
|
//!// Use embedded graphics for drawing a black line
|
||||||
|
//!let _ = Line::new(Point::new(0, 120), Point::new(0, 200))
|
||||||
|
//! .into_styled(PrimitiveStyle::with_stroke(TriColor::Black, 1))
|
||||||
|
//! .draw(&mut tricolor_display);
|
||||||
|
//!
|
||||||
|
//!// We use `chromatic` but it will be shown as red/yellow
|
||||||
|
//!let _ = Line::new(Point::new(15, 120), Point::new(15, 200))
|
||||||
|
//! .into_styled(PrimitiveStyle::with_stroke(TriColor::Chromatic, 1))
|
||||||
|
//! .draw(&mut tricolor_display);
|
||||||
|
//!
|
||||||
|
//!// Display updated frame
|
||||||
|
//!epd.update_color_frame(
|
||||||
|
//! &mut spi,
|
||||||
|
//! &tricolor_display.bw_buffer(),
|
||||||
|
//! &tricolor_display.chromatic_buffer()
|
||||||
|
//!)?;
|
||||||
|
//!epd.display_frame(&mut spi, &mut delay)?;
|
||||||
|
//!
|
||||||
|
//!// Set the EPD to sleep
|
||||||
|
//!epd.sleep(&mut spi, &mut delay)?;
|
||||||
|
//!# Ok(())
|
||||||
|
//!# }
|
||||||
|
//!```
|
||||||
|
use embedded_hal::{ |
||||||
|
blocking::{delay::*, spi::Write}, |
||||||
|
digital::v2::*, |
||||||
|
}; |
||||||
|
|
||||||
|
use crate::interface::DisplayInterface; |
||||||
|
use crate::traits::{ |
||||||
|
InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, |
||||||
|
}; |
||||||
|
|
||||||
|
/// Width of epd2in13bc in pixels
|
||||||
|
pub const WIDTH: u32 = 104; |
||||||
|
/// Height of epd2in13bc in pixels
|
||||||
|
pub const HEIGHT: u32 = 212; |
||||||
|
/// Default background color (white) of epd2in13bc display
|
||||||
|
pub const DEFAULT_BACKGROUND_COLOR: TriColor = TriColor::White; |
||||||
|
|
||||||
|
/// Number of bits for b/w buffer and same for chromatic buffer
|
||||||
|
const NUM_DISPLAY_BITS: u32 = WIDTH * HEIGHT / 8; |
||||||
|
|
||||||
|
const IS_BUSY_LOW: bool = true; |
||||||
|
const VCOM_DATA_INTERVAL: u8 = 0x07; |
||||||
|
const WHITE_BORDER: u8 = 0x70; |
||||||
|
const BLACK_BORDER: u8 = 0x30; |
||||||
|
const CHROMATIC_BORDER: u8 = 0xb0; |
||||||
|
const FLOATING_BORDER: u8 = 0xF0; |
||||||
|
|
||||||
|
use crate::color::TriColor; |
||||||
|
|
||||||
|
pub(crate) mod command; |
||||||
|
use self::command::Command; |
||||||
|
|
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
mod graphics; |
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
pub use self::graphics::Display2in13bc; |
||||||
|
|
||||||
|
/// Epd2in13bc driver
|
||||||
|
pub struct Epd2in13bc<SPI, CS, BUSY, DC, RST, DELAY> { |
||||||
|
interface: DisplayInterface<SPI, CS, BUSY, DC, RST, DELAY>, |
||||||
|
color: TriColor, |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> InternalWiAdditions<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd2in13bc<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
// Values taken from datasheet and sample code
|
||||||
|
|
||||||
|
self.interface.reset(delay, 10); |
||||||
|
|
||||||
|
// start the booster
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x17])?; |
||||||
|
|
||||||
|
// power on
|
||||||
|
self.command(spi, Command::PowerOn)?; |
||||||
|
delay.delay_ms(5); |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
// set the panel settings
|
||||||
|
self.cmd_with_data(spi, Command::PanelSetting, &[0x8F])?; |
||||||
|
|
||||||
|
self.cmd_with_data( |
||||||
|
spi, |
||||||
|
Command::VcomAndDataIntervalSetting, |
||||||
|
&[WHITE_BORDER | VCOM_DATA_INTERVAL], |
||||||
|
)?; |
||||||
|
|
||||||
|
// set resolution
|
||||||
|
self.send_resolution(spi)?; |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::VcmDcSetting, &[0x0A])?; |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> WaveshareThreeColorDisplay<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd2in13bc<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn update_color_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
black: &[u8], |
||||||
|
chromatic: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_achromatic_frame(spi, black)?; |
||||||
|
self.update_chromatic_frame(spi, chromatic) |
||||||
|
} |
||||||
|
|
||||||
|
/// Update only the black/white data of the display.
|
||||||
|
///
|
||||||
|
/// Finish by calling `update_chromatic_frame`.
|
||||||
|
fn update_achromatic_frame(&mut self, spi: &mut SPI, black: &[u8]) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission1)?; |
||||||
|
self.interface.data(spi, black)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Update only chromatic data of the display.
|
||||||
|
///
|
||||||
|
/// This data takes precedence over the black/white data.
|
||||||
|
fn update_chromatic_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
chromatic: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission2)?; |
||||||
|
self.interface.data(spi, chromatic)?; |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> WaveshareDisplay<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd2in13bc<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
type DisplayColor = TriColor; |
||||||
|
fn new( |
||||||
|
spi: &mut SPI, |
||||||
|
cs: CS, |
||||||
|
busy: BUSY, |
||||||
|
dc: DC, |
||||||
|
rst: RST, |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<Self, SPI::Error> { |
||||||
|
let interface = DisplayInterface::new(cs, busy, dc, rst); |
||||||
|
let color = DEFAULT_BACKGROUND_COLOR; |
||||||
|
|
||||||
|
let mut epd = Epd2in13bc { interface, color }; |
||||||
|
|
||||||
|
epd.init(spi, delay)?; |
||||||
|
|
||||||
|
Ok(epd) |
||||||
|
} |
||||||
|
|
||||||
|
fn sleep(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
// Section 8.2 from datasheet
|
||||||
|
self.interface.cmd_with_data( |
||||||
|
spi, |
||||||
|
Command::VcomAndDataIntervalSetting, |
||||||
|
&[FLOATING_BORDER | VCOM_DATA_INTERVAL], |
||||||
|
)?; |
||||||
|
|
||||||
|
self.command(spi, Command::PowerOff)?; |
||||||
|
// The example STM code from Github has a wait after PowerOff
|
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.init(spi, delay) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_background_color(&mut self, color: TriColor) { |
||||||
|
self.color = color; |
||||||
|
} |
||||||
|
|
||||||
|
fn background_color(&self) -> &TriColor { |
||||||
|
&self.color |
||||||
|
} |
||||||
|
|
||||||
|
fn width(&self) -> u32 { |
||||||
|
WIDTH |
||||||
|
} |
||||||
|
|
||||||
|
fn height(&self) -> u32 { |
||||||
|
HEIGHT |
||||||
|
} |
||||||
|
|
||||||
|
fn update_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
_delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission1)?; |
||||||
|
|
||||||
|
self.interface.data(spi, buffer)?; |
||||||
|
|
||||||
|
// Clear the chromatic layer
|
||||||
|
let color = self.color.get_byte_value(); |
||||||
|
|
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission2)?; |
||||||
|
self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[allow(unused)] |
||||||
|
fn update_partial_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn display_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.command(spi, Command::DisplayRefresh)?; |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_and_display_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_frame(spi, buffer, delay)?; |
||||||
|
self.display_frame(spi, delay)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn clear_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.send_resolution(spi)?; |
||||||
|
|
||||||
|
let color = DEFAULT_BACKGROUND_COLOR.get_byte_value(); |
||||||
|
|
||||||
|
// Clear the black
|
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission1)?; |
||||||
|
|
||||||
|
self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; |
||||||
|
|
||||||
|
// Clear the chromatic
|
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission2)?; |
||||||
|
self.interface.data_x_times(spi, color, NUM_DISPLAY_BITS)?; |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_lut( |
||||||
|
&mut self, |
||||||
|
_spi: &mut SPI, |
||||||
|
_refresh_rate: Option<RefreshLut>, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn is_busy(&self) -> bool { |
||||||
|
self.interface.is_busy(IS_BUSY_LOW) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> Epd2in13bc<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn command(&mut self, spi: &mut SPI, command: Command) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, command) |
||||||
|
} |
||||||
|
|
||||||
|
fn send_data(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> { |
||||||
|
self.interface.data(spi, data) |
||||||
|
} |
||||||
|
|
||||||
|
fn cmd_with_data( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
command: Command, |
||||||
|
data: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd_with_data(spi, command, data) |
||||||
|
} |
||||||
|
|
||||||
|
fn wait_until_idle(&mut self) { |
||||||
|
let _ = self.interface.wait_until_idle(IS_BUSY_LOW); |
||||||
|
} |
||||||
|
|
||||||
|
fn send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { |
||||||
|
let w = self.width(); |
||||||
|
let h = self.height(); |
||||||
|
|
||||||
|
self.command(spi, Command::ResolutionSetting)?; |
||||||
|
|
||||||
|
self.send_data(spi, &[w as u8])?; |
||||||
|
self.send_data(spi, &[(h >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[h as u8]) |
||||||
|
} |
||||||
|
|
||||||
|
/// Set the outer border of the display to the chosen color.
|
||||||
|
pub fn set_border_color(&mut self, spi: &mut SPI, color: TriColor) -> Result<(), SPI::Error> { |
||||||
|
let border = match color { |
||||||
|
TriColor::Black => BLACK_BORDER, |
||||||
|
TriColor::White => WHITE_BORDER, |
||||||
|
TriColor::Chromatic => CHROMATIC_BORDER, |
||||||
|
}; |
||||||
|
self.cmd_with_data( |
||||||
|
spi, |
||||||
|
Command::VcomAndDataIntervalSetting, |
||||||
|
&[border | VCOM_DATA_INTERVAL], |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,137 @@ |
|||||||
|
//! SPI Commands for the Waveshare 2.7" B 3 color E-Ink Display
|
||||||
|
use crate::traits; |
||||||
|
|
||||||
|
/// EPD2IN7B commands
|
||||||
|
///
|
||||||
|
/// More information can be found in the [specification](https://www.waveshare.com/w/upload/d/d8/2.7inch-e-paper-b-specification.pdf)
|
||||||
|
#[allow(dead_code)] |
||||||
|
#[derive(Copy, Clone)] |
||||||
|
pub(crate) enum Command { |
||||||
|
/// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift direction, booster switch, soft reset
|
||||||
|
PanelSetting = 0x00, |
||||||
|
/// Selecting internal and external power
|
||||||
|
PowerSetting = 0x01, |
||||||
|
PowerOff = 0x02, |
||||||
|
/// Setting Power OFF sequence
|
||||||
|
PowerOffSequenceSetting = 0x03, |
||||||
|
PowerOn = 0x04, |
||||||
|
/// This command enables the internal bandgap, which will be cleared by the next POF.
|
||||||
|
PowerOnMeasure = 0x05, |
||||||
|
/// Starting data transmission
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// self.send_data(&[0x07, 0x07, 0x17])?;
|
||||||
|
/// ```
|
||||||
|
BoosterSoftStart = 0x06, |
||||||
|
/// After this command is transmitted, the chip would enter the deep-sleep mode to save power.
|
||||||
|
///
|
||||||
|
/// The deep sleep mode would return to standby by hardware reset.
|
||||||
|
///
|
||||||
|
/// The only one parameter is a check code, the command would be excuted if check code = 0xA5.
|
||||||
|
DeepSleep = 0x07, |
||||||
|
/// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data
|
||||||
|
/// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel.
|
||||||
|
///
|
||||||
|
/// - In B/W mode, this command writes “OLD” data to SRAM.
|
||||||
|
/// - In B/W/Red mode, this command writes “B/W” data to SRAM.
|
||||||
|
DataStartTransmission1 = 0x10, |
||||||
|
/// Stopping data transmission
|
||||||
|
DataStop = 0x11, |
||||||
|
/// After this command is issued, driver will refresh display (data/VCOM) according to SRAM data and LUT.
|
||||||
|
DisplayRefresh = 0x12, |
||||||
|
/// This command starts transmitting data and write them into SRAM. To complete data transmission, command DSP (Data
|
||||||
|
/// transmission Stop) must be issued. Then the chip will start to send data/VCOM for panel.
|
||||||
|
/// - In B/W mode, this command writes “NEW” data to SRAM.
|
||||||
|
/// - In B/W/Red mode, this command writes “RED” data to SRAM.
|
||||||
|
DataStartTransmission2 = 0x13, |
||||||
|
/// The command define as follows: The register is indicates that user start to transmit data, then write to SRAM. While data transmission
|
||||||
|
/// complete, user must send command DSP (Data transmission Stop). Then chip will start to send data/VCOM for panel.
|
||||||
|
///
|
||||||
|
/// - In B/W mode, this command writes “OLD” data to SRAM.
|
||||||
|
/// - In B/W/Red mode, this command writes “B/W” data to SRAM.
|
||||||
|
PartialDataStartTransmission1 = 0x14, |
||||||
|
/// The command define as follows: The register is indicates that user start to transmit data, then write to SRAM. While data transmission
|
||||||
|
/// complete, user must send command DSP (Data transmission Stop). Then chip will start to send data/VCOM for panel.
|
||||||
|
///
|
||||||
|
/// - In B/W mode, this command writes “NEW” data to SRAM.
|
||||||
|
/// - In B/W/Red mode, this command writes “RED” data to SRAM.
|
||||||
|
PartialDataStartTransmission2 = 0x15, |
||||||
|
/// While user sent this command, driver will refresh display (data/VCOM) base on SRAM data and LUT.
|
||||||
|
///
|
||||||
|
/// Only the area (X,Y, W, L) would update, the others pixel output would follow VCOM LUT
|
||||||
|
PartialDisplayRefresh = 0x16, |
||||||
|
/// This command builds the Look-up table for VCOM
|
||||||
|
LutForVcom = 0x20, |
||||||
|
LutWhiteToWhite = 0x21, |
||||||
|
LutBlackToWhite = 0x22, |
||||||
|
LutWhiteToBlack = 0x23, |
||||||
|
LutBlackToBlack = 0x24, |
||||||
|
/// The command controls the PLL clock frequency.
|
||||||
|
PllControl = 0x30, |
||||||
|
/// This command reads the temperature sensed by the temperature sensor.
|
||||||
|
///
|
||||||
|
/// Doesn't work! Waveshare doesn't connect the read pin
|
||||||
|
TemperatureSensor = 0x40, |
||||||
|
/// This command selects Internal or External temperature sensor.
|
||||||
|
TemperatureSensorCalibration = 0x41, |
||||||
|
/// Write External Temperature Sensor
|
||||||
|
TemperatureSensorWrite = 0x42, |
||||||
|
/// Read External Temperature Sensor
|
||||||
|
///
|
||||||
|
/// Doesn't work! Waveshare doesn't connect the read pin
|
||||||
|
TemperatureSensorRead = 0x43, |
||||||
|
/// This command indicates the interval of Vcom and data output. When setting the vertical back porch, the total blanking will be kept (20 Hsync)
|
||||||
|
VcomAndDataIntervalSetting = 0x50, |
||||||
|
/// This command indicates the input power condition. Host can read this flag to learn the battery condition.
|
||||||
|
LowPowerDetection = 0x51, |
||||||
|
/// This command defines non-overlap period of Gate and Source.
|
||||||
|
TconSetting = 0x60, |
||||||
|
/// This command defines alternative resolution and this setting is of higher priority than the RES\[1:0\] in R00H (PSR).
|
||||||
|
ResolutionSetting = 0x61, |
||||||
|
SourceAndGateSetting = 0x62, |
||||||
|
/// This command reads the IC status.
|
||||||
|
///
|
||||||
|
/// Doesn't work! Waveshare doesn't connect the read pin
|
||||||
|
GetStatus = 0x71, |
||||||
|
/// Automatically measure VCOM. This command reads the IC status
|
||||||
|
AutoMeasurementVcom = 0x80, |
||||||
|
/// This command gets the VCOM value
|
||||||
|
///
|
||||||
|
/// Doesn't work! Waveshare doesn't connect the read pin
|
||||||
|
ReadVcomValue = 0x81, |
||||||
|
/// This command sets VCOM_DC value.
|
||||||
|
VcmDcSetting = 0x82, |
||||||
|
/// After this command is issued, the chip would enter the program mode.
|
||||||
|
///
|
||||||
|
/// After the programming procedure completed, a hardware reset is necessary for leaving program mode.
|
||||||
|
///
|
||||||
|
/// The only one parameter is a check code, the command would be excuted if check code = 0xA5.
|
||||||
|
ProgramMode = 0xA0, |
||||||
|
/// After this command is issued, the chip would enter the program mode.
|
||||||
|
ActiveProgramming = 0xA1, |
||||||
|
/// The command is used for reading the content of OTP for checking the data of programming.
|
||||||
|
///
|
||||||
|
/// The value of (n) is depending on the amount of programmed data, tha max address = 0xFFF.
|
||||||
|
ReadOtp = 0xA2, |
||||||
|
/// Not shown in commands table, but used in init sequence
|
||||||
|
PowerOptimization = 0xf8, |
||||||
|
} |
||||||
|
|
||||||
|
impl traits::Command for Command { |
||||||
|
/// Returns the address of the command
|
||||||
|
fn address(self) -> u8 { |
||||||
|
self as u8 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use crate::traits::Command as CommandTrait; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn command_addr() { |
||||||
|
assert_eq!(Command::PanelSetting.address(), 0x00); |
||||||
|
assert_eq!(Command::DisplayRefresh.address(), 0x12); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,55 @@ |
|||||||
|
#[rustfmt::skip] |
||||||
|
pub(crate) const LUT_VCOM_DC: [u8; 44] = [ |
||||||
|
0x00, 0x00, |
||||||
|
0x00, 0x1A, 0x1A, 0x00, 0x00, 0x01, |
||||||
|
0x00, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0x00, 0x0E, 0x01, 0x0E, 0x01, 0x10, |
||||||
|
0x00, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0x00, 0x04, 0x10, 0x00, 0x00, 0x05, |
||||||
|
0x00, 0x03, 0x0E, 0x00, 0x00, 0x0A, |
||||||
|
0x00, 0x23, 0x00, 0x00, 0x00, 0x01, |
||||||
|
]; |
||||||
|
|
||||||
|
#[rustfmt::skip] |
||||||
|
pub(crate) const LUT_WW: [u8; 42] =[ |
||||||
|
0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, |
||||||
|
0x40, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, |
||||||
|
0x80, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0x00, 0x04, 0x10, 0x00, 0x00, 0x05, |
||||||
|
0x00, 0x03, 0x0E, 0x00, 0x00, 0x0A, |
||||||
|
0x00, 0x23, 0x00, 0x00, 0x00, 0x01, |
||||||
|
]; |
||||||
|
|
||||||
|
#[rustfmt::skip] |
||||||
|
pub(crate) const LUT_BW: [u8; 42] =[ |
||||||
|
0xA0, 0x1A, 0x1A, 0x00, 0x00, 0x01, |
||||||
|
0x00, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, |
||||||
|
0x90, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0xB0, 0x04, 0x10, 0x00, 0x00, 0x05, |
||||||
|
0xB0, 0x03, 0x0E, 0x00, 0x00, 0x0A, |
||||||
|
0xC0, 0x23, 0x00, 0x00, 0x00, 0x01, |
||||||
|
]; |
||||||
|
|
||||||
|
#[rustfmt::skip] |
||||||
|
pub(crate) const LUT_BB: [u8; 42] =[ |
||||||
|
0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, |
||||||
|
0x40, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, |
||||||
|
0x80, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0x00, 0x04, 0x10, 0x00, 0x00, 0x05, |
||||||
|
0x00, 0x03, 0x0E, 0x00, 0x00, 0x0A, |
||||||
|
0x00, 0x23, 0x00, 0x00, 0x00, 0x01, |
||||||
|
]; |
||||||
|
|
||||||
|
#[rustfmt::skip] |
||||||
|
pub(crate) const LUT_WB: [u8; 42] =[ |
||||||
|
0x90, 0x1A, 0x1A, 0x00, 0x00, 0x01, |
||||||
|
0x20, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0x84, 0x0E, 0x01, 0x0E, 0x01, 0x10, |
||||||
|
0x10, 0x0A, 0x0A, 0x00, 0x00, 0x08, |
||||||
|
0x00, 0x04, 0x10, 0x00, 0x00, 0x05, |
||||||
|
0x00, 0x03, 0x0E, 0x00, 0x00, 0x0A, |
||||||
|
0x00, 0x23, 0x00, 0x00, 0x00, 0x01, |
||||||
|
]; |
||||||
@ -0,0 +1,175 @@ |
|||||||
|
use crate::epd2in7b::{DEFAULT_BACKGROUND_COLOR, HEIGHT, WIDTH}; |
||||||
|
use crate::graphics::{Display, DisplayRotation}; |
||||||
|
use embedded_graphics_core::pixelcolor::BinaryColor; |
||||||
|
use embedded_graphics_core::prelude::*; |
||||||
|
|
||||||
|
/// Full size buffer for use with the 2in7B EPD
|
||||||
|
///
|
||||||
|
/// Can also be manuall constructed:
|
||||||
|
/// `buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); WIDTH * HEIGHT / 8]`
|
||||||
|
pub struct Display2in7b { |
||||||
|
buffer: [u8; WIDTH as usize * HEIGHT as usize / 8], |
||||||
|
rotation: DisplayRotation, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for Display2in7b { |
||||||
|
fn default() -> Self { |
||||||
|
Display2in7b { |
||||||
|
buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); |
||||||
|
WIDTH as usize * HEIGHT as usize / 8], |
||||||
|
rotation: DisplayRotation::default(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl DrawTarget for Display2in7b { |
||||||
|
type Color = BinaryColor; |
||||||
|
type Error = core::convert::Infallible; |
||||||
|
|
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error> |
||||||
|
where |
||||||
|
I: IntoIterator<Item = Pixel<Self::Color>>, |
||||||
|
{ |
||||||
|
for pixel in pixels { |
||||||
|
self.draw_helper(WIDTH, HEIGHT, pixel)?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OriginDimensions for Display2in7b { |
||||||
|
fn size(&self) -> Size { |
||||||
|
Size::new(WIDTH, HEIGHT) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Display for Display2in7b { |
||||||
|
fn buffer(&self) -> &[u8] { |
||||||
|
&self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn get_mut_buffer(&mut self) -> &mut [u8] { |
||||||
|
&mut self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn set_rotation(&mut self, rotation: DisplayRotation) { |
||||||
|
self.rotation = rotation; |
||||||
|
} |
||||||
|
|
||||||
|
fn rotation(&self) -> DisplayRotation { |
||||||
|
self.rotation |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use crate::color::Black; |
||||||
|
use crate::color::Color; |
||||||
|
use crate::epd2in7b; |
||||||
|
use crate::epd2in7b::{HEIGHT, WIDTH}; |
||||||
|
use crate::graphics::{Display, DisplayRotation}; |
||||||
|
use embedded_graphics::{ |
||||||
|
prelude::*, |
||||||
|
primitives::{Line, PrimitiveStyle}, |
||||||
|
}; |
||||||
|
|
||||||
|
// test buffer length
|
||||||
|
#[test] |
||||||
|
fn graphics_size() { |
||||||
|
let display = Display2in7b::default(); |
||||||
|
assert_eq!(display.buffer().len(), 5808); |
||||||
|
} |
||||||
|
|
||||||
|
// test default background color on all bytes
|
||||||
|
#[test] |
||||||
|
fn graphics_default() { |
||||||
|
let display = Display2in7b::default(); |
||||||
|
for &byte in display.buffer() { |
||||||
|
assert_eq!(byte, epd2in7b::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_0() { |
||||||
|
let mut display = Display2in7b::default(); |
||||||
|
let _ = Line::new(Point::new(0, 0), Point::new(7, 0)) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
assert_eq!(buffer[0], Color::Black.get_byte_value()); |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!(byte, epd2in7b::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_90() { |
||||||
|
let mut display = Display2in7b::default(); |
||||||
|
display.set_rotation(DisplayRotation::Rotate90); |
||||||
|
let _ = Line::new( |
||||||
|
Point::new(0, WIDTH as i32 - 8), |
||||||
|
Point::new(0, WIDTH as i32 - 1), |
||||||
|
) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
assert_eq!(buffer[0], Color::Black.get_byte_value()); |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!(byte, epd2in7b::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_180() { |
||||||
|
let mut display = Display2in7b::default(); |
||||||
|
display.set_rotation(DisplayRotation::Rotate180); |
||||||
|
|
||||||
|
let _ = Line::new( |
||||||
|
Point::new(WIDTH as i32 - 8, HEIGHT as i32 - 1), |
||||||
|
Point::new(WIDTH as i32 - 1, HEIGHT as i32 - 1), |
||||||
|
) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
extern crate std; |
||||||
|
std::println!("{:?}", buffer); |
||||||
|
|
||||||
|
assert_eq!(buffer[0], Color::Black.get_byte_value()); |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!(byte, epd2in7b::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_270() { |
||||||
|
let mut display = Display2in7b::default(); |
||||||
|
display.set_rotation(DisplayRotation::Rotate270); |
||||||
|
let _ = Line::new( |
||||||
|
Point::new(HEIGHT as i32 - 1, 0), |
||||||
|
Point::new(HEIGHT as i32 - 1, 7), |
||||||
|
) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
extern crate std; |
||||||
|
std::println!("{:?}", buffer); |
||||||
|
|
||||||
|
assert_eq!(buffer[0], Color::Black.get_byte_value()); |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!(byte, epd2in7b::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,456 @@ |
|||||||
|
//! A simple Driver for the Waveshare 2.7" B Tri-Color E-Ink Display via SPI
|
||||||
|
//!
|
||||||
|
//! [Documentation](https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT_(B))
|
||||||
|
|
||||||
|
use embedded_hal::{ |
||||||
|
blocking::{delay::*, spi::Write}, |
||||||
|
digital::v2::*, |
||||||
|
}; |
||||||
|
|
||||||
|
use crate::interface::DisplayInterface; |
||||||
|
use crate::traits::{ |
||||||
|
InternalWiAdditions, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay, |
||||||
|
}; |
||||||
|
|
||||||
|
// The Lookup Tables for the Display
|
||||||
|
mod constants; |
||||||
|
use crate::epd2in7b::constants::*; |
||||||
|
|
||||||
|
/// Width of the display
|
||||||
|
pub const WIDTH: u32 = 176; |
||||||
|
/// Height of the display
|
||||||
|
pub const HEIGHT: u32 = 264; |
||||||
|
/// Default Background Color
|
||||||
|
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; |
||||||
|
const IS_BUSY_LOW: bool = true; |
||||||
|
|
||||||
|
use crate::color::Color; |
||||||
|
|
||||||
|
pub(crate) mod command; |
||||||
|
use self::command::Command; |
||||||
|
|
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
mod graphics; |
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
pub use self::graphics::Display2in7b; |
||||||
|
|
||||||
|
/// Epd2in7b driver
|
||||||
|
pub struct Epd2in7b<SPI, CS, BUSY, DC, RST, DELAY> { |
||||||
|
/// Connection Interface
|
||||||
|
interface: DisplayInterface<SPI, CS, BUSY, DC, RST, DELAY>, |
||||||
|
/// Background Color
|
||||||
|
color: Color, |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> InternalWiAdditions<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd2in7b<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
// reset the device
|
||||||
|
self.interface.reset(delay, 2); |
||||||
|
|
||||||
|
// power on
|
||||||
|
self.command(spi, Command::PowerOn)?; |
||||||
|
delay.delay_ms(5); |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
// set panel settings, 0xbf is bw, 0xaf is multi-color
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::PanelSetting, &[0xaf])?; |
||||||
|
|
||||||
|
// pll control
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::PllControl, &[0x3a])?; |
||||||
|
|
||||||
|
// set the power settings
|
||||||
|
self.interface.cmd_with_data( |
||||||
|
spi, |
||||||
|
Command::PowerSetting, |
||||||
|
&[0x03, 0x00, 0x2b, 0x2b, 0x09], |
||||||
|
)?; |
||||||
|
|
||||||
|
// start the booster
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::BoosterSoftStart, &[0x07, 0x07, 0x17])?; |
||||||
|
|
||||||
|
// power optimization
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::PowerOptimization, &[0x60, 0xa5])?; |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::PowerOptimization, &[0x89, 0xa5])?; |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::PowerOptimization, &[0x90, 0x00])?; |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::PowerOptimization, &[0x93, 0x2a])?; |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::PowerOptimization, &[0x73, 0x41])?; |
||||||
|
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::VcmDcSetting, &[0x12])?; |
||||||
|
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x87])?; |
||||||
|
|
||||||
|
self.set_lut(spi, None)?; |
||||||
|
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::PartialDisplayRefresh, &[0x00])?; |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> WaveshareDisplay<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd2in7b<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
type DisplayColor = Color; |
||||||
|
fn new( |
||||||
|
spi: &mut SPI, |
||||||
|
cs: CS, |
||||||
|
busy: BUSY, |
||||||
|
dc: DC, |
||||||
|
rst: RST, |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<Self, SPI::Error> { |
||||||
|
let interface = DisplayInterface::new(cs, busy, dc, rst); |
||||||
|
let color = DEFAULT_BACKGROUND_COLOR; |
||||||
|
|
||||||
|
let mut epd = Epd2in7b { interface, color }; |
||||||
|
|
||||||
|
epd.init(spi, delay)?; |
||||||
|
|
||||||
|
Ok(epd) |
||||||
|
} |
||||||
|
|
||||||
|
fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.init(spi, delay) |
||||||
|
} |
||||||
|
|
||||||
|
fn sleep(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0xf7])?; |
||||||
|
|
||||||
|
self.command(spi, Command::PowerOff)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
_delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission1)?; |
||||||
|
self.send_buffer_helper(spi, buffer)?; |
||||||
|
|
||||||
|
// Clear chromatic layer since we won't be using it here
|
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission2)?; |
||||||
|
self.interface |
||||||
|
.data_x_times(spi, !self.color.get_byte_value(), WIDTH * HEIGHT / 8)?; |
||||||
|
|
||||||
|
self.interface.cmd(spi, Command::DataStop)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_partial_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface |
||||||
|
.cmd(spi, Command::PartialDataStartTransmission1)?; |
||||||
|
|
||||||
|
self.send_data(spi, &[(x >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(x & 0xf8) as u8])?; |
||||||
|
self.send_data(spi, &[(y >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(y & 0xff) as u8])?; |
||||||
|
self.send_data(spi, &[(width >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(width & 0xf8) as u8])?; |
||||||
|
self.send_data(spi, &[(height >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(height & 0xff) as u8])?; |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
self.send_buffer_helper(spi, buffer)?; |
||||||
|
|
||||||
|
self.interface.cmd(spi, Command::DataStop) |
||||||
|
} |
||||||
|
|
||||||
|
fn display_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.command(spi, Command::DisplayRefresh)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_and_display_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_frame(spi, buffer, delay)?; |
||||||
|
self.command(spi, Command::DisplayRefresh)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn clear_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
let color_value = self.color.get_byte_value(); |
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission1)?; |
||||||
|
self.interface |
||||||
|
.data_x_times(spi, color_value, WIDTH * HEIGHT / 8)?; |
||||||
|
|
||||||
|
self.interface.cmd(spi, Command::DataStop)?; |
||||||
|
|
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission2)?; |
||||||
|
self.interface |
||||||
|
.data_x_times(spi, color_value, WIDTH * HEIGHT / 8)?; |
||||||
|
self.interface.cmd(spi, Command::DataStop)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_background_color(&mut self, color: Color) { |
||||||
|
self.color = color; |
||||||
|
} |
||||||
|
|
||||||
|
fn background_color(&self) -> &Color { |
||||||
|
&self.color |
||||||
|
} |
||||||
|
|
||||||
|
fn width(&self) -> u32 { |
||||||
|
WIDTH |
||||||
|
} |
||||||
|
|
||||||
|
fn height(&self) -> u32 { |
||||||
|
HEIGHT |
||||||
|
} |
||||||
|
|
||||||
|
fn set_lut( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
_refresh_rate: Option<RefreshLut>, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.cmd_with_data(spi, Command::LutForVcom, &LUT_VCOM_DC)?; |
||||||
|
self.cmd_with_data(spi, Command::LutWhiteToWhite, &LUT_WW)?; |
||||||
|
self.cmd_with_data(spi, Command::LutBlackToWhite, &LUT_BW)?; |
||||||
|
self.cmd_with_data(spi, Command::LutWhiteToBlack, &LUT_WB)?; |
||||||
|
self.cmd_with_data(spi, Command::LutBlackToBlack, &LUT_BB)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn is_busy(&self) -> bool { |
||||||
|
self.interface.is_busy(IS_BUSY_LOW) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> WaveshareThreeColorDisplay<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd2in7b<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn update_color_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
black: &[u8], |
||||||
|
chromatic: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_achromatic_frame(spi, black)?; |
||||||
|
self.update_chromatic_frame(spi, chromatic) |
||||||
|
} |
||||||
|
|
||||||
|
/// Update only the black/white data of the display.
|
||||||
|
///
|
||||||
|
/// Finish by calling `update_chromatic_frame`.
|
||||||
|
fn update_achromatic_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
achromatic: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission1)?; |
||||||
|
|
||||||
|
self.send_buffer_helper(spi, achromatic)?; |
||||||
|
|
||||||
|
self.interface.cmd(spi, Command::DataStop) |
||||||
|
} |
||||||
|
|
||||||
|
/// Update only chromatic data of the display.
|
||||||
|
///
|
||||||
|
/// This data takes precedence over the black/white data.
|
||||||
|
fn update_chromatic_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
chromatic: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, Command::DataStartTransmission2)?; |
||||||
|
|
||||||
|
self.send_buffer_helper(spi, chromatic)?; |
||||||
|
|
||||||
|
self.interface.cmd(spi, Command::DataStop)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> Epd2in7b<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn command(&mut self, spi: &mut SPI, command: Command) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, command) |
||||||
|
} |
||||||
|
|
||||||
|
fn send_data(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> { |
||||||
|
self.interface.data(spi, data) |
||||||
|
} |
||||||
|
|
||||||
|
fn send_buffer_helper(&mut self, spi: &mut SPI, buffer: &[u8]) -> Result<(), SPI::Error> { |
||||||
|
// Based on the waveshare implementation, all data for color values is flipped. This helper
|
||||||
|
// method makes that transmission easier
|
||||||
|
for b in buffer.iter() { |
||||||
|
self.send_data(spi, &[!b])?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn cmd_with_data( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
command: Command, |
||||||
|
data: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd_with_data(spi, command, data) |
||||||
|
} |
||||||
|
|
||||||
|
fn wait_until_idle(&mut self) { |
||||||
|
let _ = self.interface.wait_until_idle(IS_BUSY_LOW); |
||||||
|
} |
||||||
|
|
||||||
|
/// Refresh display for partial frame
|
||||||
|
pub fn display_partial_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.command(spi, Command::PartialDisplayRefresh)?; |
||||||
|
self.send_data(spi, &[(x >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(x & 0xf8) as u8])?; |
||||||
|
self.send_data(spi, &[(y >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(y & 0xff) as u8])?; |
||||||
|
self.send_data(spi, &[(width >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(width & 0xf8) as u8])?; |
||||||
|
self.send_data(spi, &[(height >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(height & 0xff) as u8])?; |
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Update black/achromatic frame
|
||||||
|
pub fn update_partial_achromatic_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
achromatic: &[u8], |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface |
||||||
|
.cmd(spi, Command::PartialDataStartTransmission1)?; |
||||||
|
self.send_data(spi, &[(x >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(x & 0xf8) as u8])?; |
||||||
|
self.send_data(spi, &[(y >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(y & 0xff) as u8])?; |
||||||
|
self.send_data(spi, &[(width >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(width & 0xf8) as u8])?; |
||||||
|
self.send_data(spi, &[(height >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(height & 0xff) as u8])?; |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
for b in achromatic.iter() { |
||||||
|
// Flipping based on waveshare implementation
|
||||||
|
self.send_data(spi, &[!b])?; |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Update partial chromatic/red frame
|
||||||
|
pub fn update_partial_chromatic_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
chromatic: &[u8], |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface |
||||||
|
.cmd(spi, Command::PartialDataStartTransmission2)?; |
||||||
|
self.send_data(spi, &[(x >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(x & 0xf8) as u8])?; |
||||||
|
self.send_data(spi, &[(y >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(y & 0xff) as u8])?; |
||||||
|
self.send_data(spi, &[(width >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(width & 0xf8) as u8])?; |
||||||
|
self.send_data(spi, &[(height >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[(height & 0xff) as u8])?; |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
for b in chromatic.iter() { |
||||||
|
// Flipping based on waveshare implementation
|
||||||
|
self.send_data(spi, &[!b])?; |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn epd_size() { |
||||||
|
assert_eq!(WIDTH, 176); |
||||||
|
assert_eq!(HEIGHT, 264); |
||||||
|
assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,83 @@ |
|||||||
|
use crate::epd2in9::{DEFAULT_BACKGROUND_COLOR, HEIGHT, WIDTH}; |
||||||
|
use crate::graphics::{Display, DisplayRotation}; |
||||||
|
use embedded_graphics_core::pixelcolor::BinaryColor; |
||||||
|
use embedded_graphics_core::prelude::*; |
||||||
|
|
||||||
|
/// Display with Fullsize buffer for use with the 2in9 EPD V2
|
||||||
|
///
|
||||||
|
/// Can also be manuall constructed:
|
||||||
|
/// `buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); WIDTH / 8 * HEIGHT]`
|
||||||
|
pub struct Display2in9 { |
||||||
|
buffer: [u8; WIDTH as usize * HEIGHT as usize / 8], |
||||||
|
rotation: DisplayRotation, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for Display2in9 { |
||||||
|
fn default() -> Self { |
||||||
|
Display2in9 { |
||||||
|
buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); |
||||||
|
WIDTH as usize * HEIGHT as usize / 8], |
||||||
|
rotation: DisplayRotation::default(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl DrawTarget for Display2in9 { |
||||||
|
type Color = BinaryColor; |
||||||
|
type Error = core::convert::Infallible; |
||||||
|
|
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error> |
||||||
|
where |
||||||
|
I: IntoIterator<Item = Pixel<Self::Color>>, |
||||||
|
{ |
||||||
|
for pixel in pixels { |
||||||
|
self.draw_helper(WIDTH, HEIGHT, pixel)?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OriginDimensions for Display2in9 { |
||||||
|
fn size(&self) -> Size { |
||||||
|
Size::new(WIDTH, HEIGHT) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Display for Display2in9 { |
||||||
|
fn buffer(&self) -> &[u8] { |
||||||
|
&self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn get_mut_buffer(&mut self) -> &mut [u8] { |
||||||
|
&mut self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn set_rotation(&mut self, rotation: DisplayRotation) { |
||||||
|
self.rotation = rotation; |
||||||
|
} |
||||||
|
|
||||||
|
fn rotation(&self) -> DisplayRotation { |
||||||
|
self.rotation |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
// test buffer length
|
||||||
|
#[test] |
||||||
|
fn graphics_size() { |
||||||
|
let display = Display2in9::default(); |
||||||
|
assert_eq!(display.buffer().len(), 4736); |
||||||
|
} |
||||||
|
|
||||||
|
// test default background color on all bytes
|
||||||
|
#[test] |
||||||
|
fn graphics_default() { |
||||||
|
let display = Display2in9::default(); |
||||||
|
for &byte in display.buffer() { |
||||||
|
assert_eq!(byte, DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,496 @@ |
|||||||
|
//! A simple Driver for the Waveshare 2.9" E-Ink Display V2 via SPI
|
||||||
|
//!
|
||||||
|
//! Specification: <https://www.waveshare.com/w/upload/7/79/2.9inch-e-paper-v2-specification.pdf>
|
||||||
|
//!
|
||||||
|
//! # Example for the 2.9 in E-Ink Display V2
|
||||||
|
//!
|
||||||
|
//!```rust, no_run
|
||||||
|
//!# use embedded_hal_mock::*;
|
||||||
|
//!# fn main() -> Result<(), MockError> {
|
||||||
|
//!use embedded_graphics::{
|
||||||
|
//! pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle},
|
||||||
|
//!};
|
||||||
|
//!use epd_waveshare::{epd2in9_v2::*, prelude::*};
|
||||||
|
//!#
|
||||||
|
//!# let expectations = [];
|
||||||
|
//!# let mut spi = spi::Mock::new(&expectations);
|
||||||
|
//!# let expectations = [];
|
||||||
|
//!# let cs_pin = pin::Mock::new(&expectations);
|
||||||
|
//!# let busy_in = pin::Mock::new(&expectations);
|
||||||
|
//!# let dc = pin::Mock::new(&expectations);
|
||||||
|
//!# let rst = pin::Mock::new(&expectations);
|
||||||
|
//!# let mut delay = delay::MockNoop::new();
|
||||||
|
//!
|
||||||
|
//!// Setup EPD
|
||||||
|
//!let mut epd = Epd2in9::new(&mut spi, cs_pin, busy_in, dc, rst, &mut delay)?;
|
||||||
|
//!
|
||||||
|
//!// Use display graphics from embedded-graphics
|
||||||
|
//!let mut display = Display2in9::default();
|
||||||
|
//!
|
||||||
|
//!// Use embedded graphics for drawing a line
|
||||||
|
//!let _ = Line::new(Point::new(0, 120), Point::new(0, 295))
|
||||||
|
//! .into_styled(PrimitiveStyle::with_stroke(Black, 1))
|
||||||
|
//! .draw(&mut display);
|
||||||
|
//!
|
||||||
|
//!// Display updated frame
|
||||||
|
//!epd.update_frame(&mut spi, &display.buffer(), &mut delay)?;
|
||||||
|
//!epd.display_frame(&mut spi, &mut delay)?;
|
||||||
|
//!
|
||||||
|
//!// Draw something new here
|
||||||
|
//!
|
||||||
|
//!// Display new image as a base image for further quick refreshes
|
||||||
|
//!epd.update_old_frame(&mut spi, &display.buffer(), &mut delay)?;
|
||||||
|
//!epd.display_frame(&mut spi, &mut delay)?;
|
||||||
|
//!
|
||||||
|
//!// Update image here
|
||||||
|
//!
|
||||||
|
//!// quick refresh of updated pixels
|
||||||
|
//!epd.update_new_frame(&mut spi, &display.buffer(), &mut delay)?;
|
||||||
|
//!epd.display_new_frame(&mut spi, &mut delay)?;
|
||||||
|
//!
|
||||||
|
//!// Set the EPD to sleep
|
||||||
|
//!epd.sleep(&mut spi, &mut delay)?;
|
||||||
|
//!# Ok(())
|
||||||
|
//!# }
|
||||||
|
//!```
|
||||||
|
|
||||||
|
/// Width of epd2in9 in pixels
|
||||||
|
pub const WIDTH: u32 = 128; |
||||||
|
/// Height of epd2in9 in pixels
|
||||||
|
pub const HEIGHT: u32 = 296; |
||||||
|
/// Default Background Color (white)
|
||||||
|
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; |
||||||
|
const IS_BUSY_LOW: bool = false; |
||||||
|
|
||||||
|
const LUT_PARTIAL_2IN9: [u8; 153] = [ |
||||||
|
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, |
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, |
||||||
|
0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, |
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, |
||||||
|
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, |
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, |
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, |
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, |
||||||
|
0x22, 0x0, 0x0, 0x0, |
||||||
|
]; |
||||||
|
|
||||||
|
use embedded_hal::{ |
||||||
|
blocking::{delay::*, spi::Write}, |
||||||
|
digital::v2::*, |
||||||
|
}; |
||||||
|
|
||||||
|
use crate::type_a::command::Command; |
||||||
|
|
||||||
|
use crate::color::Color; |
||||||
|
|
||||||
|
use crate::traits::*; |
||||||
|
|
||||||
|
use crate::interface::DisplayInterface; |
||||||
|
use crate::traits::QuickRefresh; |
||||||
|
|
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
mod graphics; |
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
pub use crate::epd2in9_v2::graphics::Display2in9; |
||||||
|
|
||||||
|
/// Epd2in9 driver
|
||||||
|
///
|
||||||
|
pub struct Epd2in9<SPI, CS, BUSY, DC, RST, DELAY> { |
||||||
|
/// SPI
|
||||||
|
interface: DisplayInterface<SPI, CS, BUSY, DC, RST, DELAY>, |
||||||
|
/// Color
|
||||||
|
background_color: Color, |
||||||
|
/// Refresh LUT
|
||||||
|
refresh: RefreshLut, |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> Epd2in9<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.interface.reset(delay, 2); |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
self.interface.cmd(spi, Command::SwReset)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
// 3 Databytes:
|
||||||
|
// A[7:0]
|
||||||
|
// 0.. A[8]
|
||||||
|
// 0.. B[2:0]
|
||||||
|
// Default Values: A = Height of Screen (0x127), B = 0x00 (GD, SM and TB=0?)
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::DriverOutputControl, &[0x27, 0x01, 0x00])?; |
||||||
|
|
||||||
|
// One Databyte with default value 0x03
|
||||||
|
// -> address: x increment, y increment, address counter is updated in x direction
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::DataEntryModeSetting, &[0x03])?; |
||||||
|
|
||||||
|
self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; |
||||||
|
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::DisplayUpdateControl1, &[0x00, 0x80])?; |
||||||
|
|
||||||
|
self.set_ram_counter(spi, 0, 0)?; |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> WaveshareDisplay<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd2in9<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
type DisplayColor = Color; |
||||||
|
fn width(&self) -> u32 { |
||||||
|
WIDTH |
||||||
|
} |
||||||
|
|
||||||
|
fn height(&self) -> u32 { |
||||||
|
HEIGHT |
||||||
|
} |
||||||
|
|
||||||
|
fn new( |
||||||
|
spi: &mut SPI, |
||||||
|
cs: CS, |
||||||
|
busy: BUSY, |
||||||
|
dc: DC, |
||||||
|
rst: RST, |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<Self, SPI::Error> { |
||||||
|
let interface = DisplayInterface::new(cs, busy, dc, rst); |
||||||
|
|
||||||
|
let mut epd = Epd2in9 { |
||||||
|
interface, |
||||||
|
background_color: DEFAULT_BACKGROUND_COLOR, |
||||||
|
refresh: RefreshLut::Full, |
||||||
|
}; |
||||||
|
|
||||||
|
epd.init(spi, delay)?; |
||||||
|
|
||||||
|
Ok(epd) |
||||||
|
} |
||||||
|
|
||||||
|
fn sleep(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
// 0x00 for Normal mode (Power on Reset), 0x01 for Deep Sleep Mode
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::DeepSleepMode, &[0x01])?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.init(spi, delay)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
_delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.interface.cmd_with_data(spi, Command::WriteRam, buffer) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_partial_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
//TODO This is copied from epd2in9 but it seems not working. Partial refresh supported by version 2?
|
||||||
|
self.wait_until_idle(); |
||||||
|
self.set_ram_area(spi, x, y, x + width, y + height)?; |
||||||
|
self.set_ram_counter(spi, x, y)?; |
||||||
|
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::WriteRam, buffer)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn display_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
// Enable clock signal, Enable Analog, Load temperature value, DISPLAY with DISPLAY Mode 1, Disable Analog, Disable OSC
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xF7])?; |
||||||
|
self.interface.cmd(spi, Command::MasterActivation)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_and_display_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_frame(spi, buffer, delay)?; |
||||||
|
self.display_frame(spi, delay)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn clear_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
// clear the ram with the background color
|
||||||
|
let color = self.background_color.get_byte_value(); |
||||||
|
|
||||||
|
self.interface.cmd(spi, Command::WriteRam)?; |
||||||
|
self.interface.data_x_times(spi, color, WIDTH / 8 * HEIGHT) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_background_color(&mut self, background_color: Color) { |
||||||
|
self.background_color = background_color; |
||||||
|
} |
||||||
|
|
||||||
|
fn background_color(&self) -> &Color { |
||||||
|
&self.background_color |
||||||
|
} |
||||||
|
|
||||||
|
fn set_lut( |
||||||
|
&mut self, |
||||||
|
_spi: &mut SPI, |
||||||
|
refresh_rate: Option<RefreshLut>, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
if let Some(refresh_lut) = refresh_rate { |
||||||
|
self.refresh = refresh_lut; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn is_busy(&self) -> bool { |
||||||
|
self.interface.is_busy(IS_BUSY_LOW) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> Epd2in9<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn wait_until_idle(&mut self) { |
||||||
|
self.interface.wait_until_idle(IS_BUSY_LOW); |
||||||
|
} |
||||||
|
|
||||||
|
fn use_full_frame(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { |
||||||
|
// choose full frame/ram
|
||||||
|
self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?; |
||||||
|
|
||||||
|
// start from the beginning
|
||||||
|
self.set_ram_counter(spi, 0, 0) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_ram_area( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
start_x: u32, |
||||||
|
start_y: u32, |
||||||
|
end_x: u32, |
||||||
|
end_y: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
assert!(start_x < end_x); |
||||||
|
assert!(start_y < end_y); |
||||||
|
|
||||||
|
// x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram
|
||||||
|
// aren't relevant
|
||||||
|
self.interface.cmd_with_data( |
||||||
|
spi, |
||||||
|
Command::SetRamXAddressStartEndPosition, |
||||||
|
&[(start_x >> 3) as u8, (end_x >> 3) as u8], |
||||||
|
)?; |
||||||
|
|
||||||
|
// 2 Databytes: A[7:0] & 0..A[8] for each - start and end
|
||||||
|
self.interface.cmd_with_data( |
||||||
|
spi, |
||||||
|
Command::SetRamYAddressStartEndPosition, |
||||||
|
&[ |
||||||
|
start_y as u8, |
||||||
|
(start_y >> 8) as u8, |
||||||
|
end_y as u8, |
||||||
|
(end_y >> 8) as u8, |
||||||
|
], |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_ram_counter(&mut self, spi: &mut SPI, x: u32, y: u32) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
// x is positioned in bytes, so the last 3 bits which show the position inside a byte in the ram
|
||||||
|
// aren't relevant
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::SetRamXAddressCounter, &[x as u8])?; |
||||||
|
|
||||||
|
// 2 Databytes: A[7:0] & 0..A[8]
|
||||||
|
self.interface.cmd_with_data( |
||||||
|
spi, |
||||||
|
Command::SetRamYAddressCounter, |
||||||
|
&[y as u8, (y >> 8) as u8], |
||||||
|
)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Set your own LUT, this function is also used internally for set_lut
|
||||||
|
fn set_lut_helper(&mut self, spi: &mut SPI, buffer: &[u8]) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::WriteLutRegister, buffer)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> QuickRefresh<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd2in9<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
/// To be followed immediately by `update_new_frame`.
|
||||||
|
fn update_old_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
_delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::WriteRam, buffer)?; |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::WriteRam2, buffer) |
||||||
|
} |
||||||
|
|
||||||
|
/// To be used immediately after `update_old_frame`.
|
||||||
|
fn update_new_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.interface.reset(delay, 2); |
||||||
|
|
||||||
|
self.set_lut_helper(spi, &LUT_PARTIAL_2IN9)?; |
||||||
|
self.interface.cmd_with_data( |
||||||
|
spi, |
||||||
|
Command::WriteOtpSelection, |
||||||
|
&[0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00], |
||||||
|
)?; |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::BorderWaveformControl, &[0x80])?; |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xC0])?; |
||||||
|
self.interface.cmd(spi, Command::MasterActivation)?; |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
self.use_full_frame(spi)?; |
||||||
|
|
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::WriteRam, buffer)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
/// For a quick refresh of the new updated frame. To be used immediately after `update_new_frame`
|
||||||
|
fn display_new_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.interface |
||||||
|
.cmd_with_data(spi, Command::DisplayUpdateControl2, &[0x0F])?; |
||||||
|
self.interface.cmd(spi, Command::MasterActivation)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Updates and displays the new frame.
|
||||||
|
fn update_and_display_new_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_new_frame(spi, buffer, delay)?; |
||||||
|
self.display_new_frame(spi, delay)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Partial quick refresh not supported yet
|
||||||
|
#[allow(unused)] |
||||||
|
fn update_partial_old_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
//TODO supported by display?
|
||||||
|
unimplemented!() |
||||||
|
} |
||||||
|
|
||||||
|
/// Partial quick refresh not supported yet
|
||||||
|
#[allow(unused)] |
||||||
|
fn update_partial_new_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
//TODO supported by display?
|
||||||
|
unimplemented!() |
||||||
|
} |
||||||
|
|
||||||
|
/// Partial quick refresh not supported yet
|
||||||
|
#[allow(unused)] |
||||||
|
fn clear_partial_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
x: u32, |
||||||
|
y: u32, |
||||||
|
width: u32, |
||||||
|
height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
//TODO supported by display?
|
||||||
|
unimplemented!() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn epd_size() { |
||||||
|
assert_eq!(WIDTH, 128); |
||||||
|
assert_eq!(HEIGHT, 296); |
||||||
|
assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,150 @@ |
|||||||
|
//! SPI Commands for the Waveshare 7.5" E-Ink Display
|
||||||
|
|
||||||
|
use crate::traits; |
||||||
|
|
||||||
|
/// EPD6in65f commands
|
||||||
|
///
|
||||||
|
/// Should rarely (never?) be needed directly.
|
||||||
|
///
|
||||||
|
/// For more infos about the addresses and what they are doing look into the PDFs.
|
||||||
|
#[allow(dead_code)] |
||||||
|
#[derive(Copy, Clone)] |
||||||
|
pub(crate) enum Command { |
||||||
|
/// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift
|
||||||
|
/// direction, booster switch, soft reset.
|
||||||
|
PanelSetting = 0x00, |
||||||
|
|
||||||
|
/// Selecting internal and external power
|
||||||
|
PowerSetting = 0x01, |
||||||
|
|
||||||
|
/// After the Power Off command, the driver will power off following the Power Off
|
||||||
|
/// Sequence; BUSY signal will become "0". This command will turn off charge pump,
|
||||||
|
/// T-con, source driver, gate driver, VCOM, and temperature sensor, but register
|
||||||
|
/// data will be kept until VDD becomes OFF. Source Driver output and Vcom will remain
|
||||||
|
/// as previous condition, which may have 2 conditions: 0V or floating.
|
||||||
|
PowerOff = 0x02, |
||||||
|
|
||||||
|
/// Setting Power OFF sequence
|
||||||
|
PowerOffSequenceSetting = 0x03, |
||||||
|
|
||||||
|
/// Turning On the Power
|
||||||
|
///
|
||||||
|
/// After the Power ON command, the driver will power on following the Power ON
|
||||||
|
/// sequence. Once complete, the BUSY signal will become "1".
|
||||||
|
PowerOn = 0x04, |
||||||
|
|
||||||
|
/// Starting data transmission
|
||||||
|
BoosterSoftStart = 0x06, |
||||||
|
|
||||||
|
/// This command makes the chip enter the deep-sleep mode to save power.
|
||||||
|
///
|
||||||
|
/// The deep sleep mode would return to stand-by by hardware reset.
|
||||||
|
///
|
||||||
|
/// The only one parameter is a check code, the command would be excuted if check code = 0xA5.
|
||||||
|
DeepSleep = 0x07, |
||||||
|
|
||||||
|
/// This command starts transmitting data and write them into SRAM. To complete data
|
||||||
|
/// transmission, command DSP (Data Stop) must be issued. Then the chip will start to
|
||||||
|
/// send data/VCOM for panel.
|
||||||
|
///
|
||||||
|
/// BLACK/WHITE or OLD_DATA
|
||||||
|
DataStartTransmission1 = 0x10, |
||||||
|
|
||||||
|
/// To stop data transmission, this command must be issued to check the `data_flag`.
|
||||||
|
///
|
||||||
|
/// After this command, BUSY signal will become "0" until the display update is
|
||||||
|
/// finished.
|
||||||
|
DataStop = 0x11, |
||||||
|
|
||||||
|
/// After this command is issued, driver will refresh display (data/VCOM) according to
|
||||||
|
/// SRAM data and LUT.
|
||||||
|
///
|
||||||
|
/// After Display Refresh command, BUSY signal will become "0" until the display
|
||||||
|
/// update is finished.
|
||||||
|
DisplayRefresh = 0x12, |
||||||
|
|
||||||
|
/// Image Process Command
|
||||||
|
ImageProcess = 0x13, |
||||||
|
|
||||||
|
/// This command builds the VCOM Look-Up Table (LUTC).
|
||||||
|
LutForVcom = 0x20, |
||||||
|
/// This command builds the Black Look-Up Table (LUTB).
|
||||||
|
LutBlack = 0x21, |
||||||
|
/// This command builds the White Look-Up Table (LUTW).
|
||||||
|
LutWhite = 0x22, |
||||||
|
/// This command builds the Gray1 Look-Up Table (LUTG1).
|
||||||
|
LutGray1 = 0x23, |
||||||
|
/// This command builds the Gray2 Look-Up Table (LUTG2).
|
||||||
|
LutGray2 = 0x24, |
||||||
|
/// This command builds the Red0 Look-Up Table (LUTR0).
|
||||||
|
LutRed0 = 0x25, |
||||||
|
/// This command builds the Red1 Look-Up Table (LUTR1).
|
||||||
|
LutRed1 = 0x26, |
||||||
|
/// This command builds the Red2 Look-Up Table (LUTR2).
|
||||||
|
LutRed2 = 0x27, |
||||||
|
/// This command builds the Red3 Look-Up Table (LUTR3).
|
||||||
|
LutRed3 = 0x28, |
||||||
|
/// This command builds the XON Look-Up Table (LUTXON).
|
||||||
|
LutXon = 0x29, |
||||||
|
|
||||||
|
/// The command controls the PLL clock frequency.
|
||||||
|
PllControl = 0x30, |
||||||
|
|
||||||
|
/// This command reads the temperature sensed by the temperature sensor.
|
||||||
|
TemperatureSensor = 0x40, |
||||||
|
/// This command selects the Internal or External temperature sensor.
|
||||||
|
TemperatureCalibration = 0x41, |
||||||
|
/// This command could write data to the external temperature sensor.
|
||||||
|
TemperatureSensorWrite = 0x42, |
||||||
|
/// This command could read data from the external temperature sensor.
|
||||||
|
TemperatureSensorRead = 0x43, |
||||||
|
|
||||||
|
/// This command indicates the interval of Vcom and data output. When setting the
|
||||||
|
/// vertical back porch, the total blanking will be kept (20 Hsync).
|
||||||
|
VcomAndDataIntervalSetting = 0x50, |
||||||
|
/// This command indicates the input power condition. Host can read this flag to learn
|
||||||
|
/// the battery condition.
|
||||||
|
LowPowerDetection = 0x51, |
||||||
|
|
||||||
|
/// This command defines non-overlap period of Gate and Source.
|
||||||
|
TconSetting = 0x60, |
||||||
|
/// This command defines alternative resolution and this setting is of higher priority
|
||||||
|
/// than the RES\[1:0\] in R00H (PSR).
|
||||||
|
TconResolution = 0x61, |
||||||
|
/// This command defines MCU host direct access external memory mode.
|
||||||
|
//SpiFlashControl = 0x65,
|
||||||
|
|
||||||
|
/// The LUT_REV / Chip Revision is read from OTP address = 25001 and 25000.
|
||||||
|
//Revision = 0x70,
|
||||||
|
/// This command reads the IC status.
|
||||||
|
GetStatus = 0x71, |
||||||
|
|
||||||
|
/// This command implements related VCOM sensing setting.
|
||||||
|
//AutoMeasurementVcom = 0x80,
|
||||||
|
/// This command gets the VCOM value.
|
||||||
|
ReadVcomValue = 0x81, |
||||||
|
/// This command sets `VCOM_DC` value.
|
||||||
|
VcmDcSetting = 0x82, |
||||||
|
// /// This is in all the Waveshare controllers for EPD6in65f, but it's not documented
|
||||||
|
// /// anywhere in the datasheet `¯\_(ツ)_/¯`
|
||||||
|
FlashMode = 0xE3, |
||||||
|
} |
||||||
|
|
||||||
|
impl traits::Command for Command { |
||||||
|
/// Returns the address of the command
|
||||||
|
fn address(self) -> u8 { |
||||||
|
self as u8 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use crate::traits::Command as CommandTrait; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn command_addr() { |
||||||
|
assert_eq!(Command::PanelSetting.address(), 0x00); |
||||||
|
assert_eq!(Command::DisplayRefresh.address(), 0x12); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,263 @@ |
|||||||
|
use crate::color::OctColor; |
||||||
|
use crate::epd5in65f::{DEFAULT_BACKGROUND_COLOR, HEIGHT, WIDTH}; |
||||||
|
use crate::graphics::{DisplayRotation, OctDisplay}; |
||||||
|
use embedded_graphics_core::prelude::*; |
||||||
|
|
||||||
|
/// Full size buffer for use with the 5in65f EPD
|
||||||
|
///
|
||||||
|
/// Can also be manually constructed:
|
||||||
|
/// `buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); WIDTH / 2 * HEIGHT]`
|
||||||
|
pub struct Display5in65f { |
||||||
|
buffer: [u8; WIDTH as usize * HEIGHT as usize / 2], |
||||||
|
rotation: DisplayRotation, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for Display5in65f { |
||||||
|
fn default() -> Self { |
||||||
|
Display5in65f { |
||||||
|
buffer: [OctColor::colors_byte(DEFAULT_BACKGROUND_COLOR, DEFAULT_BACKGROUND_COLOR); |
||||||
|
WIDTH as usize * HEIGHT as usize / 2], |
||||||
|
rotation: DisplayRotation::default(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl DrawTarget for Display5in65f { |
||||||
|
type Color = OctColor; |
||||||
|
type Error = core::convert::Infallible; |
||||||
|
|
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error> |
||||||
|
where |
||||||
|
I: IntoIterator<Item = Pixel<Self::Color>>, |
||||||
|
{ |
||||||
|
for pixel in pixels { |
||||||
|
self.draw_helper(WIDTH, HEIGHT, pixel)?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OriginDimensions for Display5in65f { |
||||||
|
fn size(&self) -> Size { |
||||||
|
Size::new(WIDTH, HEIGHT) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OctDisplay for Display5in65f { |
||||||
|
fn buffer(&self) -> &[u8] { |
||||||
|
&self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn get_mut_buffer(&mut self) -> &mut [u8] { |
||||||
|
&mut self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn set_rotation(&mut self, rotation: DisplayRotation) { |
||||||
|
self.rotation = rotation; |
||||||
|
} |
||||||
|
|
||||||
|
fn rotation(&self) -> DisplayRotation { |
||||||
|
self.rotation |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use crate::epd5in65f; |
||||||
|
use crate::graphics::{DisplayRotation, OctDisplay}; |
||||||
|
use embedded_graphics::{ |
||||||
|
prelude::*, |
||||||
|
primitives::{Line, PrimitiveStyle}, |
||||||
|
}; |
||||||
|
|
||||||
|
// test buffer length
|
||||||
|
#[test] |
||||||
|
fn graphics_size() { |
||||||
|
let display = Display5in65f::default(); |
||||||
|
assert_eq!(display.buffer().len(), 448 * 600 / 2); |
||||||
|
} |
||||||
|
|
||||||
|
// test default background color on all bytes
|
||||||
|
#[test] |
||||||
|
fn graphics_default() { |
||||||
|
let display = Display5in65f::default(); |
||||||
|
for &byte in display.buffer() { |
||||||
|
assert_eq!( |
||||||
|
byte, |
||||||
|
OctColor::colors_byte( |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR, |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR, |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_0() { |
||||||
|
let mut display = Display5in65f::default(); |
||||||
|
|
||||||
|
let _ = Line::new(Point::new(0, 0), Point::new(1, 0)) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(OctColor::Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
for &byte in buffer.iter().take(1) { |
||||||
|
assert_eq!( |
||||||
|
OctColor::split_byte(byte), |
||||||
|
Ok((OctColor::Black, OctColor::Black)) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!( |
||||||
|
OctColor::split_byte(byte), |
||||||
|
Ok(( |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR, |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR |
||||||
|
)) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_90() { |
||||||
|
let mut display = Display5in65f::default(); |
||||||
|
display.set_rotation(DisplayRotation::Rotate90); |
||||||
|
|
||||||
|
let _ = Line::new( |
||||||
|
Point::new(0, WIDTH as i32 - 2), |
||||||
|
Point::new(0, WIDTH as i32 - 1), |
||||||
|
) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(OctColor::Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
for &byte in buffer.iter().take(1) { |
||||||
|
assert_eq!( |
||||||
|
OctColor::split_byte(byte), |
||||||
|
Ok((OctColor::Black, OctColor::Black)) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!( |
||||||
|
OctColor::split_byte(byte), |
||||||
|
Ok(( |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR, |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR |
||||||
|
)) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_180() { |
||||||
|
let mut display = Display5in65f::default(); |
||||||
|
display.set_rotation(DisplayRotation::Rotate180); |
||||||
|
|
||||||
|
let _ = Line::new( |
||||||
|
Point::new(WIDTH as i32 - 2, HEIGHT as i32 - 1), |
||||||
|
Point::new(WIDTH as i32 - 1, HEIGHT as i32 - 1), |
||||||
|
) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(OctColor::Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
for &byte in buffer.iter().take(1) { |
||||||
|
assert_eq!( |
||||||
|
OctColor::split_byte(byte), |
||||||
|
Ok((OctColor::Black, OctColor::Black)) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!( |
||||||
|
OctColor::split_byte(byte), |
||||||
|
Ok(( |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR, |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR |
||||||
|
)) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_270() { |
||||||
|
let mut display = Display5in65f::default(); |
||||||
|
display.set_rotation(DisplayRotation::Rotate270); |
||||||
|
|
||||||
|
let _ = Line::new( |
||||||
|
Point::new(HEIGHT as i32 - 1, 0), |
||||||
|
Point::new(HEIGHT as i32 - 1, 1), |
||||||
|
) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(OctColor::Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
for &byte in buffer.iter().take(1) { |
||||||
|
assert_eq!( |
||||||
|
OctColor::split_byte(byte), |
||||||
|
Ok((OctColor::Black, OctColor::Black)) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!( |
||||||
|
OctColor::split_byte(byte), |
||||||
|
Ok(( |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR, |
||||||
|
epd5in65f::DEFAULT_BACKGROUND_COLOR |
||||||
|
)) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_colors() { |
||||||
|
let mut display = Display5in65f::default(); |
||||||
|
|
||||||
|
const COLORS: [OctColor; 8] = [ |
||||||
|
OctColor::HiZ, |
||||||
|
OctColor::White, |
||||||
|
OctColor::Black, |
||||||
|
OctColor::Red, |
||||||
|
OctColor::Green, |
||||||
|
OctColor::Orange, |
||||||
|
OctColor::Blue, |
||||||
|
OctColor::Yellow, |
||||||
|
]; |
||||||
|
for c in &COLORS { |
||||||
|
display.clear_buffer(*c); |
||||||
|
for b in display.buffer() { |
||||||
|
assert_eq!(OctColor::split_byte(*b), Ok((*c, *c))); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (w, c) in (0..WIDTH).zip(COLORS.iter().cycle()) { |
||||||
|
let _ = Line::new( |
||||||
|
Point::new(w as i32, 0), |
||||||
|
Point::new(w as i32, HEIGHT as i32 - 1), |
||||||
|
) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(*c, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
} |
||||||
|
|
||||||
|
COLORS |
||||||
|
.chunks(2) |
||||||
|
.cycle() |
||||||
|
.take(WIDTH as usize * 2) |
||||||
|
.cycle() |
||||||
|
.zip(display.buffer()) |
||||||
|
.for_each(|(window, b)| match (window, b) { |
||||||
|
(&[c1, c2], b) => { |
||||||
|
assert_eq!(OctColor::split_byte(*b), Ok((c1, c2))); |
||||||
|
} |
||||||
|
_ => panic!("unexpected pattern"), |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,261 @@ |
|||||||
|
//! A simple Driver for the Waveshare 6.65 inch (F) E-Ink Display via SPI
|
||||||
|
//!
|
||||||
|
//! # References
|
||||||
|
//!
|
||||||
|
//! - [Datasheet](https://www.waveshare.com/wiki/5.65inch_e-Paper_Module_(F))
|
||||||
|
//! - [Waveshare C driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/c/lib/e-Paper/EPD_5in65f.c)
|
||||||
|
//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd5in65f.py)
|
||||||
|
|
||||||
|
use embedded_hal::{ |
||||||
|
blocking::{delay::*, spi::Write}, |
||||||
|
digital::v2::{InputPin, OutputPin}, |
||||||
|
}; |
||||||
|
|
||||||
|
use crate::color::OctColor; |
||||||
|
use crate::interface::DisplayInterface; |
||||||
|
use crate::traits::{InternalWiAdditions, RefreshLut, WaveshareDisplay}; |
||||||
|
|
||||||
|
pub(crate) mod command; |
||||||
|
use self::command::Command; |
||||||
|
|
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
mod graphics; |
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
pub use self::graphics::Display5in65f; |
||||||
|
|
||||||
|
/// Width of the display
|
||||||
|
pub const WIDTH: u32 = 600; |
||||||
|
/// Height of the display
|
||||||
|
pub const HEIGHT: u32 = 448; |
||||||
|
/// Default Background Color
|
||||||
|
pub const DEFAULT_BACKGROUND_COLOR: OctColor = OctColor::White; |
||||||
|
const IS_BUSY_LOW: bool = true; |
||||||
|
|
||||||
|
/// Epd5in65f driver
|
||||||
|
///
|
||||||
|
pub struct Epd5in65f<SPI, CS, BUSY, DC, RST, DELAY> { |
||||||
|
/// Connection Interface
|
||||||
|
interface: DisplayInterface<SPI, CS, BUSY, DC, RST, DELAY>, |
||||||
|
/// Background Color
|
||||||
|
color: OctColor, |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> InternalWiAdditions<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd5in65f<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
// Reset the device
|
||||||
|
self.interface.reset(delay, 2); |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::PanelSetting, &[0xEF, 0x08])?; |
||||||
|
self.cmd_with_data(spi, Command::PowerSetting, &[0x37, 0x00, 0x23, 0x23])?; |
||||||
|
self.cmd_with_data(spi, Command::PowerOffSequenceSetting, &[0x00])?; |
||||||
|
self.cmd_with_data(spi, Command::BoosterSoftStart, &[0xC7, 0xC7, 0x1D])?; |
||||||
|
self.cmd_with_data(spi, Command::PllControl, &[0x3C])?; |
||||||
|
self.cmd_with_data(spi, Command::TemperatureSensor, &[0x00])?; |
||||||
|
self.update_vcom(spi)?; |
||||||
|
self.cmd_with_data(spi, Command::TconSetting, &[0x22])?; |
||||||
|
self.send_resolution(spi)?; |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::FlashMode, &[0xAA])?; |
||||||
|
|
||||||
|
delay.delay_ms(100); |
||||||
|
|
||||||
|
self.update_vcom(spi)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> WaveshareDisplay<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd5in65f<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
type DisplayColor = OctColor; |
||||||
|
fn new( |
||||||
|
spi: &mut SPI, |
||||||
|
cs: CS, |
||||||
|
busy: BUSY, |
||||||
|
dc: DC, |
||||||
|
rst: RST, |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<Self, SPI::Error> { |
||||||
|
let interface = DisplayInterface::new(cs, busy, dc, rst); |
||||||
|
let color = DEFAULT_BACKGROUND_COLOR; |
||||||
|
|
||||||
|
let mut epd = Epd5in65f { interface, color }; |
||||||
|
|
||||||
|
epd.init(spi, delay)?; |
||||||
|
|
||||||
|
Ok(epd) |
||||||
|
} |
||||||
|
|
||||||
|
fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.init(spi, delay) |
||||||
|
} |
||||||
|
|
||||||
|
fn sleep(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
_delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.wait_busy_high(); |
||||||
|
self.update_vcom(spi)?; |
||||||
|
self.send_resolution(spi)?; |
||||||
|
self.cmd_with_data(spi, Command::DataStartTransmission1, buffer)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_partial_frame( |
||||||
|
&mut self, |
||||||
|
_spi: &mut SPI, |
||||||
|
_buffer: &[u8], |
||||||
|
_x: u32, |
||||||
|
_y: u32, |
||||||
|
_width: u32, |
||||||
|
_height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
unimplemented!(); |
||||||
|
} |
||||||
|
|
||||||
|
fn display_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_busy_high(); |
||||||
|
self.command(spi, Command::PowerOn)?; |
||||||
|
self.wait_busy_high(); |
||||||
|
self.command(spi, Command::DisplayRefresh)?; |
||||||
|
self.wait_busy_high(); |
||||||
|
self.command(spi, Command::PowerOff)?; |
||||||
|
self.wait_busy_low(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_and_display_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_frame(spi, buffer, delay)?; |
||||||
|
self.display_frame(spi, delay)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
let bg = OctColor::colors_byte(self.color, self.color); |
||||||
|
self.wait_busy_high(); |
||||||
|
self.update_vcom(spi)?; |
||||||
|
self.send_resolution(spi)?; |
||||||
|
self.command(spi, Command::DataStartTransmission1)?; |
||||||
|
self.interface.data_x_times(spi, bg, WIDTH * HEIGHT / 2)?; |
||||||
|
self.display_frame(spi, delay)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_background_color(&mut self, color: OctColor) { |
||||||
|
self.color = color; |
||||||
|
} |
||||||
|
|
||||||
|
fn background_color(&self) -> &OctColor { |
||||||
|
&self.color |
||||||
|
} |
||||||
|
|
||||||
|
fn width(&self) -> u32 { |
||||||
|
WIDTH |
||||||
|
} |
||||||
|
|
||||||
|
fn height(&self) -> u32 { |
||||||
|
HEIGHT |
||||||
|
} |
||||||
|
|
||||||
|
fn set_lut( |
||||||
|
&mut self, |
||||||
|
_spi: &mut SPI, |
||||||
|
_refresh_rate: Option<RefreshLut>, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
unimplemented!(); |
||||||
|
} |
||||||
|
|
||||||
|
fn is_busy(&self) -> bool { |
||||||
|
self.interface.is_busy(IS_BUSY_LOW) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> Epd5in65f<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn command(&mut self, spi: &mut SPI, command: Command) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, command) |
||||||
|
} |
||||||
|
|
||||||
|
fn send_data(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> { |
||||||
|
self.interface.data(spi, data) |
||||||
|
} |
||||||
|
|
||||||
|
fn cmd_with_data( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
command: Command, |
||||||
|
data: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd_with_data(spi, command, data) |
||||||
|
} |
||||||
|
|
||||||
|
fn wait_busy_high(&mut self) { |
||||||
|
let _ = self.interface.wait_until_idle(true); |
||||||
|
} |
||||||
|
fn wait_busy_low(&mut self) { |
||||||
|
let _ = self.interface.wait_until_idle(false); |
||||||
|
} |
||||||
|
fn send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { |
||||||
|
let w = self.width(); |
||||||
|
let h = self.height(); |
||||||
|
|
||||||
|
self.command(spi, Command::TconResolution)?; |
||||||
|
self.send_data(spi, &[(w >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[w as u8])?; |
||||||
|
self.send_data(spi, &[(h >> 8) as u8])?; |
||||||
|
self.send_data(spi, &[h as u8]) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_vcom(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> { |
||||||
|
let bg_color = (self.color.get_nibble() & 0b111) << 5; |
||||||
|
self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x17 | bg_color])?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn epd_size() { |
||||||
|
assert_eq!(WIDTH, 600); |
||||||
|
assert_eq!(HEIGHT, 448); |
||||||
|
assert_eq!(DEFAULT_BACKGROUND_COLOR, OctColor::White); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,159 @@ |
|||||||
|
//! SPI Commands for the Waveshare 7.5" E-Ink Display
|
||||||
|
|
||||||
|
use crate::traits; |
||||||
|
|
||||||
|
/// EPD7in5 commands
|
||||||
|
///
|
||||||
|
/// Should rarely (never?) be needed directly.
|
||||||
|
///
|
||||||
|
/// For more infos about the addresses and what they are doing look into the PDFs.
|
||||||
|
#[allow(dead_code)] |
||||||
|
#[allow(non_camel_case_types)] |
||||||
|
#[derive(Copy, Clone)] |
||||||
|
pub(crate) enum Command { |
||||||
|
DriverOutputControl = 0x01, |
||||||
|
|
||||||
|
/// Set gate driving voltage
|
||||||
|
GateDrivingVoltageControl = 0x03, |
||||||
|
|
||||||
|
/// Set source driving voltage
|
||||||
|
SourceDrivingVoltageControl = 0x04, |
||||||
|
|
||||||
|
SoftStart = 0x0C, |
||||||
|
|
||||||
|
/// Set the scanning start position of the gate driver.
|
||||||
|
/// The valid range is from 0 to 679.
|
||||||
|
GateScanStartPosition = 0x0F, |
||||||
|
|
||||||
|
/// Deep sleep mode control
|
||||||
|
DeepSleep = 0x10, |
||||||
|
|
||||||
|
/// Define data entry sequence
|
||||||
|
DataEntry = 0x11, |
||||||
|
|
||||||
|
/// resets the commands and parameters to their S/W Reset default values except R10h-Deep Sleep Mode.
|
||||||
|
/// During operation, BUSY pad will output high.
|
||||||
|
/// Note: RAM are unaffected by this command.
|
||||||
|
SwReset = 0x12, |
||||||
|
|
||||||
|
/// After this command initiated, HV Ready detection starts.
|
||||||
|
/// BUSY pad will output high during detection.
|
||||||
|
/// The detection result can be read from the Status Bit Read (Command 0x2F).
|
||||||
|
HvReadyDetection = 0x14, |
||||||
|
|
||||||
|
/// After this command initiated, VCI detection starts.
|
||||||
|
/// BUSY pad will output high during detection.
|
||||||
|
/// The detection result can be read from the Status Bit Read (Command 0x2F).
|
||||||
|
VciDetection = 0x15, |
||||||
|
|
||||||
|
/// Temperature Sensor Selection
|
||||||
|
TemperatureSensorControl = 0x18, |
||||||
|
|
||||||
|
/// Write to temperature register
|
||||||
|
TemperatureSensorWrite = 0x1A, |
||||||
|
|
||||||
|
/// Read from temperature register
|
||||||
|
TemperatureSensorRead = 0x1B, |
||||||
|
|
||||||
|
/// Write Command to External temperature sensor.
|
||||||
|
TemperatureSensorWriteExternal = 0x1C, |
||||||
|
|
||||||
|
/// Activate Display Update Sequence
|
||||||
|
MasterActivation = 0x20, |
||||||
|
|
||||||
|
/// RAM content option for Display Update
|
||||||
|
DisplayUpdateControl1 = 0x21, |
||||||
|
|
||||||
|
/// Display Update Sequence Option
|
||||||
|
DisplayUpdateControl2 = 0x22, |
||||||
|
|
||||||
|
/// After this command, data entries will be written into the BW RAM until another command is written
|
||||||
|
WriteRamBw = 0x24, |
||||||
|
|
||||||
|
/// After this command, data entries will be written into the RED RAM until another command is written
|
||||||
|
WriteRamRed = 0x26, |
||||||
|
|
||||||
|
/// Fetch data from RAM
|
||||||
|
ReadRam = 0x27, |
||||||
|
|
||||||
|
/// Enter VCOM sensing conditions
|
||||||
|
VcomSense = 0x28, |
||||||
|
|
||||||
|
/// Enter VCOM sensing conditions
|
||||||
|
VcomSenseDuration = 0x29, |
||||||
|
|
||||||
|
/// Program VCOM register into OTP
|
||||||
|
VcomProgramOtp = 0x2A, |
||||||
|
|
||||||
|
/// Reduces a glitch when ACVCOM is toggled
|
||||||
|
VcomControl = 0x2B, |
||||||
|
|
||||||
|
/// Write VCOM register from MCU interface
|
||||||
|
VcomWrite = 0x2C, |
||||||
|
|
||||||
|
/// Read Register for Display Option
|
||||||
|
OtpRead = 0x2D, |
||||||
|
|
||||||
|
/// CRC calculation command for OTP content validation
|
||||||
|
CrcCalculation = 0x34, |
||||||
|
|
||||||
|
/// CRC Status Read
|
||||||
|
CrcRead = 0x35, |
||||||
|
|
||||||
|
/// Program OTP Selection according to the OTP Selection Control
|
||||||
|
ProgramSelection = 0x36, |
||||||
|
|
||||||
|
/// Write Register for Display Option
|
||||||
|
DisplayOptionWrite = 0x37, |
||||||
|
|
||||||
|
/// Write register for User ID
|
||||||
|
UserIdWrite = 0x38, |
||||||
|
|
||||||
|
/// Select border waveform for VBD
|
||||||
|
VbdControl = 0x3C, |
||||||
|
|
||||||
|
/// Read RAM Option
|
||||||
|
ReadRamOption = 0x41, |
||||||
|
|
||||||
|
/// Specify the start/end positions of the window address in the X direction by an address unit for RAM
|
||||||
|
SetRamXStartEnd = 0x44, |
||||||
|
|
||||||
|
/// Specify the start/end positions of the window address in the Y direction by an address unit for RAM
|
||||||
|
SetRamYStartEnd = 0x45, |
||||||
|
|
||||||
|
/// Auto write RED RAM for regular pattern
|
||||||
|
AutoWriteRed = 0x46, |
||||||
|
|
||||||
|
/// Auto write B/W RAM for regular pattern
|
||||||
|
AutoWriteBw = 0x47, |
||||||
|
|
||||||
|
/// Make initial settings for the RAM X address in the address counter (AC)
|
||||||
|
SetRamXAc = 0x4E, |
||||||
|
|
||||||
|
/// Make initial settings for the RAM Y address in the address counter (AC)
|
||||||
|
SetRamYAc = 0x4F, |
||||||
|
|
||||||
|
/// This command is an empty command; it does not have any effect on the display module.
|
||||||
|
/// However, it can be used to terminate Frame Memory Write or Read Commands.
|
||||||
|
Nop = 0x7F, |
||||||
|
} |
||||||
|
|
||||||
|
impl traits::Command for Command { |
||||||
|
/// Returns the address of the command
|
||||||
|
fn address(self) -> u8 { |
||||||
|
self as u8 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use crate::traits::Command as CommandTrait; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn command_addr() { |
||||||
|
assert_eq!(Command::MasterActivation.address(), 0x20); |
||||||
|
assert_eq!(Command::SwReset.address(), 0x12); |
||||||
|
assert_eq!(Command::DisplayUpdateControl2.address(), 0x22); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,161 @@ |
|||||||
|
use crate::epd7in5_hd::{DEFAULT_BACKGROUND_COLOR, HEIGHT, WIDTH}; |
||||||
|
use crate::graphics::{Display, DisplayRotation}; |
||||||
|
use embedded_graphics_core::pixelcolor::BinaryColor; |
||||||
|
use embedded_graphics_core::prelude::*; |
||||||
|
|
||||||
|
/// Full size buffer for use with the 7in5 EPD
|
||||||
|
///
|
||||||
|
/// Can also be manually constructed:
|
||||||
|
/// `buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); WIDTH / 8 * HEIGHT]`
|
||||||
|
pub struct Display7in5 { |
||||||
|
buffer: [u8; WIDTH as usize * HEIGHT as usize / 8], |
||||||
|
rotation: DisplayRotation, |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for Display7in5 { |
||||||
|
fn default() -> Self { |
||||||
|
Display7in5 { |
||||||
|
buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); |
||||||
|
WIDTH as usize * HEIGHT as usize / 8], |
||||||
|
rotation: DisplayRotation::default(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl DrawTarget for Display7in5 { |
||||||
|
type Color = BinaryColor; |
||||||
|
type Error = core::convert::Infallible; |
||||||
|
|
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error> |
||||||
|
where |
||||||
|
I: IntoIterator<Item = Pixel<Self::Color>>, |
||||||
|
{ |
||||||
|
for pixel in pixels { |
||||||
|
self.draw_helper(WIDTH, HEIGHT, pixel)?; |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl OriginDimensions for Display7in5 { |
||||||
|
fn size(&self) -> Size { |
||||||
|
Size::new(WIDTH, HEIGHT) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Display for Display7in5 { |
||||||
|
fn buffer(&self) -> &[u8] { |
||||||
|
&self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn get_mut_buffer(&mut self) -> &mut [u8] { |
||||||
|
&mut self.buffer |
||||||
|
} |
||||||
|
|
||||||
|
fn set_rotation(&mut self, rotation: DisplayRotation) { |
||||||
|
self.rotation = rotation; |
||||||
|
} |
||||||
|
|
||||||
|
fn rotation(&self) -> DisplayRotation { |
||||||
|
self.rotation |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
use crate::color::{Black, Color}; |
||||||
|
use crate::epd7in5_hd; |
||||||
|
use crate::graphics::{Display, DisplayRotation}; |
||||||
|
use embedded_graphics::{ |
||||||
|
prelude::*, |
||||||
|
primitives::{Line, PrimitiveStyle}, |
||||||
|
}; |
||||||
|
|
||||||
|
// test buffer length
|
||||||
|
#[test] |
||||||
|
fn graphics_size() { |
||||||
|
let display = Display7in5::default(); |
||||||
|
assert_eq!(display.buffer().len(), 58080); |
||||||
|
} |
||||||
|
|
||||||
|
// test default background color on all bytes
|
||||||
|
#[test] |
||||||
|
fn graphics_default() { |
||||||
|
let display = Display7in5::default(); |
||||||
|
for &byte in display.buffer() { |
||||||
|
assert_eq!(byte, epd7in5_hd::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_0() { |
||||||
|
let mut display = Display7in5::default(); |
||||||
|
|
||||||
|
let _ = Line::new(Point::new(0, 0), Point::new(7, 0)) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
assert_eq!(buffer[0], Color::Black.get_byte_value()); |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!(byte, epd7in5_hd::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_90() { |
||||||
|
let mut display = Display7in5::default(); |
||||||
|
display.set_rotation(DisplayRotation::Rotate90); |
||||||
|
|
||||||
|
let _ = Line::new(Point::new(0, 872), Point::new(0, 879)) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
assert_eq!(buffer[0], Color::Black.get_byte_value()); |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!(byte, epd7in5_hd::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_180() { |
||||||
|
let mut display = Display7in5::default(); |
||||||
|
display.set_rotation(DisplayRotation::Rotate180); |
||||||
|
|
||||||
|
let _ = Line::new(Point::new(872, 527), Point::new(879, 527)) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
assert_eq!(buffer[0], Color::Black.get_byte_value()); |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!(byte, epd7in5_hd::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn graphics_rotation_270() { |
||||||
|
let mut display = Display7in5::default(); |
||||||
|
display.set_rotation(DisplayRotation::Rotate270); |
||||||
|
|
||||||
|
let _ = Line::new(Point::new(527, 0), Point::new(527, 7)) |
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1)) |
||||||
|
.draw(&mut display); |
||||||
|
|
||||||
|
let buffer = display.buffer(); |
||||||
|
|
||||||
|
assert_eq!(buffer[0], Color::Black.get_byte_value()); |
||||||
|
|
||||||
|
for &byte in buffer.iter().skip(1) { |
||||||
|
assert_eq!(byte, epd7in5_hd::DEFAULT_BACKGROUND_COLOR.get_byte_value()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,265 @@ |
|||||||
|
//! A simple Driver for the Waveshare 7.5" E-Ink Display (HD) via SPI
|
||||||
|
//!
|
||||||
|
//! Color values for this driver are inverted compared to the [EPD 7in5 V2 driver](crate::epd7in5_v2)
|
||||||
|
//! *EPD 7in5 HD:* White = 1/0xFF, Black = 0/0x00
|
||||||
|
//! *EPD 7in5 V2:* White = 0/0x00, Black = 1/0xFF
|
||||||
|
//!
|
||||||
|
//! # References
|
||||||
|
//!
|
||||||
|
//! - [Datasheet](https://www.waveshare.com/w/upload/2/27/7inch_HD_e-Paper_Specification.pdf)
|
||||||
|
//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in5_HD.py)
|
||||||
|
//!
|
||||||
|
use embedded_hal::{ |
||||||
|
blocking::{delay::*, spi::Write}, |
||||||
|
digital::v2::{InputPin, OutputPin}, |
||||||
|
}; |
||||||
|
|
||||||
|
use crate::color::Color; |
||||||
|
use crate::interface::DisplayInterface; |
||||||
|
use crate::traits::{InternalWiAdditions, RefreshLut, WaveshareDisplay}; |
||||||
|
|
||||||
|
pub(crate) mod command; |
||||||
|
use self::command::Command; |
||||||
|
|
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
mod graphics; |
||||||
|
#[cfg(feature = "graphics")] |
||||||
|
pub use self::graphics::Display7in5; |
||||||
|
|
||||||
|
/// Width of the display
|
||||||
|
pub const WIDTH: u32 = 880; |
||||||
|
/// Height of the display
|
||||||
|
pub const HEIGHT: u32 = 528; |
||||||
|
/// Default Background Color
|
||||||
|
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White; // Inverted for HD as compared to 7in5 v2 (HD: 0xFF = White)
|
||||||
|
const IS_BUSY_LOW: bool = false; |
||||||
|
|
||||||
|
/// EPD7in5 (HD) driver
|
||||||
|
///
|
||||||
|
pub struct Epd7in5<SPI, CS, BUSY, DC, RST, DELAY> { |
||||||
|
/// Connection Interface
|
||||||
|
interface: DisplayInterface<SPI, CS, BUSY, DC, RST, DELAY>, |
||||||
|
/// Background Color
|
||||||
|
color: Color, |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> InternalWiAdditions<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd7in5<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
// Reset the device
|
||||||
|
self.interface.reset(delay, 2); |
||||||
|
|
||||||
|
// HD procedure as described here:
|
||||||
|
// https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd7in5_HD.py
|
||||||
|
// and as per specs:
|
||||||
|
// https://www.waveshare.com/w/upload/2/27/7inch_HD_e-Paper_Specification.pdf
|
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
self.command(spi, Command::SwReset)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::AutoWriteRed, &[0xF7])?; |
||||||
|
self.wait_until_idle(); |
||||||
|
self.cmd_with_data(spi, Command::AutoWriteBw, &[0xF7])?; |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::SoftStart, &[0xAE, 0xC7, 0xC3, 0xC0, 0x40])?; |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::DriverOutputControl, &[0xAF, 0x02, 0x01])?; |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::DataEntry, &[0x01])?; |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::SetRamXStartEnd, &[0x00, 0x00, 0x6F, 0x03])?; |
||||||
|
self.cmd_with_data(spi, Command::SetRamYStartEnd, &[0xAF, 0x02, 0x00, 0x00])?; |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::VbdControl, &[0x05])?; |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::TemperatureSensorControl, &[0x80])?; |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xB1])?; |
||||||
|
|
||||||
|
self.command(spi, Command::MasterActivation)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::SetRamXAc, &[0x00, 0x00])?; |
||||||
|
self.cmd_with_data(spi, Command::SetRamYAc, &[0x00, 0x00])?; |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> WaveshareDisplay<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
for Epd7in5<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
type DisplayColor = Color; |
||||||
|
fn new( |
||||||
|
spi: &mut SPI, |
||||||
|
cs: CS, |
||||||
|
busy: BUSY, |
||||||
|
dc: DC, |
||||||
|
rst: RST, |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<Self, SPI::Error> { |
||||||
|
let interface = DisplayInterface::new(cs, busy, dc, rst); |
||||||
|
let color = DEFAULT_BACKGROUND_COLOR; |
||||||
|
|
||||||
|
let mut epd = Epd7in5 { interface, color }; |
||||||
|
|
||||||
|
epd.init(spi, delay)?; |
||||||
|
|
||||||
|
Ok(epd) |
||||||
|
} |
||||||
|
|
||||||
|
fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.init(spi, delay) |
||||||
|
} |
||||||
|
|
||||||
|
fn sleep(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.cmd_with_data(spi, Command::DeepSleep, &[0x01])?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
_delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.wait_until_idle(); |
||||||
|
self.cmd_with_data(spi, Command::SetRamYAc, &[0x00, 0x00])?; |
||||||
|
self.cmd_with_data(spi, Command::WriteRamBw, buffer)?; |
||||||
|
self.cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xF7])?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_partial_frame( |
||||||
|
&mut self, |
||||||
|
_spi: &mut SPI, |
||||||
|
_buffer: &[u8], |
||||||
|
_x: u32, |
||||||
|
_y: u32, |
||||||
|
_width: u32, |
||||||
|
_height: u32, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
unimplemented!(); |
||||||
|
} |
||||||
|
|
||||||
|
fn display_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
self.command(spi, Command::MasterActivation)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn update_and_display_frame( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
buffer: &[u8], |
||||||
|
delay: &mut DELAY, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.update_frame(spi, buffer, delay)?; |
||||||
|
self.display_frame(spi, delay)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn clear_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error> { |
||||||
|
let pixel_count = WIDTH * HEIGHT / 8; |
||||||
|
let background_color_byte = self.color.get_byte_value(); |
||||||
|
|
||||||
|
self.wait_until_idle(); |
||||||
|
self.cmd_with_data(spi, Command::SetRamYAc, &[0x00, 0x00])?; |
||||||
|
|
||||||
|
for cmd in &[Command::WriteRamBw, Command::WriteRamRed] { |
||||||
|
self.command(spi, *cmd)?; |
||||||
|
self.interface |
||||||
|
.data_x_times(spi, background_color_byte, pixel_count)?; |
||||||
|
} |
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::DisplayUpdateControl2, &[0xF7])?; |
||||||
|
self.command(spi, Command::MasterActivation)?; |
||||||
|
self.wait_until_idle(); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn set_background_color(&mut self, color: Color) { |
||||||
|
self.color = color; |
||||||
|
} |
||||||
|
|
||||||
|
fn background_color(&self) -> &Color { |
||||||
|
&self.color |
||||||
|
} |
||||||
|
|
||||||
|
fn width(&self) -> u32 { |
||||||
|
WIDTH |
||||||
|
} |
||||||
|
|
||||||
|
fn height(&self) -> u32 { |
||||||
|
HEIGHT |
||||||
|
} |
||||||
|
|
||||||
|
fn set_lut( |
||||||
|
&mut self, |
||||||
|
_spi: &mut SPI, |
||||||
|
_refresh_rate: Option<RefreshLut>, |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
unimplemented!(); |
||||||
|
} |
||||||
|
|
||||||
|
fn is_busy(&self) -> bool { |
||||||
|
self.interface.is_busy(IS_BUSY_LOW) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST, DELAY> Epd7in5<SPI, CS, BUSY, DC, RST, DELAY> |
||||||
|
where |
||||||
|
SPI: Write<u8>, |
||||||
|
CS: OutputPin, |
||||||
|
BUSY: InputPin, |
||||||
|
DC: OutputPin, |
||||||
|
RST: OutputPin, |
||||||
|
DELAY: DelayMs<u8>, |
||||||
|
{ |
||||||
|
fn command(&mut self, spi: &mut SPI, command: Command) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd(spi, command) |
||||||
|
} |
||||||
|
|
||||||
|
fn cmd_with_data( |
||||||
|
&mut self, |
||||||
|
spi: &mut SPI, |
||||||
|
command: Command, |
||||||
|
data: &[u8], |
||||||
|
) -> Result<(), SPI::Error> { |
||||||
|
self.interface.cmd_with_data(spi, command, data) |
||||||
|
} |
||||||
|
|
||||||
|
fn wait_until_idle(&mut self) { |
||||||
|
self.interface.wait_until_idle(IS_BUSY_LOW) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn epd_size() { |
||||||
|
assert_eq!(WIDTH, 880); |
||||||
|
assert_eq!(HEIGHT, 528); |
||||||
|
assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue