WIP3: Glitchy bounce buffering
This commit is contained in:
parent
b8e3987139
commit
7e1f0d1b68
|
|
@ -60,6 +60,7 @@ use esp_hal::timer::timg::TimerGroup;
|
|||
use esp_hal::{Blocking, interrupt};
|
||||
use esp_rtos::embassy::{Executor, InterruptExecutor};
|
||||
use esp_storage::FlashStorage;
|
||||
use i_slint_core::software_renderer::TargetPixel;
|
||||
use indoc::writedoc;
|
||||
use itertools::Itertools;
|
||||
use log::{error, info, warn};
|
||||
|
|
@ -83,7 +84,7 @@ use crate::matrix::IoeMatrix;
|
|||
use crate::peripherals::st7701s::St7701s;
|
||||
use crate::proxy::create_hid_report_interceptor;
|
||||
use crate::ui::backend::{FramebufferPtr, SlintBackend};
|
||||
use crate::ui::dpi::{DmaBounce, DmaTxBounceBuf, Framebuffer};
|
||||
use crate::ui::dpi::{DmaBounce, DmaTxBounceBuf, Framebuffer, allocate_dma_buffer_in};
|
||||
use crate::vial::{
|
||||
CustomKeycodes, VIAL_KEYBOARD_DEF, VIAL_KEYBOARD_ID, VIAL_KEYBOARD_NAME, VIAL_PRODUCT_ID,
|
||||
VIAL_VENDOR_ID,
|
||||
|
|
@ -133,20 +134,53 @@ static SIGNAL_LCD_SUBMIT: Signal<CriticalSectionRawMutex, ()> = Signal::new();
|
|||
/// Used to signal that the MCU is ready to render the GUI.
|
||||
static SIGNAL_UI_RENDER: Signal<CriticalSectionRawMutex, ()> = Signal::new();
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn test_bounce_buffers_task(
|
||||
channel: DMA_CH0<'static>,
|
||||
peripheral: SPI2<'static>,
|
||||
st7701s: St7701s<'static, Blocking>,
|
||||
) {
|
||||
test_bounce_buffers(channel, peripheral, st7701s).await;
|
||||
}
|
||||
|
||||
async fn test_bounce_buffers(
|
||||
channel: DMA_CH0<'static>,
|
||||
peripheral: SPI2<'static>,
|
||||
st7701s: St7701s<'static, Blocking>,
|
||||
) -> DpiTransfer<'static, DmaTxBounceBuf, Blocking> {
|
||||
error!("TEST BOUNCE BUFFERS SECTION ENTERED");
|
||||
let windows_len = 2;
|
||||
let window_size = 368 * core::mem::size_of::<u16>();
|
||||
let buffer_src = Box::leak(vec![0_u8; windows_len * window_size].into_boxed_slice());
|
||||
let mut counter: u8 = 0;
|
||||
buffer_src.fill_with(|| {
|
||||
counter = counter.wrapping_add(1);
|
||||
counter
|
||||
});
|
||||
let windows_len = 960;
|
||||
let window_size = 8 * 368 * core::mem::size_of::<u16>();
|
||||
let buffer_src = Box::leak(allocate_dma_buffer_in(
|
||||
windows_len * window_size,
|
||||
&PSRAM_ALLOCATOR,
|
||||
));
|
||||
let buffer_src = bytemuck::cast_slice_mut::<u8, Rgb565Pixel>(buffer_src);
|
||||
let colors = (0..120_u8)
|
||||
.rev()
|
||||
.map(|val| Rgb565Pixel::from_rgb(val * 2, val * 2, val * 2))
|
||||
.collect::<Vec<_>>();
|
||||
for (index, pixel) in buffer_src.iter_mut().enumerate() {
|
||||
let mut x = (index % 368) as i16 - 120;
|
||||
let mut y = (index / 368) as i16;
|
||||
|
||||
if x < 240 {
|
||||
x = core::cmp::min(x, 240 - 1 - x);
|
||||
y = core::cmp::min(y, 960 - 1 - y);
|
||||
let min = core::cmp::min(x, y);
|
||||
|
||||
*pixel = colors[min as usize % colors.len()].clone();
|
||||
continue;
|
||||
}
|
||||
|
||||
*pixel = Rgb565Pixel::default();
|
||||
}
|
||||
let buffer_src = bytemuck::cast_slice_mut::<Rgb565Pixel, u8>(buffer_src);
|
||||
// let mut counter: u8 = 0;
|
||||
// buffer_src.fill_with(|| {
|
||||
// counter = counter.wrapping_add(1);
|
||||
// counter
|
||||
// });
|
||||
let mut buf = DmaBounce::new(
|
||||
channel,
|
||||
AnySpi::from(peripheral),
|
||||
|
|
@ -421,9 +455,15 @@ async fn main(_spawner: Spawner) {
|
|||
|
||||
info!("ST7701S-based LCD display initialized!");
|
||||
|
||||
let lcd_task = test_bounce_buffers(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.await;
|
||||
// let lcd_task = test_bounce_buffers(peripherals.DMA_CH0, peripherals.SPI2, st7701s);
|
||||
|
||||
// let _ = lcd_task.await;
|
||||
return;
|
||||
|
||||
// RMK config
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use core::{
|
||||
alloc::Layout,
|
||||
pin::Pin,
|
||||
sync::atomic::{self, AtomicBool},
|
||||
sync::atomic::{self, AtomicBool, AtomicU32, AtomicUsize},
|
||||
};
|
||||
|
||||
use alloc::{
|
||||
|
|
@ -9,7 +9,10 @@ use alloc::{
|
|||
boxed::Box,
|
||||
vec,
|
||||
};
|
||||
use embassy_sync::channel::Channel;
|
||||
use embassy_sync::{
|
||||
channel::{Channel, TrySendError},
|
||||
signal::Signal,
|
||||
};
|
||||
use embassy_time::Timer;
|
||||
use esp_alloc::MemoryCapability;
|
||||
use esp_hal::{
|
||||
|
|
@ -30,6 +33,10 @@ use esp_sync::RawMutex;
|
|||
use i_slint_core::software_renderer::Rgb565Pixel;
|
||||
use indoc::{formatdoc, indoc};
|
||||
use log::{error, info, warn};
|
||||
use rmk::{
|
||||
futures::{FutureExt, pin_mut},
|
||||
join_all,
|
||||
};
|
||||
|
||||
use crate::{PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, peripherals::st7701s::St7701s};
|
||||
|
||||
|
|
@ -93,6 +100,9 @@ pub struct DmaBounce {
|
|||
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],
|
||||
// The index of the next window about to be received into the destination bounce buffer.
|
||||
window_index_next: usize,
|
||||
frame_index_next: usize,
|
||||
}
|
||||
|
||||
impl DmaBounce {
|
||||
|
|
@ -143,6 +153,8 @@ impl DmaBounce {
|
|||
src_descs,
|
||||
bounce_dst_descs,
|
||||
bounce_src_descs,
|
||||
window_index_next: 0,
|
||||
frame_index_next: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -256,17 +268,32 @@ impl DmaBounce {
|
|||
}
|
||||
|
||||
fn enable_interrupts() {
|
||||
// TODO: Get from self.channel
|
||||
let channel_number = 2;
|
||||
let interrupt = esp_hal::peripherals::Interrupt::DMA_OUT_CH2;
|
||||
|
||||
// Enable interrupts for the peripheral
|
||||
interrupt::enable(
|
||||
esp_hal::peripherals::Interrupt::DMA_OUT_CH0,
|
||||
dma_interrupt_handler.priority(),
|
||||
)
|
||||
.unwrap();
|
||||
interrupt::enable(
|
||||
esp_hal::peripherals::Interrupt::DMA_IN_CH0,
|
||||
dma_interrupt_handler.priority(),
|
||||
)
|
||||
.unwrap();
|
||||
interrupt::enable(interrupt, dma_interrupt_handler.priority()).unwrap();
|
||||
// interrupt::enable(
|
||||
// esp_hal::peripherals::Interrupt::DMA_OUT_CH0,
|
||||
// dma_interrupt_handler.priority(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// interrupt::enable(
|
||||
// esp_hal::peripherals::Interrupt::DMA_IN_CH0,
|
||||
// dma_interrupt_handler.priority(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// interrupt::enable(
|
||||
// esp_hal::peripherals::Interrupt::DMA_OUT_CH2,
|
||||
// dma_interrupt_handler.priority(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// interrupt::enable(
|
||||
// esp_hal::peripherals::Interrupt::DMA_IN_CH2,
|
||||
// dma_interrupt_handler.priority(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// interrupt::enable(
|
||||
// esp_hal::peripherals::Interrupt::SPI2_DMA,
|
||||
// dma_interrupt_handler.priority(),
|
||||
|
|
@ -275,14 +302,23 @@ impl DmaBounce {
|
|||
|
||||
// Bind the handler
|
||||
unsafe {
|
||||
interrupt::bind_interrupt(
|
||||
esp_hal::peripherals::Interrupt::DMA_OUT_CH0,
|
||||
dma_interrupt_handler.handler(),
|
||||
);
|
||||
interrupt::bind_interrupt(
|
||||
esp_hal::peripherals::Interrupt::DMA_IN_CH0,
|
||||
dma_interrupt_handler.handler(),
|
||||
);
|
||||
interrupt::bind_interrupt(interrupt, dma_interrupt_handler.handler());
|
||||
// interrupt::bind_interrupt(
|
||||
// esp_hal::peripherals::Interrupt::DMA_OUT_CH0,
|
||||
// dma_interrupt_handler.handler(),
|
||||
// );
|
||||
// interrupt::bind_interrupt(
|
||||
// esp_hal::peripherals::Interrupt::DMA_IN_CH0,
|
||||
// dma_interrupt_handler.handler(),
|
||||
// );
|
||||
// interrupt::bind_interrupt(
|
||||
// esp_hal::peripherals::Interrupt::DMA_OUT_CH2,
|
||||
// dma_interrupt_handler.handler(),
|
||||
// );
|
||||
// interrupt::bind_interrupt(
|
||||
// esp_hal::peripherals::Interrupt::DMA_IN_CH2,
|
||||
// dma_interrupt_handler.handler(),
|
||||
// );
|
||||
// interrupt::bind_interrupt(
|
||||
// esp_hal::peripherals::Interrupt::SPI2_DMA,
|
||||
// dma_interrupt_handler.handler(),
|
||||
|
|
@ -290,26 +326,25 @@ impl DmaBounce {
|
|||
}
|
||||
|
||||
// Enable interrupts in the peripheral.
|
||||
let channel_number = 0; // TODO: Get from self.channel
|
||||
DMA::regs()
|
||||
.ch(channel_number)
|
||||
.out_int()
|
||||
.ena()
|
||||
.modify(|_, w| {
|
||||
w.out_total_eof().bit(true);
|
||||
// w.out_total_eof().bit(true);
|
||||
w.out_eof().bit(true);
|
||||
w.out_done().bit(true);
|
||||
w
|
||||
});
|
||||
DMA::regs()
|
||||
.ch(channel_number)
|
||||
.in_int()
|
||||
.ena()
|
||||
.modify(|_, w| {
|
||||
w.in_suc_eof().bit(true);
|
||||
w.in_done().bit(true);
|
||||
// w.out_done().bit(true);
|
||||
w
|
||||
});
|
||||
// DMA::regs()
|
||||
// .ch(channel_number)
|
||||
// .in_int()
|
||||
// .ena()
|
||||
// .modify(|_, w| {
|
||||
// w.in_suc_eof().bit(true);
|
||||
// w.in_done().bit(true);
|
||||
// w
|
||||
// });
|
||||
// SPI2::regs().dma_int_ena().modify(|_, w| {
|
||||
// w.slv_rd_dma_done().bit(true);
|
||||
// w.slv_wr_dma_done().bit(true);
|
||||
|
|
@ -320,7 +355,8 @@ impl DmaBounce {
|
|||
}
|
||||
|
||||
fn print_regs() {
|
||||
let channel_number = 0; // TODO: Get from self.channel
|
||||
// TODO: Get from self.channel
|
||||
for channel_number in [0, 2] {
|
||||
let out_int_raw = DMA::regs()
|
||||
.ch(channel_number as usize)
|
||||
.out_int()
|
||||
|
|
@ -329,7 +365,7 @@ impl DmaBounce {
|
|||
let in_int_raw = DMA::regs().ch(channel_number as usize).in_int().st().read();
|
||||
log::error!(
|
||||
indoc! {"
|
||||
int_raw:
|
||||
int_raw[{channel_number}]:
|
||||
flag: {flag}
|
||||
out:
|
||||
total_eof: {out_total_eof}
|
||||
|
|
@ -339,6 +375,7 @@ impl DmaBounce {
|
|||
suc_eof: {in_suc_eof}
|
||||
done: {in_done}
|
||||
"},
|
||||
channel_number = channel_number,
|
||||
flag = FLAG.load(atomic::Ordering::SeqCst),
|
||||
out_total_eof = out_int_raw.out_total_eof().bit_is_set(),
|
||||
out_eof = out_int_raw.out_eof().bit_is_set(),
|
||||
|
|
@ -346,13 +383,77 @@ impl DmaBounce {
|
|||
in_suc_eof = in_int_raw.in_suc_eof().bit_is_set(),
|
||||
in_done = in_int_raw.in_done().bit_is_set(),
|
||||
);
|
||||
error!("int_raw_msg: 0x{:08x?}", INT_CHANNEL.try_receive());
|
||||
error!(
|
||||
"int_raw_msg[{channel_number}]: 0x{:08x?}",
|
||||
INT_CHANNEL.try_receive()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 =
|
||||
&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), |_| {});
|
||||
// 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(&mut *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 = unsafe { &mut *(self.bounce_dst_descs as *mut _) };
|
||||
let src_descs = unsafe { &mut *(self.src_descs as *mut _) };
|
||||
let mut mem2mem = Mem2Mem::new(self.channel.reborrow(), self.peripheral_src.reborrow())
|
||||
.with_descriptors(bounce_dst_descs, src_descs, self.burst_config)
|
||||
.unwrap();
|
||||
let transfer = mem2mem
|
||||
.start_transfer(&mut self.bounce_buffer_dst, buffer_src_window)
|
||||
.unwrap();
|
||||
|
||||
transfer.wait().unwrap();
|
||||
}
|
||||
|
||||
// TODO: Get rid of this!
|
||||
// unsafe {
|
||||
// cache_invalidate_addr(
|
||||
// self.bounce_buffer_dst.as_ptr() as u32,
|
||||
// self.bounce_buffer_dst.len() as u32,
|
||||
// );
|
||||
// }
|
||||
// assert_eq!(self.bounce_buffer_dst, buffer_src_window);
|
||||
|
||||
self.increase_window_counter(1);
|
||||
}
|
||||
|
||||
fn increase_window_counter(&mut self, windows: usize) {
|
||||
if windows % 2 == 1 {
|
||||
core::mem::swap(&mut self.bounce_buffer_dst, &mut self.bounce_buffer_src);
|
||||
}
|
||||
|
||||
self.window_index_next = self.window_index_next + windows;
|
||||
self.frame_index_next += self.window_index_next / self.windows_len;
|
||||
self.window_index_next = self.window_index_next % self.windows_len;
|
||||
}
|
||||
|
||||
pub async fn send(&mut self) // -> DpiTransfer<'static, DmaTxBounceBuf, Blocking>
|
||||
{
|
||||
Self::enable_interrupts();
|
||||
|
||||
// Receive the first window, so that the outbound transfer can read valid data.
|
||||
self.receive_window().await;
|
||||
|
||||
let mut dma_tx_buffer = self.get_dma_tx_buffer();
|
||||
let transfer = match self
|
||||
.peripheral_dst
|
||||
|
|
@ -381,57 +482,46 @@ impl DmaBounce {
|
|||
}
|
||||
};
|
||||
|
||||
for window_index in 0..self.windows_len {
|
||||
// Descriptors are initialized by `DmaTxBuf::new`.
|
||||
let buffer_src_window =
|
||||
&mut self.buffer_src[window_index * self.window_size..][..self.window_size];
|
||||
|
||||
Self::linear_descriptors_prepare(self.src_descs, Some(buffer_src_window), |_| {});
|
||||
// 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(&mut *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 = unsafe { &mut *(self.bounce_dst_descs as *mut _) };
|
||||
let src_descs = unsafe { &mut *(self.src_descs as *mut _) };
|
||||
let mut mem2mem =
|
||||
Mem2Mem::new(self.channel.reborrow(), self.peripheral_src.reborrow())
|
||||
.with_descriptors(bounce_dst_descs, src_descs, self.burst_config)
|
||||
.unwrap();
|
||||
let transfer = mem2mem
|
||||
.start_transfer(&mut self.bounce_buffer_dst, buffer_src_window)
|
||||
.unwrap();
|
||||
|
||||
Self::print_regs();
|
||||
transfer.wait().unwrap();
|
||||
}
|
||||
|
||||
Self::print_regs();
|
||||
|
||||
// TODO: Get rid of this!
|
||||
unsafe {
|
||||
cache_invalidate_addr(
|
||||
self.bounce_buffer_dst.as_ptr() as u32,
|
||||
self.bounce_buffer_dst.len() as u32,
|
||||
);
|
||||
}
|
||||
assert_eq!(self.bounce_buffer_dst, buffer_src_window);
|
||||
}
|
||||
let mut windows_skipped_total = 0;
|
||||
|
||||
loop {
|
||||
warn!("Still sending data to DPI? {}", !transfer.is_done());
|
||||
Timer::after_secs(10).await;
|
||||
// warn!("Iteration. Done = {}", transfer.is_done());
|
||||
self.receive_window().await;
|
||||
// let windows_sent = BOUNCE_BUFFER_SENT.receive().await;
|
||||
let windows_skipped = WINDOWS_SKIPPED.wait().await;
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
@ -480,14 +570,41 @@ unsafe impl DmaTxBuffer for DmaTxBounceBuf {
|
|||
|
||||
static INT_CHANNEL: Channel<RawMutex, u32, 128> = Channel::new();
|
||||
static FLAG: AtomicBool = AtomicBool::new(false);
|
||||
// static WINDOWS_SENT: AtomicU32 = AtomicU32::new(0);
|
||||
// static BOUNCE_BUFFER_SENT: Channel<RawMutex, usize, 1> = Channel::new();
|
||||
static WINDOWS_SKIPPED: Signal<RawMutex, usize> = Signal::new();
|
||||
|
||||
#[handler(priority = Priority::Priority3)]
|
||||
#[ram] // Improves performance.
|
||||
fn dma_interrupt_handler() {
|
||||
FLAG.store(true, atomic::Ordering::SeqCst);
|
||||
let int_raw = DMA::regs().ch(0).out_int().raw().read();
|
||||
let interrupt = DMA::regs().ch(2).out_int();
|
||||
let bounce_buffer_processed = interrupt.st().read().out_eof().bit_is_set();
|
||||
if bounce_buffer_processed {
|
||||
// Clear the bit by writing 1 to the clear bits.
|
||||
interrupt.clr().write(|w| w.out_eof().bit(true));
|
||||
|
||||
INT_CHANNEL.try_send(int_raw.bits()).unwrap();
|
||||
let windows_skipped = WINDOWS_SKIPPED
|
||||
.try_take()
|
||||
.map(|windows_skipped| windows_skipped + 1)
|
||||
.unwrap_or_default();
|
||||
WINDOWS_SKIPPED.signal(windows_skipped);
|
||||
// let value = BOUNCE_BUFFER_SENT.try_receive().ok().unwrap_or_default() + 1;
|
||||
// BOUNCE_BUFFER_SENT.try_send(value).expect(
|
||||
// "failed to send bounce buffer signal, because the previous one wasn't processed yet",
|
||||
// );
|
||||
// let value = WINDOWS_SENT.fetch_add(1, atomic::Ordering::SeqCst);
|
||||
// warn!("inner int {value}");
|
||||
}
|
||||
// FLAG.store(true, atomic::Ordering::SeqCst);
|
||||
// let int_raw = DMA::regs()
|
||||
// .ch(2)
|
||||
// .out_int()
|
||||
// .st()
|
||||
// .read()
|
||||
// .out_eof()
|
||||
// .bit_is_set();
|
||||
|
||||
// INT_CHANNEL.try_send(int_raw.bits()).unwrap();
|
||||
|
||||
// let lcd_cam = unsafe { &*esp_hal::peripherals::LCD_CAM::PTR };
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue