Move bounce buffer impl into the esp-hal-bounce-buffers crate
This commit is contained in:
parent
07efc876f3
commit
3b291669ca
11
firmware/Cargo.lock
generated
11
firmware/Cargo.lock
generated
|
|
@ -30,6 +30,7 @@ dependencies = [
|
|||
"esp-backtrace",
|
||||
"esp-bootloader-esp-idf",
|
||||
"esp-hal",
|
||||
"esp-hal-bounce-buffers",
|
||||
"esp-metadata-generated 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"esp-println",
|
||||
"esp-radio",
|
||||
|
|
@ -2143,6 +2144,15 @@ dependencies = [
|
|||
"xtensa-lx-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "esp-hal-bounce-buffers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"document-features",
|
||||
"esp-hal",
|
||||
"ouroboros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "esp-hal-procmacros"
|
||||
version = "0.21.0"
|
||||
|
|
@ -6875,6 +6885,7 @@ checksum = "a0f368519fc6c85fc1afdb769fb5a51123f6158013e143656e25a3485a0d401c"
|
|||
[[package]]
|
||||
name = "spectre-api-sys"
|
||||
version = "0.1.0"
|
||||
source = "git+c:/Users/Limeth/workspace/rust/spectre-api-sys#9e844eb056c3dfee8286ac21ec40fa689a8b8aa2"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ esp-metadata-generated = { version = "0.3.0", features = ["esp32s3"] }
|
|||
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
|
||||
indoc = "2.0.7"
|
||||
ouroboros = "0.18.5"
|
||||
esp-hal-bounce-buffers = { git = "https://forgejo.limeth.cz/limeth/esp-hal-bounce-buffers", features = ["esp32s3"] }
|
||||
|
||||
# A fork of slint with patches for `allocator_api` support.
|
||||
# Don't forget to change `slint-build` in build dependencies, if this is changed.
|
||||
|
|
|
|||
|
|
@ -37,9 +37,7 @@ use embassy_time::{Duration, Timer};
|
|||
use esp_alloc::{HeapRegion, MemoryCapability};
|
||||
use esp_bootloader_esp_idf::partitions::PartitionTable;
|
||||
use esp_hal::clock::CpuClock;
|
||||
use esp_hal::dma::{
|
||||
BurstConfig, ExternalBurstConfig, InternalBurstConfig,
|
||||
};
|
||||
use esp_hal::dma::{BurstConfig, ExternalBurstConfig, InternalBurstConfig};
|
||||
use esp_hal::efuse::Efuse;
|
||||
#[cfg(not(feature = "alt-log"))]
|
||||
use esp_hal::gpio::NoPin;
|
||||
|
|
@ -59,6 +57,7 @@ use esp_hal::system::Stack;
|
|||
use esp_hal::timer::timg::TimerGroup;
|
||||
use esp_hal::uart::{Uart, UartRx};
|
||||
use esp_hal::{Blocking, interrupt};
|
||||
use esp_hal_bounce_buffers::{DmaBounce, Swapchain, allocate_dma_buffer_in};
|
||||
use esp_rtos::embassy::{Executor, InterruptExecutor};
|
||||
use esp_storage::FlashStorage;
|
||||
use i_slint_core::software_renderer::TargetPixel;
|
||||
|
|
@ -85,7 +84,7 @@ use crate::matrix::IoeMatrix;
|
|||
use crate::peripherals::st7701s::St7701s;
|
||||
use crate::proxy::create_hid_report_interceptor;
|
||||
use crate::ui::backend::SlintBackend;
|
||||
use crate::ui::dpi::{DmaBounce, Framebuffer, Swapchain, allocate_dma_buffer_in};
|
||||
use crate::ui::dpi::Framebuffer;
|
||||
use crate::vial::{
|
||||
CustomKeycodes, VIAL_KEYBOARD_DEF, VIAL_KEYBOARD_ID, VIAL_KEYBOARD_NAME, VIAL_PRODUCT_ID,
|
||||
VIAL_VENDOR_ID,
|
||||
|
|
@ -192,7 +191,7 @@ async fn test_bounce_buffers(
|
|||
|
||||
{
|
||||
let write_guard = &mut swapchain_writer.write();
|
||||
let buffer_src = write_guard.cast::<Rgb565Pixel>();
|
||||
let buffer_src = bytemuck::cast_slice_mut::<u8, Rgb565Pixel>(write_guard);
|
||||
let colors = (0..WIDTH_VISIBLE_PIXELS as u8 / 2)
|
||||
.rev()
|
||||
.map(|val| Rgb565Pixel::from_rgb(0xFF, val * 2, 0))
|
||||
|
|
@ -231,7 +230,7 @@ async fn test_bounce_buffers(
|
|||
burst_config,
|
||||
true,
|
||||
);
|
||||
buf.launch_interrupt_driven_task().await;
|
||||
buf.launch_interrupt_driven_task();
|
||||
error!("TEST BOUNCE BUFFERS SECTION DONE");
|
||||
}
|
||||
|
||||
|
|
@ -771,8 +770,7 @@ async fn main_task(peripherals: MainPeripherals) {
|
|||
.bounce_buffers
|
||||
.take()
|
||||
.unwrap()
|
||||
.launch_interrupt_driven_task()
|
||||
.await;
|
||||
.launch_interrupt_driven_task();
|
||||
|
||||
// TODO: Probably want to select! instead and re-try.
|
||||
join_all![
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use alloc::{
|
|||
};
|
||||
use critical_section::Mutex;
|
||||
use esp_hal::time::Instant;
|
||||
use esp_hal_bounce_buffers::SwapchainWriter;
|
||||
use log::{debug, info};
|
||||
use slint::{
|
||||
EventLoopError, PhysicalSize, SharedString, WindowSize,
|
||||
|
|
@ -19,10 +20,7 @@ use slint::{
|
|||
};
|
||||
use xkbcommon::xkb::{self, Keysym};
|
||||
|
||||
use crate::{
|
||||
proxy::{KEY_MESSAGE_CHANNEL, TryFromKeysym},
|
||||
ui::dpi::SwapchainWriter,
|
||||
};
|
||||
use crate::proxy::{KEY_MESSAGE_CHANNEL, TryFromKeysym};
|
||||
|
||||
use super::window_adapter::SoftwareWindowAdapter;
|
||||
|
||||
|
|
@ -135,7 +133,7 @@ impl slint::platform::Platform for SlintBackend {
|
|||
// TODO: VSYNC support.
|
||||
let mut swapchain = self.swapchain.borrow_mut();
|
||||
let mut write_guard = swapchain.write();
|
||||
let framebuffer = write_guard.cast::<Rgb565Pixel>();
|
||||
let framebuffer = bytemuck::cast_slice_mut::<u8, Rgb565Pixel>(&mut write_guard);
|
||||
renderer.render(framebuffer, self.window_size[1] as usize);
|
||||
info!("UI rendered.");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ use esp_hal::{
|
|||
ram,
|
||||
spi::master::AnySpi,
|
||||
};
|
||||
use esp_hal_bounce_buffers::{DmaBounce, Swapchain, SwapchainWriter, allocate_dma_buffer_in};
|
||||
use log::error;
|
||||
use ouroboros::self_referencing;
|
||||
|
||||
|
|
@ -61,894 +62,6 @@ pub unsafe fn cache_invalidate_addr(addr: u32, size: u32) {
|
|||
}
|
||||
}
|
||||
|
||||
// const DMA_CHANNEL_INBOUND: usize = 0;
|
||||
// const INTERRUPT_INBOUND: Interrupt = Interrupt::DMA_IN_CH0;
|
||||
const DMA_CHANNEL_OUTBOUND: usize = 2;
|
||||
const INTERRUPT_OUTBOUND: Interrupt = Interrupt::DMA_OUT_CH2;
|
||||
|
||||
#[self_referencing]
|
||||
struct ReceivingTransfer {
|
||||
mem2mem: SimpleMem2Mem<'static, Blocking>,
|
||||
#[borrows(mut mem2mem)]
|
||||
#[covariant]
|
||||
transfer: Option<SimpleMem2MemTransfer<'this, 'static, Blocking>>,
|
||||
}
|
||||
|
||||
pub struct Swapchain {
|
||||
pub framebuffers: [&'static mut [u8]; 2],
|
||||
}
|
||||
|
||||
impl Swapchain {
|
||||
pub fn into_reader_writer(self) -> (SwapchainReader, SwapchainWriter) {
|
||||
assert_eq!(
|
||||
self.framebuffers[0].len(),
|
||||
self.framebuffers[1].len(),
|
||||
"framebuffers in a swapchain must have an equal length"
|
||||
);
|
||||
|
||||
let reader_index = Arc::new(AtomicBool::new(true));
|
||||
|
||||
(
|
||||
SwapchainReader {
|
||||
framebuffers_rw: [
|
||||
self.framebuffers[0] as *const [u8],
|
||||
self.framebuffers[1] as *const [u8],
|
||||
],
|
||||
reader_index: reader_index.clone(),
|
||||
},
|
||||
SwapchainWriter {
|
||||
framebuffers_wr: [
|
||||
self.framebuffers[1] as *mut [u8],
|
||||
self.framebuffers[0] as *mut [u8],
|
||||
],
|
||||
reader_index,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Don't need to store the framebuffer length twice. Use `*const u8` instead, and store length separately.
|
||||
pub struct SwapchainReader {
|
||||
/// These are in the opposite order to `SwapchainWriter`'s framebuffers.
|
||||
framebuffers_rw: [*const [u8]; 2],
|
||||
reader_index: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
unsafe impl Send for SwapchainReader {}
|
||||
|
||||
impl SwapchainReader {
|
||||
fn len(&self) -> usize {
|
||||
self.framebuffers_rw[0].len()
|
||||
}
|
||||
|
||||
fn load_read_index(&self) -> usize {
|
||||
self.reader_index.load(atomic::Ordering::SeqCst) as usize
|
||||
}
|
||||
|
||||
fn get_latest_framebuffer(&self) -> &[u8] {
|
||||
unsafe { &*self.framebuffers_rw[self.load_read_index()] }
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Don't need to store the framebuffer length twice. Use `*mut u8` instead, and store length separately.
|
||||
pub struct SwapchainWriter {
|
||||
/// These are in the opposite order to `SwapchainReader`'s framebuffers.
|
||||
framebuffers_wr: [*mut [u8]; 2],
|
||||
reader_index: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
unsafe impl Send for SwapchainWriter {}
|
||||
|
||||
impl SwapchainWriter {
|
||||
fn len(&self) -> usize {
|
||||
self.framebuffers_wr[0].len()
|
||||
}
|
||||
|
||||
pub fn write(&mut self) -> SwapchainWriteGuard<'_> {
|
||||
let framebuffer_ptr =
|
||||
self.framebuffers_wr[self.reader_index.load(atomic::Ordering::SeqCst) as usize];
|
||||
SwapchainWriteGuard {
|
||||
framebuffer: unsafe { &mut *framebuffer_ptr },
|
||||
reader_index: &self.reader_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SwapchainWriteGuard<'a> {
|
||||
framebuffer: &'a mut [u8],
|
||||
reader_index: &'a AtomicBool,
|
||||
}
|
||||
|
||||
impl<'a> SwapchainWriteGuard<'a> {
|
||||
pub fn cast<T: NoUninit + AnyBitPattern>(&mut self) -> &mut [T] {
|
||||
bytemuck::cast_slice_mut::<_, T>(self.framebuffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SwapchainWriteGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.reader_index.fetch_xor(true, atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for SwapchainWriteGuard<'a> {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.framebuffer
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for SwapchainWriteGuard<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.framebuffer
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DmaBounce {
|
||||
// TODO: Make these generic.
|
||||
// They currently cannot be generic, because they lacks a `reborrow` method.
|
||||
channel: DMA_CH0<'static>,
|
||||
// This can also be more generic, see `DmaEligible` in `Mem2Mem::new`.
|
||||
peripheral_src: AnySpi<'static>,
|
||||
// This can also be more generic, see `DmaEligible` in `Mem2Mem::new`.
|
||||
peripheral_dst: Option<Dpi<'static, Blocking>>,
|
||||
// TODO: Combine with peripheral_dst using an enum?
|
||||
transfer_dst: Option<DpiTransfer<'static, DmaTxBounceBuf, Blocking>>,
|
||||
|
||||
// TODO: Consider having a separate burst config for the two transfers.
|
||||
burst_config: BurstConfig,
|
||||
cyclic: bool,
|
||||
/// The size of each window.
|
||||
window_size: usize,
|
||||
/// The number of windows.
|
||||
windows_len: usize,
|
||||
swapchain_src: SwapchainReader,
|
||||
// Two buffers of size `window_size`,
|
||||
// one of which is being written to, while the other is being read from.
|
||||
bounce_buffer_dst: &'static mut [u8],
|
||||
bounce_buffer_src: &'static mut [u8],
|
||||
// A descriptor list that spans a buffer of size `window_size`.
|
||||
// The buffer pointers need to be updated before each transmission to point to the correct window in the source buffer `src_buffer`.
|
||||
src_descs: &'static mut [DmaDescriptor],
|
||||
// A descriptor list that spans a buffer of size `window_size`.
|
||||
// The buffer pointers need to be updated before each transmission to point to the correct bounce buffer.
|
||||
bounce_dst_descs: &'static mut [DmaDescriptor],
|
||||
// A cyclic descriptor list that spans the buffers `bounce_buffer_dst` and `bounce_buffer_src`.
|
||||
bounce_src_descs: &'static mut [DmaDescriptor],
|
||||
descriptors_per_window: usize,
|
||||
// The index of the next window about to be received into the destination bounce buffer.
|
||||
window_index_next: usize,
|
||||
frame_index_next: usize,
|
||||
receiving_transfer: Option<ReceivingTransfer>,
|
||||
}
|
||||
|
||||
impl DmaBounce {
|
||||
/// * `allocator` - The allocator used to allocate the bounce buffers.
|
||||
/// * `channel` - The DMA channel used to transfer data from the source buffer to the bounce buffers.
|
||||
/// * `peripheral_src` - The peripheral to transfer data from the source buffer to the bounce buffers.
|
||||
/// * `peripheral_dst` - The peripheral to transfer data to, from the bounce buffers.
|
||||
/// * `buffer_src` - The source buffer, typically allocated in external memory.
|
||||
/// * `row_front_porch_bytes` - The number of arbitrary-valued bytes to be sent in front of each row to the destination peripheral.
|
||||
/// * `row_width_bytes` - The width of a row, in bytes.
|
||||
/// * `window_size_rows` - The size of a single bounce buffer, in rows.
|
||||
/// * `burst_config` - The burst config to use for memory transfers (both in and out). TODO: This could be split.
|
||||
/// * `cyclic` - Experimental! Whether to use a cyclic descriptor list for transfer from the bounce buffers to the destination peripheral.
|
||||
pub fn new(
|
||||
allocator: impl Allocator + Copy + 'static,
|
||||
channel: DMA_CH0<'static>,
|
||||
peripheral_src: AnySpi<'static>,
|
||||
peripheral_dst: Dpi<'static, Blocking>,
|
||||
swapchain_src: SwapchainReader,
|
||||
row_front_porch_bytes: usize,
|
||||
row_width_bytes: usize,
|
||||
window_size_rows: usize,
|
||||
burst_config: BurstConfig,
|
||||
cyclic: bool,
|
||||
) -> Self {
|
||||
let window_size = row_width_bytes * window_size_rows;
|
||||
|
||||
assert_eq!(
|
||||
swapchain_src.len() % window_size,
|
||||
0,
|
||||
"the size of a source buffer must be a multiple of the window size ({window_size} bytes), but it is {len} bytes large",
|
||||
len = swapchain_src.len()
|
||||
);
|
||||
|
||||
// Conservative alignment. Maxiumum of the cartesian product of [tx, rx] × [internal, external].
|
||||
let alignment = burst_config.min_compatible_alignment();
|
||||
|
||||
for &swapchain_ptr in &swapchain_src.framebuffers_rw {
|
||||
assert_eq!(
|
||||
unsafe { &*swapchain_ptr }.as_ptr() as usize % alignment,
|
||||
0,
|
||||
"the source buffer must be sufficiently aligned to {alignment} bytes for the burst config",
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
row_width_bytes % alignment,
|
||||
0,
|
||||
"the size of a row in bytes must be sufficiently aligned to {alignment} bytes for the burst config",
|
||||
);
|
||||
assert_eq!(
|
||||
row_front_porch_bytes % alignment,
|
||||
0,
|
||||
"the size of a row's front porch in bytes must be sufficiently aligned to {alignment} bytes for the burst config",
|
||||
);
|
||||
// We need to make the destination peripheral read the front porch data from somewhere,
|
||||
// and that somewhere is currently the bounce buffer.
|
||||
// Therefore the front porch must be in bounds.
|
||||
assert!(
|
||||
row_front_porch_bytes <= window_size,
|
||||
"front porch too large"
|
||||
);
|
||||
|
||||
let windows_len = swapchain_src.len() / window_size;
|
||||
// TODO: Figure out a way to avoid `leak`ing memory.
|
||||
// We probably want to store the `Box`es and then unsafely extend the lifetime at sites of usage.
|
||||
let bounce_buffer_dst =
|
||||
Box::leak(allocate_dma_buffer_in(window_size, burst_config, allocator));
|
||||
let bounce_buffer_src =
|
||||
Box::leak(allocate_dma_buffer_in(window_size, burst_config, allocator));
|
||||
let src_descs = Self::linear_descriptors_for_buffer(window_size, burst_config, |desc| {
|
||||
desc.reset_for_tx(desc.next.is_null());
|
||||
// Length for TX buffers must be set in software.
|
||||
// In RX buffers, it is set by hardware.
|
||||
desc.set_length(desc.size());
|
||||
});
|
||||
let bounce_dst_descs =
|
||||
Self::linear_descriptors_for_buffer(window_size, burst_config, |_| {});
|
||||
let (bounce_src_descs, descriptors_per_window) = Self::bounce_descriptors_for_buffer(
|
||||
windows_len,
|
||||
row_front_porch_bytes,
|
||||
row_width_bytes,
|
||||
window_size_rows,
|
||||
unsafe {
|
||||
(
|
||||
&mut *(bounce_buffer_dst as *mut _),
|
||||
&mut *(bounce_buffer_src as *mut _),
|
||||
)
|
||||
},
|
||||
burst_config,
|
||||
cyclic,
|
||||
);
|
||||
|
||||
Self {
|
||||
channel,
|
||||
peripheral_src,
|
||||
peripheral_dst: Some(peripheral_dst),
|
||||
transfer_dst: None,
|
||||
burst_config,
|
||||
cyclic,
|
||||
window_size,
|
||||
windows_len,
|
||||
swapchain_src,
|
||||
bounce_buffer_dst,
|
||||
bounce_buffer_src,
|
||||
src_descs,
|
||||
bounce_dst_descs,
|
||||
bounce_src_descs,
|
||||
descriptors_per_window,
|
||||
window_index_next: 0,
|
||||
frame_index_next: 0,
|
||||
receiving_transfer: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_descriptors_for_buffer(
|
||||
buffer_len: usize,
|
||||
burst_config: BurstConfig,
|
||||
mut setup_desc: impl FnMut(&mut DmaDescriptor),
|
||||
) -> &'static mut [DmaDescriptor] {
|
||||
let max_chunk_size = burst_config.max_compatible_chunk_size();
|
||||
let descriptors_len = dma::descriptor_count(buffer_len, max_chunk_size, false);
|
||||
// TODO: This leaks memory. Ensure it's only called during setup.
|
||||
let descriptors = Box::leak(vec![DmaDescriptor::EMPTY; descriptors_len].into_boxed_slice());
|
||||
|
||||
// Link up the descriptors.
|
||||
let mut next = core::ptr::null_mut();
|
||||
for desc in descriptors.iter_mut().rev() {
|
||||
desc.next = next;
|
||||
next = desc;
|
||||
}
|
||||
|
||||
// Prepare each descriptor's buffer size.
|
||||
let mut descriptors_it = descriptors.iter_mut();
|
||||
let mut remaining_len = buffer_len;
|
||||
|
||||
while remaining_len > 0 {
|
||||
let chunk_size = core::cmp::min(max_chunk_size, remaining_len);
|
||||
let desc = descriptors_it.next().unwrap();
|
||||
desc.set_size(chunk_size);
|
||||
(setup_desc)(desc);
|
||||
remaining_len -= chunk_size;
|
||||
}
|
||||
|
||||
descriptors
|
||||
}
|
||||
|
||||
fn prepare_descriptors_window(
|
||||
bounce_buffer: &mut [u8],
|
||||
descriptors_window: &mut [DmaDescriptor],
|
||||
row_front_porch_bytes: usize,
|
||||
row_width_bytes: usize,
|
||||
window_size_rows: usize,
|
||||
max_chunk_size: usize,
|
||||
descriptors_per_row: usize,
|
||||
descriptors_per_row_front_porch: usize,
|
||||
) {
|
||||
for (row_index_in_window, descriptors_row) in descriptors_window
|
||||
.chunks_mut(descriptors_per_row)
|
||||
.enumerate()
|
||||
{
|
||||
// let row_index = row_index_in_window + window_index * window_size_rows;
|
||||
let (descriptors_row_front_porch, descriptors_row_stored) =
|
||||
descriptors_row.split_at_mut(descriptors_per_row_front_porch);
|
||||
|
||||
// Prepare front porch descriptors.
|
||||
{
|
||||
let mut descriptors_it = descriptors_row_front_porch.iter_mut();
|
||||
let mut remaining_front_porch = row_front_porch_bytes;
|
||||
|
||||
while remaining_front_porch > 0 {
|
||||
let desc = descriptors_it.next().unwrap();
|
||||
let chunk_size = core::cmp::min(max_chunk_size, remaining_front_porch);
|
||||
remaining_front_porch -= chunk_size;
|
||||
// Just make it point at a bounce buffer.
|
||||
// It is guaranteed to have enough bytes by `DmaBounce::new`.
|
||||
desc.buffer = bounce_buffer.as_mut_ptr();
|
||||
desc.set_size(chunk_size);
|
||||
desc.set_length(chunk_size);
|
||||
desc.reset_for_tx(false);
|
||||
}
|
||||
|
||||
assert!(
|
||||
descriptors_it.next().is_none(),
|
||||
"front porch descriptors must be used up"
|
||||
);
|
||||
assert_eq!(
|
||||
descriptors_row_front_porch
|
||||
.iter()
|
||||
.map(|desc| desc.size())
|
||||
.sum::<usize>(),
|
||||
row_front_porch_bytes
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare window descriptors.
|
||||
{
|
||||
let mut remaining_bounce_buffer =
|
||||
&mut bounce_buffer[row_index_in_window * row_width_bytes..][..row_width_bytes];
|
||||
|
||||
// if remaining_bounce_buffer.len() > row_width_bytes {
|
||||
// remaining_bounce_buffer = &mut remaining_bounce_buffer[..row_width_bytes];
|
||||
// }
|
||||
|
||||
for desc in &mut *descriptors_row_stored {
|
||||
let chunk_size = core::cmp::min(max_chunk_size, remaining_bounce_buffer.len());
|
||||
desc.buffer = remaining_bounce_buffer.as_mut_ptr();
|
||||
remaining_bounce_buffer = &mut remaining_bounce_buffer[chunk_size..];
|
||||
desc.set_size(chunk_size);
|
||||
desc.set_length(chunk_size);
|
||||
desc.reset_for_tx(false);
|
||||
}
|
||||
|
||||
assert!(
|
||||
remaining_bounce_buffer.is_empty(),
|
||||
"bounce buffer must be used up"
|
||||
);
|
||||
assert_eq!(
|
||||
descriptors_row_stored
|
||||
.iter()
|
||||
.map(|desc| desc.size())
|
||||
.sum::<usize>(),
|
||||
row_width_bytes
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Set EOF bit on the last descriptor of the window, to signal
|
||||
// that the bounce buffer is done being read from.
|
||||
if let Some(last_desc) = descriptors_window.last_mut() {
|
||||
last_desc.reset_for_tx(true);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
descriptors_window
|
||||
.iter()
|
||||
.map(|desc| desc.size())
|
||||
.sum::<usize>(),
|
||||
window_size_rows * (row_front_porch_bytes + row_width_bytes)
|
||||
);
|
||||
}
|
||||
|
||||
fn bounce_descriptors_for_buffer(
|
||||
windows_len: usize,
|
||||
row_front_porch_bytes: usize,
|
||||
row_width_bytes: usize,
|
||||
window_size_rows: usize,
|
||||
bounce_buffers: (&'static mut [u8], &'static mut [u8]),
|
||||
burst_config: BurstConfig,
|
||||
cyclic: bool,
|
||||
) -> (&'static mut [DmaDescriptor], usize) {
|
||||
assert_eq!(
|
||||
bounce_buffers.0.len(),
|
||||
bounce_buffers.1.len(),
|
||||
"bounce buffers must be equal in size"
|
||||
);
|
||||
// If an odd number of windows were needed, two descriptor lists would be needed,
|
||||
assert_eq!(windows_len % 2, 0, "the number of windows must be even");
|
||||
|
||||
let buffer_len = bounce_buffers.0.len();
|
||||
|
||||
assert_eq!(
|
||||
buffer_len,
|
||||
row_width_bytes * window_size_rows,
|
||||
"the provided bounce buffers have an invalid size"
|
||||
);
|
||||
|
||||
// Implementation note:
|
||||
// A cyclic descriptor could consist of just a set of descriptors per window,
|
||||
// so two sets in total, because there are two bounce buffers.
|
||||
// However, we can also access the pointer of the EOF descriptor within the
|
||||
// EOF interrupt handler, which lets us compute which descriptor generated that
|
||||
// interrupt.
|
||||
// This is useful in the case when an interrupt is missed. Then the number of interrupts
|
||||
// handled doesn't correspond to the number of windows sent to the destination peripheral.
|
||||
// In that case, the number of windows sent can be computed from the address of the descriptor.
|
||||
let max_chunk_size = burst_config.max_compatible_chunk_size();
|
||||
let descriptors_per_row_front_porch =
|
||||
dma::descriptor_count(row_front_porch_bytes, max_chunk_size, false);
|
||||
let descriptors_per_row_stored =
|
||||
dma::descriptor_count(row_width_bytes, max_chunk_size, false);
|
||||
let descriptors_per_row = descriptors_per_row_stored + descriptors_per_row_front_porch;
|
||||
let descriptors_per_window = window_size_rows * descriptors_per_row;
|
||||
let descriptors_per_frame = descriptors_per_window * windows_len;
|
||||
let descriptors_frame =
|
||||
Box::leak(vec![DmaDescriptor::EMPTY; descriptors_per_frame].into_boxed_slice());
|
||||
|
||||
// Link up the descriptors.
|
||||
let mut next = if cyclic {
|
||||
descriptors_frame.first_mut().unwrap() as *mut _
|
||||
} else {
|
||||
core::ptr::null_mut()
|
||||
};
|
||||
|
||||
for desc in descriptors_frame.iter_mut().rev() {
|
||||
desc.next = next;
|
||||
next = desc;
|
||||
}
|
||||
|
||||
// Prepare each descriptor's buffer size.
|
||||
let bounce_buffers = [bounce_buffers.0, bounce_buffers.1];
|
||||
|
||||
for (window_index, descriptors_window) in descriptors_frame
|
||||
.chunks_mut(descriptors_per_window)
|
||||
.enumerate()
|
||||
{
|
||||
let bounce_buffer_index = window_index % 2;
|
||||
let bounce_buffer = &mut *bounce_buffers[bounce_buffer_index];
|
||||
|
||||
Self::prepare_descriptors_window(
|
||||
bounce_buffer,
|
||||
descriptors_window,
|
||||
row_front_porch_bytes,
|
||||
row_width_bytes,
|
||||
window_size_rows,
|
||||
max_chunk_size,
|
||||
descriptors_per_row,
|
||||
descriptors_per_row_front_porch,
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
descriptors_frame
|
||||
.iter()
|
||||
.map(|desc| desc.size())
|
||||
.sum::<usize>(),
|
||||
windows_len * window_size_rows * (row_front_porch_bytes + row_width_bytes)
|
||||
);
|
||||
|
||||
(descriptors_frame, descriptors_per_window)
|
||||
}
|
||||
|
||||
/// Safety:
|
||||
/// TX descriptors require read access to the buffer.
|
||||
/// RX descriptors require write access to the buffer.
|
||||
unsafe fn linear_descriptors_prepare(
|
||||
descriptors: &mut [DmaDescriptor],
|
||||
mut buffer: Option<&[u8]>,
|
||||
mut setup_desc: impl FnMut(&mut DmaDescriptor),
|
||||
) {
|
||||
for descriptor in descriptors.iter_mut() {
|
||||
if let Some(inner_buffer) = buffer {
|
||||
descriptor.buffer = inner_buffer.as_ptr() as *mut u8;
|
||||
buffer = Some(&inner_buffer[descriptor.size()..]);
|
||||
}
|
||||
(setup_desc)(descriptor);
|
||||
}
|
||||
|
||||
if let Some(buffer) = buffer {
|
||||
assert!(
|
||||
buffer.is_empty(),
|
||||
"a buffer of an incompatible length was assigned to a descriptor set"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn enable_interrupts() {
|
||||
// Enable interrupts for the peripheral, pt. 1.
|
||||
interrupt::enable(
|
||||
INTERRUPT_OUTBOUND,
|
||||
dma_outbound_interrupt_handler.priority(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Bind the interrupt handler.
|
||||
unsafe {
|
||||
interrupt::bind_interrupt(INTERRUPT_OUTBOUND, dma_outbound_interrupt_handler.handler());
|
||||
}
|
||||
|
||||
// Enable interrupts for the peripheral, pt. 2.
|
||||
DMA::regs()
|
||||
.ch(DMA_CHANNEL_OUTBOUND)
|
||||
.out_int()
|
||||
.ena()
|
||||
.modify(|_, w| w.out_eof().bit(true));
|
||||
}
|
||||
|
||||
/// Receive a window of bytes into the current dst bounce buffer.
|
||||
/// Finally, swaps the bounce buffers.
|
||||
///
|
||||
/// # Safety:
|
||||
/// TODO
|
||||
unsafe fn receive_window_start(&mut self) -> ReceivingTransfer {
|
||||
// Descriptors are initialized by `DmaTxBuf::new`.
|
||||
let buffer_src_window = &self.swapchain_src.get_latest_framebuffer()
|
||||
[self.window_index_next * self.window_size..][..self.window_size];
|
||||
|
||||
unsafe {
|
||||
Self::linear_descriptors_prepare(self.src_descs, Some(buffer_src_window), |_desc| {
|
||||
// No need to call `DmaDescriptor::reset_for_tx`, because
|
||||
// 1. we don't rely on the ownership flag;
|
||||
// 2. the EOF flag is already set during the construction of this buffer.
|
||||
});
|
||||
// TODO: Precompute a descriptor list for each buffer, then use `None` instead of `Some(&mut *self.bounce_buffer_dst)`.
|
||||
Self::linear_descriptors_prepare(
|
||||
self.bounce_dst_descs,
|
||||
Some(self.bounce_buffer_dst),
|
||||
|desc| {
|
||||
desc.reset_for_rx();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Extend the lifetime to 'static because it is required by Mem2Mem.
|
||||
//
|
||||
// Safety:
|
||||
// Pointees are done being used by the driver before this scope ends,
|
||||
// this is because we `SimpleMem2MemTransfer::wait()` on the transfer to finish.
|
||||
let bounce_dst_descs: &'static mut [DmaDescriptor] =
|
||||
unsafe { &mut *(self.bounce_dst_descs as *mut _) };
|
||||
let src_descs: &'static mut [DmaDescriptor] = unsafe { &mut *(self.src_descs as *mut _) };
|
||||
|
||||
let mem2mem = unsafe {
|
||||
Mem2Mem::new(
|
||||
self.channel.clone_unchecked(),
|
||||
self.peripheral_src.clone_unchecked(),
|
||||
)
|
||||
}
|
||||
.with_descriptors(bounce_dst_descs, src_descs, self.burst_config)
|
||||
.unwrap();
|
||||
|
||||
ReceivingTransferBuilder {
|
||||
mem2mem,
|
||||
transfer_builder: |mem2mem| {
|
||||
Some(
|
||||
mem2mem
|
||||
.start_transfer(self.bounce_buffer_dst, buffer_src_window)
|
||||
.unwrap(),
|
||||
)
|
||||
},
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Receive a window of bytes into the current dst bounce buffer.
|
||||
/// Finally, swaps the bounce buffers.
|
||||
async fn receive_window(&mut self) {
|
||||
// Descriptors are initialized by `DmaTxBuf::new`.
|
||||
let buffer_src_window = &self.swapchain_src.get_latest_framebuffer()
|
||||
[self.window_index_next * self.window_size..][..self.window_size];
|
||||
|
||||
unsafe {
|
||||
Self::linear_descriptors_prepare(self.src_descs, Some(buffer_src_window), |_desc| {
|
||||
// No need to call `DmaDescriptor::reset_for_tx`, because
|
||||
// 1. we don't rely on the ownership flag;
|
||||
// 2. the EOF flag is already set during the construction of this buffer.
|
||||
});
|
||||
// TODO: Precompute a descriptor list for each buffer, then use `None` instead of `Some(&mut *self.bounce_buffer_dst)`.
|
||||
Self::linear_descriptors_prepare(
|
||||
self.bounce_dst_descs,
|
||||
Some(self.bounce_buffer_dst),
|
||||
|desc| {
|
||||
desc.reset_for_rx();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Extend the lifetime to 'static because it is required by Mem2Mem.
|
||||
//
|
||||
// Safety:
|
||||
// Pointees are done being used by the driver before this scope ends,
|
||||
// this is because we `SimpleMem2MemTransfer::wait()` on the transfer to finish.
|
||||
let bounce_dst_descs: &'static mut [DmaDescriptor] =
|
||||
unsafe { &mut *(self.bounce_dst_descs as *mut _) };
|
||||
let src_descs: &'static mut [DmaDescriptor] =
|
||||
unsafe { &mut *(self.src_descs as *mut _) };
|
||||
|
||||
let mut mem2mem = Mem2Mem::new(self.channel.reborrow(), self.peripheral_src.reborrow())
|
||||
.into_async()
|
||||
.with_descriptors(bounce_dst_descs, src_descs, self.burst_config)
|
||||
.unwrap();
|
||||
let transfer = mem2mem
|
||||
.start_transfer(self.bounce_buffer_dst, buffer_src_window)
|
||||
.unwrap();
|
||||
|
||||
transfer.wait_async().await.unwrap();
|
||||
}
|
||||
|
||||
self.increase_window_counter(1);
|
||||
}
|
||||
|
||||
fn increase_window_counter(&mut self, windows: isize) {
|
||||
if windows.rem_euclid(2) == 1 {
|
||||
core::mem::swap(&mut self.bounce_buffer_dst, &mut self.bounce_buffer_src);
|
||||
}
|
||||
|
||||
let window_index_next = self.window_index_next as isize + windows;
|
||||
self.frame_index_next = (self.frame_index_next as isize
|
||||
+ window_index_next / self.windows_len as isize)
|
||||
as usize;
|
||||
self.window_index_next = window_index_next.rem_euclid(self.windows_len as isize) as usize;
|
||||
}
|
||||
|
||||
pub async fn launch_interrupt_driven_task(mut self) {
|
||||
Self::enable_interrupts();
|
||||
|
||||
// Receive the first 2 windows, so that the outbound transfer can read valid data.
|
||||
self.receive_window().await;
|
||||
|
||||
let dma_tx_buffer = self.get_dma_tx_buffer();
|
||||
let transfer = self
|
||||
.peripheral_dst
|
||||
.take()
|
||||
.unwrap()
|
||||
.send(self.cyclic /* Send perpetually */, dma_tx_buffer)
|
||||
.unwrap_or_else(|(error, _, _)| {
|
||||
panic!("failed to begin the transmission of the first frame: {error:?}");
|
||||
});
|
||||
self.transfer_dst = Some(transfer);
|
||||
self.receiving_transfer = Some(unsafe { self.receive_window_start() });
|
||||
|
||||
unsafe {
|
||||
*DMA_STATE.0.get() = Some(self);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dma_tx_buffer(&mut self) -> DmaTxBounceBuf {
|
||||
DmaTxBounceBuf {
|
||||
preparation: dma::Preparation {
|
||||
start: self.bounce_src_descs.first_mut().unwrap(),
|
||||
direction: dma::TransferDirection::Out,
|
||||
accesses_psram: false,
|
||||
burst_transfer: self.burst_config,
|
||||
// We don't care about ownership.
|
||||
// Just yeet whatever the descriptors point to to the destination peripheral.
|
||||
check_owner: Some(false),
|
||||
auto_write_back: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DmaTxBounceBuf {
|
||||
preparation: dma::Preparation,
|
||||
}
|
||||
|
||||
unsafe impl DmaTxBuffer for DmaTxBounceBuf {
|
||||
type View = Self;
|
||||
type Final = Self;
|
||||
|
||||
fn prepare(&mut self) -> dma::Preparation {
|
||||
dma::Preparation {
|
||||
start: self.preparation.start,
|
||||
direction: self.preparation.direction,
|
||||
accesses_psram: self.preparation.accesses_psram,
|
||||
burst_transfer: self.preparation.burst_transfer,
|
||||
check_owner: self.preparation.check_owner,
|
||||
auto_write_back: self.preparation.auto_write_back,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_view(self) -> Self::View {
|
||||
self
|
||||
}
|
||||
|
||||
fn from_view(view: Self::View) -> Self::Final {
|
||||
view
|
||||
}
|
||||
}
|
||||
|
||||
static DMA_STATE: SyncUnsafeCell<Option<DmaBounce>> = SyncUnsafeCell(UnsafeCell::new(None));
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct SyncUnsafeCell<T>(UnsafeCell<T>);
|
||||
|
||||
unsafe impl<T> Sync for SyncUnsafeCell<T> {}
|
||||
|
||||
#[handler(priority = Priority::Priority3)]
|
||||
#[ram] // Improves performance.
|
||||
fn dma_outbound_interrupt_handler() {
|
||||
let interrupt = DMA::regs().ch(DMA_CHANNEL_OUTBOUND).out_int();
|
||||
let bounce_buffer_sent = interrupt.st().read().out_eof().bit_is_set();
|
||||
|
||||
if !bounce_buffer_sent {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the bit by writing 1 to the clear bits.
|
||||
interrupt.clr().write(|w| w.out_eof().bit(true));
|
||||
|
||||
// SAFETY: This value is only ever read in our interrupt handler,
|
||||
// and interrupts are disabled, and we only use this in one thread.
|
||||
let Some(dma_state) = unsafe { &mut *DMA_STATE.0.get() }.as_mut() else {
|
||||
error!("no DMA state available when executing DMA interrupt handler");
|
||||
return;
|
||||
};
|
||||
|
||||
// The descriptor of the buffer with an EOF flag that just finished being sent.
|
||||
let descriptor_ptr = DMA::regs()
|
||||
.ch(DMA_CHANNEL_OUTBOUND)
|
||||
.out_eof_des_addr()
|
||||
.read()
|
||||
.out_eof_des_addr()
|
||||
.bits() as *const DmaDescriptor;
|
||||
// This is the index of the window that just finished being transmitted to the destination peripheral.
|
||||
let window_sent_index =
|
||||
unsafe { descriptor_ptr.offset_from_unsigned(dma_state.bounce_src_descs.as_ptr()) }
|
||||
/ dma_state.descriptors_per_window;
|
||||
// The next window to be sent is `(window_sent_index + 1) % dma_state.windows_len`.
|
||||
// That is not the window we want to buffer, because the transmissions would race.
|
||||
// We instead want to buffer the next window:
|
||||
let window_index_next = (window_sent_index + 2) % dma_state.windows_len;
|
||||
|
||||
// Swap bounce buffers.
|
||||
if (dma_state.windows_len + window_index_next - dma_state.window_index_next) % 2 == 1 {
|
||||
core::mem::swap(
|
||||
&mut dma_state.bounce_buffer_dst,
|
||||
&mut dma_state.bounce_buffer_src,
|
||||
);
|
||||
}
|
||||
|
||||
dma_state.window_index_next = window_index_next;
|
||||
|
||||
let mut receiving_transfer = dma_state
|
||||
.receiving_transfer
|
||||
.take()
|
||||
.expect("no ongoing transfer to a bounce buffer present");
|
||||
let receiving_transfer = receiving_transfer
|
||||
.with_mut(|x| x.transfer.take())
|
||||
.expect("no ongoing inner transfer to a bounce buffer present");
|
||||
|
||||
if receiving_transfer.is_done() {
|
||||
drop(receiving_transfer);
|
||||
} else {
|
||||
// error!("the transfer to a bounce buffer has not finished yet, waiting");
|
||||
receiving_transfer.wait().unwrap();
|
||||
}
|
||||
|
||||
// If there is any ongoing transfer, cancel it and start a new one.
|
||||
dma_state.receiving_transfer = Some(unsafe { dma_state.receive_window_start() });
|
||||
}
|
||||
|
||||
// #[handler(priority = Priority::Priority3)]
|
||||
// #[ram] // Improves performance.
|
||||
// fn dma_inbound_interrupt_handler() {
|
||||
// warn!("Inbound");
|
||||
|
||||
// let interrupt = DMA::regs().ch(DMA_CHANNEL_INBOUND).in_int();
|
||||
// let bounce_buffer_processed = interrupt.st().read().in_done().bit_is_set();
|
||||
// if bounce_buffer_processed {
|
||||
// // Clear the bit by writing 1 to the clear bits.
|
||||
// interrupt.clr().write(|w| w.in_done().bit(true));
|
||||
|
||||
// assert!(
|
||||
// !INBOUND_TRANSFER_FINISHED.signaled(),
|
||||
// "inbound transfer already signalled"
|
||||
// );
|
||||
// INBOUND_TRANSFER_FINISHED.signal(());
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub async fn run_lcd(
|
||||
// mut st7701s: St7701s<'static, Blocking>,
|
||||
// framebuffer: &'static mut Framebuffer,
|
||||
// ) {
|
||||
// loop {
|
||||
// // Timer::after(Duration::from_millis(100)).await;
|
||||
// // yield_now().await;
|
||||
// SIGNAL_LCD_SUBMIT.wait().await;
|
||||
|
||||
// // TODO: Use bounce buffers:
|
||||
// // https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer
|
||||
// // This can be implemented as a `DmaTxBuffer`.
|
||||
// let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) {
|
||||
// Err((error, result_dpi, result_dma_buf)) => {
|
||||
// error!(
|
||||
// "An error occurred while initiating transfer of the framebuffer to the LCD display: {error:?}"
|
||||
// );
|
||||
// st7701s.dpi = result_dpi;
|
||||
// framebuffer.dma_buf = Some(result_dma_buf);
|
||||
// continue;
|
||||
// }
|
||||
// Ok(transfer) => transfer,
|
||||
// };
|
||||
|
||||
// // This could be used to allow other tasks to be executed on the first core, but that causes
|
||||
// // the flash to be accessed, which interferes with the framebuffer transfer.
|
||||
// // For that reason, it is disabled, and this task blocks the first core, until the transfer
|
||||
// // is complete.
|
||||
// #[cfg(not(feature = "limit-fps"))]
|
||||
// while !transfer.is_done() {
|
||||
// // Timer::after_millis(1).await;
|
||||
// rmk::embassy_futures::yield_now().await;
|
||||
// }
|
||||
|
||||
// let result;
|
||||
// let dma_buf;
|
||||
// (result, st7701s.dpi, dma_buf) = transfer.wait();
|
||||
// framebuffer.dma_buf = Some(dma_buf);
|
||||
|
||||
// SIGNAL_UI_RENDER.signal(());
|
||||
|
||||
// if let Err(error) = result {
|
||||
// error!(
|
||||
// "An error occurred while transferring framebuffer to the LCD display: {error:?}"
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Allocates a buffer appropriately aligned for use with DMA.
|
||||
pub fn allocate_dma_buffer_in<A: Allocator>(
|
||||
len: usize,
|
||||
burst_config: BurstConfig,
|
||||
alloc: A,
|
||||
) -> Box<[u8], A> {
|
||||
// Conservative alignment. Maxiumum of the cartesian product of [tx, rx] × [internal, external].
|
||||
let alignment = burst_config.min_compatible_alignment();
|
||||
|
||||
assert_eq!(
|
||||
len % alignment,
|
||||
0,
|
||||
"the size of a DMA buffer must be a multiple of {alignment} bytes, but it is {len} bytes large"
|
||||
);
|
||||
|
||||
// ⚠️ Note: For chips that support DMA to/from PSRAM (ESP32-S3) DMA transfers to/from PSRAM
|
||||
// have extra alignment requirements. The address and size of the buffer pointed to by each
|
||||
// descriptor must be a multiple of the cache line (block) size. This is 32 bytes on ESP32-S3.
|
||||
// That is ensured by the `assert_eq` preceding this block.
|
||||
unsafe {
|
||||
let raw = alloc
|
||||
.allocate_zeroed(Layout::from_size_align(len, alignment).unwrap())
|
||||
.expect("failed to allocate a DMA buffer");
|
||||
Box::from_raw_in(raw.as_ptr(), alloc)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Rename or get rid of.
|
||||
pub struct Framebuffer {
|
||||
pub width: u32,
|
||||
|
|
|
|||
Loading…
Reference in a new issue