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}