Skip to content
2 changes: 2 additions & 0 deletions esp-hal/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Comment thread
fmauNeko marked this conversation as resolved.
- 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)
Expand Down
264 changes: 264 additions & 0 deletions esp-hal/src/dma/buffers/bounce_buffer.rs
Original file line number Diff line number Diff line change
@@ -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<Self, DmaBounceBufferError> {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
58 changes: 57 additions & 1 deletion esp-hal/src/lcd_cam/lcd/dpi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -147,6 +157,7 @@ where
config: Config,
) -> Result<Self, ConfigError> {
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,
Expand Down Expand Up @@ -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()
});

Expand Down Expand Up @@ -644,6 +656,50 @@ impl<BUF: DmaTxBuffer, Dm: DriverMode> 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)]
Expand Down
Loading
Loading