Skip to main content

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}