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