Add LTDC example for STM32U5G9J-DK2 demo board

This commit is contained in:
William 2024-10-25 15:40:18 +02:00
parent 45e7a7a55a
commit be52224211
3 changed files with 464 additions and 0 deletions

View File

@ -21,6 +21,8 @@ cortex-m-rt = "0.7.0"
embedded-hal = "0.2.6" embedded-hal = "0.2.6"
panic-probe = { version = "0.3", features = ["print-defmt"] } panic-probe = { version = "0.3", features = ["print-defmt"] }
heapless = { version = "0.8", default-features = false } heapless = { version = "0.8", default-features = false }
embedded-graphics = { version = "0.8.1" }
tinybmp = { version = "0.6.0" }
micromath = "2.0.0" micromath = "2.0.0"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -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<peripherals::LTDC>;
});
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(&ltdc_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<Rgb888> = 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<Rgb888>) -> FnvIndexMap<u32, u8, NUM_COLORS> {
let mut color_map: FnvIndexMap<u32, u8, NUM_COLORS> = 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<u32, u8, NUM_COLORS>) -> [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<u32, u8, NUM_COLORS>,
}
impl DoubleBuffer {
pub fn new(
buf0: &'static mut [TargetPixelType],
buf1: &'static mut [TargetPixelType],
layer_config: LtdcLayerConfig,
color_map: FnvIndexMap<u32, u8, NUM_COLORS>,
) -> Self {
Self {
buf0,
buf1,
is_buf0: true,
layer_config,
color_map,
}
}
pub fn current(&mut self) -> (&FnvIndexMap<u32, u8, NUM_COLORS>, &mut [TargetPixelType]) {
if self.is_buf0 {
(&self.color_map, self.buf0)
} else {
(&self.color_map, self.buf1)
}
}
pub async fn swap<T: ltdc::Instance>(&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<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
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
}
}
}