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 } }