Skip to main content

et_kernel/
lib.rs

1//! Shared `no_std` helpers for ET-SoC-1 compute kernels: hart identity, the
2//! U-mode trace write, a hardware memory fence, and scratchpad addressing.
3//!
4//! This is the device-side support library for the compute kernels in this
5//! package. Each kernel binary provides its own panic handler and invokes
6//! [`kernel_entry!`] to generate the `_start` entry point.
7//!
8//! # Target gating
9//!
10//! All items that contain RISC-V inline assembly are gated on
11//! `#[cfg(target_arch = "riscv64")]`: [`kernel_entry!`], [`hart_id`],
12//! [`shire_id`], [`timestamp`], [`fence`], [`trace_str`], [`Grid`], and
13//! the [`tensor`], [`pmu`], and [`cache`] modules. The library compiles as a
14//! stub on any host target (useful for `rust-analyzer` and IDE tooling);
15//! [`MsgBuf`], [`device_slice`], [`scp_shire_base`], and
16//! [`et_abi::CACHE_LINE`] remain available on all targets.
17
18#![no_std]
19
20#[cfg(target_arch = "riscv64")]
21use core::arch::asm;
22#[cfg(target_arch = "riscv64")]
23use core::ptr::{read_volatile, write_volatile};
24
25/// Generate the kernel entry point (`_start`).
26///
27/// Expands to the naked `_start` every ET-SoC-1 kernel needs: placed in
28/// `.text.init` (which the linker script lays down first at the fixed U-mode
29/// entry address), it sets the global pointer, calls the kernel's `entry_point`,
30/// and returns to firmware via `ecall`. The launch-args pointer arrives in `a0`
31/// and passes straight through to `entry_point`'s first argument.
32///
33/// The kernel binary must define
34/// `#[unsafe(no_mangle)] pub extern "C" fn entry_point(args_ptr: usize) -> i64`.
35/// Invoke this once at the crate root:
36///
37/// ```ignore
38/// et_kernel::kernel_entry!();
39/// ```
40///
41/// A missing `entry_point` or one with the wrong signature produces a
42/// compile-time error, not a silent link-time type mismatch.
43#[cfg(target_arch = "riscv64")]
44#[macro_export]
45macro_rules! kernel_entry {
46    () => {
47        // Compile-time type assertion: entry_point must have the exact C ABI
48        // signature expected by _start. A wrong signature (wrong argument type,
49        // wrong return type, unsafe qualifier, or different calling convention)
50        // is caught here rather than producing a silent ABI mismatch at link time.
51        const _: extern "C" fn(usize) -> i64 = entry_point;
52
53        #[unsafe(naked)]
54        #[unsafe(no_mangle)]
55        #[unsafe(link_section = ".text.init")]
56        pub extern "C" fn _start() -> ! {
57            ::core::arch::naked_asm!(
58                ".option push",
59                ".option norelax",
60                "la gp, __global_pointer$",
61                ".option pop",
62                "call entry_point",
63                "li a2, 0",  // KERNEL_RETURN_SUCCESS
64                "mv a1, a0", // return value
65                "li a0, 8",  // SYSCALL_RETURN_FROM_KERNEL
66                "ecall",
67            )
68        }
69    };
70}
71
72/// Base of the per-hart U-mode trace control-block array
73/// (`CM_UMODE_TRACE_CB_BASEADDR`); each entry is 64 bytes.
74#[cfg(target_arch = "riscv64")]
75pub const CB_BASE: usize = 0x8004_F23000;
76#[cfg(target_arch = "riscv64")]
77const CB_STRIDE: usize = 64;
78#[cfg(target_arch = "riscv64")]
79const CB_BASE_PER_HART: usize = 24;
80#[cfg(target_arch = "riscv64")]
81const CB_OFFSET_PER_HART: usize = 36;
82#[cfg(target_arch = "riscv64")]
83const TRACE_TYPE_STRING: u16 = 0;
84#[cfg(target_arch = "riscv64")]
85const ENTRY_HEADER_SIZE: usize = 16;
86#[cfg(target_arch = "riscv64")]
87const TRACE_STRING_MAX: usize = 512;
88
89/// Current hart ID, from the custom `hartid` CSR (`0xCD0`).
90#[cfg(target_arch = "riscv64")]
91#[inline(always)]
92pub fn hart_id() -> u32 {
93    let v: u64;
94    // SAFETY: reads a U-mode-accessible CSR with no side effects.
95    unsafe { asm!("csrr {0}, 0xcd0", out(reg) v, options(nomem, nostack, preserves_flags)) };
96    v as u32
97}
98
99/// Current shire ID (`hart_id >> 6`; 64 harts per shire).
100#[cfg(target_arch = "riscv64")]
101#[inline(always)]
102pub fn shire_id() -> u32 {
103    hart_id() >> 6
104}
105
106/// A cycle timestamp (`hpmcounter3`, CSR `0xC03`) for trace entry headers.
107///
108/// Applies the RTLMIN-6496 workaround: four back-to-back reads of CSR `0xC03`
109/// in a 16-byte-aligned block; the fourth read is the reliable value.
110#[cfg(target_arch = "riscv64")]
111#[inline(always)]
112pub fn timestamp() -> u64 {
113    let v: u64;
114    // SAFETY: reads a U-mode-accessible performance-counter CSR with no
115    // side effects. Four reads are required by the RTLMIN-6496 erratum;
116    // a single asm block prevents the compiler inserting intervening code.
117    // `nomem` is omitted so the block is treated as a memory barrier.
118    unsafe {
119        asm!(
120            ".align 4",
121            "csrrs {v0}, 0xC03, x0",
122            "csrrs {v1}, 0xC03, x0",
123            "csrrs {v2}, 0xC03, x0",
124            "csrrs {v},  0xC03, x0",
125            v0 = out(reg) _,
126            v1 = out(reg) _,
127            v2 = out(reg) _,
128            v  = out(reg) v,
129            options(nostack, preserves_flags),
130        )
131    };
132    v
133}
134
135/// Full hardware memory fence (`fence rw, rw`) that also bars compiler
136/// reordering. This is an ordering barrier, not an atomic operation.
137#[cfg(target_arch = "riscv64")]
138#[inline(always)]
139pub fn fence() {
140    // No `nomem`: the asm is treated as touching memory, so the compiler will
141    // not move loads/stores across it either.
142    unsafe { asm!("fence rw, rw", options(nostack, preserves_flags)) };
143}
144
145/// Base address of `shire`'s 2.5 MB L2 scratchpad
146/// (`ETSOC_SCP_GET_SHIRE_ADDR(shire, 0)`): `0x8000_0000 | (shire << 23)`.
147#[inline(always)]
148pub fn scp_shire_base(shire: u32) -> usize {
149    0x8000_0000usize + ((shire as usize) << 23)
150}
151
152#[cfg(target_arch = "riscv64")]
153#[inline(always)]
154fn cb_index(hart: u32) -> usize {
155    if hart < 2048 {
156        hart as usize
157    } else {
158        (hart - 32) as usize
159    }
160}
161
162#[cfg(target_arch = "riscv64")]
163#[inline(always)]
164fn align8(n: usize) -> usize {
165    (n + 7) & !7
166}
167
168/// Write `text` as a NUL-terminated string trace entry for the current hart,
169/// exactly as the SDK's `Trace_String` does (reserve via the control block, then
170/// write a `trace_string_t`).
171#[cfg(target_arch = "riscv64")]
172pub fn trace_str(text: &[u8]) {
173    let hid = hart_id();
174    let str_len = align8(text.len() + 1).min(TRACE_STRING_MAX);
175    let cb = CB_BASE + cb_index(hid) * CB_STRIDE;
176    // SAFETY: firmware populated the CB at this fixed address before launch.
177    let base = unsafe { read_volatile((cb + CB_BASE_PER_HART) as *const u64) } as usize;
178    let offset = unsafe { read_volatile((cb + CB_OFFSET_PER_HART) as *const u32) };
179    let head = base + offset as usize;
180    // SAFETY: `head` lies within this hart's reserved trace-buffer slice.
181    unsafe {
182        write_volatile(head as *mut u64, timestamp());
183        write_volatile((head + 8) as *mut u32, str_len as u32);
184        write_volatile((head + 12) as *mut u16, hid as u16);
185        write_volatile((head + 14) as *mut u16, TRACE_TYPE_STRING);
186        let s = (head + ENTRY_HEADER_SIZE) as *mut u8;
187        let mut i = 0;
188        while i < str_len {
189            let byte = if i < text.len() { text[i] } else { 0 };
190            write_volatile(s.add(i), byte);
191            i += 1;
192        }
193        write_volatile(
194            (cb + CB_OFFSET_PER_HART) as *mut u32,
195            offset + (ENTRY_HEADER_SIZE + str_len) as u32,
196        );
197    }
198}
199
200/// A fixed-capacity stack buffer for composing trace messages without `alloc`.
201pub struct MsgBuf {
202    buf: [u8; 192],
203    len: usize,
204}
205
206impl Default for MsgBuf {
207    fn default() -> Self {
208        Self::new()
209    }
210}
211
212impl MsgBuf {
213    pub fn new() -> Self {
214        MsgBuf {
215            buf: [0; 192],
216            len: 0,
217        }
218    }
219
220    /// Append raw text (truncated if the buffer fills).
221    pub fn str(&mut self, s: &[u8]) -> &mut Self {
222        let mut i = 0;
223        while i < s.len() && self.len < self.buf.len() {
224            self.buf[self.len] = s[i];
225            self.len += 1;
226            i += 1;
227        }
228        self
229    }
230
231    /// Append a decimal integer.
232    pub fn u64(&mut self, mut v: u64) -> &mut Self {
233        let mut tmp = [0u8; 20];
234        let mut c = 0;
235        loop {
236            tmp[c] = b'0' + (v % 10) as u8;
237            v /= 10;
238            c += 1;
239            if v == 0 {
240                break;
241            }
242        }
243        while c > 0 && self.len < self.buf.len() {
244            c -= 1;
245            self.buf[self.len] = tmp[c];
246            self.len += 1;
247        }
248        self
249    }
250
251    pub fn as_slice(&self) -> &[u8] {
252        &self.buf[..self.len]
253    }
254}
255
256/// Cache-line size in bytes. Per-hart outputs are placed one-per-line so that
257/// distinct harts never write the same line: false sharing silently corrupts
258/// data on this software-coherent architecture. Defined once in `et-abi` and
259/// shared with the host, so the two sides cannot disagree on the stride.
260pub use et_abi::CACHE_LINE;
261
262/// A hart's view of an SPMD launch: its identity within `n_harts` participants.
263///
264/// The safety story of the reduction demo lives here. A kernel body, given a
265/// `Grid`, can obtain only *its own* disjoint slice of the input and *its own*
266/// output cell -- it has no way to name another hart's data, so cross-hart data
267/// races are unrepresentable in the (safe) kernel body. The small `unsafe`
268/// boundary that turns device addresses into slices is confined to this module.
269#[cfg(target_arch = "riscv64")]
270pub struct Grid {
271    hart: u32,
272    n_harts: u32,
273}
274
275#[cfg(target_arch = "riscv64")]
276impl Grid {
277    /// Build from the current hart's id and the number of participating harts.
278    pub fn new(n_harts: u32) -> Self {
279        Grid {
280            hart: hart_id(),
281            n_harts,
282        }
283    }
284
285    pub fn hart(&self) -> u32 {
286        self.hart
287    }
288
289    pub fn n_harts(&self) -> u32 {
290        self.n_harts
291    }
292
293    /// Whether this hart participates (the launch runs every hart of the shire,
294    /// so surplus harts opt out).
295    pub fn active(&self) -> bool {
296        self.hart < self.n_harts
297    }
298
299    /// This hart's half-open element range of a length-`n` domain: contiguous,
300    /// disjoint across harts, and together covering all of `[0, n)` (a balanced
301    /// split, the first `n % n_harts` harts taking one extra element).
302    fn range(&self, n: usize) -> (usize, usize) {
303        let h = self.hart as usize;
304        let p = (self.n_harts as usize).max(1);
305        let base = n / p;
306        let rem = n % p;
307        let start = h * base + h.min(rem);
308        let len = base + if h < rem { 1 } else { 0 };
309        (start, start + len)
310    }
311
312    /// Borrow this hart's disjoint sub-slice of `data`.
313    pub fn my_slice<'a, T>(&self, data: &'a [T]) -> &'a [T] {
314        let (start, end) = self.range(data.len());
315        &data[start..end]
316    }
317
318    /// Borrow this hart's own output cell from an array of one cache-line-padded
319    /// `T` per hart based at device address `base`.
320    ///
321    /// # Safety
322    /// `base` must address at least `n_harts * CACHE_LINE` writable bytes of
323    /// device memory. Disjointness across harts is guaranteed by construction
324    /// (distinct `hart` ids map to distinct cache lines).
325    pub unsafe fn output_cell<'a, T>(&self, base: usize) -> &'a mut T {
326        unsafe { &mut *((base + self.hart as usize * CACHE_LINE) as *mut T) }
327    }
328}
329
330/// Tensor-extension intrinsics, all encoded as RISC-V `csrrw` writes (PRM Ch. 9).
331///
332/// **Load**: [`tensor::tensor_load`], [`tensor::tensor_load_b`],
333/// [`tensor::tensor_load_l2`].
334/// **FMA (fp32)**: [`tensor::fma32_xs`] + [`tensor::tensor_fma32`].
335/// **FMA (fp16 -> fp32)**: [`tensor::fma16a32_xs`] + [`tensor::tensor_fma16a32`]
336/// (CSR 0x801, bits 3:1 = 001).
337/// **GEMM (int8 -> int32)**: [`tensor::ima8a32_xs`] + [`tensor::tensor_ima8a32`]
338/// (CSR 0x801, bits 3:1 = 011; `DST` selects FP-register or TenC output).
339/// **Store (from FP regs)**: [`tensor::tensor_store`].
340/// **Store (from scratchpad)**: [`tensor::tensor_store_from_scp`]
341/// (CSR 0x87F, bit 48 = 1; reads L1 scratchpad lines directly to DRAM).
342/// **Reduction**: [`tensor::tensor_send`] / [`tensor::tensor_recv`]
343/// (CSR 0x800; hart-to-hart FP register exchange with optional combine via
344/// [`tensor::ReduceFunct`]).
345/// **Synchronisation**: [`tensor::tensor_wait`] / [`tensor::TensorEvent`].
346#[cfg(target_arch = "riscv64")]
347pub mod tensor;
348
349/// Performance Monitoring Unit (PMU) counter API.
350///
351/// Provides [`pmu::pmu_read`] (reads `hpmcounterN` in U-mode), the
352/// [`pmu::PmuEvent`] Minion-level event-code enum, and the
353/// [`pmu::NeighborhoodEvent`] neighbourhood-level event-code enum for
354/// characterising tensor kernel and memory-system behaviour.
355#[cfg(target_arch = "riscv64")]
356pub mod pmu;
357
358/// L1 cache management for software-coherent cross-hart sharing.
359///
360/// Provides [`cache::cache_writeback`], [`cache::cache_invalidate`], and
361/// [`cache::cache_flush`] for flushing and invalidating L1 data cache lines
362/// by virtual address and byte length. Lower-level `_to` variants accept an
363/// explicit [`cache::CacheDest`] when targeting L2 or L3 rather than DDR.
364///
365/// All functions use the `flush_va` (CSR `0x8BF`) and `evict_va` (CSR
366/// `0x89F`) hardware operations as documented in the Ainekko SDK
367/// `cacheops.h`.
368#[cfg(target_arch = "riscv64")]
369pub mod cache;
370
371/// Packed-single (PS) SIMD intrinsics for 256-bit FP registers.
372///
373/// Provides [`simd::broadcast_ps`], [`simd::fmul_ps_row`], and
374/// [`simd::scale_c_row`], encoded from `esperanto-opc.h` in the ET-SoC-1
375/// binutils fork. Requires the `f` target feature (`target-feature=+f`);
376/// without it the module is empty. Hardware-verified on aifoundry3
377/// (2026-09-18): all 1024 Minions produced correct results for `FBCX.PS`
378/// and `FMUL.PS`.
379pub mod simd;
380
381/// View `n` elements of type `T` at device address `addr` as a shared slice.
382///
383/// # Safety
384/// `addr` must point to `n` valid, aligned, initialised `T` that outlive the
385/// returned borrow and are not mutated through another path meanwhile.
386pub unsafe fn device_slice<'a, T>(addr: usize, n: usize) -> &'a [T] {
387    unsafe { core::slice::from_raw_parts(addr as *const T, n) }
388}
389
390/// Compile-time checks for items that must remain available on non-RISC-V
391/// hosts. These verify that the host-compilable API surface is intact after
392/// any future changes to the cfg gates in this file.
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn msgbuf_available_on_host() {
399        let mut b = MsgBuf::new();
400        b.str(b"et-k-rs").u64(42);
401        assert_eq!(b.as_slice(), b"et-k-rs42");
402    }
403
404    #[test]
405    fn scp_shire_base_arithmetic() {
406        assert_eq!(scp_shire_base(0), 0x8000_0000);
407        assert_eq!(scp_shire_base(1), 0x8000_0000 + (1 << 23));
408    }
409}