Skip to content

Commit 583a8f2

Browse files
author
amperstand
committed
feat(ltdc): add DSI constructor, framebuffer DrawTarget, and feature updates
1 parent 0aa1b1d commit 583a8f2

File tree

3 files changed

+227
-2
lines changed

3 files changed

+227
-2
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- DSI host: implement missing DCS short write (P0) and Generic short write (P0, P1, P2) commands
13+
- LTDC: add DSI-compatible constructor (`new_dsi()`) for DSI-driven displays that don't need LTDC pin configuration
14+
- LTDC: add `LtdcFramebuffer` with embedded-graphics `DrawTarget` support (behind `framebuffer` feature)
15+
- LTDC: add `layer_buffer_mut()`, `set_layer_transparency()`, `set_layer_buffer_address()`, `set_color_keying()` methods
16+
17+
### Changed
18+
19+
- Decouple `otm8009a` from `dsihost` feature flag — `otm8009a` is now a separate opt-in feature that auto-enables `dsihost`
20+
21+
### Fixed
22+
23+
- LTDC: fix ARGB4444 bytes-per-pixel value (was 16, now correctly 2)
24+
1025
## [v0.23.0] - 2025-09-22
1126

1227
- Implement `embedded_hal::i2c::I2c` for `I2cMasterDma` [#838]

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ synopsys-usb-otg = { version = "0.4.0", features = [
4040
sdio-host = { version = "0.9.0", optional = true }
4141
embedded-dma = "0.2.0"
4242
embedded-display-controller = { version = "^0.2.0", optional = true }
43+
otm8009a = { version = "0.1", optional = true }
4344
bare-metal = { version = "1" }
4445
void = { default-features = false, version = "1.0.2" }
4546
display-interface = { version = "0.5.0", optional = true }
@@ -92,6 +93,10 @@ version = "0.6.1"
9293
version = "0.5.0"
9394
optional = true
9495

96+
[dependencies.embedded-graphics-core]
97+
version = "0.4"
98+
optional = true
99+
95100
[dev-dependencies]
96101
static_cell = "2.1.1"
97102
defmt = "1.0.1"
@@ -518,6 +523,7 @@ dfsdm1 = ["dfsdm"]
518523
dfsdm2 = ["dfsdm"]
519524
dma2d = []
520525
dsihost = ["embedded-display-controller"]
526+
otm8009a = ["dep:otm8009a", "dsihost"]
521527
eth = []
522528
fmc = []
523529
fsmc = []
@@ -532,6 +538,7 @@ gpiok = []
532538
i2c3 = []
533539
lptim1 = []
534540
ltdc = ["dep:micromath"]
541+
framebuffer = ["dep:embedded-graphics-core"]
535542
quadspi = []
536543
otg-fs = []
537544
otg-hs = []
@@ -620,6 +627,7 @@ required-features = [
620627
"fsmc_lcd",
621628
] # stm32f413
622629

630+
623631
[[example]]
624632
name = "f469disco-lcd-test"
625633
required-features = ["stm32f469", "defmt"]

src/ltdc.rs

Lines changed: 204 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ use crate::{
1212
};
1313
use fugit::HertzU32 as Hertz;
1414

15+
#[cfg(feature = "framebuffer")]
16+
use embedded_graphics_core::{
17+
draw_target::DrawTarget,
18+
geometry::{OriginDimensions, Size},
19+
pixelcolor::{IntoStorage, Rgb565},
20+
Pixel,
21+
};
22+
1523
/// Display configuration constants
1624
pub struct DisplayConfig {
1725
pub active_width: u16,
@@ -352,6 +360,72 @@ impl<T: 'static + SupportedWord> DisplayController<T> {
352360
}
353361
}
354362

363+
/// Create a DisplayController for DSI-driven displays.
364+
///
365+
/// Unlike [`new()`](Self::new), this constructor does not configure LTDC pins
366+
/// or PLLSAI. On DSI boards the DSI host drives the pixel clock and data
367+
/// lines, so LTDC only needs its timing registers set up.
368+
pub fn new_dsi(
369+
ltdc: LTDC,
370+
dma2d: DMA2D,
371+
pixel_format: PixelFormat,
372+
config: DisplayConfig,
373+
) -> DisplayController<T> {
374+
unsafe {
375+
LTDC::enable_unchecked();
376+
LTDC::reset_unchecked();
377+
DMA2D::enable_unchecked();
378+
DMA2D::reset_unchecked();
379+
}
380+
381+
let total_width: u16 =
382+
config.h_sync + config.h_back_porch + config.active_width + config.h_front_porch - 1;
383+
let total_height: u16 =
384+
config.v_sync + config.v_back_porch + config.active_height + config.v_front_porch - 1;
385+
386+
ltdc.sscr().write(|w| {
387+
w.hsw().set(config.h_sync - 1);
388+
w.vsh().set(config.v_sync - 1)
389+
});
390+
ltdc.bpcr().write(|w| {
391+
w.ahbp().set(config.h_sync + config.h_back_porch - 1);
392+
w.avbp().set(config.v_sync + config.v_back_porch - 1)
393+
});
394+
ltdc.awcr().write(|w| {
395+
w.aaw()
396+
.set(config.h_sync + config.h_back_porch + config.active_width - 1);
397+
w.aah()
398+
.set(config.v_sync + config.v_back_porch + config.active_height - 1)
399+
});
400+
ltdc.twcr().write(|w| {
401+
w.totalw().set(total_width);
402+
w.totalh().set(total_height)
403+
});
404+
405+
ltdc.gcr().write(|w| {
406+
w.hspol().bit(config.h_sync_pol);
407+
w.vspol().bit(config.v_sync_pol);
408+
w.depol().bit(config.no_data_enable_pol);
409+
w.pcpol().bit(config.pixel_clock_pol)
410+
});
411+
412+
ltdc.bccr().write(|w| unsafe { w.bits(0xAAAAAAAA) });
413+
414+
ltdc.srcr().modify(|_, w| w.imr().set_bit());
415+
ltdc.gcr()
416+
.modify(|_, w| w.ltdcen().set_bit().den().set_bit());
417+
ltdc.srcr().modify(|_, w| w.imr().set_bit());
418+
419+
DisplayController {
420+
_ltdc: ltdc,
421+
_dma2d: dma2d,
422+
config,
423+
buffer1: None,
424+
buffer2: None,
425+
pixel_format,
426+
}
427+
}
428+
355429
/// Configure the layer
356430
///
357431
/// Note : the choice is made (for the sake of simplicity) to make the layer
@@ -424,7 +498,7 @@ impl<T: 'static + SupportedWord> DisplayController<T> {
424498
// PixelFormat::RGB888 => 24, unsupported for now because u24 does not exist
425499
PixelFormat::RGB565 => 2,
426500
PixelFormat::ARGB1555 => 2,
427-
PixelFormat::ARGB4444 => 16,
501+
PixelFormat::ARGB4444 => 2,
428502
PixelFormat::L8 => 1,
429503
PixelFormat::AL44 => 1,
430504
PixelFormat::AL88 => 2,
@@ -487,11 +561,64 @@ impl<T: 'static + SupportedWord> DisplayController<T> {
487561
})[x + self.config.active_width as usize * y] = color;
488562
}
489563

564+
/// Get a mutable reference to the layer's framebuffer.
565+
///
566+
/// Returns `None` if the layer has not been configured with [`config_layer()`](Self::config_layer).
567+
pub fn layer_buffer_mut(&mut self, layer: Layer) -> Option<&mut [T]> {
568+
match layer {
569+
Layer::L1 => self.buffer1.as_deref_mut(),
570+
Layer::L2 => self.buffer2.as_deref_mut(),
571+
}
572+
}
573+
574+
/// Set the global alpha (transparency) for a layer.
575+
///
576+
/// `alpha`: 0 = fully transparent, 255 = fully opaque.
577+
/// Takes effect after [`reload()`](Self::reload).
578+
pub fn set_layer_transparency(&self, layer: Layer, alpha: u8) {
579+
self._ltdc
580+
.layer(layer as usize)
581+
.cacr()
582+
.write(|w| w.consta().set(alpha));
583+
self.reload_on_vblank();
584+
}
585+
586+
/// Change the framebuffer address for a layer.
587+
///
588+
/// This can be used for double-buffering by swapping between two
589+
/// pre-allocated framebuffers. Takes effect after [`reload()`](Self::reload).
590+
pub fn set_layer_buffer_address(&self, layer: Layer, address: u32) {
591+
self._ltdc
592+
.layer(layer as usize)
593+
.cfbar()
594+
.write(|w| w.cfbadd().set(address));
595+
self.reload_on_vblank();
596+
}
597+
598+
/// Enable color keying on a layer.
599+
///
600+
/// Pixels matching `color_key` (RGB888 format, 24-bit) become fully
601+
/// transparent, allowing the layer below to show through.
602+
/// Takes effect after [`reload()`](Self::reload).
603+
pub fn set_color_keying(&mut self, layer: Layer, color_key: u32) {
604+
let l = self._ltdc.layer(layer as usize);
605+
l.ckcr()
606+
.write(|w| unsafe { w.bits(color_key & 0x00FF_FFFF) });
607+
l.cr().modify(|_, w| w.colken().set_bit());
608+
self.reload_on_vblank();
609+
}
610+
490611
/// Draw hardware accelerated rectangle
491612
///
492613
/// # Safety
493614
///
494-
/// TODO: use safer DMA transfers
615+
/// The caller must ensure:
616+
/// - The framebuffer for `layer` has been configured via [`config_layer()`](Self::config_layer)
617+
/// - `top_left` and `bottom_right` coordinates are within the display bounds
618+
/// - No other DMA2D operation is in progress
619+
///
620+
/// This method uses DMA2D register-to-memory mode to fill the rectangle.
621+
/// A future version may provide a safe wrapper with proper DMA completion tracking.
495622
pub unsafe fn draw_rectangle(
496623
&mut self,
497624
layer: Layer,
@@ -544,13 +671,88 @@ impl<T: 'static + SupportedWord> DisplayController<T> {
544671
self._dma2d
545672
.cr()
546673
.modify(|_, w| w.mode().bits(0b11).start().set_bit());
674+
// Wait for DMA2D transfer to complete
675+
while self._dma2d.cr().read().start().bit_is_set() {}
547676
}
548677

549678
/// Reload display controller immediatly
550679
pub fn reload(&self) {
551680
// Reload ltdc config immediatly
552681
self._ltdc.srcr().modify(|_, w| w.imr().set_bit());
553682
}
683+
684+
/// Reload display controller on next vertical blanking
685+
pub fn reload_on_vblank(&self) {
686+
self._ltdc.srcr().modify(|_, w| w.vbr().set_bit());
687+
}
688+
}
689+
690+
/// A framebuffer wrapper that implements [`DrawTarget`] for use with
691+
/// `embedded-graphics`.
692+
///
693+
/// `LtdcFramebuffer` owns a `&'static mut [T]` SDRAM buffer and provides
694+
/// pixel-level drawing via the `embedded-graphics` `DrawTarget` trait.
695+
///
696+
/// # Usage
697+
///
698+
/// ```ignore
699+
/// let mut fb = LtdcFramebuffer::new(buffer, 480, 800);
700+
/// fb.clear(Rgb565::BLACK).ok();
701+
/// // ... draw with embedded-graphics ...
702+
/// let buffer = fb.into_inner();
703+
/// display_ctrl.config_layer(Layer::L1, buffer, PixelFormat::RGB565);
704+
/// ```
705+
#[cfg(feature = "framebuffer")]
706+
pub struct LtdcFramebuffer<T: 'static + SupportedWord> {
707+
buf: &'static mut [T],
708+
width: u16,
709+
height: u16,
710+
}
711+
712+
#[cfg(feature = "framebuffer")]
713+
impl<T: 'static + SupportedWord> LtdcFramebuffer<T> {
714+
/// Create a new framebuffer wrapper.
715+
///
716+
/// # Panics
717+
///
718+
/// Panics if `buf.len() != width * height`.
719+
pub fn new(buf: &'static mut [T], width: u16, height: u16) -> Self {
720+
assert!(buf.len() == (width as usize) * (height as usize));
721+
Self { buf, width, height }
722+
}
723+
724+
/// Consume the framebuffer and return the underlying buffer.
725+
pub fn into_inner(self) -> &'static mut [T] {
726+
self.buf
727+
}
728+
}
729+
730+
#[cfg(feature = "framebuffer")]
731+
impl DrawTarget for LtdcFramebuffer<u16> {
732+
type Color = Rgb565;
733+
type Error = core::convert::Infallible;
734+
735+
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
736+
where
737+
I: IntoIterator<Item = Pixel<Self::Color>>,
738+
{
739+
let w = self.width as i32;
740+
let h = self.height as i32;
741+
for Pixel(coord, color) in pixels {
742+
let (x, y): (i32, i32) = coord.into();
743+
if x >= 0 && x < w && y >= 0 && y < h {
744+
self.buf[y as usize * self.width as usize + x as usize] = color.into_storage();
745+
}
746+
}
747+
Ok(())
748+
}
749+
}
750+
751+
#[cfg(feature = "framebuffer")]
752+
impl<T: 'static + SupportedWord> OriginDimensions for LtdcFramebuffer<T> {
753+
fn size(&self) -> Size {
754+
Size::new(self.width as u32, self.height as u32)
755+
}
554756
}
555757

556758
/// Available PixelFormats to work with

0 commit comments

Comments
 (0)