1445 lines
34 KiB
Rust
1445 lines
34 KiB
Rust
use embassy_time::Timer;
|
|
use esp_hal::{
|
|
DriverMode,
|
|
gpio::{Flex, Level, Output},
|
|
lcd_cam::lcd::{
|
|
ClockMode, DelayMode, Phase, Polarity,
|
|
dpi::{Dpi, Format, FrameTiming},
|
|
},
|
|
ledc::{self, LowSpeed, channel::ChannelIFace, timer::TimerIFace},
|
|
time::Rate,
|
|
};
|
|
use lcd::spi_write;
|
|
use log::debug;
|
|
use ouroboros::self_referencing;
|
|
use paste::paste;
|
|
// use tinyvec::ArrayVec;
|
|
|
|
mod lcd;
|
|
|
|
pub trait Command {
|
|
fn address(&self) -> u8;
|
|
fn args_iter(&'_ self) -> core::slice::Iter<'_, u8>;
|
|
}
|
|
|
|
// pub struct ArrayCommand<const N: usize> {
|
|
// pub address: u8,
|
|
// pub args: ArrayVec<[u8; N]>,
|
|
// }
|
|
|
|
// impl<const N: usize> const ArrayCommand<N> {
|
|
// const fn from(command: impl Command) -> Self {
|
|
// Self {
|
|
// address: command.address(),
|
|
// args: command.args_iter().copied().collect(),
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// impl<const N: usize> Command for ArrayCommand<N> {
|
|
// fn address(&self) -> u8 {
|
|
// self.address
|
|
// }
|
|
|
|
// fn args_iter(&'_ self) -> core::slice::Iter<'_, u8> {
|
|
// self.args.iter()
|
|
// }
|
|
// }
|
|
|
|
pub struct CustomCommand {
|
|
pub address: u8,
|
|
pub args: &'static [u8],
|
|
}
|
|
|
|
impl Command for CustomCommand {
|
|
fn address(&self) -> u8 {
|
|
self.address
|
|
}
|
|
|
|
fn args_iter(&'_ self) -> core::slice::Iter<'_, u8> {
|
|
self.args.iter()
|
|
}
|
|
}
|
|
|
|
macro_rules! define_arg {
|
|
(@field ($field:ident: $min:literal..=$max:literal)) => {
|
|
paste! {
|
|
#[allow(unused, clippy::identity_op)]
|
|
pub const fn [<with_ $field>](mut self, val: u8) -> Self {
|
|
const MASK: u8 = ((1 << ($max - $min + 1)) - 1);
|
|
self.0 &= !(MASK << $min); // Clear bits
|
|
self.0 |= (val & MASK) << $min; // Assign bits
|
|
self
|
|
}
|
|
}
|
|
};
|
|
|
|
(@field ($field:ident: $index:literal)) => {
|
|
paste! {
|
|
#[allow(unused)]
|
|
pub const fn [<with_ $field>](mut self, val: bool) -> Self {
|
|
self.0 &= !(1 << $index); // Clear bit
|
|
self.0 |= (val as u8) << $index; // Assign bit
|
|
self
|
|
}
|
|
}
|
|
};
|
|
|
|
(@field ($field:ident)) => {
|
|
paste! {
|
|
#[allow(unused)]
|
|
pub const fn [<with_ $field>](mut self, val: u8) -> Self {
|
|
self.0 = val;
|
|
self
|
|
}
|
|
}
|
|
};
|
|
|
|
($visibility:vis struct $type:ident {
|
|
const INIT = $init:literal;
|
|
$($field:ident $(: $min:literal$(..=$max:literal)?)?),*$(,)?
|
|
}) => {
|
|
#[derive(Clone, Copy)]
|
|
$visibility struct $type(u8);
|
|
|
|
impl Default for $type {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[allow(unused)]
|
|
impl $type {
|
|
pub const fn new() -> Self {
|
|
Self($init)
|
|
}
|
|
|
|
pub const fn into_byte(self) -> u8 {
|
|
self.0
|
|
}
|
|
|
|
$(
|
|
define_arg!(@field ($field $(: $min$(..=$max)?)?));
|
|
)*
|
|
}
|
|
};
|
|
|
|
($visibility:vis struct $type:ident {
|
|
$field:ident $(,)?
|
|
}) => {
|
|
#[derive(Clone, Copy)]
|
|
$visibility struct $type(u8);
|
|
|
|
impl Default for $type {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[allow(unused)]
|
|
impl $type {
|
|
pub const fn new() -> Self {
|
|
Self(0)
|
|
}
|
|
|
|
pub const fn into_byte(self) -> u8 {
|
|
self.0
|
|
}
|
|
|
|
define_arg!(@field ($field));
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! define_commands {
|
|
($(
|
|
$(#[$docs:meta])*
|
|
$visibility:vis command $command:ident {
|
|
const ADDRESS = $address:literal;
|
|
$(
|
|
arg {
|
|
$($body:tt)*
|
|
}
|
|
)*
|
|
}
|
|
)*) => {
|
|
paste! {
|
|
$(
|
|
$(
|
|
define_arg!($visibility struct [< Cmd $command Arg ${index()}>] { $($body)* });
|
|
)*
|
|
|
|
#[repr(C)]
|
|
#[derive(Default)]
|
|
$(#[$docs])*
|
|
$visibility struct [< Cmd $command >]($(pub [< Cmd $command Arg ${index()} >], $(${ignore($body)})*)*);
|
|
|
|
impl Command for [< Cmd $command >] {
|
|
fn address(&self) -> u8 {
|
|
$address
|
|
}
|
|
|
|
fn args_iter(&'_ self) -> core::slice::Iter<'_, u8> {
|
|
self.into_iter()
|
|
}
|
|
}
|
|
|
|
impl<'a> IntoIterator for &'a [< Cmd $command >] {
|
|
type Item = &'a u8;
|
|
type IntoIter = core::slice::Iter<'a, u8>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
unsafe {
|
|
core::slice::from_raw_parts(
|
|
self as *const [< Cmd $command >] as *const u8,
|
|
0 $(+ 1 $(${ignore($body)})*)*,
|
|
)
|
|
}.iter()
|
|
}
|
|
}
|
|
)*
|
|
}
|
|
};
|
|
}
|
|
|
|
define_commands! {
|
|
// System function command table 1
|
|
|
|
#[doc="No operation"]
|
|
pub command Nop {
|
|
const ADDRESS = 0x00;
|
|
}
|
|
|
|
#[doc="Software reset"]
|
|
pub command Swreset {
|
|
const ADDRESS = 0x01;
|
|
}
|
|
|
|
#[doc="ID read"]
|
|
pub command Rddid {
|
|
const ADDRESS = 0x04;
|
|
arg { id1 }
|
|
arg { id2 }
|
|
arg { id3 }
|
|
}
|
|
|
|
#[doc="ID read"]
|
|
pub command Rdnumed {
|
|
const ADDRESS = 0x05;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
err: 0..=6,
|
|
err_over: 7,
|
|
}
|
|
}
|
|
|
|
#[doc="Read the first pixel of color R"]
|
|
pub command Rdred {
|
|
const ADDRESS = 0x06;
|
|
arg { r_1st }
|
|
}
|
|
|
|
#[doc="Read the first pixel of color G"]
|
|
pub command Rdgreen {
|
|
const ADDRESS = 0x07;
|
|
arg { g_1st }
|
|
}
|
|
|
|
#[doc="Read the first pixel of color B"]
|
|
pub command Rdblue {
|
|
const ADDRESS = 0x08;
|
|
arg { b_1st }
|
|
}
|
|
|
|
#[doc="Read display power mode"]
|
|
pub command Rddpm {
|
|
const ADDRESS = 0x0A;
|
|
arg {
|
|
const INIT = 0b0000_1000;
|
|
dison: 2,
|
|
slpout: 4,
|
|
bston: 7,
|
|
}
|
|
}
|
|
|
|
#[doc="Read display MADCTR"]
|
|
pub command Rddmadctl {
|
|
const ADDRESS = 0x0B;
|
|
arg {
|
|
const INIT = 0b0000_1000;
|
|
bgr: 3,
|
|
ml: 4,
|
|
}
|
|
}
|
|
|
|
#[doc="Read display pixel format"]
|
|
pub command Rddcolmod {
|
|
const ADDRESS = 0x0C;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vipf: 5..=6,
|
|
}
|
|
}
|
|
|
|
#[doc="Read display image mode"]
|
|
pub command Rddim {
|
|
const ADDRESS = 0x0D;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
gcs: 0..=2,
|
|
alpxloff: 3,
|
|
alpxlon: 4,
|
|
invon: 5,
|
|
}
|
|
}
|
|
|
|
#[doc="Read display signal mode"]
|
|
pub command Rddsm {
|
|
const ADDRESS = 0x0E;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
telmd: 6,
|
|
teon: 7,
|
|
}
|
|
}
|
|
|
|
#[doc="Read display self-diagnostic result"]
|
|
pub command Rddsdr {
|
|
const ADDRESS = 0x0F;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
fund: 6,
|
|
rld: 7,
|
|
}
|
|
}
|
|
|
|
#[doc="Sleep in"]
|
|
pub command Slpin {
|
|
const ADDRESS = 0x10;
|
|
}
|
|
|
|
#[doc="Sleep out"]
|
|
pub command Slpout {
|
|
const ADDRESS = 0x11;
|
|
}
|
|
|
|
#[doc="Partial mode on"]
|
|
pub command Ptlon {
|
|
const ADDRESS = 0x12;
|
|
}
|
|
|
|
#[doc="Normal display mode on"]
|
|
pub command Noron {
|
|
const ADDRESS = 0x13;
|
|
}
|
|
|
|
#[doc="Display inversion off (normal)"]
|
|
pub command Invoff {
|
|
const ADDRESS = 0x20;
|
|
}
|
|
|
|
#[doc="Display inversion on"]
|
|
pub command Invon {
|
|
const ADDRESS = 0x21;
|
|
}
|
|
|
|
#[doc="All pixel off (black)"]
|
|
pub command Allpoff {
|
|
const ADDRESS = 0x22;
|
|
}
|
|
|
|
#[doc="All pixel on (white)"]
|
|
pub command Allpon {
|
|
const ADDRESS = 0x23;
|
|
}
|
|
|
|
#[doc="Gamma curve select"]
|
|
pub command Gamset {
|
|
const ADDRESS = 0x26;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
gc: 0..=3,
|
|
}
|
|
}
|
|
|
|
#[doc="Display off"]
|
|
pub command Dispoff {
|
|
const ADDRESS = 0x28;
|
|
}
|
|
|
|
#[doc="Display on"]
|
|
pub command Dispon {
|
|
const ADDRESS = 0x29;
|
|
}
|
|
|
|
#[doc="Tearing effect line off"]
|
|
pub command Teoff {
|
|
const ADDRESS = 0x34;
|
|
}
|
|
|
|
#[doc="Tearing effect line on"]
|
|
pub command Teon {
|
|
const ADDRESS = 0x35;
|
|
}
|
|
|
|
#[doc="Display data access control"]
|
|
pub command Madctl {
|
|
const ADDRESS = 0x36;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
bgr: 3,
|
|
ml: 4,
|
|
}
|
|
}
|
|
|
|
#[doc="Idle mode off"]
|
|
pub command Idmoff {
|
|
const ADDRESS = 0x38;
|
|
}
|
|
|
|
#[doc="Idle mode on"]
|
|
pub command Idmon {
|
|
const ADDRESS = 0x39;
|
|
}
|
|
|
|
#[doc="Interface pixel format"]
|
|
pub command Colmod {
|
|
const ADDRESS = 0x3A;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vipf: 4..=6,
|
|
}
|
|
}
|
|
|
|
#[doc="Read tear line"]
|
|
pub command Gsl {
|
|
const ADDRESS = 0x45;
|
|
arg { tesl_msb }
|
|
arg { tesl_lsb }
|
|
}
|
|
|
|
#[doc="Write display brightness value"]
|
|
pub command Wrdibv {
|
|
const ADDRESS = 0x51;
|
|
arg { dbv }
|
|
}
|
|
|
|
#[doc="Read display brightness value"]
|
|
pub command Rddisbv {
|
|
const ADDRESS = 0x52;
|
|
arg { dbv }
|
|
}
|
|
|
|
#[doc="Write control display value"]
|
|
pub command Wrctrld {
|
|
const ADDRESS = 0x53;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
bl: 2,
|
|
dd: 3,
|
|
bctrl: 5,
|
|
}
|
|
}
|
|
|
|
#[doc="Read control display value"]
|
|
pub command Rrctrld {
|
|
const ADDRESS = 0x54;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
bl: 2,
|
|
dd: 3,
|
|
bctrl: 5,
|
|
}
|
|
}
|
|
|
|
#[doc="Write CABC mode"]
|
|
pub command Wrcabc {
|
|
const ADDRESS = 0x55;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
cabc_md: 0..=1,
|
|
ce_md: 4..=5,
|
|
ce_on: 7,
|
|
}
|
|
}
|
|
|
|
#[doc="Read CABC mode"]
|
|
pub command Rrcabc {
|
|
const ADDRESS = 0x56;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
cabc_md: 0..=1,
|
|
ce_md: 4..=5,
|
|
ce_on: 7,
|
|
}
|
|
}
|
|
|
|
#[doc="Write CABC minimum brightness"]
|
|
pub command Wrcabcmb {
|
|
const ADDRESS = 0x5E;
|
|
arg { cmb }
|
|
}
|
|
|
|
#[doc="Read CABC minimum brightness"]
|
|
pub command Rrcabcmb {
|
|
const ADDRESS = 0x5F;
|
|
arg { cmb }
|
|
}
|
|
|
|
#[doc="Read automatic brightness control self-diagnostic result"]
|
|
pub command Rdabcsd {
|
|
const ADDRESS = 0x68;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
fund: 6,
|
|
rld: 7,
|
|
}
|
|
}
|
|
|
|
#[doc="Read black/white low bits"]
|
|
pub command Rdbwlb {
|
|
const ADDRESS = 0x70;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
wy: 0..=1,
|
|
wx: 2..=3,
|
|
bky: 4..=5,
|
|
bkx: 6..=7,
|
|
}
|
|
}
|
|
|
|
#[doc="Read Bkx"]
|
|
pub command Rdbkx {
|
|
const ADDRESS = 0x71;
|
|
arg { bkx }
|
|
}
|
|
|
|
#[doc="Read Bky"]
|
|
pub command Rdbky {
|
|
const ADDRESS = 0x72;
|
|
arg { bky }
|
|
}
|
|
|
|
#[doc="Read Wx"]
|
|
pub command Rdwx {
|
|
const ADDRESS = 0x73;
|
|
arg { wx }
|
|
}
|
|
|
|
#[doc="Read Wy"]
|
|
pub command Rdwy {
|
|
const ADDRESS = 0x74;
|
|
arg { wy }
|
|
}
|
|
|
|
#[doc="Read red/green low bits"]
|
|
pub command Rdrglb {
|
|
const ADDRESS = 0x75;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
gy: 0..=1,
|
|
gx: 2..=3,
|
|
ry: 4..=5,
|
|
rx: 6..=7,
|
|
}
|
|
}
|
|
|
|
#[doc="Read Rx"]
|
|
pub command Rdrx {
|
|
const ADDRESS = 0x76;
|
|
arg { rx }
|
|
}
|
|
|
|
#[doc="Read Ry"]
|
|
pub command Rdry {
|
|
const ADDRESS = 0x77;
|
|
arg { ry }
|
|
}
|
|
|
|
#[doc="Read Gx"]
|
|
pub command Rdgx {
|
|
const ADDRESS = 0x78;
|
|
arg { gx }
|
|
}
|
|
|
|
#[doc="Read Gy"]
|
|
pub command Rdgy {
|
|
const ADDRESS = 0x79;
|
|
arg { gy }
|
|
}
|
|
|
|
#[doc="Read blue/a-color low bits"]
|
|
pub command Rdbalb {
|
|
const ADDRESS = 0x7A;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
ay: 0..=1,
|
|
ax: 2..=3,
|
|
by: 4..=5,
|
|
bx: 6..=7,
|
|
}
|
|
}
|
|
|
|
#[doc="Read Bx"]
|
|
pub command Rdbx {
|
|
const ADDRESS = 0x7B;
|
|
arg { bx }
|
|
}
|
|
|
|
#[doc="Read By"]
|
|
pub command Rdby {
|
|
const ADDRESS = 0x7C;
|
|
arg { by }
|
|
}
|
|
|
|
#[doc="Read Ax"]
|
|
pub command Rdax {
|
|
const ADDRESS = 0x7D;
|
|
arg { ax }
|
|
}
|
|
|
|
#[doc="Read Ay"]
|
|
pub command Rday {
|
|
const ADDRESS = 0x7E;
|
|
arg { ay }
|
|
}
|
|
|
|
#[doc="Read tthe DDB from the provided location"]
|
|
pub command Rdddbs {
|
|
const ADDRESS = 0xA1;
|
|
arg { const INIT = 0x88; }
|
|
arg { const INIT = 0x02; }
|
|
arg { mid_msb }
|
|
arg { mid_lsb }
|
|
arg { const INIT = 0xFF; }
|
|
}
|
|
|
|
#[doc="Continue reading the DDB from the last read location"]
|
|
pub command Rdddbc {
|
|
const ADDRESS = 0xA8;
|
|
arg { sid_msb }
|
|
arg { sid_lsb }
|
|
arg { mid_msb }
|
|
arg { mid_lsb }
|
|
arg { const INIT = 0xFF; }
|
|
}
|
|
|
|
#[doc="Read first checksum"]
|
|
pub command Rdfcs {
|
|
const ADDRESS = 0xAA;
|
|
arg { fcs }
|
|
}
|
|
|
|
#[doc="Read continue checksum"]
|
|
pub command Rdccs {
|
|
const ADDRESS = 0xAF;
|
|
arg { ccs }
|
|
}
|
|
|
|
#[doc="Read ID1"]
|
|
pub command Rdid1 {
|
|
const ADDRESS = 0xDA;
|
|
arg { id1 }
|
|
}
|
|
|
|
#[doc="Read ID2"]
|
|
pub command Rdid2 {
|
|
const ADDRESS = 0xDB;
|
|
arg { id2 }
|
|
}
|
|
|
|
#[doc="Read ID3"]
|
|
pub command Rdid3 {
|
|
const ADDRESS = 0xDC;
|
|
arg { id3 }
|
|
}
|
|
|
|
// System function command table 2
|
|
|
|
#[doc="Command2_BKx function selection"]
|
|
pub command Cn2bkxsel {
|
|
const ADDRESS = 0xFF;
|
|
arg { const INIT = 0b0111_0111; }
|
|
arg { const INIT = 0b0000_0001; }
|
|
arg { const INIT = 0b0000_0000; }
|
|
arg { const INIT = 0b0000_0000; }
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
// The spec is ambiguous on the size of this field.
|
|
bksel: 0..=1,
|
|
cn2: 4,
|
|
}
|
|
}
|
|
|
|
// Command2_BK0
|
|
|
|
#[doc="Positive voltage gamma control"]
|
|
pub command Pvgamctrl {
|
|
const ADDRESS = 0xB0;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc0p: 0..=3,
|
|
aj0p: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc4p: 0..=5,
|
|
aj1p: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc8p: 0..=5,
|
|
aj2p: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc16p: 0..=4,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc24p: 0..=4,
|
|
aj3p: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc52p: 0..=3,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc80p: 0..=5,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc108p: 0..=3,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc147p: 0..=3,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc175p: 0..=5,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc203p: 0..=3,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc231p: 0..=4,
|
|
aj4p: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc239p: 0..=4,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc247p: 0..=5,
|
|
aj5p: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc251p: 0..=5,
|
|
aj6p: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc255p: 0..=4,
|
|
aj7p: 6..=7,
|
|
}
|
|
}
|
|
|
|
#[doc="Negative voltage gamma control"]
|
|
pub command Nvgamctrl {
|
|
const ADDRESS = 0xB1;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc0n: 0..=3,
|
|
aj0n: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc4n: 0..=5,
|
|
aj1n: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc8n: 0..=5,
|
|
aj2n: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc16n: 0..=4,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc24n: 0..=4,
|
|
// TODO: This field is not documented in the spec,
|
|
// but it seems like it should be here?
|
|
aj3n: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc52n: 0..=3,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc80n: 0..=5,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc108n: 0..=3,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc147n: 0..=3,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc175n: 0..=5,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc203n: 0..=3,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc231n: 0..=4,
|
|
aj4n: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc239n: 0..=4,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc247n: 0..=5,
|
|
aj5n: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc251n: 0..=5,
|
|
aj6n: 6..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vc255n: 0..=4,
|
|
aj7n: 6..=7,
|
|
}
|
|
}
|
|
|
|
#[doc="Digital gamma enable"]
|
|
pub command Dgmen {
|
|
const ADDRESS = 0xB8;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
dgm_on: 4,
|
|
}
|
|
}
|
|
|
|
// DGMLUTR, DGMLUTB not currently supported, because of its high number of parameters.
|
|
// It could be supported with a manual implementation of `Command`.
|
|
|
|
#[doc="PWM CLK select"]
|
|
pub command Pwmclk {
|
|
const ADDRESS = 0xBC;
|
|
arg {
|
|
const INIT = 0b0001_1000;
|
|
pwm_clk_sel: 0..=2,
|
|
}
|
|
}
|
|
|
|
#[doc="Display line setting"]
|
|
pub command Lneset {
|
|
const ADDRESS = 0xC0;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
bar: 0..=6,
|
|
lde_en: 7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
line_delta: 0..=1,
|
|
}
|
|
}
|
|
|
|
#[doc="Porch control"]
|
|
pub command Porctrl {
|
|
const ADDRESS = 0xC1;
|
|
arg { vbp }
|
|
arg { vfp }
|
|
}
|
|
|
|
#[doc="Inversion selection & frame rate control"]
|
|
pub command Invsel {
|
|
const ADDRESS = 0xC2;
|
|
arg {
|
|
const INIT = 0b0011_0000;
|
|
nlinv: 0..=2,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
rtni: 0..=4,
|
|
}
|
|
}
|
|
|
|
#[doc="RGB control"]
|
|
pub command Rgbctrl {
|
|
const ADDRESS = 0xC3;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
ep: 0,
|
|
dp: 1,
|
|
hsp: 2,
|
|
vsp: 3,
|
|
de_hv: 7,
|
|
}
|
|
arg { hbp_hvrgb }
|
|
arg { vbp_hvrgb }
|
|
}
|
|
|
|
#[doc="Partial mode control"]
|
|
pub command Parctrl {
|
|
const ADDRESS = 0xC5;
|
|
arg { ptsa_lsb }
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
ptsa_hsb: 0..=1,
|
|
}
|
|
arg { ptea_lsb }
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
ptea_hsb: 0..=1,
|
|
}
|
|
}
|
|
|
|
#[doc="Source direction control"]
|
|
pub command Sdir {
|
|
const ADDRESS = 0xC7;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
ss: 2,
|
|
}
|
|
}
|
|
|
|
#[doc="Pseudo-dot inversion driving setting"]
|
|
pub command Pdotset {
|
|
const ADDRESS = 0xC8;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
z_gltor: 5,
|
|
z_sdm1s: 6,
|
|
z_en: 7,
|
|
}
|
|
}
|
|
|
|
#[doc="Color control"]
|
|
pub command Colctrl {
|
|
const ADDRESS = 0xCD;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
epf: 0..=2,
|
|
mdt: 3,
|
|
inv_led_on: 4,
|
|
inv_led_pwm: 5,
|
|
}
|
|
}
|
|
|
|
#[doc="Spread spectrum control"]
|
|
pub command Ssctrl {
|
|
const ADDRESS = 0xCE;
|
|
arg {
|
|
const INIT = 0b0000_0100;
|
|
dssrg: 4..=5,
|
|
dsse: 7,
|
|
}
|
|
}
|
|
|
|
#[doc="Sunlight readable enhancement"]
|
|
pub command Sectrl {
|
|
const ADDRESS = 0xE0;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
sre_alpha: 0..=3,
|
|
sre: 4,
|
|
}
|
|
}
|
|
|
|
#[doc="Noise reduce control"]
|
|
pub command Nrctrl {
|
|
const ADDRESS = 0xE1;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
nr_md: 0..=1,
|
|
nre: 4,
|
|
}
|
|
}
|
|
|
|
#[doc="Sharpness control (originally has a conflicting name SECTRL)"]
|
|
pub command Shctrl {
|
|
const ADDRESS = 0xE2;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
y_gain: 0..=3,
|
|
se: 4,
|
|
}
|
|
}
|
|
|
|
#[doc="Color calibration control"]
|
|
pub command Ccctrl {
|
|
const ADDRESS = 0xE3;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
cce: 0,
|
|
}
|
|
}
|
|
|
|
#[doc="Skin tone preservation control"]
|
|
pub command Skctrl {
|
|
const ADDRESS = 0xE4;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
skin_ce_mid: 0..=1,
|
|
ske: 4,
|
|
}
|
|
}
|
|
|
|
#[doc="NVM address setting enable"]
|
|
pub command Nvmsete {
|
|
const ADDRESS = 0xEA;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
aden: 0,
|
|
}
|
|
}
|
|
|
|
#[doc="CABC control"]
|
|
pub command Cabcctrl {
|
|
const ADDRESS = 0xEE;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
led_en: 0,
|
|
ledpwr_sel: 4,
|
|
}
|
|
}
|
|
|
|
// Command2_BK1
|
|
|
|
#[doc="Vop amplitude setting"]
|
|
pub command Vrhs {
|
|
const ADDRESS = 0xB0;
|
|
arg { vrha }
|
|
}
|
|
|
|
#[doc="VCOM amplitude setting"]
|
|
pub command Vcoms {
|
|
const ADDRESS = 0xB1;
|
|
arg { vcom }
|
|
}
|
|
|
|
#[doc="VGH voltage setting"]
|
|
pub command Vghss {
|
|
const ADDRESS = 0xB2;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vghss: 0..=3,
|
|
}
|
|
}
|
|
|
|
#[doc="TEST command setting"]
|
|
pub command Tescmd {
|
|
const ADDRESS = 0xB3;
|
|
arg { const INIT = 0b1000_0000; }
|
|
}
|
|
|
|
#[doc="VGL voltage setting"]
|
|
pub command Vgls {
|
|
const ADDRESS = 0xB5;
|
|
arg {
|
|
const INIT = 0b0100_0000;
|
|
vgls: 0..=3,
|
|
}
|
|
}
|
|
|
|
#[doc="VRH_DV voltage setting"]
|
|
pub command Vrhdv {
|
|
const ADDRESS = 0xB6;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
vrh_dv: 0..=6,
|
|
}
|
|
}
|
|
|
|
#[doc="Power control 1"]
|
|
pub command Pwctrl1 {
|
|
const ADDRESS = 0xB7;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
apos: 0..=1,
|
|
apis: 2..=3,
|
|
ap: 6..=7,
|
|
}
|
|
}
|
|
|
|
#[doc="Power control 2"]
|
|
pub command Pwctrl2 {
|
|
const ADDRESS = 0xB8;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
avcl: 0..=1,
|
|
avdd: 4..=5,
|
|
}
|
|
}
|
|
|
|
#[doc="Power control 3"]
|
|
pub command Pwctrl3 {
|
|
const ADDRESS = 0xB9;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
svno_pum: 0..=1,
|
|
svpo_pum: 4..=5,
|
|
}
|
|
}
|
|
|
|
#[doc="Power pumping clk selection 1"]
|
|
pub command Pclks1 {
|
|
const ADDRESS = 0xBA;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
stp1cks: 0..=1,
|
|
stp4cks: 4..=5,
|
|
}
|
|
}
|
|
|
|
#[doc="Power pumping clk selection 2"]
|
|
pub command Pclks2 {
|
|
const ADDRESS = 0xBB;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
sbstcks: 0..=1,
|
|
}
|
|
}
|
|
|
|
#[doc="Power pumping clk selection 3"]
|
|
pub command Pclks3 {
|
|
const ADDRESS = 0xBC;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
stp2scks: 0..=1,
|
|
stp2pcks: 0..=1,
|
|
stp3cks: 4..=5,
|
|
}
|
|
}
|
|
|
|
#[doc="Source pre_drive timing set1 (also known as SPD1)"]
|
|
pub command Pdr1 {
|
|
const ADDRESS = 0xC1;
|
|
arg {
|
|
const INIT = 0b0111_0000;
|
|
t2d: 0..=3,
|
|
}
|
|
}
|
|
|
|
#[doc="Source pre_drive timing set2 (also known as SPD2)"]
|
|
pub command Pdr2 {
|
|
const ADDRESS = 0xC2;
|
|
arg {
|
|
const INIT = 0b0111_0000;
|
|
t3d: 0..=3,
|
|
}
|
|
}
|
|
|
|
#[doc="MIPI setting 1"]
|
|
pub command Mipiset1 {
|
|
const ADDRESS = 0xD0;
|
|
arg {
|
|
const INIT = 0b1000_0000;
|
|
err_sel: 0..=1,
|
|
eotp_en: 3,
|
|
}
|
|
}
|
|
|
|
#[doc="MIPI setting 2"]
|
|
pub command Mipiset2 {
|
|
const ADDRESS = 0xD1;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
mpc_tlpx0: 0..=3,
|
|
mpc_tlpx1: 4..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
mpc_tlpx2: 0..=3,
|
|
mpc_txtimeadj: 4..=7,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
mpc_ttago: 0..=3,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
mpc_ttaget: 0..=3,
|
|
}
|
|
}
|
|
|
|
#[doc="MIPI setting 3"]
|
|
pub command Mipiset3 {
|
|
const ADDRESS = 0xD2;
|
|
arg {
|
|
const INIT = 0b0011_0000;
|
|
phy_ttasure: 0..=3,
|
|
}
|
|
}
|
|
|
|
#[doc="MIPI setting 4"]
|
|
pub command Mipiset4 {
|
|
const ADDRESS = 0xD3;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
phy_csk: 0..=2,
|
|
}
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
phy_dsk0: 0..=2,
|
|
phy_dsk1: 4..=6,
|
|
}
|
|
}
|
|
|
|
// Command2_BK3
|
|
|
|
#[doc="NVM enable"]
|
|
pub command Nvmen {
|
|
const ADDRESS = 0xC8;
|
|
arg { const INIT = 0b0111_0111; }
|
|
arg { const INIT = 0b0000_0001; }
|
|
arg { const INIT = 0b1110_1110; }
|
|
arg { const INIT = 0b0000_0100; }
|
|
}
|
|
|
|
#[doc="NVM manual control setting"]
|
|
pub command Nvmset {
|
|
const ADDRESS = 0xCA;
|
|
arg {
|
|
const INIT = 0b0000_0000;
|
|
pa_msb: 0..=1,
|
|
}
|
|
arg { pa_lsb }
|
|
arg { pdin }
|
|
}
|
|
|
|
#[doc="NVM program active"]
|
|
pub command Promact {
|
|
const ADDRESS = 0xCC;
|
|
arg { const INIT = 0b1010_1010; }
|
|
}
|
|
}
|
|
|
|
#[self_referencing]
|
|
pub struct Backlight<'d> {
|
|
pub timer: ledc::timer::Timer<'d, LowSpeed>,
|
|
#[borrows(timer)]
|
|
#[covariant]
|
|
pub channel: ledc::channel::Channel<'this, LowSpeed>,
|
|
}
|
|
|
|
impl<'a> Backlight<'a> {
|
|
pub fn set_duty(&mut self, duty_pct: u8) -> Result<(), ledc::channel::Error> {
|
|
self.borrow_channel().set_duty(duty_pct)
|
|
}
|
|
}
|
|
|
|
pub struct St7701sController<'d> {
|
|
pub sck: Output<'d>,
|
|
pub mosi: Flex<'d>,
|
|
pub cs: Output<'d>,
|
|
pub backlight: Backlight<'d>,
|
|
}
|
|
|
|
impl<'d> St7701sController<'d> {
|
|
pub async fn send_init_sequence(&mut self) {
|
|
debug!("Writing ST7701S init sequence.");
|
|
|
|
for (subsequence, delay_ms) in &*lcd::INIT_SEQUENCE_COMMANDS {
|
|
for command in subsequence {
|
|
spi_write(
|
|
command.address(),
|
|
command.args_iter().copied(),
|
|
&mut self.mosi,
|
|
&mut self.sck,
|
|
&mut self.cs,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
Timer::after_millis(*delay_ms).await;
|
|
}
|
|
}
|
|
|
|
pub async fn send(&mut self, command: impl Command) {
|
|
spi_write(
|
|
command.address(),
|
|
command.args_iter().copied(),
|
|
&mut self.mosi,
|
|
&mut self.sck,
|
|
&mut self.cs,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Puts the display into sleep mode and disables the backlight.
|
|
pub async fn sleep_on(&mut self) {
|
|
self.backlight.set_duty(0).unwrap();
|
|
self.send(CmdSlpin()).await;
|
|
}
|
|
|
|
/// Brings the display out of sleep mode and enables the backlight.
|
|
pub async fn sleep_off(&mut self) {
|
|
self.send(CmdSlpout()).await;
|
|
self.backlight.set_duty(100).unwrap();
|
|
}
|
|
}
|
|
|
|
pub struct St7701s<'d, Dm>
|
|
where
|
|
Dm: DriverMode,
|
|
{
|
|
pub controller: St7701sController<'d>,
|
|
pub dpi: Dpi<'d, Dm>,
|
|
}
|
|
|
|
impl<'d, Dm> St7701s<'d, Dm>
|
|
where
|
|
Dm: DriverMode,
|
|
{
|
|
/// Initializes an ST7701S display, and puts it to sleep.
|
|
/// To turn it on, invoke `St7701sController:sleep_off`.
|
|
pub async fn new(
|
|
mut sck: Output<'d>,
|
|
mut mosi: Flex<'d>,
|
|
mut cs: Output<'d>,
|
|
mut unconfigured_dpi: Dpi<'d, Dm>,
|
|
mut bl_timer: ledc::timer::Timer<'d, LowSpeed>,
|
|
bl_channel: ledc::channel::Channel<'d, LowSpeed>,
|
|
) -> Self {
|
|
sck.apply_config(&Default::default());
|
|
sck.set_high();
|
|
cs.apply_config(&Default::default());
|
|
cs.set_high();
|
|
mosi.apply_input_config(&Default::default());
|
|
mosi.apply_output_config(&Default::default());
|
|
mosi.set_input_enable(false);
|
|
mosi.set_output_enable(true);
|
|
|
|
let lcd_config = esp_hal::lcd_cam::lcd::dpi::Config::default()
|
|
// Internal memory can use the full 16 MHz, but when external PSRAM is used, it cannot keep up with the display.
|
|
// For that reason, we choose the highest value for which it doesn't glitch by showing black
|
|
// stripes on the screen.
|
|
//
|
|
// There are three knobs you can turn to improve the bandwidth situation.
|
|
// - increase psram frequency
|
|
// - decrease the peripheral's frequency
|
|
// - prevent flash from being used whilst your program is running. (There's a PR to make
|
|
// this easy to do)
|
|
// https://github.com/esp-rs/esp-hal/pull/3024
|
|
//
|
|
// Adafruit would use 11 MHz.
|
|
// I had lowered the frequency, so that `DmaBounce` could keep up.
|
|
.with_frequency(Rate::from_mhz(9)) // From Adafruit
|
|
.with_clock_mode(ClockMode {
|
|
polarity: Polarity::IdleLow, // From Adafruit
|
|
phase: Phase::ShiftHigh, // From Adafruit
|
|
})
|
|
.with_de_mode(DelayMode::RaisingEdge)
|
|
.with_hsync_mode(DelayMode::RaisingEdge)
|
|
.with_vsync_mode(DelayMode::RaisingEdge)
|
|
.with_output_bit_mode(DelayMode::RaisingEdge)
|
|
.with_format(Format {
|
|
enable_2byte_mode: true,
|
|
..Default::default()
|
|
})
|
|
.with_timing({
|
|
// Adafruit's config for this LCD:
|
|
// https://github.com/adafruit/Adafruit_CircuitPython_Qualia/blob/742d336e05e6a4d8bdaa46e15bbf60c9f30d2eba/adafruit_qualia/displays/bar240x960.py#L81-L97
|
|
// https://github.com/adafruit/Adafruit_CircuitPython_Qualia/blob/742d336e05e6a4d8bdaa46e15bbf60c9f30d2eba/adafruit_qualia/displays/__init__.py#L59-L62
|
|
// CircuitPython code handling Adafruit's config
|
|
// https://github.com/adafruit/circuitpython/blob/97c6617817e95b1f6aa2ce458778aaa8371de39b/ports/espressif/common-hal/dotclockframebuffer/DotClockFramebuffer.c#L63
|
|
// ESP-IDF peripheral configuration code:
|
|
// https://github.com/espressif/esp-idf/blob/800f141f94c0f880c162de476512e183df671307/components/esp_lcd/rgb/esp_lcd_panel_rgb.c#L556
|
|
// Espressif's docs:
|
|
// https://docs.espressif.com/projects/esp-idf/en/v5.5.1/esp32s3/api-reference/peripherals/lcd/rgb_lcd.html#structures
|
|
|
|
// TODO: Investigate PORCTRL instruction in datasheet of ST7701
|
|
let horizontal_resolution: usize = 240;
|
|
let vertical_resolution = 960;
|
|
let overscan_left = 120;
|
|
let vsync_width = 8;
|
|
let hsync_width = 8;
|
|
let horizontal_blank_front_porch = 20;
|
|
let horizontal_blank_back_porch = 20;
|
|
let vertical_blank_front_porch = 20;
|
|
let vertical_blank_back_porch = 20;
|
|
let hsync_position = 0;
|
|
let horizontal_active_width =
|
|
(horizontal_resolution + overscan_left).div_ceil(16) * 16; // Round up to a multiple of 16.
|
|
let vertical_active_height = vertical_resolution;
|
|
FrameTiming {
|
|
horizontal_total_width: hsync_width
|
|
+ horizontal_blank_back_porch
|
|
+ horizontal_active_width
|
|
+ horizontal_blank_front_porch,
|
|
vertical_total_height: vsync_width
|
|
+ vertical_blank_back_porch
|
|
+ vertical_active_height
|
|
+ vertical_blank_front_porch,
|
|
horizontal_blank_front_porch: horizontal_blank_front_porch + hsync_width,
|
|
vertical_blank_front_porch: vertical_blank_front_porch + vsync_width,
|
|
horizontal_active_width,
|
|
vertical_active_height,
|
|
vsync_width,
|
|
hsync_width,
|
|
hsync_position,
|
|
}
|
|
})
|
|
.with_hsync_idle_level(Level::High)
|
|
.with_vsync_idle_level(Level::High)
|
|
.with_de_idle_level(Level::Low);
|
|
|
|
unconfigured_dpi.apply_config(&lcd_config).unwrap();
|
|
|
|
bl_timer
|
|
.configure(ledc::timer::config::Config {
|
|
duty: ledc::timer::config::Duty::Duty5Bit,
|
|
clock_source: ledc::timer::LSClockSource::APBClk,
|
|
frequency: Rate::from_khz(24),
|
|
})
|
|
.unwrap();
|
|
let backlight = Backlight::new(bl_timer, move |bl_timer| {
|
|
let mut bl_channel = bl_channel; // Forces bl_channel to be moved before it is mutated.
|
|
bl_channel
|
|
.configure(ledc::channel::config::Config {
|
|
timer: bl_timer,
|
|
drive_mode: esp_hal::gpio::DriveMode::PushPull,
|
|
duty_pct: 0,
|
|
})
|
|
.unwrap();
|
|
bl_channel
|
|
});
|
|
|
|
let mut lcd = Self {
|
|
controller: St7701sController {
|
|
sck,
|
|
mosi,
|
|
cs,
|
|
backlight,
|
|
},
|
|
dpi: unconfigured_dpi,
|
|
};
|
|
|
|
lcd.controller.send_init_sequence().await;
|
|
lcd.controller.sleep_on().await;
|
|
|
|
lcd
|
|
}
|
|
}
|