143 """Decoupled, pipelinable grant scheduler (`pipelined_scheduler=True`).
145 A **grant queue** holds upcoming winners for the datapath to pop, and a
146 **sweep scheduler** refills it off the critical path. That breaks the flat
147 arbiter's single-cycle `grant -> grant` loop, its dominant timing limiter at
148 high fan-in. A queued entry is a hint about who to serve next, not a promise
149 that a particular message is waiting: an entry whose input has since gone
150 idle is skipped in one cycle (`stale` below) rather than stalling the output.
152 Consequently service order is best-effort, and `queue_depth` bounds only how
153 far ahead of the datapath decisions are committed -- it is not a fairness
154 knob. See section 7.1 of `docs/components/ChannelArbiter.md` for why
155 committing early is safe and for the full ordering/latency caveats."""
156 assert num_inputs >= 2,
"GrantSchedulerMod requires at least two inputs"
157 gw = clog2(num_inputs)
159 class GrantScheduler(Module):
164 valids = Input(Bits(num_inputs))
166 launch = Input(Bits(1))
168 msg_end = Input(Bits(1))
171 grant = Output(Bits(gw))
173 grant_oh = Output(Bits(num_inputs))
175 busy = Output(Bits(1))
177 switch = Output(Bits(1))
180 def build(ports) -> None:
183 next_grant = Wire(Bits(gw),
"next_grant")
184 next_busy = Wire(Bits(1),
"next_busy")
186 next_grant, next_busy)
190 gq = SeqFIFO(Bits(gw), queue_depth, clk, rst)
191 q_pop = Wire(Bits(1),
"gq_pop")
192 q_head = gq.pop(q_pop)
193 q_nonempty = ~gq.empty
196 pending = Reg(Bits(num_inputs),
200 name=
"sched_pending")
201 pend_nonzero = pending != Bits(num_inputs)(0)
203 neg_pending = ((~pending).as_uint(num_inputs) +
204 UInt(num_inputs)(1)).as_bits(num_inputs)
205 low = pending & neg_pending
206 push = pend_nonzero & ~gq.full
217 cleared = pending & ~low
218 sweep_done = push & (cleared == Bits(num_inputs)(0))
220 Mux(~pend_nonzero | sweep_done, Mux(push, pending, cleared),
226 started = Reg(Bits(1), clk, rst, rst_value=0, name=
"grant_started")
227 sel_valid_now = (ports.valids & grant_oh).or_reduce()
230 stale = busy & ~started & ~sel_valid_now & q_nonempty
231 advance = ports.msg_end | stale
232 take_next = ~busy | advance
233 q_pop.assign(take_next & q_nonempty)
235 next_grant.assign(Mux(take_next & q_nonempty, grant, q_head))
236 next_busy.assign(Mux(take_next, busy, q_nonempty))
239 started_next = Mux(ports.launch, started, Bits(1)(1))
240 started.assign(Mux(take_next, started_next, Bits(1)(0)))
245 return GrantScheduler
250 """Combinational round-robin winner selection, factored into its own module
251 for waveform visibility.
253 Given a per-input `valids` bitmask (bit `i` is input `i`) and a `start` index,
254 `winner` is the lowest-index input that is valid and at index `>= start`
255 (cyclically), falling back to the lowest-index valid input overall; `any_valid`
256 is high when any input is valid. Purely combinational -- the owning state
257 (`rr_ptr`, `grant`/`busy`) lives in `RoundRobinControlMod`."""
258 assert num_inputs >= 2,
"RoundRobinArbiterMod requires at least two inputs"
259 gw = clog2(num_inputs)
261 class RoundRobinArbiter(Module):
262 valids = Input(Bits(num_inputs))
263 start = Input(Bits(gw))
264 winner = Output(Bits(gw))
265 any_valid = Output(Bits(1))
268 def build(ports) -> None:
271 bits_list: List[BitsSignal]) -> Tuple[BitsSignal, BitsSignal]:
272 """Index of the lowest-index set bit, plus an any-set flag, computed as
273 a balanced binary tree (O(log N) depth) rather than an O(N) chain. Each
274 node combines two subtrees, giving priority to the lower index, and
275 prefixes the selected sub-index with the branch bit."""
278 level = [(b,
None)
for b
in bits_list]
279 level += [(Bits(1)(0),
None)
for _
in range((1 << gw) - len(bits_list))]
281 while len(level) > 1:
283 for j
in range(0, len(level), 2):
285 ra, ri = level[j + 1]
291 idx = BitsSignal.concat([take_right, Mux(take_right, li, ri)])
292 nxt.append((la | ra, idx))
296 return (idx
if idx
is not None else Bits(gw)(0)), level[0][0]
298 valid_bits = [ports.valids[i]
for i
in range(num_inputs)]
299 start_u = ports.start.as_uint(gw)
301 hi = [valid_bits[i] & (UInt(gw)(i) >= start_u)
for i
in range(num_inputs)]
302 hi_idx, hi_any = priority_lsb(hi)
303 lo_idx, lo_any = priority_lsb(valid_bits)
304 ports.winner = Mux(hi_any, lo_idx, hi_idx)
305 ports.any_valid = hi_any | lo_any
307 return RoundRobinArbiter
312 """Flat round-robin grant control (the default strategy).
314 Answers "who is granted next?" combinationally in the cycle the current
315 message ends, using two `RoundRobinArbiter` instances -- one for picking up
316 from idle, one for the message-end turnaround -- plus the `rr_ptr` fairness
317 pointer, which is private to this strategy. See section 7 of
318 `docs/components/ChannelArbiter.md`.
320 `launch` is unused; it exists only to match `GrantSchedulerMod`'s
322 assert num_inputs >= 2,
"RoundRobinControlMod requires at least two inputs"
323 gw = clog2(num_inputs)
325 class RoundRobinControl(Module):
329 valids = Input(Bits(num_inputs))
330 launch = Input(Bits(1))
331 msg_end = Input(Bits(1))
333 grant = Output(Bits(gw))
334 grant_oh = Output(Bits(num_inputs))
335 busy = Output(Bits(1))
336 switch = Output(Bits(1))
339 def build(ports) -> None:
342 next_grant = Wire(Bits(gw),
"next_grant")
343 next_busy = Wire(Bits(1),
"next_busy")
345 next_grant, next_busy)
346 rr_ptr = Reg(Bits(gw), clk, rst, name=
"rr_ptr")
349 def round_robin(valids_vec: BitsSignal, start: BitsSignal,
350 name: str) -> Tuple[BitsSignal, BitsSignal]:
351 """Instantiate a RoundRobinArbiter over `valids_vec` starting from
353 inst = rr_arbiter(valids=valids_vec, start=start, instance_name=name)
354 return inst.winner, inst.any_valid
356 grant_u = grant.as_uint(gw)
357 is_last_idx = grant == Bits(gw)(num_inputs - 1)
358 grant_p1 = Mux(is_last_idx, (grant_u + UInt(gw)(1)).as_bits(gw),
361 winner_idle, any_idle = round_robin(ports.valids, rr_ptr,
"rr_idle")
368 valids_next = ports.valids & ~grant_oh
369 winner_next, any_next = round_robin(valids_next, grant_p1,
"rr_next")
371 pick = ~busy & any_idle
372 reend = busy & ports.msg_end
373 grant_if_not_reend = Mux(pick, grant, winner_idle)
374 busy_if_not_reend = Mux(pick, busy, Bits(1)(1))
376 next_grant.assign(Mux(reend, grant_if_not_reend, winner_next))
377 next_busy.assign(Mux(reend, busy_if_not_reend, any_next))
378 rr_ptr.assign(Mux(reend, rr_ptr, grant_p1))
379 ports.switch = pick | (reend & any_next)
381 return RoundRobinControl
385def ChannelArbiterMod(channel_type: Channel, num_inputs: int,
386 output_fifo_depth: int, buffer_inputs: bool,
387 telemetry: bool, mux_pipeline_levels: Optional[int],
388 pipelined_scheduler: bool, grant_queue_depth: int):
389 """Build a pipelined, list-aware N:1 channel multiplexer module. See the
390 `ChannelArbiter` convenience function for the user-facing entry point and
391 `docs/components/ChannelArbiter.md` for the design."""
393 assert num_inputs >= 2,
"ChannelArbiterMod requires at least two inputs"
394 inner = channel_type.inner_type
398 is_window = isinstance(inner, Window)
400 lowered = inner.lowered_type
401 field_names = [n
for n, _
in lowered.fields]
if isinstance(
402 lowered, StructType)
else None
403 if field_names
is None or "last" not in field_names:
405 "ChannelArbiter can only auto-detect list framing for window types "
406 "whose lowered frame is a struct with a 'last' field; got lowered "
407 f
"type {lowered}. (Serial/union-framed windows are not supported.)")
408 width = lowered.bitwidth
410 width = inner.bitwidth
413 f
"ChannelArbiter requires a fixed-width payload; got {inner}")
419 beat_type = Bits(width)
423 gw = clog2(num_inputs)
427 mux_pipeline_levels))
430 pipe_latency = tree_latency + 1
431 if output_fifo_depth
is not None and \
432 output_fifo_depth <= pipe_latency:
434 f
"output_fifo_depth ({output_fifo_depth}) must be > the pipeline "
435 f
"latency ({pipe_latency})")
437 class ChannelArbiterImpl(Module):
445 inputs = Input(Array(channel_type, num_inputs))
446 output = Output(channel_type)
449 def build(ports) -> None:
453 depth = (pipe_latency + ChannelArbiterImpl._SLACK
454 if output_fifo_depth
is None else output_fifo_depth)
455 cw = max(1, depth.bit_length())
459 def flit_last(typed_sig: Signal) -> BitsSignal:
460 """High when 'typed_sig' is the last flit of its message."""
462 return typed_sig.unwrap()[
"last"]
465 def to_bits(typed_sig: Signal) -> BitsSignal:
466 """Bitcast the payload to raw bits for the datapath."""
468 typed_sig = typed_sig.unwrap()
469 return typed_sig.bitcast(Bits(width))
471 def from_bits(bits: BitsSignal) -> Signal:
472 """Reconstruct the payload from raw bits for the output channel."""
474 return inner.wrap(bits.bitcast(inner.lowered_type))
475 return bits.bitcast(inner)
480 grant = Wire(Bits(gw),
"grant")
481 grant_oh = Wire(Bits(num_inputs),
"grant_oh")
482 busy = Wire(Bits(1),
"busy")
483 credit = Reg(UInt(cw), clk, rst, rst_value=depth, name=
"credit")
485 credit_gt0 = credit > UInt(cw)(0)
488 valids: List[BitsSignal] = []
489 last_bits: List[BitsSignal] = []
490 data_bits: List[BitsSignal] = []
491 for i
in range(num_inputs):
492 chan = ports.inputs[i]
494 chan = chan.buffer(clk, rst, stages=1)
497 ready_i = busy & grant_oh[i] & credit_gt0
498 data_i, valid_i = chan.unwrap(ready_i)
499 valids.append(valid_i)
500 last_bits.append(flit_last(data_i))
501 data_bits.append(to_bits(data_i))
504 sel_valid = Mux(grant, *valids)
505 sel_last = Mux(grant, *last_bits)
507 sel_bits = Bits(0)(0)
509 sel_bits =
_select_mux(grant, data_bits, clk, rst, mux_pipeline_levels)
513 launch = busy & sel_valid & credit_gt0
514 msg_end = launch & sel_last
522 out_valid = credit < UInt(cw)(depth)
523 payload_bits = Bits(0)(0)
529 for _
in range(tree_latency):
530 pipe_valid = pipe_valid.reg(clk, rst)
531 pipe_valid = pipe_valid.reg(clk, rst, name=
"pipe_valid")
532 pipe_beat = sel_bits.reg(clk, rst, name=
"pipe_beat")
533 fifo = SeqFIFO(beat_type, depth, clk, rst)
534 fifo.push(pipe_beat, pipe_valid)
535 fifo_pop = Wire(Bits(1),
"arb_pop")
536 out_valid = ~fifo.empty
537 payload_bits = fifo.pop(fifo_pop)
539 out_chan, out_ready = channel_type.wrap(from_bits(payload_bits),
541 ports.output = out_chan
542 pop = out_valid & out_ready
543 if fifo_pop
is not None:
547 next_credit = ((credit + pop.as_uint(cw)).as_uint(cw) -
548 launch.as_uint(cw)).as_uint(cw)
549 credit.assign(next_credit)
556 ctrl = ctrl_mod(clk=clk,
558 valids=BitsSignal.concat(list(reversed(valids))),
561 instance_name=
"arb_ctrl")
562 grant.assign(ctrl.grant)
563 grant_oh.assign(ctrl.grant_oh)
564 busy.assign(ctrl.busy)
565 arb_switch = ctrl.switch
569 Telemetry.report_signal(clk, rst, AppID(
"selectedChannel"), grant)
570 Telemetry.report_signal(clk, rst, AppID(
"busy"), busy)
572 for i
in range(num_inputs):
573 served = Counter(64)(clk=clk,
576 increment=launch & grant_oh[i])
577 Telemetry.report_signal(clk, rst, AppID(f
"grantCount_{i}"),
580 total_flits = Counter(64)(clk=clk,
584 Telemetry.report_signal(clk, rst, AppID(
"totalFlits"), total_flits.out)
585 total_msgs = Counter(64)(clk=clk,
589 Telemetry.report_signal(clk, rst, AppID(
"totalMessages"),
591 arb_switches = Counter(64)(clk=clk,
594 increment=arb_switch)
595 Telemetry.report_signal(clk, rst, AppID(
"arbSwitches"),
599 cur_len = Counter(32)(clk=clk, rst=rst, clear=msg_end, increment=launch)
600 msg_len = (cur_len.out + UInt(32)(1)).as_uint(32)
601 max_len = Reg(UInt(32), clk, rst, rst_value=0, name=
"max_list_len")
602 is_new_max = msg_end & (msg_len > max_len)
603 max_len.assign(Mux(is_new_max, max_len, msg_len))
604 Telemetry.report_signal(clk, rst, AppID(
"maxListLen"), max_len)
607 occ = (UInt(cw)(depth) - credit).as_uint(cw)
608 inflight_hw = Reg(UInt(cw), clk, rst, rst_value=0, name=
"inflight_hw")
609 is_new_hw = occ > inflight_hw
610 inflight_hw.assign(Mux(is_new_hw, inflight_hw, occ))
611 Telemetry.report_signal(clk, rst, AppID(
"inflightHighWater"),
614 return ChannelArbiterImpl
617def ChannelArbiter(input_channels: List[ChannelSignal],
621 appid: Optional[AppID] =
None,
622 output_fifo_depth: Optional[int] =
None,
623 buffer_inputs: bool =
True,
624 mux_pipeline_levels: Optional[int] =
None,
625 pipelined_scheduler: bool =
False,
626 grant_queue_depth: int = 4,
627 telemetry: bool =
True) -> ChannelSignal:
628 """Build a pipelined, list-aware N:1 channel multiplexer.
630 Unlike the combinational `pycde.esi.ChannelMux`, this is a flat registered
631 round-robin arbiter with a feed-forward output stage (output register + FIFO
632 + credit counter), so it closes timing at high fan-in. It also keeps
633 multi-flit list messages contiguous: once an input is granted, it holds the
634 output until a flit whose 'last' field is set has been transferred. List
635 framing is auto-detected from the channel type (window payloads with a 'last'
636 field); all other payloads are treated as single-flit messages.
639 input_channels: the channels to multiplex. All must share the same
641 clk, rst: clock and reset.
642 appid: optional `AppID` for the arbiter instance (e.g. to address it or to
643 disambiguate its telemetry in the appid hierarchy).
644 output_fifo_depth: depth of the output FIFO; must be greater than the
645 pipeline latency (one output register plus any selection-mux pipeline
646 latency). Defaults to that plus a small internal slack.
647 buffer_inputs: insert a per-input skid buffer to localize backpressure.
648 mux_pipeline_levels: if set, build the N:1 data-selection mux as an explicit
649 binary tree and insert a pipeline register after every this-many tree
650 levels (1 = register every level). This retimes the wide selection mux
651 for very large fan-in; the added latency is absorbed by the output FIFO /
652 credit counter. `None` (default) uses a flat combinational mux.
653 pipelined_scheduler: decouple grant selection from the datapath using a
654 grant queue fed by a sweep scheduler, instead of re-arbitrating
655 combinationally at each message end. This takes the round-robin tree out
656 of the single-cycle `grant -> grant` loop, which is the Fmax limiter at
657 high fan-in. Changes the service order (see `GrantSchedulerMod`).
658 grant_queue_depth: depth of that grant queue -- how many grant decisions
659 may be committed ahead of the datapath. Must be >= 2: a single entry
660 cannot keep the datapath fed back to back, so every message would cost a
661 refill bubble. This is not a fairness knob; a newly-valid input's wait
662 also scales with the number of concurrently active inputs (see
663 `GrantSchedulerMod`).
664 telemetry: emit telemetry (selected channel, list-length stats, etc.).
666 See `docs/components/ChannelArbiter.md`."""
668 assert len(input_channels) > 0
669 num_inputs = len(input_channels)
671 return input_channels[0]
673 channel_type = input_channels[0].type
674 for c
in input_channels:
675 if c.type != channel_type:
676 raise TypeError(
"All ChannelArbiter inputs must have the same type; got "
677 f
"{channel_type} and {c.type}")
678 if channel_type.signaling != ChannelSignaling.ValidReady:
679 raise TypeError(
"ChannelArbiter requires ValidReady channels; got "
682 if mux_pipeline_levels
is not None and mux_pipeline_levels < 1:
684 f
"mux_pipeline_levels must be >= 1, got {mux_pipeline_levels}")
692 if pipelined_scheduler
and grant_queue_depth < 2:
693 raise ValueError(f
"grant_queue_depth must be >= 2, got {grant_queue_depth}")
695 mod = ChannelArbiterMod(channel_type, num_inputs, output_fifo_depth,
696 buffer_inputs, telemetry, mux_pipeline_levels,
697 pipelined_scheduler, grant_queue_depth)
698 inputs_array = Array(channel_type, num_inputs)(input_channels)
699 inst = mod(clk=clk, rst=rst, inputs=inputs_array, appid=appid)