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#[macro_export]
29macro_rules! kernel_entry {
30    () => {
31        #[unsafe(naked)]
32        #[unsafe(no_mangle)]
33        #[unsafe(link_section = ".text.init")]
34        pub extern "C" fn _start() -> ! {
35            ::core::arch::naked_asm!(
36                ".option push",
37                ".option norelax",
38                "la gp, __global_pointer$",
39                ".option pop",
40                "call entry_point",
41                "li a2, 0",  // KERNEL_RETURN_SUCCESS
42                "mv a1, a0", // return value
43                "li a0, 8",  // SYSCALL_RETURN_FROM_KERNEL
44                "ecall",
45            )
46        }
47    };
48}
49
50/// Base of the per-hart U-mode trace control-block array
51/// (`CM_UMODE_TRACE_CB_BASEADDR`); each entry is 64 bytes.
52pub const CB_BASE: usize = 0x8004_F23000;
53const CB_STRIDE: usize = 64;
54const CB_BASE_PER_HART: usize = 24;
55const CB_OFFSET_PER_HART: usize = 36;
56const TRACE_TYPE_STRING: u16 = 0;
57const ENTRY_HEADER_SIZE: usize = 16;
58const TRACE_STRING_MAX: usize = 512;
59
60/// Current hart ID, from the custom `hartid` CSR (`0xCD0`).
61#[inline(always)]
62pub fn hart_id() -> u32 {
63    let v: u64;
64    // SAFETY: reads a U-mode-accessible CSR with no side effects.
65    unsafe { asm!("csrr {0}, 0xcd0", out(reg) v, options(nomem, nostack, preserves_flags)) };
66    v as u32
67}
68
69/// Current shire ID (`hart_id >> 6`; 64 harts per shire).
70#[inline(always)]
71pub fn shire_id() -> u32 {
72    hart_id() >> 6
73}
74
75/// A cycle timestamp (`hpmcounter3`, CSR `0xC03`) for trace entry headers.
76#[inline(always)]
77pub fn timestamp() -> u64 {
78    let v: u64;
79    // SAFETY: reads a U-mode-accessible performance-counter CSR.
80    unsafe { asm!("csrr {0}, 0xc03", out(reg) v, options(nomem, nostack, preserves_flags)) };
81    v
82}
83
84/// Full hardware memory fence (`fence rw, rw`) that also bars compiler
85/// reordering. This is an ordering barrier, not an atomic operation.
86#[inline(always)]
87pub fn fence() {
88    // No `nomem`: the asm is treated as touching memory, so the compiler will
89    // not move loads/stores across it either.
90    unsafe { asm!("fence rw, rw", options(nostack, preserves_flags)) };
91}
92
93/// Base address of `shire`'s 2.5 MB L2 scratchpad
94/// (`ETSOC_SCP_GET_SHIRE_ADDR(shire, 0)`): `0x8000_0000 | (shire << 23)`.
95#[inline(always)]
96pub fn scp_shire_base(shire: u32) -> usize {
97    0x8000_0000usize + ((shire as usize) << 23)
98}
99
100#[inline(always)]
101fn cb_index(hart: u32) -> usize {
102    if hart < 2048 {
103        hart as usize
104    } else {
105        (hart - 32) as usize
106    }
107}
108
109#[inline(always)]
110fn align8(n: usize) -> usize {
111    (n + 7) & !7
112}
113
114/// Write `text` as a NUL-terminated string trace entry for the current hart,
115/// exactly as the SDK's `Trace_String` does (reserve via the control block, then
116/// write a `trace_string_t`).
117pub fn trace_str(text: &[u8]) {
118    let hid = hart_id();
119    let str_len = align8(text.len() + 1).min(TRACE_STRING_MAX);
120    let cb = CB_BASE + cb_index(hid) * CB_STRIDE;
121    // SAFETY: firmware populated the CB at this fixed address before launch.
122    let base = unsafe { read_volatile((cb + CB_BASE_PER_HART) as *const u64) } as usize;
123    let offset = unsafe { read_volatile((cb + CB_OFFSET_PER_HART) as *const u32) };
124    let head = base + offset as usize;
125    // SAFETY: `head` lies within this hart's reserved trace-buffer slice.
126    unsafe {
127        write_volatile(head as *mut u64, timestamp());
128        write_volatile((head + 8) as *mut u32, str_len as u32);
129        write_volatile((head + 12) as *mut u16, hid as u16);
130        write_volatile((head + 14) as *mut u16, TRACE_TYPE_STRING);
131        let s = (head + ENTRY_HEADER_SIZE) as *mut u8;
132        let mut i = 0;
133        while i < str_len {
134            let byte = if i < text.len() { text[i] } else { 0 };
135            write_volatile(s.add(i), byte);
136            i += 1;
137        }
138        write_volatile(
139            (cb + CB_OFFSET_PER_HART) as *mut u32,
140            offset + (ENTRY_HEADER_SIZE + str_len) as u32,
141        );
142    }
143}
144
145/// A fixed-capacity stack buffer for composing trace messages without `alloc`.
146pub struct MsgBuf {
147    buf: [u8; 192],
148    len: usize,
149}
150
151impl Default for MsgBuf {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157impl MsgBuf {
158    pub fn new() -> Self {
159        MsgBuf {
160            buf: [0; 192],
161            len: 0,
162        }
163    }
164
165    /// Append raw text (truncated if the buffer fills).
166    pub fn str(&mut self, s: &[u8]) -> &mut Self {
167        let mut i = 0;
168        while i < s.len() && self.len < self.buf.len() {
169            self.buf[self.len] = s[i];
170            self.len += 1;
171            i += 1;
172        }
173        self
174    }
175
176    /// Append a decimal integer.
177    pub fn u64(&mut self, mut v: u64) -> &mut Self {
178        let mut tmp = [0u8; 20];
179        let mut c = 0;
180        loop {
181            tmp[c] = b'0' + (v % 10) as u8;
182            v /= 10;
183            c += 1;
184            if v == 0 {
185                break;
186            }
187        }
188        while c > 0 && self.len < self.buf.len() {
189            c -= 1;
190            self.buf[self.len] = tmp[c];
191            self.len += 1;
192        }
193        self
194    }
195
196    pub fn as_slice(&self) -> &[u8] {
197        &self.buf[..self.len]
198    }
199}
200
201/// Cache-line size in bytes. Per-hart outputs are placed one-per-line so that
202/// distinct harts never write the same line: false sharing silently corrupts
203/// data on this software-coherent architecture. Defined once in `et-abi` and
204/// shared with the host, so the two sides cannot disagree on the stride.
205pub use et_abi::CACHE_LINE;
206
207/// A hart's view of an SPMD launch: its identity within `n_harts` participants.
208///
209/// The safety story of the reduction demo lives here. A kernel body, given a
210/// `Grid`, can obtain only *its own* disjoint slice of the input and *its own*
211/// output cell -- it has no way to name another hart's data, so cross-hart data
212/// races are unrepresentable in the (safe) kernel body. The small `unsafe`
213/// boundary that turns device addresses into slices is confined to this module.
214pub struct Grid {
215    hart: u32,
216    n_harts: u32,
217}
218
219impl Grid {
220    /// Build from the current hart's id and the number of participating harts.
221    pub fn new(n_harts: u32) -> Self {
222        Grid {
223            hart: hart_id(),
224            n_harts,
225        }
226    }
227
228    pub fn hart(&self) -> u32 {
229        self.hart
230    }
231
232    pub fn n_harts(&self) -> u32 {
233        self.n_harts
234    }
235
236    /// Whether this hart participates (the launch runs every hart of the shire,
237    /// so surplus harts opt out).
238    pub fn active(&self) -> bool {
239        self.hart < self.n_harts
240    }
241
242    /// This hart's half-open element range of a length-`n` domain: contiguous,
243    /// disjoint across harts, and together covering all of `[0, n)` (a balanced
244    /// split, the first `n % n_harts` harts taking one extra element).
245    fn range(&self, n: usize) -> (usize, usize) {
246        let h = self.hart as usize;
247        let p = (self.n_harts as usize).max(1);
248        let base = n / p;
249        let rem = n % p;
250        let start = h * base + h.min(rem);
251        let len = base + if h < rem { 1 } else { 0 };
252        (start, start + len)
253    }
254
255    /// Borrow this hart's disjoint sub-slice of `data`.
256    pub fn my_slice<'a, T>(&self, data: &'a [T]) -> &'a [T] {
257        let (start, end) = self.range(data.len());
258        &data[start..end]
259    }
260
261    /// Borrow this hart's own output cell from an array of one cache-line-padded
262    /// `T` per hart based at device address `base`.
263    ///
264    /// # Safety
265    /// `base` must address at least `n_harts * CACHE_LINE` writable bytes of
266    /// device memory. Disjointness across harts is guaranteed by construction
267    /// (distinct `hart` ids map to distinct cache lines).
268    pub unsafe fn output_cell<'a, T>(&self, base: usize) -> &'a mut T {
269        unsafe { &mut *((base + self.hart as usize * CACHE_LINE) as *mut T) }
270    }
271}
272
273/// Tensor-extension intrinsics: TensorLoad, TensorLoadB, TensorFMA32,
274/// TensorStore, and TensorWait, all encoded as RISC-V CSR writes.
275pub mod tensor;
276
277/// Performance Monitoring Unit (PMU) counter API.
278///
279/// Provides [`pmu::pmu_read`] (reads `hpmcounterN` in U-mode) and the
280/// [`pmu::PmuEvent`] event-code enum for characterising tensor kernel behaviour.
281pub mod pmu;
282
283/// Packed-single (PS) SIMD intrinsics for 256-bit FP registers.
284///
285/// All functions are stubs pending confirmation of the PS opcode encodings
286/// from PRM Chapter 5. This module is hidden from published documentation
287/// until the implementations are verified on hardware.
288#[doc(hidden)]
289pub mod simd;
290
291/// View `n` elements of type `T` at device address `addr` as a shared slice.
292///
293/// # Safety
294/// `addr` must point to `n` valid, aligned, initialised `T` that outlive the
295/// returned borrow and are not mutated through another path meanwhile.
296pub unsafe fn device_slice<'a, T>(addr: usize, n: usize) -> &'a [T] {
297    unsafe { core::slice::from_raw_parts(addr as *const T, n) }
298}