Skip to main content

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}