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}