Skip to main content

u280_shell_rhdl/
control.rs

1//! Launch configuration, run state, AXI4-Lite helpers, and `HSR1` summary.
2
3use rhdl::prelude::*;
4
5use crate::abi::{
6    ABI_WORD_BITS, INPUT_ALIGNMENT_MASK, MAX_BATCH_BITS, OUTPUT_ALIGNMENT_MASK,
7    SHA256_PROFILE_WIRE_TAG_BITS, SUMMARY_ABI_REVISION_HI_BITS, SUMMARY_ABI_REVISION_LO_BITS,
8};
9
10/// Whole-run controller state.
11#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
12pub enum RunState {
13    /// Arguments are writable and no HBM transaction is owned.
14    #[default]
15    Idle,
16    /// Frozen arguments are checked before any memory request is issued.
17    Validate,
18    /// New reads, signer admissions, frame capture, and payload writes may run.
19    Execute,
20    /// Stop new work while completing already asserted or accepted AXI work.
21    AbortDrain,
22    /// First full cycle of signer-only synchronous reset.
23    CoreReset0,
24    /// Second full cycle of signer-only synchronous reset.
25    CoreReset1,
26    /// Terminal one-beat summary address.
27    SummaryAw,
28    /// Terminal one-beat summary data.
29    SummaryW,
30    /// Terminal summary response.
31    SummaryB,
32    /// Completion event; returns to `Idle` on the following cycle.
33    Done,
34}
35
36/// Host-programmable launch arguments.
37#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
38pub struct LaunchConfig {
39    /// HBM0 base containing packed 64-byte input jobs.
40    pub input_base: b64,
41    /// HBM1 base of the 4 KiB output-slot array.
42    pub output_base: b64,
43    /// HBM1 pointer for the independent 64-byte terminal summary.
44    pub summary_base: b64,
45    /// Job count; validated to fit 16 bits.
46    pub batch: b32,
47    /// Required exact ABI guard word.
48    pub abi_word: b32,
49    /// Opaque run nonce copied into the summary.
50    pub nonce: b64,
51}
52
53/// Result of fail-closed launch validation.
54#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
55pub struct ConfigValidation {
56    /// All fixed ABI, alignment, nonzero, overflow, and overlap rules passed.
57    pub valid: bool,
58    /// Static diagnostic bit inventory for host debugging.
59    pub detail: b32,
60}
61
62/// Validate all arguments before issuing either HBM master.
63#[kernel]
64#[allow(clippy::assign_op_pattern)] // Keep each reviewed failure branch visibly mapped to its fixed diagnostic bit.
65pub fn validate_config_kernel(config: LaunchConfig) -> ConfigValidation {
66    let batch64: b64 = config.batch.resize();
67    let input_bytes = batch64 << 6;
68    let output_bytes = batch64 << 12;
69    let max64 = b64(0xffff_ffff_ffff_ffff);
70    let input_overflow = config.input_base > max64 - input_bytes;
71    let output_overflow = config.output_base > max64 - output_bytes;
72    let summary_overflow = config.summary_base > max64 - b64(64);
73    let output_end = config.output_base + output_bytes;
74    let summary_end = config.summary_base + b64(64);
75    let overlaps_output = config.batch != b32(0)
76        && config.summary_base < output_end
77        && config.output_base < summary_end;
78
79    let mut detail = b32(0);
80    if config.abi_word != ABI_WORD_BITS {
81        detail = detail | b32(1 << 0);
82    }
83    if config.batch > MAX_BATCH_BITS {
84        detail = detail | b32(1 << 1);
85    }
86    if config.input_base == b64(0) {
87        detail = detail | b32(1 << 2);
88    }
89    if config.output_base == b64(0) {
90        detail = detail | b32(1 << 3);
91    }
92    if config.summary_base == b64(0) {
93        detail = detail | b32(1 << 4);
94    }
95    if config.input_base & INPUT_ALIGNMENT_MASK != b64(0) {
96        detail = detail | b32(1 << 5);
97    }
98    if config.output_base & OUTPUT_ALIGNMENT_MASK != b64(0) {
99        detail = detail | b32(1 << 6);
100    }
101    if config.summary_base & b64(63) != b64(0) {
102        detail = detail | b32(1 << 7);
103    }
104    if input_overflow {
105        detail = detail | b32(1 << 8);
106    }
107    if output_overflow {
108        detail = detail | b32(1 << 9);
109    }
110    if summary_overflow {
111        detail = detail | b32(1 << 10);
112    }
113    if overlaps_output {
114        detail = detail | b32(1 << 11);
115    }
116    ConfigValidation {
117        valid: detail == b32(0),
118        detail,
119    }
120}
121
122/// Apply AXI4-Lite byte strobes to one register value.
123#[kernel]
124#[allow(clippy::cast_sign_loss, clippy::needless_range_loop)] // The fixed 0..4 lane range proves both casts nonnegative; preserve the reviewed kernel shape.
125pub fn apply_write_strobes_kernel(previous: b32, value: b32, strobes: b4) -> b32 {
126    let mut next = previous;
127    for lane in 0..4 {
128        let lane_selected = ((strobes >> b2(lane as u128)) & b4(1)) != b4(0);
129        if lane_selected {
130            let mask = b32(0xff) << b5((lane * 8) as u128);
131            next = (next & !mask) | (value & mask);
132        }
133    }
134    next
135}
136
137/// Low 32 bits of a 64-bit register.
138#[kernel]
139pub fn low_word_kernel(value: b64) -> b32 {
140    value.resize()
141}
142
143/// High 32 bits of a 64-bit register.
144#[kernel]
145pub fn high_word_kernel(value: b64) -> b32 {
146    (value >> 32).resize()
147}
148
149/// Replace a byte-strobed low half of a 64-bit register.
150#[kernel]
151pub fn replace_low_word_kernel(previous: b64, value: b32, strobes: b4) -> b64 {
152    let prior_low: b32 = previous.resize();
153    let next_low = apply_write_strobes_kernel(prior_low, value, strobes);
154    (previous & b64(0xffff_ffff_0000_0000)) | next_low.resize()
155}
156
157/// Replace a byte-strobed high half of a 64-bit register.
158#[kernel]
159pub fn replace_high_word_kernel(previous: b64, value: b32, strobes: b4) -> b64 {
160    let prior_high: b32 = (previous >> 32).resize();
161    let next_high = apply_write_strobes_kernel(prior_high, value, strobes);
162    (previous & b64(0x0000_0000_ffff_ffff)) | (next_high.resize() << 32)
163}
164
165/// Summary-write evidence packed into the host-visible terminal status.
166#[allow(clippy::struct_excessive_bools)] // Preserve the four independent packed evidence bits and their ABI order.
167#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
168pub struct SummaryWriteStatus {
169    /// Terminal summary address was accepted.
170    pub aw: bool,
171    /// Terminal summary data was accepted.
172    pub w: bool,
173    /// Terminal summary response was drained.
174    pub b: bool,
175    /// Terminal summary response was exact ID-zero `OKAY`.
176    pub b_canonical: bool,
177}
178
179/// Pack the terminal status register independently from the in-memory summary.
180#[kernel]
181pub fn last_status_kernel(terminal_code: b8, error_flags: b16, summary: SummaryWriteStatus) -> b32 {
182    let aw = if summary.aw { b32(1) } else { b32(0) };
183    let w = if summary.w { b32(1) } else { b32(0) };
184    let b = if summary.b { b32(1) } else { b32(0) };
185    let canonical = if summary.b_canonical { b32(1) } else { b32(0) };
186    terminal_code.resize()
187        | (error_flags.resize() << 8)
188        | (aw << 24)
189        | (w << 25)
190        | (b << 26)
191        | (canonical << 27)
192}
193
194/// Host-visible success requires summary occurrence and canonical retirement.
195#[kernel]
196pub fn last_status_is_host_success_kernel(status: b32) -> bool {
197    status == b32(0x0f00_0000)
198}
199
200/// Values serialized into the one-line terminal summary.
201#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
202pub struct SummaryFields {
203    /// Terminal code, zero only on exact successful closure.
204    pub terminal_code: b8,
205    /// Opaque host nonce.
206    pub nonce: b64,
207    /// Execute/abort payload interval in cycles.
208    pub payload_cycles: b64,
209    /// First admission through final captured frame, inclusive.
210    pub core_latency_cycles: b64,
211    /// First-to-last captured frame span, zero for fewer than two frames.
212    pub capture_span_cycles: b64,
213    /// Frozen launch batch.
214    pub batch: b16,
215    /// Accepted input addresses.
216    pub input_ar: b16,
217    /// Checked input responses.
218    pub input_r: b16,
219    /// Signer admissions.
220    pub core_admissions: b16,
221    /// Complete canonical frames captured in BRAM.
222    pub frames_captured: b16,
223    /// Explicit signer error completions.
224    pub core_errors: b16,
225    /// Accepted payload write addresses.
226    pub payload_aw: b16,
227    /// Drained payload write responses.
228    pub payload_b: b16,
229    /// Accepted payload write-data beats.
230    pub payload_w: b32,
231    /// Payloads retired on exact ID-zero `OKAY` responses.
232    pub memory_completed: b16,
233    /// Sticky error-cause union, excluding a later summary B failure.
234    pub error_flags: b16,
235}
236
237/// Serialize the fixed 64-byte little-endian `HSR1` record.
238#[kernel]
239#[allow(clippy::needless_range_loop)] // Pinned RHDL does not lower iterators.
240pub fn summary_line_kernel(fields: SummaryFields) -> [b8; 64] {
241    let mut data = [b8(0); 64];
242    data[0] = b8(0x48);
243    data[1] = b8(0x53);
244    data[2] = b8(0x52);
245    data[3] = b8(0x31);
246    data[4] = SUMMARY_ABI_REVISION_LO_BITS;
247    data[5] = SUMMARY_ABI_REVISION_HI_BITS;
248    data[6] = SHA256_PROFILE_WIRE_TAG_BITS;
249    data[7] = fields.terminal_code;
250
251    for byte in 0..8 {
252        let shift = b6((byte * 8) as u128);
253        data[8 + byte] = (fields.nonce >> shift).resize();
254        data[16 + byte] = (fields.payload_cycles >> shift).resize();
255        data[24 + byte] = (fields.core_latency_cycles >> shift).resize();
256        data[32 + byte] = (fields.capture_span_cycles >> shift).resize();
257    }
258
259    let word5: b64 = fields.batch.resize()
260        | (fields.input_ar.resize() << 16)
261        | (fields.input_r.resize() << 32)
262        | (fields.core_admissions.resize() << 48);
263    let word6: b64 = fields.frames_captured.resize()
264        | (fields.core_errors.resize() << 16)
265        | (fields.payload_aw.resize() << 32)
266        | (fields.payload_b.resize() << 48);
267    let word7: b64 = fields.payload_w.resize()
268        | (fields.memory_completed.resize() << 32)
269        | (fields.error_flags.resize() << 48);
270    for byte in 0..8 {
271        let shift = b6((byte * 8) as u128);
272        data[40 + byte] = (word5 >> shift).resize();
273        data[48 + byte] = (word6 >> shift).resize();
274        data[56 + byte] = (word7 >> shift).resize();
275    }
276    data
277}