//! Drives the 8-bit parallel display on a WT32-SC01 Plus //! //! This example clears the screen with red and then blue every second. //! //! The following wiring is assumed: //! - Backlight => GPIO45 //! - Reset => GPIO4 //! - TE => GPIO48 //! - CD => GPIO0 //! - WR => GPIO47 //! - D0 => GPIO9 //! - D1 => GPIO46 //! - D2 => GPIO3 //! - D3 => GPIO8 //! - D4 => GPIO18 //! - D5 => GPIO17 //! - D6 => GPIO16 //! - D7 => GPIO15 //% CHIPS: esp32s3 #![no_std] #![no_main] use esp_backtrace as _; use esp_hal::{ delay::Delay, dma::DmaTxBuf, dma_tx_buffer, gpio::{Input, Level, Output, Pull}, lcd_cam::{ lcd::i8080::{Config, TxEightBits, I8080}, LcdCam, }, prelude::*, Blocking, }; use esp_println::println; #[entry] fn main() -> ! { let peripherals = esp_hal::init(esp_hal::Config::default()); let lcd_backlight = peripherals.GPIO45; let lcd_reset = peripherals.GPIO4; let lcd_rs = peripherals.GPIO0; // Command/Data selection let lcd_wr = peripherals.GPIO47; // Write clock let lcd_te = peripherals.GPIO48; // Frame sync let dma_tx_buf = dma_tx_buffer!(4000).unwrap(); let delay = Delay::new(); let mut backlight = Output::new(lcd_backlight, Level::Low); let mut reset = Output::new(lcd_reset, Level::Low); let tear_effect = Input::new(lcd_te, Pull::None); let tx_pins = TxEightBits::new( peripherals.GPIO9, peripherals.GPIO46, peripherals.GPIO3, peripherals.GPIO8, peripherals.GPIO18, peripherals.GPIO17, peripherals.GPIO16, peripherals.GPIO15, ); let lcd_cam = LcdCam::new(peripherals.LCD_CAM); let mut i8080_config = Config::default(); i8080_config.frequency = 20.MHz(); let i8080 = I8080::new(lcd_cam.lcd, peripherals.DMA_CH0, tx_pins, i8080_config) .unwrap() .with_ctrl_pins(lcd_rs, lcd_wr); // Note: This isn't provided in the HAL since different drivers may require // different considerations, like how to manage the CS pin, the CD pin, // cancellation semantics, 8 vs 16 bit, non-native primitives like Rgb565, // Rgb888, etc. This Bus is just provided as an example of how to implement // your own. struct Bus<'d> { resources: Option<(I8080<'d, Blocking>, DmaTxBuf)>, } impl<'d> Bus<'d> { fn use_resources( &mut self, func: impl FnOnce(I8080<'d, Blocking>, DmaTxBuf) -> (T, I8080<'d, Blocking>, DmaTxBuf), ) -> T { let (i8080, buf) = self.resources.take().unwrap(); let (result, i8080, buf) = func(i8080, buf); self.resources = Some((i8080, buf)); result } pub fn send(&mut self, cmd: u8, data: &[u8]) { self.use_resources(|i8080, mut buf| { buf.fill(data); match i8080.send(cmd, 0, buf) { Ok(transfer) => transfer.wait(), Err((result, i8080, buf)) => (Err(result), i8080, buf), } }) .unwrap(); } } let mut bus = Bus { resources: Some((i8080, dma_tx_buf)), }; { // https://gist.github.com/sukesh-ak/610508bc84779a26efdcf969bf51a2d1 // https://github.com/lovyan03/LovyanGFX/blob/302169a6f23e9a2a6451f03311c366d182193831/src/lgfx/v1/panel/Panel_ST7796.hpp#L28 reset.set_low(); delay.delay_micros(8_000); reset.set_high(); delay.delay_micros(64_000); // const CMD_FRMCTR1: u8 = 0xB1; // const CMD_FRMCTR2: u8 = 0xB2; // const CMD_FRMCTR3: u8 = 0xB3; const CMD_INVCTR: u8 = 0xB4; const CMD_DFUNCTR: u8 = 0xB6; // const CMD_ETMOD: u8 = 0xB7; // const CMD_PWCTR1: u8 = 0xC0; const CMD_PWCTR2: u8 = 0xC1; const CMD_PWCTR3: u8 = 0xC2; // const CMD_PWCTR4: u8 = 0xC3; // const CMD_PWCTR5: u8 = 0xC4; const CMD_VMCTR: u8 = 0xC5; const CMD_GMCTRP1: u8 = 0xE0; // Positive Gamma Correction const CMD_GMCTRN1: u8 = 0xE1; // Negative Gamma Correction const CMD_DOCA: u8 = 0xE8; // Display Output Ctrl Adjust const CMD_CSCON: u8 = 0xF0; // Command Set Control bus.send(CMD_CSCON, &[0xC3]); // Enable extension command 2 part I bus.send(CMD_CSCON, &[0x96]); // Enable extension command 2 part II bus.send(CMD_INVCTR, &[0x01]); // 1-dot inversion bus.send( CMD_DFUNCTR, &[ 0x80, // Display Function Control //Bypass 0x22, /* Source Output Scan from S1 to S960, Gate Output scan from G1 to * G480, scan cycle=2 */ 0x3B, ], ); // LCD Drive Line=8*(59+1) bus.send( CMD_DOCA, &[ 0x40, 0x8A, 0x00, 0x00, 0x29, // Source eqaulizing period time= 22.5 us 0x19, // Timing for "Gate start"=25 (Tclk) 0xA5, // Timing for "Gate End"=37 (Tclk), Gate driver EQ function ON 0x33, ], ); bus.send(CMD_PWCTR2, &[0x06]); // Power control2 //VAP(GVDD)=3.85+( vcom+vcom offset), VAN(GVCL)=-3.85+( // vcom+vcom offset) bus.send(CMD_PWCTR3, &[0xA7]); // Power control 3 //Source driving current level=low, Gamma driving current // level=High bus.send(CMD_VMCTR, &[0x18]); // VCOM Control //VCOM=0.9 delay.delay_micros(120_000); bus.send( CMD_GMCTRP1, &[ 0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B, ], ); bus.send( CMD_GMCTRN1, &[ 0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B, ], ); delay.delay_micros(120_000); bus.send(CMD_CSCON, &[0x3C]); // Command Set control // Disable extension command 2 partI bus.send(CMD_CSCON, &[0x69]); // Command Set control // Disable // extension command 2 partII bus.send(0x11, &[]); // ExitSleepMode delay.delay_micros(130_000); bus.send(0x38, &[]); // ExitIdleMode bus.send(0x29, &[]); // SetDisplayOn bus.send(0x21, &[]); // SetInvertMode(ColorInversion::Inverted) // let madctl = SetAddressMode::from(options); // bus.send(madctl)?; bus.send(0x3A, &[0x55]); // RGB565 } // Tearing Effect Line On bus.send(0x35, &[0]); let width = 320u16; let height = 480u16; { println!("Set addresses"); let width_b = width.to_be_bytes(); let height_b = height.to_be_bytes(); bus.send(0x2A, &[0, 0, width_b[0], width_b[1]]); // CASET bus.send(0x2B, &[0, 0, height_b[0], height_b[1]]) // PASET } println!("Drawing"); const RED: u16 = 0b00000_000000_11111; const BLUE: u16 = 0b11111_000000_00000; backlight.set_high(); let total_pixels = width as usize * height as usize; let total_bytes = total_pixels * 2; let (mut i8080, mut dma_tx_buf) = bus.resources.take().unwrap(); dma_tx_buf.set_length(dma_tx_buf.capacity()); for color in [RED, BLUE].iter().cycle() { let color = color.to_be_bytes(); for chunk in dma_tx_buf.as_mut_slice().chunks_mut(2) { chunk.copy_from_slice(&color); } // Naive implementation of tear prevention. A more robust implementation would // use an interrupt handler to start shipping out the next frame the // moment the tear effect pin goes high. async/await would be too slow // and would risk missing the inter-refresh window. { // Wait for display to start refreshing. while tear_effect.is_high() {} // Wait for display to finish refreshing. while tear_effect.is_low() {} // Now we have the maximum amount of time between each refresh // available, for drawing. } let mut bytes_left_to_write = total_bytes; (_, i8080, dma_tx_buf) = i8080.send(0x2Cu8, 0, dma_tx_buf).unwrap().wait(); bytes_left_to_write -= dma_tx_buf.len(); while bytes_left_to_write >= dma_tx_buf.len() { (_, i8080, dma_tx_buf) = i8080.send(0x3Cu8, 0, dma_tx_buf).unwrap().wait(); bytes_left_to_write -= dma_tx_buf.len(); } if bytes_left_to_write > 0 { dma_tx_buf.set_length(bytes_left_to_write); (_, i8080, dma_tx_buf) = i8080.send(0x3Cu8, 0, dma_tx_buf).unwrap().wait(); dma_tx_buf.set_length(dma_tx_buf.capacity()); } delay.delay_millis(1_000); } loop { delay.delay_millis(1_000); } }