Initial support for Waveshare 2in13 v2 e-ink screen
This initial support covers both the full (slow) update of the display and the partial (fast) update.embedded-hal-1.0
parent
82b8c98538
commit
27e367c89c
|
|
@ -18,6 +18,7 @@ edition = "2018"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
embedded-graphics = { version = "0.6.1", optional = true}
|
embedded-graphics = { version = "0.6.1", optional = true}
|
||||||
embedded-hal = {version = "0.2.3", features = ["unproven"]}
|
embedded-hal = {version = "0.2.3", features = ["unproven"]}
|
||||||
|
bit_field = "0.10.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
linux-embedded-hal = "0.3"
|
linux-embedded-hal = "0.3"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
#![deny(warnings)]
|
||||||
|
|
||||||
|
use embedded_graphics::{
|
||||||
|
fonts::{Font12x16, Font6x8, Text},
|
||||||
|
prelude::*,
|
||||||
|
primitives::{Circle, Line},
|
||||||
|
style::PrimitiveStyle,
|
||||||
|
text_style,
|
||||||
|
};
|
||||||
|
use embedded_hal::prelude::*;
|
||||||
|
use epd_waveshare::{
|
||||||
|
color::*,
|
||||||
|
epd2in13_v2::{Display2in13, EPD2in13},
|
||||||
|
graphics::{Display, DisplayRotation},
|
||||||
|
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
|
||||||
|
|
||||||
|
fn main() -> Result<(), std::io::Error> {
|
||||||
|
// 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(4_000_000)
|
||||||
|
.mode(spidev::SpiModeFlags::SPI_MODE_0)
|
||||||
|
.build();
|
||||||
|
spi.configure(&options).expect("spi configuration");
|
||||||
|
|
||||||
|
// Configure Digital I/O Pin to be used as Chip Select for SPI
|
||||||
|
let cs = Pin::new(26); //BCM7 CE0
|
||||||
|
cs.export().expect("cs export");
|
||||||
|
while !cs.is_exported() {}
|
||||||
|
cs.set_direction(Direction::Out).expect("CS Direction");
|
||||||
|
cs.set_value(1).expect("CS Value set to 1");
|
||||||
|
|
||||||
|
let busy = Pin::new(5); //pin 29
|
||||||
|
busy.export().expect("busy export");
|
||||||
|
while !busy.is_exported() {}
|
||||||
|
busy.set_direction(Direction::In).expect("busy Direction");
|
||||||
|
//busy.set_value(1).expect("busy Value set to 1");
|
||||||
|
|
||||||
|
let dc = Pin::new(6); //pin 31 //bcm6
|
||||||
|
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(16); //pin 36 //bcm16
|
||||||
|
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");
|
||||||
|
|
||||||
|
let mut delay = Delay {};
|
||||||
|
|
||||||
|
let mut epd2in13 =
|
||||||
|
EPD2in13::new(&mut spi, cs, busy, dc, rst, &mut delay).expect("eink initalize error");
|
||||||
|
|
||||||
|
//println!("Test all the rotations");
|
||||||
|
let mut display = Display2in13::default();
|
||||||
|
|
||||||
|
display.set_rotation(DisplayRotation::Rotate0);
|
||||||
|
draw_text(&mut display, "Rotate 0!", 5, 50);
|
||||||
|
|
||||||
|
display.set_rotation(DisplayRotation::Rotate90);
|
||||||
|
draw_text(&mut display, "Rotate 90!", 5, 50);
|
||||||
|
|
||||||
|
display.set_rotation(DisplayRotation::Rotate180);
|
||||||
|
draw_text(&mut display, "Rotate 180!", 5, 50);
|
||||||
|
|
||||||
|
display.set_rotation(DisplayRotation::Rotate270);
|
||||||
|
draw_text(&mut display, "Rotate 270!", 5, 50);
|
||||||
|
|
||||||
|
epd2in13.update_frame(&mut spi, &display.buffer())?;
|
||||||
|
epd2in13
|
||||||
|
.display_frame(&mut spi)
|
||||||
|
.expect("display frame new graphics");
|
||||||
|
delay.delay_ms(5000u16);
|
||||||
|
|
||||||
|
//println!("Now test new graphics with default rotation and some special stuff:");
|
||||||
|
display.clear_buffer(Color::White);
|
||||||
|
|
||||||
|
// draw a analog clock
|
||||||
|
let _ = Circle::new(Point::new(64, 64), 40)
|
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1))
|
||||||
|
.draw(&mut display);
|
||||||
|
let _ = Line::new(Point::new(64, 64), Point::new(30, 40))
|
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 4))
|
||||||
|
.draw(&mut display);
|
||||||
|
let _ = Line::new(Point::new(64, 64), Point::new(80, 40))
|
||||||
|
.into_styled(PrimitiveStyle::with_stroke(Black, 1))
|
||||||
|
.draw(&mut display);
|
||||||
|
|
||||||
|
// draw white on black background
|
||||||
|
let _ = Text::new("It's working-WoB!", Point::new(90, 10))
|
||||||
|
.into_styled(text_style!(
|
||||||
|
font = Font6x8,
|
||||||
|
text_color = White,
|
||||||
|
background_color = Black
|
||||||
|
))
|
||||||
|
.draw(&mut display);
|
||||||
|
|
||||||
|
// use bigger/different font
|
||||||
|
let _ = Text::new("It's working-WoB!", Point::new(90, 40))
|
||||||
|
.into_styled(text_style!(
|
||||||
|
font = Font12x16,
|
||||||
|
text_color = White,
|
||||||
|
background_color = Black
|
||||||
|
))
|
||||||
|
.draw(&mut display);
|
||||||
|
|
||||||
|
// Demonstrating how to use the partial refresh feature of the screen.
|
||||||
|
// Real animations can be used.
|
||||||
|
epd2in13
|
||||||
|
.set_refresh(&mut spi, &mut delay, RefreshLUT::QUICK)
|
||||||
|
.unwrap();
|
||||||
|
epd2in13.clear_frame(&mut spi).unwrap();
|
||||||
|
|
||||||
|
// a moving `Hello World!`
|
||||||
|
let limit = 10;
|
||||||
|
for i in 0..limit {
|
||||||
|
draw_text(&mut display, " Hello World! ", 5 + i * 12, 50);
|
||||||
|
|
||||||
|
epd2in13
|
||||||
|
.update_and_display_frame(&mut spi, &display.buffer())
|
||||||
|
.expect("display frame new graphics");
|
||||||
|
delay.delay_ms(1_000u16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a spinning bar without any delay between frames. Shows how «fast»
|
||||||
|
// the screen can refresh for this kind of change (small single character)
|
||||||
|
display.clear_buffer(Color::White);
|
||||||
|
epd2in13
|
||||||
|
.update_and_display_frame(&mut spi, &display.buffer())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spinner = ["|", "/", "-", "\\"];
|
||||||
|
for i in 0..10 {
|
||||||
|
display.clear_buffer(Color::White);
|
||||||
|
draw_text(&mut display, spinner[i % spinner.len()], 10, 100);
|
||||||
|
epd2in13
|
||||||
|
.update_and_display_frame(&mut spi, &display.buffer())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Finished tests - going to sleep");
|
||||||
|
epd2in13.sleep(&mut spi)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_text(display: &mut Display2in13, text: &str, x: i32, y: i32) {
|
||||||
|
let _ = Text::new(text, Point::new(x, y))
|
||||||
|
.into_styled(text_style!(
|
||||||
|
font = Font6x8,
|
||||||
|
text_color = Black,
|
||||||
|
background_color = White
|
||||||
|
))
|
||||||
|
.draw(display);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,287 @@
|
||||||
|
//! SPI Commands for the Waveshare 2.13" v2
|
||||||
|
|
||||||
|
use crate::traits;
|
||||||
|
|
||||||
|
extern crate bit_field;
|
||||||
|
use bit_field::BitField;
|
||||||
|
|
||||||
|
/// EPD2in13 v2
|
||||||
|
///
|
||||||
|
/// 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 {
|
||||||
|
DRIVER_OUTPUT_CONTROL = 0x01,
|
||||||
|
GATE_DRIVING_VOLTAGE_CTRL = 0x03,
|
||||||
|
SOURCE_DRIVING_VOLTAGE_CTRL = 0x04,
|
||||||
|
BOOSTER_SOFT_START_CONTROL = 0x0C,
|
||||||
|
GATE_SCAN_START_POSITION = 0x0F,
|
||||||
|
DEEP_SLEEP_MODE = 0x10,
|
||||||
|
DATA_ENTRY_MODE_SETTING = 0x11,
|
||||||
|
SW_RESET = 0x12,
|
||||||
|
HV_READY_DETECTION = 0x14,
|
||||||
|
VCI_DETECTION = 0x15,
|
||||||
|
TEMPERATURE_SENSOR_CONTROL_WRITE = 0x1A,
|
||||||
|
TEMPERATURE_SENSOR_CONTROL_READ = 0x1B,
|
||||||
|
TEMPERATURE_SENSOR_EXT_CONTROL_WRITE = 0x1C,
|
||||||
|
MASTER_ACTIVATION = 0x20,
|
||||||
|
DISPLAY_UPDATE_CONTROL_1 = 0x21,
|
||||||
|
DISPLAY_UPDATE_CONTROL_2 = 0x22,
|
||||||
|
WRITE_RAM = 0x24,
|
||||||
|
WRITE_RAM_RED = 0x26,
|
||||||
|
READ_RAM = 0x27,
|
||||||
|
VCOM_SENSE = 0x28,
|
||||||
|
VCOM_SENSE_DURATION = 0x29,
|
||||||
|
PROGRAM_VCOM_OPT = 0x2A,
|
||||||
|
WRITE_VCOM_REGISTER = 0x2C,
|
||||||
|
OTP_REGISTER_READ = 0x2D,
|
||||||
|
STATUS_BIT_READ = 0x2F,
|
||||||
|
PROGRAM_WS_OTP = 0x30,
|
||||||
|
LOAD_WS_OTP = 0x31,
|
||||||
|
WRITE_LUT_REGISTER = 0x32,
|
||||||
|
PROGRAM_OTP_SELECTION = 0x36,
|
||||||
|
WRITE_OTP_SELECTION = 0x37,
|
||||||
|
SET_DUMMY_LINE_PERIOD = 0x3A,
|
||||||
|
SET_GATE_LINE_WIDTH = 0x3B,
|
||||||
|
BORDER_WAVEFORM_CONTROL = 0x3C,
|
||||||
|
READ_RAM_OPTION = 0x41,
|
||||||
|
SET_RAM_X_ADDRESS_START_END_POSITION = 0x44,
|
||||||
|
SET_RAM_Y_ADDRESS_START_END_POSITION = 0x45,
|
||||||
|
AUTO_WRITE_RED_RAM_REGULAR_PATTERN = 0x46,
|
||||||
|
AUTO_WRITE_BW_RAM_REGULAR_PATTERN = 0x47,
|
||||||
|
SET_RAM_X_ADDRESS_COUNTER = 0x4E,
|
||||||
|
SET_RAM_Y_ADDRESS_COUNTER = 0x4F,
|
||||||
|
SET_ANALOG_BLOCK_CONTROL = 0x74,
|
||||||
|
SET_DIGITAL_BLOCK_CONTROL = 0x7E,
|
||||||
|
|
||||||
|
NOP = 0x7F,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DriverOutput {
|
||||||
|
pub scan_is_linear: bool,
|
||||||
|
pub scan_g0_is_first: bool,
|
||||||
|
pub scan_dir_incr: bool,
|
||||||
|
|
||||||
|
pub width: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DriverOutput {
|
||||||
|
pub fn to_bytes(&self) -> [u8; 3] {
|
||||||
|
[
|
||||||
|
self.width as u8,
|
||||||
|
(self.width >> 8) as u8,
|
||||||
|
*0u8.set_bit(0, !self.scan_dir_incr)
|
||||||
|
.set_bit(1, !self.scan_g0_is_first)
|
||||||
|
.set_bit(2, !self.scan_is_linear),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// These are not directly documented, but the bitfield is easily reversed from
|
||||||
|
/// documentation and sample code
|
||||||
|
/// [7|6|5|4|3|2|1|0]
|
||||||
|
/// | | | | | | | `--- disable clock
|
||||||
|
/// | | | | | | `----- disable analog
|
||||||
|
/// | | | | | `------- display
|
||||||
|
/// | | | | `--------- undocumented and unknown use,
|
||||||
|
/// | | | | but used in waveshare reference code
|
||||||
|
/// | | | `----------- load LUT
|
||||||
|
/// | | `------------- load temp
|
||||||
|
/// | `--------------- enable clock
|
||||||
|
/// `----------------- enable analog
|
||||||
|
|
||||||
|
pub(crate) struct DisplayUpdateControl2(pub u8);
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl DisplayUpdateControl2 {
|
||||||
|
pub fn new() -> DisplayUpdateControl2 {
|
||||||
|
DisplayUpdateControl2(0x00)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable_clock(mut self) -> Self {
|
||||||
|
self.0.set_bit(0, true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable_analog(mut self) -> Self {
|
||||||
|
self.0.set_bit(1, true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display(mut self) -> Self {
|
||||||
|
self.0.set_bit(2, true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_lut(mut self) -> Self {
|
||||||
|
self.0.set_bit(4, true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_temp(mut self) -> Self {
|
||||||
|
self.0.set_bit(5, true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_clock(mut self) -> Self {
|
||||||
|
self.0.set_bit(6, true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_analog(mut self) -> Self {
|
||||||
|
self.0.set_bit(7, true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub(crate) enum DataEntryModeIncr {
|
||||||
|
X_DECR_Y_DECR = 0x0,
|
||||||
|
X_INCR_Y_DECR = 0x1,
|
||||||
|
X_DECR_Y_INCR = 0x2,
|
||||||
|
X_INCR_Y_INCR = 0x3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub(crate) enum DataEntryModeDir {
|
||||||
|
X_DIR = 0x0,
|
||||||
|
Y_DIR = 0x4,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub(crate) enum BorderWaveFormVBD {
|
||||||
|
GS = 0x0,
|
||||||
|
FIX_LEVEL = 0x1,
|
||||||
|
VCOM = 0x2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub(crate) enum BorderWaveFormFixLevel {
|
||||||
|
VSS = 0x0,
|
||||||
|
VSH1 = 0x1,
|
||||||
|
VSL = 0x2,
|
||||||
|
VSH2 = 0x3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub(crate) enum BorderWaveFormGS {
|
||||||
|
LUT0 = 0x0,
|
||||||
|
LUT1 = 0x1,
|
||||||
|
LUT2 = 0x2,
|
||||||
|
LUT3 = 0x3,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct BorderWaveForm {
|
||||||
|
pub vbd: BorderWaveFormVBD,
|
||||||
|
pub fix_level: BorderWaveFormFixLevel,
|
||||||
|
pub gs_trans: BorderWaveFormGS,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorderWaveForm {
|
||||||
|
pub fn to_u8(&self) -> u8 {
|
||||||
|
*0u8.set_bits(6..8, self.vbd as u8)
|
||||||
|
.set_bits(4..6, self.fix_level as u8)
|
||||||
|
.set_bits(0..2, self.gs_trans as u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum DeepSleepMode {
|
||||||
|
// Sleeps and keeps access to RAM and controller
|
||||||
|
NORMAL = 0x00,
|
||||||
|
|
||||||
|
// Sleeps without access to RAM/controller but keeps RAM content
|
||||||
|
MODE_1 = 0x01,
|
||||||
|
|
||||||
|
// Same as MODE_1 but RAM content is not kept
|
||||||
|
MODE_2 = 0x11,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct GateDrivingVoltage(pub u8);
|
||||||
|
pub(crate) struct SourceDrivingVoltage(pub u8);
|
||||||
|
pub(crate) struct VCOM(pub u8);
|
||||||
|
|
||||||
|
pub(crate) trait I32Ext {
|
||||||
|
fn vcom(self) -> VCOM;
|
||||||
|
fn gate_driving_decivolt(self) -> GateDrivingVoltage;
|
||||||
|
fn source_driving_decivolt(self) -> SourceDrivingVoltage;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl I32Ext for i32 {
|
||||||
|
// This is really not very nice. Until I find something better, this will be
|
||||||
|
// a placeholder.
|
||||||
|
fn vcom(self) -> VCOM {
|
||||||
|
assert!(self >= -30 && self <= -2);
|
||||||
|
let u = match -self {
|
||||||
|
2 => 0x08,
|
||||||
|
3 => 0x0B,
|
||||||
|
4 => 0x10,
|
||||||
|
5 => 0x14,
|
||||||
|
6 => 0x17,
|
||||||
|
7 => 0x1B,
|
||||||
|
8 => 0x20,
|
||||||
|
9 => 0x24,
|
||||||
|
10 => 0x28,
|
||||||
|
11 => 0x2C,
|
||||||
|
12 => 0x2F,
|
||||||
|
13 => 0x34,
|
||||||
|
14 => 0x37,
|
||||||
|
15 => 0x3C,
|
||||||
|
16 => 0x40,
|
||||||
|
17 => 0x44,
|
||||||
|
18 => 0x48,
|
||||||
|
19 => 0x4B,
|
||||||
|
20 => 0x50,
|
||||||
|
21 => 0x54,
|
||||||
|
22 => 0x58,
|
||||||
|
23 => 0x5B,
|
||||||
|
24 => 0x5F,
|
||||||
|
25 => 0x64,
|
||||||
|
26 => 0x68,
|
||||||
|
27 => 0x6C,
|
||||||
|
28 => 0x6F,
|
||||||
|
29 => 0x73,
|
||||||
|
30 => 0x78,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
VCOM(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gate_driving_decivolt(self) -> GateDrivingVoltage {
|
||||||
|
assert!(self >= 100 && self <= 210 && self % 5 == 0);
|
||||||
|
GateDrivingVoltage(((self - 100) / 5 + 0x03) as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_driving_decivolt(self) -> SourceDrivingVoltage {
|
||||||
|
assert!(
|
||||||
|
(self >= 24 && self <= 88)
|
||||||
|
|| (self >= 90 && self <= 180 && self % 5 == 0)
|
||||||
|
|| (self >= -180 && self <= -90 && self % 5 == 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if self >= 24 && self <= 88 {
|
||||||
|
SourceDrivingVoltage(((self - 24) + 0x8E) as u8)
|
||||||
|
} else if self >= 90 && self <= 180 {
|
||||||
|
SourceDrivingVoltage(((self - 90) / 2 + 0x23) as u8)
|
||||||
|
} else {
|
||||||
|
SourceDrivingVoltage((((-self - 90) / 5) * 2 + 0x1A) as u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl traits::Command for Command {
|
||||||
|
/// Returns the address of the command
|
||||||
|
fn address(self) -> u8 {
|
||||||
|
self as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#[rustfmt::skip]
|
||||||
|
|
||||||
|
// Original Waveforms from Waveshare
|
||||||
|
pub(crate) const LUT_FULL_UPDATE: [u8; 70] =[
|
||||||
|
0x80,0x60,0x40,0x00,0x00,0x00,0x00, // LUT0: BB: VS 0 ~7
|
||||||
|
0x10,0x60,0x20,0x00,0x00,0x00,0x00, // LUT1: BW: VS 0 ~7
|
||||||
|
0x80,0x60,0x40,0x00,0x00,0x00,0x00, // LUT2: WB: VS 0 ~7
|
||||||
|
0x10,0x60,0x20,0x00,0x00,0x00,0x00, // LUT3: WW: VS 0 ~7
|
||||||
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00, // LUT4: VCOM: VS 0 ~7
|
||||||
|
|
||||||
|
0x03,0x03,0x00,0x00,0x02, // TP0 A~D RP0
|
||||||
|
0x09,0x09,0x00,0x00,0x02, // TP1 A~D RP1
|
||||||
|
0x03,0x03,0x00,0x00,0x02, // TP2 A~D RP2
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP3 A~D RP3
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP4 A~D RP4
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP5 A~D RP5
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP6 A~D RP6
|
||||||
|
];
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub(crate) const LUT_PARTIAL_UPDATE: [u8; 70] =[
|
||||||
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00, // LUT0: BB: VS 0 ~7
|
||||||
|
0x80,0x00,0x00,0x00,0x00,0x00,0x00, // LUT1: BW: VS 0 ~7
|
||||||
|
0x40,0x00,0x00,0x00,0x00,0x00,0x00, // LUT2: WB: VS 0 ~7
|
||||||
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00, // LUT3: WW: VS 0 ~7
|
||||||
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00, // LUT4: VCOM: VS 0 ~7
|
||||||
|
|
||||||
|
0x0A,0x00,0x00,0x00,0x00, // TP0 A~D RP0
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP1 A~D RP1
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP2 A~D RP2
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP3 A~D RP3
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP4 A~D RP4
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP5 A~D RP5
|
||||||
|
0x00,0x00,0x00,0x00,0x00, // TP6 A~D RP6
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
use crate::buffer_len;
|
||||||
|
use crate::epd2in13_v2::{DEFAULT_BACKGROUND_COLOR, HEIGHT, WIDTH};
|
||||||
|
use crate::graphics::{Display, DisplayRotation};
|
||||||
|
use embedded_graphics::pixelcolor::BinaryColor;
|
||||||
|
use embedded_graphics::prelude::*;
|
||||||
|
|
||||||
|
/// Full size buffer for use with the 2in13 v2 EPD
|
||||||
|
///
|
||||||
|
/// Can also be manually constructed:
|
||||||
|
/// `buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value(); WIDTH / 8 * HEIGHT]`
|
||||||
|
pub struct Display2in13 {
|
||||||
|
buffer: [u8; buffer_len(WIDTH as usize, HEIGHT as usize)],
|
||||||
|
rotation: DisplayRotation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Display2in13 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Display2in13 {
|
||||||
|
buffer: [DEFAULT_BACKGROUND_COLOR.get_byte_value();
|
||||||
|
buffer_len(WIDTH as usize, HEIGHT as usize)],
|
||||||
|
rotation: DisplayRotation::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawTarget<BinaryColor> for Display2in13 {
|
||||||
|
type Error = core::convert::Infallible;
|
||||||
|
|
||||||
|
fn draw_pixel(&mut self, pixel: Pixel<BinaryColor>) -> Result<(), Self::Error> {
|
||||||
|
self.draw_helper(WIDTH, HEIGHT, pixel)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
Size::new(WIDTH, HEIGHT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Display2in13 {
|
||||||
|
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::epd2in13_v2;
|
||||||
|
use crate::graphics::{Display, DisplayRotation};
|
||||||
|
use embedded_graphics::{primitives::Line, style::PrimitiveStyle};
|
||||||
|
|
||||||
|
// test buffer length
|
||||||
|
#[test]
|
||||||
|
fn graphics_size() {
|
||||||
|
let display = Display2in13::default();
|
||||||
|
assert_eq!(display.buffer().len(), 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// test default background color on all bytes
|
||||||
|
#[test]
|
||||||
|
fn graphics_default() {
|
||||||
|
let display = Display2in13::default();
|
||||||
|
for &byte in display.buffer() {
|
||||||
|
assert_eq!(byte, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graphics_rotation_0() {
|
||||||
|
let mut display = Display2in13::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, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graphics_rotation_90() {
|
||||||
|
let mut display = Display2in13::default();
|
||||||
|
display.set_rotation(DisplayRotation::Rotate90);
|
||||||
|
|
||||||
|
let _ = Line::new(
|
||||||
|
Point::new(0, (WIDTH - 8) as i32),
|
||||||
|
Point::new(0, (WIDTH - 1) as i32),
|
||||||
|
)
|
||||||
|
.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, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graphics_rotation_180() {
|
||||||
|
let mut display = Display2in13::default();
|
||||||
|
display.set_rotation(DisplayRotation::Rotate180);
|
||||||
|
|
||||||
|
let _ = Line::new(
|
||||||
|
Point::new((WIDTH - 8) as i32, (HEIGHT - 1) as i32),
|
||||||
|
Point::new((WIDTH - 1) as i32, (HEIGHT - 1) as i32),
|
||||||
|
)
|
||||||
|
.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, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graphics_rotation_270() {
|
||||||
|
let mut display = Display2in13::default();
|
||||||
|
display.set_rotation(DisplayRotation::Rotate270);
|
||||||
|
|
||||||
|
let _ = Line::new(
|
||||||
|
Point::new((HEIGHT - 1) as i32, 0),
|
||||||
|
Point::new((HEIGHT - 1) as i32, 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, epd2in13_v2::DEFAULT_BACKGROUND_COLOR.get_byte_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,575 @@
|
||||||
|
//! A Driver for the Waveshare 2.13" E-Ink Display (V2) via SPI
|
||||||
|
//!
|
||||||
|
//! # References
|
||||||
|
//!
|
||||||
|
//! - [Waveshare product page](https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT)
|
||||||
|
//! - [Waveshare C driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/c/lib/e-Paper/EPD_2in13_V2.c)
|
||||||
|
//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd2in13_V2.py)
|
||||||
|
//! - [Controller Datasheet SS1780](http://www.e-paper-display.com/download_detail/downloadsId=682.html)
|
||||||
|
//!
|
||||||
|
|
||||||
|
use embedded_hal::{
|
||||||
|
blocking::{delay::*, spi::Write},
|
||||||
|
digital::v2::{InputPin, OutputPin},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::buffer_len;
|
||||||
|
use crate::color::Color;
|
||||||
|
use crate::interface::DisplayInterface;
|
||||||
|
use crate::traits::{InternalWiAdditions, RefreshLUT, WaveshareDisplay};
|
||||||
|
|
||||||
|
pub(crate) mod command;
|
||||||
|
use self::command::{
|
||||||
|
BorderWaveForm, BorderWaveFormFixLevel, BorderWaveFormGS, BorderWaveFormVBD, Command,
|
||||||
|
DataEntryModeDir, DataEntryModeIncr, DeepSleepMode, DisplayUpdateControl2, DriverOutput,
|
||||||
|
GateDrivingVoltage, I32Ext, SourceDrivingVoltage, VCOM,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) mod constants;
|
||||||
|
use self::constants::{LUT_FULL_UPDATE, LUT_PARTIAL_UPDATE};
|
||||||
|
|
||||||
|
#[cfg(feature = "graphics")]
|
||||||
|
mod graphics;
|
||||||
|
#[cfg(feature = "graphics")]
|
||||||
|
pub use self::graphics::Display2in13;
|
||||||
|
|
||||||
|
/// Width of the display.
|
||||||
|
pub const WIDTH: u32 = 122;
|
||||||
|
|
||||||
|
/// Height of the display
|
||||||
|
pub const HEIGHT: u32 = 250;
|
||||||
|
|
||||||
|
/// Default Background Color
|
||||||
|
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White;
|
||||||
|
const IS_BUSY_LOW: bool = false;
|
||||||
|
|
||||||
|
/// EPD2in13 (V2) driver
|
||||||
|
///
|
||||||
|
pub struct EPD2in13<SPI, CS, BUSY, DC, RST> {
|
||||||
|
/// Connection Interface
|
||||||
|
interface: DisplayInterface<SPI, CS, BUSY, DC, RST>,
|
||||||
|
|
||||||
|
sleep_mode: DeepSleepMode,
|
||||||
|
|
||||||
|
/// Background Color
|
||||||
|
background_color: Color,
|
||||||
|
refresh: RefreshLUT,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST> InternalWiAdditions<SPI, CS, BUSY, DC, RST>
|
||||||
|
for EPD2in13<SPI, CS, BUSY, DC, RST>
|
||||||
|
where
|
||||||
|
SPI: Write<u8>,
|
||||||
|
CS: OutputPin,
|
||||||
|
BUSY: InputPin,
|
||||||
|
DC: OutputPin,
|
||||||
|
RST: OutputPin,
|
||||||
|
{
|
||||||
|
fn init<DELAY: DelayMs<u8>>(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
delay: &mut DELAY,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
// HW reset
|
||||||
|
self.interface.reset(delay);
|
||||||
|
|
||||||
|
if self.refresh == RefreshLUT::QUICK {
|
||||||
|
self.set_vcom_register(spi, (-9).vcom())?;
|
||||||
|
self.wait_until_idle();
|
||||||
|
|
||||||
|
self.set_lut(spi, Some(self.refresh))?;
|
||||||
|
|
||||||
|
// Python code does this, not sure why
|
||||||
|
// self.cmd_with_data(spi, Command::WRITE_OTP_SELECTION, &[0, 0, 0, 0, 0x40, 0, 0])?;
|
||||||
|
|
||||||
|
// During partial update, clock/analog are not disabled between 2
|
||||||
|
// updates.
|
||||||
|
self.set_display_update_control_2(
|
||||||
|
spi,
|
||||||
|
DisplayUpdateControl2::new().enable_analog().enable_clock(),
|
||||||
|
)?;
|
||||||
|
self.command(spi, Command::MASTER_ACTIVATION)?;
|
||||||
|
self.wait_until_idle();
|
||||||
|
|
||||||
|
self.set_border_waveform(
|
||||||
|
spi,
|
||||||
|
BorderWaveForm {
|
||||||
|
vbd: BorderWaveFormVBD::GS,
|
||||||
|
fix_level: BorderWaveFormFixLevel::VSS,
|
||||||
|
gs_trans: BorderWaveFormGS::LUT1,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
self.wait_until_idle();
|
||||||
|
self.command(spi, Command::SW_RESET)?;
|
||||||
|
self.wait_until_idle();
|
||||||
|
|
||||||
|
self.set_driver_output(
|
||||||
|
spi,
|
||||||
|
DriverOutput {
|
||||||
|
scan_is_linear: true,
|
||||||
|
scan_g0_is_first: true,
|
||||||
|
scan_dir_incr: true,
|
||||||
|
width: (HEIGHT - 1) as u16,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// These 2 are the reset values
|
||||||
|
self.set_dummy_line_period(spi, 0x30)?;
|
||||||
|
self.set_gate_scan_start_position(spi, 0)?;
|
||||||
|
|
||||||
|
self.set_data_entry_mode(
|
||||||
|
spi,
|
||||||
|
DataEntryModeIncr::X_INCR_Y_INCR,
|
||||||
|
DataEntryModeDir::X_DIR,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Use simple X/Y auto increase
|
||||||
|
self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?;
|
||||||
|
self.set_ram_address_counters(spi, 0, 0)?;
|
||||||
|
|
||||||
|
self.set_border_waveform(
|
||||||
|
spi,
|
||||||
|
BorderWaveForm {
|
||||||
|
vbd: BorderWaveFormVBD::GS,
|
||||||
|
fix_level: BorderWaveFormFixLevel::VSS,
|
||||||
|
gs_trans: BorderWaveFormGS::LUT3,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.set_vcom_register(spi, (-21).vcom())?;
|
||||||
|
|
||||||
|
self.set_gate_driving_voltage(spi, 190.gate_driving_decivolt())?;
|
||||||
|
self.set_source_driving_voltage(
|
||||||
|
spi,
|
||||||
|
150.source_driving_decivolt(),
|
||||||
|
50.source_driving_decivolt(),
|
||||||
|
(-150).source_driving_decivolt(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.set_gate_line_width(spi, 10)?;
|
||||||
|
|
||||||
|
self.set_lut(spi, Some(self.refresh))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wait_until_idle();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST> WaveshareDisplay<SPI, CS, BUSY, DC, RST>
|
||||||
|
for EPD2in13<SPI, CS, BUSY, DC, RST>
|
||||||
|
where
|
||||||
|
SPI: Write<u8>,
|
||||||
|
CS: OutputPin,
|
||||||
|
BUSY: InputPin,
|
||||||
|
DC: OutputPin,
|
||||||
|
RST: OutputPin,
|
||||||
|
{
|
||||||
|
fn new<DELAY: DelayMs<u8>>(
|
||||||
|
spi: &mut SPI,
|
||||||
|
cs: CS,
|
||||||
|
busy: BUSY,
|
||||||
|
dc: DC,
|
||||||
|
rst: RST,
|
||||||
|
delay: &mut DELAY,
|
||||||
|
) -> Result<Self, SPI::Error> {
|
||||||
|
let mut epd = EPD2in13 {
|
||||||
|
interface: DisplayInterface::new(cs, busy, dc, rst),
|
||||||
|
sleep_mode: DeepSleepMode::MODE_1,
|
||||||
|
background_color: DEFAULT_BACKGROUND_COLOR,
|
||||||
|
refresh: RefreshLUT::FULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
epd.init(spi, delay)?;
|
||||||
|
Ok(epd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wake_up<DELAY: DelayMs<u8>>(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
delay: &mut DELAY,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
self.init(spi, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sleep(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> {
|
||||||
|
self.wait_until_idle();
|
||||||
|
|
||||||
|
// All sample code enables and disables analog/clocks...
|
||||||
|
self.set_display_update_control_2(
|
||||||
|
spi,
|
||||||
|
DisplayUpdateControl2::new()
|
||||||
|
.enable_analog()
|
||||||
|
.enable_clock()
|
||||||
|
.disable_analog()
|
||||||
|
.disable_clock(),
|
||||||
|
)?;
|
||||||
|
self.command(spi, Command::MASTER_ACTIVATION)?;
|
||||||
|
|
||||||
|
self.set_sleep_mode(spi, self.sleep_mode)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_frame(&mut self, spi: &mut SPI, buffer: &[u8]) -> Result<(), SPI::Error> {
|
||||||
|
assert!(buffer.len() == buffer_len(WIDTH as usize, HEIGHT as usize));
|
||||||
|
self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?;
|
||||||
|
self.set_ram_address_counters(spi, 0, 0)?;
|
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::WRITE_RAM, buffer)?;
|
||||||
|
|
||||||
|
if self.refresh == RefreshLUT::FULL {
|
||||||
|
// Always keep the base buffer equal to current if not doing partial refresh.
|
||||||
|
self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?;
|
||||||
|
self.set_ram_address_counters(spi, 0, 0)?;
|
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::WRITE_RAM_RED, buffer)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updating only a part of the frame is not supported when using the
|
||||||
|
/// partial refresh feature. The function will panic if called when set to
|
||||||
|
/// use partial refresh.
|
||||||
|
fn update_partial_frame(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
buffer: &[u8],
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
assert!((width * height / 8) as usize == buffer.len());
|
||||||
|
|
||||||
|
// This should not be used when doing partial refresh. The RAM_RED must
|
||||||
|
// be updated with the last buffer having been displayed. Doing partial
|
||||||
|
// update directly in RAM makes this update impossible (we can't read
|
||||||
|
// RAM content). Using this function will most probably make the actual
|
||||||
|
// display incorrect as the controler will compare with something
|
||||||
|
// incorrect.
|
||||||
|
assert!(self.refresh == RefreshLUT::FULL);
|
||||||
|
|
||||||
|
self.set_ram_area(spi, x, y, x + width, y + height)?;
|
||||||
|
self.set_ram_address_counters(spi, x, y)?;
|
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::WRITE_RAM, buffer)?;
|
||||||
|
|
||||||
|
if self.refresh == RefreshLUT::FULL {
|
||||||
|
// Always keep the base buffer equals to current if not doing partial refresh.
|
||||||
|
self.set_ram_area(spi, x, y, x + width, y + height)?;
|
||||||
|
self.set_ram_address_counters(spi, x, y)?;
|
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::WRITE_RAM_RED, buffer)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Never use directly this function when using partial refresh, or also
|
||||||
|
/// keep the base buffer in syncd using `set_partial_base_buffer` function.
|
||||||
|
fn display_frame(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> {
|
||||||
|
if self.refresh == RefreshLUT::FULL {
|
||||||
|
self.set_display_update_control_2(
|
||||||
|
spi,
|
||||||
|
DisplayUpdateControl2::new()
|
||||||
|
.enable_clock()
|
||||||
|
.enable_analog()
|
||||||
|
.display()
|
||||||
|
.disable_analog()
|
||||||
|
.disable_clock(),
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
self.set_display_update_control_2(spi, DisplayUpdateControl2::new().display())?;
|
||||||
|
}
|
||||||
|
self.command(spi, Command::MASTER_ACTIVATION)?;
|
||||||
|
self.wait_until_idle();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_and_display_frame(&mut self, spi: &mut SPI, buffer: &[u8]) -> Result<(), SPI::Error> {
|
||||||
|
self.update_frame(spi, buffer)?;
|
||||||
|
self.display_frame(spi)?;
|
||||||
|
|
||||||
|
if self.refresh == RefreshLUT::QUICK {
|
||||||
|
self.set_partial_base_buffer(spi, buffer)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_frame(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> {
|
||||||
|
let color = self.background_color.get_byte_value();
|
||||||
|
|
||||||
|
self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?;
|
||||||
|
self.set_ram_address_counters(spi, 0, 0)?;
|
||||||
|
|
||||||
|
self.command(spi, Command::WRITE_RAM)?;
|
||||||
|
self.interface.data_x_times(
|
||||||
|
spi,
|
||||||
|
color,
|
||||||
|
buffer_len(WIDTH as usize, HEIGHT as usize) as u32,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Always keep the base buffer equals to current if not doing partial refresh.
|
||||||
|
if self.refresh == RefreshLUT::FULL {
|
||||||
|
self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?;
|
||||||
|
self.set_ram_address_counters(spi, 0, 0)?;
|
||||||
|
|
||||||
|
self.command(spi, Command::WRITE_RAM_RED)?;
|
||||||
|
self.interface.data_x_times(
|
||||||
|
spi,
|
||||||
|
color,
|
||||||
|
buffer_len(WIDTH as usize, HEIGHT as usize) as u32,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_background_color(&mut self, background_color: Color) {
|
||||||
|
self.background_color = background_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn background_color(&self) -> &Color {
|
||||||
|
&self.background_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> {
|
||||||
|
let buffer = match refresh_rate {
|
||||||
|
Some(RefreshLUT::FULL) | None => &LUT_FULL_UPDATE,
|
||||||
|
Some(RefreshLUT::QUICK) => &LUT_PARTIAL_UPDATE,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::WRITE_LUT_REGISTER, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_busy(&self) -> bool {
|
||||||
|
self.interface.is_busy(IS_BUSY_LOW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<SPI, CS, BUSY, DC, RST> EPD2in13<SPI, CS, BUSY, DC, RST>
|
||||||
|
where
|
||||||
|
SPI: Write<u8>,
|
||||||
|
CS: OutputPin,
|
||||||
|
BUSY: InputPin,
|
||||||
|
DC: OutputPin,
|
||||||
|
RST: OutputPin,
|
||||||
|
{
|
||||||
|
/// When using partial refresh, the controller uses the provided buffer for
|
||||||
|
/// comparison with new buffer.
|
||||||
|
pub fn set_partial_base_buffer(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
buffer: &[u8],
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
assert!(buffer_len(WIDTH as usize, HEIGHT as usize) == buffer.len());
|
||||||
|
self.set_ram_area(spi, 0, 0, WIDTH - 1, HEIGHT - 1)?;
|
||||||
|
self.set_ram_address_counters(spi, 0, 0)?;
|
||||||
|
|
||||||
|
self.cmd_with_data(spi, Command::WRITE_RAM_RED, buffer)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects which sleep mode will be used when triggering the deep sleep.
|
||||||
|
pub fn set_deep_sleep_mode(&mut self, mode: DeepSleepMode) {
|
||||||
|
self.sleep_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the refresh mode. When changing mode, the screen will be
|
||||||
|
/// re-initialized accordingly.
|
||||||
|
pub fn set_refresh<DELAY: DelayMs<u8>>(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
delay: &mut DELAY,
|
||||||
|
refresh: RefreshLUT,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
if self.refresh != refresh {
|
||||||
|
self.refresh = refresh;
|
||||||
|
self.init(spi, delay)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_gate_scan_start_position(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
start: u16,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
assert!(start <= 295);
|
||||||
|
self.cmd_with_data(
|
||||||
|
spi,
|
||||||
|
Command::GATE_SCAN_START_POSITION,
|
||||||
|
&[(start & 0xFF) as u8, ((start >> 8) & 0x1) as u8],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_border_waveform(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
borderwaveform: BorderWaveForm,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
self.cmd_with_data(
|
||||||
|
spi,
|
||||||
|
Command::BORDER_WAVEFORM_CONTROL,
|
||||||
|
&[borderwaveform.to_u8()],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_vcom_register(&mut self, spi: &mut SPI, vcom: VCOM) -> Result<(), SPI::Error> {
|
||||||
|
self.cmd_with_data(spi, Command::WRITE_VCOM_REGISTER, &[vcom.0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_gate_driving_voltage(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
voltage: GateDrivingVoltage,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
self.cmd_with_data(spi, Command::GATE_DRIVING_VOLTAGE_CTRL, &[voltage.0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_dummy_line_period(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
number_of_lines: u8,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
assert!(number_of_lines <= 127);
|
||||||
|
self.cmd_with_data(spi, Command::SET_DUMMY_LINE_PERIOD, &[number_of_lines])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_gate_line_width(&mut self, spi: &mut SPI, width: u8) -> Result<(), SPI::Error> {
|
||||||
|
self.cmd_with_data(spi, Command::SET_GATE_LINE_WIDTH, &[width & 0x0F])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the source driving voltage value
|
||||||
|
fn set_source_driving_voltage(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
vsh1: SourceDrivingVoltage,
|
||||||
|
vsh2: SourceDrivingVoltage,
|
||||||
|
vsl: SourceDrivingVoltage,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
self.cmd_with_data(
|
||||||
|
spi,
|
||||||
|
Command::SOURCE_DRIVING_VOLTAGE_CTRL,
|
||||||
|
&[vsh1.0, vsh2.0, vsl.0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare the actions that the next master activation command will
|
||||||
|
/// trigger.
|
||||||
|
fn set_display_update_control_2(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
value: DisplayUpdateControl2,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
self.cmd_with_data(spi, Command::DISPLAY_UPDATE_CONTROL_2, &[value.0])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggers the deep sleep mode
|
||||||
|
fn set_sleep_mode(&mut self, spi: &mut SPI, mode: DeepSleepMode) -> Result<(), SPI::Error> {
|
||||||
|
self.cmd_with_data(spi, Command::DEEP_SLEEP_MODE, &[mode as u8])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_driver_output(&mut self, spi: &mut SPI, output: DriverOutput) -> Result<(), SPI::Error> {
|
||||||
|
self.cmd_with_data(spi, Command::DRIVER_OUTPUT_CONTROL, &output.to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the data entry mode (ie. how X and Y positions changes when writing
|
||||||
|
/// data to RAM)
|
||||||
|
fn set_data_entry_mode(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
counter_incr_mode: DataEntryModeIncr,
|
||||||
|
counter_direction: DataEntryModeDir,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
let mode = counter_incr_mode as u8 | counter_direction as u8;
|
||||||
|
self.cmd_with_data(spi, Command::DATA_ENTRY_MODE_SETTING, &[mode])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets both X and Y pixels ranges
|
||||||
|
fn set_ram_area(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
start_x: u32,
|
||||||
|
start_y: u32,
|
||||||
|
end_x: u32,
|
||||||
|
end_y: u32,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
self.cmd_with_data(
|
||||||
|
spi,
|
||||||
|
Command::SET_RAM_X_ADDRESS_START_END_POSITION,
|
||||||
|
&[(start_x >> 3) as u8, (end_x >> 3) as u8],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.cmd_with_data(
|
||||||
|
spi,
|
||||||
|
Command::SET_RAM_Y_ADDRESS_START_END_POSITION,
|
||||||
|
&[
|
||||||
|
start_y as u8,
|
||||||
|
(start_y >> 8) as u8,
|
||||||
|
end_y as u8,
|
||||||
|
(end_y >> 8) as u8,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets both X and Y pixels counters when writing data to RAM
|
||||||
|
fn set_ram_address_counters(
|
||||||
|
&mut self,
|
||||||
|
spi: &mut SPI,
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
) -> Result<(), SPI::Error> {
|
||||||
|
self.wait_until_idle();
|
||||||
|
self.cmd_with_data(spi, Command::SET_RAM_X_ADDRESS_COUNTER, &[(x >> 3) as u8])?;
|
||||||
|
|
||||||
|
self.cmd_with_data(
|
||||||
|
spi,
|
||||||
|
Command::SET_RAM_Y_ADDRESS_COUNTER,
|
||||||
|
&[y as u8, (y >> 8) as u8],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 122);
|
||||||
|
assert_eq!(HEIGHT, 250);
|
||||||
|
assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -74,6 +74,7 @@ mod interface;
|
||||||
|
|
||||||
pub mod epd1in54;
|
pub mod epd1in54;
|
||||||
pub mod epd1in54b;
|
pub mod epd1in54b;
|
||||||
|
pub mod epd2in13_v2;
|
||||||
pub mod epd2in9;
|
pub mod epd2in9;
|
||||||
pub mod epd2in9bc;
|
pub mod epd2in9bc;
|
||||||
pub mod epd4in2;
|
pub mod epd4in2;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ pub(crate) trait Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seperates the different LUT for the Display Refresh process
|
/// Seperates the different LUT for the Display Refresh process
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||||
pub enum RefreshLUT {
|
pub enum RefreshLUT {
|
||||||
/// The "normal" full Lookuptable for the Refresh-Sequence
|
/// The "normal" full Lookuptable for the Refresh-Sequence
|
||||||
FULL,
|
FULL,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue