2025-12-27 21:03:52 +01:00
|
|
|
use embassy_time::Timer;
|
|
|
|
|
use esp_hal::{
|
|
|
|
|
DriverMode,
|
|
|
|
|
gpio::{Flex, Level, Output},
|
|
|
|
|
lcd_cam::lcd::{
|
2026-01-28 22:46:14 +01:00
|
|
|
ClockMode, DelayMode, Phase, Polarity,
|
|
|
|
|
dpi::{Dpi, Format, FrameTiming},
|
2025-12-27 21:03:52 +01:00
|
|
|
},
|
2026-02-27 21:25:59 +01:00
|
|
|
ledc::{self, LowSpeed, channel::ChannelIFace, timer::TimerIFace},
|
2025-12-27 21:03:52 +01:00
|
|
|
time::Rate,
|
|
|
|
|
};
|
|
|
|
|
use log::debug;
|
2026-02-27 21:25:59 +01:00
|
|
|
use ouroboros::self_referencing;
|
2025-12-24 02:07:21 +01:00
|
|
|
|
2026-02-28 03:15:04 +01:00
|
|
|
use crate::peripherals::st7701s::commands::Command;
|
2025-12-27 21:03:52 +01:00
|
|
|
|
2026-02-28 03:15:04 +01:00
|
|
|
mod commands;
|
|
|
|
|
mod init_sequence;
|
|
|
|
|
mod spi;
|
2025-12-24 02:07:21 +01:00
|
|
|
|
2026-02-27 21:25:59 +01:00
|
|
|
#[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.");
|
|
|
|
|
|
2026-02-28 03:15:04 +01:00
|
|
|
for (subsequence, delay_ms) in &*init_sequence::INIT_SEQUENCE_COMMANDS {
|
2026-02-27 21:25:59 +01:00
|
|
|
for command in subsequence {
|
2026-02-28 03:15:04 +01:00
|
|
|
spi::spi_write(
|
2026-02-27 21:25:59 +01:00
|
|
|
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) {
|
2026-02-28 03:15:04 +01:00
|
|
|
spi::spi_write(
|
2026-02-27 21:25:59 +01:00
|
|
|
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();
|
2026-02-28 03:15:04 +01:00
|
|
|
self.send(commands::CmdSlpin()).await;
|
2026-02-27 21:25:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Brings the display out of sleep mode and enables the backlight.
|
|
|
|
|
pub async fn sleep_off(&mut self) {
|
2026-02-28 03:15:04 +01:00
|
|
|
self.send(commands::CmdSlpout()).await;
|
2026-02-27 21:25:59 +01:00
|
|
|
self.backlight.set_duty(100).unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-27 21:03:52 +01:00
|
|
|
pub struct St7701s<'d, Dm>
|
|
|
|
|
where
|
|
|
|
|
Dm: DriverMode,
|
|
|
|
|
{
|
2026-02-27 21:25:59 +01:00
|
|
|
pub controller: St7701sController<'d>,
|
2025-12-27 21:03:52 +01:00
|
|
|
pub dpi: Dpi<'d, Dm>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'d, Dm> St7701s<'d, Dm>
|
|
|
|
|
where
|
|
|
|
|
Dm: DriverMode,
|
|
|
|
|
{
|
2026-02-27 21:25:59 +01:00
|
|
|
/// Initializes an ST7701S display, and puts it to sleep.
|
|
|
|
|
/// To turn it on, invoke `St7701sController:sleep_off`.
|
2025-12-27 21:03:52 +01:00
|
|
|
pub async fn new(
|
|
|
|
|
mut sck: Output<'d>,
|
|
|
|
|
mut mosi: Flex<'d>,
|
|
|
|
|
mut cs: Output<'d>,
|
|
|
|
|
mut unconfigured_dpi: Dpi<'d, Dm>,
|
2026-02-27 21:25:59 +01:00
|
|
|
mut bl_timer: ledc::timer::Timer<'d, LowSpeed>,
|
|
|
|
|
bl_channel: ledc::channel::Channel<'d, LowSpeed>,
|
2025-12-27 21:03:52 +01:00
|
|
|
) -> 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
|
2026-02-17 00:51:02 +01:00
|
|
|
//
|
|
|
|
|
// Adafruit would use 11 MHz.
|
|
|
|
|
// I had lowered the frequency, so that `DmaBounce` could keep up.
|
2026-02-27 21:25:59 +01:00
|
|
|
.with_frequency(Rate::from_mhz(9)) // From Adafruit
|
2025-12-27 21:03:52 +01:00
|
|
|
.with_clock_mode(ClockMode {
|
|
|
|
|
polarity: Polarity::IdleLow, // From Adafruit
|
|
|
|
|
phase: Phase::ShiftHigh, // From Adafruit
|
|
|
|
|
})
|
2026-01-01 03:22:43 +01:00
|
|
|
.with_de_mode(DelayMode::RaisingEdge)
|
|
|
|
|
.with_hsync_mode(DelayMode::RaisingEdge)
|
|
|
|
|
.with_vsync_mode(DelayMode::RaisingEdge)
|
|
|
|
|
.with_output_bit_mode(DelayMode::RaisingEdge)
|
2025-12-27 21:03:52 +01:00
|
|
|
.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();
|
|
|
|
|
|
2026-02-27 21:25:59 +01:00
|
|
|
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
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-27 21:03:52 +01:00
|
|
|
let mut lcd = Self {
|
2026-02-27 21:25:59 +01:00
|
|
|
controller: St7701sController {
|
|
|
|
|
sck,
|
|
|
|
|
mosi,
|
|
|
|
|
cs,
|
|
|
|
|
backlight,
|
|
|
|
|
},
|
2025-12-27 21:03:52 +01:00
|
|
|
dpi: unconfigured_dpi,
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-27 21:25:59 +01:00
|
|
|
lcd.controller.send_init_sequence().await;
|
|
|
|
|
lcd.controller.sleep_on().await;
|
2025-12-27 21:03:52 +01:00
|
|
|
|
|
|
|
|
lcd
|
|
|
|
|
}
|
2025-12-24 02:07:21 +01:00
|
|
|
}
|