rlvgl_platform/hwcore/dca.rs
1//! DMA Cacheable Buffers — typestate ownership for cache-coherent DMA.
2//!
3//! Lands the contract ratified in
4//! [`docs/concepts/DCB-00-CONCEPTS.md`](../../../docs/concepts/DCB-00-CONCEPTS.md):
5//! D-cache maintenance for DMA buffers is a property of the type system
6//! rather than a property of the call site. Each [`DcaBuf`] is in exactly
7//! one of {[`Cpu`], [`DeviceRead`], [`DeviceWrite`], [`CircRead`],
8//! [`CircWrite`]} at a time, and the typestate transitions emit the
9//! correct cache op (clean before device-read, invalidate before
10//! CPU-read-after-device-write) automatically.
11//!
12//! ## Single-master scope
13//!
14//! Per DCB-00 §7 (non-goal): a `DcaBuf` has at most one DMA master at a
15//! time. Multi-master coherency and cross-core (CM7↔CM4) sharing live
16//! outside this contract — DAA-03 §7 / INV-D14 governs cross-core
17//! regions, which live in D3 SRAM4 and are non-cacheable from CM7.
18//!
19//! ## Cache-controller plumbing
20//!
21//! [`DcaCache`] abstracts the SCB primitives so host tests can run the
22//! typestate round-trip without a real Cortex-M present. On target,
23//! [`cortex_m::peripheral::SCB`] is the concrete implementer; the
24//! `DcaCache` impl is the **only** site in `rlvgl-platform` that may
25//! call `clean_dcache_by_*` / `invalidate_dcache_by_*` on the SCB
26//! (DCB-00 §9 INV-D8: scanner rule `raw_dcache` whitelists this module).
27//!
28//! ## INV-D13 — SCB ownership consolidation
29//!
30//! New code MUST NOT take a `&mut SCB` borrow outside of constructing a
31//! [`DcaCacheCtx`] or a DCB-owning engine driver. The follow-on scanner
32//! rule `raw_scb_for_cache` referenced in DCB-00 §9 INV-D13 is deferred
33//! to a separate phase; in DCB-01 the convention is enforced by review.
34//!
35//! ## Status
36//!
37//! DCB-01 lands the typestate API + the `raw_dcache` scanner rule with
38//! a starting `BASELINE` covering the three pre-existing manual cache
39//! sites (DCB-00 §4 / §9 INV-D10). DCB-02..DCB-02c retrofit those
40//! sites onto [`DcaBuf`] and shrink `BASELINE` to empty.
41
42use core::cell::UnsafeCell;
43use core::marker::PhantomData;
44use core::mem::size_of;
45
46use crate::hwcore::addr::{DmaAddr, PhysAddr};
47
48// ── Constants ──────────────────────────────────────────────────────────
49
50/// Cortex-M7 D-cache line size, in bytes.
51///
52/// Architectural per ARMv7-M ARM. DCB enforces alignment and padding to
53/// this granule via [`DcaBuf`]'s `#[repr(C, align(32))]` and a
54/// const-time size assertion.
55pub const CACHE_LINE: usize = 32;
56
57// ── Direction markers ──────────────────────────────────────────────────
58
59/// Direction marker for circular DMA: device reads RAM (CPU is the
60/// producer; e.g. SAI1 TX, DMA2D source).
61pub enum Read {}
62
63/// Direction marker for circular DMA: device writes RAM (CPU is the
64/// consumer; e.g. SAI1 RX, ADC stream).
65pub enum Write {}
66
67// ── DcaCache trait + DcaCacheCtx ───────────────────────────────────────
68
69/// Cache-controller abstraction.
70///
71/// On target, the `cortex-m` feature provides an implementation for
72/// [`cortex_m::peripheral::SCB`] (the only site in `rlvgl-platform`
73/// allowed to call the raw SCB cache APIs — DCB-00 §9 INV-D8).
74///
75/// On host, [`NullCache`] is a no-op implementer suitable for typestate
76/// round-trip tests where the DMA never actually runs.
77pub trait DcaCache {
78 /// Clean (write-back) cache lines covering `[addr, addr + len)`.
79 fn clean(&mut self, addr: usize, len: usize);
80
81 /// Invalidate cache lines covering `[addr, addr + len)`.
82 fn invalidate(&mut self, addr: usize, len: usize);
83
84 /// Clean+invalidate cache lines covering `[addr, addr + len)`.
85 ///
86 /// DCB-00 §6 INV-D5 documents this as idempotent over an aligned,
87 /// padded extent. DCB itself does not insert clean+invalidate at
88 /// any §5 transition (the directional ops are sufficient under
89 /// INV-D3 line-sharing prohibition); this method is exposed for
90 /// engine drivers that need bidirectional handoff for unrelated
91 /// reasons.
92 fn clean_invalidate(&mut self, addr: usize, len: usize);
93
94 /// Emit a memory barrier sufficient to drain pending writes from
95 /// the architectural write buffer (the AXI write buffer on
96 /// Cortex-M7; the equivalent on other targets).
97 ///
98 /// Required by DCB-00 §6 INV-D16: a `clean` alone does not flush
99 /// the AXI write buffer on STM32H7 + Write-Through SDRAM; pixel
100 /// writes can sit in the buffer past the cache write-back. The
101 /// `DeviceLtdcScan<T, N>` typestate's `present()` /
102 /// `start_ltdc_scan` / `stop_scan` boundaries call this *in
103 /// addition to* the `clean` to enforce ordering for
104 /// continuous-read consumers (LTDC, DCMI display-out, parallel
105 /// RGB).
106 ///
107 /// Routing the DSB through this trait keeps raw
108 /// `cortex_m::asm::dsb()` (or equivalent) call sites contained
109 /// to DCB's owning module — preserving the discipline scanner's
110 /// containment guarantee for the architecturally-mandated
111 /// barrier intrinsic.
112 fn barrier(&mut self);
113}
114
115/// No-op [`DcaCache`] for host tests.
116///
117/// Tracks last-call metadata so tests can assert the right op was
118/// emitted at the right transition without a real cache.
119#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
120pub struct NullCache {
121 /// Sequence-numbered last operation, useful in unit tests.
122 pub last: Option<NullCacheOp>,
123}
124
125/// Recorded operation kind for [`NullCache`] assertions.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum NullCacheOp {
128 /// `clean(addr, len)` was called.
129 Clean(usize, usize),
130 /// `invalidate(addr, len)` was called.
131 Invalidate(usize, usize),
132 /// `clean_invalidate(addr, len)` was called.
133 CleanInvalidate(usize, usize),
134 /// `barrier()` was called.
135 Barrier,
136}
137
138impl DcaCache for NullCache {
139 fn clean(&mut self, addr: usize, len: usize) {
140 self.last = Some(NullCacheOp::Clean(addr, len));
141 }
142 fn invalidate(&mut self, addr: usize, len: usize) {
143 self.last = Some(NullCacheOp::Invalidate(addr, len));
144 }
145 fn clean_invalidate(&mut self, addr: usize, len: usize) {
146 self.last = Some(NullCacheOp::CleanInvalidate(addr, len));
147 }
148 fn barrier(&mut self) {
149 self.last = Some(NullCacheOp::Barrier);
150 }
151}
152
153#[cfg(feature = "cortex-m")]
154impl DcaCache for cortex_m::peripheral::SCB {
155 fn clean(&mut self, addr: usize, len: usize) {
156 // SAFETY: caller (DCB typestate transition) has established that
157 // `[addr, addr+len)` is the extent of a `DcaBuf` whose alignment
158 // and padding satisfy INV-D1 / INV-D2 — i.e. addr is cache-line
159 // aligned and len is a multiple of CACHE_LINE. The DcaBuf is
160 // owned by a single typestate handle, so no concurrent CPU
161 // access can race the clean.
162 unsafe {
163 cortex_m::peripheral::SCB::clean_dcache_by_address(self, addr, len);
164 }
165 }
166 fn invalidate(&mut self, addr: usize, len: usize) {
167 // SAFETY: as above; INV-D3 forbids cache-line sharing so this
168 // invalidate cannot discard live data belonging to an adjacent
169 // owner.
170 unsafe {
171 cortex_m::peripheral::SCB::invalidate_dcache_by_address(self, addr, len);
172 }
173 }
174 fn clean_invalidate(&mut self, addr: usize, len: usize) {
175 // SAFETY: as above.
176 unsafe {
177 cortex_m::peripheral::SCB::clean_invalidate_dcache_by_address(self, addr, len);
178 }
179 }
180 fn barrier(&mut self) {
181 // The architecturally-mandated DSB barrier; on Cortex-M7 this
182 // drains the AXI write buffer (the load-bearing primitive
183 // for `DeviceLtdcScan<T, N>::present()` per DCB-00 §6
184 // INV-D16). Routing through this method (rather than
185 // letting consumers call `cortex_m::asm::dsb()` directly)
186 // keeps the discipline scanner's `compiler_fence` /
187 // barrier-intrinsic containment intact — the raw
188 // intrinsic stays inside `hwcore::dca`.
189 cortex_m::asm::dsb();
190 }
191}
192
193/// Owning wrapper around a [`DcaCache`].
194///
195/// Per DCB-00 §9 INV-D13, the canonical place to plumb a `&mut SCB`
196/// into the platform crate. Engine drivers (e.g. `AudioPlayer`,
197/// `SdmmcEngine`) build a `DcaCacheCtx` once at construction; their
198/// per-transfer methods take `&mut DcaCacheCtx<...>` rather than
199/// `&mut SCB` directly. Application code MUST NOT construct a bare
200/// `&mut SCB` outside of (a) a `DcaCacheCtx` constructor or (b) the
201/// pre-DCB grandfathered call sites.
202pub struct DcaCacheCtx<'a, C: DcaCache> {
203 cache: &'a mut C,
204}
205
206impl<'a, C: DcaCache> DcaCacheCtx<'a, C> {
207 /// Wrap a cache controller for use by DCB transitions.
208 #[inline]
209 pub fn new(cache: &'a mut C) -> Self {
210 Self { cache }
211 }
212
213 /// Borrow the underlying cache controller. Reserved for engine
214 /// drivers that need to issue DCB-internal cache ops on extents
215 /// that are not themselves [`DcaBuf`]s (e.g. an `MPU` carve-out
216 /// scratch zone). Call sites count against the discipline scanner
217 /// budget unless DCB owns them.
218 #[inline]
219 pub fn cache_mut(&mut self) -> &mut C {
220 self.cache
221 }
222}
223
224// ── DcaBuf storage ─────────────────────────────────────────────────────
225
226/// Owning, cache-line-aligned, cache-line-padded DMA buffer.
227///
228/// DCB-00 §3 / §6:
229///
230/// - **INV-D1** — aligned to [`CACHE_LINE`] via `#[repr(C, align(32))]`.
231/// - **INV-D2** — `T*N` MUST be a multiple of [`CACHE_LINE`]. Enforced
232/// at construction by [`DcaBuf::new`]; misuse produces a `const`
233/// panic at compile time. Pad `N` upward (or wrap `T` in a
234/// cache-line-sized newtype) if your element geometry forces a
235/// non-multiple.
236/// - **INV-D3** — no two `DcaBuf`s share a cache line. Implied by
237/// INV-D1 + INV-D2 because every `DcaBuf` starts on a line and
238/// covers an integer number of lines.
239/// - **INV-D4** — single owner. Enforced by the typestate handles
240/// ([`Cpu`] / [`DeviceRead`] / [`DeviceWrite`] / [`CircRead`] /
241/// [`CircWrite`]); see [`DcaBuf::cpu`].
242///
243/// `T` MUST be `Copy` so the storage can be initialised in `const`
244/// context without dropping uninitialised values, and so DMA can read
245/// or overwrite bytes without disturbing destructors.
246#[repr(C, align(32))]
247pub struct DcaBuf<T: Copy, const N: usize> {
248 storage: UnsafeCell<[T; N]>,
249}
250
251// SAFETY: `DcaBuf` is `Sync` because every access path goes through a
252// typestate handle that holds an `&mut DcaBuf`. The handle's exclusive
253// borrow excludes concurrent CPU access; the DMA-owned typestates
254// document that the CPU MUST NOT touch the buffer through the handle.
255// Static placement is supported (the typical SAI/audio pattern).
256unsafe impl<T: Copy + Send, const N: usize> Sync for DcaBuf<T, N> {}
257
258impl<T: Copy, const N: usize> DcaBuf<T, N> {
259 /// Construct a buffer in CPU-owned state from an initial value
260 /// array.
261 ///
262 /// `const fn` so the result is usable as a `static` initialiser.
263 /// The compiler-evaluated assertion below enforces INV-D2: the
264 /// total byte length MUST be a multiple of [`CACHE_LINE`]. A
265 /// failed assertion is a compile-time error, not a runtime panic.
266 #[inline]
267 pub const fn new(init: [T; N]) -> Self {
268 // INV-D2 / INV-D1: post-condition checks. `align_of::<Self>()`
269 // is forced to 32 by the `#[repr(C, align(32))]` on the
270 // struct; INV-D2 requires the size of the storage payload to
271 // be a whole multiple of CACHE_LINE so the buffer occupies an
272 // integer number of cache lines.
273 assert!(
274 (size_of::<T>() * N).is_multiple_of(CACHE_LINE),
275 "DcaBuf<T, N>: size_of::<T>() * N must be a multiple of CACHE_LINE (32). Pad N upward or wrap T in a cache-line-sized newtype.",
276 );
277 assert!(
278 size_of::<T>() * N > 0,
279 "DcaBuf<T, N>: empty buffers are not permitted (no DMA target).",
280 );
281 Self {
282 storage: UnsafeCell::new(init),
283 }
284 }
285
286 /// Total byte length of the storage payload.
287 ///
288 /// Equal to `size_of::<T>() * N` and, by INV-D2, a multiple of
289 /// [`CACHE_LINE`].
290 #[inline]
291 pub const fn byte_len(&self) -> usize {
292 size_of::<T>() * N
293 }
294
295 /// CPU virtual address of the storage payload, as a `usize`.
296 ///
297 /// Used by [`DcaBuf::dma_addr`] and by the typestate transitions
298 /// to compute the cache-op extent.
299 #[inline]
300 fn addr_usize(&self) -> usize {
301 self.storage.get() as *mut u8 as usize
302 }
303
304 /// DMA bus address of the storage payload.
305 ///
306 /// On Cortex-M7 / STM32H747 the DMA address space coincides
307 /// numerically with the CPU physical address space; this method
308 /// is the ratified conversion site (DCB-00 §4 source-of-truth row
309 /// for `hwcore::addr`). Asserts cache-line alignment, which
310 /// follows from INV-D1.
311 pub fn dma_addr(&self) -> DmaAddr {
312 let addr = self.addr_usize();
313 // On the embedded target `addr` fits in u32; on host
314 // (64-bit usize) the cast truncates, but the low 32 bits of
315 // an aligned address preserve cache-line alignment, so the
316 // DmaAddr conversion succeeds. Host tests that read this
317 // value must not feed it to real DMA hardware.
318 let phys = PhysAddr::new(addr as u32);
319 DmaAddr::from_phys(phys, CACHE_LINE)
320 .expect("DcaBuf is #[repr(C, align(32))]; storage address is always cache-line aligned")
321 }
322
323 /// Take ownership of the buffer as a CPU-owned typestate handle.
324 ///
325 /// Borrows `self` exclusively; while the returned [`Cpu`] (or any
326 /// typestate-transitioned descendant) is alive, no other code may
327 /// reach the storage. This is the entry point for all typestate
328 /// transitions.
329 #[inline]
330 pub fn cpu(&mut self) -> Cpu<'_, T, N> {
331 Cpu { buf: self }
332 }
333}
334
335// ── Cpu typestate ──────────────────────────────────────────────────────
336
337/// CPU-owned typestate handle.
338///
339/// Per DCB-00 §3 glossary: "the CPU may freely read and write the
340/// buffer; no DMA master is reading or writing it." Cache state is
341/// unconstrained — the CPU's view is authoritative.
342pub struct Cpu<'a, T: Copy, const N: usize> {
343 buf: &'a mut DcaBuf<T, N>,
344}
345
346impl<'a, T: Copy, const N: usize> Cpu<'a, T, N> {
347 /// Read-only view of the storage as a fixed-size array.
348 #[inline]
349 pub fn as_slice(&self) -> &[T; N] {
350 // SAFETY: typestate Cpu holds an `&mut DcaBuf` — no other
351 // borrow of the storage exists for the lifetime of `self`.
352 unsafe { &*self.buf.storage.get() }
353 }
354
355 /// Mutable view of the storage as a fixed-size array.
356 #[inline]
357 pub fn as_mut_slice(&mut self) -> &mut [T; N] {
358 // SAFETY: as above; `&mut self` is the unique borrow.
359 unsafe { &mut *self.buf.storage.get() }
360 }
361
362 /// DMA bus address of the storage.
363 ///
364 /// Available in CPU-owned state for callers that pre-program the
365 /// DMA engine before lending. Once lent, the same address is
366 /// returned by [`Cpu::lend_for_read`] / [`Cpu::lend_for_write`].
367 #[inline]
368 pub fn dma_addr(&self) -> DmaAddr {
369 self.buf.dma_addr()
370 }
371
372 /// Transition to [`DeviceRead`] (DMA reads RAM, CPU is producer).
373 ///
374 /// Cache op (DCB-00 §5): `clean_dcache_by_address` over the full
375 /// padded extent so any CPU-written cache lines are written back
376 /// to RAM before the DMA reads them.
377 pub fn lend_for_read<C: DcaCache>(
378 self,
379 ctx: &mut DcaCacheCtx<'_, C>,
380 ) -> (DeviceRead<'a, T, N>, DmaAddr) {
381 let addr = self.buf.addr_usize();
382 let len = self.buf.byte_len();
383 ctx.cache.clean(addr, len);
384 let dma_addr = self.buf.dma_addr();
385 (DeviceRead { buf: self.buf }, dma_addr)
386 }
387
388 /// Transition to [`DeviceWrite`] (DMA writes RAM, CPU is consumer).
389 ///
390 /// Cache op (DCB-00 §5): `invalidate_dcache_by_address` over the
391 /// full padded extent so any stale CPU cache lines are evicted
392 /// before the DMA writes; INV-D3 (no cache-line sharing) prevents
393 /// adjacent-line refill from reintroducing stale data during
394 /// transfer, so no exit-side op is needed in
395 /// [`DeviceWrite::complete`].
396 pub fn lend_for_write<C: DcaCache>(
397 self,
398 ctx: &mut DcaCacheCtx<'_, C>,
399 ) -> (DeviceWrite<'a, T, N>, DmaAddr) {
400 let addr = self.buf.addr_usize();
401 let len = self.buf.byte_len();
402 ctx.cache.invalidate(addr, len);
403 let dma_addr = self.buf.dma_addr();
404 (DeviceWrite { buf: self.buf }, dma_addr)
405 }
406
407 /// Transition to [`CircRead`] (continuous DMA read; CPU may access
408 /// the inactive half via [`HalfGuard`]).
409 ///
410 /// Entry cache op: `clean_dcache_by_address` over the full padded
411 /// extent (CPU may have pre-filled the buffer before arming).
412 pub fn start_circular_read<C: DcaCache>(
413 self,
414 ctx: &mut DcaCacheCtx<'_, C>,
415 ) -> CircRead<'a, T, N> {
416 let addr = self.buf.addr_usize();
417 let len = self.buf.byte_len();
418 ctx.cache.clean(addr, len);
419 CircRead { buf: self.buf }
420 }
421
422 /// Transition to [`CircWrite`] (continuous DMA write; CPU may
423 /// access the inactive half via [`HalfGuard`]).
424 ///
425 /// Entry cache op: `invalidate_dcache_by_address` over the full
426 /// padded extent so the CPU's first read after the first
427 /// half-period observes DMA-written data.
428 pub fn start_circular_write<C: DcaCache>(
429 self,
430 ctx: &mut DcaCacheCtx<'_, C>,
431 ) -> CircWrite<'a, T, N> {
432 let addr = self.buf.addr_usize();
433 let len = self.buf.byte_len();
434 ctx.cache.invalidate(addr, len);
435 CircWrite { buf: self.buf }
436 }
437
438 /// Transition to [`LtdcScan`] (continuous-read consumer; LTDC,
439 /// DCMI display-out, parallel RGB).
440 ///
441 /// Per DCB-00 §5 transition table + INV-D16: emits
442 /// [`DcaCache::clean`] over the full padded extent **and** a
443 /// [`DcaCache::barrier`] (DSB on Cortex-M; AXI-write-buffer
444 /// drain on STM32H7 + Write-Through SDRAM). Both are required;
445 /// the clean alone does not enforce ordering on architectures
446 /// where the cache write-back queues into a separate write
447 /// buffer.
448 pub fn start_ltdc_scan<C: DcaCache>(self, ctx: &mut DcaCacheCtx<'_, C>) -> LtdcScan<'a, T, N> {
449 let addr = self.buf.addr_usize();
450 let len = self.buf.byte_len();
451 ctx.cache.clean(addr, len);
452 ctx.cache.barrier();
453 LtdcScan { buf: self.buf }
454 }
455}
456
457// ── DeviceRead / DeviceWrite (one-shot) ────────────────────────────────
458
459/// One-shot DMA-read typestate handle.
460///
461/// While alive, the CPU MUST NOT access the buffer. Drop or
462/// [`DeviceRead::complete`] returns the buffer to [`Cpu`] state with
463/// no exit cache op (the device only read RAM; the CPU's cached copy
464/// is unchanged from before the transfer).
465pub struct DeviceRead<'a, T: Copy, const N: usize> {
466 buf: &'a mut DcaBuf<T, N>,
467}
468
469impl<'a, T: Copy, const N: usize> DeviceRead<'a, T, N> {
470 /// DMA bus address of the buffer.
471 #[inline]
472 pub fn dma_addr(&self) -> DmaAddr {
473 self.buf.dma_addr()
474 }
475
476 /// Transition back to [`Cpu`] state.
477 ///
478 /// Called by an engine completion handler once the DMA "transfer
479 /// complete" interrupt or status bit confirms the device has
480 /// stopped reading. Per DCB-00 §5 transition table, no cache op
481 /// is emitted — the device only read RAM.
482 #[inline]
483 pub fn complete(self) -> Cpu<'a, T, N> {
484 Cpu { buf: self.buf }
485 }
486}
487
488/// One-shot DMA-write typestate handle.
489///
490/// While alive, the CPU MUST NOT access the buffer. Drop or
491/// [`DeviceWrite::complete`] returns the buffer to [`Cpu`] state with
492/// no exit cache op (the entry-side invalidate already prepared the
493/// cache for the post-transfer CPU read).
494pub struct DeviceWrite<'a, T: Copy, const N: usize> {
495 buf: &'a mut DcaBuf<T, N>,
496}
497
498impl<'a, T: Copy, const N: usize> DeviceWrite<'a, T, N> {
499 /// DMA bus address of the buffer.
500 #[inline]
501 pub fn dma_addr(&self) -> DmaAddr {
502 self.buf.dma_addr()
503 }
504
505 /// Transition back to [`Cpu`] state.
506 ///
507 /// Per DCB-00 §5 transition table: no cache op at exit. The
508 /// entry-side invalidate evicted the buffer's cache lines, INV-D3
509 /// forbids adjacent-line refill from reintroducing stale data
510 /// during transfer, so the CPU's first read after this call hits
511 /// RAM and observes the DMA-written data.
512 #[inline]
513 pub fn complete(self) -> Cpu<'a, T, N> {
514 Cpu { buf: self.buf }
515 }
516}
517
518// ── CircRead / CircWrite + HalfGuard ───────────────────────────────────
519
520/// Identifies which half of a circular buffer is currently *inactive*
521/// (i.e. safe for CPU access via a [`HalfGuard`]).
522#[derive(Copy, Clone, Eq, PartialEq, Debug)]
523pub enum Half {
524 /// First half: indices `[0, N/2)`.
525 First,
526 /// Second half: indices `[N/2, N)`.
527 Second,
528}
529
530/// Continuous DMA-read typestate handle.
531///
532/// The DMA engine reads the buffer in circular mode (typically
533/// double-buffer for audio TX, video stream-out, etc.). The CPU is
534/// the producer and may fill the *inactive* half through a
535/// [`HalfGuard<Read, _, _>`] obtained from [`CircRead::half_guard`].
536pub struct CircRead<'a, T: Copy, const N: usize> {
537 buf: &'a mut DcaBuf<T, N>,
538}
539
540impl<'a, T: Copy, const N: usize> CircRead<'a, T, N> {
541 /// DMA bus address of the buffer (stable for the typestate's
542 /// lifetime; equal to the address returned during the
543 /// `start_circular_*` transition).
544 #[inline]
545 pub fn dma_addr(&self) -> DmaAddr {
546 self.buf.dma_addr()
547 }
548
549 /// Acquire a guard over the inactive half.
550 ///
551 /// `half` names which half is currently safe to access — the
552 /// caller knows from the engine's stream-position register
553 /// (`NDTR` for STM32 DMA, `LIVR` for SAI, etc.) which half the
554 /// DMA is currently servicing. Per DCB-00 §6 INV-D7, releasing
555 /// the guard with [`HalfGuard::release`] re-checks the
556 /// stream-position.
557 ///
558 /// **No cache op is emitted at construction.** Per DCB-00 §5
559 /// (DCB-01b-A 2026-05-03 amendment), the `Read` direction's
560 /// cache op fires at `release` — the CPU is about to write
561 /// new data via the guard's slice, so cleaning *now* would
562 /// publish stale data. The clean at release publishes the
563 /// just-completed CPU writes before the engine wraps to this
564 /// half on its next pass.
565 ///
566 /// `ctx` is unused at construction for `Read` direction (kept
567 /// in the signature for symmetry with `CircWrite::half_guard`,
568 /// which uses it for the entry-side invalidate).
569 pub fn half_guard<'b, C: DcaCache>(
570 &'b mut self,
571 _ctx: &mut DcaCacheCtx<'_, C>,
572 half: Half,
573 ) -> HalfGuard<'b, Read, T, N> {
574 // INV-D2 / half_extent debug_assert is still useful for
575 // sanity-checking the call-site geometry, even though no
576 // cache op fires here. Compute and discard.
577 let _ = half_extent::<T, N>(self.buf.addr_usize(), half);
578 HalfGuard {
579 buf: self.buf,
580 half,
581 _dir: PhantomData,
582 }
583 }
584
585 /// Stop the circular transfer and transition back to [`Cpu`].
586 ///
587 /// Caller MUST stop the engine before calling. Cache op:
588 /// [`DcaCache::clean`] over the full padded extent, ensuring any
589 /// CPU-written data still in cache is published to RAM before
590 /// the buffer leaves DCB ownership.
591 pub fn stop_circular<C: DcaCache>(self, ctx: &mut DcaCacheCtx<'_, C>) -> Cpu<'a, T, N> {
592 let addr = self.buf.addr_usize();
593 let len = self.buf.byte_len();
594 ctx.cache.clean(addr, len);
595 Cpu { buf: self.buf }
596 }
597}
598
599/// Continuous DMA-write typestate handle.
600///
601/// The DMA engine writes the buffer in circular mode. The CPU is the
602/// consumer and may drain the *inactive* half through a
603/// [`HalfGuard<Write, _, _>`] obtained from [`CircWrite::half_guard`].
604pub struct CircWrite<'a, T: Copy, const N: usize> {
605 buf: &'a mut DcaBuf<T, N>,
606}
607
608impl<'a, T: Copy, const N: usize> CircWrite<'a, T, N> {
609 /// DMA bus address of the buffer.
610 #[inline]
611 pub fn dma_addr(&self) -> DmaAddr {
612 self.buf.dma_addr()
613 }
614
615 /// Acquire a guard over the inactive half.
616 ///
617 /// Cache op for [`Write`] direction: [`DcaCache::invalidate`]
618 /// over the inactive half so the CPU's next read of that half
619 /// observes DMA-written data.
620 pub fn half_guard<'b, C: DcaCache>(
621 &'b mut self,
622 ctx: &mut DcaCacheCtx<'_, C>,
623 half: Half,
624 ) -> HalfGuard<'b, Write, T, N> {
625 let (addr, len) = half_extent::<T, N>(self.buf.addr_usize(), half);
626 ctx.cache.invalidate(addr, len);
627 HalfGuard {
628 buf: self.buf,
629 half,
630 _dir: PhantomData,
631 }
632 }
633
634 /// Stop the circular transfer and transition back to [`Cpu`].
635 ///
636 /// Caller MUST stop the engine before calling. Cache op:
637 /// [`DcaCache::invalidate`] over the full padded extent so the
638 /// CPU's next read after release observes the final DMA-written
639 /// state, not stale cache lines.
640 pub fn stop_circular<C: DcaCache>(self, ctx: &mut DcaCacheCtx<'_, C>) -> Cpu<'a, T, N> {
641 let addr = self.buf.addr_usize();
642 let len = self.buf.byte_len();
643 ctx.cache.invalidate(addr, len);
644 Cpu { buf: self.buf }
645 }
646}
647
648// ── LtdcScan (continuous-read consumer; DeviceLtdcScan typestate) ──────
649
650/// Continuous-read typestate handle for fixed-rate pixel-stream
651/// consumers (LTDC, DCMI display-out, parallel RGB).
652///
653/// Distinguished from [`CircRead`]:
654///
655/// - the consumer never pauses (no half-rotation; same buffer is
656/// read continuously),
657/// - the CPU paints in place between consumer reads rather than
658/// producing into an inactive half,
659/// - per-frame ordering is enforced by [`LtdcScan::present`] (clean
660/// plus DSB) at checkpoints chosen by the caller, not by an
661/// engine-side completion flag.
662///
663/// The `DIR` parameter is **deliberately omitted** — the CPU is
664/// always producer for this family. The inverse direction (camera
665/// frame-grab via DCMI) would be a separate `LtdcGrab` family;
666/// not in scope here, would ratify in a future §15 amendment with
667/// a named first user.
668pub struct LtdcScan<'a, T: Copy, const N: usize> {
669 buf: &'a mut DcaBuf<T, N>,
670}
671
672impl<'a, T: Copy, const N: usize> LtdcScan<'a, T, N> {
673 /// DMA bus address of the buffer (stable for the typestate's
674 /// lifetime; equal to the address returned during the
675 /// `start_ltdc_scan` transition). Pass to the consumer engine's
676 /// frame-buffer base register (e.g. LTDC `CFBAR`).
677 #[inline]
678 pub fn dma_addr(&self) -> DmaAddr {
679 self.buf.dma_addr()
680 }
681
682 /// In-place CPU access to the full buffer.
683 ///
684 /// Returns a `&mut [T; N]` valid until the next call that
685 /// borrows `self` mutably ([`LtdcScan::present`] /
686 /// [`LtdcScan::stop_scan`]). The borrow checker rejects
687 /// concurrent `paint_full` / `present` calls.
688 ///
689 /// No cache op is emitted here; ordering between the CPU's
690 /// writes and the consumer's reads is enforced at `present`
691 /// (DCB-00 §5 transition table).
692 #[inline]
693 pub fn paint_full(&mut self) -> &mut [T; N] {
694 // SAFETY: typestate LtdcScan holds an `&mut DcaBuf` — no
695 // other borrow of the storage exists for `self`'s lifetime.
696 // The consumer reads the same memory continuously but does
697 // so at engine-determined timing; aliasing with the consumer
698 // is the documented contract of `DeviceLtdcScan<T, N>` and
699 // is mediated by `present()`.
700 unsafe { &mut *self.buf.storage.get() }
701 }
702
703 /// Per-frame checkpoint: publish CPU writes to RAM for the
704 /// consumer's next read.
705 ///
706 /// Per DCB-00 §6 INV-D16 emits **both** [`DcaCache::clean`]
707 /// over the full padded extent AND [`DcaCache::barrier`]
708 /// (DSB-equivalent). Does not transition the typestate — the
709 /// consumer engine is still reading; only the ordering between
710 /// CPU writes and consumer reads is checkpointed.
711 pub fn present<C: DcaCache>(&mut self, ctx: &mut DcaCacheCtx<'_, C>) {
712 let addr = self.buf.addr_usize();
713 let len = self.buf.byte_len();
714 ctx.cache.clean(addr, len);
715 ctx.cache.barrier();
716 }
717
718 /// Transition back to [`Cpu`].
719 ///
720 /// Caller MUST stop the consumer engine before calling. Per
721 /// DCB-00 §5: emits a final [`DcaCache::clean`] +
722 /// [`DcaCache::barrier`] over the full padded extent so the
723 /// next phase's CPU read sees the buffer's terminal state.
724 pub fn stop_scan<C: DcaCache>(self, ctx: &mut DcaCacheCtx<'_, C>) -> Cpu<'a, T, N> {
725 let addr = self.buf.addr_usize();
726 let len = self.buf.byte_len();
727 ctx.cache.clean(addr, len);
728 ctx.cache.barrier();
729 Cpu { buf: self.buf }
730 }
731}
732
733/// RAII guard for half-buffer access in a circular DMA transfer.
734///
735/// Holds an `&'b mut` reborrow of the parent [`CircRead`] /
736/// [`CircWrite`], so a second `half_guard` call is rejected by the
737/// borrow checker until this guard is dropped.
738///
739/// The `DIR` parameter is one of [`Read`] / [`Write`] and selects
740/// which cache op is emitted at construction (DCB-00 §5 transition
741/// row for `DeviceActiveCirc<DIR>` HalfGuard).
742pub struct HalfGuard<'a, DIR, T: Copy, const N: usize> {
743 buf: &'a mut DcaBuf<T, N>,
744 half: Half,
745 _dir: PhantomData<DIR>,
746}
747
748impl<'a, DIR, T: Copy, const N: usize> HalfGuard<'a, DIR, T, N> {
749 /// Read-only view of the guarded half.
750 #[inline]
751 pub fn as_slice(&self) -> &[T] {
752 let (lo, hi) = half_indices::<N>(self.half);
753 // SAFETY: guard borrows the parent CircRead/CircWrite mutably,
754 // which itself holds an `&mut DcaBuf` — no other access path
755 // to the storage exists for `self`'s lifetime. Slicing
756 // [lo, hi) is in bounds: lo,hi ∈ [0, N] by half_indices.
757 unsafe {
758 let storage = &*self.buf.storage.get();
759 core::slice::from_raw_parts(storage.as_ptr().add(lo), hi - lo)
760 }
761 }
762
763 /// Mutable view of the guarded half.
764 #[inline]
765 pub fn as_mut_slice(&mut self) -> &mut [T] {
766 let (lo, hi) = half_indices::<N>(self.half);
767 // SAFETY: as above; `&mut self` is the unique borrow.
768 unsafe {
769 let storage = &mut *self.buf.storage.get();
770 core::slice::from_raw_parts_mut(storage.as_mut_ptr().add(lo), hi - lo)
771 }
772 }
773
774 /// Which half this guard exposes.
775 #[inline]
776 pub fn half(&self) -> Half {
777 self.half
778 }
779}
780
781/// Direction-specific `release` for `HalfGuard<Read, _, _>`.
782///
783/// Per DCB-00 §5 (DCB-01b-A 2026-05-03 amendment) the `Read`
784/// direction's cache op fires at `release`, not at construction:
785/// the CPU is the producer; cleaning before the writes would
786/// publish stale data. The clean here publishes the just-completed
787/// CPU writes before the engine wraps to this half on its next
788/// pass.
789impl<'a, T: Copy, const N: usize> HalfGuard<'a, Read, T, N> {
790 /// Release the guard. Emits [`DcaCache::clean`] over the
791 /// guarded half's extent (the cache op for the `Read`
792 /// direction; publishes CPU writes), then performs the
793 /// INV-D7 stream-position checkpoint.
794 ///
795 /// `current_half` is the half the DMA engine is **currently**
796 /// servicing; the caller reads it from `NDTR` / `LIVR` / etc.
797 /// immediately before this call. If the DMA has crossed into
798 /// the half this guard exposes (i.e. `current_half == self.half`),
799 /// the post-condition check fires per DCB-00 §6 INV-D7:
800 /// `panic!` in `debug_assertions`, error return in release.
801 pub fn release<C: DcaCache>(
802 self,
803 ctx: &mut DcaCacheCtx<'_, C>,
804 current_half: Half,
805 ) -> Result<(), HalfGuardOverrun> {
806 // Cache op first: publish the just-completed CPU writes
807 // before checking the live stream-position. If the engine
808 // has flipped, we still want the clean to fire so the
809 // stale-data window is bounded — the overrun is reported
810 // separately for the caller to act on.
811 let (addr, len) = half_extent::<T, N>(self.buf.addr_usize(), self.half);
812 ctx.cache.clean(addr, len);
813 check_half_overrun(self.half, current_half)
814 }
815}
816
817/// Direction-specific `release` for `HalfGuard<Write, _, _>`.
818///
819/// Per DCB-00 §5 the `Write` direction's cache op fires at
820/// construction (`invalidate` over the inactive half). `release`
821/// performs only the INV-D7 stream-position checkpoint — the CPU
822/// has only read; no dirty lines need publishing.
823impl<'a, T: Copy, const N: usize> HalfGuard<'a, Write, T, N> {
824 /// Release the guard. No cache op (the entry-side invalidate
825 /// already drained stale lines). Performs the INV-D7
826 /// stream-position checkpoint.
827 ///
828 /// `_ctx` is unused for `Write` direction (kept for symmetry
829 /// with `HalfGuard<Read>::release`).
830 pub fn release<C: DcaCache>(
831 self,
832 _ctx: &mut DcaCacheCtx<'_, C>,
833 current_half: Half,
834 ) -> Result<(), HalfGuardOverrun> {
835 check_half_overrun(self.half, current_half)
836 }
837}
838
839/// Internal helper: direction-independent INV-D7 live-recheck.
840#[inline]
841fn check_half_overrun(guard_half: Half, current_half: Half) -> Result<(), HalfGuardOverrun> {
842 if current_half == guard_half {
843 #[cfg(debug_assertions)]
844 {
845 panic!(
846 "DCB HalfGuard overrun: DMA crossed into the inactive half ({guard_half:?}) during the guard's lifetime; INV-D7 violated. Stream is faster than the CPU consumer/producer."
847 );
848 }
849 #[cfg(not(debug_assertions))]
850 {
851 return Err(HalfGuardOverrun { half: guard_half });
852 }
853 }
854 Ok(())
855}
856
857/// Error returned by [`HalfGuard::release`] in release builds when the
858/// DMA stream crossed into the guarded half during the guard's
859/// lifetime — INV-D7 violation.
860#[derive(Copy, Clone, Eq, PartialEq, Debug)]
861pub struct HalfGuardOverrun {
862 /// The half the guard exposed at construction.
863 pub half: Half,
864}
865
866// ── DcaDoubleBuf storage (DMA double-buffer mode) ──────────────────────
867
868/// Identifies which bank of a double-buffer-mode DMA buffer is the
869/// engine's *current target* (or, conversely, which bank is inactive
870/// and safe for CPU access via a [`BankGuard`]).
871///
872/// Maps to the STM32 DMA `CR.CT` bit (bit 19 on H7): `M0` ↔ `CT=0`,
873/// `M1` ↔ `CT=1`. Per DCB-00 §6 INV-D15 the bit is the source-of-truth
874/// for `BankGuard::release` live-recheck.
875#[derive(Copy, Clone, Eq, PartialEq, Debug)]
876pub enum Bank {
877 /// First bank (programmed into the DMA stream's `M0AR` register).
878 M0,
879 /// Second bank (programmed into the DMA stream's `M1AR` register).
880 M1,
881}
882
883/// Owning handle for **STM32 DMA double-buffer mode** storage: a pair
884/// of cache-line-aligned, cache-line-padded banks of `[T; N]`.
885///
886/// The two banks MAY live at arbitrary disjoint addresses (DCB-00 §6
887/// INV-D14): the typical embedded use is `from_addrs(m0_addr, m1_addr)`
888/// wrapping pre-existing fixed-address regions (e.g. SAI1 RX banks at
889/// `0x3000_0000` + `0x3000_1000`). Host tests use [`DcaDoubleBuf::new`]
890/// over two stack-allocated [`DcaBuf`]s.
891///
892/// Distinct from [`DcaBuf`] because the engine register layout (M0AR +
893/// M1AR + `CT` bit + `MBM` flag) is different from circular mode. A
894/// buffer family chooses one or the other at construction and cannot
895/// switch — see DCB-00 §5 "Parallel family: `DcaDoubleBuf<T, N>`".
896pub struct DcaDoubleBuf<'b, T: Copy, const N: usize> {
897 m0: &'b mut DcaBuf<T, N>,
898 m1: &'b mut DcaBuf<T, N>,
899}
900
901impl<'b, T: Copy, const N: usize> DcaDoubleBuf<'b, T, N> {
902 /// Wrap two pre-existing [`DcaBuf`]s as the M0 / M1 banks.
903 ///
904 /// The borrow checker enforces that no other access path to either
905 /// bank exists for the lifetime of the returned `DcaDoubleBuf`.
906 /// Host tests use this; embedded code typically uses
907 /// [`DcaDoubleBuf::from_addrs`].
908 #[inline]
909 pub fn new(m0: &'b mut DcaBuf<T, N>, m1: &'b mut DcaBuf<T, N>) -> Self {
910 Self { m0, m1 }
911 }
912
913 /// Take ownership of this storage as a CPU-owned typestate handle.
914 #[inline]
915 pub fn cpu(&mut self) -> DbufCpu<'_, 'b, T, N> {
916 DbufCpu { buf: self }
917 }
918}
919
920impl<T: Copy, const N: usize> DcaDoubleBuf<'static, T, N> {
921 /// Construct from two fixed bank addresses with `'static` lifetime.
922 ///
923 /// Both addresses are reinterpreted as `&'static mut DcaBuf<T, N>`;
924 /// the typical embedded use case is wrapping pre-existing fixed-
925 /// address DMA buffers (e.g. `0x3000_0000` and `0x3000_1000` for
926 /// SAI1 RX banks BUF0 and BUF1).
927 ///
928 /// # Safety
929 ///
930 /// The caller MUST ensure that:
931 ///
932 /// - both `m0_addr` and `m1_addr` name disjoint, mapped, writable
933 /// RAM regions of `size_of::<DcaBuf<T, N>>()` bytes each, valid
934 /// for `'static`,
935 /// - the two regions do not overlap each other and share no cache
936 /// line with each other or with any other `DcaBuf` /
937 /// `DcaDoubleBuf` (INV-D3 / INV-D14),
938 /// - both addresses are aligned to [`CACHE_LINE`] (32 bytes) — the
939 /// `#[repr(C, align(32))]` requirement on `DcaBuf<T, N>`,
940 /// - no other `&mut` to overlapping bytes exists for `'static`,
941 /// - `from_addrs` is called *at most once* per pair of addresses;
942 /// producing two `DcaDoubleBuf<'static, T, N>` instances over
943 /// the same physical regions would alias the underlying
944 /// `&'static mut`.
945 pub unsafe fn from_addrs(m0_addr: usize, m1_addr: usize) -> Self {
946 debug_assert!(
947 m0_addr.is_multiple_of(CACHE_LINE),
948 "DcaDoubleBuf::from_addrs: m0_addr must be cache-line aligned (INV-D14)",
949 );
950 debug_assert!(
951 m1_addr.is_multiple_of(CACHE_LINE),
952 "DcaDoubleBuf::from_addrs: m1_addr must be cache-line aligned (INV-D14)",
953 );
954 debug_assert!(
955 m0_addr != m1_addr,
956 "DcaDoubleBuf::from_addrs: m0_addr and m1_addr must be distinct",
957 );
958 // SAFETY: caller contract — addresses name disjoint, mapped,
959 // exclusive `'static` regions of `DcaBuf<T, N>` size and
960 // alignment.
961 let m0 = unsafe { &mut *(m0_addr as *mut DcaBuf<T, N>) };
962 // SAFETY: as above.
963 let m1 = unsafe { &mut *(m1_addr as *mut DcaBuf<T, N>) };
964 Self { m0, m1 }
965 }
966}
967
968// ── DbufCpu typestate ──────────────────────────────────────────────────
969
970/// CPU-owned typestate handle for a [`DcaDoubleBuf`].
971///
972/// Per DCB-00 §3 / §5: the CPU may freely read and write either bank;
973/// no DMA master is reading or writing them. Cache state is
974/// unconstrained — the CPU's view is authoritative.
975pub struct DbufCpu<'a, 'b, T: Copy, const N: usize> {
976 buf: &'a mut DcaDoubleBuf<'b, T, N>,
977}
978
979impl<'a, 'b, T: Copy, const N: usize> DbufCpu<'a, 'b, T, N> {
980 /// Read-only view of the M0 bank.
981 #[inline]
982 pub fn as_m0_slice(&self) -> &[T; N] {
983 // SAFETY: typestate DbufCpu holds `&mut DcaDoubleBuf` which
984 // itself holds `&mut DcaBuf` for each bank — no other borrow
985 // of the storage exists for `self`'s lifetime.
986 unsafe { &*self.buf.m0.storage.get() }
987 }
988
989 /// Read-only view of the M1 bank.
990 #[inline]
991 pub fn as_m1_slice(&self) -> &[T; N] {
992 // SAFETY: as above.
993 unsafe { &*self.buf.m1.storage.get() }
994 }
995
996 /// Mutable view of the M0 bank.
997 #[inline]
998 pub fn as_m0_mut_slice(&mut self) -> &mut [T; N] {
999 // SAFETY: as above; `&mut self` is the unique borrow.
1000 unsafe { &mut *self.buf.m0.storage.get() }
1001 }
1002
1003 /// Mutable view of the M1 bank.
1004 #[inline]
1005 pub fn as_m1_mut_slice(&mut self) -> &mut [T; N] {
1006 // SAFETY: as above.
1007 unsafe { &mut *self.buf.m1.storage.get() }
1008 }
1009
1010 /// DMA bus address of the M0 bank (program into the engine's `M0AR`).
1011 #[inline]
1012 pub fn m0_dma_addr(&self) -> DmaAddr {
1013 self.buf.m0.dma_addr()
1014 }
1015
1016 /// DMA bus address of the M1 bank (program into the engine's `M1AR`).
1017 #[inline]
1018 pub fn m1_dma_addr(&self) -> DmaAddr {
1019 self.buf.m1.dma_addr()
1020 }
1021
1022 /// Transition to [`DbufRead`] (DMA reads RAM, CPU is producer).
1023 ///
1024 /// Cache op (DCB-00 §5 transition table): `clean_dcache_by_address`
1025 /// over **both** banks (CPU may have pre-filled either bank before
1026 /// arming).
1027 pub fn start_double_buffer_read<C: DcaCache>(
1028 self,
1029 ctx: &mut DcaCacheCtx<'_, C>,
1030 ) -> DbufRead<'a, 'b, T, N> {
1031 let m0_addr = self.buf.m0.addr_usize();
1032 let m1_addr = self.buf.m1.addr_usize();
1033 let len = self.buf.m0.byte_len();
1034 ctx.cache.clean(m0_addr, len);
1035 ctx.cache.clean(m1_addr, len);
1036 DbufRead { buf: self.buf }
1037 }
1038
1039 /// Transition to [`DbufWrite`] (DMA writes RAM, CPU is consumer).
1040 ///
1041 /// Cache op (DCB-00 §5 transition table): `invalidate_dcache_by_address`
1042 /// over **both** banks so the CPU's first read after the first
1043 /// bank flip observes DMA-written data.
1044 pub fn start_double_buffer_write<C: DcaCache>(
1045 self,
1046 ctx: &mut DcaCacheCtx<'_, C>,
1047 ) -> DbufWrite<'a, 'b, T, N> {
1048 let m0_addr = self.buf.m0.addr_usize();
1049 let m1_addr = self.buf.m1.addr_usize();
1050 let len = self.buf.m0.byte_len();
1051 ctx.cache.invalidate(m0_addr, len);
1052 ctx.cache.invalidate(m1_addr, len);
1053 DbufWrite { buf: self.buf }
1054 }
1055}
1056
1057// ── DbufRead / DbufWrite + BankGuard ──────────────────────────────────
1058
1059/// Continuous DMA-read typestate handle for double-buffer mode.
1060///
1061/// The DMA engine reads M0 and M1 alternately in continuous transfer.
1062/// The CPU is the producer and may fill the *inactive* bank through a
1063/// [`BankGuard<Read, _, _>`] obtained from [`DbufRead::bank_guard`].
1064pub struct DbufRead<'a, 'b, T: Copy, const N: usize> {
1065 buf: &'a mut DcaDoubleBuf<'b, T, N>,
1066}
1067
1068impl<'a, 'b, T: Copy, const N: usize> DbufRead<'a, 'b, T, N> {
1069 /// DMA bus address of the M0 bank (stable for the typestate's
1070 /// lifetime).
1071 #[inline]
1072 pub fn m0_dma_addr(&self) -> DmaAddr {
1073 self.buf.m0.dma_addr()
1074 }
1075
1076 /// DMA bus address of the M1 bank.
1077 #[inline]
1078 pub fn m1_dma_addr(&self) -> DmaAddr {
1079 self.buf.m1.dma_addr()
1080 }
1081
1082 /// Acquire a guard over the inactive bank.
1083 ///
1084 /// `current_target` names the bank the engine is **currently**
1085 /// servicing (read from the stream's `CT` bit). The guard exposes
1086 /// the *opposite* bank.
1087 ///
1088 /// **No cache op is emitted at construction.** Per DCB-00 §5
1089 /// (DCB-01b-A 2026-05-03 amendment), the `Read` direction's
1090 /// cache op fires at `release` — the CPU is about to write
1091 /// new data via the guard's slice, so cleaning *now* would
1092 /// publish stale data. The clean at release publishes the
1093 /// just-completed CPU writes before the engine flips back to
1094 /// this bank.
1095 ///
1096 /// `_ctx` is unused at construction for `Read` direction (kept
1097 /// for symmetry with `DbufWrite::bank_guard`, which uses it
1098 /// for the entry-side invalidate).
1099 pub fn bank_guard<'g, C: DcaCache>(
1100 &'g mut self,
1101 _ctx: &mut DcaCacheCtx<'_, C>,
1102 current_target: Bank,
1103 ) -> BankGuard<'g, Read, T, N> {
1104 let exposed_bank = inactive_bank(current_target);
1105 let exposed = match exposed_bank {
1106 Bank::M0 => &mut *self.buf.m0,
1107 Bank::M1 => &mut *self.buf.m1,
1108 };
1109 BankGuard {
1110 bank_storage: exposed,
1111 bank: exposed_bank,
1112 _dir: PhantomData,
1113 }
1114 }
1115
1116 /// Stop the double-buffer transfer and transition back to
1117 /// [`DbufCpu`].
1118 ///
1119 /// Caller MUST stop the engine before calling. Cache op:
1120 /// [`DcaCache::clean`] over both banks.
1121 pub fn stop_double_buffer<C: DcaCache>(
1122 self,
1123 ctx: &mut DcaCacheCtx<'_, C>,
1124 ) -> DbufCpu<'a, 'b, T, N> {
1125 let m0_addr = self.buf.m0.addr_usize();
1126 let m1_addr = self.buf.m1.addr_usize();
1127 let len = self.buf.m0.byte_len();
1128 ctx.cache.clean(m0_addr, len);
1129 ctx.cache.clean(m1_addr, len);
1130 DbufCpu { buf: self.buf }
1131 }
1132}
1133
1134/// Continuous DMA-write typestate handle for double-buffer mode.
1135///
1136/// The DMA engine writes M0 and M1 alternately. The CPU is the
1137/// consumer and may drain the *inactive* bank through a
1138/// [`BankGuard<Write, _, _>`].
1139pub struct DbufWrite<'a, 'b, T: Copy, const N: usize> {
1140 buf: &'a mut DcaDoubleBuf<'b, T, N>,
1141}
1142
1143impl<'a, 'b, T: Copy, const N: usize> DbufWrite<'a, 'b, T, N> {
1144 /// DMA bus address of the M0 bank.
1145 #[inline]
1146 pub fn m0_dma_addr(&self) -> DmaAddr {
1147 self.buf.m0.dma_addr()
1148 }
1149
1150 /// DMA bus address of the M1 bank.
1151 #[inline]
1152 pub fn m1_dma_addr(&self) -> DmaAddr {
1153 self.buf.m1.dma_addr()
1154 }
1155
1156 /// Acquire a guard over the inactive bank.
1157 ///
1158 /// Cache op for [`Write`] direction: [`DcaCache::invalidate`] over
1159 /// the inactive bank's full extent so the CPU's next read of that
1160 /// bank observes DMA-written data.
1161 pub fn bank_guard<'g, C: DcaCache>(
1162 &'g mut self,
1163 ctx: &mut DcaCacheCtx<'_, C>,
1164 current_target: Bank,
1165 ) -> BankGuard<'g, Write, T, N> {
1166 let exposed_bank = inactive_bank(current_target);
1167 let exposed = match exposed_bank {
1168 Bank::M0 => &mut *self.buf.m0,
1169 Bank::M1 => &mut *self.buf.m1,
1170 };
1171 let addr = exposed.addr_usize();
1172 let len = exposed.byte_len();
1173 ctx.cache.invalidate(addr, len);
1174 BankGuard {
1175 bank_storage: exposed,
1176 bank: exposed_bank,
1177 _dir: PhantomData,
1178 }
1179 }
1180
1181 /// Stop the double-buffer transfer and transition back to
1182 /// [`DbufCpu`].
1183 pub fn stop_double_buffer<C: DcaCache>(
1184 self,
1185 ctx: &mut DcaCacheCtx<'_, C>,
1186 ) -> DbufCpu<'a, 'b, T, N> {
1187 let m0_addr = self.buf.m0.addr_usize();
1188 let m1_addr = self.buf.m1.addr_usize();
1189 let len = self.buf.m0.byte_len();
1190 ctx.cache.invalidate(m0_addr, len);
1191 ctx.cache.invalidate(m1_addr, len);
1192 DbufCpu { buf: self.buf }
1193 }
1194}
1195
1196/// RAII guard for inactive-bank access in a double-buffer DMA transfer.
1197///
1198/// Holds an `&'g mut` reborrow of one bank's [`DcaBuf`], obtained by
1199/// reborrowing through the parent [`DbufRead`] / [`DbufWrite`]. A
1200/// second concurrent `bank_guard` call is rejected by the borrow
1201/// checker until this guard is dropped.
1202///
1203/// The `DIR` parameter is one of [`Read`] / [`Write`] and selects
1204/// which cache op was emitted at construction (DCB-00 §5 transition
1205/// row for `DeviceActiveDoubleBuf<DIR>` BankGuard).
1206pub struct BankGuard<'a, DIR, T: Copy, const N: usize> {
1207 bank_storage: &'a mut DcaBuf<T, N>,
1208 bank: Bank,
1209 _dir: PhantomData<DIR>,
1210}
1211
1212impl<'a, DIR, T: Copy, const N: usize> BankGuard<'a, DIR, T, N> {
1213 /// Read-only view of the guarded bank.
1214 #[inline]
1215 pub fn as_slice(&self) -> &[T; N] {
1216 // SAFETY: guard borrows the parent DbufRead/DbufWrite mutably,
1217 // which itself holds `&mut DcaDoubleBuf` (and through it
1218 // `&mut DcaBuf` for each bank) — no other access path to this
1219 // bank exists for `self`'s lifetime.
1220 unsafe { &*self.bank_storage.storage.get() }
1221 }
1222
1223 /// Mutable view of the guarded bank.
1224 #[inline]
1225 pub fn as_mut_slice(&mut self) -> &mut [T; N] {
1226 // SAFETY: as above; `&mut self` is the unique borrow.
1227 unsafe { &mut *self.bank_storage.storage.get() }
1228 }
1229
1230 /// Which bank this guard exposes.
1231 #[inline]
1232 pub fn bank(&self) -> Bank {
1233 self.bank
1234 }
1235}
1236
1237/// Direction-specific `release` for `BankGuard<Read, _, _>`.
1238///
1239/// Per DCB-00 §5 (DCB-01b-A 2026-05-03 amendment) the `Read`
1240/// direction's cache op fires at `release`, not at construction:
1241/// the CPU is the producer; cleaning before the writes would
1242/// publish stale data. The clean here publishes the just-completed
1243/// CPU writes before the engine flips back to this bank.
1244impl<'a, T: Copy, const N: usize> BankGuard<'a, Read, T, N> {
1245 /// Release the guard. Emits [`DcaCache::clean`] over the
1246 /// guarded bank's full extent, then performs the INV-D15
1247 /// CT-bit checkpoint.
1248 ///
1249 /// `current_target` is the bank the DMA engine is **currently**
1250 /// servicing per the latest read of the stream's `CT` bit. If
1251 /// the engine has flipped CT into the bank this guard exposes
1252 /// (i.e. `current_target == self.bank`), the post-condition
1253 /// check fires: `panic!` in `debug_assertions` builds, error
1254 /// return in release builds.
1255 pub fn release<C: DcaCache>(
1256 self,
1257 ctx: &mut DcaCacheCtx<'_, C>,
1258 current_target: Bank,
1259 ) -> Result<(), BankGuardOverrun> {
1260 let addr = self.bank_storage.addr_usize();
1261 let len = self.bank_storage.byte_len();
1262 ctx.cache.clean(addr, len);
1263 check_bank_overrun(self.bank, current_target)
1264 }
1265}
1266
1267/// Direction-specific `release` for `BankGuard<Write, _, _>`.
1268///
1269/// Per DCB-00 §5 the `Write` direction's cache op fires at
1270/// construction (`invalidate` over the inactive bank). `release`
1271/// performs only the INV-D15 CT-bit checkpoint — the CPU has
1272/// only read; no dirty lines need publishing.
1273impl<'a, T: Copy, const N: usize> BankGuard<'a, Write, T, N> {
1274 /// Release the guard. No cache op (the entry-side invalidate
1275 /// already drained stale lines). Performs the INV-D15 CT-bit
1276 /// checkpoint.
1277 ///
1278 /// `_ctx` is unused for `Write` direction (kept for symmetry
1279 /// with `BankGuard<Read>::release`).
1280 pub fn release<C: DcaCache>(
1281 self,
1282 _ctx: &mut DcaCacheCtx<'_, C>,
1283 current_target: Bank,
1284 ) -> Result<(), BankGuardOverrun> {
1285 check_bank_overrun(self.bank, current_target)
1286 }
1287}
1288
1289/// Internal helper: direction-independent INV-D15 live-recheck.
1290#[inline]
1291fn check_bank_overrun(guard_bank: Bank, current_target: Bank) -> Result<(), BankGuardOverrun> {
1292 if current_target == guard_bank {
1293 #[cfg(debug_assertions)]
1294 {
1295 panic!(
1296 "DCB BankGuard overrun: DMA flipped CT into the inactive bank ({guard_bank:?}) during the guard's lifetime; INV-D15 violated. Stream is faster than the CPU consumer/producer."
1297 );
1298 }
1299 #[cfg(not(debug_assertions))]
1300 {
1301 return Err(BankGuardOverrun { bank: guard_bank });
1302 }
1303 }
1304 Ok(())
1305}
1306
1307/// Error returned by [`BankGuard::release`] in release builds when the
1308/// DMA engine flipped its `CT` bit into the guarded bank during the
1309/// guard's lifetime — INV-D15 violation.
1310#[derive(Copy, Clone, Eq, PartialEq, Debug)]
1311pub struct BankGuardOverrun {
1312 /// The bank the guard exposed at construction.
1313 pub bank: Bank,
1314}
1315
1316/// Internal helper: the inactive bank, given the current target.
1317#[inline]
1318const fn inactive_bank(current_target: Bank) -> Bank {
1319 match current_target {
1320 Bank::M0 => Bank::M1,
1321 Bank::M1 => Bank::M0,
1322 }
1323}
1324
1325// ── Internal helpers ──────────────────────────────────────────────────
1326
1327/// Compute (addr, len) of one half of a `DcaBuf<T, N>` starting at
1328/// `base_addr`. INV-D2 ensures the byte length is a multiple of
1329/// CACHE_LINE; for the half-extent to also be a multiple of CACHE_LINE
1330/// (so the cache op on one half doesn't touch the other), `N * sizeof(T)`
1331/// MUST be a multiple of `2 * CACHE_LINE`. Asserted at runtime here;
1332/// the constructor will be tightened to a `const` assertion when
1333/// `generic_const_exprs` stabilises.
1334fn half_extent<T: Copy, const N: usize>(base_addr: usize, half: Half) -> (usize, usize) {
1335 let total = size_of::<T>() * N;
1336 debug_assert!(
1337 total.is_multiple_of(2 * CACHE_LINE),
1338 "DcaBuf used with half_guard must have N*sizeof(T) divisible by 2*CACHE_LINE so each half is itself cache-line aligned",
1339 );
1340 let half_bytes = total / 2;
1341 let off = match half {
1342 Half::First => 0,
1343 Half::Second => half_bytes,
1344 };
1345 (base_addr + off, half_bytes)
1346}
1347
1348/// Half boundaries as element indices.
1349const fn half_indices<const N: usize>(half: Half) -> (usize, usize) {
1350 let mid = N / 2;
1351 match half {
1352 Half::First => (0, mid),
1353 Half::Second => (mid, N),
1354 }
1355}
1356
1357// ── Tests ──────────────────────────────────────────────────────────────
1358
1359#[cfg(test)]
1360mod tests {
1361 use super::*;
1362
1363 // 32 bytes (16 i16 elements) — minimum INV-D2-compliant size.
1364 type Buf = DcaBuf<i16, 16>;
1365
1366 #[test]
1367 fn dcabuf_alignment_is_32() {
1368 let buf = Buf::new([0; 16]);
1369 let addr = (&buf as *const Buf) as usize;
1370 assert_eq!(addr % CACHE_LINE, 0, "INV-D1: 32-byte alignment");
1371 }
1372
1373 #[test]
1374 fn dcabuf_size_is_multiple_of_cache_line() {
1375 assert_eq!(core::mem::size_of::<Buf>() % CACHE_LINE, 0);
1376 }
1377
1378 #[test]
1379 fn cpu_round_trip_through_device_read() {
1380 let mut buf = Buf::new([0; 16]);
1381 let mut cache = NullCache::default();
1382 let mut ctx = DcaCacheCtx::new(&mut cache);
1383 let cpu = buf.cpu();
1384 let (pending, _addr) = cpu.lend_for_read(&mut ctx);
1385 let _ = pending.complete();
1386 }
1387
1388 #[test]
1389 fn lend_for_read_emits_clean() {
1390 let mut buf = Buf::new([0; 16]);
1391 let mut cache = NullCache::default();
1392 {
1393 let mut ctx = DcaCacheCtx::new(&mut cache);
1394 let cpu = buf.cpu();
1395 let (pending, _addr) = cpu.lend_for_read(&mut ctx);
1396 let _ = pending.complete();
1397 }
1398 assert!(matches!(cache.last, Some(NullCacheOp::Clean(_, 32))));
1399 }
1400
1401 #[test]
1402 fn lend_for_write_emits_invalidate() {
1403 let mut buf = Buf::new([0; 16]);
1404 let mut cache = NullCache::default();
1405 {
1406 let mut ctx = DcaCacheCtx::new(&mut cache);
1407 let cpu = buf.cpu();
1408 let (pending, _addr) = cpu.lend_for_write(&mut ctx);
1409 let _ = pending.complete();
1410 }
1411 assert!(matches!(cache.last, Some(NullCacheOp::Invalidate(_, 32))));
1412 }
1413
1414 #[test]
1415 fn cpu_can_read_and_write_storage() {
1416 let mut buf = Buf::new([0; 16]);
1417 let mut cpu = buf.cpu();
1418 cpu.as_mut_slice()[3] = 42;
1419 assert_eq!(cpu.as_slice()[3], 42);
1420 }
1421
1422 #[test]
1423 fn cpu_dma_addr_is_cache_line_aligned() {
1424 let mut buf = Buf::new([0; 16]);
1425 let cpu = buf.cpu();
1426 let addr = cpu.dma_addr();
1427 assert_eq!(addr.raw() as usize % CACHE_LINE, 0);
1428 }
1429
1430 #[test]
1431 fn circ_read_half_guard_emits_clean_per_half() {
1432 // 64 bytes → two 32-byte halves, both cache-line aligned.
1433 type CircBuf = DcaBuf<u8, 64>;
1434 let mut buf = CircBuf::new([0; 64]);
1435 let mut cache = NullCache::default();
1436 let mut ctx = DcaCacheCtx::new(&mut cache);
1437 let cpu = buf.cpu();
1438 let mut circ = cpu.start_circular_read(&mut ctx);
1439 assert!(matches!(
1440 ctx.cache_mut().last,
1441 Some(NullCacheOp::Clean(_, 64))
1442 ));
1443
1444 // Per DCB-01b-A 2026-05-03: Read direction emits NO cache op
1445 // at half_guard construction; the clean fires at release.
1446 let mut guard = circ.half_guard(&mut ctx, Half::Second);
1447 // The last op observed is still the start_circular_read clean
1448 // (no new op fired by half_guard).
1449 assert!(matches!(
1450 ctx.cache_mut().last,
1451 Some(NullCacheOp::Clean(_, 64))
1452 ));
1453 guard.as_mut_slice()[0] = 0xAB;
1454 // Engine reports we're still in First half — release OK.
1455 // The release-time clean for Read direction publishes the
1456 // CPU writes we just made.
1457 guard.release(&mut ctx, Half::First).unwrap();
1458 assert!(matches!(
1459 ctx.cache_mut().last,
1460 Some(NullCacheOp::Clean(_, 32))
1461 ));
1462
1463 let _cpu = circ.stop_circular(&mut ctx);
1464 assert!(matches!(
1465 ctx.cache_mut().last,
1466 Some(NullCacheOp::Clean(_, 64))
1467 ));
1468 }
1469
1470 #[test]
1471 fn circ_write_half_guard_emits_invalidate_per_half() {
1472 type CircBuf = DcaBuf<u8, 64>;
1473 let mut buf = CircBuf::new([0; 64]);
1474 let mut cache = NullCache::default();
1475 let mut ctx = DcaCacheCtx::new(&mut cache);
1476 let cpu = buf.cpu();
1477 let mut circ = cpu.start_circular_write(&mut ctx);
1478 assert!(matches!(
1479 ctx.cache_mut().last,
1480 Some(NullCacheOp::Invalidate(_, 64))
1481 ));
1482
1483 {
1484 let _guard = circ.half_guard(&mut ctx, Half::First);
1485 }
1486 assert!(matches!(
1487 ctx.cache_mut().last,
1488 Some(NullCacheOp::Invalidate(_, 32))
1489 ));
1490
1491 let _cpu = circ.stop_circular(&mut ctx);
1492 assert!(matches!(
1493 ctx.cache_mut().last,
1494 Some(NullCacheOp::Invalidate(_, 64))
1495 ));
1496 }
1497
1498 #[test]
1499 fn half_indices_split_evenly() {
1500 assert_eq!(half_indices::<16>(Half::First), (0, 8));
1501 assert_eq!(half_indices::<16>(Half::Second), (8, 16));
1502 }
1503
1504 #[test]
1505 #[cfg(not(debug_assertions))]
1506 fn half_guard_release_returns_err_on_overrun_in_release() {
1507 type CircBuf = DcaBuf<u8, 64>;
1508 let mut buf = CircBuf::new([0; 64]);
1509 let mut cache = NullCache::default();
1510 let mut ctx = DcaCacheCtx::new(&mut cache);
1511 let cpu = buf.cpu();
1512 let mut circ = cpu.start_circular_read(&mut ctx);
1513 let guard = circ.half_guard(&mut ctx, Half::Second);
1514 // DMA crossed into Second — overrun. Per DCB-01b-A the
1515 // release-time clean still fires before the overrun check.
1516 assert_eq!(
1517 guard.release(&mut ctx, Half::Second),
1518 Err(HalfGuardOverrun { half: Half::Second })
1519 );
1520 }
1521
1522 // ── DcaDoubleBuf (DCB-01b) ─────────────────────────────────────────
1523
1524 type DbBank = DcaBuf<i16, 16>;
1525
1526 #[test]
1527 fn dbufcpu_round_trip_through_active_double_buffer_write() {
1528 let mut m0 = DbBank::new([0; 16]);
1529 let mut m1 = DbBank::new([0; 16]);
1530 let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1531 let mut cache = NullCache::default();
1532 let mut ctx = DcaCacheCtx::new(&mut cache);
1533 let cpu = dca.cpu();
1534 let active = cpu.start_double_buffer_write(&mut ctx);
1535 let _cpu_back = active.stop_double_buffer(&mut ctx);
1536 }
1537
1538 #[test]
1539 fn dbufcpu_can_read_and_write_either_bank() {
1540 let mut m0 = DbBank::new([0; 16]);
1541 let mut m1 = DbBank::new([0; 16]);
1542 let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1543 let mut cpu = dca.cpu();
1544 cpu.as_m0_mut_slice()[3] = 42;
1545 cpu.as_m1_mut_slice()[7] = 99;
1546 assert_eq!(cpu.as_m0_slice()[3], 42);
1547 assert_eq!(cpu.as_m1_slice()[7], 99);
1548 }
1549
1550 #[test]
1551 fn dbufcpu_dma_addrs_are_distinct_and_aligned() {
1552 let mut m0 = DbBank::new([0; 16]);
1553 let mut m1 = DbBank::new([0; 16]);
1554 let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1555 let cpu = dca.cpu();
1556 let a0 = cpu.m0_dma_addr().raw() as usize;
1557 let a1 = cpu.m1_dma_addr().raw() as usize;
1558 assert_eq!(a0 % CACHE_LINE, 0);
1559 assert_eq!(a1 % CACHE_LINE, 0);
1560 assert_ne!(a0, a1, "M0 / M1 must address distinct memory");
1561 }
1562
1563 #[test]
1564 fn start_double_buffer_read_cleans_both_banks() {
1565 let mut m0 = DbBank::new([0; 16]);
1566 let mut m1 = DbBank::new([0; 16]);
1567 let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1568 let mut cache = NullCache::default();
1569 {
1570 let mut ctx = DcaCacheCtx::new(&mut cache);
1571 let cpu = dca.cpu();
1572 let active = cpu.start_double_buffer_read(&mut ctx);
1573 let _ = active.stop_double_buffer(&mut ctx);
1574 }
1575 // The last operation observed by the cache during start_*_read
1576 // is the *second* clean (M1). The first clean (M0) was overwritten.
1577 // Accept either; both must be `Clean`.
1578 assert!(matches!(cache.last, Some(NullCacheOp::Clean(_, 32))));
1579 }
1580
1581 #[test]
1582 fn start_double_buffer_write_invalidates_both_banks() {
1583 let mut m0 = DbBank::new([0; 16]);
1584 let mut m1 = DbBank::new([0; 16]);
1585 let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1586 let mut cache = NullCache::default();
1587 {
1588 let mut ctx = DcaCacheCtx::new(&mut cache);
1589 let cpu = dca.cpu();
1590 let active = cpu.start_double_buffer_write(&mut ctx);
1591 let _ = active.stop_double_buffer(&mut ctx);
1592 }
1593 assert!(matches!(cache.last, Some(NullCacheOp::Invalidate(_, 32))));
1594 }
1595
1596 #[test]
1597 fn dbuf_read_bank_guard_emits_clean_on_inactive_bank() {
1598 let mut m0 = DbBank::new([0; 16]);
1599 let mut m1 = DbBank::new([0; 16]);
1600 let m0_addr = (&raw const m0) as usize;
1601 let m1_addr = (&raw const m1) as usize;
1602 let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1603 let mut cache = NullCache::default();
1604 let mut ctx = DcaCacheCtx::new(&mut cache);
1605 let cpu = dca.cpu();
1606 let mut active = cpu.start_double_buffer_read(&mut ctx);
1607 // Engine currently on M0 → inactive bank is M1.
1608 // Per DCB-01b-A 2026-05-03: Read direction emits NO cache op
1609 // at bank_guard construction; the clean fires at release.
1610 let mut guard = active.bank_guard(&mut ctx, Bank::M0);
1611 assert_eq!(guard.bank(), Bank::M1);
1612 guard.as_mut_slice()[0] = 0xAB;
1613 // Engine still on M0 — release OK. The release-time clean
1614 // for Read direction publishes the CPU writes we just made.
1615 guard.release(&mut ctx, Bank::M0).unwrap();
1616 // Confirm cache op was a Clean over M1's extent (32 bytes).
1617 let last = ctx.cache_mut().last.unwrap();
1618 match last {
1619 NullCacheOp::Clean(addr, len) => {
1620 assert_eq!(len, 32);
1621 assert_eq!(addr, m1_addr, "expected M1 (inactive) cleaned");
1622 assert_ne!(addr, m0_addr);
1623 }
1624 other => panic!("expected Clean, got {other:?}"),
1625 }
1626 }
1627
1628 #[test]
1629 fn dbuf_write_bank_guard_emits_invalidate_on_inactive_bank() {
1630 let mut m0 = DbBank::new([0; 16]);
1631 let mut m1 = DbBank::new([0; 16]);
1632 let m0_addr = (&raw const m0) as usize;
1633 let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1634 let mut cache = NullCache::default();
1635 let mut ctx = DcaCacheCtx::new(&mut cache);
1636 let cpu = dca.cpu();
1637 let mut active = cpu.start_double_buffer_write(&mut ctx);
1638 // Engine currently on M1 → inactive bank is M0.
1639 let guard = active.bank_guard(&mut ctx, Bank::M1);
1640 assert_eq!(guard.bank(), Bank::M0);
1641 let last = ctx.cache_mut().last.unwrap();
1642 match last {
1643 NullCacheOp::Invalidate(addr, len) => {
1644 assert_eq!(len, 32);
1645 assert_eq!(addr, m0_addr, "expected M0 (inactive) invalidated");
1646 }
1647 other => panic!("expected Invalidate, got {other:?}"),
1648 }
1649 guard.release(&mut ctx, Bank::M1).unwrap();
1650 }
1651
1652 #[test]
1653 fn inactive_bank_swaps_correctly() {
1654 assert_eq!(inactive_bank(Bank::M0), Bank::M1);
1655 assert_eq!(inactive_bank(Bank::M1), Bank::M0);
1656 }
1657
1658 #[test]
1659 #[cfg(not(debug_assertions))]
1660 fn bank_guard_release_returns_err_on_overrun_in_release() {
1661 let mut m0 = DbBank::new([0; 16]);
1662 let mut m1 = DbBank::new([0; 16]);
1663 let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1664 let mut cache = NullCache::default();
1665 let mut ctx = DcaCacheCtx::new(&mut cache);
1666 let cpu = dca.cpu();
1667 let mut active = cpu.start_double_buffer_read(&mut ctx);
1668 let guard = active.bank_guard(&mut ctx, Bank::M0);
1669 // Guard exposes M1; engine flipped CT into M1 → overrun.
1670 // Per DCB-01b-A the release-time clean still fires before
1671 // the overrun check.
1672 assert_eq!(
1673 guard.release(&mut ctx, Bank::M1),
1674 Err(BankGuardOverrun { bank: Bank::M1 })
1675 );
1676 }
1677
1678 // ── LtdcScan (DCB-01c) ──────────────────────────────────────────
1679
1680 type ScanBuf = DcaBuf<u8, 64>;
1681
1682 #[test]
1683 fn ltdc_scan_round_trip_through_cpu() {
1684 let mut buf = ScanBuf::new([0; 64]);
1685 let mut cache = NullCache::default();
1686 let mut ctx = DcaCacheCtx::new(&mut cache);
1687 let cpu = buf.cpu();
1688 let scan = cpu.start_ltdc_scan(&mut ctx);
1689 let _cpu_back = scan.stop_scan(&mut ctx);
1690 }
1691
1692 #[test]
1693 fn start_ltdc_scan_emits_clean_then_barrier() {
1694 let mut buf = ScanBuf::new([0; 64]);
1695 let mut cache = NullCache::default();
1696 {
1697 let mut ctx = DcaCacheCtx::new(&mut cache);
1698 let cpu = buf.cpu();
1699 let scan = cpu.start_ltdc_scan(&mut ctx);
1700 // After start: last op observed is the barrier (issued
1701 // *after* the clean per INV-D16).
1702 assert_eq!(ctx.cache_mut().last, Some(NullCacheOp::Barrier));
1703 let _ = scan.stop_scan(&mut ctx);
1704 }
1705 // After stop: the FINAL op is also a barrier (clean + barrier
1706 // emitted at exit).
1707 assert_eq!(cache.last, Some(NullCacheOp::Barrier));
1708 }
1709
1710 #[test]
1711 fn ltdc_scan_paint_full_yields_buffer_slice() {
1712 let mut buf = ScanBuf::new([0; 64]);
1713 let mut cache = NullCache::default();
1714 let mut ctx = DcaCacheCtx::new(&mut cache);
1715 let cpu = buf.cpu();
1716 let mut scan = cpu.start_ltdc_scan(&mut ctx);
1717 let pixels = scan.paint_full();
1718 pixels[0] = 0xAA;
1719 pixels[63] = 0x55;
1720 // Borrow ends here; subsequent paint_full / present ok.
1721 assert_eq!(scan.paint_full()[0], 0xAA);
1722 assert_eq!(scan.paint_full()[63], 0x55);
1723 }
1724
1725 #[test]
1726 fn ltdc_scan_present_emits_clean_then_barrier_no_transition() {
1727 let mut buf = ScanBuf::new([0; 64]);
1728 let mut cache = NullCache::default();
1729 let mut ctx = DcaCacheCtx::new(&mut cache);
1730 let cpu = buf.cpu();
1731 let mut scan = cpu.start_ltdc_scan(&mut ctx);
1732 // Drop the start_ltdc_scan op record by observing it then
1733 // calling present, which must emit clean + barrier again.
1734 scan.paint_full()[5] = 0x42;
1735 scan.present(&mut ctx);
1736 assert_eq!(ctx.cache_mut().last, Some(NullCacheOp::Barrier));
1737 // Typestate is still LtdcScan — paint_full should still work.
1738 scan.paint_full()[7] = 0x33;
1739 scan.present(&mut ctx);
1740 let _ = scan.stop_scan(&mut ctx);
1741 }
1742
1743 #[test]
1744 fn ltdc_scan_dma_addr_is_cache_line_aligned() {
1745 let mut buf = ScanBuf::new([0; 64]);
1746 let mut cache = NullCache::default();
1747 let mut ctx = DcaCacheCtx::new(&mut cache);
1748 let cpu = buf.cpu();
1749 let scan = cpu.start_ltdc_scan(&mut ctx);
1750 let addr = scan.dma_addr();
1751 assert_eq!(addr.raw() as usize % CACHE_LINE, 0);
1752 let _ = scan.stop_scan(&mut ctx);
1753 }
1754
1755 #[test]
1756 fn ltdc_scan_present_clean_extent_is_full_buffer() {
1757 // Track multiple ops by inspecting them in sequence — switch
1758 // to a custom local `LoggingCache` that records all calls.
1759 struct LoggingCache {
1760 ops: alloc::vec::Vec<NullCacheOp>,
1761 }
1762 impl DcaCache for LoggingCache {
1763 fn clean(&mut self, addr: usize, len: usize) {
1764 self.ops.push(NullCacheOp::Clean(addr, len));
1765 }
1766 fn invalidate(&mut self, addr: usize, len: usize) {
1767 self.ops.push(NullCacheOp::Invalidate(addr, len));
1768 }
1769 fn clean_invalidate(&mut self, addr: usize, len: usize) {
1770 self.ops.push(NullCacheOp::CleanInvalidate(addr, len));
1771 }
1772 fn barrier(&mut self) {
1773 self.ops.push(NullCacheOp::Barrier);
1774 }
1775 }
1776
1777 let mut buf = ScanBuf::new([0; 64]);
1778 let mut cache = LoggingCache {
1779 ops: alloc::vec::Vec::new(),
1780 };
1781 {
1782 let mut ctx = DcaCacheCtx::new(&mut cache);
1783 let cpu = buf.cpu();
1784 let mut scan = cpu.start_ltdc_scan(&mut ctx);
1785 scan.present(&mut ctx);
1786 let _ = scan.stop_scan(&mut ctx);
1787 }
1788 // Expected sequence: start (clean+barrier), present (clean+
1789 // barrier), stop (clean+barrier) = 6 ops, alternating.
1790 assert_eq!(cache.ops.len(), 6);
1791 for (i, op) in cache.ops.iter().enumerate() {
1792 if i % 2 == 0 {
1793 assert!(
1794 matches!(op, NullCacheOp::Clean(_, 64)),
1795 "expected Clean(_, 64) at index {i}, got {op:?}"
1796 );
1797 } else {
1798 assert_eq!(*op, NullCacheOp::Barrier);
1799 }
1800 }
1801 }
1802}