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/// - \[47:6\]: VA bits \[47:6\] (`line_addr` is 64B-aligned, so bits \[5:0\] = 0;
99///   the mask `0x0000_FFFF_FFFF_FFC0` preserves bits \[47:6\] only)
100/// - \[3:0\]: `hw_count` (0..=15, encodes 1..=16 lines)
101///
102/// x31 layout: `(stride & !63) | id`. For stride=64, id=0: x31 = 64.
103///
104/// # Safety
105/// `line_addr` must be 64-byte aligned. `hw_count` must be in `0..=15`.
106#[inline(always)]
107unsafe fn evict_va_hw(dst: CacheDest, line_addr: usize, hw_count: u64) {
108    let csr_enc: u64 = ((dst as u64) << 58) | (line_addr as u64 & 0x0000_FFFF_FFFF_FFC0) | hw_count;
109    // Omit `nomem`: the asm is treated as a memory barrier; the compiler will
110    // not move loads/stores across it.
111    unsafe {
112        asm!(
113            "mv t6, {x31val}",
114            "csrw 0x89f, {csr_enc}",
115            x31val  = in(reg) 64_u64,
116            csr_enc = in(reg) csr_enc,
117            out("x31") _,
118            options(nostack, preserves_flags),
119        );
120    }
121}
122
123/// Issues a single `flush_va` CSR write (CSR `0x8BF`).
124///
125/// Writes back `hw_count + 1` dirty cache lines to `dst`; the lines remain
126/// cached as clean. All parameters and the CSR field layout are identical to
127/// [`evict_va_hw`], differing only in the CSR address.
128///
129/// # Safety
130/// `line_addr` must be 64-byte aligned. `hw_count` must be in `0..=15`.
131#[inline(always)]
132unsafe fn flush_va_hw(dst: CacheDest, line_addr: usize, hw_count: u64) {
133    let csr_enc: u64 = ((dst as u64) << 58) | (line_addr as u64 & 0x0000_FFFF_FFFF_FFC0) | hw_count;
134    unsafe {
135        asm!(
136            "mv t6, {x31val}",
137            "csrw 0x8bf, {csr_enc}",
138            x31val  = in(reg) 64_u64,
139            csr_enc = in(reg) csr_enc,
140            out("x31") _,
141            options(nostack, preserves_flags),
142        );
143    }
144}
145
146/// Waits for all outstanding cache operations to complete.
147///
148/// Issues `TensorWait(ID=6)` (CSR `0x830`, xs bits \[3:0\] = 6), which stalls
149/// the hart until every previously issued `evict_va`, `flush_va`,
150/// `prefetch_va`, and `TensorLoadL2Scp` has completed. Required after any
151/// cache management instruction and before any subsequent memory access to the
152/// affected cache lines (PRM Table 9-2, event code 6; PRM Section 8.1.3).
153#[inline(always)]
154fn wait_cacheops() {
155    // Only compiled for the device target; host-side unit tests see a no-op.
156    #[cfg(target_arch = "riscv64")]
157    // SAFETY: csrrw to the U-mode-accessible TensorWait CSR (0x830) with
158    // EVENT=6 stalls the hart until cache ops complete; no memory effects
159    // other than the ordering it enforces.
160    unsafe {
161        asm!(
162            "csrrw x0, 0x830, {xs}",
163            xs = in(reg) 6_u64,
164            options(nostack, preserves_flags),
165        );
166    }
167}
168
169// ---------------------------------------------------------------------------
170// Range helpers
171// ---------------------------------------------------------------------------
172
173/// Number of 64-byte cache lines covering the byte range `[addr, addr + len)`.
174///
175/// The result accounts for a partially covered first line: if `addr` is not
176/// 64-byte aligned, the first line begins at `addr & !63`.
177#[inline(always)]
178fn line_count(addr: usize, len: usize) -> usize {
179    if len == 0 {
180        return 0;
181    }
182    let line_start = addr & !63;
183    let line_end = (addr + len + 63) & !63;
184    (line_end - line_start) >> 6
185}
186
187/// Evicts all cache lines in `[addr, addr + len)` to `dst`, in batches of 16.
188///
189/// The hardware field `hw_count` is 0-indexed (0 = 1 line, 15 = 16 lines).
190/// Each batch issues one `evict_va` CSR write covering `batch` lines.
191#[inline]
192fn do_evict(dst: CacheDest, addr: usize, len: usize) {
193    let n = line_count(addr, len);
194    if n == 0 {
195        return;
196    }
197    let mut line = addr & !63;
198    let mut rem = n;
199    while rem > 0 {
200        let batch = rem.min(16);
201        // SAFETY: `line` is 64-byte aligned; `batch - 1` is in 0..=15.
202        unsafe {
203            evict_va_hw(dst, line, (batch - 1) as u64);
204        }
205        line += batch * 64;
206        rem -= batch;
207    }
208}
209
210/// Writes back all dirty cache lines in `[addr, addr + len)` to `dst`,
211/// in batches of 16.
212#[inline]
213fn do_flush(dst: CacheDest, addr: usize, len: usize) {
214    let n = line_count(addr, len);
215    if n == 0 {
216        return;
217    }
218    let mut line = addr & !63;
219    let mut rem = n;
220    while rem > 0 {
221        let batch = rem.min(16);
222        // SAFETY: `line` is 64-byte aligned; `batch - 1` is in 0..=15.
223        unsafe {
224            flush_va_hw(dst, line, (batch - 1) as u64);
225        }
226        line += batch * 64;
227        rem -= batch;
228    }
229}
230
231// ---------------------------------------------------------------------------
232// Public API - high-level (always targets main memory)
233// ---------------------------------------------------------------------------
234
235/// Writes back dirty L1 cache lines in `[addr, addr + len)` to main memory.
236///
237/// Issues `flush_va` for every covered line, then stalls via `TensorWait(6)`
238/// until all writeback traffic has reached DDR. After this call, the flushed
239/// data is visible to host DMA and to other shires reading from DDR.
240/// The lines remain cached as clean.
241///
242/// Callers must issue [`crate::fence`] before this function to commit all
243/// prior CPU stores to L1 (PRM Section 8.1.3).
244///
245/// Equivalent to [`cache_writeback_to`]`(CacheDest::Mem, addr, len)`.
246///
247/// # Safety
248/// `addr` must be a valid virtual address; `[addr, addr + len)` must lie
249/// within device memory accessible to this hart.
250#[inline]
251pub unsafe fn cache_writeback(addr: usize, len: usize) {
252    do_flush(CacheDest::Mem, addr, len);
253    wait_cacheops();
254}
255
256/// Invalidates (evicts) L1 cache lines in `[addr, addr + len)`.
257///
258/// Issues `evict_va` for every covered line, then stalls via `TensorWait(6)`
259/// until all eviction traffic is complete. Subsequent loads to the range will
260/// fetch fresh data from DDR. Issue on the consumer side of a cross-hart or
261/// host-DMA coherence protocol after receiving the producer's synchronisation
262/// signal and before reading the produced data.
263///
264/// Callers must issue [`crate::fence`] before this function (PRM Section
265/// 8.1.3).
266///
267/// Equivalent to [`cache_invalidate_to`]`(CacheDest::Mem, addr, len)`.
268///
269/// # Safety
270/// `addr` must be a valid virtual address; `[addr, addr + len)` must lie
271/// within device memory accessible to this hart. Invalidating dirty lines
272/// without a prior writeback discards uncommitted data; use [`cache_flush`]
273/// when lines may be dirty.
274#[inline]
275pub unsafe fn cache_invalidate(addr: usize, len: usize) {
276    do_evict(CacheDest::Mem, addr, len);
277    wait_cacheops();
278}
279
280/// Writes back then invalidates L1 cache lines in `[addr, addr + len)`.
281///
282/// Issues `flush_va` for every covered line followed by `evict_va` for the
283/// same lines, then stalls via `TensorWait(6)`. Use when the calling hart has
284/// both dirty data to publish and potentially stale lines to discard.
285///
286/// Callers must issue [`crate::fence`] before this function (PRM Section
287/// 8.1.3).
288///
289/// # Safety
290/// `addr` must be a valid virtual address; `[addr, addr + len)` must lie
291/// within device memory accessible to this hart.
292#[inline]
293pub unsafe fn cache_flush(addr: usize, len: usize) {
294    do_flush(CacheDest::Mem, addr, len);
295    do_evict(CacheDest::Mem, addr, len);
296    wait_cacheops();
297}
298
299// ---------------------------------------------------------------------------
300// Public API - lower-level (explicit destination)
301// ---------------------------------------------------------------------------
302
303/// Writes back dirty cache lines in `[addr, addr + len)` to `dst`.
304///
305/// Lower-level variant of [`cache_writeback`] with an explicit destination.
306/// Issues `flush_va` then `TensorWait(6)`. Pass [`CacheDest::L2`] to make
307/// data visible to other Minions in the same shire without propagating to DDR.
308///
309/// # Safety
310/// Same constraints as [`cache_writeback`].
311#[inline]
312pub unsafe fn cache_writeback_to(dst: CacheDest, addr: usize, len: usize) {
313    do_flush(dst, addr, len);
314    wait_cacheops();
315}
316
317/// Invalidates cache lines in `[addr, addr + len)`, evicting to `dst`.
318///
319/// Lower-level variant of [`cache_invalidate`] with an explicit destination.
320/// Issues `evict_va` then `TensorWait(6)`.
321///
322/// # Safety
323/// Same constraints as [`cache_invalidate`].
324#[inline]
325pub unsafe fn cache_invalidate_to(dst: CacheDest, addr: usize, len: usize) {
326    do_evict(dst, addr, len);
327    wait_cacheops();
328}
329
330// ---------------------------------------------------------------------------
331// Tests (host-only; do not touch the hardware CSRs)
332// ---------------------------------------------------------------------------
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn line_count_zero_len() {
340        assert_eq!(line_count(0x1000, 0), 0);
341    }
342
343    #[test]
344    fn line_count_aligned_exact() {
345        // Exactly 1, 2, 3 cache lines starting at a 64-byte boundary.
346        assert_eq!(line_count(0x100, 64), 1);
347        assert_eq!(line_count(0x100, 128), 2);
348        assert_eq!(line_count(0x100, 192), 3);
349    }
350
351    #[test]
352    fn line_count_unaligned_addr_single_line() {
353        // addr=0x110 (offset 16 within a line), len=48: range is 0x110..0x140,
354        // wholly within the single line 0x100..0x140.
355        assert_eq!(line_count(0x110, 48), 1);
356    }
357
358    #[test]
359    fn line_count_unaligned_addr_two_lines() {
360        // addr=0x110, len=64: range is 0x110..0x150, crosses the 0x140 boundary.
361        assert_eq!(line_count(0x110, 64), 2);
362    }
363
364    #[test]
365    fn line_count_one_byte_past_boundary() {
366        // A single byte at the start of a new cache line adds exactly one line.
367        assert_eq!(line_count(0x100, 65), 2);
368    }
369
370    #[test]
371    fn cache_dest_discriminants() {
372        assert_eq!(CacheDest::L1 as u64, 0);
373        assert_eq!(CacheDest::L2 as u64, 1);
374        assert_eq!(CacheDest::L3 as u64, 2);
375        assert_eq!(CacheDest::Mem as u64, 3);
376    }
377
378    #[test]
379    fn evict_csr_encoding() {
380        // Verify the CSR encoding for a 64-byte-aligned address with Mem dest.
381        let addr: usize = 0x0000_8000_0001_0000; // 64B-aligned
382        let hw_count: u64 = 15; // 16 lines
383        let dst = CacheDest::Mem;
384        let csr_enc: u64 = ((dst as u64) << 58) | (addr as u64 & 0x0000_FFFF_FFFF_FFC0) | hw_count;
385        // dst=3 at bits 59:58
386        assert_eq!((csr_enc >> 58) & 0x3, 3);
387        // hw_count at bits 3:0
388        assert_eq!(csr_enc & 0xF, 15);
389        // addr embedded at bits 47:6 (addr is 64B-aligned, bits 5:0 = 0)
390        assert_eq!(csr_enc & (addr as u64), addr as u64);
391    }
392
393    #[test]
394    fn flush_csr_encoding_matches_evict_layout() {
395        // flush_va (0x8BF) uses the same field layout as evict_va (0x89F);
396        // verify the encoding formula produces the same bit pattern.
397        let addr = 0x0000_8000_0002_0000_usize;
398        let hw_count = 7_u64;
399        let dst = CacheDest::L2;
400        let enc = ((dst as u64) << 58) | (addr as u64 & 0x0000_FFFF_FFFF_FFC0) | hw_count;
401        assert_eq!((enc >> 58) & 0x3, CacheDest::L2 as u64);
402        assert_eq!(enc & 0xF, 7);
403    }
404}