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}