From be5222421177b632cd120272e08156b64cc0b886 Mon Sep 17 00:00:00 2001 From: William <174336620+williams-one@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:40:18 +0200 Subject: [PATCH] Add LTDC example for STM32U5G9J-DK2 demo board --- examples/stm32u5/Cargo.toml | 2 + examples/stm32u5/src/bin/ferris.bmp | Bin 0 -> 6794 bytes examples/stm32u5/src/bin/ltdc.rs | 462 ++++++++++++++++++++++++++++ 3 files changed, 464 insertions(+) create mode 100644 examples/stm32u5/src/bin/ferris.bmp create mode 100644 examples/stm32u5/src/bin/ltdc.rs diff --git a/examples/stm32u5/Cargo.toml b/examples/stm32u5/Cargo.toml index ad7db4c32..f594ad71a 100644 --- a/examples/stm32u5/Cargo.toml +++ b/examples/stm32u5/Cargo.toml @@ -21,6 +21,8 @@ cortex-m-rt = "0.7.0" embedded-hal = "0.2.6" panic-probe = { version = "0.3", features = ["print-defmt"] } heapless = { version = "0.8", default-features = false } +embedded-graphics = { version = "0.8.1" } +tinybmp = { version = "0.6.0" } micromath = "2.0.0" diff --git a/examples/stm32u5/src/bin/ferris.bmp b/examples/stm32u5/src/bin/ferris.bmp new file mode 100644 index 0000000000000000000000000000000000000000..7a222ab84ddd9f2707ca8dcebad84b1b39c21818 GIT binary patch literal 6794 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 literal 0 HcmV?d00001 diff --git a/examples/stm32u5/src/bin/ltdc.rs b/examples/stm32u5/src/bin/ltdc.rs new file mode 100644 index 000000000..5bf2cd67d --- /dev/null +++ b/examples/stm32u5/src/bin/ltdc.rs @@ -0,0 +1,462 @@ +#![no_std] +#![no_main] +#![macro_use] +#![allow(static_mut_refs)] + +/// This example was derived from examples\stm32h735\src\bin\ltdc.rs +/// It demonstrates the LTDC lcd display peripheral and was tested on an STM32U5G9J-DK2 demo board (embassy-stm32 feature "stm32u5g9zj" and probe-rs chip "STM32U5G9ZJTxQ") +/// +use bouncy_box::BouncyBox; +use defmt::{info, unwrap}; +use embassy_executor::Spawner; +use embassy_stm32::gpio::{Level, Output, Speed}; +use embassy_stm32::ltdc::{self, Ltdc, LtdcConfiguration, LtdcLayer, LtdcLayerConfig, PolarityActive, PolarityEdge}; +use embassy_stm32::{bind_interrupts, peripherals}; +use embassy_time::{Duration, Timer}; +use embedded_graphics::draw_target::DrawTarget; +use embedded_graphics::geometry::{OriginDimensions, Point, Size}; +use embedded_graphics::image::Image; +use embedded_graphics::pixelcolor::raw::RawU24; +use embedded_graphics::pixelcolor::Rgb888; +use embedded_graphics::prelude::*; +use embedded_graphics::primitives::Rectangle; +use embedded_graphics::Pixel; +use heapless::{Entry, FnvIndexMap}; +use tinybmp::Bmp; +use {defmt_rtt as _, panic_probe as _}; + +const DISPLAY_WIDTH: usize = 800; +const DISPLAY_HEIGHT: usize = 480; +const MY_TASK_POOL_SIZE: usize = 2; + +// the following two display buffers consume 261120 bytes that just about fits into axis ram found on the mcu +pub static mut FB1: [TargetPixelType; DISPLAY_WIDTH * DISPLAY_HEIGHT] = [0; DISPLAY_WIDTH * DISPLAY_HEIGHT]; +pub static mut FB2: [TargetPixelType; DISPLAY_WIDTH * DISPLAY_HEIGHT] = [0; DISPLAY_WIDTH * DISPLAY_HEIGHT]; + +bind_interrupts!(struct Irqs { + LTDC => ltdc::InterruptHandler; +}); + +const NUM_COLORS: usize = 256; + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = rcc_setup::stm32u5g9zj_init(); + + // enable ICACHE + embassy_stm32::pac::ICACHE.cr().write(|w| { + w.set_en(true); + }); + + // blink the led on another task + let led = Output::new(p.PD2, Level::High, Speed::Low); + unwrap!(spawner.spawn(led_task(led))); + + // numbers from STM32U5G9J-DK2.ioc + const RK050HR18H_HSYNC: u16 = 5; // Horizontal synchronization + const RK050HR18H_HBP: u16 = 8; // Horizontal back porch + const RK050HR18H_HFP: u16 = 8; // Horizontal front porch + const RK050HR18H_VSYNC: u16 = 5; // Vertical synchronization + const RK050HR18H_VBP: u16 = 8; // Vertical back porch + const RK050HR18H_VFP: u16 = 8; // Vertical front porch + + // NOTE: all polarities have to be reversed with respect to the STM32U5G9J-DK2 CubeMX parametrization + let ltdc_config = LtdcConfiguration { + active_width: DISPLAY_WIDTH as _, + active_height: DISPLAY_HEIGHT as _, + h_back_porch: RK050HR18H_HBP, + h_front_porch: RK050HR18H_HFP, + v_back_porch: RK050HR18H_VBP, + v_front_porch: RK050HR18H_VFP, + h_sync: RK050HR18H_HSYNC, + v_sync: RK050HR18H_VSYNC, + h_sync_polarity: PolarityActive::ActiveHigh, + v_sync_polarity: PolarityActive::ActiveHigh, + data_enable_polarity: PolarityActive::ActiveHigh, + pixel_clock_polarity: PolarityEdge::RisingEdge, + }; + + info!("init ltdc"); + let mut ltdc_de = Output::new(p.PD6, Level::Low, Speed::High); + let mut ltdc_disp_ctrl = Output::new(p.PE4, Level::Low, Speed::High); + let mut ltdc_bl_ctrl = Output::new(p.PE6, Level::Low, Speed::High); + let mut ltdc = Ltdc::new_with_pins( + p.LTDC, // PERIPHERAL + Irqs, // IRQS + p.PD3, // CLK + p.PE0, // HSYNC + p.PD13, // VSYNC + p.PB9, // B0 + p.PB2, // B1 + p.PD14, // B2 + p.PD15, // B3 + p.PD0, // B4 + p.PD1, // B5 + p.PE7, // B6 + p.PE8, // B7 + p.PC8, // G0 + p.PC9, // G1 + p.PE9, // G2 + p.PE10, // G3 + p.PE11, // G4 + p.PE12, // G5 + p.PE13, // G6 + p.PE14, // G7 + p.PC6, // R0 + p.PC7, // R1 + p.PE15, // R2 + p.PD8, // R3 + p.PD9, // R4 + p.PD10, // R5 + p.PD11, // R6 + p.PD12, // R7 + ); + ltdc.init(<dc_config); + ltdc_de.set_low(); + ltdc_bl_ctrl.set_high(); + ltdc_disp_ctrl.set_high(); + + // we only need to draw on one layer for this example (not to be confused with the double buffer) + info!("enable bottom layer"); + let layer_config = LtdcLayerConfig { + pixel_format: ltdc::PixelFormat::L8, // 1 byte per pixel + layer: LtdcLayer::Layer1, + window_x0: 0, + window_x1: DISPLAY_WIDTH as _, + window_y0: 0, + window_y1: DISPLAY_HEIGHT as _, + }; + + let ferris_bmp: Bmp = Bmp::from_slice(include_bytes!("./ferris.bmp")).unwrap(); + let color_map = build_color_lookup_map(&ferris_bmp); + let clut = build_clut(&color_map); + + // enable the bottom layer with a 256 color lookup table + ltdc.init_layer(&layer_config, Some(&clut)); + + // Safety: the DoubleBuffer controls access to the statically allocated frame buffers + // and it is the only thing that mutates their content + let mut double_buffer = DoubleBuffer::new( + unsafe { FB1.as_mut() }, + unsafe { FB2.as_mut() }, + layer_config, + color_map, + ); + + // this allows us to perform some simple animation for every frame + let mut bouncy_box = BouncyBox::new( + ferris_bmp.bounding_box(), + Rectangle::new(Point::zero(), Size::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32)), + 2, + ); + + loop { + // cpu intensive drawing to the buffer that is NOT currently being copied to the LCD screen + double_buffer.clear(); + let position = bouncy_box.next_point(); + let ferris = Image::new(&ferris_bmp, position); + unwrap!(ferris.draw(&mut double_buffer)); + + // perform async dma data transfer to the lcd screen + unwrap!(double_buffer.swap(&mut ltdc).await); + } +} + +/// builds the color look-up table from all unique colors found in the bitmap. This should be a 256 color indexed bitmap to work. +fn build_color_lookup_map(bmp: &Bmp) -> FnvIndexMap { + let mut color_map: FnvIndexMap = heapless::FnvIndexMap::new(); + let mut counter: u8 = 0; + + // add black to position 0 + color_map.insert(Rgb888::new(0, 0, 0).into_storage(), counter).unwrap(); + counter += 1; + + for Pixel(_point, color) in bmp.pixels() { + let raw = color.into_storage(); + if let Entry::Vacant(v) = color_map.entry(raw) { + v.insert(counter).expect("more than 256 colors detected"); + counter += 1; + } + } + color_map +} + +/// builds the color look-up table from the color map provided +fn build_clut(color_map: &FnvIndexMap) -> [ltdc::RgbColor; NUM_COLORS] { + let mut clut = [ltdc::RgbColor::default(); NUM_COLORS]; + for (color, index) in color_map.iter() { + let color = Rgb888::from(RawU24::new(*color)); + clut[*index as usize] = ltdc::RgbColor { + red: color.r(), + green: color.g(), + blue: color.b(), + }; + } + + clut +} + +#[embassy_executor::task(pool_size = MY_TASK_POOL_SIZE)] +async fn led_task(mut led: Output<'static>) { + let mut counter = 0; + loop { + info!("blink: {}", counter); + counter += 1; + + // on + led.set_low(); + Timer::after(Duration::from_millis(50)).await; + + // off + led.set_high(); + Timer::after(Duration::from_millis(450)).await; + } +} + +pub type TargetPixelType = u8; + +// A simple double buffer +pub struct DoubleBuffer { + buf0: &'static mut [TargetPixelType], + buf1: &'static mut [TargetPixelType], + is_buf0: bool, + layer_config: LtdcLayerConfig, + color_map: FnvIndexMap, +} + +impl DoubleBuffer { + pub fn new( + buf0: &'static mut [TargetPixelType], + buf1: &'static mut [TargetPixelType], + layer_config: LtdcLayerConfig, + color_map: FnvIndexMap, + ) -> Self { + Self { + buf0, + buf1, + is_buf0: true, + layer_config, + color_map, + } + } + + pub fn current(&mut self) -> (&FnvIndexMap, &mut [TargetPixelType]) { + if self.is_buf0 { + (&self.color_map, self.buf0) + } else { + (&self.color_map, self.buf1) + } + } + + pub async fn swap(&mut self, ltdc: &mut Ltdc<'_, T>) -> Result<(), ltdc::Error> { + let (_, buf) = self.current(); + let frame_buffer = buf.as_ptr(); + self.is_buf0 = !self.is_buf0; + ltdc.set_buffer(self.layer_config.layer, frame_buffer as *const _).await + } + + /// Clears the buffer + pub fn clear(&mut self) { + let (color_map, buf) = self.current(); + let black = Rgb888::new(0, 0, 0).into_storage(); + let color_index = color_map.get(&black).expect("no black found in the color map"); + + for a in buf.iter_mut() { + *a = *color_index; // solid black + } + } +} + +// Implement DrawTarget for +impl DrawTarget for DoubleBuffer { + type Color = Rgb888; + type Error = (); + + /// Draw a pixel + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + let size = self.size(); + let width = size.width as i32; + let height = size.height as i32; + let (color_map, buf) = self.current(); + + for pixel in pixels { + let Pixel(point, color) = pixel; + + if point.x >= 0 && point.y >= 0 && point.x < width && point.y < height { + let index = point.y * width + point.x; + let raw_color = color.into_storage(); + + match color_map.get(&raw_color) { + Some(x) => { + buf[index as usize] = *x; + } + None => panic!("color not found in color map: {}", raw_color), + }; + } else { + // Ignore invalid points + } + } + + Ok(()) + } +} + +impl OriginDimensions for DoubleBuffer { + /// Return the size of the display + fn size(&self) -> Size { + Size::new( + (self.layer_config.window_x1 - self.layer_config.window_x0) as _, + (self.layer_config.window_y1 - self.layer_config.window_y0) as _, + ) + } +} + +mod rcc_setup { + + use embassy_stm32::rcc; + use embassy_stm32::time::Hertz; + use embassy_stm32::{Config, Peripherals}; + + /// Sets up clocks for the stm32u5g9zj mcu + /// change this if you plan to use a different microcontroller + pub fn stm32u5g9zj_init() -> Peripherals { + // setup power and clocks for an STM32U5G9J-DK2 run from an external 16 Mhz external oscillator + let mut config = Config::default(); + config.rcc.hse = Some(rcc::Hse { + freq: Hertz(16_000_000), + mode: rcc::HseMode::Oscillator, + }); + config.rcc.pll1 = Some(rcc::Pll { + source: rcc::PllSource::HSE, + prediv: rcc::PllPreDiv::DIV1, + mul: rcc::PllMul::MUL10, + divp: None, + divq: None, + divr: Some(rcc::PllDiv::DIV1), + }); + config.rcc.sys = rcc::Sysclk::PLL1_R; // 160 Mhz + config.rcc.pll3 = Some(rcc::Pll { + source: rcc::PllSource::HSE, + prediv: rcc::PllPreDiv::DIV4, // PLL_M + mul: rcc::PllMul::MUL125, // PLL_N + divp: None, + divq: None, + divr: Some(rcc::PllDiv::DIV20), + }); + config.rcc.mux.ltdcsel = rcc::mux::Ltdcsel::PLL3_R; // 25 MHz + embassy_stm32::init(config) + } +} + +mod bouncy_box { + use embedded_graphics::geometry::Point; + use embedded_graphics::primitives::Rectangle; + + enum Direction { + DownLeft, + DownRight, + UpLeft, + UpRight, + } + + pub struct BouncyBox { + direction: Direction, + child_rect: Rectangle, + parent_rect: Rectangle, + current_point: Point, + move_by: usize, + } + + // This calculates the coordinates of a chile rectangle bounced around inside a parent bounded box + impl BouncyBox { + pub fn new(child_rect: Rectangle, parent_rect: Rectangle, move_by: usize) -> Self { + let center_box = parent_rect.center(); + let center_img = child_rect.center(); + let current_point = Point::new(center_box.x - center_img.x / 2, center_box.y - center_img.y / 2); + Self { + direction: Direction::DownRight, + child_rect, + parent_rect, + current_point, + move_by, + } + } + + pub fn next_point(&mut self) -> Point { + let direction = &self.direction; + let img_height = self.child_rect.size.height as i32; + let box_height = self.parent_rect.size.height as i32; + let img_width = self.child_rect.size.width as i32; + let box_width = self.parent_rect.size.width as i32; + let move_by = self.move_by as i32; + + match direction { + Direction::DownLeft => { + self.current_point.x -= move_by; + self.current_point.y += move_by; + + let x_out_of_bounds = self.current_point.x < 0; + let y_out_of_bounds = (self.current_point.y + img_height) > box_height; + + if x_out_of_bounds && y_out_of_bounds { + self.direction = Direction::UpRight + } else if x_out_of_bounds && !y_out_of_bounds { + self.direction = Direction::DownRight + } else if !x_out_of_bounds && y_out_of_bounds { + self.direction = Direction::UpLeft + } + } + Direction::DownRight => { + self.current_point.x += move_by; + self.current_point.y += move_by; + + let x_out_of_bounds = (self.current_point.x + img_width) > box_width; + let y_out_of_bounds = (self.current_point.y + img_height) > box_height; + + if x_out_of_bounds && y_out_of_bounds { + self.direction = Direction::UpLeft + } else if x_out_of_bounds && !y_out_of_bounds { + self.direction = Direction::DownLeft + } else if !x_out_of_bounds && y_out_of_bounds { + self.direction = Direction::UpRight + } + } + Direction::UpLeft => { + self.current_point.x -= move_by; + self.current_point.y -= move_by; + + let x_out_of_bounds = self.current_point.x < 0; + let y_out_of_bounds = self.current_point.y < 0; + + if x_out_of_bounds && y_out_of_bounds { + self.direction = Direction::DownRight + } else if x_out_of_bounds && !y_out_of_bounds { + self.direction = Direction::UpRight + } else if !x_out_of_bounds && y_out_of_bounds { + self.direction = Direction::DownLeft + } + } + Direction::UpRight => { + self.current_point.x += move_by; + self.current_point.y -= move_by; + + let x_out_of_bounds = (self.current_point.x + img_width) > box_width; + let y_out_of_bounds = self.current_point.y < 0; + + if x_out_of_bounds && y_out_of_bounds { + self.direction = Direction::DownLeft + } else if x_out_of_bounds && !y_out_of_bounds { + self.direction = Direction::UpLeft + } else if !x_out_of_bounds && y_out_of_bounds { + self.direction = Direction::DownRight + } + } + } + + self.current_point + } + } +}