From 3ad59eac099333622153fc280bca7c33756a1b93 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Mon, 7 Sep 2026 22:13:21 -0500 Subject: [PATCH] lcd driver --- Cargo.lock | 68 +++++++++++++++++++++++++++++++ Cargo.toml | 3 ++ src/display.rs | 64 ++++++++++++++++++++++++++++++ src/framebuffer.rs | 66 +++++++++++++++++++++++++++++++ src/main.rs | 99 +++++++++++++++++++++++++++++++++++----------- 5 files changed, 278 insertions(+), 22 deletions(-) create mode 100644 src/display.rs create mode 100644 src/framebuffer.rs diff --git a/Cargo.lock b/Cargo.lock index 3d45ef1..de2a827 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "bitfield" version = "0.19.5" @@ -326,6 +332,29 @@ dependencies = [ "nb 1.1.0", ] +[[package]] +name = "embedded-graphics" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8da660bb0c829b34a56a965490597f82a55e767b91f9543be80ce8ccb416fe" +dependencies = [ + "az", + "byteorder", + "embedded-graphics-core", + "float-cmp", + "micromath", +] + +[[package]] +name = "embedded-graphics-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95743bef3ff70fcba3930246c4e6872882bbea0dcc6da2ca860112e0cd4bd09f" +dependencies = [ + "az", + "byteorder", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -351,6 +380,16 @@ dependencies = [ "embedded-hal 1.0.0", ] +[[package]] +name = "embedded-hal-bus" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513e0b3a8fb7d3013a8ae17a834283f170deaf7d0eeab0a7c1a36ad4dd356d22" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", +] + [[package]] name = "embedded-io" version = "0.6.1" @@ -730,15 +769,27 @@ dependencies = [ name = "espboard" version = "0.1.0" dependencies = [ + "embedded-graphics", + "embedded-hal-bus", "esp-backtrace", "esp-bootloader-esp-idf", "esp-hal", "esp-hal-smartled", "esp-println", + "mipidsi", "smart-leds", "smart-leds-trait", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -953,6 +1004,23 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "micromath" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" + +[[package]] +name = "mipidsi" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790ebd28bd67addbccf41b1c0c188c26bb9f5bdcd91d4d6da9bd558e20d97a1d" +dependencies = [ + "embedded-graphics-core", + "embedded-hal 1.0.0", + "heapless 0.8.0", +] + [[package]] name = "nb" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index a64c42b..af52398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,14 @@ version = "0.1.0" edition = "2024" [dependencies] +embedded-graphics = "0.8" +embedded-hal-bus = "0.3" esp-backtrace = { version = "0.20.0", features = ["esp32c6", "println", "panic-handler"] } esp-bootloader-esp-idf = { version = "0.6.0", features = ["esp32c6"] } esp-hal = { version = "1.2.0", features = ["esp32c6", "unstable"] } esp-hal-smartled = { version = "0.18", default-features = false, features = ["esp32c6"] } esp-println = { version = "0.18.0", default-features = false, features = ["esp32c6", "jtag-serial"] } +mipidsi = "0.10" smart-leds = "0.4" smart-leds-trait = "0.3" diff --git a/src/display.rs b/src/display.rs new file mode 100644 index 0000000..5510818 --- /dev/null +++ b/src/display.rs @@ -0,0 +1,64 @@ +use embedded_hal_bus::spi::{ExclusiveDevice, NoDelay}; +use esp_hal::delay::Delay; +use esp_hal::gpio::{Level, Output, OutputConfig}; +use esp_hal::peripherals::{GPIO14, GPIO15, GPIO21, GPIO6, GPIO7, SPI2}; +use esp_hal::spi::master::{Config as SpiConfig, Spi}; +use esp_hal::spi::Mode; +use esp_hal::time::Rate; +use esp_hal::Blocking; +use mipidsi::interface::SpiInterface; +use mipidsi::models::ST7789; +use mipidsi::options::{ColorInversion, ColorOrder, Orientation, Rotation}; +use mipidsi::{Builder, Display}; + +pub const WIDTH: u32 = 172; +pub const HEIGHT: u32 = 320; + +const SPI_FREQ: Rate = Rate::from_mhz(40); + +/// Column offset of the 172-px-wide panel within the ST7789's 240-px framebuffer. +const COLUMN_OFFSET: u16 = 34; + +type Device = ExclusiveDevice, Output<'static>, NoDelay>; +type Interface<'a> = SpiInterface<'a, Device, Output<'static>>; + +/// The mipidsi [`Display`], which implements embedded-graphics' [`DrawTarget`]. +/// +/// The lifetime is the borrow of the pixel buffer passed to [`init`]. +pub type Lcd<'a> = Display, ST7789, Output<'static>>; + +pub fn init<'a>( + spi2: SPI2<'static>, + sck: GPIO7<'static>, + mosi: GPIO6<'static>, + dc: GPIO15<'static>, + cs: GPIO14<'static>, + rst: GPIO21<'static>, + buffer: &'a mut [u8], +) -> Lcd<'a> { + let spi = Spi::new(spi2, SpiConfig::default().with_frequency(SPI_FREQ).with_mode(Mode::_0)) + .expect("Failed to configure SPI") + .with_sck(sck) + .with_mosi(mosi); + + let cs_pin = Output::new(cs, Level::High, OutputConfig::default()); + let device = ExclusiveDevice::new_no_delay(spi, cs_pin).expect("Failed to create SPI device"); + + let dc_pin = Output::new(dc, Level::Low, OutputConfig::default()); + let interface = SpiInterface::new(device, dc_pin, buffer); + + let mut rst_pin = Output::new(rst, Level::High, OutputConfig::default()); + rst_pin.set_high(); + + let mut delay = Delay::new(); + + 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") +} \ No newline at end of file diff --git a/src/framebuffer.rs b/src/framebuffer.rs new file mode 100644 index 0000000..e510884 --- /dev/null +++ b/src/framebuffer.rs @@ -0,0 +1,66 @@ +use embedded_graphics::draw_target::DrawTarget; +use embedded_graphics::geometry::{OriginDimensions, Size}; +use embedded_graphics::pixelcolor::{Rgb565, RgbColor}; +use embedded_graphics::Pixel; + +use crate::display::{HEIGHT, WIDTH}; + +const PIXEL_COUNT: usize = (WIDTH * HEIGHT) as usize; + +/// The full-screen back buffer (172 × 320 × 2 bytes ≈ 110 KB). +static mut PIXELS: [Rgb565; PIXEL_COUNT] = [Rgb565::BLACK; PIXEL_COUNT]; + +/// A full-frame framebuffer that implements embedded-graphics' [`DrawTarget`]. +/// +/// Draw primitives into it, then blit the whole thing to the display once per +/// frame to avoid flicker. The backing memory is a `static`, so this is only +/// safe to use from a single thread (which is all this application has). +pub struct Framebuffer; + +impl Framebuffer { + pub fn clear(&mut self) { + // SAFETY: only used from the single main loop. + unsafe { &mut *core::ptr::addr_of_mut!(PIXELS) }.fill(Rgb565::BLACK); + } + + /// The raw pixels, ready to be sent to the display. + pub fn pixels(&self) -> &'static [Rgb565] { + // SAFETY: the buffer lives for the whole program and is only borrowed + // for the duration of the (blocking) blit. + unsafe { &*core::ptr::addr_of!(PIXELS) } + } +} + +impl DrawTarget for Framebuffer { + type Color = Rgb565; + type Error = core::convert::Infallible; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + // SAFETY: only used from the single main loop. + let buffer = unsafe { &mut *core::ptr::addr_of_mut!(PIXELS) }; + + for pixel in pixels { + let (x, y) = (pixel.0.x, pixel.0.y); + if x >= 0 && y >= 0 && x < WIDTH as i32 && y < HEIGHT as i32 { + buffer[(y as usize) * WIDTH as usize + (x as usize)] = pixel.1; + } + } + + Ok(()) + } +} + +impl OriginDimensions for Framebuffer { + fn size(&self) -> Size { + Size::new(WIDTH, HEIGHT) + } +} + +impl Default for Framebuffer { + fn default() -> Self { + Self + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b78d321..b04f0f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,14 +2,23 @@ #![no_main] mod backlight; +mod display; +mod framebuffer; mod led; +use embedded_graphics::draw_target::DrawTarget; +use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::prelude::*; +use embedded_graphics::primitives::{PrimitiveStyle, Rectangle, Triangle}; use esp_backtrace as _; use esp_bootloader_esp_idf::esp_app_desc; use esp_hal::{init, rmt::Rmt, time::Rate, Config}; use esp_println::println; use smart_leds::hsv::{hsv2rgb, Hsv}; -use smart_leds::{brightness, gamma}; +use smart_leds::RGB8; + +const SAT: u8 = 200; +const VAL: u8 = 200; esp_app_desc!(); @@ -26,34 +35,80 @@ fn main() -> ! { backlight::setup_timer(peripherals.LEDC).expect("Failed to init backlight timer"); let mut backlight = backlight::LcdBacklight::new(&timer, peripherals.GPIO22).expect("Failed to init backlight"); + backlight.set_level(0.5).unwrap(); + + let mut buffer = [0u8; 4096]; + let mut lcd = display::init( + peripherals.SPI2, + peripherals.GPIO7, + peripherals.GPIO6, + peripherals.GPIO15, + peripherals.GPIO14, + peripherals.GPIO21, + &mut buffer, + ); let delay = esp_hal::delay::Delay::new(); - let levels = [1.0f32, 0.75, 0.5, 0.25, 0.0, 0.5]; - let mut level_idx = 0; + println!("LCD {}x{} initialized", display::WIDTH, display::HEIGHT); + lcd.clear(Rgb565::BLACK).unwrap(); + + let mut fb = framebuffer::Framebuffer; + let screen = Rectangle::new(Point::zero(), Size::new(display::WIDTH, display::HEIGHT)); + + let mut pts = [ + Point::new(86, 20), + Point::new(61, 50), + Point::new(111, 50), + ]; + let mut vx: i32 = 3; + let mut vy: i32 = 2; + let mut frame = 0u32; loop { - for hue in 0..=255 { - let rgb = hsv2rgb(Hsv { - hue, - sat: 255, - val: 255, - }); - let lit = brightness(gamma(core::iter::once(rgb)), 40) - .next() - .unwrap(); - led.set_rgb(lit).unwrap(); + let hue = (frame % 256) as u8; + let lit = hsv2rgb(Hsv { + hue, + sat: SAT, + val: VAL, + }); + led.set_rgb(RGB8::new(0,0,0)).unwrap(); + let accent = Rgb565::new(lit.r / 8, lit.g / 4, lit.b / 8); - if hue % 32 == 0 { - println!("hue {hue}: ({}, {}, {})", lit.r, lit.g, lit.b); - } - - delay.delay_millis(10); + for p in &mut pts { + p.x += vx; + p.y += vy; } - let level = levels[level_idx % levels.len()]; - backlight.set_level(level).unwrap(); - println!("backlight level: {level}"); - level_idx += 1; + let min_x = pts.iter().map(|p| p.x).min().unwrap(); + let max_x = pts.iter().map(|p| p.x).max().unwrap(); + let min_y = pts.iter().map(|p| p.y).min().unwrap(); + let max_y = pts.iter().map(|p| p.y).max().unwrap(); + + if min_x <= 0 || max_x >= display::WIDTH as i32 { + vx = -vx; + } + if min_y <= 0 || max_y >= display::HEIGHT as i32 { + vy = -vy; + } + + fb.clear(); + Triangle::new(pts[0], pts[1], pts[2]) + .into_styled(PrimitiveStyle::with_fill(accent)) + .draw(&mut fb) + .unwrap(); + + lcd.fill_contiguous(&screen, fb.pixels().iter().cloned()) + .unwrap(); + + if frame % 32 == 0 { + println!( + "frame {frame}: apex=({}, {}), v=({}, {}), hue={hue}", + pts[0].x, pts[0].y, vx, vy + ); + } + + frame = frame.wrapping_add(1); + //delay.delay_millis(30); } } \ No newline at end of file