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