diff --git a/esp-hal/CHANGELOG.md b/esp-hal/CHANGELOG.md index e2f4c1c3e78..cc49e892106 100644 --- a/esp-hal/CHANGELOG.md +++ b/esp-hal/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `I2c::with_timeout` (#2361) - `Spi::half_duplex_read` and `Spi::half_duplex_write` (#2373) - `Cpu::COUNT` and `Cpu::current()` (#?) +- Add RGB/DPI driver (#2415) +- Add `DmaLoopBuf` (#2415) ### Changed diff --git a/esp-hal/src/dma/buffers.rs b/esp-hal/src/dma/buffers.rs index b3f2d43351f..48aa156db96 100644 --- a/esp-hal/src/dma/buffers.rs +++ b/esp-hal/src/dma/buffers.rs @@ -1,4 +1,7 @@ -use core::ptr::null_mut; +use core::{ + ops::{Deref, DerefMut}, + ptr::null_mut, +}; use super::*; use crate::soc::is_slice_in_dram; @@ -1000,3 +1003,89 @@ unsafe impl DmaRxBuffer for EmptyBuf { 0 } } + +/// DMA Loop Buffer +/// +/// This consists of a single descriptor that points to itself and points to a +/// single buffer, resulting in the buffer being transmitted over and over +/// again, indefinitely. +/// +/// Note: A DMA descriptor is 12 bytes. If your buffer is significantly shorter +/// than this, the DMA channel will spend more time reading the descriptor than +/// it does reading the buffer, which may leave it unable to keep up with the +/// bandwidth requirements of some peripherals at high frequencies. +pub struct DmaLoopBuf { + descriptor: &'static mut DmaDescriptor, + buffer: &'static mut [u8], +} + +impl DmaLoopBuf { + /// Create a new [DmaLoopBuf]. + pub fn new( + descriptor: &'static mut DmaDescriptor, + buffer: &'static mut [u8], + ) -> Result { + if !is_slice_in_dram(buffer) { + return Err(DmaBufError::UnsupportedMemoryRegion); + } + if !is_slice_in_dram(core::slice::from_ref(descriptor)) { + return Err(DmaBufError::UnsupportedMemoryRegion); + } + + if buffer.len() > max_chunk_size(None) { + return Err(DmaBufError::InsufficientDescriptors); + } + + descriptor.set_owner(Owner::Dma); // Doesn't matter + descriptor.set_suc_eof(false); + descriptor.set_length(buffer.len()); + descriptor.set_size(buffer.len()); + descriptor.buffer = buffer.as_mut_ptr(); + descriptor.next = descriptor; + + Ok(Self { descriptor, buffer }) + } + + /// Consume the buf, returning the descriptor and buffer. + pub fn split(self) -> (&'static mut DmaDescriptor, &'static mut [u8]) { + (self.descriptor, self.buffer) + } +} + +unsafe impl DmaTxBuffer for DmaLoopBuf { + type View = Self; + + fn prepare(&mut self) -> Preparation { + Preparation { + start: self.descriptor, + block_size: None, + is_burstable: true, + } + } + + fn into_view(self) -> Self::View { + self + } + + fn from_view(view: Self::View) -> Self { + view + } + + fn length(&self) -> usize { + panic!("DmaLoopBuf does not have a length") + } +} + +impl Deref for DmaLoopBuf { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.buffer + } +} + +impl DerefMut for DmaLoopBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + self.buffer + } +} diff --git a/esp-hal/src/dma/mod.rs b/esp-hal/src/dma/mod.rs index c228517e691..6a7b8142d94 100644 --- a/esp-hal/src/dma/mod.rs +++ b/esp-hal/src/dma/mod.rs @@ -761,6 +761,30 @@ macro_rules! dma_rx_stream_buffer { }}; } +/// Convenience macro to create a [DmaLoopBuf] from a buffer size. +/// +/// ## Usage +/// ```rust,no_run +#[doc = crate::before_snippet!()] +/// use esp_hal::dma_loop_buffer; +/// +/// let buf = dma_loop_buffer!(2000); +/// # } +/// ``` +#[macro_export] +macro_rules! dma_loop_buffer { + ($size:expr) => {{ + const { + ::core::assert!($size <= 4095, "size must be <= 4095"); + ::core::assert!($size > 0, "size must be > 0"); + } + + let (buffer, descriptors) = $crate::dma_buffers_impl!($size, $size, is_circular = false); + + $crate::dma::DmaLoopBuf::new(&mut descriptors[0], buffer).unwrap() + }}; +} + /// DMA Errors #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/esp-hal/src/lcd_cam/lcd/dpi.rs b/esp-hal/src/lcd_cam/lcd/dpi.rs new file mode 100644 index 00000000000..c18e3a4a906 --- /dev/null +++ b/esp-hal/src/lcd_cam/lcd/dpi.rs @@ -0,0 +1,646 @@ +//! # LCD - RGB/Digital Parallel Interface Mode +//! +//! ## Overview +//! +//! ## Example +//! +//! ### A display. + +use core::{ + mem::ManuallyDrop, + ops::{Deref, DerefMut}, +}; + +use fugit::HertzU32; + +use crate::{ + clock::Clocks, + dma::{ChannelTx, DmaChannelConvert, DmaEligible, DmaError, DmaPeripheral, DmaTxBuffer, Tx}, + gpio::{interconnect::PeripheralOutput, Level, OutputSignal}, + lcd_cam::{ + lcd::{ClockMode, DelayMode, Lcd, Phase, Polarity}, + private::calculate_clkm, + BitOrder, + ByteOrder, + }, + peripheral::{Peripheral, PeripheralRef}, + peripherals::LCD_CAM, + Mode, +}; + +/// Represents the RGB LCD interface. +pub struct Dpi<'d> { + lcd_cam: PeripheralRef<'d, LCD_CAM>, + tx_channel: ChannelTx<'d, ::Dma>, +} + +impl<'d> Dpi<'d> { + /// Create a new instance of the RGB/DPI driver. + pub fn new( + lcd: Lcd<'d, DM>, + channel: ChannelTx<'d, CH>, + frequency: HertzU32, + config: Config, + ) -> Self + where + CH: DmaChannelConvert<::Dma>, + { + let lcd_cam = lcd.lcd_cam; + + let clocks = Clocks::get(); + // Due to https://www.espressif.com/sites/default/files/documentation/esp32-s3_errata_en.pdf + // the LCD_PCLK divider must be at least 2. To make up for this the user + // provided frequency is doubled to match. + let (i, divider) = calculate_clkm( + (frequency.to_Hz() * 2) as _, + &[ + clocks.xtal_clock.to_Hz() as _, + clocks.cpu_clock.to_Hz() as _, + clocks.crypto_pwm_clock.to_Hz() as _, + ], + ); + + lcd_cam.lcd_clock().write(|w| unsafe { + // Force enable the clock for all configuration registers. + w.clk_en().set_bit(); + w.lcd_clk_sel().bits((i + 1) as _); + w.lcd_clkm_div_num().bits(divider.div_num as _); + w.lcd_clkm_div_b().bits(divider.div_b as _); + w.lcd_clkm_div_a().bits(divider.div_a as _); // LCD_PCLK = LCD_CLK / 2 + w.lcd_clk_equ_sysclk().clear_bit(); + w.lcd_clkcnt_n().bits(2 - 1); // Must not be 0. + w.lcd_ck_idle_edge() + .bit(config.clock_mode.polarity == Polarity::IdleHigh); + w.lcd_ck_out_edge() + .bit(config.clock_mode.phase == Phase::ShiftHigh) + }); + lcd_cam.lcd_user().modify(|_, w| w.lcd_reset().set_bit()); + + lcd_cam + .lcd_rgb_yuv() + .write(|w| w.lcd_conv_bypass().clear_bit()); + + lcd_cam.lcd_user().modify(|_, w| { + if config.format.enable_2byte_mode { + w.lcd_8bits_order().bit(false); + w.lcd_byte_order() + .bit(config.format.byte_order == ByteOrder::Inverted); + } else { + w.lcd_8bits_order() + .bit(config.format.byte_order == ByteOrder::Inverted); + w.lcd_byte_order().bit(false); + } + w.lcd_bit_order() + .bit(config.format.bit_order == BitOrder::Inverted); + w.lcd_2byte_en().bit(config.format.enable_2byte_mode); + + // Only valid in Intel8080 mode. + w.lcd_cmd().clear_bit(); + w.lcd_dummy().clear_bit(); + + // This needs to be explicitly set for RGB mode. + w.lcd_dout().set_bit() + }); + + let timing = &config.timing; + lcd_cam.lcd_ctrl().modify(|_, w| unsafe { + // Enable RGB mode, and input VSYNC, HSYNC, and DE signals. + w.lcd_rgb_mode_en().set_bit(); + + w.lcd_hb_front() + .bits((timing.horizontal_blank_front_porch as u16).saturating_sub(1)); + w.lcd_va_height() + .bits((timing.vertical_active_height as u16).saturating_sub(1)); + w.lcd_vt_height() + .bits((timing.vertical_total_height as u16).saturating_sub(1)) + }); + lcd_cam.lcd_ctrl1().modify(|_, w| unsafe { + w.lcd_vb_front() + .bits((timing.vertical_blank_front_porch as u8).saturating_sub(1)); + w.lcd_ha_width() + .bits((timing.horizontal_active_width as u16).saturating_sub(1)); + w.lcd_ht_width() + .bits((timing.horizontal_total_width as u16).saturating_sub(1)) + }); + lcd_cam.lcd_ctrl2().modify(|_, w| unsafe { + w.lcd_vsync_width() + .bits((timing.vsync_width as u8).saturating_sub(1)); + w.lcd_vsync_idle_pol().bit(config.vsync_idle_level.into()); + w.lcd_de_idle_pol().bit(config.de_idle_level.into()); + w.lcd_hs_blank_en().bit(config.hs_blank_en); + w.lcd_hsync_width() + .bits((timing.hsync_width as u8).saturating_sub(1)); + w.lcd_hsync_idle_pol().bit(config.hsync_idle_level.into()); + w.lcd_hsync_position().bits(timing.hsync_position as u8) + }); + + lcd_cam.lcd_misc().modify(|_, w| unsafe { + // TODO: Find out what this field actually does. + // Set the threshold for Async Tx FIFO full event. (5 bits) + w.lcd_afifo_threshold_num().bits((1 << 5) - 1); + + // Doesn't matter for RGB mode. + w.lcd_vfk_cyclelen().bits(0); + w.lcd_vbk_cyclelen().bits(0); + + // 1: Send the next frame data when the current frame is sent out. + // 0: LCD stops when the current frame is sent out. + w.lcd_next_frame_en().clear_bit(); + + // Enable blank region when LCD sends data out. + w.lcd_bk_en().bit(!config.disable_black_region) + }); + lcd_cam.lcd_dly_mode().modify(|_, w| unsafe { + w.lcd_de_mode().bits(config.de_mode as u8); + w.lcd_hsync_mode().bits(config.hsync_mode as u8); + w.lcd_vsync_mode().bits(config.vsync_mode as u8); + w + }); + lcd_cam.lcd_data_dout_mode().modify(|_, w| unsafe { + w.dout0_mode().bits(config.output_bit_mode as u8); + w.dout1_mode().bits(config.output_bit_mode as u8); + w.dout2_mode().bits(config.output_bit_mode as u8); + w.dout3_mode().bits(config.output_bit_mode as u8); + w.dout4_mode().bits(config.output_bit_mode as u8); + w.dout5_mode().bits(config.output_bit_mode as u8); + w.dout6_mode().bits(config.output_bit_mode as u8); + w.dout7_mode().bits(config.output_bit_mode as u8); + w.dout8_mode().bits(config.output_bit_mode as u8); + w.dout9_mode().bits(config.output_bit_mode as u8); + w.dout10_mode().bits(config.output_bit_mode as u8); + w.dout11_mode().bits(config.output_bit_mode as u8); + w.dout12_mode().bits(config.output_bit_mode as u8); + w.dout13_mode().bits(config.output_bit_mode as u8); + w.dout14_mode().bits(config.output_bit_mode as u8); + w.dout15_mode().bits(config.output_bit_mode as u8) + }); + + lcd_cam.lcd_user().modify(|_, w| w.lcd_update().set_bit()); + + Self { + lcd_cam, + tx_channel: channel.degrade(), + } + } + + /// Configures the control pins for the RGB/DPI interface. + pub fn with_ctrl_pins< + VSYNC: PeripheralOutput, + HSYNC: PeripheralOutput, + DE: PeripheralOutput, + PCLK: PeripheralOutput, + >( + self, + vsync: impl Peripheral

+ 'd, + hsync: impl Peripheral

+ 'd, + de: impl Peripheral

+ 'd, + pclk: impl Peripheral

+ 'd, + ) -> Self { + crate::into_mapped_ref!(vsync); + crate::into_mapped_ref!(hsync); + crate::into_mapped_ref!(de); + crate::into_mapped_ref!(pclk); + + vsync.set_to_push_pull_output(crate::private::Internal); + hsync.set_to_push_pull_output(crate::private::Internal); + de.set_to_push_pull_output(crate::private::Internal); + pclk.set_to_push_pull_output(crate::private::Internal); + + vsync.connect_peripheral_to_output(OutputSignal::LCD_V_SYNC, crate::private::Internal); + hsync.connect_peripheral_to_output(OutputSignal::LCD_H_SYNC, crate::private::Internal); + de.connect_peripheral_to_output(OutputSignal::LCD_H_ENABLE, crate::private::Internal); + pclk.connect_peripheral_to_output(OutputSignal::LCD_PCLK, crate::private::Internal); + + self + } + + /// Configures the data pins for the RGB/DPI interface. + #[allow(clippy::too_many_arguments)] + pub fn with_data_pins< + D0: PeripheralOutput, + D1: PeripheralOutput, + D2: PeripheralOutput, + D3: PeripheralOutput, + D4: PeripheralOutput, + D5: PeripheralOutput, + D6: PeripheralOutput, + D7: PeripheralOutput, + D8: PeripheralOutput, + D9: PeripheralOutput, + D10: PeripheralOutput, + D11: PeripheralOutput, + D12: PeripheralOutput, + D13: PeripheralOutput, + D14: PeripheralOutput, + D15: PeripheralOutput, + >( + self, + d0: impl Peripheral

+ 'd, + d1: impl Peripheral

+ 'd, + d2: impl Peripheral

+ 'd, + d3: impl Peripheral

+ 'd, + d4: impl Peripheral

+ 'd, + d5: impl Peripheral

+ 'd, + d6: impl Peripheral

+ 'd, + d7: impl Peripheral

+ 'd, + d8: impl Peripheral

+ 'd, + d9: impl Peripheral

+ 'd, + d10: impl Peripheral

+ 'd, + d11: impl Peripheral

+ 'd, + d12: impl Peripheral

+ 'd, + d13: impl Peripheral

+ 'd, + d14: impl Peripheral

+ 'd, + d15: impl Peripheral

+ 'd, + ) -> Self { + crate::into_mapped_ref!(d0); + crate::into_mapped_ref!(d1); + crate::into_mapped_ref!(d2); + crate::into_mapped_ref!(d3); + crate::into_mapped_ref!(d4); + crate::into_mapped_ref!(d5); + crate::into_mapped_ref!(d6); + crate::into_mapped_ref!(d7); + crate::into_mapped_ref!(d8); + crate::into_mapped_ref!(d9); + crate::into_mapped_ref!(d10); + crate::into_mapped_ref!(d11); + crate::into_mapped_ref!(d12); + crate::into_mapped_ref!(d13); + crate::into_mapped_ref!(d14); + crate::into_mapped_ref!(d15); + + d0.set_to_push_pull_output(crate::private::Internal); + d1.set_to_push_pull_output(crate::private::Internal); + d2.set_to_push_pull_output(crate::private::Internal); + d3.set_to_push_pull_output(crate::private::Internal); + d4.set_to_push_pull_output(crate::private::Internal); + d5.set_to_push_pull_output(crate::private::Internal); + d6.set_to_push_pull_output(crate::private::Internal); + d7.set_to_push_pull_output(crate::private::Internal); + d8.set_to_push_pull_output(crate::private::Internal); + d9.set_to_push_pull_output(crate::private::Internal); + d10.set_to_push_pull_output(crate::private::Internal); + d11.set_to_push_pull_output(crate::private::Internal); + d12.set_to_push_pull_output(crate::private::Internal); + d13.set_to_push_pull_output(crate::private::Internal); + d14.set_to_push_pull_output(crate::private::Internal); + d15.set_to_push_pull_output(crate::private::Internal); + + d0.connect_peripheral_to_output(OutputSignal::LCD_DATA_0, crate::private::Internal); + d1.connect_peripheral_to_output(OutputSignal::LCD_DATA_1, crate::private::Internal); + d2.connect_peripheral_to_output(OutputSignal::LCD_DATA_2, crate::private::Internal); + d3.connect_peripheral_to_output(OutputSignal::LCD_DATA_3, crate::private::Internal); + d4.connect_peripheral_to_output(OutputSignal::LCD_DATA_4, crate::private::Internal); + d5.connect_peripheral_to_output(OutputSignal::LCD_DATA_5, crate::private::Internal); + d6.connect_peripheral_to_output(OutputSignal::LCD_DATA_6, crate::private::Internal); + d7.connect_peripheral_to_output(OutputSignal::LCD_DATA_7, crate::private::Internal); + d8.connect_peripheral_to_output(OutputSignal::LCD_DATA_8, crate::private::Internal); + d9.connect_peripheral_to_output(OutputSignal::LCD_DATA_9, crate::private::Internal); + d10.connect_peripheral_to_output(OutputSignal::LCD_DATA_10, crate::private::Internal); + d11.connect_peripheral_to_output(OutputSignal::LCD_DATA_11, crate::private::Internal); + d12.connect_peripheral_to_output(OutputSignal::LCD_DATA_12, crate::private::Internal); + d13.connect_peripheral_to_output(OutputSignal::LCD_DATA_13, crate::private::Internal); + d14.connect_peripheral_to_output(OutputSignal::LCD_DATA_14, crate::private::Internal); + d15.connect_peripheral_to_output(OutputSignal::LCD_DATA_15, crate::private::Internal); + + self + } + + /// Same as [Self::with_data_pins] but specifies which pin likely + /// corresponds to which color. + #[allow(clippy::too_many_arguments)] + pub fn with_color_pins< + D0: PeripheralOutput, + D1: PeripheralOutput, + D2: PeripheralOutput, + D3: PeripheralOutput, + D4: PeripheralOutput, + D5: PeripheralOutput, + D6: PeripheralOutput, + D7: PeripheralOutput, + D8: PeripheralOutput, + D9: PeripheralOutput, + D10: PeripheralOutput, + D11: PeripheralOutput, + D12: PeripheralOutput, + D13: PeripheralOutput, + D14: PeripheralOutput, + D15: PeripheralOutput, + >( + self, + b0: impl Peripheral

+ 'd, + b1: impl Peripheral

+ 'd, + b2: impl Peripheral

+ 'd, + b3: impl Peripheral

+ 'd, + b4: impl Peripheral

+ 'd, + g0: impl Peripheral

+ 'd, + g1: impl Peripheral

+ 'd, + g2: impl Peripheral

+ 'd, + g3: impl Peripheral

+ 'd, + g4: impl Peripheral

+ 'd, + g5: impl Peripheral

+ 'd, + r0: impl Peripheral

+ 'd, + r1: impl Peripheral

+ 'd, + r2: impl Peripheral

+ 'd, + r3: impl Peripheral

+ 'd, + r4: impl Peripheral

+ 'd, + ) -> Self { + self.with_data_pins( + b0, b1, b2, b3, b4, g0, g1, g2, g3, g4, g5, r0, r1, r2, r3, r4, + ) + } + + /// Sending out the [DmaTxBuffer] to the RGB/DPI interface. + /// + /// - `next_frame_en`: Automatically send the next frame data when the + /// current frame is sent out. + pub fn send( + mut self, + next_frame_en: bool, + mut buf: TX, + ) -> Result, (DmaError, Self, TX)> { + let result = unsafe { + self.tx_channel + .prepare_transfer(DmaPeripheral::LcdCam, &mut buf) + } + .and_then(|_| self.tx_channel.start_transfer()); + if let Err(err) = result { + return Err((err, self, buf)); + } + + // Reset LCD control unit and Async Tx FIFO + self.lcd_cam + .lcd_user() + .modify(|_, w| w.lcd_reset().set_bit()); + self.lcd_cam + .lcd_misc() + .modify(|_, w| w.lcd_afifo_reset().set_bit()); + + self.lcd_cam.lcd_misc().modify(|_, w| { + // 1: Send the next frame data when the current frame is sent out. + // 0: LCD stops when the current frame is sent out. + w.lcd_next_frame_en().bit(next_frame_en) + }); + + // Start the transfer. + self.lcd_cam.lcd_user().modify(|_, w| { + w.lcd_update().set_bit(); + w.lcd_start().set_bit() + }); + + Ok(DpiTransfer { + dpi: ManuallyDrop::new(self), + buffer_view: ManuallyDrop::new(buf.into_view()), + }) + } +} + +/// Represents an ongoing (or potentially finished) transfer using the RGB LCD +/// interface +pub struct DpiTransfer<'d, BUF: DmaTxBuffer> { + dpi: ManuallyDrop>, + buffer_view: ManuallyDrop, +} + +impl<'d, BUF: DmaTxBuffer> DpiTransfer<'d, BUF> { + /// Returns true when [Self::wait] will not block. + pub fn is_done(&self) -> bool { + self.dpi + .lcd_cam + .lcd_user() + .read() + .lcd_start() + .bit_is_clear() + } + + /// Stops this transfer on the spot and returns the peripheral and buffer. + pub fn stop(mut self) -> (Dpi<'d>, BUF) { + self.stop_peripherals(); + let (dpi, view) = self.release(); + (dpi, BUF::from_view(view)) + } + + /// Waits for the transfer to finish and returns the peripheral and buffer. + /// + /// Note: If you specified `next_frame_en` as true in [Dpi::send], you're + /// just waiting for a DMA error when you call this. + pub fn wait(mut self) -> (Result<(), DmaError>, Dpi<'d>, BUF) { + while !self.is_done() { + core::hint::spin_loop(); + } + + // Stop the DMA. + // + // If the user sends more data to the DMA than the LCD_CAM needs for a single + // frame, the DMA will still be running after the LCD_CAM stops. + self.dpi.tx_channel.stop_transfer(); + + // Note: There is no "done" interrupt to clear. + + let (dpi, view) = self.release(); + let result = if dpi.tx_channel.has_error() { + Err(DmaError::DescriptorError) + } else { + Ok(()) + }; + + (result, dpi, BUF::from_view(view)) + } + + fn release(mut self) -> (Dpi<'d>, BUF::View) { + // SAFETY: Since forget is called on self, we know that self.dpi and + // self.buffer_view won't be touched again. + let result = unsafe { + let dpi = ManuallyDrop::take(&mut self.dpi); + let view = ManuallyDrop::take(&mut self.buffer_view); + (dpi, view) + }; + core::mem::forget(self); + result + } + + fn stop_peripherals(&mut self) { + // Stop the LCD_CAM peripheral. + self.dpi + .lcd_cam + .lcd_user() + .modify(|_, w| w.lcd_start().clear_bit()); + + // Stop the DMA + self.dpi.tx_channel.stop_transfer(); + } +} + +impl<'d, BUF: DmaTxBuffer> Deref for DpiTransfer<'d, BUF> { + type Target = BUF::View; + + fn deref(&self) -> &Self::Target { + &self.buffer_view + } +} + +impl<'d, BUF: DmaTxBuffer> DerefMut for DpiTransfer<'d, BUF> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.buffer_view + } +} + +impl<'d, BUF: DmaTxBuffer> Drop for DpiTransfer<'d, BUF> { + fn drop(&mut self) { + self.stop_peripherals(); + + // SAFETY: This is Drop, we know that self.dpi and self.buf_view + // won't be touched again. + let view = unsafe { + ManuallyDrop::drop(&mut self.dpi); + ManuallyDrop::take(&mut self.buffer_view) + }; + let _ = BUF::from_view(view); + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// Configuration settings for the RGB/DPI interface. +pub struct Config { + /// Specifies the clock mode, including polarity and phase settings. + pub clock_mode: ClockMode, + + /// Format of the byte data sent out. + pub format: Format, + + /// Timing settings for the peripheral. + pub timing: FrameTiming, + + /// The vsync signal level in IDLE state. + pub vsync_idle_level: Level, + + /// The hsync signal level in IDLE state. + pub hsync_idle_level: Level, + + /// The de signal level in IDLE state. + pub de_idle_level: Level, + + /// If enabled, the hsync pulse will be sent out in vertical blanking lines. + /// i.e. When no valid data is actually sent out. Otherwise, hysnc + /// pulses will only be sent out in active region lines. + pub hs_blank_en: bool, + + /// Disables blank region when LCD sends data out. + pub disable_black_region: bool, + + /// The output LCD_DE is delayed by module clock LCD_CLK. + pub de_mode: DelayMode, + /// The output LCD_HSYNC is delayed by module clock LCD_CLK. + pub hsync_mode: DelayMode, + /// The output LCD_VSYNC is delayed by module clock LCD_CLK. + pub vsync_mode: DelayMode, + /// The output data bits are delayed by module clock LCD_CLK. + pub output_bit_mode: DelayMode, +} + +impl Default for Config { + fn default() -> Self { + Config { + clock_mode: Default::default(), + format: Default::default(), + timing: Default::default(), + vsync_idle_level: Level::Low, + hsync_idle_level: Level::Low, + de_idle_level: Level::Low, + hs_blank_en: true, + disable_black_region: false, + de_mode: Default::default(), + hsync_mode: Default::default(), + vsync_mode: Default::default(), + output_bit_mode: Default::default(), + } + } +} + +/// Controls how the peripheral should treat data received from the DMA. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Format { + /// Configures the bit order for data transmission. + pub bit_order: BitOrder, + + /// Configures the byte order for data transmission. + /// + /// - In 8-bit mode, [ByteOrder::Inverted] means every two bytes are + /// swapped. + /// - In 16-bit mode, this controls the byte order (endianness). + pub byte_order: ByteOrder, + + /// If true, the width of the output is 16 bits. + /// Otherwise, the width of the output is 8 bits. + pub enable_2byte_mode: bool, +} + +/// The timing numbers for the driver to follow. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct FrameTiming { + /// The horizontal total width of a frame. + /// + /// This should be greater than `horizontal_blank_front_porch` + + /// `horizontal_active_width`. + /// + /// Max is 4096 (12 bits). + pub horizontal_total_width: usize, + + /// The horizontal blank front porch of a frame. + /// + /// This is the number of PCLKs between the start of the line and the start + /// of active data in the line. + /// + /// Note: This includes `hsync_width`. + /// + /// Max is 2048 (11 bits). + pub horizontal_blank_front_porch: usize, + + /// The horizontal active width of a frame. i.e. The number of pixels in a + /// line. This is typically the horizontal resolution of the screen. + /// + /// Max is 4096 (12 bits). + pub horizontal_active_width: usize, + + /// The vertical total height of a frame. + /// + /// This should be greater than `vertical_blank_front_porch` + + /// `vertical_active_height`. + /// + /// Max is 1024 (10 bits). + pub vertical_total_height: usize, + + /// The vertical blank front porch height of a frame. + /// + /// This is the number of (blank/invalid) lines before the start of the + /// frame. + /// + /// Note: This includes `vsync_width`. + /// + /// Max is 256 (8 bits). + pub vertical_blank_front_porch: usize, + + /// The vertical active height of a frame. i.e. The number of lines in a + /// frame. This is typically the vertical resolution of the screen. + /// + /// Max is 1024 (10 bits). + pub vertical_active_height: usize, + + /// It is the width of LCD_VSYNC active pulse in a line. + /// + /// Max is 128 (7 bits). + pub vsync_width: usize, + + /// The width of LCD_HSYNC active pulse in a line. + /// + /// Max is 128 (7 bits). + pub hsync_width: usize, + + /// It is the position of LCD_HSYNC active pulse in a line. + /// + /// Max is 128 (7 bits). + pub hsync_position: usize, +} diff --git a/esp-hal/src/lcd_cam/lcd/mod.rs b/esp-hal/src/lcd_cam/lcd/mod.rs index cf7e8036b02..7727400fa15 100644 --- a/esp-hal/src/lcd_cam/lcd/mod.rs +++ b/esp-hal/src/lcd_cam/lcd/mod.rs @@ -12,6 +12,7 @@ use crate::{peripheral::PeripheralRef, peripherals::LCD_CAM}; +pub mod dpi; pub mod i8080; /// Represents an LCD interface. diff --git a/examples/src/bin/lcd_dpi.rs b/examples/src/bin/lcd_dpi.rs new file mode 100644 index 00000000000..5b5e345da78 --- /dev/null +++ b/examples/src/bin/lcd_dpi.rs @@ -0,0 +1,162 @@ +//! Drives the 16-bit parallel RGB display on Makerfabs ESP32-S3-Parallel-TFT-with-Touch-4.3inch +//! +//! This example fills the screen with every color. +//! +//! The following wiring is assumed: +//! - LCD_VYSNC => GPIO41 +//! - LCD_HSYNC => GPIO39 +//! - LCD_DE => GPIO40 +//! - LCD_PCLK => GPIO42 +//! - LCD_DATA0 => GPIO8 +//! - LCD_DATA1 => GPIO3 +//! - LCD_DATA2 => GPIO46 +//! - LCD_DATA3 => GPIO9 +//! - LCD_DATA4 => GPIO1 +//! - LCD_DATA5 => GPIO5 +//! - LCD_DATA6 => GPIO6 +//! - LCD_DATA7 => GPIO7 +//! - LCD_DATA8 => GPIO15 +//! - LCD_DATA9 => GPIO16 +//! - LCD_DATA10 => GPIO4 +//! - LCD_DATA11 => GPIO45 +//! - LCD_DATA12 => GPIO48 +//! - LCD_DATA13 => GPIO47 +//! - LCD_DATA14 => GPIO21 +//! - LCD_DATA15 => GPIO14 + +//% CHIPS: esp32s3 + +#![no_std] +#![no_main] + +use core::iter::empty; + +use esp_backtrace as _; +use esp_hal::{ + dma::{Dma, DmaPriority}, + dma_loop_buffer, + gpio::{Io, Level}, + lcd_cam::{ + lcd::{ + dpi::{Config, Dpi, Format, FrameTiming}, + ClockMode, + Phase, + Polarity, + }, + LcdCam, + }, + prelude::*, +}; +use esp_println::println; + +#[entry] +fn main() -> ! { + esp_println::logger::init_logger_from_env(); + + let peripherals = esp_hal::init(esp_hal::Config::default()); + + let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); + + let dma = Dma::new(peripherals.DMA); + let channel = dma.channel2.configure(true, DmaPriority::Priority0); + let lcd_cam = LcdCam::new(peripherals.LCD_CAM); + + let mut dma_buf = dma_loop_buffer!(2 * 16); + + let config = Config { + clock_mode: ClockMode { + polarity: Polarity::IdleLow, + phase: Phase::ShiftHigh, + }, + format: Format { + enable_2byte_mode: true, + ..Default::default() + }, + // https://www.makerfabs.com/desfile/files/QT4300H40R10-V03-Spec.pdf + timing: FrameTiming { + horizontal_active_width: 800, + horizontal_total_width: 928, // 889..1143 + horizontal_blank_front_porch: 40, // 1..255 + + vertical_active_height: 480, + vertical_total_height: 525, // 513..767 + vertical_blank_front_porch: 13, // 1..255 + + hsync_width: 48, // 1..255 + vsync_width: 3, // 3..255 + + hsync_position: 0, + }, + vsync_idle_level: Level::High, + hsync_idle_level: Level::High, + de_idle_level: Level::Low, + disable_black_region: false, + ..Default::default() + }; + + // https://raw.githubusercontent.com/Makerfabs/ESP32-S3-Parallel-TFT-with-Touch-4.3inch/main/hardware/ESP32-S3%20Parallel%20TFT%20with%20Touch%204.3%E2%80%9C%20V3.1.PDF + let mut dpi = Dpi::new(lcd_cam.lcd, channel.tx, 30.MHz(), config) + .with_ctrl_pins( + io.pins.gpio41, + io.pins.gpio39, + io.pins.gpio40, + io.pins.gpio42, + ) + .with_data_pins( + // Blue + io.pins.gpio8, + io.pins.gpio3, + io.pins.gpio46, + io.pins.gpio9, + io.pins.gpio1, + // Green + io.pins.gpio5, + io.pins.gpio6, + io.pins.gpio7, + io.pins.gpio15, + io.pins.gpio16, + io.pins.gpio4, + // Red + io.pins.gpio45, + io.pins.gpio48, + io.pins.gpio47, + io.pins.gpio21, + io.pins.gpio14, + ); + + const MAX_RED: u16 = (1 << 5) - 1; + const MAX_GREEN: u16 = (1 << 6) - 1; + const MAX_BLUE: u16 = (1 << 5) - 1; + + fn rgb(r: u16, g: u16, b: u16) -> u16 { + (r << 11) | (g << 5) | (b << 0) + } + + let mut colors = empty() + // Start with red and gradually add green + .chain((0..=MAX_GREEN).map(|g| rgb(MAX_RED, g, 0))) + // Then remove the red + .chain((0..=MAX_RED).rev().map(|r| rgb(r, MAX_GREEN, 0))) + // Then add blue + .chain((0..=MAX_BLUE).map(|b| rgb(0, MAX_GREEN, b))) + // Then remove green + .chain((0..=MAX_GREEN).rev().map(|g| rgb(0, g, MAX_BLUE))) + // Then add red + .chain((0..=MAX_RED).map(|r| rgb(r, 0, MAX_BLUE))) + // Then remove blue + .chain((0..=MAX_BLUE).rev().map(|b| rgb(MAX_RED, 0, b))) + // Once we get we have red, and we can start again. + .cycle(); + + println!("Rendering"); + loop { + let transfer = dpi.send(false, dma_buf).map_err(|e| e.0).unwrap(); + (_, dpi, dma_buf) = transfer.wait(); + + if let Some(color) = colors.next() { + for chunk in dma_buf.chunks_mut(2) { + chunk.copy_from_slice(&color.to_le_bytes()); + } + } + } +} diff --git a/hil-test/Cargo.toml b/hil-test/Cargo.toml index 300190c173a..e281a351b1c 100644 --- a/hil-test/Cargo.toml +++ b/hil-test/Cargo.toml @@ -71,6 +71,10 @@ harness = false name = "i2s" harness = false +[[test]] +name = "lcd_cam" +harness = false + [[test]] name = "lcd_cam_i8080" harness = false diff --git a/hil-test/tests/lcd_cam.rs b/hil-test/tests/lcd_cam.rs new file mode 100644 index 00000000000..cf3eed897fb --- /dev/null +++ b/hil-test/tests/lcd_cam.rs @@ -0,0 +1,162 @@ +//! LCD_CAM Camera and DPI tests + +//% CHIPS: esp32s3 +//% FEATURES: defmt + +#![no_std] +#![no_main] + +use esp_hal::{ + dma::{Dma, DmaPriority, DmaRxBuf, DmaTxBuf}, + dma_buffers, + gpio::{ + interconnect::{InputSignal, OutputSignal}, + GpioPin, + Io, + Level, + NoPin, + OutputPin, + }, + lcd_cam::{ + cam::{Camera, RxEightBits}, + lcd::{ + dpi, + dpi::{Dpi, Format, FrameTiming}, + ClockMode, + Phase, + Polarity, + }, + LcdCam, + }, + Blocking, +}; +use fugit::RateExtU32; +use hil_test as _; + +struct Context { + io: Io, + dma: Dma<'static>, + lcd_cam: LcdCam<'static, Blocking>, + dma_tx_buf: DmaTxBuf, + dma_rx_buf: DmaRxBuf, +} + +#[cfg(test)] +#[embedded_test::tests] +mod tests { + use super::*; + + #[init] + fn init() -> Context { + let peripherals = esp_hal::init(esp_hal::Config::default()); + let dma = Dma::new(peripherals.DMA); + let lcd_cam = LcdCam::new(peripherals.LCD_CAM); + let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); + + let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(50 * 50); + let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap(); + let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap(); + + Context { + io, + dma, + lcd_cam, + dma_tx_buf, + dma_rx_buf, + } + } + + #[test] + fn test_camera_can_receive_from_rgb(ctx: Context) { + let channel = ctx.dma.channel2.configure(false, DmaPriority::Priority0); + + fn split_pin(pin: GpioPin) -> (InputSignal, OutputSignal) + where + GpioPin: OutputPin, + { + let input = pin.peripheral_input(); + let output = pin.into_peripheral_output(); + (input, output) + } + + let (vsync_in, vsync_out) = split_pin(ctx.io.pins.gpio6); + let (hsync_in, hsync_out) = split_pin(ctx.io.pins.gpio7); + let (de_in, de_out) = split_pin(ctx.io.pins.gpio14); + let (pclk_in, pclk_out) = split_pin(ctx.io.pins.gpio13); + let (d0_in, d0_out) = split_pin(ctx.io.pins.gpio11); + let (d1_in, d1_out) = split_pin(ctx.io.pins.gpio9); + let (d2_in, d2_out) = split_pin(ctx.io.pins.gpio8); + let (d3_in, d3_out) = split_pin(ctx.io.pins.gpio10); + let (d4_in, d4_out) = split_pin(ctx.io.pins.gpio12); + let (d5_in, d5_out) = split_pin(ctx.io.pins.gpio18); + let (d6_in, d6_out) = split_pin(ctx.io.pins.gpio17); + let (d7_in, d7_out) = split_pin(ctx.io.pins.gpio16); + + let dpi = Dpi::new( + ctx.lcd_cam.lcd, + channel.tx, + 500u32.kHz(), + dpi::Config { + clock_mode: ClockMode { + polarity: Polarity::IdleHigh, + phase: Phase::ShiftLow, + }, + format: Format { + enable_2byte_mode: false, + ..Default::default() + }, + // Send a 50x50 video + timing: FrameTiming { + horizontal_total_width: 65, + hsync_width: 5, + horizontal_blank_front_porch: 10, + horizontal_active_width: 50, + + vertical_total_height: 65, + vsync_width: 5, + vertical_blank_front_porch: 10, + vertical_active_height: 50, + + hsync_position: 0, + }, + vsync_idle_level: Level::High, + hsync_idle_level: Level::High, + de_idle_level: Level::Low, + disable_black_region: false, + ..Default::default() + }, + ) + .with_ctrl_pins(vsync_out, hsync_out, de_out, pclk_out) + .with_data_pins( + d0_out, d1_out, d2_out, d3_out, d4_out, d5_out, d6_out, d7_out, NoPin, NoPin, NoPin, + NoPin, NoPin, NoPin, NoPin, NoPin, + ); + + let camera = Camera::new( + ctx.lcd_cam.cam, + channel.rx, + RxEightBits::new(d0_in, d1_in, d2_in, d3_in, d4_in, d5_in, d6_in, d7_in), + 1u32.MHz(), + ) + .with_ctrl_pins_and_de(vsync_in, hsync_in, de_in) + .with_pixel_clock(pclk_in); + + let mut dma_tx_buf = ctx.dma_tx_buf; + let mut dma_rx_buf = ctx.dma_rx_buf; + + for (i, b) in dma_tx_buf.as_mut_slice().iter_mut().enumerate() { + *b = ((i + 0) % 256) as u8; + } + + let camera_transfer = camera.receive(dma_rx_buf).map_err(|e| e.0).unwrap(); + // Note: next_frame_en is not false because the RGB driver doesn't send a VSYNC + // at the end of the frame, which means the DMA doesn't flush the last + // few bytes it receives. + let dpi_transfer = dpi.send(true, dma_tx_buf).map_err(|e| e.0).unwrap(); + + (_, _, dma_rx_buf) = camera_transfer.wait(); + (_, dma_tx_buf) = dpi_transfer.stop(); + + assert_eq!(dma_tx_buf.as_slice(), dma_rx_buf.as_slice()); + } +}