aboutsummaryrefslogtreecommitdiff
path: root/examples/stm32u5/src
diff options
context:
space:
mode:
authorWilliam <[email protected]>2024-10-25 15:40:18 +0200
committerWilliam <[email protected]>2024-10-25 15:40:18 +0200
commitbe5222421177b632cd120272e08156b64cc0b886 (patch)
tree060f09df5e8625eaa87d353c06535d7cdef1cf55 /examples/stm32u5/src
parent45e7a7a55aa6b5b8d41d81949b75b1b4a154f9bd (diff)
Add LTDC example for STM32U5G9J-DK2 demo board
Diffstat (limited to 'examples/stm32u5/src')
-rw-r--r--examples/stm32u5/src/bin/ferris.bmpbin0 -> 6794 bytes
-rw-r--r--examples/stm32u5/src/bin/ltdc.rs462
2 files changed, 462 insertions, 0 deletions
diff --git a/examples/stm32u5/src/bin/ferris.bmp b/examples/stm32u5/src/bin/ferris.bmp
new file mode 100644
index 000000000..7a222ab84
--- /dev/null
+++ b/examples/stm32u5/src/bin/ferris.bmp
Binary files differ
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 @@
1#![no_std]
2#![no_main]
3#![macro_use]
4#![allow(static_mut_refs)]
5
6/// This example was derived from examples\stm32h735\src\bin\ltdc.rs
7/// 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")
8///
9use bouncy_box::BouncyBox;
10use defmt::{info, unwrap};
11use embassy_executor::Spawner;
12use embassy_stm32::gpio::{Level, Output, Speed};
13use embassy_stm32::ltdc::{self, Ltdc, LtdcConfiguration, LtdcLayer, LtdcLayerConfig, PolarityActive, PolarityEdge};
14use embassy_stm32::{bind_interrupts, peripherals};
15use embassy_time::{Duration, Timer};
16use embedded_graphics::draw_target::DrawTarget;
17use embedded_graphics::geometry::{OriginDimensions, Point, Size};
18use embedded_graphics::image::Image;
19use embedded_graphics::pixelcolor::raw::RawU24;
20use embedded_graphics::pixelcolor::Rgb888;
21use embedded_graphics::prelude::*;
22use embedded_graphics::primitives::Rectangle;
23use embedded_graphics::Pixel;
24use heapless::{Entry, FnvIndexMap};
25use tinybmp::Bmp;
26use {defmt_rtt as _, panic_probe as _};
27
28const DISPLAY_WIDTH: usize = 800;
29const DISPLAY_HEIGHT: usize = 480;
30const MY_TASK_POOL_SIZE: usize = 2;
31
32// the following two display buffers consume 261120 bytes that just about fits into axis ram found on the mcu
33pub static mut FB1: [TargetPixelType; DISPLAY_WIDTH * DISPLAY_HEIGHT] = [0; DISPLAY_WIDTH * DISPLAY_HEIGHT];
34pub static mut FB2: [TargetPixelType; DISPLAY_WIDTH * DISPLAY_HEIGHT] = [0; DISPLAY_WIDTH * DISPLAY_HEIGHT];
35
36bind_interrupts!(struct Irqs {
37 LTDC => ltdc::InterruptHandler<peripherals::LTDC>;
38});
39
40const NUM_COLORS: usize = 256;
41
42#[embassy_executor::main]
43async fn main(spawner: Spawner) {
44 let p = rcc_setup::stm32u5g9zj_init();
45
46 // enable ICACHE
47 embassy_stm32::pac::ICACHE.cr().write(|w| {
48 w.set_en(true);
49 });
50
51 // blink the led on another task
52 let led = Output::new(p.PD2, Level::High, Speed::Low);
53 unwrap!(spawner.spawn(led_task(led)));
54
55 // numbers from STM32U5G9J-DK2.ioc
56 const RK050HR18H_HSYNC: u16 = 5; // Horizontal synchronization
57 const RK050HR18H_HBP: u16 = 8; // Horizontal back porch
58 const RK050HR18H_HFP: u16 = 8; // Horizontal front porch
59 const RK050HR18H_VSYNC: u16 = 5; // Vertical synchronization
60 const RK050HR18H_VBP: u16 = 8; // Vertical back porch
61 const RK050HR18H_VFP: u16 = 8; // Vertical front porch
62
63 // NOTE: all polarities have to be reversed with respect to the STM32U5G9J-DK2 CubeMX parametrization
64 let ltdc_config = LtdcConfiguration {
65 active_width: DISPLAY_WIDTH as _,
66 active_height: DISPLAY_HEIGHT as _,
67 h_back_porch: RK050HR18H_HBP,
68 h_front_porch: RK050HR18H_HFP,
69 v_back_porch: RK050HR18H_VBP,
70 v_front_porch: RK050HR18H_VFP,
71 h_sync: RK050HR18H_HSYNC,
72 v_sync: RK050HR18H_VSYNC,
73 h_sync_polarity: PolarityActive::ActiveHigh,
74 v_sync_polarity: PolarityActive::ActiveHigh,
75 data_enable_polarity: PolarityActive::ActiveHigh,
76 pixel_clock_polarity: PolarityEdge::RisingEdge,
77 };
78
79 info!("init ltdc");
80 let mut ltdc_de = Output::new(p.PD6, Level::Low, Speed::High);
81 let mut ltdc_disp_ctrl = Output::new(p.PE4, Level::Low, Speed::High);
82 let mut ltdc_bl_ctrl = Output::new(p.PE6, Level::Low, Speed::High);
83 let mut ltdc = Ltdc::new_with_pins(
84 p.LTDC, // PERIPHERAL
85 Irqs, // IRQS
86 p.PD3, // CLK
87 p.PE0, // HSYNC
88 p.PD13, // VSYNC
89 p.PB9, // B0
90 p.PB2, // B1
91 p.PD14, // B2
92 p.PD15, // B3
93 p.PD0, // B4
94 p.PD1, // B5
95 p.PE7, // B6
96 p.PE8, // B7
97 p.PC8, // G0
98 p.PC9, // G1
99 p.PE9, // G2
100 p.PE10, // G3
101 p.PE11, // G4
102 p.PE12, // G5
103 p.PE13, // G6
104 p.PE14, // G7
105 p.PC6, // R0
106 p.PC7, // R1
107 p.PE15, // R2
108 p.PD8, // R3
109 p.PD9, // R4
110 p.PD10, // R5
111 p.PD11, // R6
112 p.PD12, // R7
113 );
114 ltdc.init(&ltdc_config);
115 ltdc_de.set_low();
116 ltdc_bl_ctrl.set_high();
117 ltdc_disp_ctrl.set_high();
118
119 // we only need to draw on one layer for this example (not to be confused with the double buffer)
120 info!("enable bottom layer");
121 let layer_config = LtdcLayerConfig {
122 pixel_format: ltdc::PixelFormat::L8, // 1 byte per pixel
123 layer: LtdcLayer::Layer1,
124 window_x0: 0,
125 window_x1: DISPLAY_WIDTH as _,
126 window_y0: 0,
127 window_y1: DISPLAY_HEIGHT as _,
128 };
129
130 let ferris_bmp: Bmp<Rgb888> = Bmp::from_slice(include_bytes!("./ferris.bmp")).unwrap();
131 let color_map = build_color_lookup_map(&ferris_bmp);
132 let clut = build_clut(&color_map);
133
134 // enable the bottom layer with a 256 color lookup table
135 ltdc.init_layer(&layer_config, Some(&clut));
136
137 // Safety: the DoubleBuffer controls access to the statically allocated frame buffers
138 // and it is the only thing that mutates their content
139 let mut double_buffer = DoubleBuffer::new(
140 unsafe { FB1.as_mut() },
141 unsafe { FB2.as_mut() },
142 layer_config,
143 color_map,
144 );
145
146 // this allows us to perform some simple animation for every frame
147 let mut bouncy_box = BouncyBox::new(
148 ferris_bmp.bounding_box(),
149 Rectangle::new(Point::zero(), Size::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32)),
150 2,
151 );
152
153 loop {
154 // cpu intensive drawing to the buffer that is NOT currently being copied to the LCD screen
155 double_buffer.clear();
156 let position = bouncy_box.next_point();
157 let ferris = Image::new(&ferris_bmp, position);
158 unwrap!(ferris.draw(&mut double_buffer));
159
160 // perform async dma data transfer to the lcd screen
161 unwrap!(double_buffer.swap(&mut ltdc).await);
162 }
163}
164
165/// builds the color look-up table from all unique colors found in the bitmap. This should be a 256 color indexed bitmap to work.
166fn build_color_lookup_map(bmp: &Bmp<Rgb888>) -> FnvIndexMap<u32, u8, NUM_COLORS> {
167 let mut color_map: FnvIndexMap<u32, u8, NUM_COLORS> = heapless::FnvIndexMap::new();
168 let mut counter: u8 = 0;
169
170 // add black to position 0
171 color_map.insert(Rgb888::new(0, 0, 0).into_storage(), counter).unwrap();
172 counter += 1;
173
174 for Pixel(_point, color) in bmp.pixels() {
175 let raw = color.into_storage();
176 if let Entry::Vacant(v) = color_map.entry(raw) {
177 v.insert(counter).expect("more than 256 colors detected");
178 counter += 1;
179 }
180 }
181 color_map
182}
183
184/// builds the color look-up table from the color map provided
185fn build_clut(color_map: &FnvIndexMap<u32, u8, NUM_COLORS>) -> [ltdc::RgbColor; NUM_COLORS] {
186 let mut clut = [ltdc::RgbColor::default(); NUM_COLORS];
187 for (color, index) in color_map.iter() {
188 let color = Rgb888::from(RawU24::new(*color));
189 clut[*index as usize] = ltdc::RgbColor {
190 red: color.r(),
191 green: color.g(),
192 blue: color.b(),
193 };
194 }
195
196 clut
197}
198
199#[embassy_executor::task(pool_size = MY_TASK_POOL_SIZE)]
200async fn led_task(mut led: Output<'static>) {
201 let mut counter = 0;
202 loop {
203 info!("blink: {}", counter);
204 counter += 1;
205
206 // on
207 led.set_low();
208 Timer::after(Duration::from_millis(50)).await;
209
210 // off
211 led.set_high();
212 Timer::after(Duration::from_millis(450)).await;
213 }
214}
215
216pub type TargetPixelType = u8;
217
218// A simple double buffer
219pub struct DoubleBuffer {
220 buf0: &'static mut [TargetPixelType],
221 buf1: &'static mut [TargetPixelType],
222 is_buf0: bool,
223 layer_config: LtdcLayerConfig,
224 color_map: FnvIndexMap<u32, u8, NUM_COLORS>,
225}
226
227impl DoubleBuffer {
228 pub fn new(
229 buf0: &'static mut [TargetPixelType],
230 buf1: &'static mut [TargetPixelType],
231 layer_config: LtdcLayerConfig,
232 color_map: FnvIndexMap<u32, u8, NUM_COLORS>,
233 ) -> Self {
234 Self {
235 buf0,
236 buf1,
237 is_buf0: true,
238 layer_config,
239 color_map,
240 }
241 }
242
243 pub fn current(&mut self) -> (&FnvIndexMap<u32, u8, NUM_COLORS>, &mut [TargetPixelType]) {
244 if self.is_buf0 {
245 (&self.color_map, self.buf0)
246 } else {
247 (&self.color_map, self.buf1)
248 }
249 }
250
251 pub async fn swap<T: ltdc::Instance>(&mut self, ltdc: &mut Ltdc<'_, T>) -> Result<(), ltdc::Error> {
252 let (_, buf) = self.current();
253 let frame_buffer = buf.as_ptr();
254 self.is_buf0 = !self.is_buf0;
255 ltdc.set_buffer(self.layer_config.layer, frame_buffer as *const _).await
256 }
257
258 /// Clears the buffer
259 pub fn clear(&mut self) {
260 let (color_map, buf) = self.current();
261 let black = Rgb888::new(0, 0, 0).into_storage();
262 let color_index = color_map.get(&black).expect("no black found in the color map");
263
264 for a in buf.iter_mut() {
265 *a = *color_index; // solid black
266 }
267 }
268}
269
270// Implement DrawTarget for
271impl DrawTarget for DoubleBuffer {
272 type Color = Rgb888;
273 type Error = ();
274
275 /// Draw a pixel
276 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
277 where
278 I: IntoIterator<Item = Pixel<Self::Color>>,
279 {
280 let size = self.size();
281 let width = size.width as i32;
282 let height = size.height as i32;
283 let (color_map, buf) = self.current();
284
285 for pixel in pixels {
286 let Pixel(point, color) = pixel;
287
288 if point.x >= 0 && point.y >= 0 && point.x < width && point.y < height {
289 let index = point.y * width + point.x;
290 let raw_color = color.into_storage();
291
292 match color_map.get(&raw_color) {
293 Some(x) => {
294 buf[index as usize] = *x;
295 }
296 None => panic!("color not found in color map: {}", raw_color),
297 };
298 } else {
299 // Ignore invalid points
300 }
301 }
302
303 Ok(())
304 }
305}
306
307impl OriginDimensions for DoubleBuffer {
308 /// Return the size of the display
309 fn size(&self) -> Size {
310 Size::new(
311 (self.layer_config.window_x1 - self.layer_config.window_x0) as _,
312 (self.layer_config.window_y1 - self.layer_config.window_y0) as _,
313 )
314 }
315}
316
317mod rcc_setup {
318
319 use embassy_stm32::rcc;
320 use embassy_stm32::time::Hertz;
321 use embassy_stm32::{Config, Peripherals};
322
323 /// Sets up clocks for the stm32u5g9zj mcu
324 /// change this if you plan to use a different microcontroller
325 pub fn stm32u5g9zj_init() -> Peripherals {
326 // setup power and clocks for an STM32U5G9J-DK2 run from an external 16 Mhz external oscillator
327 let mut config = Config::default();
328 config.rcc.hse = Some(rcc::Hse {
329 freq: Hertz(16_000_000),
330 mode: rcc::HseMode::Oscillator,
331 });
332 config.rcc.pll1 = Some(rcc::Pll {
333 source: rcc::PllSource::HSE,
334 prediv: rcc::PllPreDiv::DIV1,
335 mul: rcc::PllMul::MUL10,
336 divp: None,
337 divq: None,
338 divr: Some(rcc::PllDiv::DIV1),
339 });
340 config.rcc.sys = rcc::Sysclk::PLL1_R; // 160 Mhz
341 config.rcc.pll3 = Some(rcc::Pll {
342 source: rcc::PllSource::HSE,
343 prediv: rcc::PllPreDiv::DIV4, // PLL_M
344 mul: rcc::PllMul::MUL125, // PLL_N
345 divp: None,
346 divq: None,
347 divr: Some(rcc::PllDiv::DIV20),
348 });
349 config.rcc.mux.ltdcsel = rcc::mux::Ltdcsel::PLL3_R; // 25 MHz
350 embassy_stm32::init(config)
351 }
352}
353
354mod bouncy_box {
355 use embedded_graphics::geometry::Point;
356 use embedded_graphics::primitives::Rectangle;
357
358 enum Direction {
359 DownLeft,
360 DownRight,
361 UpLeft,
362 UpRight,
363 }
364
365 pub struct BouncyBox {
366 direction: Direction,
367 child_rect: Rectangle,
368 parent_rect: Rectangle,
369 current_point: Point,
370 move_by: usize,
371 }
372
373 // This calculates the coordinates of a chile rectangle bounced around inside a parent bounded box
374 impl BouncyBox {
375 pub fn new(child_rect: Rectangle, parent_rect: Rectangle, move_by: usize) -> Self {
376 let center_box = parent_rect.center();
377 let center_img = child_rect.center();
378 let current_point = Point::new(center_box.x - center_img.x / 2, center_box.y - center_img.y / 2);
379 Self {
380 direction: Direction::DownRight,
381 child_rect,
382 parent_rect,
383 current_point,
384 move_by,
385 }
386 }
387
388 pub fn next_point(&mut self) -> Point {
389 let direction = &self.direction;
390 let img_height = self.child_rect.size.height as i32;
391 let box_height = self.parent_rect.size.height as i32;
392 let img_width = self.child_rect.size.width as i32;
393 let box_width = self.parent_rect.size.width as i32;
394 let move_by = self.move_by as i32;
395
396 match direction {
397 Direction::DownLeft => {
398 self.current_point.x -= move_by;
399 self.current_point.y += move_by;
400
401 let x_out_of_bounds = self.current_point.x < 0;
402 let y_out_of_bounds = (self.current_point.y + img_height) > box_height;
403
404 if x_out_of_bounds && y_out_of_bounds {
405 self.direction = Direction::UpRight
406 } else if x_out_of_bounds && !y_out_of_bounds {
407 self.direction = Direction::DownRight
408 } else if !x_out_of_bounds && y_out_of_bounds {
409 self.direction = Direction::UpLeft
410 }
411 }
412 Direction::DownRight => {
413 self.current_point.x += move_by;
414 self.current_point.y += move_by;
415
416 let x_out_of_bounds = (self.current_point.x + img_width) > box_width;
417 let y_out_of_bounds = (self.current_point.y + img_height) > box_height;
418
419 if x_out_of_bounds && y_out_of_bounds {
420 self.direction = Direction::UpLeft
421 } else if x_out_of_bounds && !y_out_of_bounds {
422 self.direction = Direction::DownLeft
423 } else if !x_out_of_bounds && y_out_of_bounds {
424 self.direction = Direction::UpRight
425 }
426 }
427 Direction::UpLeft => {
428 self.current_point.x -= move_by;
429 self.current_point.y -= move_by;
430
431 let x_out_of_bounds = self.current_point.x < 0;
432 let y_out_of_bounds = self.current_point.y < 0;
433
434 if x_out_of_bounds && y_out_of_bounds {
435 self.direction = Direction::DownRight
436 } else if x_out_of_bounds && !y_out_of_bounds {
437 self.direction = Direction::UpRight
438 } else if !x_out_of_bounds && y_out_of_bounds {
439 self.direction = Direction::DownLeft
440 }
441 }
442 Direction::UpRight => {
443 self.current_point.x += move_by;
444 self.current_point.y -= move_by;
445
446 let x_out_of_bounds = (self.current_point.x + img_width) > box_width;
447 let y_out_of_bounds = self.current_point.y < 0;
448
449 if x_out_of_bounds && y_out_of_bounds {
450 self.direction = Direction::DownLeft
451 } else if x_out_of_bounds && !y_out_of_bounds {
452 self.direction = Direction::UpLeft
453 } else if !x_out_of_bounds && y_out_of_bounds {
454 self.direction = Direction::DownRight
455 }
456 }
457 }
458
459 self.current_point
460 }
461 }
462}