From f20f4e799311a1719dce8384cee199fd0c614378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hlusi=C4=8Dka?= Date: Sun, 22 Feb 2026 00:59:01 +0100 Subject: [PATCH] Hook up bounce buffers to the renderer --- firmware/acid-firmware/.cargo/config.toml | 18 +- firmware/acid-firmware/src/logging.rs | 19 +- firmware/acid-firmware/src/main.rs | 196 ++++++++++++-- firmware/acid-firmware/src/ui/dpi.rs | 314 +++++++++++++--------- firmware/acid-firmware/src/ui/mod.rs | 12 +- 5 files changed, 373 insertions(+), 186 deletions(-) diff --git a/firmware/acid-firmware/.cargo/config.toml b/firmware/acid-firmware/.cargo/config.toml index e823237..b6eef99 100644 --- a/firmware/acid-firmware/.cargo/config.toml +++ b/firmware/acid-firmware/.cargo/config.toml @@ -40,12 +40,12 @@ ACID_COMPOSE_LOCALE = "cs_CZ.UTF-8" build-std = ["alloc", "core"] [patch.crates-io] -esp-backtrace = { git = "https://github.com/Limeth/esp-hal.git", rev = "72e22e2de678297da65a185023309a76aef8a5ca" } -esp-hal = { git = "https://github.com/Limeth/esp-hal.git", rev = "72e22e2de678297da65a185023309a76aef8a5ca" } -esp-storage = { git = "https://github.com/Limeth/esp-hal.git", rev = "72e22e2de678297da65a185023309a76aef8a5ca" } -esp-alloc = { git = "https://github.com/Limeth/esp-hal.git", rev = "72e22e2de678297da65a185023309a76aef8a5ca" } -esp-println = { git = "https://github.com/Limeth/esp-hal.git", rev = "72e22e2de678297da65a185023309a76aef8a5ca" } -esp-radio = { git = "https://github.com/Limeth/esp-hal.git", rev = "72e22e2de678297da65a185023309a76aef8a5ca" } -esp-rtos = { git = "https://github.com/Limeth/esp-hal.git", rev = "72e22e2de678297da65a185023309a76aef8a5ca" } -esp-bootloader-esp-idf = { git = "https://github.com/Limeth/esp-hal.git", rev = "72e22e2de678297da65a185023309a76aef8a5ca" } -esp-sync = { git = "https://github.com/Limeth/esp-hal.git", rev = "72e22e2de678297da65a185023309a76aef8a5ca" } +# esp-backtrace = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" } +# esp-hal = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" } +# esp-storage = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" } +# esp-alloc = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" } +# esp-println = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" } +# esp-radio = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" } +# esp-rtos = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" } +# esp-bootloader-esp-idf = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" } +# esp-sync = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" } diff --git a/firmware/acid-firmware/src/logging.rs b/firmware/acid-firmware/src/logging.rs index eb77df9..0a5c184 100644 --- a/firmware/acid-firmware/src/logging.rs +++ b/firmware/acid-firmware/src/logging.rs @@ -57,10 +57,9 @@ fn with_formatted_log_record( pub mod usb { use super::*; - pub fn setup_logging() -> impl Future { + pub fn setup_logging() { esp_println::logger::init_logger(LOG_LEVEL_FILTER); log::info!("Logger initialized!"); - async {} } } @@ -153,17 +152,7 @@ pub mod uart { loop {} } - pub fn setup_logging( - uart: impl esp_hal::uart::Instance + 'static, - tx: impl PeripheralOutput<'static>, - rx: impl PeripheralInput<'static>, - ) -> impl Future { - let (uart_rx, uart_tx) = Uart::new(uart, Default::default()) - .unwrap() - .with_tx(tx) - .with_rx(rx) - .split(); - + pub fn setup_logging(uart_tx: UartTx<'static, Blocking>) { critical_section::with(|cs| { *ALT_LOGGER_UART.borrow(cs).borrow_mut() = Some(uart_tx); }); @@ -174,7 +163,6 @@ pub mod uart { } info!("Logger initialized!"); - console::run_console(uart_rx.into_async()) } } @@ -187,10 +175,9 @@ pub mod rtt { use panic_rtt_target as _; // Use the RTT panic handler. use rtt_target::ChannelMode; - pub fn setup_logging() -> impl Future { + pub fn setup_logging() { rtt_target::rtt_init_log!(LOG_LEVEL_FILTER, ChannelMode::BlockIfFull); log::info!("Logger initialized!"); - async {} } } diff --git a/firmware/acid-firmware/src/main.rs b/firmware/acid-firmware/src/main.rs index 7a4dfa2..b916c0c 100644 --- a/firmware/acid-firmware/src/main.rs +++ b/firmware/acid-firmware/src/main.rs @@ -21,6 +21,7 @@ use core::cell::RefCell; use core::fmt::Write; use core::sync::atomic::{AtomicBool, Ordering}; +use alloc::alloc::Global; use alloc::boxed::Box; use alloc::collections::vec_deque::VecDeque; use alloc::format; @@ -28,6 +29,7 @@ use alloc::string::String; use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; +use cfg_if::cfg_if; use embassy_embedded_hal::adapter::BlockingAsync; use embassy_embedded_hal::flash::partition::Partition; use embassy_executor::Spawner; @@ -43,6 +45,8 @@ use esp_hal::dma::{ BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig, InternalBurstConfig, }; use esp_hal::efuse::Efuse; +#[cfg(not(feature = "alt-log"))] +use esp_hal::gpio::NoPin; use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull}; use esp_hal::i2c::master::{I2c, I2cAddress}; use esp_hal::interrupt::software::{SoftwareInterrupt, SoftwareInterruptControl}; @@ -57,6 +61,7 @@ use esp_hal::sha::ShaBackend; use esp_hal::spi::master::AnySpi; use esp_hal::system::Stack; use esp_hal::timer::timg::TimerGroup; +use esp_hal::uart::{Uart, UartRx}; use esp_hal::{Blocking, interrupt}; use esp_rtos::embassy::{Executor, InterruptExecutor}; use esp_storage::FlashStorage; @@ -128,11 +133,11 @@ pub static PSRAM_ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty(); static KEYBOARD_REPORT_PROXY: Channel = Channel::new(); static LCD_ENABLED: AtomicBool = AtomicBool::new(false); -/// Used to signal that MCU is ready to submit the framebuffer to the LCD. -static SIGNAL_LCD_SUBMIT: Signal = Signal::new(); +// /// Used to signal that MCU is ready to submit the framebuffer to the LCD. +// static SIGNAL_LCD_SUBMIT: Signal = Signal::new(); -/// Used to signal that the MCU is ready to render the GUI. -static SIGNAL_UI_RENDER: Signal = Signal::new(); +// /// Used to signal that the MCU is ready to render the GUI. +// static SIGNAL_UI_RENDER: Signal = Signal::new(); #[embassy_executor::task] async fn test_bounce_buffers_task( @@ -154,8 +159,13 @@ async fn test_bounce_buffers( const ROWS_PER_WINDOW: usize = 8; let windows_len = HEIGHT / ROWS_PER_WINDOW; let window_size = ROWS_PER_WINDOW * WIDTH * core::mem::size_of::(); + let burst_config = BurstConfig { + internal_memory: InternalBurstConfig::Enabled, + external_memory: ExternalBurstConfig::Size64, + }; let buffer_src = Box::leak(allocate_dma_buffer_in( windows_len * window_size, + burst_config, &PSRAM_ALLOCATOR, )); let buffer_src = bytemuck::cast_slice_mut::(buffer_src); @@ -176,8 +186,8 @@ async fn test_bounce_buffers( }) .collect::>(); for (index, pixel) in buffer_src.iter_mut().enumerate() { - let mut x = (index % 368) as i16 - 120; - let mut y = (index / 368) as i16; + let mut x = (index % WIDTH) as i16 - 120; + let mut y = (index / WIDTH) as i16; if x < 240 { x = core::cmp::min(x, 240 - 1 - x); @@ -197,6 +207,7 @@ async fn test_bounce_buffers( // counter // }); let mut buf = DmaBounce::new( + Global, channel, AnySpi::from(peripheral), st7701s.dpi, @@ -204,7 +215,7 @@ async fn test_bounce_buffers( window_size, BurstConfig { internal_memory: InternalBurstConfig::Enabled, - external_memory: ExternalBurstConfig::Size32, + external_memory: ExternalBurstConfig::Size64, }, false, ); @@ -222,18 +233,31 @@ async fn main(_spawner: Spawner) { .with_psram(PsramConfig { size: PsramSize::AutoDetect, core_clock: Some(SpiTimingConfigCoreClock::SpiTimingConfigCoreClock80m), - flash_frequency: FlashFreq::default(), - ram_frequency: SpiRamFreq::Freq80m, + flash_frequency: FlashFreq::FlashFreq120m, + ram_frequency: SpiRamFreq::Freq120m, }); let peripherals: esp_hal::peripherals::Peripherals = esp_hal::init(config); + #[allow(unused)] + let (uart_rx, uart_tx) = { + #[cfg(feature = "alt-log")] + let (tx, rx) = (peripherals.GPIO12, peripherals.GPIO5); + #[cfg(not(feature = "alt-log"))] + let (tx, rx) = (NoPin, NoPin); + + Uart::new(peripherals.UART2, Default::default()) + .unwrap() + .with_tx(tx) + .with_rx(rx) + .split() + }; + #[cfg(feature = "usb-log")] - let console_task = logging::usb::setup_logging(); + logging::usb::setup_logging(); #[cfg(feature = "alt-log")] - let console_task = - logging::uart::setup_logging(peripherals.UART2, peripherals.GPIO12, peripherals.GPIO5); + logging::uart::setup_logging(uart_tx); #[cfg(feature = "rtt-log")] - let console_task = logging::rtt::setup_logging(); + logging::rtt::setup_logging(); // Use the internal DRAM as the heap. // Memory reclaimed from the esp-idf bootloader. @@ -291,6 +315,7 @@ async fn main(_spawner: Spawner) { let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); esp_rtos::start(timg0.timer0, software_interrupt.software_interrupt0); + // A task executor that is able to handle interrupts, and then return back to executing tasks. static EXECUTOR_CORE_0: StaticCell> = StaticCell::new(); let executor_core_0 = InterruptExecutor::new(software_interrupt.software_interrupt2); let executor_core_0 = EXECUTOR_CORE_0.init(executor_core_0); @@ -298,6 +323,110 @@ async fn main(_spawner: Spawner) { info!("ESP-RTOS started!"); + let main_task_peripherals = MainPeripherals { + uart_rx, + software_interrupt1: software_interrupt.software_interrupt1, + RNG: peripherals.RNG, + ADC1: peripherals.ADC1, + USB0: peripherals.USB0, + FLASH: peripherals.FLASH, + LCD_CAM: peripherals.LCD_CAM, + DMA_CH0: peripherals.DMA_CH0, + DMA_CH2: peripherals.DMA_CH2, + I2C0: peripherals.I2C0, + SPI2: peripherals.SPI2, + CPU_CTRL: peripherals.CPU_CTRL, + GPIO0: gpio0, + GPIO1: peripherals.GPIO1, + GPIO2: peripherals.GPIO2, + GPIO3: peripherals.GPIO3, + GPIO4: peripherals.GPIO4, + #[cfg(not(feature = "alt-log"))] + GPIO5: peripherals.GPIO5, + GPIO6: peripherals.GPIO6, + GPIO7: peripherals.GPIO7, + GPIO8: peripherals.GPIO8, + GPIO9: peripherals.GPIO9, + #[cfg(not(feature = "alt-log"))] + GPIO12: peripherals.GPIO12, + GPIO13: peripherals.GPIO13, + GPIO14: peripherals.GPIO14, + GPIO15: peripherals.GPIO15, + GPIO16: peripherals.GPIO16, + GPIO19: peripherals.GPIO19, + GPIO20: peripherals.GPIO20, + GPIO34: peripherals.GPIO34, + GPIO35: peripherals.GPIO35, + GPIO36: peripherals.GPIO36, + GPIO37: peripherals.GPIO37, + GPIO38: peripherals.GPIO38, + GPIO39: peripherals.GPIO39, + GPIO40: peripherals.GPIO40, + GPIO41: peripherals.GPIO41, + GPIO42: peripherals.GPIO42, + GPIO43: peripherals.GPIO43, + GPIO44: peripherals.GPIO44, + }; + + interrupt_core_0_spawner.must_spawn(main_task(main_task_peripherals)); +} + +/// Peripherals passed to the main task. +#[allow(non_snake_case)] +struct MainPeripherals { + uart_rx: UartRx<'static, Blocking>, + software_interrupt1: SoftwareInterrupt<'static, 1>, + RNG: esp_hal::peripherals::RNG<'static>, + ADC1: esp_hal::peripherals::ADC1<'static>, + USB0: esp_hal::peripherals::USB0<'static>, + FLASH: esp_hal::peripherals::FLASH<'static>, + LCD_CAM: esp_hal::peripherals::LCD_CAM<'static>, + DMA_CH0: esp_hal::peripherals::DMA_CH0<'static>, + DMA_CH2: esp_hal::peripherals::DMA_CH2<'static>, + I2C0: esp_hal::peripherals::I2C0<'static>, + SPI2: esp_hal::peripherals::SPI2<'static>, + CPU_CTRL: esp_hal::peripherals::CPU_CTRL<'static>, + GPIO0: Output<'static>, + GPIO1: esp_hal::peripherals::GPIO1<'static>, + GPIO2: esp_hal::peripherals::GPIO2<'static>, + GPIO3: esp_hal::peripherals::GPIO3<'static>, + GPIO4: esp_hal::peripherals::GPIO4<'static>, + #[cfg(not(feature = "alt-log"))] + GPIO5: esp_hal::peripherals::GPIO5<'static>, + GPIO6: esp_hal::peripherals::GPIO6<'static>, + GPIO7: esp_hal::peripherals::GPIO7<'static>, + GPIO8: esp_hal::peripherals::GPIO8<'static>, + GPIO9: esp_hal::peripherals::GPIO9<'static>, + // GPIO10: esp_hal::peripherals::GPIO10<'static>, + #[cfg(not(feature = "alt-log"))] + GPIO12: esp_hal::peripherals::GPIO12<'static>, + GPIO13: esp_hal::peripherals::GPIO13<'static>, + GPIO14: esp_hal::peripherals::GPIO14<'static>, + GPIO15: esp_hal::peripherals::GPIO15<'static>, + GPIO16: esp_hal::peripherals::GPIO16<'static>, + // GPIO18: esp_hal::peripherals::GPIO18<'static>, + GPIO19: esp_hal::peripherals::GPIO19<'static>, + GPIO20: esp_hal::peripherals::GPIO20<'static>, + // GPIO33: esp_hal::peripherals::GPIO33<'static>, + GPIO34: esp_hal::peripherals::GPIO34<'static>, + GPIO35: esp_hal::peripherals::GPIO35<'static>, + GPIO36: esp_hal::peripherals::GPIO36<'static>, + GPIO37: esp_hal::peripherals::GPIO37<'static>, + GPIO38: esp_hal::peripherals::GPIO38<'static>, + GPIO39: esp_hal::peripherals::GPIO39<'static>, + GPIO40: esp_hal::peripherals::GPIO40<'static>, + GPIO41: esp_hal::peripherals::GPIO41<'static>, + GPIO42: esp_hal::peripherals::GPIO42<'static>, + GPIO43: esp_hal::peripherals::GPIO43<'static>, + GPIO44: esp_hal::peripherals::GPIO44<'static>, + // GPIO45: esp_hal::peripherals::GPIO45<'static>, + // GPIO46: esp_hal::peripherals::GPIO46<'static>, + // GPIO47: esp_hal::peripherals::GPIO47<'static>, + // GPIO48: esp_hal::peripherals::GPIO48<'static>, +} + +#[embassy_executor::task] +async fn main_task(peripherals: MainPeripherals) { // Enable the TRNG source, so `Trng` can be constructed. let _trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1); @@ -453,7 +582,7 @@ async fn main(_spawner: Spawner) { .with_data9(peripherals.GPIO15) .with_data10(peripherals.GPIO16) // Red - .with_data11(gpio0) + .with_data11(peripherals.GPIO0) .with_data12(peripherals.GPIO1) .with_data13(peripherals.GPIO2) .with_data14(peripherals.GPIO3) @@ -469,16 +598,16 @@ async fn main(_spawner: Spawner) { info!("ST7701S-based LCD display initialized!"); - interrupt_core_0_spawner.must_spawn(test_bounce_buffers_task( - peripherals.DMA_CH0, - peripherals.SPI2, - st7701s, - )); + // interrupt_core_0_spawner.must_spawn(test_bounce_buffers_task( + // peripherals.DMA_CH0, + // peripherals.SPI2, + // st7701s, + // )); // let lcd_task = test_bounce_buffers(peripherals.DMA_CH0, peripherals.SPI2, st7701s); // let _ = lcd_task.await; - return; + // return; // RMK config let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]); @@ -561,11 +690,10 @@ async fn main(_spawner: Spawner) { static FRAMEBUFFER: StaticCell = StaticCell::new(); let framebuffer = FRAMEBUFFER.init(Framebuffer::new( - 368, - // It is likely due to DMA transfer requirements. - // The size of each chunk transferred via DMA must be a multiple of 32 bytes. - // (360 * size_of()) % 32 == 16 - // Make it so that the renderer renders into the smallest transmissible region of memory. + peripherals.DMA_CH0, + peripherals.SPI2.into(), + st7701s.dpi, + // The burst config (16/32/64) doesn't seem to affect the alignment of the row size. // // | | ( displayed range ) | // | [ pad ] [ pad ] @@ -577,7 +705,10 @@ async fn main(_spawner: Spawner) { // TODO: Compute the appropriate ranges to pass to the renderer and DPI peripheral. // The renderer should pass the size of the `pad`ding to the GUI is parameters, // to align the content to the displayed range. + 368, 960, + 16, + false, )); info!("Framebuffer created!"); @@ -590,13 +721,13 @@ async fn main(_spawner: Spawner) { let second_core_stack = SECOND_CORE_STACK.init(Stack::new()); esp_rtos::start_second_core( peripherals.CPU_CTRL, - // software_interrupt.software_interrupt0, - software_interrupt.software_interrupt1, + // peripherals.software_interrupt0, + peripherals.software_interrupt1, second_core_stack, move || { // static EXECUTOR: StaticCell> = StaticCell::new(); // let exec = EXECUTOR.init(InterruptExecutor::new( - // software_interrupt.software_interrupt2, + // peripherals.software_interrupt2, // )); // let spawner = exec.start(Priority::Priority3); // spawner.must_spawn(run_renderer_task()); @@ -627,6 +758,13 @@ async fn main(_spawner: Spawner) { run_alloc_stats_reporter(), // We currently send the framebuffer data using the main core, which does not seem to slow // down the rest of the tasks too much. + // async { + // warn!("Waiting..."); + // Timer::after_secs(3).await; + // warn!("Waited."); + // framebuffer.bounce_buffers.send().await; + // }, + framebuffer.bounce_buffers.send(), // ui::dpi::run_lcd(st7701s, framebuffer), // lcd_task, run_devices! ( @@ -644,7 +782,7 @@ async fn main(_spawner: Spawner) { ), create_hid_report_interceptor(), user_controller.event_loop(), - console_task + console::run_console(peripherals.uart_rx.into_async()) ] .await; } diff --git a/firmware/acid-firmware/src/ui/dpi.rs b/firmware/acid-firmware/src/ui/dpi.rs index 398b889..6eb2ec0 100644 --- a/firmware/acid-firmware/src/ui/dpi.rs +++ b/firmware/acid-firmware/src/ui/dpi.rs @@ -20,7 +20,7 @@ use esp_hal::{ dma::{ self, AnyGdmaChannel, BufView, BurstConfig, DmaChannel, DmaChannelConvert, DmaDescriptor, DmaDescriptorFlags, DmaEligible, DmaRxStreamBuf, DmaTxBuf, DmaTxBuffer, DmaTxInterrupt, - ExternalBurstConfig, Mem2Mem, SimpleMem2MemTransfer, + ExternalBurstConfig, InternalBurstConfig, Mem2Mem, SimpleMem2MemTransfer, }, dma_descriptors, handler, interrupt::{self, Priority}, @@ -38,10 +38,7 @@ use rmk::{ join_all, }; -use crate::{ - PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, peripherals::st7701s::St7701s, - util::DurationExt, -}; +use crate::{PSRAM_ALLOCATOR, peripherals::st7701s::St7701s, util::DurationExt}; /// THIS IS TAKEN FROM https://github.com/esp-rs/esp-hal/blob/main/esp-hal/src/soc/esp32s3/mod.rs /// Write back a specific range of data in the cache. @@ -115,6 +112,7 @@ pub struct DmaBounce { impl DmaBounce { pub fn new( + allocator: impl Allocator + Copy + 'static, channel: DMA_CH0<'static>, peripheral_src: AnySpi<'static>, peripheral_dst: Dpi<'static, Blocking>, @@ -129,9 +127,28 @@ impl DmaBounce { "the size of a source buffer must be a multiple of the window size ({window_size} bytes), but it is {len} bytes large", len = buffer_src.len() ); + + // Conservative alignment. Maxiumum of the cartesian product of [tx, rx] × [internal, external]. + let alignment = burst_config.min_compatible_alignment(); + + assert_eq!( + buffer_src.as_ptr() as usize % alignment, + 0, + "the source buffer must be sufficiently aligned to {alignment} bytes for the burst config", + ); + assert_eq!( + window_size % alignment, + 0, + "the size of the source buffer must be sufficiently aligned to {alignment} bytes for the burst config", + ); + let windows_len = buffer_src.len() / window_size; - let bounce_buffer_dst = Box::leak(allocate_dma_buffer_in(window_size, Global)); - let bounce_buffer_src = Box::leak(allocate_dma_buffer_in(window_size, Global)); + // 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. @@ -343,7 +360,7 @@ impl DmaBounce { if let Some(buffer) = buffer { assert!( buffer.is_empty(), - "a buffer of an incompatible length was asssigned to a descriptor set" + "a buffer of an incompatible length was assigned to a descriptor set" ); } } @@ -383,8 +400,10 @@ impl DmaBounce { let buffer_src_window = &mut self.buffer_src[self.window_index_next * self.window_size..][..self.window_size]; - Self::linear_descriptors_prepare(self.src_descs, Some(buffer_src_window), |desc| { - // desc.reset_for_tx(desc.next.is_null()); + 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( @@ -401,8 +420,11 @@ impl DmaBounce { // 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 = unsafe { &mut *(self.bounce_dst_descs as *mut _) }; - let src_descs = unsafe { &mut *(self.src_descs as *mut _) }; + 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) @@ -411,7 +433,6 @@ impl DmaBounce { .start_transfer(&mut self.bounce_buffer_dst, buffer_src_window) .unwrap(); - // INBOUND_TRANSFER_FINISHED.wait().await; transfer.wait_async().await.unwrap(); } @@ -428,8 +449,7 @@ impl DmaBounce { self.window_index_next = self.window_index_next % self.windows_len; } - pub async fn send(&mut self) // -> DpiTransfer<'static, DmaTxBounceBuf, Blocking> - { + pub async fn send(&mut self) { Self::enable_interrupts(); // Receive the first window, so that the outbound transfer can read valid data. @@ -448,26 +468,60 @@ impl DmaBounce { let mut windows_skipped_total = 0; loop { + // warn!( + // "Receiving window: {} {}", + // self.window_index_next, self.frame_index_next + // ); self.receive_window().await; + // warn!( + // "Window received: {} {}", + // self.window_index_next, self.frame_index_next + // ); let windows_skipped = WINDOWS_SKIPPED .wait() - // .with_timeout(Duration::from_secs(1)) - .await; - // .unwrap_or_else(|_| { - // error!("Timed out while waiting for skipped windows"); - // 0 - // }); + .with_timeout(Duration::from_millis(100)) + .await + .unwrap_or_else(|_| { + // error!("Timed out when waiting for skipped windows."); + 0 // TODO: This should be -1 to repeat the same window. + }); + + // let windows_skipped = match windows_skipped { + // Ok(windows_skipped) => windows_skipped, + // Err(_) => { + // warn!( + // "Waiting for skipped windows timed out. Transfer done: {}", + // transfer.is_done() + // ); + // if transfer.is_done() { + // let (result, _, _) = transfer.wait(); + // panic!("Transfer result: {result:?}"); + // } + // 0 + // } + // }; if windows_skipped > 0 { self.increase_window_counter(windows_skipped); windows_skipped_total += windows_skipped; - error!( - "Skipped {windows_skipped} windows. Windows skipped per frame: {:.2}%", - 100.0 * windows_skipped_total as f32 / (self.frame_index_next + 1) as f32 - ); + // error!( + // "Skipped {windows_skipped} windows. Windows skipped per frame: {:.2}%", + // 100.0 * windows_skipped_total as f32 / (self.frame_index_next + 1) as f32 + // ); } - if !self.cyclic && self.window_index_next == 1 { + // warn!( + // "X: {} {} {}", + // windows_skipped, self.window_index_next, self.frame_index_next + // ); + + if !self.cyclic && (self.window_index_next == 1 || transfer.is_done()) { + if self.window_index_next > 1 { + self.increase_window_counter(self.windows_len - self.window_index_next + 1); + } else if self.window_index_next == 0 { + self.increase_window_counter(1); + } + // TODO: Investigate why the DPI transfer isn't done at this point. // The `DpiTransfer::wait()` below takes 0.001039 s. // Perhaps it's the minimum screen refresh period? @@ -497,33 +551,15 @@ impl DmaBounce { .unwrap_or_else(|(error, _, _)| { panic!("failed to begin the transmission of a frame: {error:?}"); }); + + FRAMES_SKIPPED.signal( + FRAMES_SKIPPED + .try_take() + .map(|frames_skipped| frames_skipped + 1) + .unwrap_or_default(), + ); } } - - // loop { - // // BOUNCE_BUFFER_SENT.receive().await; - // warn!("Iteration. Done = {}", transfer.is_done()); - // let receive_window = self.receive_window().fuse(); - // pin_mut!(receive_window); - // // let mut send_buffer = BOUNCE_BUFFER_SENT.wait().fuse(); - // let mut send_buffer = BOUNCE_BUFFER_SENT.receive().fuse(); - // let window_received_first = rmk::futures::select_biased! { - // () = receive_window => Ok(()), - // windows_sent = send_buffer => Err(windows_sent), - // }; - - // match window_received_first { - // Ok(()) => { - // send_buffer.await; - // } - // Err(windows_sent) => { - // error!("Sent {windows_sent} windows before a window could be received."); - // receive_window.await; - // } - // } - // } - - // transfer } fn get_dma_tx_buffer(&mut self) -> DmaTxBounceBuf { @@ -568,6 +604,8 @@ unsafe impl DmaTxBuffer for DmaTxBounceBuf { } } +/// Intended to be listened on by the renderer, to synchronize the refresh frequency with. +pub static FRAMES_SKIPPED: Signal = Signal::new(); static WINDOWS_SKIPPED: Signal = Signal::new(); // static INBOUND_TRANSFER_FINISHED: Signal = Signal::new(); @@ -580,11 +618,12 @@ fn dma_outbound_interrupt_handler() { // Clear the bit by writing 1 to the clear bits. interrupt.clr().write(|w| w.out_eof().bit(true)); - let windows_skipped = WINDOWS_SKIPPED - .try_take() - .map(|windows_skipped| windows_skipped + 1) - .unwrap_or_default(); - WINDOWS_SKIPPED.signal(windows_skipped); + WINDOWS_SKIPPED.signal( + WINDOWS_SKIPPED + .try_take() + .map(|windows_skipped| windows_skipped + 1) + .unwrap_or_default(), + ); } } @@ -607,69 +646,68 @@ fn dma_outbound_interrupt_handler() { // } // } -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; +// 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, - }; +// // 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; - } +// // 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); +// let result; +// let dma_buf; +// (result, st7701s.dpi, dma_buf) = transfer.wait(); +// framebuffer.dma_buf = Some(dma_buf); - SIGNAL_UI_RENDER.signal(()); +// SIGNAL_UI_RENDER.signal(()); - if let Err(error) = result { - error!( - "An error occurred while transferring framebuffer to the LCD display: {error:?}" - ); - } - } -} - -pub struct Framebuffer { - pub width: u32, - pub height: u32, - pub dma_buf: Option, -} +// 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(len: usize, alloc: A) -> Box<[u8], A> { - const DMA_ALIGNMENT: usize = 32; +pub fn allocate_dma_buffer_in( + 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 % DMA_ALIGNMENT, + len % alignment, 0, - "the size of a DMA buffer must be a multiple of {DMA_ALIGNMENT} bytes, but it is {len} bytes large" + "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 @@ -678,40 +716,58 @@ pub fn allocate_dma_buffer_in(len: usize, alloc: A) -> Box<[u8], A // That is ensured by the `assert_eq` preceding this block. unsafe { let raw = alloc - .allocate_zeroed(Layout::from_size_align(len, DMA_ALIGNMENT).unwrap()) + .allocate_zeroed(Layout::from_size_align(len, alignment).unwrap()) .expect("failed to allocate a DMA buffer"); Box::from_raw_in(raw.as_ptr(), alloc) } } -impl Framebuffer { - pub fn new(width: u32, height: u32) -> Self { - let buffer_len = width as usize * height as usize * core::mem::size_of::(); - let buffer = allocate_dma_buffer_in(buffer_len, &PSRAM_ALLOCATOR); - let burst_config: BurstConfig = ExternalBurstConfig::Size16.into(); +pub struct Framebuffer { + pub width: u32, + pub height: u32, + pub bounce_buffers: DmaBounce, +} - info!( - "PSRAM SPI burst config: max_compatible_chunk_size={}", - burst_config.max_compatible_chunk_size() +impl Framebuffer { + pub fn new( + channel: DMA_CH0<'static>, + peripheral_src: AnySpi<'static>, + peripheral_dst: Dpi<'static, Blocking>, + width: u32, + height: u32, + rows_per_window: usize, + cyclic: bool, + ) -> Self { + let buffer_size = width as usize * height as usize * core::mem::size_of::(); + let window_size = rows_per_window * width as usize * core::mem::size_of::(); + let burst_config = BurstConfig { + internal_memory: InternalBurstConfig::Enabled, + external_memory: ExternalBurstConfig::Size64, + }; + let psram_buffer = Box::leak(allocate_dma_buffer_in( + buffer_size, + burst_config, + &PSRAM_ALLOCATOR, + )); + let bounce_buffers = DmaBounce::new( + Global, + channel, + peripheral_src, + peripheral_dst, + psram_buffer, + window_size, + burst_config, + cyclic, ); - let dma_buf_descs_len = esp_hal::dma::descriptor_count( - buffer_len, - burst_config.max_compatible_chunk_size(), - false, - ); - // Descriptors are initialized by `DmaTxBuf::new`. - let dma_buf_descs = vec![DmaDescriptor::EMPTY; dma_buf_descs_len].into_boxed_slice(); - // We just leak the buffers. - let dma_buf = DmaTxBuf::new(Box::leak(dma_buf_descs), Box::leak(buffer)).unwrap(); Self { width, height, - dma_buf: Some(dma_buf), + bounce_buffers, } } pub fn as_target_pixels(&mut self) -> &mut [Rgb565Pixel] { - bytemuck::cast_slice_mut::<_, Rgb565Pixel>(self.dma_buf.as_mut().unwrap().as_mut_slice()) + bytemuck::cast_slice_mut::<_, Rgb565Pixel>(self.bounce_buffers.buffer_src) } } diff --git a/firmware/acid-firmware/src/ui/mod.rs b/firmware/acid-firmware/src/ui/mod.rs index d2011c7..06f1307 100644 --- a/firmware/acid-firmware/src/ui/mod.rs +++ b/firmware/acid-firmware/src/ui/mod.rs @@ -29,7 +29,7 @@ use spectre_api_sys::{ #[cfg(feature = "limit-fps")] use crate::FRAME_DURATION_MIN; use crate::{ - PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, + PSRAM_ALLOCATOR, db::{ AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers, PartitionAcid, ReadTransactionExt, @@ -38,6 +38,7 @@ use crate::{ proxy::OUTPUT_STRING_CHANNEL, ui::{ backend::SlintBackend, + dpi::FRAMES_SKIPPED, messages::{ CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUserSites, CallbackMessageUsers, LoginResult, @@ -373,10 +374,15 @@ impl State { loop { slint::run_event_loop().unwrap(); - SIGNAL_LCD_SUBMIT.signal(()); + // SIGNAL_LCD_SUBMIT.signal(()); #[cfg(feature = "limit-fps")] embassy_time::Timer::after(FRAME_DURATION_MIN).await; - SIGNAL_UI_RENDER.wait().await; + let frames_skipped = FRAMES_SKIPPED.wait().await; + + if frames_skipped > 0 { + error!("Renderer missed {frames_skipped} frames."); + } + // SIGNAL_UI_RENDER.wait().await; } #[expect(unreachable_code)]