Compare commits

..
2 Commits
Author SHA1 Message Date
randrews 0116ce97f8 more refactor 2026-09-09 12:52:13 -05:00
randrews 05803ad866 refactor 2026-09-08 22:46:31 -05:00
6 changed files with 135 additions and 111 deletions
Generated
+1
View File
@@ -964,6 +964,7 @@ dependencies = [
"mipidsi",
"smart-leds",
"smart-leds-trait",
"static_cell",
]
[[package]]
+2 -1
View File
@@ -18,10 +18,11 @@ esp-rtos = { version = "0.4", features = ["embassy", "esp32c6"] }
mipidsi = "0.10"
smart-leds = "0.4"
smart-leds-trait = "0.3"
static_cell = { version = "2.1.1" }
[profile.dev]
codegen-units = 1
incremental = false
[profile.release]
codegen-units = 1
codegen-units = 1
+10 -5
View File
@@ -5,6 +5,7 @@ use esp_hal::ledc::timer::{self, Timer, TimerIFace};
use esp_hal::ledc::{LSGlobalClkSource, Ledc, LowSpeed};
use esp_hal::peripherals::LEDC;
use esp_hal::time::Rate;
use static_cell::StaticCell;
/// PWM frequency for the backlight (Hz).
const BACKLIGHT_FREQ: Rate = Rate::from_khz(5);
@@ -13,11 +14,13 @@ const BACKLIGHT_FREQ: Rate = Rate::from_khz(5);
/// running the display above 50% brightness, so `level = 1.0` maps to this.
const MAX_DUTY_PCT: u8 = 50;
static BACKLIGHT_TIMER: StaticCell<(Ledc, Timer<'static, LowSpeed>)> = StaticCell::new();
/// Create and configure the LEDC timer used by the backlight.
///
/// Returns the `Ledc` and `Timer` so the caller can build a [`LcdBacklight`]
/// Returns the `Ledc` and `Timer` so the caller can build a [`Backlight`]
/// that borrows the timer.
pub fn setup_timer(
fn setup_timer(
ledc: LEDC<'static>,
) -> Result<(Ledc<'static>, Timer<'static, LowSpeed>), timer::Error> {
let mut ledc = Ledc::new(ledc);
@@ -37,15 +40,17 @@ pub type Error = channel::Error;
/// Backlight PWM driver. The channel holds a reference to the caller-owned
/// timer, so this type's lifetime is bounded by the timer's lifetime.
pub struct LcdBacklight<'a> {
pub struct Backlight<'a> {
channel: Channel<'a, LowSpeed>,
}
impl<'a> LcdBacklight<'a> {
impl<'a> Backlight<'a> {
pub fn new(
timer: &'a dyn TimerIFace<LowSpeed>,
ledc: LEDC<'static>,
pin: impl PeripheralOutput<'a>,
) -> Result<Self, Error> {
let (_ledc, timer) = BACKLIGHT_TIMER.init(setup_timer(ledc)
.expect("Failed to init backlight timer"));
let mut channel = Channel::new(channel::Number::Channel0, pin);
channel.configure(channel::config::Config {
timer,
+77 -76
View File
@@ -9,6 +9,7 @@ use mipidsi::interface::SpiInterface;
use mipidsi::models::ST7789;
use mipidsi::options::{ColorInversion, ColorOrder, Orientation, Rotation};
use mipidsi::Builder;
use crate::framebuffer::Framebuffer;
pub const WIDTH: u32 = 172;
pub const HEIGHT: u32 = 320;
@@ -72,93 +73,93 @@ pub struct LcdPins {
}
/// The raw LCD resources, ready to drive the async framebuffer path.
pub struct LcdParts {
pub struct Display {
pub spi: SpiDmaBus,
pub cs: Output<'static>,
pub dc: Output<'static>,
}
pub fn init_parts(pins: LcdPins) -> LcdParts {
let mut buffer = [0u8; 4096];
let dma_tx_buf = esp_hal::dma_tx_buffer!(DMA_BUF_SIZE).expect("Failed to create DMA TX buffer");
let dma_rx_buf = esp_hal::dma_rx_buffer!(4).expect("Failed to create DMA RX buffer");
impl Display {
pub fn new(pins: LcdPins) -> Self {
let mut buffer = [0u8; 4096];
let dma_tx_buf = esp_hal::dma_tx_buffer!(DMA_BUF_SIZE).expect("Failed to create DMA TX buffer");
let dma_rx_buf = esp_hal::dma_rx_buffer!(4).expect("Failed to create DMA RX buffer");
let spi = Spi::new(
pins.spi2,
SpiConfig::default().with_frequency(SPI_FREQ).with_mode(Mode::_0),
)
.expect("Failed to configure SPI")
.with_sck(pins.sck)
.with_mosi(pins.mosi)
.with_dma(pins.dma_ch)
.with_buffers(dma_rx_buf, dma_tx_buf)
.into_async();
let spi = Spi::new(
pins.spi2,
SpiConfig::default().with_frequency(SPI_FREQ).with_mode(Mode::_0),
)
.expect("Failed to configure SPI")
.with_sck(pins.sck)
.with_mosi(pins.mosi)
.with_dma(pins.dma_ch)
.with_buffers(dma_rx_buf, dma_tx_buf)
.into_async();
let cs_pin = Output::new(pins.cs, Level::High, OutputConfig::default());
let device = LcdDevice {
spi,
cs: cs_pin,
};
let cs_pin = Output::new(pins.cs, Level::High, OutputConfig::default());
let device = LcdDevice {
spi,
cs: cs_pin,
};
let dc_pin = Output::new(pins.dc, Level::Low, OutputConfig::default());
let interface = SpiInterface::new(device, dc_pin, &mut buffer);
let dc_pin = Output::new(pins.dc, Level::Low, OutputConfig::default());
let interface = SpiInterface::new(device, dc_pin, &mut buffer);
let mut rst_pin = Output::new(pins.rst, Level::High, OutputConfig::default());
rst_pin.set_high();
let mut rst_pin = Output::new(pins.rst, Level::High, OutputConfig::default());
rst_pin.set_high();
let mut delay = esp_hal::delay::Delay::new();
let mut delay = esp_hal::delay::Delay::new();
let lcd = Builder::new(ST7789, interface)
.display_size(WIDTH as u16, HEIGHT as u16)
.display_offset(COLUMN_OFFSET, 0)
.orientation(Orientation::new().rotate(Rotation::Deg0))
.color_order(ColorOrder::Rgb)
.invert_colors(ColorInversion::Inverted)
.reset_pin(rst_pin)
.init(&mut delay)
.expect("Failed to init display");
let lcd = Builder::new(ST7789, interface)
.display_size(WIDTH as u16, HEIGHT as u16)
.display_offset(COLUMN_OFFSET, 0)
.orientation(Orientation::new().rotate(Rotation::Deg0))
.color_order(ColorOrder::Rgb)
.invert_colors(ColorInversion::Inverted)
.reset_pin(rst_pin)
.init(&mut delay)
.expect("Failed to init display");
let (interface, _model, _rst) = lcd.release();
let (device, dc) = interface.release();
let (spi, cs) = device.into_inner();
let (interface, _model, _rst) = lcd.release();
let (device, dc) = interface.release();
let (spi, cs) = device.into_inner();
LcdParts { spi, cs, dc }
Display { spi, cs, dc }
}
/// Stream a full framebuffer to the panel asynchronously (DMA).
///
/// Sends the address window and RAMWR command, then the pixel bytes as one
/// (internally chunked) DMA transfer. Returns when the transfer completes.
pub async fn send_framebuffer(
&mut self,
framebuffer: &Framebuffer,
) -> Result<(), SpiError> {
let x0 = COLUMN_OFFSET;
let x1 = COLUMN_OFFSET + WIDTH as u16 - 1;
let y1 = HEIGHT as u16 - 1;
self.cs.set_low();
// MIPI DCS: DC low for the command byte, high for its arguments.
self.dc.set_low();
self.spi.write_async(&[0x2A]).await?;
self.dc.set_high();
self.spi.write_async(&[(x0 >> 8) as u8, x0 as u8, (x1 >> 8) as u8, x1 as u8])
.await?;
self.dc.set_low();
self.spi.write_async(&[0x2B]).await?;
self.dc.set_high();
self.spi.write_async(&[0, 0, (y1 >> 8) as u8, y1 as u8]).await?;
self.dc.set_low();
self.spi.write_async(&[0x2C]).await?;
self.dc.set_high();
self.spi.write_async(framebuffer.as_bytes()).await?;
self.cs.set_high();
Ok(())
}
}
/// Stream a full framebuffer to the panel asynchronously (DMA).
///
/// Sends the address window and RAMWR command, then the pixel bytes as one
/// (internally chunked) DMA transfer. Returns when the transfer completes.
pub async fn send_framebuffer(
spi: &mut SpiDmaBus,
dc: &mut Output<'static>,
cs: &mut Output<'static>,
bytes: &[u8],
) -> Result<(), SpiError> {
let x0 = COLUMN_OFFSET;
let x1 = COLUMN_OFFSET + WIDTH as u16 - 1;
let y1 = HEIGHT as u16 - 1;
cs.set_low();
// MIPI DCS: DC low for the command byte, high for its arguments.
dc.set_low();
spi.write_async(&[0x2A]).await?;
dc.set_high();
spi.write_async(&[(x0 >> 8) as u8, x0 as u8, (x1 >> 8) as u8, x1 as u8])
.await?;
dc.set_low();
spi.write_async(&[0x2B]).await?;
dc.set_high();
spi.write_async(&[0, 0, (y1 >> 8) as u8, y1 as u8]).await?;
dc.set_low();
spi.write_async(&[0x2C]).await?;
dc.set_high();
spi.write_async(bytes).await?;
cs.set_high();
Ok(())
}
+8 -5
View File
@@ -1,9 +1,10 @@
use esp_hal::gpio::interconnect::PeripheralOutput;
use esp_hal::rmt::TxChannelCreator;
use esp_hal::peripherals::RMT;
use esp_hal::rmt::Rmt;
use esp_hal::time::Rate;
use esp_hal::Blocking;
use esp_hal_smartled::{RmtSmartLeds, WS2812_TIMING, buffer_size, color_order};
use smart_leds_trait::{RGB8, SmartLedsWrite};
use esp_hal_smartled::{buffer_size, color_order, RmtSmartLeds, WS2812_TIMING};
use smart_leds_trait::{SmartLedsWrite, RGB8};
const RMT_FREQ: Rate = Rate::from_mhz(80);
@@ -15,11 +16,13 @@ pub struct RgbLed {
impl RgbLed {
pub fn new(
channel: impl TxChannelCreator<'static, Blocking>,
rmt_periph: RMT<'static>,
pin: impl PeripheralOutput<'static>,
) -> Result<Self, esp_hal_smartled::Error> {
let rmt = Rmt::new(rmt_periph, Rate::from_mhz(80))
.expect("Failed to init RMT");
Ok(Self {
led: SmartLed::new_with_memsize(WS2812_TIMING, channel, pin, 2, RMT_FREQ)?,
led: SmartLed::new_with_memsize(WS2812_TIMING, rmt.channel0, pin, 2, RMT_FREQ)?,
})
}
+37 -24
View File
@@ -14,9 +14,11 @@ use embassy_time::{Instant, Timer};
use esp_backtrace as _;
use esp_bootloader_esp_idf::esp_app_desc;
use esp_hal::timer::timg::TimerGroup;
use esp_hal::{init, rmt::Rmt, time::Rate, Config};
use esp_hal::{init, Config};
use esp_hal::peripherals::{GPIO22, LEDC};
use esp_println::println;
use smart_leds::hsv::{hsv2rgb, Hsv};
use crate::display::Display;
const SAT: u8 = 255;
const VAL: u8 = 255;
@@ -24,7 +26,12 @@ const VAL: u8 = 255;
esp_app_desc!();
#[embassy_executor::task]
async fn led_task(mut led: led::RgbLed) {
async fn led_task(
rmt_periph: esp_hal::peripherals::RMT<'static>,
led_pin: esp_hal::peripherals::GPIO8<'static>,
) {
let mut led = led::RgbLed::new(rmt_periph, led_pin).expect("Failed to init RGB LED");
let mut frame = 0u32;
loop {
let hue = (frame % 256) as u8;
@@ -42,18 +49,19 @@ async fn led_task(mut led: led::RgbLed) {
#[embassy_executor::task]
async fn display_task(
pins: display::LcdPins,
mut fb: framebuffer::Framebuffer,
timer: esp_hal::ledc::timer::Timer<'static, esp_hal::ledc::LowSpeed>,
bl_pin: esp_hal::peripherals::GPIO22<'static>,
ledc: LEDC<'static>,
bl_pin: GPIO22<'static>,
) {
let mut parts = display::init_parts(pins);
let mut display = Display::new(pins);
println!("LCD {}x{} initialized", display::WIDTH, display::HEIGHT);
let mut backlight =
backlight::LcdBacklight::new(&timer, bl_pin).expect("Failed to init backlight");
let mut backlight = backlight::Backlight::new(ledc, bl_pin)
.expect("Failed to init backlight");
backlight.set_level(0.5).unwrap();
println!("backlight set to 0.5");
let mut fb = framebuffer::Framebuffer;
let mut pts = [
Point::new(86, 20),
Point::new(61, 50),
@@ -64,6 +72,10 @@ async fn display_task(
let mut frame = 0u32;
let mut fps_window_start = Instant::now();
let mut fps_frames = 0u32;
let mut window_draw_us: u64 = 0;
let mut window_send_us: u64 = 0;
let mut last_draw_us: u64;
let mut last_send_us: u64;
loop {
let hue = (frame % 256) as u8;
@@ -91,30 +103,40 @@ async fn display_task(
vy = -vy;
}
let draw_start = Instant::now();
fb.clear();
Triangle::new(pts[0], pts[1], pts[2])
.into_styled(PrimitiveStyle::with_fill(accent))
.draw(&mut fb)
.unwrap();
last_draw_us = draw_start.elapsed().as_micros() as u64;
display::send_framebuffer(&mut parts.spi, &mut parts.dc, &mut parts.cs, fb.as_bytes())
.await
.unwrap();
let send_start = Instant::now();
display.send_framebuffer(&fb).await.unwrap();
last_send_us = send_start.elapsed().as_micros() as u64;
if frame % 32 == 0 {
println!(
"frame {frame}: apex=({}, {}), v=({}, {}), hue={hue}",
"frame {frame}: apex=({}, {}), v=({}, {}), hue={hue} | draw {last_draw_us} us, send {last_send_us} us",
pts[0].x, pts[0].y, vx, vy
);
}
fps_frames += 1;
window_draw_us += last_draw_us;
window_send_us += last_send_us;
if fps_frames >= 60 {
let elapsed_ms = fps_window_start.elapsed().as_millis().max(1);
let fps = (fps_frames as u64 * 1000) / elapsed_ms;
println!("fps: {fps} ({fps_frames} frames in {elapsed_ms} ms)");
let avg_draw_us = window_draw_us / fps_frames as u64;
let avg_send_us = window_send_us / fps_frames as u64;
println!(
"fps: {fps} ({fps_frames} frames in {elapsed_ms} ms) | avg draw {avg_draw_us} us, avg send {avg_send_us} us"
);
fps_frames = 0;
fps_window_start = Instant::now();
window_draw_us = 0;
window_send_us = 0;
}
frame = frame.wrapping_add(1);
@@ -127,15 +149,7 @@ async fn main(spawner: Spawner) -> ! {
let timg0 = TimerGroup::new(peripherals.TIMG0);
esp_rtos::start(timg0.timer0, peripherals.FROM_CPU_INTR0);
let led = {
let rmt = Rmt::new(peripherals.RMT, Rate::from_mhz(80)).expect("Failed to init RMT");
led::RgbLed::new(rmt.channel0, peripherals.GPIO8).expect("Failed to init RGB LED")
};
let (_ledc, timer) =
backlight::setup_timer(peripherals.LEDC).expect("Failed to init backlight timer");
spawner.spawn(led_task(led).unwrap());
spawner.spawn(led_task(peripherals.RMT, peripherals.GPIO8).unwrap());
spawner.spawn(display_task(
display::LcdPins {
spi2: peripherals.SPI2,
@@ -146,8 +160,7 @@ async fn main(spawner: Spawner) -> ! {
rst: peripherals.GPIO21,
dma_ch: peripherals.DMA_CH0,
},
framebuffer::Framebuffer,
timer,
peripherals.LEDC,
peripherals.GPIO22,
).unwrap());