Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5a5c9e8
stm32-neochrom: add embassy driver crate for NemaGFX
leftger Jul 22, 2026
97a36a7
examples/stm32n6: add NeoChrom NemaGFX smoke test
leftger Jul 22, 2026
9d79742
examples/stm32n6: fix unused mut in neochrom example
leftger Jul 22, 2026
14fda65
Merge branch 'embassy-rs:main' into feat/stm32-neochrom
leftger Jul 22, 2026
b993714
stm32/gpu2d: give the error interrupt handler a real body
leftger Jul 23, 2026
421868e
stm32-neochrom: bridge nema-gfx-hal to the real GPU2D driver
leftger Jul 23, 2026
e90e87a
Merge branch 'embassy-rs:main' into feat/stm32-neochrom
leftger Jul 23, 2026
31d2230
feat(neochrom): integrate baremetal GPU2D hardware MMIO bridge and ex…
leftger Jul 24, 2026
c91bb6b
feat(neochrom): add neochrom_lcd end-to-end example with LTDC and RIF…
leftger Jul 24, 2026
5f1841f
Merge branch 'main' into feat/stm32-neochrom
leftger Jul 24, 2026
9608369
feat(neochrom): add fill_rect, draw_line, fill_circle, and blit 2D ac…
leftger Jul 24, 2026
f8a5f89
fix(neochrom): cast NEMA_TEX_BORDER to u8 in blit binding
leftger Jul 24, 2026
b3f99b2
feat(neochrom): add fill_rounded_rect, fill_triangle, fill_quad, blit…
leftger Jul 24, 2026
a35ffc0
feat(neochrom): add embedded-graphics integration and comprehensive G…
leftger Jul 24, 2026
565ca72
Merge branch 'main' into feat/stm32-neochrom
leftger Jul 24, 2026
7ddd3c0
fix(neochrom): depend on published embedded-graphics-core instead of …
leftger Jul 24, 2026
4cd2366
style(neochrom): apply rustfmt formatting
leftger Jul 24, 2026
c7369c3
feat(neochrom): add coherency/command modules, GPU surface support, a…
leftger Jul 28, 2026
2d75bff
WIP
leftger Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions embassy-stm32-neochrom/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
[package]
name = "embassy-stm32-neochrom"
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"
description = "Embassy driver for ST NeoChrom GPU via NemaGFX."
keywords = ["embedded", "async", "stm32", "gpu", "neochrom"]
categories = ["embedded", "hardware-support", "no-std"]
repository = "https://github.com/embassy-rs/embassy"
documentation = "https://docs.embassy.dev/embassy-stm32-neochrom"
readme = "README.md"

publish = false

[package.metadata.embassy]
build = [
{ target = "thumbv8m.main-none-eabihf", features = ["stm32n657x0", "stub-gpu2d"] },
]

[package.metadata.embassy_docs]
src_base = "https://github.com/embassy-rs/embassy/blob/embassy-stm32-neochrom-v$VERSION/embassy-stm32-neochrom/src/"
src_base_git = "https://github.com/embassy-rs/embassy/blob/$COMMIT/embassy-stm32-neochrom/src/"
target = "thumbv8m.main-none-eabihf"
features = ["stm32n657x0", "stub-gpu2d"]

[package.metadata.docs.rs]
features = ["stm32n657x0", "stub-gpu2d"]

[dependencies]
embassy-stm32 = { version = "0.6.0", path = "../embassy-stm32", features = ["unstable-pac"] }
embassy-hal-internal = { version = "0.5.0", path = "../embassy-hal-internal" }
stm32-bindings = { path = "../../stm32-bindings/build/stm32-bindings", optional = true, default-features = false }
nema-gfx-hal = { path = "../../stm32-bindings/nema-gfx-hal", default-features = false }

embedded-graphics-core = { version = "0.4.0", optional = true }

aligned = "0.4.2"
critical-section = "1.2"
heapless = { version = "0.9", default-features = false }

defmt = { version = "1.0.1", optional = true }
log = { version = "0.4.17", optional = true }

[features]
default = ["stub-gpu2d"]
defmt = ["dep:defmt", "embassy-hal-internal/defmt"]
log = ["dep:log"]
embedded-graphics = ["dep:embedded-graphics-core"]

## Use the in-tree GPU2D HAL stub (CI / early bring-up).
stub-gpu2d = ["nema-gfx-hal/stub"]

## NemaGFX core presets (forwarded to stm32-bindings).
neochrom-m55 = ["dep:stm32-bindings", "stm32-bindings/neochrom-m55"]
neochrom-m33-revc = ["dep:stm32-bindings", "stm32-bindings/neochrom-m33-revc"]
neochrom-m33-nemapvg = ["dep:stm32-bindings", "stm32-bindings/neochrom-m33-nemapvg"]
neochrom-m7 = ["dep:stm32-bindings", "stm32-bindings/neochrom-m7"]

## Chip families
n6 = []
u5 = []
h7rs = []

stm32n657x0 = ["embassy-stm32/stm32n657x0", "neochrom-m55", "n6", "nema-gfx-hal/stm32n6"]
stm32n655x0 = ["embassy-stm32/stm32n655x0", "neochrom-m55", "n6", "nema-gfx-hal/stm32n6"]
stm32n647x0 = ["embassy-stm32/stm32n647x0", "neochrom-m55", "n6", "nema-gfx-hal/stm32n6"]

stm32u599nj = ["embassy-stm32/stm32u599nj", "neochrom-m33-revc", "u5", "nema-gfx-hal/stm32u5"]
stm32u5g9nj = ["embassy-stm32/stm32u5g9nj", "neochrom-m33-nemapvg", "u5", "nema-gfx-hal/stm32u5"]

stm32h7s7z8 = ["embassy-stm32/stm32h7s7z8", "neochrom-m7", "h7rs", "nema-gfx-hal/stm32h7rs"]
stm32h7r7z8 = ["embassy-stm32/stm32h7r7z8", "neochrom-m7", "h7rs", "nema-gfx-hal/stm32h7rs"]
45 changes: 45 additions & 0 deletions embassy-stm32-neochrom/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# embassy-stm32-neochrom

Embassy integration for ST NeoChrom (GPU2D) using the [NemaGFX](https://github.com/STMicroelectronics/x-cube-image-processing/tree/main/Middleware/NemaGFX) middleware via [`stm32-bindings`](https://github.com/embassy-rs/stm32-bindings).

## Status

Early bring-up. Provides a safe driver over the generated NemaGFX FFI and the platform HAL from `nema-gfx-hal`.

## Local development

Generate `stm32-bindings` first:

```bash
cd ../stm32-bindings
cargo run --release --bin stm32-bindings-gen -- --module nema_gfx
```

Then build this crate (example chip feature):

```bash
cd ../embassy/embassy-stm32-neochrom
cargo check --target thumbv8m.main-none-eabihf --features stm32n657x0,stub-gpu2d
```

## Features

| Feature | Description |
|---------|-------------|
| `stm32n657x0` | STM32N657 + Cortex-M55 NemaGFX library |
| `stub-gpu2d` | Link GPU2D HAL stub instead of STM32Cube (default) |
| `neochrom-m55` | NemaGFX bindings + M55 prebuilt library only |
| `embedded-graphics` | `NeoChromTarget` DrawTarget wrapper |

Disable `default-features` and `stub-gpu2d` when wiring a real STM32Cube GPU2D HAL on hardware.

## Driver highlights

- **Batched frames**: `begin_frame()` / `*_in_frame()` / `end_frame()` or `end_frame_async().await`
- **Persistent command list**: 8 KiB circular CL (ST Resize_GPU pattern)
- **Error propagation**: `nema_get_error()` and GPU2D `SystemError`
- **Cache coherency**: I-cache invalidate + D-cache clean/invalidate around submissions
- **Stroke APIs**: `draw_stroke_rect`, `draw_stroke_line_aa`, `draw_stroke_triangle_aa`, etc.
- **Textured triangles**: `blit_tri_fit`, `blit_tri_uv`

One-shot helpers such as `clear()`, `fill_rect()`, and `blit()` remain available; they submit immediately when no frame is open.
118 changes: 118 additions & 0 deletions embassy-stm32-neochrom/src/coherency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! CPU cache maintenance for GPU-accessible framebuffers.

use crate::color::ColorFormat;
use crate::ffi::nema_gfx::{NEMA_TEX_BORDER, nema_stride_size, nema_texture_size};
use crate::framebuffer::GpuSurface;

/// Snapshot of a [`GpuSurface`] for post-GPU cache maintenance.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SurfaceSyncInfo {
pub(crate) phys_addr: usize,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) format: ColorFormat,
pub(crate) stride: i32,
}

impl SurfaceSyncInfo {
/// Capture sync metadata from any GPU surface.
pub fn from_surface(surface: &impl GpuSurface) -> Self {
Self {
phys_addr: surface.phys_addr(),
width: surface.width(),
height: surface.height(),
format: surface.format(),
stride: surface.stride(),
}
}

fn byte_len(self) -> usize {
let w = self.width as i32;
let h = self.height as i32;
if w <= 0 || h <= 0 {
return 0;
}

let fmt = self.format.nema_format();
if self.stride >= 0 {
return (self.stride * h) as usize;
}

unsafe {
let size = nema_texture_size(fmt, NEMA_TEX_BORDER as u8, w, h);
if size > 0 {
return size as usize;
}

let stride = nema_stride_size(fmt, NEMA_TEX_BORDER as u8, w);
(stride * h) as usize
}
}
}

/// Prepare CPU-written surfaces before the GPU reads them.
pub fn sync_before_gpu(surfaces: &[SurfaceSyncInfo]) {
for surface in surfaces {
clean_dcache(*surface);
}
}

/// Ensure CPU/LTDC observers see GPU-written surface contents.
pub fn sync_after_gpu(surfaces: &[SurfaceSyncInfo]) {
#[cfg(any(feature = "n6", feature = "u5"))]
embassy_stm32::icache::invalidate();

for surface in surfaces {
clean_invalidate_dcache(*surface);
}
}

fn clean_dcache(surface: SurfaceSyncInfo) {
let len = surface.byte_len();
if len == 0 {
return;
}
dcache_maintain(surface.phys_addr, len, MaintainOp::Clean);
}

fn clean_invalidate_dcache(surface: SurfaceSyncInfo) {
let len = surface.byte_len();
if len == 0 {
return;
}
dcache_maintain(surface.phys_addr, len, MaintainOp::CleanInvalidate);
}

#[derive(Clone, Copy)]
enum MaintainOp {
Clean,
CleanInvalidate,
}

fn dcache_maintain(addr: usize, len: usize, op: MaintainOp) {
#[cfg(all(any(feature = "n6", feature = "u5", feature = "h7rs"), target_arch = "arm"))]
{
unsafe extern "C" {
fn SCB_CleanDCache_by_Addr(addr: *mut u32, dsize: i32);
fn SCB_CleanInvalidateDCache_by_Addr(addr: *mut u32, dsize: i32);
}

const LINE: usize = 32;
let start = addr & !(LINE - 1);
let end = (addr + len + LINE - 1) & !(LINE - 1);
let mut ptr = start;
while ptr < end {
let chunk = core::cmp::min(LINE, end - ptr);
unsafe {
match op {
MaintainOp::Clean => SCB_CleanDCache_by_Addr(ptr as *mut u32, chunk as i32),
MaintainOp::CleanInvalidate => SCB_CleanInvalidateDCache_by_Addr(ptr as *mut u32, chunk as i32),
}
}
ptr += LINE;
}
}

#[cfg(not(all(any(feature = "n6", feature = "u5", feature = "h7rs"), target_arch = "arm")))]
let _ = (addr, len, op);
}
62 changes: 62 additions & 0 deletions embassy-stm32-neochrom/src/color.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! RGBA color in the format expected by NemaGFX clear/fill APIs.

use crate::ffi::nema_gfx::{NEMA_A8, NEMA_L8, NEMA_RGB565, NEMA_RGBA4444, NEMA_RGBA8888};

/// Opaque 32-bit RGBA color (`0xAARRGGBB`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Rgba8888(pub u32);

impl Rgba8888 {
/// Construct from 8-bit RGBA channels.
#[inline]
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self(((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32))
}

/// Solid red.
pub const RED: Self = Self::new(0xFF, 0x00, 0x00, 0xFF);
/// Solid green.
pub const GREEN: Self = Self::new(0x00, 0xFF, 0x00, 0xFF);
/// Solid blue.
pub const BLUE: Self = Self::new(0x00, 0x00, 0xFF, 0xFF);
/// Opaque black.
pub const BLACK: Self = Self::new(0x00, 0x00, 0x00, 0xFF);
/// Opaque white.
pub const WHITE: Self = Self::new(0xFF, 0xFF, 0xFF, 0xFF);

/// Value passed to [`crate::ffi::nema_gfx::nema_clear`].
#[inline]
pub const fn bits(self) -> u32 {
self.0
}
}

/// NemaGFX texture color formats.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u32)]
pub enum ColorFormat {
/// 32-bit RGBA8888 (4 bytes per pixel).
Rgba8888 = 0,
/// 16-bit RGB565 (2 bytes per pixel).
Rgb565 = 4,
/// 16-bit RGBA4444 (2 bytes per pixel).
Rgba4444 = 5,
/// 8-bit Alpha mask (1 byte per pixel).
A8 = 8,
/// 8-bit Grayscale (1 byte per pixel).
L8 = 11,
}

impl ColorFormat {
/// NemaGFX texture format constant for [`crate::ffi::nema_gfx::nema_bind_dst_tex`].
pub const fn nema_format(self) -> u32 {
match self {
Self::Rgba8888 => NEMA_RGBA8888,
Self::Rgb565 => NEMA_RGB565,
Self::Rgba4444 => NEMA_RGBA4444,
Self::A8 => NEMA_A8,
Self::L8 => NEMA_L8,
}
}
}
Loading