Skip to content

Commit

Permalink
Add an example of a compressed DMA frame buffer
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominic Fischer committed Oct 2, 2024
1 parent 30aef58 commit 82cd2c3
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 7 deletions.
13 changes: 13 additions & 0 deletions esp-hal/src/dma/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2143,6 +2143,19 @@ pub struct Preparation {
// burst_mode, alignment, check_owner, etc.
}

impl Preparation {
/// Preparation for a bare minimum DMA transfer.
/// - Internal RAM only.
/// - Burst mode disabled.
/// - Maximum aligment.
pub fn minimal(start: *mut DmaDescriptor) -> Self {
Preparation {
start,
block_size: None,
}
}
}

/// [DmaTxBuffer] is a DMA descriptor + memory combo that can be used for
/// transmitting data from a DMA channel to a peripheral's FIFO.
///
Expand Down
139 changes: 132 additions & 7 deletions examples/src/bin/lcd_i8080.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,16 @@
use esp_backtrace as _;
use esp_hal::{
delay::Delay,
dma::{Dma, DmaChannel0, DmaPriority, DmaTxBuf},
dma::{
Dma,
DmaChannel0,
DmaDescriptor,
DmaPriority,
DmaTxBuf,
DmaTxBuffer,
Owner,
Preparation,
},
dma_tx_buffer,
gpio::{Input, Io, Level, Output, Pull},
lcd_cam::{
Expand All @@ -36,6 +45,7 @@ use esp_hal::{
Blocking,
};
use esp_println::println;
use static_cell::ConstStaticCell;

#[entry]
fn main() -> ! {
Expand Down Expand Up @@ -206,32 +216,34 @@ fn main() -> ! {
// Tearing Effect Line On
bus.send(0x35, &[0]);

let width = 320u16;
let height = 480u16;
const WIDTH: usize = 320;
const HEIGHT: usize = 480;

{
println!("Set addresses");

let width_b = width.to_be_bytes();
let height_b = height.to_be_bytes();
let width_b = (WIDTH as u16).to_be_bytes();
let height_b = (HEIGHT as u16).to_be_bytes();
bus.send(0x2A, &[0, 0, width_b[0], width_b[1]]); // CASET
bus.send(0x2B, &[0, 0, height_b[0], height_b[1]]) // PASET
}

println!("Drawing");

const RED: u16 = 0b00000_000000_11111;
const GREEN: u16 = 0b00000_111111_00000;
const BLUE: u16 = 0b11111_000000_00000;

backlight.set_high();

let total_pixels = width as usize * height as usize;
let total_pixels = WIDTH * HEIGHT;
let total_bytes = total_pixels * 2;

let (mut i8080, mut dma_tx_buf) = bus.resources.take().unwrap();

dma_tx_buf.set_length(dma_tx_buf.capacity());

for color in [RED, BLUE].iter().cycle() {
for color in [RED, BLUE].iter().cycle().take(4) {
let color = color.to_be_bytes();
for chunk in dma_tx_buf.as_mut_slice().chunks_mut(2) {
chunk.copy_from_slice(&color);
Expand Down Expand Up @@ -268,7 +280,120 @@ fn main() -> ! {
delay.delay_millis(1_000);
}

// This compressed buffer works out to 160 DMA descriptors and a 1920 byte buffer, using up a
// total of 3,840 bytes (160 * (4 * 3) + 1920). This is much more space efficient than the
// 307,200 bytes (320 * 480 * 2) it would have taken to send out a full frame buffer at once.
// It is also more time/CPU efficient than sending out the frame in chunks, as you don't have
// to pay for the latency of starting each chunk, and the CPU has more time to do other things.
let mut compressed_buffer = {
const DESCRIPTOR_COUNT: usize = 160;
const BUFFER_SIZE: usize = 1920;

static DESCRIPTORS: ConstStaticCell<[DmaDescriptor; DESCRIPTOR_COUNT]> =
ConstStaticCell::new([DmaDescriptor::EMPTY; DESCRIPTOR_COUNT]);
static SHARED_BUFFER: ConstStaticCell<[u8; BUFFER_SIZE]> =
ConstStaticCell::new([0; BUFFER_SIZE]);

let descriptors = DESCRIPTORS.take();
let shared_buffer = SHARED_BUFFER.take();

CompressedBuffer::new(descriptors, shared_buffer)
};

for color in [RED, GREEN, BLUE].iter().cycle() {
// This is very quick as it only sets 1920 bytes, but it can also be skipped by having one
// compressed buffer for each color.
compressed_buffer.set_color(*color);

// Naive implementation of tear prevention.
{
// Wait for display to start refreshing.
while tear_effect.is_high() {}
// Wait for display to finish refreshing.
while tear_effect.is_low() {}

// Now we have the maximum amount of time between each refresh available, for drawing.
}

// Send an entire frame buffer in a single transfer.
(_, i8080, compressed_buffer) = i8080
.send(0x2Cu8, 0, compressed_buffer)
.map_err(|e| e.0)
.unwrap()
.wait();

delay.delay_millis(300);
}

loop {
delay.delay_millis(1_000);
}
}

/// A simple implementation of a compressed solid frame buffer, using several descriptors to send
/// out the same buffer multiple times.
///
/// To draw text or shapes in this, one can simply modify a small subset of the descriptors to
/// point to a different buffer. This will need some kind of allocation scheme.
struct CompressedBuffer {
descriptors: &'static mut [DmaDescriptor],
buffer: &'static mut [u8],
}

impl CompressedBuffer {
fn new(
descriptors: &'static mut [DmaDescriptor],
buffer: &'static mut [u8],
) -> CompressedBuffer {
assert!(buffer.len() < 4096);
assert!(buffer.len() > 0);

let mut next = core::ptr::null_mut();
for desc in descriptors.iter_mut().rev() {
// Setup the linked list
desc.next = next;
next = desc;

// Use the shared buffer.
desc.buffer = buffer.as_mut_ptr();
desc.set_size(buffer.len());
desc.set_length(buffer.len());

// Optional but no harm in doing it.
desc.set_owner(Owner::Dma);
}

CompressedBuffer {
descriptors,
buffer,
}
}

fn set_color(&mut self, color: u16) {
for chunk in self.buffer.chunks_mut(2) {
chunk.copy_from_slice(&color.to_be_bytes());
}
}
}

unsafe impl DmaTxBuffer for CompressedBuffer {
type View = CompressedBufferView;

fn prepare(&mut self) -> Preparation {
Preparation::minimal(self.descriptors.as_mut_ptr())
}

fn into_view(self) -> Self::View {
CompressedBufferView(self)
}

fn from_view(view: Self::View) -> Self {
view.0
}

fn length(&self) -> usize {
unimplemented!()
}
}

struct CompressedBufferView(CompressedBuffer);

0 comments on commit 82cd2c3

Please sign in to comment.