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}