diff --git a/Cargo.lock b/Cargo.lock index 2f10ab5..76f8a25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -964,6 +964,7 @@ dependencies = [ "mipidsi", "smart-leds", "smart-leds-trait", + "static_cell", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 244264b..2c3eeac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 \ No newline at end of file +codegen-units = 1 diff --git a/src/backlight.rs b/src/backlight.rs index f34b18b..1561457 100644 --- a/src/backlight.rs +++ b/src/backlight.rs @@ -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, + ledc: LEDC<'static>, pin: impl PeripheralOutput<'a>, ) -> Result { + 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, diff --git a/src/display.rs b/src/display.rs index 92721c2..536c525 100644 --- a/src/display.rs +++ b/src/display.rs @@ -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(()) -} \ No newline at end of file diff --git a/src/led.rs b/src/led.rs index be529b5..6344bb6 100644 --- a/src/led.rs +++ b/src/led.rs @@ -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 { + 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)?, }) } diff --git a/src/main.rs b/src/main.rs index b2ed0ba..6d47678 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ mod backlight; mod display; mod framebuffer; mod led; -pub mod board; use embedded_graphics::pixelcolor::Rgb565; use embedded_graphics::prelude::*; @@ -15,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; @@ -29,8 +30,7 @@ async fn led_task( rmt_periph: esp_hal::peripherals::RMT<'static>, led_pin: esp_hal::peripherals::GPIO8<'static>, ) { - let rmt = Rmt::new(rmt_periph, Rate::from_mhz(80)).expect("Failed to init RMT"); - let mut led = led::RgbLed::new(rmt.channel0, led_pin).expect("Failed to init RGB LED"); + let mut led = led::RgbLed::new(rmt_periph, led_pin).expect("Failed to init RGB LED"); let mut frame = 0u32; loop { @@ -49,16 +49,14 @@ async fn led_task( #[embassy_executor::task] async fn display_task( pins: display::LcdPins, - ledc: esp_hal::peripherals::LEDC<'static>, - 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 (_ledc, timer) = - backlight::setup_timer(ledc).expect("Failed to init backlight timer"); - 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"); @@ -74,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; @@ -101,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);