rhdl_dsig_scalable_core/hbm_writer.rs
1//! Write a slice's frames into HBM, one 26-beat burst per frame.
2//!
3//! # The problem
4//!
5//! Each [slice](crate::slice) produces finished signatures as a stream of
6//! 64-byte *frame beats* ([`FrameBeat`]): 26 beats make one frame, the
7//! 1,642-byte signature plus the message index, padded. Those frames must end
8//! up in HBM where the host can read them back and verify them. Something has
9//! to turn "here is a beat" into legal AXI4 write bursts (see
10//! [`crate::axi4`]), and it has to say honestly how many frames actually
11//! landed.
12//!
13//! # The reasoning
14//!
15//! * **Where does frame `f` go?** Frame `f` of a slice lands at
16//! `base + f · 2048`: a 2 KiB stride keeps every burst inside one 4 KiB page
17//! as AXI requires, and needs no multiplier. The host reads the first
18//! 1,664 bytes of each 2 KiB slot. (26 beats × 64 bytes = 1,664 bytes; the
19//! rest of the slot is unused.) Multiplying by 2,048 is just a left shift
20//! by [`FRAME_STRIDE_LOG2`] = 11 bits, which costs no logic.
21//! * **One HBM port per slice.** Every slice owns one writer and one HBM
22//! port, so frame writes scale with the slice count and slices never
23//! compete for a port (the planner's "frame writes" stage in
24//! [`crate::config`]).
25//! * **Count responses, not beats.** A beat that left the kernel may still
26//! fail in memory. Only the B (write response) channel says the data is in
27//! HBM, so that is what [`WriterOutput::frames_written`] counts and what the
28//! engine waits for before it declares the run done.
29//!
30//! # How it works
31//!
32//! ```text
33//! signer beats ──▶ [W] ──wvalid/wready──▶ HBM
34//! ▲ only after its AW
35//! frame counter ──▶ [AW] base + f << 11, awlen = 25
36//! [B] count responses; any non-OKAY response sets `error`
37//! ```
38//!
39//! The address for frame `f` is issued when the frame's first beat is waiting,
40//! and data beats of frame `f` flow only after that address was accepted, so
41//! the master never promises data it does not have. At most
42//! [`MAX_OUTSTANDING`] bursts await responses. `frames_written` counts frames
43//! whose write response came back OKAY; that, not beats sent, is what the run
44//! is measured on. (Precisely: it counts every write response, and any
45//! response that was not OKAY also sets the sticky `error` flag, which the
46//! engine reports in its `FAULT` register. A run with no fault therefore has
47//! every counted frame acknowledged OKAY.)
48//!
49//! # Three counters, one invariant
50//!
51//! The whole writer is three 32-bit frame counters and an error bit
52//! ([`WriterState`]). Because every frame is exactly one burst, each AXI
53//! channel's progress is just "how many frames has it finished":
54//!
55//! ```text
56//! b_frames ≤ w_frames ≤ aw_frames ≤ b_frames + MAX_OUTSTANDING
57//! (answered) (all beats (address
58//! sent) accepted)
59//!
60//! aw_frames == w_frames → the next frame still needs its address
61//! aw_frames > w_frames → the current frame's address is out; send beats
62//! aw_frames − b_frames → bursts awaiting a response (capped at 16)
63//! ```
64//!
65//! Because an address is only requested once a beat is waiting, and at most
66//! one address runs ahead of the data, `aw_frames` is at most one ahead of
67//! `w_frames`.
68//!
69//! # Timing
70//!
71//! Everything is combinational from the current state and inputs to the
72//! outputs, with the counters updated at the clock edge. So a beat that is
73//! valid with an address already accepted goes out the same cycle `wready` is
74//! high, and a whole frame can stream at one beat per clock (26 clocks) once
75//! its address is accepted.
76//!
77//! # Neighbours
78//!
79//! The beats come from [`crate::slice_signer`] (through [`crate::slice`]);
80//! the `beat_ready` answer goes back to it. [`crate::engine`] instantiates
81//! one writer per slice, gives each its `base` address from the host
82//! registers, sums `frames_written` to decide when the run is done, and ORs
83//! the `error` bits into `FAULT` bit 0.
84
85use crate::axi4::{RESP_OKAY, WriteRequest, WriteResponse};
86use crate::slice_signer::{FRAME_BEATS, FrameBeat};
87use rhdl::prelude::*;
88use rhdl_fpga::core::dff::DFF;
89
90/// Bursts in flight before the master waits for responses.
91///
92/// HBM answers a write burst some cycles after its last beat. If the writer
93/// waited for each answer before starting the next frame it would idle for
94/// that whole round trip; allowing up to 16 unanswered bursts hides the
95/// latency. The cap bounds how far the writer can run ahead of writes the
96/// memory has confirmed.
97pub const MAX_OUTSTANDING: usize = 16;
98/// log2 of the frame stride in bytes (2 KiB).
99///
100/// Frame `f` starts at `base + (f << 11)`. A power-of-two stride turns the
101/// address multiply into a free wire shift, and 2 KiB divides 4 KiB, so a
102/// 1,664-byte frame never crosses a 4 KiB AXI page.
103pub const FRAME_STRIDE_LOG2: usize = 11;
104
105/// Writer inputs, sampled every clock.
106#[derive(Clone, Copy, Debug, PartialEq, Digital)]
107pub struct WriterInput {
108 /// Base byte address of this slice's frame region (2 KiB aligned; 64
109 /// bits). Set by the host through the engine's `frames{s}` argument and
110 /// held constant for the whole run.
111 pub base: b64,
112 /// Frame beats from the signer: `valid`, 64 data bytes and `last` (set on
113 /// the 26th beat of a frame).
114 pub beat: FrameBeat,
115 /// Channel signals from memory (the `ready`s and the B response).
116 pub memory: WriteResponse,
117}
118
119/// Writer outputs.
120#[derive(Clone, Copy, Debug, PartialEq, Digital)]
121pub struct WriterOutput {
122 /// Channel signals to memory (AW, W and `bready`).
123 pub memory: WriteRequest,
124 /// The presented beat was accepted (1 bit). The signer must hold the same
125 /// beat until it sees this high, then present the next one.
126 pub beat_ready: bool,
127 /// Frames acknowledged OKAY (32 bits; see the module notes for the exact
128 /// meaning). Read by the host per slice as `FRAMES[s]`.
129 pub frames_written: b32,
130 /// A write response was not OKAY (sticky until reset).
131 pub error: bool,
132}
133
134/// Writer state (all resettable; small).
135///
136/// Only counters live here, no data: the 64-byte beat goes straight from the
137/// signer to the W channel, so the state is 3 × 32 + 1 = 97 flip-flops and
138/// the writer needs no RAM.
139#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
140pub struct WriterState {
141 /// Addresses accepted (32 bits): frames whose AW transfer has happened.
142 pub aw_frames: b32,
143 /// Frames whose last beat was accepted (32 bits).
144 pub w_frames: b32,
145 /// Responses received (32 bits): B transfers seen.
146 pub b_frames: b32,
147 /// Error seen (sticky): some B response was not OKAY.
148 pub error: bool,
149}
150
151/// The per-slice HBM write master.
152///
153/// An RHDL [`Synchronous`] component: a struct of clocked children (here one
154/// register, `state`) plus a pure kernel function, [`hbm_writer_kernel`],
155/// that computes outputs and next state every clock.
156#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
157pub struct HbmWriter {
158 /// The three frame counters and the error bit, reset to zero.
159 state: DFF<WriterState>,
160}
161
162/// A fresh writer: nothing addressed, written or answered.
163impl Default for HbmWriter {
164 fn default() -> Self {
165 Self {
166 state: DFF::new(WriterState::default()),
167 }
168 }
169}
170
171/// Binds the writer's input and output types to its kernel.
172impl SynchronousIO for HbmWriter {
173 type I = WriterInput;
174 type O = WriterOutput;
175 type Kernel = hbm_writer_kernel;
176}
177
178/// The writer's logic.
179///
180/// * Inputs: `clock_reset` (reset forces every `valid` low), the signer's
181/// beat, the memory's `ready`s and B response, and `q`, the current
182/// [`WriterState`].
183/// * Outputs: the AXI4 write request, `beat_ready` back to the signer, the
184/// acknowledged-frame count and the sticky error; plus `HbmWriterD`, the
185/// next state.
186/// * Timing: purely combinational within one cycle; the counters advance at
187/// the next clock edge. `frames_written` and `error` are the registered
188/// values, so they lag the B transfer by one clock.
189#[kernel]
190pub fn hbm_writer_kernel(
191 clock_reset: ClockReset,
192 input: WriterInput,
193 q: HbmWriterQ,
194) -> (WriterOutput, HbmWriterD) {
195 // High during reset: no channel may claim `valid` then.
196 let resetting = clock_reset.reset.any();
197 // Current counters, and a copy we update to form the next state.
198 let s = q.state;
199 let mut next = s;
200 // Bursts whose address was accepted but whose response has not arrived.
201 let outstanding = s.aw_frames - s.b_frames;
202 // Address of the frame whose beats are (or will next be) on the W channel.
203 let need_address = s.aw_frames == s.w_frames && input.beat.valid;
204 // AW step: ask for the address only when a beat is actually waiting and
205 // fewer than MAX_OUTSTANDING bursts are unanswered.
206 let awvalid = need_address && outstanding < b32(MAX_OUTSTANDING as u128) && !resetting;
207 if awvalid && input.memory.awready {
208 next.aw_frames = s.aw_frames + b32(1);
209 }
210 // W step: data may flow only once this frame's address has been accepted,
211 // so we never put beats on W that the memory has no address for.
212 let address_ready = s.aw_frames > s.w_frames;
213 let wvalid = input.beat.valid && address_ready && !resetting;
214 // The beat transfers this cycle (valid and ready both high).
215 let accepted = wvalid && input.memory.wready;
216 // The 26th beat closes the frame on W.
217 if accepted && input.beat.last {
218 next.w_frames = s.w_frames + b32(1);
219 }
220 // B step: `bready` is always high, so every response is counted the cycle
221 // it appears; a bad code latches the error.
222 if input.memory.bvalid {
223 next.b_frames = s.b_frames + b32(1);
224 if input.memory.bresp != RESP_OKAY {
225 next.error = true;
226 }
227 }
228 // Address of frame number `w_frames` (the next frame to be written):
229 // base + w_frames · 2048, done as a shift.
230 let awaddr = input.base + (s.w_frames.resize::<64>() << (FRAME_STRIDE_LOG2 as u128));
231 (
232 WriterOutput {
233 memory: WriteRequest {
234 awvalid,
235 awaddr,
236 // Every burst is one whole frame: 26 beats, encoded as 25.
237 awlen: b8((FRAME_BEATS - 1) as u128),
238 wvalid,
239 wdata: input.beat.data,
240 wlast: input.beat.last,
241 // We can always count a response, so never stall B.
242 bready: true,
243 },
244 // Tell the signer its beat went out, so it can present the next.
245 beat_ready: accepted,
246 frames_written: s.b_frames,
247 error: s.error,
248 },
249 HbmWriterD { state: next },
250 )
251}