rhdl_dsig_scalable_core/relay.rs
1//! Relay stations and delay lines: registers that let slice traffic cross dies.
2//!
3//! # The problem: handshakes that span three SLRs
4//!
5//! U280 is three dies (SLRs) stacked on an interposer. The HBM ports, the
6//! AXI-Lite control port and therefore the engine's shared blocks (control
7//! registers, [message reader](crate::msg_reader) and
8//! [HBM writers](crate::hbm_writer)) belong at the bottom of SLR0, the
9//! [certificate unit](crate::cert_unit) sits in SLR1, and build v9
10//! (2026-10-17) pinned each [slice](crate::slice) wholly into one SLR
11//! (`fpga/u280/engine/floorplan.tcl`) so that no slice straddles a die.
12//!
13//! That worked: every slice landed inside its SLR. But every signal between
14//! a slice and a shared block was built for neighbours, with logic on both
15//! ends of the wire. The slices in SLR2 were two die crossings away, and v9
16//! routed at WNS -4.715 ns (TNS -81,716). Of its 5,000 worst paths:
17//!
18//! | paths | link |
19//! |---|---|
20//! | 2,809 | writer → HBM: the writer's `wvalid` is combinational from the slice's beat, and `beat_ready` from HBM's `wready` goes straight back into the signer |
21//! | 875 | control registers → the slices' BLAKE3 lanes (run configuration) |
22//! | ~180 | certificate unit ↔ slices (certificates in, roots out) |
23//! | ~30 | slice status counters → the register-read mux |
24//!
25//! The worst one ran from a writer counter in SLR1 through the writer's logic,
26//! into slice 3's signer in SLR2, and back down to the HBM interconnect in
27//! SLR0: 9.6 ns for a 4.878 ns clock, 93 % of it wire.
28//!
29//! # The decision: registers on both ends of every long wire
30//!
31//! A long wire is cheap if a flip-flop drives it and at most one LUT sits in
32//! front of the flip-flop it reaches. So every slice-boundary signal goes
33//! through registers in the engine ([`crate::engine`]), by kind:
34//!
35//! * **Full-rate handshakes** (frame beats out) go through a [`RelayPipe`]:
36//! [`PIPE_STAGES`] Carloni relay stations ([`Relay`]). A handshake cannot
37//! simply be delayed, because the receiver's "taken" must reach the sender
38//! in the same clock.
39//! * **Slower handshakes** (messages and certificates in) go through a
40//! [`ToggleLink`]: plain registers plus a one-bit toggle protocol. It moves
41//! one item per 6 clocks but costs almost no LUTs (relays cost 21,031 LUTs
42//! on these two paths in build v12). A signer takes at most one message per
43//! message-hash round trip and one certificate per 128 signatures.
44//! * **Everything else** (run configuration and `run`, subtree-root pulses,
45//! the `roots_popped` credit, status counters and the fault bit) goes
46//! through a [`Delay`]: [`DELAY_STAGES`] plain registers. These signals
47//! tolerate latency: configuration is static during a run, a root is a
48//! one-clock pulse that arrives two clocks later, a late credit only makes
49//! the slice more cautious, and the host reads status long after it
50//! changes.
51//!
52//! # Relay stations
53//!
54//! A *relay station* (L. P. Carloni, "From Latency-Insensitive Design to
55//! Communication-Based System-Level Design", Proc. IEEE 2015, figure 4) is a
56//! skid buffer: two data registers and a two-state controller inserted into a
57//! valid/ready link. Its outputs in **both** directions come from flip-flops:
58//!
59//! * `void_out` and the data come from the `main` register;
60//! * `stop_out` ("not ready") is just "the controller is in Stall".
61//!
62//! It still moves one item per clock: when the far side stops, the item
63//! already in flight is caught in `aux` instead of being lost. Each station
64//! adds one clock of latency.
65//!
66//! ```text
67//! SLR2: signer ─▶ [relay 0] ─┐
68//! SLR1: └▶ [relay 1] ─┐
69//! SLR0: └▶ [relay 2] ─▶ writer ─▶ HBM
70//! ◀── stop (registered) ── each station ── stop ◀── !beat_ready
71//! ```
72//!
73//! RHDL ships the same station as `rhdl_fpga::lid::carloni::Carloni`. This
74//! one differs in one way: its two data registers are resetless
75//! ([`DFF::resetless`]), so the wide payloads (a certificate is 1,345 bits)
76//! add no loads to the engine's reset tree (build v8's worst paths started at
77//! the reset). Only `void` and the state reset.
78//!
79//! # Protocol mapping
80//!
81//! | handshake | relay |
82//! |---|---|
83//! | `valid` | `!void` |
84//! | the receiver took the item this clock (`beat_ready`, `message_taken`, `certificate_taken`) | `!stop` |
85//!
86//! The receivers' answers are all "taken this clock", so they are low when
87//! nothing is offered. A station with an empty output ignores `stop`, so
88//! that is harmless. The sender sees "taken" as `valid && !stop_out` of the
89//! first station.
90
91use rhdl::prelude::*;
92use rhdl_fpga::core::dff::DFF;
93
94/// Relay stations in each [`RelayPipe`].
95pub const PIPE_STAGES: usize = 3;
96
97/// Index of a pipe's last station (RHDL kernel loops need a named bound,
98/// not an expression).
99const PIPE_LAST: usize = PIPE_STAGES - 1;
100
101/// Registers in each [`Delay`].
102pub const DELAY_STAGES: usize = 2;
103
104/// Index of a delay line's last register.
105const DELAY_LAST: usize = DELAY_STAGES - 1;
106
107/// Inputs of one relay station (Carloni's names).
108#[derive(Clone, Copy, Debug, PartialEq, Digital)]
109pub struct RelayIn<T: Digital> {
110 /// The item from upstream (ignored while `void_in`).
111 pub data_in: T,
112 /// Upstream offers nothing this clock.
113 pub void_in: bool,
114 /// Downstream cannot take an item this clock.
115 pub stop_in: bool,
116}
117
118/// Outputs of one relay station; every field comes from a register.
119#[derive(Clone, Copy, Debug, PartialEq, Digital)]
120pub struct RelayOut<T: Digital> {
121 /// The item held for downstream.
122 pub data_out: T,
123 /// No item is held for downstream.
124 pub void_out: bool,
125 /// Upstream must hold its item (the station is full).
126 pub stop_out: bool,
127}
128
129/// One Carloni relay station carrying a `T`.
130#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
131pub struct Relay<T: Digital> {
132 /// The item presented downstream (resetless).
133 main: DFF<T>,
134 /// The item caught while downstream was stopped (resetless).
135 aux: DFF<T>,
136 /// `main` holds no item; resets to empty.
137 void: DFF<bool>,
138 /// Stall: `aux` holds an item and upstream is stopped; resets to Run.
139 stall: DFF<bool>,
140}
141
142impl<T: Digital> Default for Relay<T> {
143 /// Empty and running after reset; data registers don't-care.
144 fn default() -> Self {
145 Self {
146 main: DFF::resetless(),
147 aux: DFF::resetless(),
148 void: DFF::new(true),
149 stall: DFF::new(false),
150 }
151 }
152}
153
154impl<T: Digital> SynchronousIO for Relay<T> {
155 type I = RelayIn<T>;
156 type O = RelayOut<T>;
157 type Kernel = relay_kernel<T>;
158}
159
160/// Carloni's controller (figure 4), with the state as one bit.
161///
162/// * **Run** (`stall` low): `main` loads the incoming item whenever
163/// downstream is not stopped, or `main` is empty. If downstream stops while
164/// both `main` and the input hold items, the input item goes into `aux`
165/// and the station moves to Stall.
166/// * **Stall**: `stop_out` is high. When downstream releases, `main` takes
167/// the item from `aux` and the station returns to Run (still stopping
168/// upstream for that one clock, so nothing new arrives while `aux` drains).
169#[kernel]
170pub fn relay_kernel<T: Digital>(cr: ClockReset, i: RelayIn<T>, q: RelayQ<T>) -> (RelayOut<T>, RelayD<T>) {
171 let mut d = RelayD::<T>::dont_care();
172 // Control signals, as named in the paper's figure.
173 let mut sel = false;
174 let mut main_en = false;
175 let mut aux_en = false;
176 let mut stop_out = false;
177 let void_out = q.void;
178 let will_stall = i.stop_in && !i.void_in && !void_out;
179 d.stall = q.stall;
180 if !q.stall {
181 if !i.stop_in || (!i.void_in && void_out) {
182 main_en = true;
183 } else if will_stall {
184 d.stall = true;
185 aux_en = true;
186 }
187 } else {
188 stop_out = true;
189 if !i.stop_in {
190 sel = true;
191 main_en = true;
192 d.stall = false;
193 }
194 }
195 d.aux = if aux_en { i.data_in } else { q.aux };
196 let data = if sel { q.aux } else { i.data_in };
197 d.main = if main_en { data } else { q.main };
198 let void = if sel { false } else { i.void_in };
199 d.void = if main_en { void } else { q.void };
200 let mut o = RelayOut::<T> {
201 data_out: q.main,
202 void_out: q.void,
203 stop_out,
204 };
205 if cr.reset.any() {
206 o.void_out = true;
207 o.stop_out = true;
208 }
209 (o, d)
210}
211
212/// Inputs of a [`RelayPipe`].
213#[derive(Clone, Copy, Debug, PartialEq, Digital)]
214pub struct PipeIn<T: Digital> {
215 /// The sender's item.
216 pub data: T,
217 /// The sender offers `data` this clock.
218 pub valid: bool,
219 /// The receiver took the pipe's output item this clock.
220 pub taken: bool,
221}
222
223/// Outputs of a [`RelayPipe`]; both come from registers.
224#[derive(Clone, Copy, Debug, PartialEq, Digital)]
225pub struct PipeOut<T: Digital> {
226 /// The item for the receiver.
227 pub data: T,
228 /// An item is offered to the receiver.
229 pub valid: bool,
230 /// The pipe can take the sender's item this clock (the sender's item was
231 /// taken if it also offered one).
232 pub ready: bool,
233}
234
235/// [`PIPE_STAGES`] relay stations in a row: sender side first.
236#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
237pub struct RelayPipe<T: Digital> {
238 /// The stations; `stages[0]` takes the sender's item.
239 stages: [Relay<T>; PIPE_STAGES],
240}
241
242impl<T: Digital> Default for RelayPipe<T> {
243 /// Every station empty.
244 fn default() -> Self {
245 Self {
246 stages: core::array::from_fn(|_| Relay::default()),
247 }
248 }
249}
250
251impl<T: Digital> SynchronousIO for RelayPipe<T> {
252 type I = PipeIn<T>;
253 type O = PipeOut<T>;
254 type Kernel = relay_pipe_kernel<T>;
255}
256
257/// Wire the stations: data and void flow toward the receiver, stop flows
258/// back.
259#[kernel]
260pub fn relay_pipe_kernel<T: Digital>(_cr: ClockReset, i: PipeIn<T>, q: RelayPipeQ<T>) -> (PipeOut<T>, RelayPipeD<T>) {
261 let mut d = RelayPipeD::<T>::dont_care();
262 d.stages[0].data_in = i.data;
263 d.stages[0].void_in = !i.valid;
264 for k in 1..PIPE_STAGES {
265 d.stages[k].data_in = q.stages[k - 1].data_out;
266 d.stages[k].void_in = q.stages[k - 1].void_out;
267 }
268 for k in 0..PIPE_LAST {
269 d.stages[k].stop_in = q.stages[k + 1].stop_out;
270 }
271 d.stages[PIPE_LAST].stop_in = !i.taken;
272 let last = q.stages[PIPE_LAST];
273 (
274 PipeOut::<T> {
275 data: last.data_out,
276 valid: !last.void_out,
277 ready: !q.stages[0].stop_out,
278 },
279 d,
280 )
281}
282
283/// Forward data registers in a [`ToggleLink`] after its `held` register.
284pub const TOGGLE_STAGES: usize = 2;
285
286/// Index of a toggle link's last forward register.
287const TOGGLE_LAST: usize = TOGGLE_STAGES - 1;
288
289/// A valid/ready link for rare, wide items, built from plain registers.
290///
291/// # Why not a relay pipe
292///
293/// A [`Relay`] station chooses between its `main` and `aux` registers, which
294/// costs a LUT per data bit per station. For the certificate path that came
295/// to 4,760 LUTs per slice in build v12 (19,037 of its 410,003), to carry one
296/// certificate per 128 signatures. This link carries the same handshake with
297/// no per-bit logic: the data only ever moves forward, through plain
298/// registers, and one bit in each direction carries the protocol.
299///
300/// # The protocol: a toggle (two-phase) handshake
301///
302/// ```text
303/// sender side receiver side
304/// item ─▶ [held] ─▶ [fwd 0] ─▶ [fwd 1] ─────────────▶ data, valid = seq != last
305/// [seq ] ─▶ [seq 0] ─▶ [seq 1] ──────────────┘
306/// ready = seq == ack ◀── [ack 1] ◀── [ack 0] ◀── [last] ◀── taken
307/// ```
308///
309/// * The sender side loads an item into `held` and flips `seq`, but only when
310/// `seq == ack`: the receiver has taken everything sent so far.
311/// * `held` and `seq` travel forward together through [`TOGGLE_STAGES`]
312/// registers. The receiver sees a new item while the delayed `seq` differs
313/// from `last`, the `seq` of the item it took most recently.
314/// * Taking the item copies the delayed `seq` into `last`, which ends `valid`
315/// at the next clock and travels back as `ack`.
316///
317/// `held` keeps the item until the receiver has taken it, so the forward
318/// registers need no enable and no reset: they copy every clock. Every
319/// output comes from a register or compares two registers. One item makes a
320/// round trip in `2 * TOGGLE_STAGES + 2` clocks (6 with two stages), so this
321/// link suits items that arrive far less often than that.
322#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
323pub struct ToggleLink<T: Digital> {
324 /// The item being delivered (resetless; loaded only when `ready`).
325 held: DFF<T>,
326 /// Flips once per item loaded; resets low.
327 seq: DFF<bool>,
328 /// `held`, delayed (resetless).
329 fwd: [DFF<T>; TOGGLE_STAGES],
330 /// `seq`, delayed alongside `fwd`.
331 fwd_seq: [DFF<bool>; TOGGLE_STAGES],
332 /// The `seq` of the item the receiver took most recently; resets low.
333 last: DFF<bool>,
334 /// `last`, delayed back to the sender side.
335 ack: [DFF<bool>; TOGGLE_STAGES],
336}
337
338impl<T: Digital> Default for ToggleLink<T> {
339 /// Empty and ready after reset; data registers don't-care.
340 fn default() -> Self {
341 Self {
342 held: DFF::resetless(),
343 seq: DFF::new(false),
344 fwd: core::array::from_fn(|_| DFF::resetless()),
345 fwd_seq: core::array::from_fn(|_| DFF::new(false)),
346 last: DFF::new(false),
347 ack: core::array::from_fn(|_| DFF::new(false)),
348 }
349 }
350}
351
352impl<T: Digital> SynchronousIO for ToggleLink<T> {
353 type I = PipeIn<T>;
354 type O = PipeOut<T>;
355 type Kernel = toggle_link_kernel<T>;
356}
357
358/// Load, shift and compare (see [`ToggleLink`]).
359#[kernel]
360pub fn toggle_link_kernel<T: Digital>(cr: ClockReset, i: PipeIn<T>, q: ToggleLinkQ<T>) -> (PipeOut<T>, ToggleLinkD<T>) {
361 let mut d = ToggleLinkD::<T>::dont_care();
362 let ready = q.seq == q.ack[TOGGLE_LAST];
363 let load = i.valid && ready;
364 d.held = if load { i.data } else { q.held };
365 d.seq = if load { !q.seq } else { q.seq };
366 d.fwd[0] = q.held;
367 d.fwd_seq[0] = q.seq;
368 for k in 1..TOGGLE_STAGES {
369 d.fwd[k] = q.fwd[k - 1];
370 d.fwd_seq[k] = q.fwd_seq[k - 1];
371 }
372 let valid = q.fwd_seq[TOGGLE_LAST] != q.last;
373 d.last = if i.taken && valid { q.fwd_seq[TOGGLE_LAST] } else { q.last };
374 d.ack[0] = q.last;
375 for k in 1..TOGGLE_STAGES {
376 d.ack[k] = q.ack[k - 1];
377 }
378 let mut o = PipeOut::<T> {
379 data: q.fwd[TOGGLE_LAST],
380 valid,
381 ready,
382 };
383 if cr.reset.any() {
384 o.valid = false;
385 o.ready = false;
386 }
387 (o, d)
388}
389
390/// [`DELAY_STAGES`] plain registers: the output is the input from
391/// `DELAY_STAGES` clocks ago. Resets to `T::dont_care()` (all zeros), so a
392/// delayed `valid`, `run` or fault bit is low after reset.
393#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
394pub struct Delay<T: Digital> {
395 /// The registers; `stages[0]` takes the input.
396 stages: [DFF<T>; DELAY_STAGES],
397}
398
399impl<T: Digital> Default for Delay<T> {
400 /// Every register reset to zero.
401 fn default() -> Self {
402 Self {
403 stages: core::array::from_fn(|_| DFF::new(T::dont_care())),
404 }
405 }
406}
407
408impl<T: Digital> SynchronousIO for Delay<T> {
409 type I = T;
410 type O = T;
411 type Kernel = delay_kernel<T>;
412}
413
414/// Shift the input one register along per clock.
415#[kernel]
416pub fn delay_kernel<T: Digital>(_cr: ClockReset, i: T, q: DelayQ<T>) -> (T, DelayD<T>) {
417 let mut d = DelayD::<T>::dont_care();
418 d.stages[0] = i;
419 for k in 1..DELAY_STAGES {
420 d.stages[k] = q.stages[k - 1];
421 }
422 (q.stages[DELAY_LAST], d)
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 /// Test payload: a number and some filler.
430 type Item = (b16, [b8; 8]);
431
432 /// The item carrying number `n`.
433 fn item(n: u32) -> Item {
434 (b16(u128::from(n)), core::array::from_fn(|k| b8(u128::from((n + k as u32) & 0xff))))
435 }
436
437 /// Drive `count` items through a link (a [`RelayPipe`] or a
438 /// [`ToggleLink`]) with the given sender
439 /// and receiver patterns (`true` = offer / take this clock, by cycle
440 /// number). The receiver takes an item only when one is offered and its
441 /// pattern allows, like the engine's receivers. Returns the received
442 /// item numbers and the cycles it took.
443 fn run<P>(pipe: P, count: u32, offer: impl Fn(u32) -> bool, accept: impl Fn(u32) -> bool) -> (Vec<u32>, u32)
444 where
445 P: Synchronous + SynchronousIO<I = PipeIn<Item>, O = PipeOut<Item>>,
446 {
447 let mut state = pipe.init();
448 let input = |n: u32, valid: bool, taken: bool| PipeIn::<Item> {
449 data: item(n),
450 valid,
451 taken,
452 };
453 // Two clocks of reset.
454 for _ in 0..2 {
455 pipe.sim(clock_reset(clock(false), reset(true)), input(0, false, false), &mut state);
456 pipe.sim(clock_reset(clock(true), reset(true)), input(0, false, false), &mut state);
457 }
458 let mut sent = 0;
459 let mut received = Vec::new();
460 let mut cycle = 0;
461 while received.len() < count as usize {
462 assert!(cycle < 100 * count + 100, "pipe stalled");
463 // The sender offers its next item when its pattern says so.
464 let offering = sent < count && offer(cycle);
465 // Both outputs are registered, so they do not depend on this
466 // clock's inputs; read them in the low half of the clock.
467 let out = pipe.sim(clock_reset(clock(false), reset(false)), input(sent, offering, false), &mut state);
468 // The receiver takes the output item when offered and allowed.
469 let take = out.valid && accept(cycle);
470 if take {
471 received.push(out.data.0.raw() as u32);
472 assert_eq!(out.data, item(out.data.0.raw() as u32), "payload intact");
473 }
474 let was_sent = offering && out.ready;
475 // Present the final inputs, then the rising edge.
476 let now = input(sent, offering, take);
477 pipe.sim(clock_reset(clock(false), reset(false)), now, &mut state);
478 pipe.sim(clock_reset(clock(true), reset(false)), now, &mut state);
479 if was_sent {
480 sent += 1;
481 }
482 cycle += 1;
483 }
484 (received, cycle)
485 }
486
487 /// Always offering and always taking, the pipe moves one item per clock
488 /// after its [`PIPE_STAGES`] clocks of latency.
489 #[test]
490 fn full_rate_with_three_clocks_of_latency() {
491 let (got, cycles) = run(RelayPipe::<Item>::default(), 200, |_| true, |_| true);
492 assert_eq!(got, (0..200).collect::<Vec<_>>());
493 assert!(cycles <= 200 + PIPE_STAGES as u32 + 1, "{cycles} cycles");
494 }
495
496 /// Random gaps on both sides: every item arrives once, in order, intact.
497 #[test]
498 fn random_backpressure_keeps_order() {
499 // xorshift patterns with fixed seeds; `keep` of 8 clocks are on.
500 let pattern = |seed: u64, keep: u64| {
501 move |c: u32| {
502 let mut x = seed ^ (u64::from(c) + 1).wrapping_mul(0x9e37_79b9_7f4a_7c15);
503 x ^= x << 13;
504 x ^= x >> 7;
505 x ^= x << 17;
506 x % 8 < keep
507 }
508 };
509 for (up, down) in [(7, 3), (3, 7), (5, 5), (8, 1), (1, 8)] {
510 let (got, _) = run(RelayPipe::<Item>::default(), 300, pattern(0x1234 + up, up), pattern(0x9876 + down, down));
511 assert_eq!(got, (0..300).collect::<Vec<_>>(), "offer {up}/8, take {down}/8");
512 let (got, _) = run(ToggleLink::<Item>::default(), 100, pattern(0x1234 + up, up), pattern(0x9876 + down, down));
513 assert_eq!(got, (0..100).collect::<Vec<_>>(), "toggle: offer {up}/8, take {down}/8");
514 }
515 }
516
517 /// Always offering and always taking, a [`ToggleLink`] delivers one item
518 /// per round trip of `2 * TOGGLE_STAGES + 2` clocks, never two at once.
519 #[test]
520 fn toggle_link_one_item_per_round_trip() {
521 let round_trip = 2 * TOGGLE_STAGES as u32 + 2;
522 let (got, cycles) = run(ToggleLink::<Item>::default(), 50, |_| true, |_| true);
523 assert_eq!(got, (0..50).collect::<Vec<_>>());
524 assert!(cycles <= 50 * round_trip + round_trip, "{cycles} cycles");
525 assert!(cycles >= 49 * round_trip, "{cycles} cycles: faster than a round trip?");
526 }
527
528 /// A [`ToggleLink`]'s outputs depend only on its registers.
529 #[test]
530 fn toggle_link_outputs_are_registered() {
531 let link = ToggleLink::<Item>::default();
532 let mut state = link.init();
533 let idle = PipeIn::<Item> {
534 data: item(0),
535 valid: false,
536 taken: false,
537 };
538 link.sim(clock_reset(clock(false), reset(true)), idle, &mut state);
539 link.sim(clock_reset(clock(true), reset(true)), idle, &mut state);
540 let a = link.sim(clock_reset(clock(false), reset(false)), idle, &mut state);
541 let busy = PipeIn::<Item> {
542 data: item(1),
543 valid: true,
544 taken: true,
545 };
546 let b = link.sim(clock_reset(clock(false), reset(false)), busy, &mut state);
547 assert_eq!(a, b);
548 }
549
550 /// The pipe's outputs in both directions come from registers: changing
551 /// only this clock's inputs (no clock edge) changes nothing.
552 #[test]
553 fn outputs_are_registered() {
554 let pipe = RelayPipe::<Item>::default();
555 let mut state = pipe.init();
556 let idle = PipeIn::<Item> {
557 data: item(0),
558 valid: false,
559 taken: false,
560 };
561 pipe.sim(clock_reset(clock(false), reset(true)), idle, &mut state);
562 pipe.sim(clock_reset(clock(true), reset(true)), idle, &mut state);
563 let a = pipe.sim(clock_reset(clock(false), reset(false)), idle, &mut state);
564 let busy = PipeIn::<Item> {
565 data: item(1),
566 valid: true,
567 taken: true,
568 };
569 let b = pipe.sim(clock_reset(clock(false), reset(false)), busy, &mut state);
570 assert_eq!(a, b);
571 }
572
573 /// A [`Delay`] outputs zero during and after reset until real inputs
574 /// have shifted through, then the input from [`DELAY_STAGES`] clocks ago.
575 #[test]
576 fn delay_shifts_by_its_stages() {
577 let delay = Delay::<b16>::default();
578 let mut state = delay.init();
579 for _ in 0..2 {
580 delay.sim(clock_reset(clock(false), reset(true)), b16(0xffff), &mut state);
581 delay.sim(clock_reset(clock(true), reset(true)), b16(0xffff), &mut state);
582 }
583 for t in 0..10_u128 {
584 let out = delay.sim(clock_reset(clock(false), reset(false)), b16(t + 1), &mut state);
585 let expected = if t < DELAY_STAGES as u128 { 0 } else { t + 1 - DELAY_STAGES as u128 };
586 assert_eq!(out, b16(expected), "clock {t}");
587 delay.sim(clock_reset(clock(true), reset(false)), b16(t + 1), &mut state);
588 }
589 }
590}