Skip to main content

rhdl_dsig_scalable_core/
msg_reader.rs

1//! Read 16-byte messages from HBM and deal them to the [`SLICES`] slices.
2//!
3//! # The problem
4//!
5//! Every signature signs one message, and the messages start out in HBM,
6//! written there by the host before the run. The [slices](crate::slice) need
7//! them one at a time, each slice at its own pace, and every resulting frame
8//! must be traceable back to the exact message it signed. This module is the
9//! single AXI4 read master (see [`crate::axi4`]) that fetches the messages
10//! and hands them out like a card dealer.
11//!
12//! # The reasoning
13//!
14//! * **Fetch in bursts, deal one by one.** HBM is efficient with long bursts
15//!   and inefficient with tiny reads. A message is only 16 bytes, so the
16//!   reader fetches 16-beat (1 KiB, 64-message) bursts into a small block-RAM
17//!   FIFO (first-in, first-out queue) and unpacks four messages from each
18//!   64-byte beat.
19//! * **Round-robin, in order.** Message `n` always goes to slice
20//!   `n mod SLICES`. The rule is fixed, so the host knows in advance which
21//!   slice's frame region will hold each signature, and no tag or arbiter is
22//!   needed. Because `SLICES` is a power of two, `mod` is a bit mask
23//!   (`SLICE_MASK`); RHDL kernels avoid `/` and `%`.
24//! * **Credits instead of backpressure.** A read address is issued only when
25//!   the FIFO has room for the whole burst. Data that the reader asked for
26//!   therefore always fits, and `rready` can be tied high. That is simpler
27//!   and safer than stalling the R channel.
28//!
29//! # How it works
30//!
31//! The host writes `count` messages back to back starting at `base` (4 KiB
32//! aligned). Each 64-byte beat holds four messages. Message `n` goes to slice
33//! `n mod SLICES`, carrying its index `n`, which the slice writes into the frame
34//! padding so the host can check every frame against the right message.
35//!
36//! ```text
37//!   HBM ──16-beat bursts──▶ beat FIFO (64 beats, credit-limited) ──▶ unpack 4/beat
38//!                                                                     │ msg n
39//!                                               slice (n mod SLICES) buffer ◀┘
40//! ```
41//!
42//! Read addresses are issued only while the FIFO has room for the whole burst,
43//! so read data can always be accepted (`rready` is constant high). A slice
44//! buffer holds one message; dealing stalls when the target slice's buffer is
45//! still full, which keeps messages strictly in order.
46//!
47//! # Four counters track every beat
48//!
49//! All bookkeeping is in *beats* (64 bytes = 4 messages) except `dealt`,
50//! which counts messages:
51//!
52//! ```text
53//!   consumed ≤ received ≤ requested ≤ ceil(count / 4)
54//!
55//!   requested − consumed   beats the FIFO has promised room for (≤ 64)
56//!   received − consumed    beats sitting in the FIFO, ready to unpack
57//!   lane                   which of the 4 messages in the head beat is next
58//!   dealt                  messages handed to slices = index of the next one
59//! ```
60//!
61//! The FIFO is a 64-entry ring of 64-byte words: the low 6 bits of
62//! `received` are the write slot and the low 6 bits of `consumed` the read
63//! slot, so the ring wraps for free.
64//!
65//! # Timing
66//!
67//! The block-RAM FIFO has one clock of read latency. Moving a beat into the
68//! `head` register therefore takes two steps: a cycle that sets
69//! `read_pending`, then a cycle that captures the RAM output. After that the
70//! dealer can hand out at most one message per clock, so one beat (4
71//! messages) is unpacked in 4 clocks when the target slices are free. A
72//! slice consumes a message far less often than that (one key per 68 clocks,
73//! see [`crate::slice_schedule`]), so the reader is not the bottleneck.
74//!
75//! # Neighbours
76//!
77//! [`crate::engine`] supplies `base`, `count` and `run` from its registers,
78//! forwards the AXI read channel, and wires each slice's `message_taken`
79//! back here as `taken`. The [`Message`] type, and the rule that a message
80//! is held until taken, come from [`crate::slice_signer`]. The `dealt` count
81//! is the host-visible `DEALT` register and `error` is `FAULT` bit 1.
82
83use crate::axi4::{RESP_OKAY, ReadRequest, ReadResponse};
84use crate::cert_unit::SLICES;
85use crate::slice_signer::Message;
86use rhdl::prelude::*;
87use rhdl_fpga::core::{
88    dff::DFF,
89    ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
90};
91
92/// `SLICES − 1`, the mask for `n mod SLICES`.
93///
94/// 3 bits wide because the largest build has 8 slices (slice numbers 0..=7).
95/// `n & SLICE_MASK` equals `n mod SLICES` only because `SLICES` is a power of
96/// two (2, 4 or 8).
97const SLICE_MASK: b3 = b3((SLICES - 1) as u128);
98
99/// Beats per read burst (1 KiB).
100///
101/// 16 beats × 64 bytes = 1 KiB = 64 messages. Bursts start at
102/// `base + 64 · requested`, and `requested` advances by 16 except for the
103/// final, shorter burst, so with a 4 KiB-aligned base no burst crosses a
104/// 4 KiB page.
105pub const BURST_BEATS: usize = 16;
106/// Beat FIFO capacity.
107///
108/// 64 beats (4 KiB, 256 messages), which is the 2⁶ entries addressed by the
109/// FIFO's 6-bit address. It holds four full bursts, so one burst can be in
110/// flight from HBM while earlier ones are still being dealt.
111pub const FIFO_BEATS: usize = 64;
112
113/// Reader inputs, sampled every clock.
114#[derive(Clone, Copy, Debug, PartialEq, Digital)]
115pub struct ReaderInput {
116    /// Byte address of message 0 (4 KiB aligned; 64 bits). Host argument
117    /// `messages`.
118    pub base: b64,
119    /// Number of messages to read (32 bits). Host argument `count`; it need
120    /// not be a multiple of 4.
121    pub count: b32,
122    /// Start reading (1 bit). High while the engine is running; no read
123    /// address is issued while it is low.
124    pub run: bool,
125    /// Memory read channel signals (AR `ready` and the R channel).
126    pub memory: ReadResponse,
127    /// Each slice consumed its buffered message this cycle (one bit per
128    /// slice). This frees that slice's one-message buffer.
129    pub taken: [bool; SLICES],
130}
131
132/// Reader outputs.
133#[derive(Clone, Copy, Debug, PartialEq, Digital)]
134pub struct ReaderOutput {
135    /// Memory read channel signals (AR and `rready`).
136    pub memory: ReadRequest,
137    /// The buffered message offered to each slice. `valid` is high while the
138    /// buffer is full; the slice holds off until it is ready, then pulses
139    /// `taken`.
140    pub messages: [Message; SLICES],
141    /// Messages dealt to slices so far (32 bits; the host's `DEALT`
142    /// register).
143    pub dealt: b32,
144    /// A read response was not OKAY (sticky until reset).
145    pub error: bool,
146}
147
148/// Resettable control.
149///
150/// Every field here must start from a known value after reset, so it lives in
151/// a register with a reset value (all zero / false).
152#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
153pub struct ReaderControl {
154    /// Beats requested (32 bits): sum of the burst lengths whose AR transfer
155    /// has happened.
156    pub requested: b32,
157    /// Beats received (32 bits): R transfers written into the FIFO. Its low
158    /// 6 bits are the FIFO write slot.
159    pub received: b32,
160    /// Beats fully unpacked (32 bits). Its low 6 bits are the FIFO read slot.
161    pub consumed: b32,
162    /// Message within the current beat (0..=3, 2 bits): the beat's 16-byte
163    /// lane that will be dealt next.
164    pub lane: b2,
165    /// The FIFO output register holds beat `consumed`, copied into `head`.
166    pub head_valid: bool,
167    /// A FIFO read is outstanding: the RAM's output will be valid next clock.
168    pub read_pending: bool,
169    /// Messages dealt (32 bits). Also the index of the next message, which
170    /// picks its slice (`dealt mod SLICES`).
171    pub dealt: b32,
172    /// Slice buffers occupied (one bit per slice).
173    pub full: [bool; SLICES],
174    /// Error seen (sticky): some R beat came back not OKAY.
175    pub error: bool,
176}
177
178/// Resetless data.
179///
180/// Wide data registers do not need a reset value, because the control bits
181/// above say whether they hold anything meaningful. Skipping the reset saves
182/// routing and logic on hundreds of bits.
183#[derive(Clone, Copy, Debug, PartialEq, Digital)]
184pub struct ReaderData {
185    /// Beat being unpacked (64 bytes); meaningful while `head_valid`.
186    pub head: [b8; 64],
187    /// Buffered message per slice; meaningful while that slice's `full` bit
188    /// is set.
189    pub buffers: [Message; SLICES],
190}
191
192/// The message reader and dealer.
193///
194/// Three clocked children: the beat FIFO in block RAM, the resettable control
195/// register and the resetless data register. The per-clock logic is
196/// [`message_reader_kernel`].
197#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
198pub struct MessageReader {
199    /// 64 × 512-bit synchronous block RAM (6-bit address): the beat FIFO.
200    fifo: SyncBRAM<[b8; 64], 6>,
201    /// Counters, flags and slice-buffer occupancy ([`ReaderControl`]).
202    control: DFF<ReaderControl>,
203    /// The head beat and the per-slice message buffers ([`ReaderData`]).
204    data: DFF<ReaderData>,
205}
206
207/// A fresh reader: empty FIFO, zero counters, data registers left undefined
208/// ("don't care") because nothing reads them until a control bit says so.
209impl Default for MessageReader {
210    fn default() -> Self {
211        Self {
212            fifo: SyncBRAM::default(),
213            control: DFF::new(ReaderControl::default()),
214            data: DFF::resetless(),
215        }
216    }
217}
218
219/// Binds the reader's input and output types to its kernel.
220impl SynchronousIO for MessageReader {
221    type I = ReaderInput;
222    type O = ReaderOutput;
223    type Kernel = message_reader_kernel;
224}
225
226/// The reader's logic.
227///
228/// Runs once per clock and has four parts, in the order of a message's
229/// journey: issue read addresses (AR), store returned beats in the FIFO
230/// (R → FIFO), free slice buffers that were taken, and unpack the head beat
231/// into the next slice's buffer (unpack + deal).
232///
233/// * Inputs: `clock_reset`, the [`ReaderInput`] (host settings, memory
234///   signals, per-slice `taken`), and `q`, the current FIFO output, control
235///   and data registers.
236/// * Outputs: the [`ReaderOutput`] and `MessageReaderD`, the next register
237///   values plus this clock's FIFO read and write ports.
238/// * Timing: a message dealt in cycle `t` appears as `valid` to its slice in
239///   cycle `t + 1` (the outputs come from registers); a slice's `taken` in
240///   cycle `t` lets a new message be dealt into its buffer in cycle `t + 1`.
241#[allow(clippy::needless_range_loop)]
242#[kernel]
243pub fn message_reader_kernel(
244    clock_reset: ClockReset,
245    input: ReaderInput,
246    q: MessageReaderQ,
247) -> (ReaderOutput, MessageReaderD) {
248    // High during reset: no `valid` may be raised then.
249    let resetting = clock_reset.reset.any();
250    // Next control and data values; start from the current ones and edit.
251    let mut c = q.control;
252    let mut data = q.data;
253    // Beats needed: ceil(count / 4).
254    let total_beats = (input.count + b32(3)) >> 2;
255
256    // ---------------------------------------------------- AR
257    // Beats already promised FIFO space (requested but not yet unpacked).
258    let in_fifo = q.control.requested - q.control.consumed;
259    // A whole burst still fits: this is the credit check that lets `rready`
260    // stay high.
261    let room = in_fifo <= b32((FIFO_BEATS - BURST_BEATS) as u128);
262    // Some beats of the message buffer have not been asked for yet.
263    let more = q.control.requested < total_beats;
264    let arvalid = input.run && room && more && !resetting;
265    // Length of the next burst: 16 beats, or whatever is left at the end.
266    let remaining = total_beats - q.control.requested;
267    let burst: b32 = if remaining < b32(BURST_BEATS as u128) {
268        remaining
269    } else {
270        b32(BURST_BEATS as u128)
271    };
272    if arvalid && input.memory.arready {
273        c.requested = q.control.requested + burst;
274    }
275    // Byte address of beat `requested`: base + 64 · requested (shift by 6).
276    let araddr = input.base + (q.control.requested.resize::<64>() << 6);
277
278    // ---------------------------------------------------- R → FIFO
279    // Every arriving beat is written; space was reserved when it was asked for.
280    let write = input.memory.rvalid && !resetting;
281    if write {
282        c.received = q.control.received + b32(1);
283        if input.memory.rresp != RESP_OKAY {
284            c.error = true;
285        }
286    }
287    // FIFO write slot: low 6 bits of the received count (a 64-entry ring).
288    let write_address: b6 = q.control.received.resize();
289
290    // ---------------------------------------------------- slice buffers
291    // A slice that took its message this cycle has an empty buffer next cycle.
292    let mut full = q.control.full;
293    for s in 0..SLICES {
294        if input.taken[s] {
295            full[s] = false;
296        }
297    }
298
299    // ---------------------------------------------------- unpack + deal
300    // FIFO read slot: by default keep reading beat `consumed`.
301    let mut read_address: b6 = q.control.consumed.resize();
302    // Refill the head register in two steps because the block RAM answers one
303    // clock after it is addressed: first mark a read pending, then capture.
304    if q.control.read_pending {
305        data.head = q.fifo;
306        c.head_valid = true;
307        c.read_pending = false;
308    } else if !q.control.head_valid && q.control.received > q.control.consumed {
309        c.read_pending = true;
310    }
311    // Deal one message per clock while a head beat is loaded and messages remain.
312    if q.control.head_valid && q.control.dealt < input.count {
313        // SLICES is a power of two, so `n mod SLICES` is a mask.
314        let target: b3 = q.control.dealt.resize::<3>() & SLICE_MASK;
315        // Is the target slice's buffer empty? (Uses last cycle's `full`, so a
316        // buffer freed this cycle is refilled next cycle.)
317        let mut free = false;
318        for s in 0..SLICES {
319            if target == b3(s as u128) && !q.control.full[s] {
320                free = true;
321            }
322        }
323        // If it is not free we simply wait: skipping ahead to another slice
324        // would break the fixed `n mod SLICES` order.
325        if free {
326            // Pick the 16-byte lane `lane` out of the 64-byte head beat. RHDL
327            // needs constant array indices, so loop over all four lanes and
328            // keep the one that matches.
329            // Bytes past 16 stay zero: the HBM batch engine reads 16-byte messages
330            // (its exporter refuses profile 2).
331            let mut bytes = [b8(0); crate::MESSAGE_BYTES];
332            for l in 0..4 {
333                if q.control.lane == b2(l as u128) {
334                    for b in 0..16 {
335                        bytes[b] = q.data.head[16 * l + b];
336                    }
337                }
338            }
339            // The message carries its global index so the frame can name it.
340            let message = Message {
341                valid: true,
342                bytes,
343                index: q.control.dealt.resize(),
344            };
345            // Store it in the target slice's buffer and mark the buffer full.
346            for s in 0..SLICES {
347                if target == b3(s as u128) {
348                    data.buffers[s] = message;
349                    full[s] = true;
350                }
351            }
352            c.dealt = q.control.dealt + b32(1);
353            // 2-bit lane wraps from 3 back to 0 on its own.
354            c.lane = q.control.lane + b2(1);
355            // The beat is finished after its 4th message, or early if this was
356            // the very last message: retire it and point the FIFO at the next.
357            if q.control.lane == b2(3) || q.control.dealt + b32(1) == input.count {
358                c.head_valid = false;
359                c.consumed = q.control.consumed + b32(1);
360                read_address = (q.control.consumed + b32(1)).resize();
361            }
362        }
363    }
364    c.full = full;
365    // Offer each slice its registered buffer; `valid` is the registered full
366    // bit, forced low during reset.
367    let mut messages = q.data.buffers;
368    for s in 0..SLICES {
369        messages[s].valid = q.control.full[s] && !resetting;
370    }
371
372    // Next state plus this clock's FIFO ports (read slot, and the write of
373    // any arriving beat).
374    let d = MessageReaderD {
375        fifo: RamIn::<[b8; 64], 6> {
376            read_addr: read_address,
377            write: RamWrite::<[b8; 64], 6> {
378                addr: write_address,
379                value: input.memory.rdata,
380                enable: write,
381            },
382        },
383        control: c,
384        data,
385    };
386    (
387        ReaderOutput {
388            memory: ReadRequest {
389                arvalid,
390                araddr,
391                // AXI encodes the beat count minus one.
392                arlen: (burst - b32(1)).resize(),
393                // Always ready: the credit check above reserved the space.
394                rready: true,
395            },
396            messages,
397            dealt: q.control.dealt,
398            error: q.control.error,
399        },
400        d,
401    )
402}