rhdl_dsig_scalable_core/engine.rs
1//! The sustained-signing RTL kernel: eight slices, one certificate unit, HBM.
2//!
3//! (Eight in the default build; [`SLICES`] is 2, 4 or 8, chosen by the
4//! `DSIG_SLICES` build variable. The diagrams below draw the 8-slice case.)
5//!
6//! # The problem
7//!
8//! Everything else in this crate is a block: a key factory, a signer, a
9//! certificate unit. A card needs *one* top-level RTL kernel that Vitis can
10//! link: a box with a host control port and some HBM ports. This module is
11//! that box. It answers three questions:
12//!
13//! 1. **How does the host talk to it?** Through a register file on
14//! AXI4-Lite that follows the Vitis `ap_ctrl_hs` convention ("handshake"
15//! control: the host sets `ap_start`, the kernel reports `ap_idle`,
16//! `ap_done`, `ap_ready`). That lets ordinary XRT host code start the
17//! kernel and pass arguments without a custom driver.
18//! 2. **How do messages get in and frames get out?** One HBM read master
19//! ([`crate::msg_reader`]) for messages and one HBM write master per slice
20//! ([`crate::hbm_writer`]) for frames.
21//! 3. **How do we measure it honestly?** With a free-running cycle counter
22//! that is latched at start and again when the last frame is acknowledged
23//! by memory.
24//!
25//! # The pieces and how data flows
26//!
27//! ```text
28//! AXI-Lite (host) HBM
29//! │ ▲ ▲ … ▲
30//! ┌─────────────▼───────────────────────────────────┼──┼───┼──┐
31//! │ registers ── config/start ──┐ writer0 … writer7 │
32//! │ ▲ counters, ticks ▼ ▲ ▲ │
33//! │ │ MessageReader ──msg n──▶ slice (n mod 8) │
34//! │ │ ▲ HBM │ roots / certs │
35//! │ └────────────── CertUnit ◀───────────────┘ │
36//! └──────────────────────────────────────────────────────────────┘
37//! ```
38//!
39//! Follow one message through the kernel:
40//!
41//! 1. The host writes the arguments (addresses, counts, namespace, secrets)
42//! into the registers, then sets `ap_start`.
43//! 2. [`MessageReader`] fetches messages from HBM and deals message `n` to
44//! slice `n mod SLICES`.
45//! 3. Each [`Slice`] has been making one-time keys since start. It builds its
46//! subtree root for each batch and hands the root to the [`CertUnit`],
47//! which joins all slice roots into the batch root and signs it with
48//! Ed25519. The certificate comes back to every slice.
49//! 4. The slice signs the message with a fresh key and streams the
50//! 26-beat frame to its own [`HbmWriter`], which writes it to HBM.
51//!
52//! Every signal between a slice and a shared block passes through registers
53//! ([`crate::relay`]): handshakes through relay pipes, the rest through
54//! two-register delay lines. The slices sit in their own SLRs, up to two die
55//! crossings from the shared blocks next to the HBM, and build v9 showed
56//! that an unregistered link across that distance cannot run at 205 MHz.
57//! 5. When the writers' acknowledged-frame counts add up to `count`, the
58//! kernel stops, latches `DONE_TICK` and raises `ap_done`.
59//!
60//! # The run lifecycle
61//!
62//! ```text
63//! reset ──▶ IDLE ──host writes args──▶ IDLE ──AP_CTRL bit0=1──▶ RUNNING
64//! │ (args writable, │
65//! │ no read path for secrets) frames_total == count
66//! │ ▼
67//! └────────── only a reset leaves ◀──────────────── DONE
68//! (start_tick, done_tick
69//! latched; ap_done set)
70//! ```
71//!
72//! `running` and `done` in [`EngineControl`] encode the states: IDLE is
73//! neither, RUNNING is `running`, DONE is `done`. A start request is ignored
74//! unless both are clear, and argument writes are ignored unless both are
75//! clear, so nothing about a run can change after it starts.
76//!
77//! # How a run is measured
78//!
79//! The host programs the configuration registers, then writes `CTRL.start`.
80//! The kernel latches the free-running tick counter at start, makes every key
81//! from scratch, and latches the counter again when the last frame's HBM write
82//! response arrives. `(done_tick − start_tick)` cycles at the clock frequency
83//! the host measures through the same counter
84//! (reading the low word snapshots the high word) is
85//! the denominator of the reported rate. The numerator is the number of frames
86//! the host reads back and verifies.
87//!
88//! One run per kernel reset: the kernel never restarts on its own, and the host
89//! must allocate a fresh namespace (epoch, batch range, master seed) for every
90//! run, because keys made but not spent are still burned.
91//!
92//! # Register map (32-bit words)
93//!
94//! The control block follows the Vitis `ap_ctrl_hs` convention, so the host
95//! drives the kernel with ordinary `xrt::kernel` / `xrt::run` calls and
96//! arguments; every argument below is declared in `kernel.xml` at its offset.
97//!
98//! | Offset | Name | Access |
99//! |---|---|---|
100//! | `0x000` | `AP_CTRL`: bit 0 `ap_start`, 1 `ap_done` (clear on read), 2 `ap_idle`, 3 `ap_ready` | RW |
101//! | `0x004` | `GIE` global interrupt enable | RW |
102//! | `0x008` | `IER` bit 0 = done interrupt | RW |
103//! | `0x00c` | `ISR` bit 0 = done (write 1 to toggle) | RW |
104//! | `0x010` | arg `messages`: message buffer address (u64) | W |
105//! | `0x018 + 8s` | arg `frames{s}`: slice `s` frame buffer address (u64) | W |
106//! | `0x058` | arg `count`: messages to sign (u32) | W |
107//! | `0x05c` | arg `batches`: batches to make (u32) | W |
108//! | `0x060` | arg `epoch` (u64) | W |
109//! | `0x068` | arg `first_batch` (u64) | W |
110//! | `0x070 + 4w` | args `cv0`..`cv7`: derive-key prefix CV (secret) | W |
111//! | `0x090 + 4w` | args `seed0`..`seed7`: Ed25519 seed (secret) | W |
112//! | `0x200` | `MAGIC` = `0x33475344` ("DSG3") | R |
113//! | `0x204` | `ABI` = [`ABI_VERSION`] | R |
114//! | `0x208` | `SHAPE`: slices in bits 0..8, table mask in bits 8..24 | R |
115//! | `0x20c` | `FAULT` bits: 0 writer, 1 reader, 2 cert, 3 slice | R |
116//! | `0x210`/`0x214` | `TICKS` lo (snapshots hi) / hi | R |
117//! | `0x218`/`0x21c` | `START_TICK` lo/hi | R |
118//! | `0x220`/`0x224` | `DONE_TICK` lo/hi | R |
119//! | `0x228` | `CERTIFIED` batches | R |
120//! | `0x22c` | `DEALT` messages | R |
121//! | `0x230`/`0x234` | `CERT_BUSY` cycles lo/hi | R |
122//! | `0x240 + 4s` | `FRAMES[s]` acknowledged per slice | R |
123//! | `0x260 + 4s` | `KEYS[s]` started per slice | R |
124//! | `0x280 + 4w` | `PUBLIC_KEY` words (8 × u32) | R |
125//!
126//! Arguments are accepted only while the kernel is idle and have no read
127//! path, so secrets cannot be read back. A completed kernel stays done until
128//! the next reset: one run per reset, each with a fresh namespace.
129//!
130//! # Why the rules are so strict
131//!
132//! A W-OTS+ one-time key that signs two different messages leaks enough of
133//! its chains to forge. So the kernel is built so that a key can never be
134//! reused, even by accident:
135//!
136//! * A one-time key is never reused after admission, timeout, reset or
137//! uncertain launch. The kernel has no "resume": after a reset it starts
138//! from nothing, and the host must give it a fresh namespace (epoch, batch
139//! range, master seed). [`crate::lease`] is the host-side guard that never
140//! hands out the same run directory, namespace or batch range twice.
141//! * Secrets are write-only. `cv0..cv7` (the derive-key prefix chaining
142//! value) and `seed0..seed7` (the Ed25519 seed) have no read path in the
143//! register file. The master seed itself is never stored on the card; the
144//! host passes only the derived prefix CV (see
145//! [`crate::slice_model::prefix_cv`]).
146//! * Arguments freeze at start. Writes to argument offsets are dropped while
147//! running or done.
148//!
149//! # Neighbours
150//!
151//! | Child | Module | Role here |
152//! |---|---|---|
153//! | `reader` | [`crate::msg_reader`] | HBM messages → per-slice buffers |
154//! | `slices` | [`crate::slice`] | key factory, cache, subtree, signer |
155//! | `cert` | [`crate::cert_unit`] | Merkle top + Ed25519 certificate per batch |
156//! | `writers` | [`crate::hbm_writer`] | frames → HBM, one port per slice |
157//! | `beats` | [`crate::relay`] | relay pipes: frame beats flow at up to one per clock |
158//! | `messages`, `certificates` | [`crate::relay`] | toggle links: at most one message per hash round trip and one certificate per 128 signatures need no relay stations |
159//! | `to_slice`, `from_slice` | [`crate::relay`] | delay lines for configuration, roots, credit and status |
160//! | (types) | [`crate::axi4`] | the AXI4 / AXI4-Lite signal structs |
161//!
162//! The Verilog for this kernel is produced by the `dsig_engine_export`
163//! binary, and the host program that drives it is `dsig_engine_host`.
164
165use crate::axi4::{ReadRequest, ReadResponse, WriteRequest, WriteResponse};
166use crate::cert_unit::{CertConfig, CertInput, CertUnit, SLICES};
167use crate::ed25519::ScalarWords;
168use crate::hbm_writer::{HbmWriter, WriterInput};
169use crate::msg_reader::{MessageReader, ReaderInput};
170use crate::relay::{Delay, PipeIn, RelayPipe, ToggleLink};
171use crate::slice_merkle::SubtreeRoot;
172use crate::slice_signer::{Certificate, FrameBeat, Message};
173use crate::slice::{Slice, SliceInput};
174use crate::slice_factory::FactoryConfig;
175use crate::axi4::{AxiLiteIn, AxiLiteOut};
176use rhdl::core::ReplicatedSynchronous;
177use rhdl::prelude::*;
178use rhdl_fpga::core::dff::DFF;
179
180/// `MAGIC` register value ("DSG3").
181///
182/// Read at `0x200`. The bytes `0x44 0x53 0x47 0x33` are ASCII "DSG3" in
183/// little-endian order. The card runner
184/// (`fpga/u280/engine/host/dsig_engine_runner.cpp`) checks it before anything
185/// else, so it never drives a different kernel with this register map.
186pub const MAGIC: b32 = b32(0x3347_5344);
187/// Register-map version.
188///
189/// Read at `0x204`. It changes whenever the register map changes, so host
190/// code can tell which layout a kernel image has; a moved register could, for
191/// example, turn a count into an address.
192pub const ABI_VERSION: b32 = b32(0x0003_0001);
193
194/// Kernel inputs: every signal that enters the kernel from outside.
195#[derive(Clone, Copy, Debug, PartialEq, Digital)]
196pub struct EngineInput {
197 /// Host control channel (AXI4-Lite into the register file).
198 pub axi: AxiLiteIn,
199 /// Message read channel (the HBM port of the message reader).
200 pub messages: ReadResponse,
201 /// Frame write channels, one per slice (each slice's own HBM port).
202 pub frames: [WriteResponse; SLICES],
203}
204
205/// Kernel outputs: every signal that leaves the kernel.
206#[derive(Clone, Copy, Debug, PartialEq, Digital)]
207pub struct EngineOutput {
208 /// Host control channel (register reads, write acknowledgements and the
209 /// interrupt line).
210 pub axi: AxiLiteOut,
211 /// Message read channel.
212 pub messages: ReadRequest,
213 /// Frame write channels, one per slice.
214 pub frames: [WriteRequest; SLICES],
215}
216
217/// Host-written configuration: the kernel's arguments.
218///
219/// Written over AXI4-Lite before start and frozen while running. Together,
220/// `prefix_cv`, `epoch` and the batch range `first_batch .. first_batch +
221/// batches` decide every key the run makes, which is why the host must never
222/// repeat them (see the module notes).
223#[derive(Clone, Copy, Debug, PartialEq, Digital)]
224pub struct Registers {
225 /// Derive-key prefix CV (8 × 32 bits, secret, write-only): the BLAKE3
226 /// chaining value from which every one-time key of this signer is
227 /// derived. Args `cv0`..`cv7` at `0x070 + 4w`.
228 pub prefix_cv: [b32; 8],
229 /// Epoch (64 bits): part of the run's namespace; mixed into every key
230 /// and into every certificate envelope.
231 pub epoch: b64,
232 /// First batch id (64 bits): the number of the run's first 128-key
233 /// batch. Batch ids are part of the namespace.
234 pub first_batch: b64,
235 /// Batches to make (32 bits). Each batch is 128 keys.
236 pub batches: b32,
237 /// Messages to sign (32 bits). The run is done when this many frames
238 /// have been acknowledged by HBM.
239 pub count: b32,
240 /// Message base address (64-bit HBM byte address, 4 KiB aligned).
241 pub msg_base: b64,
242 /// Frame base address per slice (64-bit HBM byte address, 2 KiB aligned).
243 pub out_base: [b64; SLICES],
244 /// Ed25519 seed words (32-bit halves; 8 × 32 = 256 bits, secret,
245 /// write-only): the certificate signing key. Args `seed0`..`seed7` at
246 /// `0x090 + 4w`.
247 pub seed: [b32; 8],
248}
249
250/// Resettable control state.
251///
252/// This one register holds the configuration, the run state machine, the
253/// timing counters and the AXI4-Lite handshake latches. All of it resets to
254/// zero (see `Default for Engine`), which is the IDLE state with no
255/// arguments.
256#[derive(Clone, Copy, Debug, PartialEq, Digital)]
257pub struct EngineControl {
258 /// Configuration (the host-written arguments).
259 pub regs: Registers,
260 /// Running: set by `ap_start`, cleared when the last frame is acknowledged.
261 pub running: bool,
262 /// Finished (sticky until reset). Blocks a second start and any argument
263 /// write, enforcing one run per reset.
264 pub done: bool,
265 /// `ap_done` not yet read by the host. `ap_ctrl_hs` makes `ap_done` clear
266 /// on read, so this bit is cleared when `AP_CTRL` is read.
267 pub done_unread: bool,
268 /// Global interrupt enable (`GIE`, bit 0 of `0x004`).
269 pub gie: bool,
270 /// Interrupt enable (done) (`IER`, bit 0 of `0x008`).
271 pub ier: bool,
272 /// Interrupt status (done) (`ISR`, bit 0 of `0x00c`). Set when the run
273 /// finishes; the host toggles it by writing 1.
274 pub isr: bool,
275 /// Free-running cycle counter (64 bits). Counts every clock from reset;
276 /// the host uses it both to time the run and, by sampling it twice
277 /// against its own wall clock, to measure the clock frequency.
278 pub ticks: b64,
279 /// Tick at start (64 bits): the value of `ticks` on the cycle the start
280 /// write was handled.
281 pub start_tick: b64,
282 /// Tick when the last frame was acknowledged (64 bits).
283 pub done_tick: b64,
284 /// High half captured when `TICKS` lo is read. A 64-bit counter is read
285 /// as two 32-bit words; capturing the high half at the moment the low
286 /// half is read makes the pair consistent even if the low half wraps in
287 /// between.
288 pub ticks_hi: b32,
289 /// Latched write address (12 bits). AXI4-Lite may deliver the address
290 /// (AW) and the data (W) on different cycles, so each is held until both
291 /// have arrived.
292 pub aw_addr: b12,
293 /// A write address is latched.
294 pub aw_have: bool,
295 /// Latched write data (32 bits).
296 pub w_data: b32,
297 /// Write data is latched.
298 pub w_have: bool,
299 /// Write response pending (B `valid`); held until the host's `b_ready`.
300 pub b_valid: bool,
301 /// Read data pending (R `valid`); held until the host's `r_ready`.
302 pub r_valid: bool,
303 /// Read data (32 bits), held stable while `r_valid` waits.
304 pub r_data: b32,
305}
306
307/// The kernel.
308///
309/// `TABLE_MASK` selects which AES rounds of each Haraka chain-step lane use
310/// T-table ROMs instead of LUT logic (bit `r` = round `r`; see
311/// [`crate::chain_step::TableChainStepLane`]). It is passed down to every
312/// slice and reported in the `SHAPE` register.
313#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
314pub struct Engine<const TABLE_MASK: usize> {
315 /// Register file, run state, tick counters ([`EngineControl`]).
316 control: DFF<EngineControl>,
317 /// The single HBM message reader and dealer.
318 reader: MessageReader,
319 /// The shared Merkle-top and Ed25519 certificate unit.
320 cert: CertUnit,
321 /// `SLICES` identical slices: RHDL lowers one and instantiates it.
322 slices: ReplicatedSynchronous<Slice<TABLE_MASK>, SLICES>,
323 /// One HBM frame writer per slice.
324 writers: [HbmWriter; SLICES],
325 // The links below come last, so the older children keep their netlist
326 // names (`c0`..`c4`); the floorplan finds these as `c5`..`c9`.
327 /// Frame beats, signer → writer (`c5/c<s>`).
328 beats: [RelayPipe<FrameBeat>; SLICES],
329 /// Messages, reader → slice (`c6/c<s>`): the signer takes at most one
330 /// per message-hash round trip, so a [`ToggleLink`] keeps up.
331 messages: [ToggleLink<Message>; SLICES],
332 /// Certificates, cert unit → slice (`c7/c<s>`): rare and 1,345 bits
333 /// wide, so a [`ToggleLink`] (no per-bit logic) rather than relays.
334 certificates: [ToggleLink<Certificate>; SLICES],
335 /// Configuration, `run` and root credit, engine → slice (`c8/c<s>`).
336 to_slice: [Delay<SliceControl>; SLICES],
337 /// Root pulses, status and fault, slice → engine (`c9/c<s>`).
338 from_slice: [Delay<SliceReport>; SLICES],
339}
340
341/// What the engine sends a slice that tolerates latency: run configuration
342/// (static during a run), `run`, and the root credit (a late credit only
343/// makes the slice wait longer).
344#[derive(Clone, Copy, Debug, PartialEq, Digital)]
345pub struct SliceControl {
346 /// Key-factory configuration, including the slice's number.
347 pub config: FactoryConfig,
348 /// Allow key starts.
349 pub run: bool,
350 /// Batches whose roots the cert unit has taken.
351 pub roots_popped: b32,
352}
353
354/// What a slice reports that tolerates latency: the one-clock subtree-root
355/// pulse (the cert unit queues roots, and credit covers the delay), the
356/// status counters the host reads, and the fault bit.
357#[derive(Clone, Copy, Debug, PartialEq, Digital)]
358pub struct SliceReport {
359 /// Subtree root pulse for the cert unit.
360 pub root: SubtreeRoot,
361 /// Real keys started (`KEYS[s]`).
362 pub keys_started: b32,
363 /// Internal fault.
364 pub fault: bool,
365}
366
367/// The power-on kernel: IDLE, every argument zero, every child reset.
368impl<const TABLE_MASK: usize> Default for Engine<TABLE_MASK> {
369 fn default() -> Self {
370 Self {
371 // Reset value of the control register: IDLE, no arguments, all
372 // counters and handshake latches clear.
373 control: DFF::new(EngineControl {
374 regs: Registers {
375 prefix_cv: [b32(0); 8],
376 epoch: b64(0),
377 first_batch: b64(0),
378 batches: b32(0),
379 count: b32(0),
380 msg_base: b64(0),
381 out_base: [b64(0); SLICES],
382 seed: [b32(0); 8],
383 },
384 running: false,
385 done: false,
386 done_unread: false,
387 gie: false,
388 ier: false,
389 isr: false,
390 ticks: b64(0),
391 start_tick: b64(0),
392 done_tick: b64(0),
393 ticks_hi: b32(0),
394 aw_addr: b12(0),
395 aw_have: false,
396 w_data: b32(0),
397 w_have: false,
398 b_valid: false,
399 r_valid: false,
400 r_data: b32(0),
401 }),
402 reader: MessageReader::default(),
403 cert: CertUnit::default(),
404 // One slice template, replicated SLICES times by RHDL.
405 slices: ReplicatedSynchronous::new(Slice::default()),
406 writers: core::array::from_fn(|_| HbmWriter::default()),
407 beats: core::array::from_fn(|_| RelayPipe::default()),
408 messages: core::array::from_fn(|_| ToggleLink::default()),
409 certificates: core::array::from_fn(|_| ToggleLink::default()),
410 to_slice: core::array::from_fn(|_| Delay::default()),
411 from_slice: core::array::from_fn(|_| Delay::default()),
412 }
413 }
414}
415
416/// Binds the kernel's input and output types to [`engine_kernel`].
417impl<const TABLE_MASK: usize> SynchronousIO for Engine<TABLE_MASK> {
418 type I = EngineInput;
419 type O = EngineOutput;
420 type Kernel = engine_kernel<TABLE_MASK>;
421}
422
423/// Replace the low or high half of a 64-bit register.
424///
425/// AXI4-Lite moves 32 bits at a time, so a 64-bit argument (an address, the
426/// epoch, the first batch id) arrives as two writes: the low word at offset
427/// `X` and the high word at `X + 4`.
428///
429/// * Inputs: `current`, the register's present 64-bit value; `value`, the
430/// 32-bit word just written; `high`, true for the `X + 4` half.
431/// * Output: `current` with that half replaced and the other half kept.
432/// * Timing: pure combinational logic (a mask and an OR), no state.
433#[kernel]
434pub fn set_half_kernel(current: b64, value: b32, high: bool) -> b64 {
435 // Keep the low half, place `value` in bits 32..64; or keep the high half
436 // and place `value` in bits 0..32.
437 if high {
438 (current & b64(0xffff_ffff)) | (value.resize::<64>() << 32)
439 } else {
440 (current & b64(0xffff_ffff_0000_0000)) | value.resize::<64>()
441 }
442}
443
444/// The kernel's wiring and register file.
445///
446/// Every clock this function:
447///
448/// 1. advances the tick counter;
449/// 2. sums progress from the writers and ORs fault flags from every child,
450/// and finishes the run when all `count` frames are acknowledged;
451/// 3. serves at most one AXI4-Lite register write and one register read;
452/// 4. wires the children together: reader → slices, slices ↔ certificate
453/// unit, slices → writers (every slice link through [`crate::relay`]
454/// registers), and the HBM ports to the outside.
455///
456/// * Inputs: `clock_reset`, the [`EngineInput`] pins, and `q`, the registered
457/// outputs of every child (`q.control`, `q.reader`, `q.cert`, `q.slices[s]`,
458/// `q.writers[s]`).
459/// * Outputs: the [`EngineOutput`] pins and `EngineD`, the next control value
460/// and every child's inputs for this clock.
461/// * Timing: in RHDL, `d` carries each child's inputs and `q` its outputs.
462/// The AXI4-Lite outputs come only from the registered control state, and
463/// the HBM outputs are the children's outputs passed through. A register
464/// write takes effect on the clock after both its address and data are
465/// latched; a register read answers on the clock after the address.
466#[allow(clippy::too_many_lines, clippy::needless_range_loop)]
467#[kernel]
468pub fn engine_kernel<const TABLE_MASK: usize>(
469 clock_reset: ClockReset,
470 input: EngineInput,
471 q: EngineQ<TABLE_MASK>,
472) -> (EngineOutput, EngineD<TABLE_MASK>) {
473 // Not used by this level's logic (see `let _ = resetting` at the end);
474 // every child register receives the same reset directly.
475 let resetting = clock_reset.reset.any();
476 // `c` becomes the next control value; `regs` is the current configuration.
477 let mut c = q.control;
478 let regs = q.control.regs;
479 // The free-running tick counter: +1 every clock, never stopped.
480 c.ticks = q.control.ticks + b64(1);
481
482 // ------------------------------------------------ progress and faults
483 // Frames acknowledged by HBM across all slices; the run's finish line.
484 let mut frames_total = b32(0);
485 // Any writer got a non-OKAY write response.
486 let mut writer_error = false;
487 // Any slice reported an internal fault.
488 let mut slice_fault = false;
489 for s in 0..SLICES {
490 frames_total = frames_total + q.writers[s].frames_written;
491 writer_error = writer_error || q.writers[s].error;
492 slice_fault = slice_fault || q.from_slice[s].fault;
493 }
494 // The `FAULT` register: bit 0 writer, 1 reader, 2 cert, 3 slice.
495 let fault_bits: b32 = (if writer_error { b32(1) } else { b32(0) })
496 | (if q.reader.error { b32(2) } else { b32(0) })
497 | (if q.cert.fault { b32(4) } else { b32(0) })
498 | (if slice_fault { b32(8) } else { b32(0) });
499 // RUNNING → DONE: every requested frame is in HBM. Latch the end time and
500 // raise `ap_done` and the done interrupt status.
501 if q.control.running && frames_total == regs.count {
502 c.running = false;
503 c.done = true;
504 c.done_unread = true;
505 c.isr = true;
506 c.done_tick = q.control.ticks;
507 }
508
509 // ------------------------------------------------ AXI-Lite write
510 // Latch AW and W independently (they may arrive on different clocks);
511 // `aw_ready`/`w_ready` are low while a value is held.
512 let axi = input.axi;
513 if axi.aw_valid && !q.control.aw_have {
514 c.aw_addr = axi.aw_addr;
515 c.aw_have = true;
516 }
517 if axi.w_valid && !q.control.w_have {
518 c.w_data = axi.w_data;
519 c.w_have = true;
520 }
521 // The host took the write response.
522 if q.control.b_valid && axi.b_ready {
523 c.b_valid = false;
524 }
525 // Both halves are here and the previous response is gone: perform the
526 // write, free the latches and raise the response.
527 if q.control.aw_have && q.control.w_have && !q.control.b_valid {
528 // Register offset and the 32-bit value written to it.
529 let a = q.control.aw_addr;
530 let v = q.control.w_data;
531 c.aw_have = false;
532 c.w_have = false;
533 c.b_valid = true;
534 // AP_CTRL bit 0 (`ap_start`): IDLE → RUNNING, only once per reset.
535 if a == b12(0x000) && (v & b32(1)) == b32(1) && !q.control.running && !q.control.done {
536 c.running = true;
537 c.start_tick = q.control.ticks;
538 }
539 if a == b12(0x004) {
540 c.gie = (v & b32(1)) == b32(1);
541 }
542 if a == b12(0x008) {
543 c.ier = (v & b32(1)) == b32(1);
544 }
545 // ISR is "write 1 to toggle", the `ap_ctrl_hs` convention.
546 if a == b12(0x00c) && (v & b32(1)) == b32(1) {
547 c.isr = !q.control.isr;
548 }
549 // Arguments: accepted only in IDLE, so a run's namespace, secrets and
550 // buffers cannot change once it has started.
551 if !q.control.running && !q.control.done {
552 if a == b12(0x010) || a == b12(0x014) {
553 c.regs.msg_base = set_half_kernel(regs.msg_base, v, a == b12(0x014));
554 }
555 for s in 0..SLICES {
556 if a == b12((0x018 + 8 * s) as u128) || a == b12((0x01c + 8 * s) as u128) {
557 c.regs.out_base[s] =
558 set_half_kernel(regs.out_base[s], v, a == b12((0x01c + 8 * s) as u128));
559 }
560 }
561 if a == b12(0x058) {
562 c.regs.count = v;
563 }
564 if a == b12(0x05c) {
565 c.regs.batches = v;
566 }
567 if a == b12(0x060) || a == b12(0x064) {
568 c.regs.epoch = set_half_kernel(regs.epoch, v, a == b12(0x064));
569 }
570 if a == b12(0x068) || a == b12(0x06c) {
571 c.regs.first_batch = set_half_kernel(regs.first_batch, v, a == b12(0x06c));
572 }
573 // The two secrets, one 32-bit word per offset. There is no read
574 // case for these offsets below: they are write-only.
575 for w in 0..8 {
576 if a == b12((0x070 + 4 * w) as u128) {
577 c.regs.prefix_cv[w] = v;
578 }
579 if a == b12((0x090 + 4 * w) as u128) {
580 c.regs.seed[w] = v;
581 }
582 }
583 }
584 }
585
586 // ------------------------------------------------ AXI-Lite read
587 // The host took the read data.
588 if q.control.r_valid && axi.r_ready {
589 c.r_valid = false;
590 }
591 // Accept a new read address only when no answer is waiting, so `r_data`
592 // stays stable until the host takes it.
593 let ar_ready = !q.control.r_valid;
594 if axi.ar_valid && ar_ready {
595 // Offset being read, and the word to return (0 for unmapped offsets
596 // and for the write-only arguments and secrets).
597 let a = axi.ar_addr;
598 let mut value = b32(0);
599 // AP_CTRL: bit 0 ap_start (reads back as "running"), 1 ap_done,
600 // 2 ap_idle, 3 ap_ready. Reading it clears ap_done/ap_ready.
601 if a == b12(0x000) {
602 let idle = !q.control.running;
603 value = (if q.control.running { b32(1) } else { b32(0) })
604 | (if q.control.done_unread {
605 b32(2)
606 } else {
607 b32(0)
608 })
609 | (if idle { b32(4) } else { b32(0) })
610 | (if q.control.done_unread {
611 b32(8)
612 } else {
613 b32(0)
614 });
615 c.done_unread = false;
616 }
617 if a == b12(0x004) {
618 value = if q.control.gie { b32(1) } else { b32(0) };
619 }
620 if a == b12(0x008) {
621 value = if q.control.ier { b32(1) } else { b32(0) };
622 }
623 if a == b12(0x00c) {
624 value = if q.control.isr { b32(1) } else { b32(0) };
625 }
626 if a == b12(0x200) {
627 value = MAGIC;
628 }
629 if a == b12(0x204) {
630 value = ABI_VERSION;
631 }
632 // SHAPE: what this image was compiled with, so the host can size its
633 // buffers and argument list (slices in bits 0..8, TABLE_MASK above).
634 if a == b12(0x208) {
635 value = b32((SLICES | (TABLE_MASK << 8)) as u128);
636 }
637 if a == b12(0x20c) {
638 value = fault_bits;
639 }
640 // TICKS lo: also snapshot the high half, so the following read of
641 // 0x214 returns the matching upper word.
642 if a == b12(0x210) {
643 value = q.control.ticks.resize();
644 c.ticks_hi = (q.control.ticks >> 32).resize();
645 }
646 if a == b12(0x214) {
647 value = q.control.ticks_hi;
648 }
649 if a == b12(0x218) {
650 value = q.control.start_tick.resize();
651 }
652 if a == b12(0x21c) {
653 value = (q.control.start_tick >> 32).resize();
654 }
655 if a == b12(0x220) {
656 value = q.control.done_tick.resize();
657 }
658 if a == b12(0x224) {
659 value = (q.control.done_tick >> 32).resize();
660 }
661 if a == b12(0x228) {
662 value = q.cert.certified;
663 }
664 if a == b12(0x22c) {
665 value = q.reader.dealt;
666 }
667 if a == b12(0x230) {
668 value = q.cert.busy_cycles.resize();
669 }
670 if a == b12(0x234) {
671 value = (q.cert.busy_cycles >> 32).resize();
672 }
673 // Per-slice progress: frames acknowledged and keys started.
674 for s in 0..SLICES {
675 if a == b12((0x240 + 4 * s) as u128) {
676 value = q.writers[s].frames_written;
677 }
678 if a == b12((0x260 + 4 * s) as u128) {
679 value = q.from_slice[s].keys_started;
680 }
681 }
682 // The Ed25519 public key the certificate unit derived from the seed:
683 // four 64-bit words, returned as eight 32-bit halves (low half first).
684 for w in 0..4 {
685 if a == b12((0x280 + 8 * w) as u128) {
686 value = q.cert.public_key.words[w].resize();
687 }
688 if a == b12((0x284 + 8 * w) as u128) {
689 value = (q.cert.public_key.words[w] >> 32).resize();
690 }
691 }
692 // Register the answer; it is presented next clock and held until taken.
693 c.r_valid = true;
694 c.r_data = value;
695 }
696
697 // ------------------------------------------------ datapath wiring
698 // The one "go" signal every child sees.
699 let running = q.control.running;
700 // Ed25519 seed as the cert unit wants it: four 64-bit words built from
701 // the eight 32-bit register words (even index = low half).
702 let mut seed_words = [b64(0); 4];
703 for w in 0..4 {
704 seed_words[w] =
705 regs.seed[2 * w].resize::<64>() | (regs.seed[2 * w + 1].resize::<64>() << 32);
706 }
707 // Every slice link goes through registers (`crate::relay`): the slices
708 // sit up to two die crossings away from the shared blocks.
709 //
710 // Gather per-slice values for the shared children: subtree roots (out
711 // of each slice's delay line) go to the cert unit, and the cert unit and
712 // the reader see "taken" when the first station of the slice's relay
713 // pipe takes what they offer. Arrays are first filled from slice 0
714 // because RHDL needs a typed initial value.
715 let mut roots = [q.from_slice[0].root; SLICES];
716 let mut cert_taken = [false; SLICES];
717 let mut message_taken = [false; SLICES];
718 for s in 0..SLICES {
719 roots[s] = q.from_slice[s].root;
720 cert_taken[s] = q.cert.certificates[s].valid && q.certificates[s].ready;
721 message_taken[s] = q.reader.messages[s].valid && q.messages[s].ready;
722 }
723 // Delay-line inputs toward each slice: the shared run configuration
724 // plus its own slice number (which decides which keys of each batch the
725 // slice makes, so no two slices make the same key), `run`, and the root
726 // credit. Built as SLICES copies of slice 0's, then patched per slice.
727 let mut to_slice = [SliceControl {
728 config: FactoryConfig {
729 prefix_cv: regs.prefix_cv,
730 epoch: regs.epoch,
731 first_batch: regs.first_batch,
732 batches: regs.batches,
733 slice: b3(0),
734 },
735 run: running,
736 roots_popped: q.cert.roots_popped,
737 }; SLICES];
738 // Each slice's inputs, all from link registers: its delayed control,
739 // its message and certificate out of their relay pipes, and its beat
740 // pipe's `ready`. A relay pipe's `valid` is copied into the item's own
741 // `valid` field.
742 let mut slices = [SliceInput {
743 config: q.to_slice[0].config,
744 run: q.to_slice[0].run,
745 message: q.messages[0].data,
746 certificate: q.certificates[0].data,
747 beat_ready: q.beats[0].ready,
748 roots_popped: q.to_slice[0].roots_popped,
749 }; SLICES];
750 // Each writer: its frame base, its slice's beat out of the beat pipe,
751 // and its own HBM port's responses.
752 let mut writers = [WriterInput {
753 base: regs.out_base[0],
754 beat: q.beats[0].data,
755 memory: input.frames[0],
756 }; SLICES];
757 // Relay-pipe inputs: what the sender offers, and whether the receiver
758 // took the pipe's output this clock.
759 let mut beats = [PipeIn::<FrameBeat> {
760 data: q.slices[0].beat,
761 valid: q.slices[0].beat.valid,
762 taken: q.writers[0].beat_ready,
763 }; SLICES];
764 let mut messages = [PipeIn::<Message> {
765 data: q.reader.messages[0],
766 valid: q.reader.messages[0].valid,
767 taken: q.slices[0].message_taken,
768 }; SLICES];
769 let mut certificates = [PipeIn::<Certificate> {
770 data: q.cert.certificates[0],
771 valid: q.cert.certificates[0].valid,
772 taken: q.slices[0].certificate_taken,
773 }; SLICES];
774 // Delay-line inputs from each slice.
775 let mut from_slice = [SliceReport {
776 root: q.slices[0].root,
777 keys_started: q.slices[0].keys_started,
778 fault: q.slices[0].fault,
779 }; SLICES];
780 for s in 0..SLICES {
781 to_slice[s].config.slice = b3(s as u128);
782 slices[s] = SliceInput {
783 config: q.to_slice[s].config,
784 run: q.to_slice[s].run,
785 message: q.messages[s].data,
786 certificate: q.certificates[s].data,
787 beat_ready: q.beats[s].ready,
788 roots_popped: q.to_slice[s].roots_popped,
789 };
790 slices[s].message.valid = q.messages[s].valid;
791 slices[s].certificate.valid = q.certificates[s].valid;
792 writers[s] = WriterInput {
793 base: regs.out_base[s],
794 beat: q.beats[s].data,
795 memory: input.frames[s],
796 };
797 writers[s].beat.valid = q.beats[s].valid;
798 beats[s] = PipeIn::<FrameBeat> {
799 data: q.slices[s].beat,
800 valid: q.slices[s].beat.valid,
801 taken: q.writers[s].beat_ready,
802 };
803 messages[s] = PipeIn::<Message> {
804 data: q.reader.messages[s],
805 valid: q.reader.messages[s].valid,
806 taken: q.slices[s].message_taken,
807 };
808 certificates[s] = PipeIn::<Certificate> {
809 data: q.cert.certificates[s],
810 valid: q.cert.certificates[s].valid,
811 taken: q.slices[s].certificate_taken,
812 };
813 from_slice[s] = SliceReport {
814 root: q.slices[s].root,
815 keys_started: q.slices[s].keys_started,
816 fault: q.slices[s].fault,
817 };
818 }
819 // The writers' AXI4 write requests become the kernel's HBM output pins.
820 let mut frames = [q.writers[0].memory; SLICES];
821 for s in 0..SLICES {
822 frames[s] = q.writers[s].memory;
823 }
824 // Next state: control register plus the inputs of every child.
825 let d = EngineD::<TABLE_MASK> {
826 control: c,
827 reader: ReaderInput {
828 base: regs.msg_base,
829 count: regs.count,
830 run: running,
831 memory: input.messages,
832 taken: message_taken,
833 },
834 cert: CertInput {
835 config: CertConfig {
836 epoch: regs.epoch,
837 first_batch: regs.first_batch,
838 key_seed: ScalarWords { words: seed_words },
839 run: running,
840 },
841 roots,
842 taken: cert_taken,
843 },
844 slices,
845 writers,
846 beats,
847 messages,
848 certificates,
849 to_slice,
850 from_slice,
851 };
852 let _ = resetting;
853 (
854 EngineOutput {
855 // All AXI4-Lite outputs come straight from registers. Responses
856 // are always OKAY; unmapped reads return 0 and unmapped writes are
857 // ignored. The interrupt is a level: GIE and IER and ISR.
858 axi: AxiLiteOut {
859 aw_ready: !q.control.aw_have,
860 w_ready: !q.control.w_have,
861 b_valid: q.control.b_valid,
862 b_resp: b2(0),
863 ar_ready,
864 r_valid: q.control.r_valid,
865 r_data: q.control.r_data,
866 r_resp: b2(0),
867 interrupt: q.control.gie && q.control.ier && q.control.isr,
868 },
869 messages: q.reader.memory,
870 frames,
871 },
872 d,
873 )
874}