Skip to main content

et_kernel/
cache.rs

1//! Cache management operations for the ET-SoC-1 Minion processor.
2//!
3//! The ET-SoC-1 implements a software-coherent memory model: the RISC-V
4//! `fence` instruction orders CPU-visible stores but does not flush dirty L1
5//! data cache lines to L2 or DDR. Cross-hart, cross-shire, and host-visible
6//! coherence therefore require explicit cache management via dedicated CSRs.
7//!
8//! # Cache hierarchy
9//!
10//! Each Minion core has a private L1 data cache with 64-byte lines. The L2
11//! is shared among all Minions in a shire (512 KB on aifoundry3). The L3 is
12//! shared across all compute shires (32 MB on aifoundry3). Host DMA reads
13//! bypass all Minion caches and observe only DDR.
14//!
15//! # Producer/consumer protocol
16//!
17//! Per PRM Section 8.1.3, software must `fence` before a cache op (to commit
18//! all prior CPU stores to L1) and issue `TensorWait(CacheOp)` after (to
19//! guarantee the op completed before any subsequent memory access to the
20//! affected lines). The high-level functions below handle the TensorWait
21//! internally; only the preceding `fence` is the caller's responsibility.
22//!
23//! ```text
24//! // Hart A (producer):
25//! // ... write data ...
26//! fence();                                          // commit stores to L1
27//! unsafe { cache_writeback(ptr as usize, len); }  // flush L1 to DDR + TensorWait
28//!
29//! // <synchronisation, e.g. via a shared flag + fence on both sides>
30//!
31//! // Hart B (consumer):
32//! fence();                                          // receive synchronisation
33//! unsafe { cache_invalidate(ptr as usize, len); }  // discard stale L1 + TensorWait
34//! // ... read data ...
35//! ```
36//!
37//! Use [`cache_flush`] when a region may contain both dirty (locally modified)
38//! and stale lines, performing writeback then invalidation atomically at the
39//! function level.
40//!
41//! # Cache levels
42//!
43//! The high-level functions [`cache_writeback`], [`cache_invalidate`], and
44//! [`cache_flush`] propagate to main memory ([`CacheDest::Mem`]), which is the
45//! safest choice for cross-shire and host-DMA coherence. The lower-level
46//! `_to` variants accept an explicit [`CacheDest`] for intra-shire operations
47//! that need only reach L2.
48
49use core::arch::asm;
50
51// ---------------------------------------------------------------------------
52// CSR addresses (cacheops.h, Ainekko SDK)
53// ---------------------------------------------------------------------------
54
55/// `evict_va` CSR: evicts cache lines by virtual address up to a target level.
56pub const CSR_EVICT_VA: u16 = 0x89F;
57/// `flush_va` CSR: writes back dirty cache lines by virtual address to a target level.
58pub const CSR_FLUSH_VA: u16 = 0x8BF;
59
60// ---------------------------------------------------------------------------
61// Cache destination enum
62// ---------------------------------------------------------------------------
63
64/// Target cache hierarchy level for cache management operations.
65///
66/// Specifies how far up the cache hierarchy a writeback or eviction
67/// propagates. Use [`Mem`](CacheDest::Mem) for host-DMA visibility;
68/// [`L2`](CacheDest::L2) to make data visible to other Minions in the same
69/// shire without a full writeback to DDR.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71#[repr(u64)]
72pub enum CacheDest {
73    /// Propagate to L1 only (reserved; provided for completeness).
74    L1 = 0,
75    /// Propagate to the shire-local L2 shared cache.
76    L2 = 1,
77    /// Propagate to the globally shared L3 cache.
78    L3 = 2,
79    /// Propagate to main memory (DDR); required for host DMA visibility.
80    Mem = 3,
81}
82
83// ---------------------------------------------------------------------------
84// Hardware primitives
85// ---------------------------------------------------------------------------
86
87/// Issues a single `evict_va` CSR write (CSR `0x89F`).
88///
89/// Evicts `hw_count + 1` cache lines starting at `line_addr` (64-byte
90/// aligned), using a stride of 64 bytes per hardware iteration. The
91/// hardware reads x31 (t6) implicitly at the moment of the CSR write; this
92/// function loads t6 = 64 (stride=64, id=0) immediately before the
93/// instruction to satisfy that dependency.
94///
95/// CSR field layout (cacheops.h `evict_va`):
96/// - \[63\]: `use_tmask` = 0
97/// - \[59:58\]: `dst` (`CacheDest` discriminant)
98/// - \[57:6\]: VA bits \[57:6\] (`line_addr` is 64B-aligned, so bits \[5:0\] = 0)
99/// - \[3:0\]: `hw_count` (0..=15, encodes 1..=16 lines)
100///
101/// x31 layout: `(stride & !63) | id`. For stride=64, id=0: x31 = 64.
102///
103/// # Safety
104/// `line_addr` must be 64-byte aligned. `hw_count` must be in `0..=15`.
105#[inline(always)]
106unsafe fn evict_va_hw(dst: CacheDest, line_addr: usize, hw_count: u64) {
107    let csr_enc: u64 = ((dst as u64) << 58) | (line_addr as u64 & 0x0000_FFFF_FFFF_FFC0) | hw_count;
108    // Omit `nomem`: the asm is treated as a memory barrier; the compiler will
109    // not move loads/stores across it.
110    unsafe {
111        asm!(
112            "mv t6, {x31val}",
113            "csrw 0x89f, {csr_enc}",
114            x31val  = in(reg) 64_u64,
115            csr_enc = in(reg) csr_enc,
116            out("t6") _,
117            options(nostack, preserves_flags),
118        );
119    }
120}
121
122/// Issues a single `flush_va` CSR write (CSR `0x8BF`).
123///
124/// Writes back `hw_count + 1` dirty cache lines to `dst`; the lines remain
125/// cached as clean. All parameters and the CSR field layout are identical to
126/// [`evict_va_hw`], differing only in the CSR address.
127///
128/// # Safety
129/// `line_addr` must be 64-byte aligned. `hw_count` must be in `0..=15`.
130#[inline(always)]
131unsafe fn flush_va_hw(dst: CacheDest, line_addr: usize, hw_count: u64) {
132    let csr_enc: u64 = ((dst as u64) << 58) | (line_addr as u64 & 0x0000_FFFF_FFFF_FFC0) | hw_count;
133    unsafe {
134        asm!(
135            "mv t6, {x31val}",
136            "csrw 0x8bf, {csr_enc}",
137            x31val  = in(reg) 64_u64,
138            csr_enc = in(reg) csr_enc,
139            out("t6") _,
140            options(nostack, preserves_flags),
141        );
142    }
143}
144
145/// Waits for all outstanding cache operations to complete.
146///
147/// Issues `TensorWait(ID=6)` (CSR `0x830`, xs bits \[3:0\] = 6), which stalls
148/// the hart until every previously issued `evict_va`, `flush_va`,
149/// `prefetch_va`, and `TensorLoadL2Scp` has completed. Required after any
150/// cache management instruction and before any subsequent memory access to the
151/// affected cache lines (PRM Table 9-2, event code 6; PRM Section 8.1.3).
152#[inline(always)]
153fn wait_cacheops() {
154    // Only compiled for the device target; host-side unit tests see a no-op.
155    #[cfg(target_arch = "riscv64")]
156    // SAFETY: csrrw to the U-mode-accessible TensorWait CSR (0x830) with
157    // EVENT=6 stalls the hart until cache ops complete; no memory effects
158    // other than the ordering it enforces.
159    unsafe {
160        asm!(
161            "csrrw x0, 0x830, {xs}",
162            xs = in(reg) 6_u64,
163            options(nostack, preserves_flags),
164        );
165    }
166}
167
168// ---------------------------------------------------------------------------
169// Range helpers
170// ---------------------------------------------------------------------------
171
172/// Number of 64-byte cache lines covering the byte range `[addr, addr + len)`.
173///
174/// The result accounts for a partially covered first line: if `addr` is not
175/// 64-byte aligned, the first line begins at `addr & !63`.
176#[inline(always)]
177fn line_count(addr: usize, len: usize) -> usize {
178    if len == 0 {
179        return 0;
180    }
181    let line_start = addr & !63;
182    let line_end = (addr + len + 63) & !63;
183    (line_end - line_start) >> 6
184}
185
186/// Evicts all cache lines in `[addr, addr + len)` to `dst`, in batches of 16.
187///
188/// The hardware field `hw_count` is 0-indexed (0 = 1 line, 15 = 16 lines).
189/// Each batch issues one `evict_va` CSR write covering `batch` lines.
190#[inline]
191fn do_evict(dst: CacheDest, addr: usize, len: usize) {
192    let n = line_count(addr, len);
193    if n == 0 {
194        return;
195    }
196    let mut line = addr & !63;
197    let mut rem = n;
198    while rem > 0 {
199        let batch = rem.min(16);
200        // SAFETY: `line` is 64-byte aligned; `batch - 1` is in 0..=15.
201        unsafe {
202            evict_va_hw(dst, line, (batch - 1) as u64);
203        }
204        line += batch * 64;
205        rem -= batch;
206    }
207}
208
209/// Writes back all dirty cache lines in `[addr, addr + len)` to `dst`,
210/// in batches of 16.
211#[inline]
212fn do_flush(dst: CacheDest, addr: usize, len: usize) {
213    let n = line_count(addr, len);
214    if n == 0 {
215        return;
216    }
217    let mut line = addr & !63;
218    let mut rem = n;
219    while rem > 0 {
220        let batch = rem.min(16);
221        // SAFETY: `line` is 64-byte aligned; `batch - 1` is in 0..=15.
222        unsafe {
223            flush_va_hw(dst, line, (batch - 1) as u64);
224        }
225        line += batch * 64;
226        rem -= batch;
227    }
228}
229
230// ---------------------------------------------------------------------------
231// Public API - high-level (always targets main memory)
232// ---------------------------------------------------------------------------
233
234/// Writes back dirty L1 cache lines in `[addr, addr + len)` to main memory.
235///
236/// Issues `flush_va` for every covered line, then stalls via `TensorWait(6)`
237/// until all writeback traffic has reached DDR. After this call, the flushed
238/// data is visible to host DMA and to other shires reading from DDR.
239/// The lines remain cached as clean.
240///
241/// Callers must issue [`crate::fence`] before this function to commit all
242/// prior CPU stores to L1 (PRM Section 8.1.3).
243///
244/// Equivalent to [`cache_writeback_to`]`(CacheDest::Mem, addr, len)`.
245///
246/// # Safety
247/// `addr` must be a valid virtual address; `[addr, addr + len)` must lie
248/// within device memory accessible to this hart.
249#[inline]
250pub unsafe fn cache_writeback(addr: usize, len: usize) {
251    do_flush(CacheDest::Mem, addr, len);
252    wait_cacheops();
253}
254
255/// Invalidates (evicts) L1 cache lines in `[addr, addr + len)`.
256///
257/// Issues `evict_va` for every covered line, then stalls via `TensorWait(6)`
258/// until all eviction traffic is complete. Subsequent loads to the range will
259/// fetch fresh data from DDR. Issue on the consumer side of a cross-hart or
260/// host-DMA coherence protocol after receiving the producer's synchronisation
261/// signal and before reading the produced data.
262///
263/// Callers must issue [`crate::fence`] before this function (PRM Section
264/// 8.1.3).
265///
266/// Equivalent to [`cache_invalidate_to`]`(CacheDest::Mem, addr, len)`.
267///
268/// # Safety
269/// `addr` must be a valid virtual address; `[addr, addr + len)` must lie
270/// within device memory accessible to this hart. Invalidating dirty lines
271/// without a prior writeback discards uncommitted data; use [`cache_flush`]
272/// when lines may be dirty.
273#[inline]
274pub unsafe fn cache_invalidate(addr: usize, len: usize) {
275    do_evict(CacheDest::Mem, addr, len);
276    wait_cacheops();
277}
278
279/// Writes back then invalidates L1 cache lines in `[addr, addr + len)`.
280///
281/// Issues `flush_va` for every covered line followed by `evict_va` for the
282/// same lines, then stalls via `TensorWait(6)`. Use when the calling hart has
283/// both dirty data to publish and potentially stale lines to discard.
284///
285/// Callers must issue [`crate::fence`] before this function (PRM Section
286/// 8.1.3).
287///
288/// # Safety
289/// `addr` must be a valid virtual address; `[addr, addr + len)` must lie
290/// within device memory accessible to this hart.
291#[inline]
292pub unsafe fn cache_flush(addr: usize, len: usize) {
293    do_flush(CacheDest::Mem, addr, len);
294    do_evict(CacheDest::Mem, addr, len);
295    wait_cacheops();
296}
297
298// ---------------------------------------------------------------------------
299// Public API - lower-level (explicit destination)
300// ---------------------------------------------------------------------------
301
302/// Writes back dirty cache lines in `[addr, addr + len)` to `dst`.
303///
304/// Lower-level variant of [`cache_writeback`] with an explicit destination.
305/// Issues `flush_va` then `TensorWait(6)`. Pass [`CacheDest::L2`] to make
306/// data visible to other Minions in the same shire without propagating to DDR.
307///
308/// # Safety
309/// Same constraints as [`cache_writeback`].
310#[inline]
311pub unsafe fn cache_writeback_to(dst: CacheDest, addr: usize, len: usize) {
312    do_flush(dst, addr, len);
313    wait_cacheops();
314}
315
316/// Invalidates cache lines in `[addr, addr + len)`, evicting to `dst`.
317///
318/// Lower-level variant of [`cache_invalidate`] with an explicit destination.
319/// Issues `evict_va` then `TensorWait(6)`.
320///
321/// # Safety
322/// Same constraints as [`cache_invalidate`].
323#[inline]
324pub unsafe fn cache_invalidate_to(dst: CacheDest, addr: usize, len: usize) {
325    do_evict(dst, addr, len);
326    wait_cacheops();
327}
328
329// ---------------------------------------------------------------------------
330// Tests (host-only; do not touch the hardware CSRs)
331// ---------------------------------------------------------------------------
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn line_count_zero_len() {
339        assert_eq!(line_count(0x1000, 0), 0);
340    }
341
342    #[test]
343    fn line_count_aligned_exact() {
344        // Exactly 1, 2, 3 cache lines starting at a 64-byte boundary.
345        assert_eq!(line_count(0x100, 64), 1);
346        assert_eq!(line_count(0x100, 128), 2);
347        assert_eq!(line_count(0x100, 192), 3);
348    }
349
350    #[test]
351    fn line_count_unaligned_addr_single_line() {
352        // addr=0x110 (offset 16 within a line), len=48: range is 0x110..0x140,
353        // wholly within the single line 0x100..0x140.
354        assert_eq!(line_count(0x110, 48), 1);
355    }
356
357    #[test]
358    fn line_count_unaligned_addr_two_lines() {
359        // addr=0x110, len=64: range is 0x110..0x150, crosses the 0x140 boundary.
360        assert_eq!(line_count(0x110, 64), 2);
361    }
362
363    #[test]
364    fn line_count_one_byte_past_boundary() {
365        // A single byte at the start of a new cache line adds exactly one line.
366        assert_eq!(line_count(0x100, 65), 2);
367    }
368
369    #[test]
370    fn cache_dest_discriminants() {
371        assert_eq!(CacheDest::L1 as u64, 0);
372        assert_eq!(CacheDest::L2 as u64, 1);
373        assert_eq!(CacheDest::L3 as u64, 2);
374        assert_eq!(CacheDest::Mem as u64, 3);
375    }
376
377    #[test]
378    fn evict_csr_encoding() {
379        // Verify the CSR encoding for a 64-byte-aligned address with Mem dest.
380        let addr: usize = 0x0000_8000_0001_0000; // 64B-aligned
381        let hw_count: u64 = 15; // 16 lines
382        let dst = CacheDest::Mem;
383        let csr_enc: u64 = ((dst as u64) << 58) | (addr as u64 & 0x0000_FFFF_FFFF_FFC0) | hw_count;
384        // dst=3 at bits 59:58
385        assert_eq!((csr_enc >> 58) & 0x3, 3);
386        // hw_count at bits 3:0
387        assert_eq!(csr_enc & 0xF, 15);
388        // addr embedded at bits 57:6 (addr is 64B-aligned, bits 5:0 = 0)
389        assert_eq!(csr_enc & (addr as u64), addr as u64);
390    }
391
392    #[test]
393    fn flush_csr_encoding_matches_evict_layout() {
394        // flush_va (0x8BF) uses the same field layout as evict_va (0x89F);
395        // verify the encoding formula produces the same bit pattern.
396        let addr = 0x0000_8000_0002_0000_usize;
397        let hw_count = 7_u64;
398        let dst = CacheDest::L2;
399        let enc = ((dst as u64) << 58) | (addr as u64 & 0x0000_FFFF_FFFF_FFC0) | hw_count;
400        assert_eq!((enc >> 58) & 0x3, CacheDest::L2 as u64);
401        assert_eq!(enc & 0xF, 7);
402    }
403}