Stalls & Bubbles
A handshaked pipeline stalls naturally: if a stage does not hand off, everything upstream of it stops receiving grants and freezes in place. That gives you two practical ways to stall a Kathryn pipeline on purpose:
- Stall inside a stage — put a wait (or any multi-cycle work) between the
stage’s
pipand itszync. The hand-off is delayed, and back-pressure does the rest. - Stall at a boundary — assert an arbiter’s hold input
(
PipCon.set_hold(...), or thestall()convenience). All grants on that boundary freeze while the hold is high.
Both examples below extend the three-stage pipeline from
Pipeline Basics: stage 1 counts
a <= a + 1, stage 2 latches b <= a, stage 3 latches c <= b.
Conditional stall inside a stage
Section titled “Conditional stall inside a stage”From test model tc17_pip_zync_cond_stall. Stage 2 guards its hand-off with a
one-shot condition: when the value about to be delivered (a + v) reaches v2,
it inserts a sywait(5) before the zync, so the whole pipeline stalls for
five extra cycles and then resumes.
self.v = val(8, 1, "v")self.v2 = val(8, 6, "v2")
# stage 1 — free-running counterwith pip(self.pip_cons[0], auto_req=True): with zync(self.pip_cons[1]): self.a |= self.a + self.v
# stage 2 — conditional one-shot stall before the hand-offwith pip(self.pip_cons[1]): with seq(): with cif((self.a + self.v) == self.v2): sywait(5) # delay the hand-off by 5 cycles with zync(self.pip_cons[2]): self.b |= self.a
# stage 3with pip(self.pip_cons[2]): with zync(self.pip_cons[3], auto_ack=True): self.c |= self.bBecause pip auto-opens an inner skeleton, the explicit seq() sequences the
guard and the hand-off: first evaluate the cif, possibly wait, then zync.
Timing
Section titled “Timing”With v = 1 and v2 = 6 the guard is true exactly once, when a == 5. The
observable trace (asserted cycle-accurately by the test bench):
| phase | a | b | c | note |
|---|---|---|---|---|
| free-run | 1, 2, … 5, 6 | one behind | two behind | +1 per cycle |
| stall | 6 for 6 samples | frozen at 5 | frozen | entry cycle + the 5 sywait holds |
| release | 7 | 6 | 5 | whole pipe steps together |
| next | 8 | 7 | 6 | back to +1 per cycle |
Stage 1 is never told to stop — it simply stops receiving grants on arb1
because stage 2 is busy waiting. That is back-pressure working as designed.
sequenceDiagram
participant S1 as "stage 1"
participant A1 as "arb1"
participant S2 as "stage 2"
S1->>A1: request (counting a)
Note over S2: cif hit → sywait(5) before zync
Note over S1,A1: hand-off delayed, no grant on arb1
Note over S1,S2: a frozen at 6, b frozen at 5 (5 cycles)
S2->>A1: wait done → zync completes
A1-->>S1: grant resumes, pipe steps together
One-cycle bubble via an arbiter hold
Section titled “One-cycle bubble via an arbiter hold”From test model tc18_pip_zync_stall_bubble. Here the pipeline itself is
untouched; a parallel control thread pulses the stage-1/stage-2 arbiter’s hold
once, five cycles in:
# ... the same three stages as the baseline ...
with seq(): sywait(5) self.pip_cons[1].stall() # drive arb1's hold — active while this step isArb.stall() creates a fresh 1-bit wire, drives it to constant 1, and binds it
as the arbiter’s hold gate (set_hold). Because the drive lives inside a seq
step, it is only active while that step is — after the 5-cycle wait the hold is
asserted for a single cycle, punching exactly one bubble into the flow.
sequenceDiagram
participant C as "parallel seq"
participant A1 as "arb1 (hold)"
participant P as "stages 1 and 2"
C->>A1: sywait(5), then stall() drives hold
Note over A1: hold high for one cycle
Note over P: a, b freeze — one bubble
A1-->>P: hold drops, grants resume
Note over P: back to full throughput, no data lost
Timing
Section titled “Timing”| phase | a | b | c | note |
|---|---|---|---|---|
| free-run | 1 … 5 | one behind | two behind | +1 per cycle |
| bubble | 5 | 4 | 4 | arb1 held for one cycle; a, b freeze |
| release | 6 | 5 | 4 | grants resume |
| next | 7 | 6 | 5 | steady +1 per cycle again |
Every cycle-to-cycle step of a is either +1 (free run) or 0 (the hold) — the
counter never jumps or rewinds, and after the bubble the pipeline is back to
full throughput with no data lost.
Hold vs. reset
Section titled “Hold vs. reset”A hold freezes grants: in-flight requests stay pending and continue as soon
as the hold drops — nothing is lost. An arbiter reset (flush() /
set_reset) clears grants, which discards the in-flight handshake and can
deadlock the chain if you don’t re-launch it. That failure mode and its cure
are covered in Flush & Hazards.