acid/firmware/acid-firmware/src/peripherals/st7701s/mod.rs
Jakub Hlusička c3f58178fc Code cleanup
2026-02-28 03:15:04 +01:00

228 lines
8.4 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 log::debug;
use ouroboros::self_referencing;
use crate::peripherals::st7701s::commands::Command;
mod commands;
mod init_sequence;
mod spi;
#[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 &*init_sequence::INIT_SEQUENCE_COMMANDS {
for command in subsequence {
spi::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::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(commands::CmdSlpin()).await;
}
/// Brings the display out of sleep mode and enables the backlight.
pub async fn sleep_off(&mut self) {
self.send(commands::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
}
}