Skip to main content

et_kernel/
tensor.rs

1//! Tensor-extension intrinsics for the ET-SoC-1 Minion core.
2//!
3//! All tensor instructions on the ET-SoC-1 are encoded as standard RISC-V
4//! `csrrw xd, <csr>, xs` writes (see PRM Chapter 9). No custom opcode or
5//! target-feature extension is required: `riscv64imac` suffices because the
6//! operand registers are ordinary integer GPRs (the source value `xs` is an
7//! integer register; the FP register file is accessed implicitly by the
8//! tensor co-processor hardware, not by the instruction encoding).
9//!
10//! # Concurrency model
11//!
12//! The tensor co-processor operates independently of the RISC-V hart's
13//! integer pipeline. Issuing a tensor instruction initiates an asynchronous
14//! operation; the hart must call [`tensor_wait`] with the appropriate
15//! [`TensorEvent`] before reading results or reusing the scratchpad. The
16//! ordering guarantees are:
17//!
18//! - `TensorWait(Load0)` before `tensor_fma32`: scratchpad A is populated.
19//! - `TensorWait(Fma)` before `tensor_store`: FP register file holds final C.
20//! - `fence rw, rw` (via [`crate::fence`]) after `tensor_store`: stores are
21//!   visible to other Minions and the DMA engine before the kernel returns.
22//!
23//! # Scratchpad layout
24//!
25//! Each Minion has a private 48-line L1 scratchpad (3 072 bytes). Only the
26//! primary hart of the Minion (hart 0, i.e. `mhartid & 1 == 0`) should issue
27//! tensor load/store/FMA instructions; the companion hart (hart 1) must not
28//! touch the same scratchpad lines concurrently.
29
30use core::arch::asm;
31
32// ---------------------------------------------------------------------------
33// CSR addresses (PRM Chapter 9, Table 9-1)
34// ---------------------------------------------------------------------------
35
36/// TensorFMA CSR (`tensor_fma`): selects the FMA variant via xs bits 3:1.
37/// (PRM Table 9-7: TensorFMA32 = 3:1 000, TensorFMA16A32 = 001, ...)
38pub const CSR_TENSOR_FMA:   u16 = 0x801;
39/// TensorWait CSR (`tensor_wait`): stalls the hart until the requested event.
40pub const CSR_TENSOR_WAIT:  u16 = 0x830;
41/// TensorError CSR (`tensor_error`): latched error flags from the co-processor.
42/// (PRM Table 9-1: 0x808, not 0x831)
43pub const CSR_TENSOR_ERROR: u16 = 0x808;
44/// TensorMask CSR (`tensor_mask`): per-row enable bits for the A tile.
45/// (PRM Table 9-1: 0x805, not 0x832)
46pub const CSR_TENSOR_MASK:  u16 = 0x805;
47/// TensorStore CSR (`tensor_store`): store from FP registers (bit 48 = 0) or
48/// from the L1 scratchpad (bit 48 = 1 = TensorStoreFromScp) to memory.
49/// (PRM Table 9-7: 0x87F, not 0x83E)
50pub const CSR_TENSOR_STORE: u16 = 0x87F;
51/// TensorLoad / TensorLoadB CSR (`tensor_load`): load from memory to the L1
52/// scratchpad (xs bit 52 = 0) or to the TenB register file (bit 52 = 1).
53pub const CSR_TENSOR_LOAD:    u16 = 0x83F;
54/// TensorLoadL2Scp CSR: loads rows from memory to the shire L2 cache without
55/// consuming any L1 scratchpad lines. Useful for prefetching A strips while
56/// the current k-loop tile executes, so the subsequent `tensor_load` (L1 fill)
57/// completes from L2 rather than DRAM.
58pub const CSR_TENSOR_LOAD_L2: u16 = 0x85F;
59
60// ---------------------------------------------------------------------------
61// TensorWait event codes (PRM Table 9-2, xs bits 3:0)
62// ---------------------------------------------------------------------------
63
64/// Tensor co-processor synchronisation events for [`tensor_wait`].
65///
66/// The four-bit EVENT field in the TensorWait `xs` register selects which
67/// outstanding operation the hart waits for before the instruction retires.
68#[repr(u8)]
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum TensorEvent {
71    /// Completion of all TensorLoad operations issued with ID = 0.
72    Load0 = 0,
73    /// Completion of all TensorLoad operations issued with ID = 1.
74    Load1 = 1,
75    /// Completion of all preceding TensorFMA operations; the FP register file
76    /// holds the final accumulated C tile and may be read or stored.
77    Fma   = 7,
78    /// Completion of all preceding TensorStore DMA transfers (PRM Table 9-2,
79    /// event code 8). Drains only the tensor store DMA, allowing the compiler
80    /// more freedom to reorder non-tensor memory accesses around it. Prefer
81    /// this over a full `fence rw, rw` when only tensor-store ordering is
82    /// required (e.g. confirming one tile is written before reusing FP registers
83    /// for the next tile in a pipelined loop).
84    Store = 8,
85}
86
87// ---------------------------------------------------------------------------
88// TensorError (PRM Table 9-3)
89// ---------------------------------------------------------------------------
90
91/// Tensor co-processor error status, returned by [`check_tensor_error`].
92///
93/// The raw value is the 64-bit content of the `tensor_error` CSR (0x808).
94/// Named bit accessors will be added once PRM Table 9-3 bit positions are
95/// confirmed on hardware. Use [`raw`](TensorError::raw) to inspect the value
96/// directly in the interim.
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub struct TensorError(u64);
99
100impl TensorError {
101    /// Returns the raw CSR value as read from `tensor_error` (CSR 0x808).
102    #[inline]
103    pub fn raw(self) -> u64 {
104        self.0
105    }
106}
107
108// ---------------------------------------------------------------------------
109// Public intrinsic functions
110// ---------------------------------------------------------------------------
111
112/// Stall the hart until the specified tensor co-processor event fires.
113///
114/// This must be called between dependent tensor operations to enforce ordering
115/// -- the co-processor and the hart pipeline are otherwise decoupled.
116#[inline(always)]
117pub fn tensor_wait(event: TensorEvent) {
118    let xs: u64 = event as u64;
119    // SAFETY: csrrw to a U-mode-accessible tensor CSR with no memory effects
120    // from the hart's perspective; the co-processor drains its pipeline.
121    unsafe {
122        asm!(
123            concat!("csrrw x0, ", stringify!(0x830), ", {xs}"),
124            xs = in(reg) xs,
125            options(nomem, nostack, preserves_flags),
126        );
127    }
128}
129
130/// Read the tensor co-processor error status register.
131///
132/// Returns 0 when no error has occurred since the last reset. A non-zero
133/// value encodes the error class in bits defined by PRM Table 9-3. Call
134/// after `tensor_wait` to check for co-processor faults. Prefer
135/// [`check_tensor_error`] to obtain a typed result.
136#[must_use = "tensor_error() returns the co-processor fault status; \
137              a non-zero value indicates a hardware error that must be handled"]
138#[inline(always)]
139pub fn tensor_error() -> u64 {
140    let v: u64;
141    // SAFETY: csrrs with rs1 = x0 reads without side effect.
142    unsafe {
143        asm!(
144            concat!("csrrs {v}, ", stringify!(0x808), ", x0"),
145            v = out(reg) v,
146            options(nomem, nostack, preserves_flags),
147        );
148    }
149    v
150}
151
152/// Check the tensor co-processor error register and return a typed result.
153///
154/// Returns `Ok(())` when no fault has been latched. Returns `Err(TensorError)`
155/// containing the raw CSR value otherwise. Call after `tensor_wait` to verify
156/// that the preceding tensor operation completed without fault. Named bit
157/// accessors on [`TensorError`] will be added once PRM Table 9-3 bit positions
158/// are confirmed on hardware.
159///
160/// # Example
161/// ```no_run
162/// # use et_kernel::tensor::{TensorEvent, tensor_wait, check_tensor_error};
163/// # unsafe {
164/// tensor_wait(TensorEvent::Fma);
165/// check_tensor_error().expect("TensorFMA fault");
166/// # }
167/// ```
168#[inline(always)]
169pub fn check_tensor_error() -> Result<(), TensorError> {
170    let v = tensor_error();
171    if v == 0 { Ok(()) } else { Err(TensorError(v)) }
172}
173
174/// Initiate an asynchronous TensorLoadL2Scp from memory into the shire L2 cache.
175///
176/// Identical to [`tensor_load`] in xs encoding and x31 convention, but targets
177/// CSR `0x85F` (TensorLoadL2Scp) rather than `0x83F`. The rows are loaded into
178/// the shire L2 without consuming any L1 scratchpad lines. Use this to prefetch
179/// A strips while the current k-loop FMA executes; the subsequent
180/// [`tensor_load`] for the same address will then complete from L2 rather than
181/// DRAM, removing A-DMA latency from the FMA critical path.
182///
183/// # Parameters
184/// Same as [`tensor_load`]: `addr` (64-byte aligned), `start` (L2 target line
185/// index), `rows` (rows to load minus one, 0..=15), `id` (load event selector),
186/// `stride` (row stride in bytes, 64-byte aligned).
187///
188/// # Safety
189/// Same constraints as [`tensor_load`]: `addr` must be aligned and within
190/// device memory; must be called from the primary hart.
191#[inline(always)]
192pub unsafe fn tensor_load_l2(addr: usize, start: u8, rows: u8, id: bool, stride: u64) {
193    // xs layout is identical to TensorLoad; only the CSR address differs.
194    let xs: u64 = ((start as u64 & 0x3F) << 53)
195               |  (addr as u64)
196               |  (rows as u64 & 0xF);
197    unsafe {
198        asm!(
199            "mv t6, {stride}",
200            concat!("csrrw x0, ", stringify!(0x85F), ", {xs}"),
201            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
202            xs     = in(reg) xs,
203            out("t6") _,
204            options(nostack),
205        );
206    }
207}
208
209/// Write the per-row enable mask for the next TensorFMA.
210///
211/// Bit `i` in `mask` enables row `i` of the A tile. Setting bit `i = 0`
212/// suppresses the update to C row `i` (useful for partial M tiles when the
213/// mask register is more convenient than setting AROWS). For most uses,
214/// leave the mask at its reset value of all-ones and control the tile size
215/// via the AROWS field in [`tensor_fma32`].
216#[inline(always)]
217pub fn set_tensor_mask(mask: u16) {
218    let xs: u64 = mask as u64;
219    unsafe {
220        asm!(
221            concat!("csrrw x0, ", stringify!(0x805), ", {xs}"),
222            xs = in(reg) xs,
223            options(nomem, nostack, preserves_flags),
224        );
225    }
226}
227
228/// Initiate an asynchronous TensorLoad from memory into the L1 scratchpad.
229///
230/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into
231/// L1 scratchpad lines `start` through `start + rows`. Row `i` is read from
232/// address `addr + i * stride`. The operation is asynchronous: call
233/// `tensor_wait(TensorEvent::Load0)` (or `Load1` if `id = true`) before
234/// reading the scratchpad in a subsequent [`tensor_fma32`].
235///
236/// # Parameters
237/// - `addr`: 64-byte aligned virtual address of the first row in memory.
238/// - `start`: L1 scratchpad starting line index (0..=47).
239/// - `rows`: number of rows to load minus one (ROWS field, 0..=15).
240///   Loads `rows + 1` cache lines.
241/// - `id`: selects the TensorWait event (false = `Load0`, true = `Load1`).
242/// - `stride`: row stride in bytes (64-byte aligned); placed in x31 by this
243///   function immediately before the CSRRW instruction.
244///
245/// # Safety
246/// - `addr` must be 64-byte aligned and point to `(rows + 1) * stride` valid,
247///   readable bytes of device memory.
248/// - Must be called from the primary hart of the Minion (mhartid & 1 == 0).
249#[inline(always)]
250pub unsafe fn tensor_load(addr: usize, start: u8, rows: u8, id: bool, stride: u64) {
251    // xs bit layout (PRM Table 9-5):
252    //   63: MSK=0, 62: COOP=0, 61:59=000 (TensorLoad variant),
253    //   58:53=START (6-bit scratchpad line index),
254    //   52=0 (TensorLoad, not TensorLoadB),
255    //   51:48=0 (reserved), 47:6=ADDR>>6 (addr is 64B-aligned so bits 5:0 = 0),
256    //   5:4=0 (reserved), 3:0=ROWS.
257    let xs: u64 = ((start as u64 & 0x3F) << 53)
258               |  (addr as u64)           // bits 47:6; addr is 64B-aligned so addr & !63 == addr
259               |  (rows as u64 & 0xF);
260    // x31 (t6) carries the row stride; the hardware reads it implicitly.
261    unsafe {
262        asm!(
263            "mv t6, {stride}",
264            concat!("csrrw x0, ", stringify!(0x83F), ", {xs}"),
265            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
266            xs     = in(reg) xs,
267            out("t6") _,
268            options(nostack),
269        );
270    }
271}
272
273/// Initiate an asynchronous TensorLoadB from memory into the TenB register file.
274///
275/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into the
276/// dedicated TenB buffer. This forward-pairs with the next [`tensor_fma32`]
277/// call that uses `tenb = true`; the FMA waits internally for the load to
278/// complete, so no explicit `tensor_wait` is needed between LoadB and FMA.
279///
280/// # Parameters
281/// - `addr`: 64-byte aligned virtual address of the first B row in memory.
282/// - `rows`: B rows to load minus one (ACOLS of the subsequent FMA, 0..=15).
283/// - `coop`: set for cooperative multi-hart loading (advanced; leave false).
284/// - `stride`: row stride of B in bytes (64-byte aligned); placed in x31.
285/// - `id`: load event identifier placed in bit 0 of x31 (false = `Load0`,
286///   true = `Load1`). Use `Load1` when a `tensor_load` with `id: false` is
287///   also in flight, so that `tensor_wait(Load0)` waits only for the A tile
288///   and not for the B DMA (which forward-pairs with the FMA anyway).
289///
290/// # Safety
291/// Same alignment and primary-hart constraints as [`tensor_load`].
292#[inline(always)]
293pub unsafe fn tensor_load_b(addr: usize, rows: u8, coop: bool, stride: u64, id: bool) {
294    // xs bit layout (PRM Table 9-6):
295    //   63: MSK=0, 62: COOP, 61:53=0 (reserved),
296    //   52=1 (TensorLoadB distinguisher),
297    //   51:48=0 (reserved), 47:6=ADDR>>6, 5:4=0, 3:0=ROWS.
298    // x31 bit 0 = ID (identical mechanism to TensorLoad; PRM Chapter 9).
299    let xs: u64 = ((coop as u64)  << 62)
300               |  (1_u64          << 52)
301               |  (addr as u64)           // 64B-aligned: bits 47:6 correct
302               |  (rows as u64 & 0xF);
303    unsafe {
304        asm!(
305            "mv t6, {stride}",
306            concat!("csrrw x0, ", stringify!(0x83F), ", {xs}"),
307            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
308            xs     = in(reg) xs,
309            out("t6") _,
310            options(nostack),
311        );
312    }
313}
314
315/// Build the xs value for a TensorFMA32 instruction.
316///
317/// The FMA computes C[i][j] += A[i][k] * B[k][j] (or C = A*B when
318/// `mul_only = true`), accumulating into the FP register file.
319///
320/// # Parameters
321/// - `bcols`:    B column groups minus one (BCOLS field, 0..=3; output columns
322///   = 4*(bcols+1), e.g. 3 -> 16 columns).
323/// - `arows`:    A tile rows minus one (AROWS field, 0..=15).
324/// - `acols`:    A tile columns minus one (ACOLS field, 0..=15); also the
325///   number of B rows loaded by the preceding [`tensor_load_b`].
326/// - `aoffset`:  byte offset within each scratchpad line where A row data
327///   begins, in 4-byte units (AOFFSET, 0..=15). Use 0 when A columns start
328///   at the beginning of a cache line.
329/// - `tenb`:     `true` to read B from the TenB register file (filled by the
330///   preceding [`tensor_load_b`]); `false` to read from the L1 scratchpad
331///   at `bstart`.
332/// - `bstart`:   scratchpad line index of B (ignored when `tenb = true`).
333/// - `astart`:   scratchpad line index of A (ASTART field, 0..=47).
334/// - `mul_only`: `true` for C = A*B (ignore existing FP register values);
335///   `false` for C += A*B (accumulate into current FP registers).
336/// - `use_mask`: apply the tensor_mask row-enable register.
337#[must_use = "the returned xs value must be passed to tensor_fma32; discarding it issues no instruction"]
338#[allow(clippy::too_many_arguments)]
339#[inline]
340pub fn fma32_xs(
341    bcols:    u8,
342    arows:    u8,
343    acols:    u8,
344    aoffset:  u8,
345    tenb:     bool,
346    bstart:   u8,
347    astart:   u8,
348    mul_only: bool,
349    use_mask: bool,
350) -> u64 {
351    // xs bit layout (PRM Table 9-4):
352    //   63: MSK, 62:57: reserved (0), 56:55: BCOLS, 54:51: AROWS,
353    //   50:47: ACOLS, 46:43: AOFFSET, 42:21: reserved (0), 20: TENB,
354    //   19:18: reserved (0), 17:12: BSTART, 11:10: reserved (0),
355    //   9:4: ASTART, 3:1: 000 (FMA32 TensorType), 0: MUL.
356    ((use_mask as u64)       << 63)
357  | ((bcols   as u64 & 0x3)  << 55)
358  | ((arows   as u64 & 0xF)  << 51)
359  | ((acols   as u64 & 0xF)  << 47)
360  | ((aoffset as u64 & 0xF)  << 43)
361  | ((tenb    as u64)        << 20)
362  | ((bstart  as u64 & 0x3F) << 12)
363  | ((astart  as u64 & 0x3F) <<  4)
364  // bits 3:1 = 000 (FMA32 TensorType selector)
365  | (mul_only as u64)
366}
367
368/// Initiate an asynchronous TensorFMA32.
369///
370/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`fma32_xs`]. The
371/// operation is asynchronous: call `tensor_wait(TensorEvent::Fma)` before
372/// reading the FP register file or issuing a subsequent [`tensor_store`].
373///
374/// # Safety
375/// - The L1 scratchpad must be fully populated (TensorLoad with subsequent
376///   `tensor_wait(Load0)`) before this call when `tenb = false`, or
377///   equivalently [`tensor_load_b`] must have been issued before this call
378///   for the TenB path.
379/// - Must be called from the primary hart of the Minion.
380#[inline(always)]
381pub unsafe fn tensor_fma32(xs: u64) {
382    unsafe {
383        asm!(
384            concat!("csrrw x0, ", stringify!(0x801), ", {xs}"),
385            xs = in(reg) xs,
386            options(nostack),
387        );
388    }
389}
390
391/// Initiate an asynchronous TensorStore from the FP register file to memory.
392///
393/// Stores `arows + 1` rows of 64 bytes each (16 f32 per row, occupying two
394/// consecutive 256-bit FP registers) to memory. Row `i` is stored to
395/// address `addr + i * stride`, reading from FP registers f[2i] and f[2i+1].
396/// The operation is asynchronous: call [`crate::fence`] after to guarantee
397/// visibility to other agents before the kernel returns.
398///
399/// # Parameters
400/// - `addr`:  64-byte aligned virtual address of the first C row in memory.
401/// - `arows`: number of C rows to store minus one (ROWS field, 0..=15).
402/// - `stride`: row stride of C in bytes (64-byte aligned); placed in x31.
403///
404/// # Safety
405/// - `addr` must be 64-byte aligned and point to `(arows + 1) * stride` bytes
406///   of writable device memory.
407/// - `tensor_wait(TensorEvent::Fma)` must have been called first.
408/// - Must be called from the primary hart of the Minion.
409#[inline(always)]
410pub unsafe fn tensor_store(addr: usize, arows: u8, stride: u64) {
411    // xs bit layout (PRM Table 9-7):
412    //   63:62: STEP=0 (fstep=1; row i uses f[2i] and f[2i+1]),
413    //   61:57: FREG=0 (start at f0),
414    //   56:55: SIZE=3 (64 bytes = 16 f32 per row, two 256-bit registers),
415    //   54:51: ROWS,
416    //   50:49: COOP=0 (no cooperative multi-hart store),
417    //   48:   0 (store from FP registers, not from Scp),
418    //   47:4: ADDR >> 4 (addr is 64B-aligned, so addr & !0xF == addr),
419    //   3:0:  0000.
420    // Zero fields (STEP=0 at 63:62, FREG=0 at 61:57, COOP=0 at 50:49,
421    // source=FP-registers at 48) are left as the natural zero of u64.
422    let xs: u64 = (3_u64 << 55)                        // SIZE=3 (64B/row)
423               |  ((arows as u64) << 51)               // ROWS
424               |  (addr as u64 & !0xF_usize as u64);   // ADDR[47:4]; addr is 64B-aligned
425    // x31 carries the C row stride; TensorStore uses bits [47:4] of x31.
426    unsafe {
427        asm!(
428            "mv t6, {stride}",
429            concat!("csrrw x0, ", stringify!(0x87F), ", {xs}"),
430            stride = in(reg) stride,
431            xs     = in(reg) xs,
432            out("t6") _,
433            options(nostack),
434        );
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    /// Verify the FMA32 xs bit packing for a standard full-tile configuration:
443    /// BCOLS=3, AROWS=15, ACOLS=15, AOFFSET=0, TENB=1, BSTART=0, ASTART=0.
444    #[test]
445    fn fma32_xs_full_tile() {
446        let xs = fma32_xs(3, 15, 15, 0, true, 0, 0, false, false);
447        // BCOLS=3 at bits 56:55 -> 3 << 55
448        assert_eq!(xs & (0x3 << 55), 3 << 55);
449        // AROWS=15 at bits 54:51 -> 15 << 51
450        assert_eq!(xs & (0xF << 51), 15 << 51);
451        // ACOLS=15 at bits 50:47
452        assert_eq!(xs & (0xF << 47), 15 << 47);
453        // TENB=1 at bit 20
454        assert_eq!(xs & (1 << 20), 1 << 20);
455        // MUL=0, MSK=0
456        assert_eq!(xs & 1, 0);
457        assert_eq!(xs >> 63, 0);
458    }
459
460    /// Verify mul_only sets bit 0.
461    #[test]
462    fn fma32_xs_mul_only() {
463        let xs = fma32_xs(3, 15, 15, 0, true, 0, 0, true, false);
464        assert_eq!(xs & 1, 1);
465    }
466
467    /// Verify that TensorEvent discriminants match PRM Table 9-2.
468    #[test]
469    fn tensor_event_discriminants() {
470        assert_eq!(TensorEvent::Load0 as u64, 0);
471        assert_eq!(TensorEvent::Load1 as u64, 1);
472        assert_eq!(TensorEvent::Fma   as u64, 7);
473        assert_eq!(TensorEvent::Store as u64, 8);
474    }
475
476    /// Verify TensorLoad xs encoding for addr=0x1000, start=0, rows=15.
477    #[test]
478    fn tensor_load_xs_encoding() {
479        let addr: usize = 0x0080_0000_1000; // 64B-aligned
480        let start: u8 = 0;
481        let rows: u8 = 15;
482        let xs: u64 = ((start as u64 & 0x3F) << 53)
483                   |  (addr as u64)
484                   |  (rows as u64 & 0xF);
485        // START field (bits 58:53) = 0
486        assert_eq!((xs >> 53) & 0x3F, 0);
487        // bit 52 = 0 (TensorLoad, not TensorLoadB)
488        assert_eq!((xs >> 52) & 1, 0);
489        // ROWS = 15
490        assert_eq!(xs & 0xF, 15);
491        // ADDR embedded at bits 47:6 (addr = 0x80_0000_1000, bits fit in 47:6)
492        let addr_bits = addr as u64 & 0x0000_FFFF_FFFF_FFFF;
493        assert_eq!(xs & addr_bits, addr_bits);
494    }
495
496    /// Verify TensorStore xs encoding: STEP=0, FREG=0, SIZE=3.
497    #[test]
498    fn tensor_store_xs_fields() {
499        let addr: usize = 0x0080_0000_2000; // 64B-aligned
500        let arows: u8 = 7;
501        let xs: u64 = (3_u64 << 55)              // SIZE=3
502                   |  ((arows as u64) << 51)
503                   |  (addr as u64 & !0xF_usize as u64);
504        // SIZE = 3 at bits 56:55
505        assert_eq!((xs >> 55) & 0x3, 3);
506        // ROWS = 7 at bits 54:51
507        assert_eq!((xs >> 51) & 0xF, 7);
508        // STEP = 0 at bits 63:62
509        assert_eq!(xs >> 62, 0);
510        // FREG = 0 at bits 61:57
511        assert_eq!((xs >> 57) & 0x1F, 0);
512        // ADDR is embedded (addr is 64B-aligned, so !0xF == addr)
513        assert_eq!(xs & (addr as u64), addr as u64);
514    }
515}