From 1f0ba047b96fcfc6d34f8e9a69618d07b6415155 Mon Sep 17 00:00:00 2001 From: Florian Maunier Date: Thu, 26 Mar 2026 00:17:20 +0100 Subject: [PATCH 1/7] feat(lcd_cam): add DPI bounce buffer support for PSRAM-backed RGB displays Fix lcd_always_out_en not being set in the DPI driver's apply_config(), which caused the LCD_CAM peripheral to stop outputting pixel data after lcd_dout_cyclelen cycles instead of streaming continuously from DMA. Add DmaBounceBuffer and Dpi::send_bounce_buffered() to enable driving large (800x480+) RGB displays with framebuffers in PSRAM, matching the bounce buffer architecture used by ESP-IDF's esp_lcd_panel_rgb.c. Architecture: - Two SRAM bounce buffers with a circular 2-descriptor DMA chain - DpiBounceTransfer::poll() checks for GDMA EOF events and copies the next framebuffer chunk into the completed bounce buffer - Double-buffered rendering via set_back_buffer() / back_buffer() / swap_buffers() for tear-free frame transitions Add lcd_dpi_bounce qa-test example demonstrating the full pipeline on the PandaTouch board (ESP32-S3, 800x480 RGB565, 23MHz, Octal PSRAM). --- esp-hal/CHANGELOG.md | 2 + esp-hal/src/lcd_cam/lcd/dpi.rs | 417 +++++++++++++++++++++++++++++- qa-test/src/bin/lcd_dpi_bounce.rs | 232 +++++++++++++++++ 3 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 qa-test/src/bin/lcd_dpi_bounce.rs diff --git a/esp-hal/CHANGELOG.md b/esp-hal/CHANGELOG.md index 0c9778d4c88..b22f05f6df0 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) +- `lcd_cam`: Added `DmaBounceBuffer` type to enable PSRAM-backed framebuffers for large DPI (RGB) displays via a GDMA EOF interrupt-driven bounce pipeline. (#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/lcd_cam/lcd/dpi.rs b/esp-hal/src/lcd_cam/lcd/dpi.rs index c9c619f4718..b9f1a9668e8 100644 --- a/esp-hal/src/lcd_cam/lcd/dpi.rs +++ b/esp-hal/src/lcd_cam/lcd/dpi.rs @@ -96,15 +96,28 @@ //! ``` use core::{ + cell::RefCell, marker::PhantomData, mem::ManuallyDrop, ops::{Deref, DerefMut}, }; +use critical_section::Mutex; + use crate::{ Blocking, DriverMode, - dma::{ChannelTx, DmaError, DmaPeripheral, DmaTxBuffer, PeripheralTxChannel, TxChannelFor}, + dma::{ + ChannelTx, + DmaDescriptor, + DmaError, + DmaPeripheral, + DmaTxBuffer, + DmaTxInterrupt, + Owner, + PeripheralTxChannel, + TxChannelFor, + }, gpio::{Level, OutputConfig, OutputSignal, interconnect::PeripheralOutput}, lcd_cam::{ BitOrder, @@ -120,12 +133,218 @@ use crate::{ time::Rate, }; +static BOUNCE_STATE: Mutex>> = Mutex::new(RefCell::new(None)); + /// Errors that can occur when configuring the DPI peripheral. #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ConfigError { /// Clock configuration error. Clock(ClockError), + /// Framebuffer size is not divisible by bounce buffer size, or bounce + /// buffers are not equal in size. + InvalidBounceBufferSize, +} + +/// Bounce buffer state for continuous RGB DPI output from a PSRAM framebuffer. +/// +/// Holds two SRAM bounce buffers and their circular DMA descriptor chain. +/// The GDMA EOF interrupt (or [`DpiBounceTransfer::poll`]) refills the completed +/// buffer from the PSRAM framebuffer to maintain continuous display output. +/// +/// Ported from ESP-IDF's RGB panel bounce buffer architecture: +/// +/// +/// # Note +/// +/// The framebuffer must be a multiple of the bounce buffer size. +#[instability::unstable] +#[allow(dead_code)] +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, +} + +#[allow(dead_code)] +impl DmaBounceBuffer { + /// Create a new bounce buffer state. + /// + /// # Arguments + /// - `descriptors`: `'static` storage for the two DMA descriptors + /// - `framebuffer`: the PSRAM framebuffer to stream (must be a multiple of `bounce_buf0.len()`) + /// - `bounce_buf0` / `bounce_buf1`: SRAM buffers for the ping-pong DMA pipeline (must be equal + /// size) + /// + /// # Errors + /// + /// Returns [`ConfigError::InvalidBounceBufferSize`] if the framebuffer length is not divisible + /// by the bounce buffer size. + #[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(ConfigError::InvalidBounceBufferSize); + } + + 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, + }; + + this.setup_descriptors(); + + Ok(this) + } + + fn setup_descriptors(&mut self) { + let bb_size = self.bounce_bufs[0].len(); + // SAFETY: `self.descriptors` is a raw pointer derived from a `&'static mut [DmaDescriptor; + // 2]` passed by the caller to `DmaBounceBuffer::new()`. The static lifetime + // guarantees the memory is valid for the entire program. Exclusive access is + // maintained because `DmaBounceBuffer::new()` takes ownership of the `&'static mut` + // reference and we hold `&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 as `setup_descriptors()` — `self.descriptors` always points to + // valid static memory that lives for `'static`. `as_mut_ptr` on a mutable slice is safe. + 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, completed_buf_idx: usize) -> bool { + self.fill_bounce_buf(completed_buf_idx); + + self.eof_count += 1; + if self.eof_count >= self.expect_eof_count { + self.eof_count = 0; + true + } else { + false + } + } + + pub(crate) fn arm_interrupt( + &self, + channel: &mut ChannelTx>>, + ) { + channel.listen_out(DmaTxInterrupt::Eof); + } + + pub(crate) fn install(self) { + critical_section::with(|cs| { + *BOUNCE_STATE.borrow_ref_mut(cs) = Some(self); + }); + } + + /// 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; + } + + 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 &'static mut [u8] of length + // self.fb_len, set in new() or swapped via set_framebuffer(). bounce_pos + // is always < fb_len and fb_len is a multiple of bb_size, so the range + // [bounce_pos..bounce_pos+bb_size] is always within 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; + } +} + +// SAFETY: `DmaBounceBuffer` contains a raw pointer (`*mut [DmaDescriptor; 2]`) which prevents +// auto-derived `Send`/`Sync`. The pointer is constructed from a `&'static mut [DmaDescriptor; 2]` +// and is valid for the `'static` lifetime. Access to the pointed-to data is controlled +// exclusively through `DmaBounceBuffer` (via `&mut self` methods and `install()` which moves +// ownership into a `critical_section::Mutex`). It is therefore safe to send across threads +// and access behind a shared reference when protected by a critical section. +unsafe impl Send for DmaBounceBuffer {} +// SAFETY: See the safety rationale above for `Send`. +unsafe impl Sync for DmaBounceBuffer {} + +/// Refill bounce buffers from the PSRAM framebuffer. +/// +/// Called by [`DpiBounceTransfer::poll`] to check for completed DMA +/// transfers and copy the next framebuffer chunk into the finished bounce +/// buffer. Must be called with the DMA channel reference so the EOF +/// interrupt can be checked and cleared properly. +pub(crate) fn bounce_buffer_refill( + channel: &mut ChannelTx>>, +) -> bool { + if channel + .pending_out_interrupts() + .contains(DmaTxInterrupt::Eof) + { + channel.clear_out(DmaTxInterrupt::Eof); + + critical_section::with(|cs| { + if let Some(state) = BOUNCE_STATE.borrow_ref_mut(cs).as_mut() { + let buf_idx = state.eof_count % 2; + return state.refill(buf_idx); + } + false + }) + } else { + false + } } /// Represents the RGB LCD interface. @@ -147,6 +366,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 +447,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() }); @@ -500,6 +721,63 @@ where self.with_data_pin(OutputSignal::LCD_DATA_15, pin) } + /// Start a continuous bounce-buffered RGB DPI transfer from a PSRAM framebuffer. + /// + /// Sets up a circular two-descriptor DMA chain that continuously streams data from + /// `bounce_state`'s framebuffer to the LCD_CAM peripheral, refilling each bounce + /// buffer on every GDMA EOF interrupt. + /// + /// After calling this method, call [`DpiBounceTransfer::poll`] periodically to + /// refill bounce buffers, or rely on the automatically bound GDMA TX interrupt + /// handler for interrupt-driven operation. + /// + /// # Errors + /// + /// Returns `(DmaError, Dpi, DmaBounceBuffer)` on DMA setup failure. + #[instability::unstable] + pub fn send_bounce_buffered( + mut self, + mut bounce_state: DmaBounceBuffer, + ) -> Result, (DmaError, Dpi<'d, Dm>, DmaBounceBuffer)> { + bounce_state.initial_fill(); + + // SAFETY: `DmaTxBuffer::prepare()` for `DmaBounceBuffer` returns a `Preparation` whose + // `start` pointer addresses the first element of a `'static` descriptor array. The + // descriptors remain valid and at stable addresses for the entire duration of the + // transfer. + let result = unsafe { + self.tx_channel + .prepare_transfer(DmaPeripheral::LcdCam, &mut bounce_state) + } + .and_then(|_| self.tx_channel.start_transfer()); + if let Err(err) = result { + return Err((err, self, bounce_state)); + } + + bounce_state.install(); + + self.regs() + .lcd_user() + .modify(|_, w| w.lcd_reset().set_bit()); + self.regs() + .lcd_misc() + .modify(|_, w| w.lcd_afifo_reset().set_bit()); + + self.regs() + .lcd_misc() + .modify(|_, w| w.lcd_next_frame_en().set_bit()); + + self.regs().lcd_user().modify(|_, w| { + w.lcd_update().set_bit(); + w.lcd_start().set_bit() + }); + + Ok(DpiBounceTransfer { + dpi: ManuallyDrop::new(self), + back_buffer: None, + }) + } + /// Sending out the [DmaTxBuffer] to the RGB/DPI interface. /// /// - `next_frame_en`: Automatically send the next frame data when the current frame is sent @@ -545,6 +823,143 @@ where } } +unsafe impl DmaTxBuffer for DmaBounceBuffer { + type View = (); + type Final = (); + + fn prepare(&mut self) -> crate::dma::Preparation { + crate::dma::Preparation { + start: self.first_descriptor_ptr(), + #[cfg(psram_dma)] + accesses_psram: false, + direction: crate::dma::TransferDirection::Out, + burst_transfer: crate::dma::BurstConfig::default(), + check_owner: Some(false), + auto_write_back: true, + } + } + + fn into_view(self) -> Self::View {} + + fn from_view(_view: Self::View) -> Self::Final {} +} + +#[instability::unstable] +/// Represents an ongoing continuous (bounce-buffered) DPI RGB transfer. +/// +/// Stop with [`DpiBounceTransfer::stop`], or call [`DpiBounceTransfer::poll`] +/// periodically to refill bounce buffers. +pub struct DpiBounceTransfer<'d, Dm: DriverMode> { + dpi: ManuallyDrop>, + back_buffer: Option<(*mut u8, usize)>, +} + +impl<'d, Dm: DriverMode> DpiBounceTransfer<'d, Dm> { + /// Returns true if the transfer is no longer active. + #[instability::unstable] + pub fn is_done(&self) -> bool { + self.dpi.regs().lcd_user().read().lcd_start().bit_is_clear() + } + + /// Stops the transfer and returns ownership of the DPI peripheral. + #[instability::unstable] + pub fn stop(mut self) -> Dpi<'d, Dm> { + self.stop_peripheral(); + // SAFETY: This is the only path that extracts `self.dpi` from `ManuallyDrop`. + // `core::mem::forget(self)` below prevents `Drop` from running and taking it again. + let dpi = unsafe { ManuallyDrop::take(&mut self.dpi) }; + core::mem::forget(self); + dpi + } + + /// Poll for completed DMA transfers and refill bounce buffers. + /// + /// Call this in a tight loop to keep the display refreshed. Each call + /// checks for a GDMA EOF event and, if one occurred, copies the next + /// framebuffer chunk into the completed bounce buffer. + #[instability::unstable] + pub fn poll(&mut self) -> bool { + bounce_buffer_refill(&mut self.dpi.tx_channel) + } + + /// Register a back buffer for double-buffered rendering. + /// + /// The back buffer must be the same size as the front buffer passed to + /// [`DmaBounceBuffer::new`]. After calling this, use [`Self::back_buffer`] + /// to draw into it and [`Self::swap_buffers`] to swap at the next frame + /// boundary. + #[instability::unstable] + pub fn set_back_buffer(&mut self, buf: &'static mut [u8]) { + 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 a &'static mut [u8] passed to set_back_buffer(). + // We have exclusive access via &mut self. + unsafe { core::slice::from_raw_parts_mut(ptr, len) } + }) + } + + /// Swap front and back buffers at the next frame boundary. + /// + /// Polls until the current frame finishes, then atomically swaps which + /// framebuffer the bounce buffer pipeline reads from. After this call, + /// the old front buffer becomes the new back buffer (available via + /// [`Self::back_buffer`]) and the old back buffer becomes the new front + /// (displayed on screen). + /// + /// Does nothing if no back buffer was registered. + #[instability::unstable] + pub fn swap_buffers(&mut self) { + let Some((back_ptr, back_len)) = self.back_buffer else { + return; + }; + + loop { + if self.poll() { + break; + } + } + + critical_section::with(|cs| { + if let Some(state) = BOUNCE_STATE.borrow_ref_mut(cs).as_mut() { + let old_fb = state.framebuffer; + state.set_framebuffer(back_ptr as *const u8); + self.back_buffer = Some((old_fb as *mut u8, back_len)); + } + }); + } + + fn stop_peripheral(&mut self) { + self.dpi.tx_channel.unlisten_out(DmaTxInterrupt::Eof); + self.dpi.tx_channel.clear_out(DmaTxInterrupt::Eof); + + critical_section::with(|cs| { + *BOUNCE_STATE.borrow_ref_mut(cs) = None; + }); + + self.dpi + .regs() + .lcd_user() + .modify(|_, w| w.lcd_start().clear_bit()); + self.dpi.tx_channel.stop_transfer(); + } +} + +impl Drop for DpiBounceTransfer<'_, Dm> { + fn drop(&mut self) { + self.stop_peripheral(); + // SAFETY: `drop()` is only called once, so `self.dpi` has not been taken. + // This mirrors the pattern used in `DpiTransfer::drop()`. + unsafe { ManuallyDrop::drop(&mut self.dpi) }; + } +} + /// Represents an ongoing (or potentially finished) transfer using the RGB LCD /// interface pub struct DpiTransfer<'d, BUF: DmaTxBuffer, Dm: DriverMode> { 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..58047cdc1db --- /dev/null +++ b/qa-test/src/bin/lcd_dpi_bounce.rs @@ -0,0 +1,232 @@ +//! 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::DmaDescriptor, + gpio::{Level, Output, OutputConfig}, + lcd_cam::{ + LcdCam, + lcd::{ + ClockMode, + Phase, + Polarity, + dpi::{Config, DmaBounceBuffer, Dpi, Format, FrameTiming}, + }, + }, + main, + peripherals::Peripherals, + psram, + 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 init_psram_heap(start: *mut u8, size: usize) { + unsafe { + esp_alloc::HEAP.add_region(esp_alloc::HeapRegion::new( + start, + size, + esp_alloc::MemoryCapability::External.into(), + )); + } +} + +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()); + + let (start, size) = psram::psram_raw_parts(&peripherals.PSRAM); + if size == 0 { + panic!("No PSRAM detected"); + } + init_psram_heap(start, size); + + 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_bounce_buffered(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(); + } + } +} From 3e08efaa3e377d7db6a4a50e222e9b7f75a249db Mon Sep 17 00:00:00 2001 From: Florian Maunier Date: Thu, 26 Mar 2026 00:57:17 +0100 Subject: [PATCH 2/7] fix(lcd_cam): address PR review feedback and CI failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes: - Update send_bounce_buffered() docs to reflect polling-only API (ISR binding was removed earlier; docs still mentioned it) - Validate back buffer size in set_back_buffer() to prevent OOB reads when framebuffer pointer is swapped - Break swap_buffers() spin loop on is_done() to avoid deadlock if DMA enters error state CI fixes: - Gate all bounce buffer types behind #[cfg(feature = "unstable")] so critical_section (an optional dep) isn't required in non-unstable builds — fixes esp-hal (xtensa), msrv (xtensa), docs (xtensa) - Fix clippy: map_or(false, ...) -> is_some_and(...) --- esp-hal/src/lcd_cam/lcd/dpi.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/esp-hal/src/lcd_cam/lcd/dpi.rs b/esp-hal/src/lcd_cam/lcd/dpi.rs index b9f1a9668e8..df7e9f2fee8 100644 --- a/esp-hal/src/lcd_cam/lcd/dpi.rs +++ b/esp-hal/src/lcd_cam/lcd/dpi.rs @@ -96,12 +96,12 @@ //! ``` use core::{ - cell::RefCell, marker::PhantomData, mem::ManuallyDrop, ops::{Deref, DerefMut}, }; +#[cfg(feature = "unstable")] use critical_section::Mutex; use crate::{ @@ -133,7 +133,9 @@ use crate::{ time::Rate, }; -static BOUNCE_STATE: Mutex>> = Mutex::new(RefCell::new(None)); +#[cfg(feature = "unstable")] +static BOUNCE_STATE: Mutex>> = + Mutex::new(core::cell::RefCell::new(None)); /// Errors that can occur when configuring the DPI peripheral. #[derive(Debug, Clone, Copy, PartialEq)] @@ -146,6 +148,7 @@ pub enum ConfigError { InvalidBounceBufferSize, } +#[cfg(feature = "unstable")] /// Bounce buffer state for continuous RGB DPI output from a PSRAM framebuffer. /// /// Holds two SRAM bounce buffers and their circular DMA descriptor chain. @@ -170,6 +173,7 @@ pub struct DmaBounceBuffer { pub(crate) eof_count: usize, } +#[cfg(feature = "unstable")] #[allow(dead_code)] impl DmaBounceBuffer { /// Create a new bounce buffer state. @@ -310,6 +314,7 @@ impl DmaBounceBuffer { } } +#[cfg(feature = "unstable")] // SAFETY: `DmaBounceBuffer` contains a raw pointer (`*mut [DmaDescriptor; 2]`) which prevents // auto-derived `Send`/`Sync`. The pointer is constructed from a `&'static mut [DmaDescriptor; 2]` // and is valid for the `'static` lifetime. Access to the pointed-to data is controlled @@ -318,8 +323,10 @@ impl DmaBounceBuffer { // and access behind a shared reference when protected by a critical section. unsafe impl Send for DmaBounceBuffer {} // SAFETY: See the safety rationale above for `Send`. +#[cfg(feature = "unstable")] unsafe impl Sync for DmaBounceBuffer {} +#[cfg(feature = "unstable")] /// Refill bounce buffers from the PSRAM framebuffer. /// /// Called by [`DpiBounceTransfer::poll`] to check for completed DMA @@ -724,16 +731,16 @@ where /// Start a continuous bounce-buffered RGB DPI transfer from a PSRAM framebuffer. /// /// Sets up a circular two-descriptor DMA chain that continuously streams data from - /// `bounce_state`'s framebuffer to the LCD_CAM peripheral, refilling each bounce - /// buffer on every GDMA EOF interrupt. + /// `bounce_state`'s framebuffer to the LCD_CAM peripheral. Bounce buffers are + /// refilled in software as part of the transfer's polling API. /// /// After calling this method, call [`DpiBounceTransfer::poll`] periodically to - /// refill bounce buffers, or rely on the automatically bound GDMA TX interrupt - /// handler for interrupt-driven operation. + /// refill bounce buffers for continuous operation. /// /// # Errors /// /// Returns `(DmaError, Dpi, DmaBounceBuffer)` on DMA setup failure. + #[cfg(feature = "unstable")] #[instability::unstable] pub fn send_bounce_buffered( mut self, @@ -823,6 +830,7 @@ where } } +#[cfg(feature = "unstable")] unsafe impl DmaTxBuffer for DmaBounceBuffer { type View = (); type Final = (); @@ -844,6 +852,7 @@ unsafe impl DmaTxBuffer for DmaBounceBuffer { fn from_view(_view: Self::View) -> Self::Final {} } +#[cfg(feature = "unstable")] #[instability::unstable] /// Represents an ongoing continuous (bounce-buffered) DPI RGB transfer. /// @@ -854,6 +863,7 @@ pub struct DpiBounceTransfer<'d, Dm: DriverMode> { back_buffer: Option<(*mut u8, usize)>, } +#[cfg(feature = "unstable")] impl<'d, Dm: DriverMode> DpiBounceTransfer<'d, Dm> { /// Returns true if the transfer is no longer active. #[instability::unstable] @@ -890,6 +900,13 @@ impl<'d, Dm: DriverMode> DpiBounceTransfer<'d, Dm> { /// boundary. #[instability::unstable] pub fn set_back_buffer(&mut self, buf: &'static mut [u8]) { + let valid = critical_section::with(|cs| { + BOUNCE_STATE + .borrow_ref(cs) + .as_ref() + .is_some_and(|state| buf.len() == state.fb_len) + }); + assert!(valid, "back buffer length must match front buffer length"); self.back_buffer = Some((buf.as_mut_ptr(), buf.len())); } @@ -921,7 +938,7 @@ impl<'d, Dm: DriverMode> DpiBounceTransfer<'d, Dm> { }; loop { - if self.poll() { + if self.poll() || self.is_done() { break; } } @@ -951,6 +968,7 @@ impl<'d, Dm: DriverMode> DpiBounceTransfer<'d, Dm> { } } +#[cfg(feature = "unstable")] impl Drop for DpiBounceTransfer<'_, Dm> { fn drop(&mut self) { self.stop_peripheral(); From 1c617f5407fe5cc615727ba7aaca115f8a2c857a Mon Sep 17 00:00:00 2001 From: Florian Maunier Date: Thu, 26 Mar 2026 11:41:44 +0100 Subject: [PATCH 3/7] refactor(dma): add DmaBounceBuffer to dma/buffers.rs with proper DmaTxBuffer impl --- esp-hal/src/dma/buffers.rs | 269 +++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) diff --git a/esp-hal/src/dma/buffers.rs b/esp-hal/src/dma/buffers.rs index edfbfaca3b6..ca168abc9d1 100644 --- a/esp-hal/src/dma/buffers.rs +++ b/esp-hal/src/dma/buffers.rs @@ -479,6 +479,275 @@ pub unsafe trait DmaRxBuffer { fn from_view(view: Self::View) -> Self::Final; } +#[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] +#[allow(dead_code)] +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")] +#[allow(dead_code)] +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] +#[allow(dead_code)] +pub struct DmaBounceBufferView { + inner: DmaBounceBuffer, + back_buffer: Option<(*mut u8, usize)>, +} + +#[cfg(feature = "unstable")] +#[allow(dead_code)] +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. + /// Returns old front buffer pointer for the caller to store as new back buffer. + pub(crate) fn swap_framebuffer(&mut self) -> Option<(*mut u8, usize)> { + if let Some((back_ptr, back_len)) = self.back_buffer.take() { + let old_front = self.inner.framebuffer; + self.inner.set_framebuffer(back_ptr as *const u8); + Some((old_front as *mut u8, back_len)) + } else { + None + } + } +} + +#[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 + } +} + /// An in-progress view into [DmaRxBuf]/[DmaTxBuf]. /// /// In the future, this could support peeking into state of the From 571ee3fb5cba36731d89945717beda2092935465 Mon Sep 17 00:00:00 2001 From: Florian Maunier Date: Thu, 26 Mar 2026 11:52:40 +0100 Subject: [PATCH 4/7] refactor(lcd_cam): use generic send() for bounce buffers, remove send_bounce_buffered --- esp-hal/src/dma/buffers.rs | 10 +- esp-hal/src/lcd_cam/lcd/dpi.rs | 484 ++++----------------------------- 2 files changed, 52 insertions(+), 442 deletions(-) diff --git a/esp-hal/src/dma/buffers.rs b/esp-hal/src/dma/buffers.rs index ca168abc9d1..1e08d610ec8 100644 --- a/esp-hal/src/dma/buffers.rs +++ b/esp-hal/src/dma/buffers.rs @@ -686,14 +686,12 @@ impl DmaBounceBufferView { } /// Swap the front and back framebuffer pointers at a frame boundary. - /// Returns old front buffer pointer for the caller to store as new back buffer. - pub(crate) fn swap_framebuffer(&mut self) -> Option<(*mut u8, usize)> { - if let Some((back_ptr, back_len)) = self.back_buffer.take() { + 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); - Some((old_front as *mut u8, back_len)) - } else { - None + self.back_buffer = Some((old_front as *mut u8, fb_len)); } } } diff --git a/esp-hal/src/lcd_cam/lcd/dpi.rs b/esp-hal/src/lcd_cam/lcd/dpi.rs index df7e9f2fee8..4040f26939a 100644 --- a/esp-hal/src/lcd_cam/lcd/dpi.rs +++ b/esp-hal/src/lcd_cam/lcd/dpi.rs @@ -101,29 +101,15 @@ use core::{ ops::{Deref, DerefMut}, }; -#[cfg(feature = "unstable")] -use critical_section::Mutex; - use crate::{ - Blocking, - DriverMode, + Blocking, DriverMode, dma::{ - ChannelTx, - DmaDescriptor, - DmaError, - DmaPeripheral, - DmaTxBuffer, - DmaTxInterrupt, - Owner, - PeripheralTxChannel, - TxChannelFor, + ChannelTx, DmaBounceBuffer, DmaBounceBufferView, DmaError, DmaPeripheral, DmaTxBuffer, + DmaTxInterrupt, PeripheralTxChannel, TxChannelFor, }, gpio::{Level, OutputConfig, OutputSignal, interconnect::PeripheralOutput}, lcd_cam::{ - BitOrder, - ByteOrder, - ClockError, - calculate_clkm, + BitOrder, ByteOrder, ClockError, calculate_clkm, lcd::{ClockMode, DelayMode, Lcd, Phase, Polarity}, }, pac, @@ -133,225 +119,12 @@ use crate::{ time::Rate, }; -#[cfg(feature = "unstable")] -static BOUNCE_STATE: Mutex>> = - Mutex::new(core::cell::RefCell::new(None)); - /// Errors that can occur when configuring the DPI peripheral. #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ConfigError { /// Clock configuration error. Clock(ClockError), - /// Framebuffer size is not divisible by bounce buffer size, or bounce - /// buffers are not equal in size. - InvalidBounceBufferSize, -} - -#[cfg(feature = "unstable")] -/// Bounce buffer state for continuous RGB DPI output from a PSRAM framebuffer. -/// -/// Holds two SRAM bounce buffers and their circular DMA descriptor chain. -/// The GDMA EOF interrupt (or [`DpiBounceTransfer::poll`]) refills the completed -/// buffer from the PSRAM framebuffer to maintain continuous display output. -/// -/// Ported from ESP-IDF's RGB panel bounce buffer architecture: -/// -/// -/// # Note -/// -/// The framebuffer must be a multiple of the bounce buffer size. -#[instability::unstable] -#[allow(dead_code)] -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, -} - -#[cfg(feature = "unstable")] -#[allow(dead_code)] -impl DmaBounceBuffer { - /// Create a new bounce buffer state. - /// - /// # Arguments - /// - `descriptors`: `'static` storage for the two DMA descriptors - /// - `framebuffer`: the PSRAM framebuffer to stream (must be a multiple of `bounce_buf0.len()`) - /// - `bounce_buf0` / `bounce_buf1`: SRAM buffers for the ping-pong DMA pipeline (must be equal - /// size) - /// - /// # Errors - /// - /// Returns [`ConfigError::InvalidBounceBufferSize`] if the framebuffer length is not divisible - /// by the bounce buffer size. - #[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(ConfigError::InvalidBounceBufferSize); - } - - 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, - }; - - this.setup_descriptors(); - - Ok(this) - } - - fn setup_descriptors(&mut self) { - let bb_size = self.bounce_bufs[0].len(); - // SAFETY: `self.descriptors` is a raw pointer derived from a `&'static mut [DmaDescriptor; - // 2]` passed by the caller to `DmaBounceBuffer::new()`. The static lifetime - // guarantees the memory is valid for the entire program. Exclusive access is - // maintained because `DmaBounceBuffer::new()` takes ownership of the `&'static mut` - // reference and we hold `&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 as `setup_descriptors()` — `self.descriptors` always points to - // valid static memory that lives for `'static`. `as_mut_ptr` on a mutable slice is safe. - 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, completed_buf_idx: usize) -> bool { - self.fill_bounce_buf(completed_buf_idx); - - self.eof_count += 1; - if self.eof_count >= self.expect_eof_count { - self.eof_count = 0; - true - } else { - false - } - } - - pub(crate) fn arm_interrupt( - &self, - channel: &mut ChannelTx>>, - ) { - channel.listen_out(DmaTxInterrupt::Eof); - } - - pub(crate) fn install(self) { - critical_section::with(|cs| { - *BOUNCE_STATE.borrow_ref_mut(cs) = Some(self); - }); - } - - /// 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; - } - - 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 &'static mut [u8] of length - // self.fb_len, set in new() or swapped via set_framebuffer(). bounce_pos - // is always < fb_len and fb_len is a multiple of bb_size, so the range - // [bounce_pos..bounce_pos+bb_size] is always within 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")] -// SAFETY: `DmaBounceBuffer` contains a raw pointer (`*mut [DmaDescriptor; 2]`) which prevents -// auto-derived `Send`/`Sync`. The pointer is constructed from a `&'static mut [DmaDescriptor; 2]` -// and is valid for the `'static` lifetime. Access to the pointed-to data is controlled -// exclusively through `DmaBounceBuffer` (via `&mut self` methods and `install()` which moves -// ownership into a `critical_section::Mutex`). It is therefore safe to send across threads -// and access behind a shared reference when protected by a critical section. -unsafe impl Send for DmaBounceBuffer {} -// SAFETY: See the safety rationale above for `Send`. -#[cfg(feature = "unstable")] -unsafe impl Sync for DmaBounceBuffer {} - -#[cfg(feature = "unstable")] -/// Refill bounce buffers from the PSRAM framebuffer. -/// -/// Called by [`DpiBounceTransfer::poll`] to check for completed DMA -/// transfers and copy the next framebuffer chunk into the finished bounce -/// buffer. Must be called with the DMA channel reference so the EOF -/// interrupt can be checked and cleared properly. -pub(crate) fn bounce_buffer_refill( - channel: &mut ChannelTx>>, -) -> bool { - if channel - .pending_out_interrupts() - .contains(DmaTxInterrupt::Eof) - { - channel.clear_out(DmaTxInterrupt::Eof); - - critical_section::with(|cs| { - if let Some(state) = BOUNCE_STATE.borrow_ref_mut(cs).as_mut() { - let buf_idx = state.eof_count % 2; - return state.refill(buf_idx); - } - false - }) - } else { - false - } } /// Represents the RGB LCD interface. @@ -728,63 +501,6 @@ where self.with_data_pin(OutputSignal::LCD_DATA_15, pin) } - /// Start a continuous bounce-buffered RGB DPI transfer from a PSRAM framebuffer. - /// - /// Sets up a circular two-descriptor DMA chain that continuously streams data from - /// `bounce_state`'s framebuffer to the LCD_CAM peripheral. Bounce buffers are - /// refilled in software as part of the transfer's polling API. - /// - /// After calling this method, call [`DpiBounceTransfer::poll`] periodically to - /// refill bounce buffers for continuous operation. - /// - /// # Errors - /// - /// Returns `(DmaError, Dpi, DmaBounceBuffer)` on DMA setup failure. - #[cfg(feature = "unstable")] - #[instability::unstable] - pub fn send_bounce_buffered( - mut self, - mut bounce_state: DmaBounceBuffer, - ) -> Result, (DmaError, Dpi<'d, Dm>, DmaBounceBuffer)> { - bounce_state.initial_fill(); - - // SAFETY: `DmaTxBuffer::prepare()` for `DmaBounceBuffer` returns a `Preparation` whose - // `start` pointer addresses the first element of a `'static` descriptor array. The - // descriptors remain valid and at stable addresses for the entire duration of the - // transfer. - let result = unsafe { - self.tx_channel - .prepare_transfer(DmaPeripheral::LcdCam, &mut bounce_state) - } - .and_then(|_| self.tx_channel.start_transfer()); - if let Err(err) = result { - return Err((err, self, bounce_state)); - } - - bounce_state.install(); - - self.regs() - .lcd_user() - .modify(|_, w| w.lcd_reset().set_bit()); - self.regs() - .lcd_misc() - .modify(|_, w| w.lcd_afifo_reset().set_bit()); - - self.regs() - .lcd_misc() - .modify(|_, w| w.lcd_next_frame_en().set_bit()); - - self.regs().lcd_user().modify(|_, w| { - w.lcd_update().set_bit(); - w.lcd_start().set_bit() - }); - - Ok(DpiBounceTransfer { - dpi: ManuallyDrop::new(self), - back_buffer: None, - }) - } - /// Sending out the [DmaTxBuffer] to the RGB/DPI interface. /// /// - `next_frame_en`: Automatically send the next frame data when the current frame is sent @@ -830,154 +546,6 @@ where } } -#[cfg(feature = "unstable")] -unsafe impl DmaTxBuffer for DmaBounceBuffer { - type View = (); - type Final = (); - - fn prepare(&mut self) -> crate::dma::Preparation { - crate::dma::Preparation { - start: self.first_descriptor_ptr(), - #[cfg(psram_dma)] - accesses_psram: false, - direction: crate::dma::TransferDirection::Out, - burst_transfer: crate::dma::BurstConfig::default(), - check_owner: Some(false), - auto_write_back: true, - } - } - - fn into_view(self) -> Self::View {} - - fn from_view(_view: Self::View) -> Self::Final {} -} - -#[cfg(feature = "unstable")] -#[instability::unstable] -/// Represents an ongoing continuous (bounce-buffered) DPI RGB transfer. -/// -/// Stop with [`DpiBounceTransfer::stop`], or call [`DpiBounceTransfer::poll`] -/// periodically to refill bounce buffers. -pub struct DpiBounceTransfer<'d, Dm: DriverMode> { - dpi: ManuallyDrop>, - back_buffer: Option<(*mut u8, usize)>, -} - -#[cfg(feature = "unstable")] -impl<'d, Dm: DriverMode> DpiBounceTransfer<'d, Dm> { - /// Returns true if the transfer is no longer active. - #[instability::unstable] - pub fn is_done(&self) -> bool { - self.dpi.regs().lcd_user().read().lcd_start().bit_is_clear() - } - - /// Stops the transfer and returns ownership of the DPI peripheral. - #[instability::unstable] - pub fn stop(mut self) -> Dpi<'d, Dm> { - self.stop_peripheral(); - // SAFETY: This is the only path that extracts `self.dpi` from `ManuallyDrop`. - // `core::mem::forget(self)` below prevents `Drop` from running and taking it again. - let dpi = unsafe { ManuallyDrop::take(&mut self.dpi) }; - core::mem::forget(self); - dpi - } - - /// Poll for completed DMA transfers and refill bounce buffers. - /// - /// Call this in a tight loop to keep the display refreshed. Each call - /// checks for a GDMA EOF event and, if one occurred, copies the next - /// framebuffer chunk into the completed bounce buffer. - #[instability::unstable] - pub fn poll(&mut self) -> bool { - bounce_buffer_refill(&mut self.dpi.tx_channel) - } - - /// Register a back buffer for double-buffered rendering. - /// - /// The back buffer must be the same size as the front buffer passed to - /// [`DmaBounceBuffer::new`]. After calling this, use [`Self::back_buffer`] - /// to draw into it and [`Self::swap_buffers`] to swap at the next frame - /// boundary. - #[instability::unstable] - pub fn set_back_buffer(&mut self, buf: &'static mut [u8]) { - let valid = critical_section::with(|cs| { - BOUNCE_STATE - .borrow_ref(cs) - .as_ref() - .is_some_and(|state| buf.len() == state.fb_len) - }); - assert!(valid, "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 a &'static mut [u8] passed to set_back_buffer(). - // We have exclusive access via &mut self. - unsafe { core::slice::from_raw_parts_mut(ptr, len) } - }) - } - - /// Swap front and back buffers at the next frame boundary. - /// - /// Polls until the current frame finishes, then atomically swaps which - /// framebuffer the bounce buffer pipeline reads from. After this call, - /// the old front buffer becomes the new back buffer (available via - /// [`Self::back_buffer`]) and the old back buffer becomes the new front - /// (displayed on screen). - /// - /// Does nothing if no back buffer was registered. - #[instability::unstable] - pub fn swap_buffers(&mut self) { - let Some((back_ptr, back_len)) = self.back_buffer else { - return; - }; - - loop { - if self.poll() || self.is_done() { - break; - } - } - - critical_section::with(|cs| { - if let Some(state) = BOUNCE_STATE.borrow_ref_mut(cs).as_mut() { - let old_fb = state.framebuffer; - state.set_framebuffer(back_ptr as *const u8); - self.back_buffer = Some((old_fb as *mut u8, back_len)); - } - }); - } - - fn stop_peripheral(&mut self) { - self.dpi.tx_channel.unlisten_out(DmaTxInterrupt::Eof); - self.dpi.tx_channel.clear_out(DmaTxInterrupt::Eof); - - critical_section::with(|cs| { - *BOUNCE_STATE.borrow_ref_mut(cs) = None; - }); - - self.dpi - .regs() - .lcd_user() - .modify(|_, w| w.lcd_start().clear_bit()); - self.dpi.tx_channel.stop_transfer(); - } -} - -#[cfg(feature = "unstable")] -impl Drop for DpiBounceTransfer<'_, Dm> { - fn drop(&mut self) { - self.stop_peripheral(); - // SAFETY: `drop()` is only called once, so `self.dpi` has not been taken. - // This mirrors the pattern used in `DpiTransfer::drop()`. - unsafe { ManuallyDrop::drop(&mut self.dpi) }; - } -} - /// Represents an ongoing (or potentially finished) transfer using the RGB LCD /// interface pub struct DpiTransfer<'d, BUF: DmaTxBuffer, Dm: DriverMode> { @@ -1077,6 +645,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)] From 8ead7cdda6176a2392a13d44fafb27c381ef8ddc Mon Sep 17 00:00:00 2001 From: Florian Maunier Date: Thu, 26 Mar 2026 11:56:27 +0100 Subject: [PATCH 5/7] refactor(example): update lcd_dpi_bounce to use new DmaBounceBuffer API --- esp-hal/CHANGELOG.md | 2 +- esp-hal/src/lcd_cam/lcd/dpi.rs | 19 +++++++++++++++---- qa-test/src/bin/lcd_dpi_bounce.rs | 9 +++------ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/esp-hal/CHANGELOG.md b/esp-hal/CHANGELOG.md index b22f05f6df0..c076f53ca3e 100644 --- a/esp-hal/CHANGELOG.md +++ b/esp-hal/CHANGELOG.md @@ -52,7 +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) -- `lcd_cam`: Added `DmaBounceBuffer` type to enable PSRAM-backed framebuffers for large DPI (RGB) displays via a GDMA EOF interrupt-driven bounce pipeline. (#5262) +- `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 diff --git a/esp-hal/src/lcd_cam/lcd/dpi.rs b/esp-hal/src/lcd_cam/lcd/dpi.rs index 4040f26939a..6be7c58a663 100644 --- a/esp-hal/src/lcd_cam/lcd/dpi.rs +++ b/esp-hal/src/lcd_cam/lcd/dpi.rs @@ -102,14 +102,25 @@ use core::{ }; use crate::{ - Blocking, DriverMode, + Blocking, + DriverMode, dma::{ - ChannelTx, DmaBounceBuffer, DmaBounceBufferView, DmaError, DmaPeripheral, DmaTxBuffer, - DmaTxInterrupt, PeripheralTxChannel, TxChannelFor, + ChannelTx, + DmaBounceBuffer, + DmaBounceBufferView, + DmaError, + DmaPeripheral, + DmaTxBuffer, + DmaTxInterrupt, + PeripheralTxChannel, + TxChannelFor, }, gpio::{Level, OutputConfig, OutputSignal, interconnect::PeripheralOutput}, lcd_cam::{ - BitOrder, ByteOrder, ClockError, calculate_clkm, + BitOrder, + ByteOrder, + ClockError, + calculate_clkm, lcd::{ClockMode, DelayMode, Lcd, Phase, Polarity}, }, pac, diff --git a/qa-test/src/bin/lcd_dpi_bounce.rs b/qa-test/src/bin/lcd_dpi_bounce.rs index 58047cdc1db..012a848f1c2 100644 --- a/qa-test/src/bin/lcd_dpi_bounce.rs +++ b/qa-test/src/bin/lcd_dpi_bounce.rs @@ -38,7 +38,7 @@ use esp_alloc as _; use esp_backtrace as _; use esp_hal::{ delay::Delay, - dma::DmaDescriptor, + dma::{DmaBounceBuffer, DmaDescriptor}, gpio::{Level, Output, OutputConfig}, lcd_cam::{ LcdCam, @@ -46,7 +46,7 @@ use esp_hal::{ ClockMode, Phase, Polarity, - dpi::{Config, DmaBounceBuffer, Dpi, Format, FrameTiming}, + dpi::{Config, Dpi, Format, FrameTiming}, }, }, main, @@ -196,10 +196,7 @@ fn main() -> ! { }; println!("Initialising"); - let mut transfer = dpi - .send_bounce_buffered(bounce_state) - .map_err(|e| e.0) - .unwrap(); + let mut transfer = dpi.send(true, bounce_state).map_err(|e| e.0).unwrap(); transfer.set_back_buffer(framebuffer1); println!("Rendering"); From b6d60ac16d4b1e644c6f5aa67a388457ab98be4f Mon Sep 17 00:00:00 2001 From: Florian Maunier Date: Thu, 26 Mar 2026 12:04:52 +0100 Subject: [PATCH 6/7] fix(dma): remove unnecessary #[allow(dead_code)] from DmaBounceBuffer types --- esp-hal/src/dma/buffers.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/esp-hal/src/dma/buffers.rs b/esp-hal/src/dma/buffers.rs index 1e08d610ec8..3e644e64dbc 100644 --- a/esp-hal/src/dma/buffers.rs +++ b/esp-hal/src/dma/buffers.rs @@ -496,7 +496,6 @@ pub enum DmaBounceBufferError { /// /// Holds two SRAM bounce buffers and their circular DMA descriptor chain. #[instability::unstable] -#[allow(dead_code)] pub struct DmaBounceBuffer { descriptors: *mut [DmaDescriptor; 2], bounce_bufs: [&'static mut [u8]; 2], @@ -509,7 +508,6 @@ pub struct DmaBounceBuffer { } #[cfg(feature = "unstable")] -#[allow(dead_code)] impl DmaBounceBuffer { /// Create a new bounce buffer state. #[instability::unstable] @@ -639,14 +637,12 @@ impl DmaBounceBuffer { /// Accessible via `Deref` on transfer types that use [`DmaBounceBuffer`]. /// Provides methods for double-buffered rendering. #[instability::unstable] -#[allow(dead_code)] pub struct DmaBounceBufferView { inner: DmaBounceBuffer, back_buffer: Option<(*mut u8, usize)>, } #[cfg(feature = "unstable")] -#[allow(dead_code)] impl DmaBounceBufferView { /// Register a back buffer for double-buffered rendering. /// From 28f28ea1c04e8a164cbf0459ee4ae1adc8babb4c Mon Sep 17 00:00:00 2001 From: Florian Maunier Date: Thu, 26 Mar 2026 13:41:11 +0100 Subject: [PATCH 7/7] refactor(dma): move DmaBounceBuffer to buffers/bounce_buffer.rs submodule, use psram_allocator macro --- esp-hal/src/dma/buffers/bounce_buffer.rs | 264 +++++++++++++++++ .../src/dma/{buffers.rs => buffers/mod.rs} | 268 +----------------- qa-test/src/bin/lcd_dpi_bounce.rs | 18 +- 3 files changed, 270 insertions(+), 280 deletions(-) create mode 100644 esp-hal/src/dma/buffers/bounce_buffer.rs rename esp-hal/src/dma/{buffers.rs => buffers/mod.rs} (87%) 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 87% rename from esp-hal/src/dma/buffers.rs rename to esp-hal/src/dma/buffers/mod.rs index 3e644e64dbc..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))] @@ -479,269 +484,6 @@ pub unsafe trait DmaRxBuffer { fn from_view(view: Self::View) -> Self::Final; } -#[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 - } -} - /// An in-progress view into [DmaRxBuf]/[DmaTxBuf]. /// /// In the future, this could support peeking into state of the diff --git a/qa-test/src/bin/lcd_dpi_bounce.rs b/qa-test/src/bin/lcd_dpi_bounce.rs index 012a848f1c2..dcf3814bec7 100644 --- a/qa-test/src/bin/lcd_dpi_bounce.rs +++ b/qa-test/src/bin/lcd_dpi_bounce.rs @@ -51,7 +51,6 @@ use esp_hal::{ }, main, peripherals::Peripherals, - psram, time::Rate, }; use esp_println::println; @@ -66,16 +65,6 @@ 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 init_psram_heap(start: *mut u8, size: usize) { - unsafe { - esp_alloc::HEAP.add_region(esp_alloc::HeapRegion::new( - start, - size, - esp_alloc::MemoryCapability::External.into(), - )); - } -} - fn rgb565(r: u16, g: u16, b: u16) -> u16 { (r << 11) | (g << 5) | b } @@ -85,12 +74,7 @@ fn main() -> ! { esp_println::logger::init_logger_from_env(); let peripherals: Peripherals = esp_hal::init(esp_hal::Config::default()); - - let (start, size) = psram::psram_raw_parts(&peripherals.PSRAM); - if size == 0 { - panic!("No PSRAM detected"); - } - init_psram_heap(start, size); + esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram); let delay = Delay::new();