diff --git a/esp-hal/CHANGELOG.md b/esp-hal/CHANGELOG.md index 0c9778d4c88..c076f53ca3e 100644 --- a/esp-hal/CHANGELOG.md +++ b/esp-hal/CHANGELOG.md @@ -52,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for ADC1 on ESP32C5. (#5215) - ESP32-C61: RNG (#5244) - C61: Add GPIO support (#5248) +- `dma`: Added `DmaBounceBuffer` and `DmaBounceBufferView` types (in `esp_hal::dma`) for streaming PSRAM-backed framebuffers via a ping-pong SRAM bounce buffer pipeline. Pass a `DmaBounceBuffer` to `Dpi::send(true, bounce_state)` for continuous RGB display output. (#5262) ### Changed @@ -91,6 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `lcd_cam`: Set `lcd_always_out_en` in the DPI (RGB) driver for continuous pixel clock output. (#5262) - SHA: Fixed potential unsoundness in `ShaDigest` by requiring exclusive access to the peripheral (#4837) - ESP32: ADC1 readings are no longer inverted (#4423) - RMT: All blocking methods now return the channel on failure. (#4302) diff --git a/esp-hal/src/dma/buffers/bounce_buffer.rs b/esp-hal/src/dma/buffers/bounce_buffer.rs new file mode 100644 index 00000000000..6547a70231c --- /dev/null +++ b/esp-hal/src/dma/buffers/bounce_buffer.rs @@ -0,0 +1,264 @@ +use super::*; + +#[cfg(feature = "unstable")] +#[instability::unstable] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum DmaBounceBufferError { + /// Framebuffer size is not divisible by bounce buffer size, + /// or bounce buffers are not equal in size, or bounce buffers are empty. + InvalidBounceBufferSize, + /// Bounce buffers or descriptors are not in DRAM. + UnsupportedMemoryRegion, +} + +#[cfg(feature = "unstable")] +/// Bounce buffer state for continuous output from a framebuffer. +/// +/// Holds two SRAM bounce buffers and their circular DMA descriptor chain. +#[instability::unstable] +pub struct DmaBounceBuffer { + descriptors: *mut [DmaDescriptor; 2], + bounce_bufs: [&'static mut [u8]; 2], + framebuffer: *const u8, + fb_len: usize, + pub(crate) bounce_pos: usize, + pub(crate) expect_eof_count: usize, + pub(crate) eof_count: usize, + pub(crate) ping_pong_idx: usize, +} + +#[cfg(feature = "unstable")] +impl DmaBounceBuffer { + /// Create a new bounce buffer state. + #[instability::unstable] + pub fn new( + descriptors: &'static mut [DmaDescriptor; 2], + framebuffer: &'static mut [u8], + bounce_buf0: &'static mut [u8], + bounce_buf1: &'static mut [u8], + ) -> Result { + let bb_size = bounce_buf0.len(); + if bb_size == 0 + || bb_size != bounce_buf1.len() + || !framebuffer.len().is_multiple_of(bb_size) + { + return Err(DmaBounceBufferError::InvalidBounceBufferSize); + } + + if !is_slice_in_dram(bounce_buf0) || !is_slice_in_dram(bounce_buf1) { + return Err(DmaBounceBufferError::UnsupportedMemoryRegion); + } + if !is_slice_in_dram(core::slice::from_ref(&descriptors[0])) + || !is_slice_in_dram(core::slice::from_ref(&descriptors[1])) + { + return Err(DmaBounceBufferError::UnsupportedMemoryRegion); + } + + let expect_eof_count = framebuffer.len() / bb_size; + let fb_len = framebuffer.len(); + let fb_ptr = framebuffer.as_ptr(); + + let mut this = Self { + descriptors: descriptors as *mut [DmaDescriptor; 2], + bounce_bufs: [bounce_buf0, bounce_buf1], + framebuffer: fb_ptr, + fb_len, + bounce_pos: 0, + expect_eof_count, + eof_count: 0, + ping_pong_idx: 0, + }; + + this.setup_descriptors(); + + Ok(this) + } + + fn setup_descriptors(&mut self) { + let bb_size = self.bounce_bufs[0].len(); + // SAFETY: `self.descriptors` comes from `&'static mut [DmaDescriptor; 2]` + // in `new()`, remains valid for program lifetime, and is accessed with + // exclusive access via `&mut self`. + let descriptors = unsafe { &mut *self.descriptors }; + + descriptors[0] = DmaDescriptor::EMPTY; + descriptors[0].set_size(bb_size); + descriptors[0].set_length(bb_size); + descriptors[0].set_suc_eof(true); + descriptors[0].set_owner(Owner::Dma); + descriptors[0].buffer = self.bounce_bufs[0].as_mut_ptr(); + + descriptors[1] = DmaDescriptor::EMPTY; + descriptors[1].set_size(bb_size); + descriptors[1].set_length(bb_size); + descriptors[1].set_suc_eof(true); + descriptors[1].set_owner(Owner::Dma); + descriptors[1].buffer = self.bounce_bufs[1].as_mut_ptr(); + + descriptors[0].next = &mut descriptors[1] as *mut DmaDescriptor; + descriptors[1].next = &mut descriptors[0] as *mut DmaDescriptor; + } + + pub(crate) fn first_descriptor_ptr(&mut self) -> *mut DmaDescriptor { + // SAFETY: Same rationale as in `setup_descriptors()`. + unsafe { (*self.descriptors).as_mut_ptr() } + } + + pub(crate) fn initial_fill(&mut self) { + self.fill_bounce_buf(0); + self.fill_bounce_buf(1); + } + + pub(crate) fn refill(&mut self) -> bool { + let idx = self.ping_pong_idx; + self.fill_bounce_buf(idx); + self.ping_pong_idx = 1 - self.ping_pong_idx; + + self.eof_count += 1; + if self.eof_count >= self.expect_eof_count { + self.eof_count = 0; + true + } else { + false + } + } + + /// Swap the active framebuffer pointer and reset position to start of frame. + pub(crate) fn set_framebuffer(&mut self, fb: *const u8) { + self.framebuffer = fb; + self.bounce_pos = 0; + self.eof_count = 0; + self.ping_pong_idx = 0; + } + + fn fill_bounce_buf(&mut self, bounce_buf_idx: usize) { + let bb_size = self.bounce_bufs[0].len(); + + if self.bounce_pos >= self.fb_len { + self.bounce_pos = 0; + } + + let end = self.bounce_pos + bb_size; + + // SAFETY: `self.framebuffer` points to a valid framebuffer of length + // `self.fb_len`. `bounce_pos` always points to a valid chunk boundary and + // `bb_size` divides `fb_len`, so this range is in-bounds. + let src = + unsafe { core::slice::from_raw_parts(self.framebuffer.add(self.bounce_pos), bb_size) }; + + self.bounce_bufs[bounce_buf_idx].copy_from_slice(src); + self.bounce_pos = end; + } +} + +#[cfg(feature = "unstable")] +/// In-progress view into a [`DmaBounceBuffer`] during an active DMA transfer. +/// +/// Accessible via `Deref` on transfer types that use [`DmaBounceBuffer`]. +/// Provides methods for double-buffered rendering. +#[instability::unstable] +pub struct DmaBounceBufferView { + inner: DmaBounceBuffer, + back_buffer: Option<(*mut u8, usize)>, +} + +#[cfg(feature = "unstable")] +impl DmaBounceBufferView { + /// Register a back buffer for double-buffered rendering. + /// + /// The back buffer must be the same size as the framebuffer passed to + /// [`DmaBounceBuffer::new`]. + #[instability::unstable] + pub fn set_back_buffer(&mut self, buf: &'static mut [u8]) { + assert!( + buf.len() == self.inner.fb_len, + "back buffer length must match front buffer length" + ); + self.back_buffer = Some((buf.as_mut_ptr(), buf.len())); + } + + /// Get a mutable reference to the back buffer for drawing. + /// + /// Returns `None` if no back buffer was registered via [`Self::set_back_buffer`]. + #[instability::unstable] + pub fn back_buffer(&mut self) -> Option<&mut [u8]> { + self.back_buffer.map(|(ptr, len)| { + // SAFETY: `ptr` comes from `&'static mut [u8]` set via `set_back_buffer()`. + // Access is exclusive via `&mut self`. + unsafe { core::slice::from_raw_parts_mut(ptr, len) } + }) + } + + /// Returns the length of the framebuffer. + #[instability::unstable] + pub fn fb_len(&self) -> usize { + self.inner.fb_len + } + + /// Called by poll() to refill a completed bounce buffer from the framebuffer. + /// Returns true when a frame boundary is reached. + pub(crate) fn refill(&mut self) -> bool { + self.inner.refill() + } + + /// Swap the front and back framebuffer pointers at a frame boundary. + pub(crate) fn swap_framebuffer(&mut self) { + if let Some((back_ptr, _)) = self.back_buffer.take() { + let old_front = self.inner.framebuffer; + let fb_len = self.inner.fb_len; + self.inner.set_framebuffer(back_ptr as *const u8); + self.back_buffer = Some((old_front as *mut u8, fb_len)); + } + } +} + +#[cfg(feature = "unstable")] +// SAFETY: `DmaBounceBuffer` contains a raw pointer to descriptor storage derived +// from a `&'static mut [DmaDescriptor; 2]`. Access to internal mutable state is +// synchronized by `&mut self` methods; no global shared mutable state is used. +unsafe impl Send for DmaBounceBuffer {} +#[cfg(feature = "unstable")] +// SAFETY: Same rationale as for `Send`. +unsafe impl Sync for DmaBounceBuffer {} + +#[cfg(feature = "unstable")] +// SAFETY: `DmaBounceBufferView` contains `DmaBounceBuffer` (Send/Sync) and a raw +// pointer to a `&'static mut [u8]` back buffer, accessed exclusively via `&mut self`. +unsafe impl Send for DmaBounceBufferView {} +#[cfg(feature = "unstable")] +// SAFETY: Same rationale as for `Send`. +unsafe impl Sync for DmaBounceBufferView {} + +#[cfg(feature = "unstable")] +unsafe impl DmaTxBuffer for DmaBounceBuffer { + type View = DmaBounceBufferView; + type Final = DmaBounceBuffer; + + fn prepare(&mut self) -> Preparation { + self.initial_fill(); + + Preparation { + start: self.first_descriptor_ptr(), + #[cfg(psram_dma)] + accesses_psram: false, + direction: TransferDirection::Out, + burst_transfer: BurstConfig::default(), + // Circular chain: owner bit is not guaranteed to be set after wraparound. + check_owner: Some(false), + auto_write_back: true, + } + } + + fn into_view(self) -> DmaBounceBufferView { + DmaBounceBufferView { + inner: self, + back_buffer: None, + } + } + + fn from_view(view: DmaBounceBufferView) -> DmaBounceBuffer { + // Back-buffer registration is intentionally dropped here. + view.inner + } +} diff --git a/esp-hal/src/dma/buffers.rs b/esp-hal/src/dma/buffers/mod.rs similarity index 99% rename from esp-hal/src/dma/buffers.rs rename to esp-hal/src/dma/buffers/mod.rs index edfbfaca3b6..b0a21622b3f 100644 --- a/esp-hal/src/dma/buffers.rs +++ b/esp-hal/src/dma/buffers/mod.rs @@ -10,6 +10,11 @@ use crate::soc::is_slice_in_dram; #[cfg(psram_dma)] use crate::soc::{is_slice_in_psram, is_valid_psram_address, is_valid_ram_address}; +#[cfg(feature = "unstable")] +mod bounce_buffer; +#[cfg(feature = "unstable")] +pub use self::bounce_buffer::*; + /// Error returned from Dma[Rx|Tx|RxTx]Buf operations. #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] #[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 index c9c619f4718..6be7c58a663 100644 --- a/esp-hal/src/lcd_cam/lcd/dpi.rs +++ b/esp-hal/src/lcd_cam/lcd/dpi.rs @@ -104,7 +104,17 @@ use core::{ use crate::{ Blocking, DriverMode, - dma::{ChannelTx, DmaError, DmaPeripheral, DmaTxBuffer, PeripheralTxChannel, TxChannelFor}, + dma::{ + ChannelTx, + DmaBounceBuffer, + DmaBounceBufferView, + DmaError, + DmaPeripheral, + DmaTxBuffer, + DmaTxInterrupt, + PeripheralTxChannel, + TxChannelFor, + }, gpio::{Level, OutputConfig, OutputSignal, interconnect::PeripheralOutput}, lcd_cam::{ BitOrder, @@ -147,6 +157,7 @@ where config: Config, ) -> Result { let tx_channel = ChannelTx::new(channel.degrade()); + // TODO: set eof_till_data_popped = false via PAC when field is accessible let mut this = Self { lcd_cam: lcd.lcd_cam, @@ -227,6 +238,7 @@ where w.lcd_dummy().clear_bit(); // This needs to be explicitly set for RGB mode. + w.lcd_always_out_en().set_bit(); w.lcd_dout().set_bit() }); @@ -644,6 +656,50 @@ impl Drop for DpiTransfer<'_, BUF, Dm> { } } +#[cfg(feature = "unstable")] +impl<'d, Dm: DriverMode> DpiTransfer<'d, DmaBounceBuffer, Dm> { + /// Check for a completed DMA EOF and refill the completed bounce buffer + /// from the PSRAM framebuffer. + /// + /// Returns `true` at each frame boundary (when all bounce-buffer chunks + /// for a full frame have been refilled). Call this in a tight loop to + /// keep the display refreshed. + #[instability::unstable] + pub fn poll(&mut self) -> bool { + if self + .dpi + .tx_channel + .pending_out_interrupts() + .contains(DmaTxInterrupt::Eof) + { + self.dpi.tx_channel.clear_out(DmaTxInterrupt::Eof); + self.buffer_view.refill() + } else { + false + } + } + + /// Swap front and back buffers at the next frame boundary. + /// + /// Spins until a frame boundary is reached (or the DMA stops / hits an + /// error), then atomically swaps which framebuffer the bounce-buffer + /// pipeline reads from. After this call the old front buffer becomes the + /// new back buffer (accessible via [`DmaBounceBufferView::back_buffer`]) + /// and the old back buffer becomes the new front (displayed on screen). + /// + /// Does nothing if no back buffer was registered via + /// [`DmaBounceBufferView::set_back_buffer`]. + #[instability::unstable] + pub fn swap_buffers(&mut self) { + loop { + if self.poll() || self.is_done() || self.dpi.tx_channel.has_error() { + break; + } + } + DmaBounceBufferView::swap_framebuffer(&mut self.buffer_view); + } +} + /// Configuration settings for the RGB/DPI interface. #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, procmacros::BuilderLite)] diff --git a/qa-test/src/bin/lcd_dpi_bounce.rs b/qa-test/src/bin/lcd_dpi_bounce.rs new file mode 100644 index 00000000000..dcf3814bec7 --- /dev/null +++ b/qa-test/src/bin/lcd_dpi_bounce.rs @@ -0,0 +1,213 @@ +//! Drives the 16-bit parallel RGB (DPI) display on PandaTouch v1.x using +//! a PSRAM framebuffer with GDMA bounce buffer pipeline. +//! +//! The following wiring is assumed (PandaTouch BSP pinout): +//! - LCD_PCLK => GPIO5 +//! - LCD_DE => GPIO38 +//! - LCD_RST => GPIO46 +//! - LCD_BL => GPIO21 +//! - LCD_DATA0 => GPIO17 (B3) +//! - LCD_DATA1 => GPIO18 (B4) +//! - LCD_DATA2 => GPIO48 (B5) +//! - LCD_DATA3 => GPIO47 (B6) +//! - LCD_DATA4 => GPIO39 (B7) +//! - LCD_DATA5 => GPIO11 (G2) +//! - LCD_DATA6 => GPIO12 (G3) +//! - LCD_DATA7 => GPIO13 (G4) +//! - LCD_DATA8 => GPIO14 (G5) +//! - LCD_DATA9 => GPIO15 (G6) +//! - LCD_DATA10 => GPIO16 (G7) +//! - LCD_DATA11 => GPIO6 (R3) +//! - LCD_DATA12 => GPIO7 (R4) +//! - LCD_DATA13 => GPIO8 (R5) +//! - LCD_DATA14 => GPIO9 (R6) +//! - LCD_DATA15 => GPIO10 (R7) + +//% CHIPS: esp32s3 +//% FEATURES: esp-hal/unstable esp-hal/psram +//% ENV: ESP_HAL_CONFIG_PSRAM_MODE=octal + +#![no_std] +#![no_main] + +extern crate alloc; + +use alloc::vec::Vec; + +use esp_alloc as _; +use esp_backtrace as _; +use esp_hal::{ + delay::Delay, + dma::{DmaBounceBuffer, DmaDescriptor}, + gpio::{Level, Output, OutputConfig}, + lcd_cam::{ + LcdCam, + lcd::{ + ClockMode, + Phase, + Polarity, + dpi::{Config, Dpi, Format, FrameTiming}, + }, + }, + main, + peripherals::Peripherals, + time::Rate, +}; +use esp_println::println; + +esp_bootloader_esp_idf::esp_app_desc!(); + +// SAFETY: Each static mut below is passed exactly once into DmaBounceBuffer, +// which becomes the sole owner via install() into the global ISR critical section. +static mut BOUNCE_DESCRIPTORS: [DmaDescriptor; 2] = [DmaDescriptor::EMPTY; 2]; + +const BOUNCE_BUF_SIZE: usize = 10 * 800 * 2; // 10 lines × 800 px × 2 bytes +static mut BOUNCE_BUF0: [u8; BOUNCE_BUF_SIZE] = [0u8; BOUNCE_BUF_SIZE]; +static mut BOUNCE_BUF1: [u8; BOUNCE_BUF_SIZE] = [0u8; BOUNCE_BUF_SIZE]; + +fn rgb565(r: u16, g: u16, b: u16) -> u16 { + (r << 11) | (g << 5) | b +} + +#[main] +fn main() -> ! { + esp_println::logger::init_logger_from_env(); + + let peripherals: Peripherals = esp_hal::init(esp_hal::Config::default()); + esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram); + + let delay = Delay::new(); + + let mut rst = Output::new(peripherals.GPIO46, Level::Low, OutputConfig::default()); + delay.delay_millis(100u32); + rst.set_high(); + delay.delay_millis(100u32); + + let _backlight = Output::new(peripherals.GPIO21, Level::High, OutputConfig::default()); + + let channel = peripherals.DMA_CH0; + let lcd_cam = LcdCam::new(peripherals.LCD_CAM); + + const FB_SIZE: usize = 800 * 480 * 2; + let mut fb0_vec: Vec = Vec::with_capacity(FB_SIZE); + fb0_vec.resize(FB_SIZE, 0u8); + let framebuffer0: &'static mut [u8] = fb0_vec.leak(); + + let mut fb1_vec: Vec = Vec::with_capacity(FB_SIZE); + fb1_vec.resize(FB_SIZE, 0u8); + let framebuffer1: &'static mut [u8] = fb1_vec.leak(); + + let blue = rgb565(0, 0, 31).to_le_bytes(); + for chunk in framebuffer0.chunks_exact_mut(2) { + chunk.copy_from_slice(&blue); + } + + let config = Config::default() + .with_frequency(Rate::from_mhz(23)) + .with_clock_mode(ClockMode { + polarity: Polarity::IdleLow, + // pclk_active_neg = true → data shifts on rising edge, + // clock idle is low, so output at falling edge = ShiftHigh. + phase: Phase::ShiftHigh, + }) + .with_format(Format { + enable_2byte_mode: true, + ..Default::default() + }) + .with_timing(FrameTiming { + // Total = active + hsync_pulse_width + hbp + hfp + // 800 + 4 + 8 + 8 = 820 + horizontal_total_width: 820, + horizontal_active_width: 800, + // blank_front_porch includes hsync_width per esp-hal docs: hsw + hbp = 4 + 8 = 12 + horizontal_blank_front_porch: 12, + // Total = active + vsync_pulse_width + vbp + vfp + // 480 + 4 + 16 + 16 = 516 + vertical_total_height: 516, + vertical_active_height: 480, + // vsync_width + vbp = 4 + 16 = 20 + vertical_blank_front_porch: 20, + hsync_width: 4, + vsync_width: 4, + hsync_position: 0, + }) + .with_vsync_idle_level(Level::High) + .with_hsync_idle_level(Level::High) + .with_de_idle_level(Level::Low) + .with_disable_black_region(false); + + let dpi = Dpi::new(lcd_cam.lcd, channel, config) + .unwrap() + .with_de(peripherals.GPIO38) + .with_pclk(peripherals.GPIO5) + // Blue B3-B7 + .with_data0(peripherals.GPIO17) + .with_data1(peripherals.GPIO18) + .with_data2(peripherals.GPIO48) + .with_data3(peripherals.GPIO47) + .with_data4(peripherals.GPIO39) + // Green G2-G7 + .with_data5(peripherals.GPIO11) + .with_data6(peripherals.GPIO12) + .with_data7(peripherals.GPIO13) + .with_data8(peripherals.GPIO14) + .with_data9(peripherals.GPIO15) + .with_data10(peripherals.GPIO16) + // Red R3-R7 + .with_data11(peripherals.GPIO6) + .with_data12(peripherals.GPIO7) + .with_data13(peripherals.GPIO8) + .with_data14(peripherals.GPIO9) + .with_data15(peripherals.GPIO10); + + // SAFETY: Each static mut is accessed only once here, before the bounce + // buffer state is installed into the global ISR slot. After install(), + // DmaBounceBuffer is the sole owner of these regions. + let bounce_state = unsafe { + DmaBounceBuffer::new( + &mut *core::ptr::addr_of_mut!(BOUNCE_DESCRIPTORS), + framebuffer0, + core::slice::from_raw_parts_mut( + core::ptr::addr_of_mut!(BOUNCE_BUF0).cast::(), + BOUNCE_BUF_SIZE, + ), + core::slice::from_raw_parts_mut( + core::ptr::addr_of_mut!(BOUNCE_BUF1).cast::(), + BOUNCE_BUF_SIZE, + ), + ) + .expect("bounce buffer setup failed") + }; + + println!("Initialising"); + let mut transfer = dpi.send(true, bounce_state).map_err(|e| e.0).unwrap(); + transfer.set_back_buffer(framebuffer1); + println!("Rendering"); + + let colors: &[u16] = &[ + rgb565(31, 0, 0), + rgb565(0, 63, 0), + rgb565(0, 0, 31), + rgb565(31, 63, 31), + rgb565(0, 0, 0), + ]; + + let mut color_idx = 0usize; + let mut next_change = esp_hal::time::Instant::now(); + loop { + transfer.poll(); + + if next_change <= esp_hal::time::Instant::now() { + next_change = esp_hal::time::Instant::now() + esp_hal::time::Duration::from_secs(2); + + let draw = transfer.back_buffer().unwrap(); + let bytes = colors[color_idx % colors.len()].to_le_bytes(); + for chunk in draw.chunks_exact_mut(2) { + chunk.copy_from_slice(&bytes); + } + + color_idx += 1; + transfer.swap_buffers(); + } + } +}