aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorDavid Haig <[email protected]>2024-06-27 20:13:20 +0100
committerDavid Haig <[email protected]>2024-06-27 20:13:20 +0100
commit0e84bd8a91095c035b1b2fdf0c0d8d95b736cc9f (patch)
tree43aa42333cecdca822bac3555b1ecada345219fb /examples
parent26e660722cca9151e5a9331c328421145509ab20 (diff)
Add support for the stm32 ltdc display peripheral
Diffstat (limited to 'examples')
-rw-r--r--examples/stm32h7/Cargo.toml2
-rw-r--r--examples/stm32h7/src/bin/ferris.bmpbin0 -> 6794 bytes
-rw-r--r--examples/stm32h7/src/bin/ltdc.rs484
3 files changed, 486 insertions, 0 deletions
diff --git a/examples/stm32h7/Cargo.toml b/examples/stm32h7/Cargo.toml
index 0584f3916..6f3007492 100644
--- a/examples/stm32h7/Cargo.toml
+++ b/examples/stm32h7/Cargo.toml
@@ -34,6 +34,8 @@ stm32-fmc = "0.3.0"
34embedded-storage = "0.3.1" 34embedded-storage = "0.3.1"
35static_cell = "2" 35static_cell = "2"
36chrono = { version = "^0.4", default-features = false } 36chrono = { version = "^0.4", default-features = false }
37embedded-graphics = { version = "0.8.1" }
38tinybmp = { version = "0.5" }
37 39
38# cargo build/run 40# cargo build/run
39[profile.dev] 41[profile.dev]
diff --git a/examples/stm32h7/src/bin/ferris.bmp b/examples/stm32h7/src/bin/ferris.bmp
new file mode 100644
index 000000000..7a222ab84
--- /dev/null
+++ b/examples/stm32h7/src/bin/ferris.bmp
Binary files differ
diff --git a/examples/stm32h7/src/bin/ltdc.rs b/examples/stm32h7/src/bin/ltdc.rs
new file mode 100644
index 000000000..3bd307012
--- /dev/null
+++ b/examples/stm32h7/src/bin/ltdc.rs
@@ -0,0 +1,484 @@
1#![no_std]
2#![no_main]
3#![macro_use]
4#![allow(static_mut_refs)]
5
6/// This example demonstrates the LTDC lcd display peripheral and was tested to run on an stm32h735g-dk (embassy-stm32 feature "stm32h735ig" and probe-rs chip "STM32H735IGKx")
7/// Even though the dev kit has 16MB of attached PSRAM this example uses the 320KB of internal AXIS RAM found on the mcu itself to make the example more standalone and portable.
8/// For this reason a 256 color lookup table had to be used to keep the memory requirement down to an acceptable level.
9/// The example bounces a ferris crab bitmap around the screen while blinking an led on another task
10///
11use bouncy_box::BouncyBox;
12use defmt::{info, unwrap};
13use embassy_executor::Spawner;
14use embassy_stm32::{
15 bind_interrupts,
16 gpio::{Level, Output, Speed},
17 ltdc::{self, Ltdc, LtdcConfiguration, LtdcLayer, LtdcLayerConfig, PolarityActive, PolarityEdge},
18 peripherals,
19};
20use embassy_time::{Duration, Timer};
21use embedded_graphics::{
22 draw_target::DrawTarget,
23 geometry::{OriginDimensions, Point, Size},
24 image::Image,
25 pixelcolor::{raw::RawU24, Rgb888},
26 prelude::*,
27 primitives::Rectangle,
28 Pixel,
29};
30use heapless::{Entry, FnvIndexMap};
31use tinybmp::Bmp;
32use {defmt_rtt as _, panic_probe as _};
33
34const DISPLAY_WIDTH: usize = 480;
35const DISPLAY_HEIGHT: usize = 272;
36const MY_TASK_POOL_SIZE: usize = 2;
37
38// the following two display buffers consume 261120 bytes that just about fits into axis ram found on the mcu
39// see memory.x linker script
40pub static mut FB1: [TargetPixelType; DISPLAY_WIDTH * DISPLAY_HEIGHT] = [0; DISPLAY_WIDTH * DISPLAY_HEIGHT];
41pub static mut FB2: [TargetPixelType; DISPLAY_WIDTH * DISPLAY_HEIGHT] = [0; DISPLAY_WIDTH * DISPLAY_HEIGHT];
42
43// a basic memory.x linker script for the stm32h735g-dk is as follows:
44/*
45MEMORY
46{
47 FLASH : ORIGIN = 0x08000000, LENGTH = 1024K
48 RAM : ORIGIN = 0x24000000, LENGTH = 320K
49}
50*/
51
52bind_interrupts!(struct Irqs {
53 LTDC => ltdc::InterruptHandler<peripherals::LTDC>;
54});
55
56const NUM_COLORS: usize = 256;
57
58#[embassy_executor::main]
59async fn main(spawner: Spawner) {
60 let p = rcc_setup::stm32h735g_init();
61
62 // blink the led on another task
63 let led = Output::new(p.PC3, Level::High, Speed::Low);
64 unwrap!(spawner.spawn(led_task(led)));
65
66 // numbers from STMicroelectronics/STM32CubeH7 STM32H735G-DK C-based example
67 const RK043FN48H_HSYNC: u16 = 41; // Horizontal synchronization
68 const RK043FN48H_HBP: u16 = 13; // Horizontal back porch
69 const RK043FN48H_HFP: u16 = 32; // Horizontal front porch
70 const RK043FN48H_VSYNC: u16 = 10; // Vertical synchronization
71 const RK043FN48H_VBP: u16 = 2; // Vertical back porch
72 const RK043FN48H_VFP: u16 = 2; // Vertical front porch
73
74 let ltdc_config = LtdcConfiguration {
75 active_width: DISPLAY_WIDTH as _,
76 active_height: DISPLAY_HEIGHT as _,
77 h_back_porch: RK043FN48H_HBP - 11, // -11 from MX_LTDC_Init
78 h_front_porch: RK043FN48H_HFP,
79 v_back_porch: RK043FN48H_VBP,
80 v_front_porch: RK043FN48H_VFP,
81 h_sync: RK043FN48H_HSYNC,
82 v_sync: RK043FN48H_VSYNC,
83 h_sync_polarity: PolarityActive::ActiveLow,
84 v_sync_polarity: PolarityActive::ActiveLow,
85 data_enable_polarity: PolarityActive::ActiveHigh,
86 pixel_clock_polarity: PolarityEdge::FallingEdge,
87 };
88
89 info!("init ltdc");
90 let mut ltdc = Ltdc::new(
91 p.LTDC, Irqs, p.PG7, p.PC6, p.PA4, p.PG14, p.PD0, p.PD6, p.PA8, p.PE12, p.PA3, p.PB8, p.PB9, p.PB1, p.PB0,
92 p.PA6, p.PE11, p.PH15, p.PH4, p.PC7, p.PD3, p.PE0, p.PH3, p.PH8, p.PH9, p.PH10, p.PH11, p.PE1, p.PE15,
93 );
94 ltdc.init(&ltdc_config);
95
96 // we only need to draw on one layer for this example (not to be confused with the double buffer)
97 info!("enable bottom layer");
98 let layer_config = LtdcLayerConfig {
99 pixel_format: ltdc::PixelFormat::L8, // 1 byte per pixel
100 layer: LtdcLayer::Layer1,
101 window_x0: 0,
102 window_x1: DISPLAY_WIDTH as _,
103 window_y0: 0,
104 window_y1: DISPLAY_HEIGHT as _,
105 };
106
107 let ferris_bmp: Bmp<Rgb888> = Bmp::from_slice(include_bytes!("./ferris.bmp")).unwrap();
108 let color_map = build_color_lookup_map(&ferris_bmp);
109 let clut = build_clut(&color_map);
110
111 // enable the bottom layer with a 256 color lookup table
112 ltdc.enable_layer(&layer_config, Some(&clut));
113
114 // Safety: the DoubleBuffer controls access to the statically allocated frame buffers
115 // and it is the only thing that mutates their content
116 let mut double_buffer = DoubleBuffer::new(
117 unsafe { FB1.as_mut() },
118 unsafe { FB2.as_mut() },
119 layer_config,
120 color_map,
121 );
122
123 // this allows us to perform some simple animation for every frame
124 let mut bouncy_box = BouncyBox::new(
125 ferris_bmp.bounding_box(),
126 Rectangle::new(Point::zero(), Size::new(DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32)),
127 2,
128 );
129
130 loop {
131 // cpu intensive drawing to the buffer that is NOT currently being copied to the LCD screen
132 double_buffer.clear();
133 let position = bouncy_box.next_point();
134 let ferris = Image::new(&ferris_bmp, position);
135 unwrap!(ferris.draw(&mut double_buffer));
136
137 // perform async dma data transfer to the lcd screen
138 unwrap!(double_buffer.swap(&mut ltdc).await);
139 }
140}
141
142/// builds the color look-up table from all unique colors found in the bitmap. This should be a 256 color indexed bitmap to work.
143fn build_color_lookup_map(bmp: &Bmp<Rgb888>) -> FnvIndexMap<u32, u8, NUM_COLORS> {
144 let mut color_map: FnvIndexMap<u32, u8, NUM_COLORS> = heapless::FnvIndexMap::new();
145 let mut counter: u8 = 0;
146
147 // add black to position 0
148 color_map.insert(Rgb888::new(0, 0, 0).into_storage(), counter).unwrap();
149 counter += 1;
150
151 for Pixel(_point, color) in bmp.pixels() {
152 let raw = color.into_storage();
153 if let Entry::Vacant(v) = color_map.entry(raw) {
154 v.insert(counter).expect("more than 256 colors detected");
155 counter += 1;
156 }
157 }
158 color_map
159}
160
161/// builds the color look-up table from the color map provided
162fn build_clut(color_map: &FnvIndexMap<u32, u8, NUM_COLORS>) -> [ltdc::RgbColor; NUM_COLORS] {
163 let mut clut = [ltdc::RgbColor::default(); NUM_COLORS];
164 for (color, index) in color_map.iter() {
165 let color = Rgb888::from(RawU24::new(*color));
166 clut[*index as usize] = ltdc::RgbColor {
167 red: color.r(),
168 green: color.g(),
169 blue: color.b(),
170 };
171 }
172
173 clut
174}
175
176#[embassy_executor::task(pool_size = MY_TASK_POOL_SIZE)]
177async fn led_task(mut led: Output<'static>) {
178 let mut counter = 0;
179 loop {
180 info!("blink: {}", counter);
181 counter += 1;
182
183 // on
184 led.set_low();
185 Timer::after(Duration::from_millis(50)).await;
186
187 // off
188 led.set_high();
189 Timer::after(Duration::from_millis(450)).await;
190 }
191}
192
193pub type TargetPixelType = u8;
194
195// A simple double buffer
196pub struct DoubleBuffer {
197 buf0: &'static mut [TargetPixelType],
198 buf1: &'static mut [TargetPixelType],
199 is_buf0: bool,
200 layer_config: LtdcLayerConfig,
201 color_map: FnvIndexMap<u32, u8, NUM_COLORS>,
202}
203
204impl DoubleBuffer {
205 pub fn new(
206 buf0: &'static mut [TargetPixelType],
207 buf1: &'static mut [TargetPixelType],
208 layer_config: LtdcLayerConfig,
209 color_map: FnvIndexMap<u32, u8, NUM_COLORS>,
210 ) -> Self {
211 Self {
212 buf0,
213 buf1,
214 is_buf0: true,
215 layer_config,
216 color_map,
217 }
218 }
219
220 pub fn current(&mut self) -> (&FnvIndexMap<u32, u8, NUM_COLORS>, &mut [TargetPixelType]) {
221 if self.is_buf0 {
222 (&self.color_map, self.buf0)
223 } else {
224 (&self.color_map, self.buf1)
225 }
226 }
227
228 pub async fn swap<T: ltdc::Instance>(&mut self, ltdc: &mut Ltdc<'_, T>) -> Result<(), ltdc::Error> {
229 let (_, buf) = self.current();
230 let frame_buffer = buf.as_ptr();
231 self.is_buf0 = !self.is_buf0;
232 ltdc.set_buffer(self.layer_config.layer, frame_buffer as *const _).await
233 }
234
235 /// Clears the buffer
236 pub fn clear(&mut self) {
237 let (color_map, buf) = self.current();
238 let black = Rgb888::new(0, 0, 0).into_storage();
239 let color_index = color_map.get(&black).expect("no black found in the color map");
240
241 for a in buf.iter_mut() {
242 *a = *color_index; // solid black
243 }
244 }
245}
246
247// Implement DrawTarget for
248impl DrawTarget for DoubleBuffer {
249 type Color = Rgb888;
250 type Error = ();
251
252 /// Draw a pixel
253 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
254 where
255 I: IntoIterator<Item = Pixel<Self::Color>>,
256 {
257 let size = self.size();
258 let width = size.width as i32;
259 let height = size.height as i32;
260 let (color_map, buf) = self.current();
261
262 for pixel in pixels {
263 let Pixel(point, color) = pixel;
264
265 if point.x >= 0 && point.y >= 0 && point.x < width && point.y < height {
266 let index = point.y * width + point.x;
267 let raw_color = color.into_storage();
268
269 match color_map.get(&raw_color) {
270 Some(x) => {
271 buf[index as usize] = *x;
272 }
273 None => panic!("color not found in color map: {}", raw_color),
274 };
275 } else {
276 // Ignore invalid points
277 }
278 }
279
280 Ok(())
281 }
282}
283
284impl OriginDimensions for DoubleBuffer {
285 /// Return the size of the display
286 fn size(&self) -> Size {
287 Size::new(
288 (self.layer_config.window_x1 - self.layer_config.window_x0) as _,
289 (self.layer_config.window_y1 - self.layer_config.window_y0) as _,
290 )
291 }
292}
293
294mod rcc_setup {
295
296 use embassy_stm32::{rcc::*, Peripherals};
297 use embassy_stm32::{
298 rcc::{Hse, HseMode},
299 time::Hertz,
300 Config,
301 };
302
303 /// Sets up clocks for the stm32h735g mcu
304 /// change this if you plan to use a different microcontroller
305 pub fn stm32h735g_init() -> Peripherals {
306 /*
307 https://github.com/STMicroelectronics/STM32CubeH7/blob/master/Projects/STM32H735G-DK/Examples/GPIO/GPIO_EXTI/Src/main.c
308 @brief System Clock Configuration
309 The system Clock is configured as follow :
310 System Clock source = PLL (HSE)
311 SYSCLK(Hz) = 520000000 (CPU Clock)
312 HCLK(Hz) = 260000000 (AXI and AHBs Clock)
313 AHB Prescaler = 2
314 D1 APB3 Prescaler = 2 (APB3 Clock 130MHz)
315 D2 APB1 Prescaler = 2 (APB1 Clock 130MHz)
316 D2 APB2 Prescaler = 2 (APB2 Clock 130MHz)
317 D3 APB4 Prescaler = 2 (APB4 Clock 130MHz)
318 HSE Frequency(Hz) = 25000000
319 PLL_M = 5
320 PLL_N = 104
321 PLL_P = 1
322 PLL_Q = 4
323 PLL_R = 2
324 VDD(V) = 3.3
325 Flash Latency(WS) = 3
326 */
327
328 // setup power and clocks for an stm32h735g-dk run from an external 25 Mhz external oscillator
329 let mut config = Config::default();
330 config.rcc.hse = Some(Hse {
331 freq: Hertz::mhz(25),
332 mode: HseMode::Oscillator,
333 });
334 config.rcc.hsi = None;
335 config.rcc.csi = false;
336 config.rcc.pll1 = Some(Pll {
337 source: PllSource::HSE,
338 prediv: PllPreDiv::DIV5, // PLL_M
339 mul: PllMul::MUL104, // PLL_N
340 divp: Some(PllDiv::DIV1),
341 divq: Some(PllDiv::DIV4),
342 divr: Some(PllDiv::DIV2),
343 });
344 // numbers adapted from Drivers/BSP/STM32H735G-DK/stm32h735g_discovery_ospi.c
345 // MX_OSPI_ClockConfig
346 config.rcc.pll2 = Some(Pll {
347 source: PllSource::HSE,
348 prediv: PllPreDiv::DIV5, // PLL_M
349 mul: PllMul::MUL80, // PLL_N
350 divp: Some(PllDiv::DIV5),
351 divq: Some(PllDiv::DIV2),
352 divr: Some(PllDiv::DIV2),
353 });
354 // numbers adapted from Drivers/BSP/STM32H735G-DK/stm32h735g_discovery_lcd.c
355 // MX_LTDC_ClockConfig
356 config.rcc.pll3 = Some(Pll {
357 source: PllSource::HSE,
358 prediv: PllPreDiv::DIV5, // PLL_M
359 mul: PllMul::MUL160, // PLL_N
360 divp: Some(PllDiv::DIV2),
361 divq: Some(PllDiv::DIV2),
362 divr: Some(PllDiv::DIV83),
363 });
364 config.rcc.voltage_scale = VoltageScale::Scale0;
365 config.rcc.supply_config = SupplyConfig::DirectSMPS;
366 config.rcc.sys = Sysclk::PLL1_P;
367 config.rcc.ahb_pre = AHBPrescaler::DIV2;
368 config.rcc.apb1_pre = APBPrescaler::DIV2;
369 config.rcc.apb2_pre = APBPrescaler::DIV2;
370 config.rcc.apb3_pre = APBPrescaler::DIV2;
371 config.rcc.apb4_pre = APBPrescaler::DIV2;
372 let p = embassy_stm32::init(config);
373 p
374 }
375}
376
377mod bouncy_box {
378 use embedded_graphics::{geometry::Point, primitives::Rectangle};
379
380 enum Direction {
381 DownLeft,
382 DownRight,
383 UpLeft,
384 UpRight,
385 }
386
387 pub struct BouncyBox {
388 direction: Direction,
389 child_rect: Rectangle,
390 parent_rect: Rectangle,
391 current_point: Point,
392 move_by: usize,
393 }
394
395 // This calculates the coordinates of a chile rectangle bounced around inside a parent bounded box
396 impl BouncyBox {
397 pub fn new(child_rect: Rectangle, parent_rect: Rectangle, move_by: usize) -> Self {
398 let center_box = parent_rect.center();
399 let center_img = child_rect.center();
400 let current_point = Point::new(center_box.x - center_img.x / 2, center_box.y - center_img.y / 2);
401 Self {
402 direction: Direction::DownRight,
403 child_rect,
404 parent_rect,
405 current_point,
406 move_by,
407 }
408 }
409
410 pub fn next_point(&mut self) -> Point {
411 let direction = &self.direction;
412 let img_height = self.child_rect.size.height as i32;
413 let box_height = self.parent_rect.size.height as i32;
414 let img_width = self.child_rect.size.width as i32;
415 let box_width = self.parent_rect.size.width as i32;
416 let move_by = self.move_by as i32;
417
418 match direction {
419 Direction::DownLeft => {
420 self.current_point.x -= move_by;
421 self.current_point.y += move_by;
422
423 let x_out_of_bounds = self.current_point.x < 0;
424 let y_out_of_bounds = (self.current_point.y + img_height) > box_height;
425
426 if x_out_of_bounds && y_out_of_bounds {
427 self.direction = Direction::UpRight
428 } else if x_out_of_bounds && !y_out_of_bounds {
429 self.direction = Direction::DownRight
430 } else if !x_out_of_bounds && y_out_of_bounds {
431 self.direction = Direction::UpLeft
432 }
433 }
434 Direction::DownRight => {
435 self.current_point.x += move_by;
436 self.current_point.y += move_by;
437
438 let x_out_of_bounds = (self.current_point.x + img_width) > box_width;
439 let y_out_of_bounds = (self.current_point.y + img_height) > box_height;
440
441 if x_out_of_bounds && y_out_of_bounds {
442 self.direction = Direction::UpLeft
443 } else if x_out_of_bounds && !y_out_of_bounds {
444 self.direction = Direction::DownLeft
445 } else if !x_out_of_bounds && y_out_of_bounds {
446 self.direction = Direction::UpRight
447 }
448 }
449 Direction::UpLeft => {
450 self.current_point.x -= move_by;
451 self.current_point.y -= move_by;
452
453 let x_out_of_bounds = self.current_point.x < 0;
454 let y_out_of_bounds = self.current_point.y < 0;
455
456 if x_out_of_bounds && y_out_of_bounds {
457 self.direction = Direction::DownRight
458 } else if x_out_of_bounds && !y_out_of_bounds {
459 self.direction = Direction::UpRight
460 } else if !x_out_of_bounds && y_out_of_bounds {
461 self.direction = Direction::DownLeft
462 }
463 }
464 Direction::UpRight => {
465 self.current_point.x += move_by;
466 self.current_point.y -= move_by;
467
468 let x_out_of_bounds = (self.current_point.x + img_width) > box_width;
469 let y_out_of_bounds = self.current_point.y < 0;
470
471 if x_out_of_bounds && y_out_of_bounds {
472 self.direction = Direction::DownLeft
473 } else if x_out_of_bounds && !y_out_of_bounds {
474 self.direction = Direction::UpLeft
475 } else if !x_out_of_bounds && y_out_of_bounds {
476 self.direction = Direction::DownRight
477 }
478 }
479 }
480
481 self.current_point
482 }
483 }
484}