CIRCT 24.0.0git
Loading...
Searching...
No Matches
common.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5from __future__ import annotations
6from math import ceil
7
8from pycde.common import Clock, Input, InputChannel, Output, OutputChannel, Reset
9from pycde.constructs import (AssignableSignal, ControlReg, Counter, Mux,
10 NamedWire, Reg, Wire)
11from pycde import esi
12from pycde.module import Module, generator, modparams
13from pycde.signals import BitsSignal, ChannelSignal, StructSignal
14from pycde.support import clog2
15from pycde.system import System
16from pycde.types import (Array, Bits, Bundle, BundledChannel, Channel,
17 ChannelDirection, StructType, Type, UInt, Window)
18
19from pycde.seq import FIFO as SeqFIFO
20from ..components import ChannelArbiter, MaxOutstandingLimiter
21
22from typing import Callable, Dict, List, Optional, Tuple
23import typing
24
25MagicNumber = 0x207D98E5_E5100E51 # random + ESI__ESI
26VersionNumber = 1 # Keep in sync with circt::esi::esiApiVersion.
27
28IndirectionMagicNumber = 0x312bf0cc_E5100E51 # random + ESI__ESI
29IndirectionVersionNumber = 0 # Version 0: format subject to change
30
31# Magic value which, when written by the host to header slot 7, requests a
32# design reset. Keep in sync with 'ResetMagicNumber' in the runtime
33# (cpp/include/esi/Accelerator.h). This magic number guards against "write
34# spraying" which other devices have been know to do on boot.
35ResetMagicNumber = 0x00000E510000B007
36# Number of cycles to wait after a reset is requested before asserting it. This
37# gives in-flight transactions time to drain.
38ResetCycles = 8192
39
40MMIOWordBytes = esi.MMIODataType.width // 8
41
42
44 """Module which will be created later by CIRCT which will contain the
45 compressed manifest."""
46
47 module_name = "__ESI_Manifest_ROM"
48
49 clk = Clock()
50 address = Input(Bits(29))
51 # Data is two cycles delayed after address changes.
52 data = Output(Bits(64))
53
54
56 """Wrap the manifest ROM with ESI bundle."""
57
58 clk = Clock()
59 read = Input(esi.MMIO.read.type)
60
61 @generator
62 def build(self):
63 data, data_valid = Wire(Bits(64)), Wire(Bits(1))
64 data_chan, data_ready = Channel(Bits(64)).wrap(data, data_valid)
65 address_chan = self.read.unpack(data=data_chan)['offset']
66 address, address_valid = address_chan.unwrap(data_ready)
67 address_words = address.as_bits(32)[3:] # Lop off the lower three bits.
68
69 rom = ESI_Manifest_ROM(clk=self.clk, address=address_words)
70 data.assign(rom.data)
71 data_valid.assign(address_valid.reg(self.clk, name="data_valid", cycles=2))
72
73
74@modparams
75def HeaderMMIO(manifest_loc: int) -> Module:
76
77 class HeaderMMIO(Module):
78 """Construct the ESI header MMIO adhering to the MMIO layout specified in
79 the ChannelMMIO service implementation."""
80
81 clk = Clock()
82 rst = Reset()
83 read = Input(esi.MMIO.read_write.type)
84 # Asserted for one cycle when the host writes the reset magic number to
85 # header slot 7. Propagates up to the BSP which performs the actual reset.
86 reset_request = Output(Bits(1))
87
88 @generator
89 def build(ports):
90 clk = ports.clk
91 rst = ports.rst
92 data_chan_wire = Wire(Channel(esi.MMIODataType))
93 input_bundles = ports.read.unpack(data=data_chan_wire)
94 cmd_chan = input_bundles['cmd']
95
96 # Two-stage half-throughput pipeline: stage 1 captures the incoming
97 # command, stage 2 holds the looked-up response. Each stage carries its
98 # own occupancy bit.
99 cmd_ready = Wire(Bits(1))
100 s1_to_s2_xact = Wire(Bits(1))
101 cmd_raw, cmd_valid = cmd_chan.unwrap(cmd_ready)
102
103 # Stage 1: command capture register and occupancy bit.
104 s1_load = cmd_valid & cmd_ready
105 cmd = cmd_raw.reg(clk, rst, ce=s1_load, name="cmd")
106 s1_valid = ControlReg(clk,
107 rst,
108 asserts=[s1_load],
109 resets=[s1_to_s2_xact],
110 name="s1_valid")
111 # Accept a new command when stage 1 is empty.
112 cmd_ready.assign(~s1_valid)
113
114 address_words = cmd.offset.as_bits()[3:] # Lop off the lower three bits.
115 slot = address_words[:3]
116
117 cycles = Counter(64)(clk=ports.clk,
118 rst=ports.rst,
119 clear=Bits(1)(0),
120 increment=Bits(1)(1),
121 instance_name="cycle_counter")
122
123 # Layout the header as an array.
124 core_freq = System.current().core_freq
125 if core_freq is None:
126 core_freq = 0
127 header = Array(Bits(64), 8)([
128 0, # Generally a good idea to not use address 0.
129 MagicNumber, # ESI magic number.
130 VersionNumber, # ESI version number.
131 manifest_loc, # Absolute address of the manifest ROM.
132 0, # Reserved for future use.
133 cycles.out.as_bits(), # Cycle counter.
134 core_freq, # Core frequency, if known.
135 0, # Slot 7: write the reset magic number here to request a reset.
136 ])
137 header.name = "header"
138
139 # Stage 2: registered response value and its occupancy bit.
140 s2_valid = Wire(Bits(1))
141 data_chan_ready = Wire(Bits(1))
142 s2_xact = s2_valid & data_chan_ready
143 # Stage 1 advances into stage 2 only when stage 2 is empty.
144 s1_to_s2_xact.assign(s1_valid & ~s2_valid)
145
146 header_out = header[slot].reg(clk=clk,
147 rst=rst,
148 ce=s1_to_s2_xact,
149 name="header_out")
150 s2_valid.assign(
151 ControlReg(clk,
152 rst,
153 asserts=[s1_to_s2_xact],
154 resets=[s2_xact],
155 name="header_out_valid"))
156 # Wrap the response.
157 data_chan, data_chan_ready_sig = Channel(esi.MMIODataType).wrap(
158 header_out, s2_valid)
159 data_chan_wire.assign(data_chan)
160 data_chan_ready.assign(data_chan_ready_sig)
161
162 # Detect a write of the reset magic number to slot 7. Register the request
163 # so it is a clean one-cycle pulse, asserted as the command advances into
164 # the response stage. 'DesignResetController' latches it, so a single-cycle
165 # pulse is sufficient to trigger the reset.
166 reset_detect = (cmd.write & (slot == Bits(3)(7)) &
167 (cmd.data == Bits(64)(ResetMagicNumber)))
168 ports.reset_request = reset_detect & s1_to_s2_xact
169
170 return HeaderMMIO
171
172
173@modparams
175 data_type: Type, num_outs: int,
176 next_sel_width: int) -> type["ChannelDemuxNImpl"]:
177 """N-way channel demultiplexer for valid/ready signaling. Contains
178 valid/ready registers on the output channels. The selection signal is now
179 embedded in the input channel payload as a struct {sel, data}. Input
180 signals ready when the selected output register is empty."""
181
182 assert num_outs >= 1, "num_outs must be at least 1."
183
184 class ChannelDemuxNImpl(Module):
185 clk = Clock()
186 rst = Reset()
187
188 # Input channel now carries selection along with data.
189 InPayloadType = StructType([
190 ("sel", Bits(clog2(num_outs))),
191 ("next_sel", Bits(next_sel_width)),
192 ("data", data_type),
193 ])
194 inp = Input(Channel(InPayloadType))
195 OutPayloadType = StructType([
196 ("next_sel", Bits(next_sel_width)),
197 ("data", data_type),
198 ])
199 # Outputs are channels of OutPayloadType, which includes both 'next_sel' and 'data' fields.
200 for i in range(num_outs):
201 locals()[f"output_{i}"] = Output(Channel(OutPayloadType))
202
203 @generator
204 def generate(ports) -> None:
205 # Half-stage demux: one register per output channel. Input is ready
206 # when the currently selected output register is empty (not valid).
207 clk = ports.clk
208 rst = ports.rst
209 sel_width = clog2(num_outs)
210
211 # Unwrap input with backpressure from selected output register.
212 input_ready = Wire(Bits(1), name="input_ready")
213 in_payload, in_valid = ports.inp.unwrap(input_ready)
214 in_sel = in_payload.sel
215 in_next_sel = in_payload.next_sel
216 in_data = in_payload.data
217
218 # Track per-output valid regs and build a purely combinational
219 # expression 'selected_valid_expr' = OR_i((sel==i)&valid_i). Avoid
220 # assigning to a Wire multiple times.
221 valid_regs: List[BitsSignal] = []
222 selected_valid_expr = Bits(1)(0)
223
224 for i in range(num_outs):
225 # Write when input transaction targets this output and output not holding data yet.
226 will_write = Wire(Bits(1), name=f"will_write_{i}")
227 write_cond = (in_valid & input_ready & (in_sel == Bits(sel_width)(i)))
228 will_write.assign(write_cond)
229
230 # Data and next_sel registers.
231 out_msg_reg = ChannelDemuxNImpl.OutPayloadType({
232 "next_sel": in_next_sel,
233 "data": in_data
234 }).reg(clk=clk, rst=rst, ce=will_write, name=f"out{i}_msg_reg")
235
236 # Valid register cleared on successful downstream consume.
237 consume = Wire(Bits(1), name=f"consume_{i}")
238 valid_reg = ControlReg(
239 clk=clk,
240 rst=rst,
241 asserts=[will_write],
242 resets=[consume],
243 name=f"out{i}_valid_reg",
244 )
245 valid_regs.append(valid_reg)
246
247 # Channel wrapper.
248 ch_sig, ch_ready = Channel(ChannelDemuxNImpl.OutPayloadType).wrap(
249 out_msg_reg, valid_reg)
250 setattr(ports, f"output_{i}", ch_sig)
251 consume.assign(valid_reg & ch_ready)
252
253 # Accumulate selected_valid expression.
254 selected_valid_expr = selected_valid_expr | (
255 (in_sel == Bits(sel_width)(i)) & valid_reg)
256
257 # Input ready only when selected output has no valid data latched.
258 input_ready.assign(selected_valid_expr ^ Bits(1)(1))
259
260 def get_out(self, index: int) -> ChannelSignal:
261 return getattr(self, f"output_{index}")
262
263 return ChannelDemuxNImpl
264
265
266@modparams
268 data_type: Type, num_outs: int,
269 branching_factor_log2: int) -> type["ChannelDemuxTree"]:
270 """Pipelined N-way channel demultiplexer for valid/ready signaling. This
271 implementation uses a tree structure of
272 ChannelDemuxN_HalfStage_ReadyBlocking modules to reduce fanout pressure.
273 Supports maximum half-throughput to save complexity and area.
274 """
275
276 root_sel_width = clog2(num_outs)
277 # Simplify algorithm by making sure num_outs is a power of two.
278 num_outs = 2**root_sel_width
279 sel_width = branching_factor_log2
280 fanout = 2**sel_width
281
282 class ChannelDemuxTree(Module):
283 clk = Clock()
284 rst = Reset()
285 # Input now embeds selection bits alongside data.
286 InPayloadType = StructType([
287 ("sel", Bits(clog2(num_outs))),
288 ("data", data_type),
289 ])
290 inp = Input(Channel(InPayloadType))
291
292 # Outputs (data only).
293 for i in range(num_outs):
294 locals()[f"output_{i}"] = Output(Channel(data_type))
295
296 @generator
297 def build(ports) -> None:
298 assert branching_factor_log2 > 0
299 if num_outs == 1:
300 # Strip selection bits and return single channel.
301 setattr(ports, "output_0", ports.inp.transform(lambda p: p.data))
302 return
303
304 def payload_type(sel_width: int, next_sel_width: int) -> Type:
305 return StructType([
306 ("sel", Bits(sel_width)),
307 ("next_sel", Bits(next_sel_width)),
308 ("data", data_type),
309 ])
310
311 def next_sel_width_calc(curr_sel_width) -> int:
312 return max(curr_sel_width - sel_width, 0)
313
314 def payload_next(curr_msg: StructSignal) -> StructSignal:
315 """Given current level payload, produce next level payload by
316 stripping off the top selection bits."""
317
318 next_sel_width = next_sel_width_calc(curr_msg.next_sel.type.width)
319 curr_sel_width = curr_msg.next_sel.type.width
320 new_sel_width = min(curr_sel_width, sel_width)
321 return payload_type(
322 new_sel_width,
323 next_sel_width,
324 )({
325 # Use the MSB bits of next_sel as the next level selection.
326 "sel": (curr_msg.next_sel[next_sel_width:]
327 if curr_sel_width > 0 else Bits(0)(0)),
328 "next_sel": (curr_msg.next_sel[:next_sel_width]
329 if next_sel_width > 0 else Bits(0)(0)),
330 "data": curr_msg.data,
331 })
332
333 current_channels: List[ChannelSignal] = [
334 ports.inp.transform(lambda m: payload_type(0, root_sel_width)({
335 "sel": Bits(0)(0),
336 "next_sel": m.sel,
337 "data": m.data,
338 }))
339 ]
340
341 curr_sel_width = root_sel_width
342 level = 0
343 while len(current_channels) < num_outs:
344 next_level: List[ChannelSignal] = []
345 level_num_outs = min(2**curr_sel_width, fanout)
346 for i, c in enumerate(current_channels):
348 data_type,
349 num_outs=level_num_outs,
350 next_sel_width=next_sel_width_calc(curr_sel_width),
351 )(
352 clk=ports.clk,
353 rst=ports.rst,
354 inp=c.transform(payload_next),
355 instance_name=f"demux_l{level}_i{i}",
356 )
357 for j in range(level_num_outs):
358 next_level.append(dmux.get_out(j))
359 current_channels = next_level
360 curr_sel_width -= sel_width
361 level += 1
362
363 for i in range(num_outs):
364 # Strip off next_sel bits for final output.
365 setattr(
366 ports,
367 f"output_{i}",
368 current_channels[i].transform(lambda p: p.data),
369 )
370
371 def get_out(self, index: int) -> ChannelSignal:
372 return getattr(self, f"output_{index}")
373
374 return ChannelDemuxTree
375
376
378 regions: Tuple[Tuple[int, Optional[int]],
379 ...]) -> type["MMIOPrefixRouterImpl"]:
380 """Build a pipelined address-prefix tree which routes MMIO commands.
381
382 Each ``(base, size)`` must describe a disjoint, power-of-two-aligned block.
383 The last region may pass ``size=None`` to claim every address at or above its
384 base; the manifest uses this since its size isn't known during generation.
385 Internal nodes test one address bit and register both outgoing paths. Leaves
386 replace the global address with the block-local low bits, so no subtractor is
387 needed (the open-ended region is the one exception).
388
389 For four 0x100-byte regions at 0x0/0x100/0x200/0x300 plus an open-ended
390 region at 0x400: the open-ended region is peeled off first, then each level
391 splits the remaining candidates in half, so depth is ``O(log N)`` rather than
392 one level per region::
393
394 cmd ──▶ addr[31:10]≠0 ──1──▶ @0x400 (open ended, offset -= 0x400)
395 │
396 0
397 â–¼
398 addr[9]
399 ╱ ╲
400 0 1
401 â–¼ â–¼
402 addr[8] addr[8]
403 ╱ ╲ ╱ ╲
404 0 1 0 1
405 â–¼ â–¼ â–¼ â–¼
406 @0x0 @0x100 @0x200 @0x300
407
408 The four leaves all take their client-local offset as ``addr[7:0]``.
409 """
410
411 assert len(regions) > 1, "MMIO routing requires at least two regions"
412
413 # An open-ended final region matches anything with a bit set above the sized
414 # regions, so it needs no size and can never overflow its window.
415 open_ended_base: Optional[int] = None
416 sized_regions = regions
417 if regions[-1][1] is None:
418 open_ended_base = regions[-1][0]
419 assert open_ended_base > 0 and \
420 open_ended_base & (open_ended_base - 1) == 0, \
421 "an open-ended MMIO region must start at a power-of-two address"
422 sized_regions = regions[:-1]
423
424 for base, size in sized_regions:
425 assert size > 0 and size & (size - 1) == 0, \
426 "MMIO region sizes must be powers of two"
427 assert base % size == 0, "MMIO regions must be aligned to their size"
428 assert all(
429 a_base + a_size <= b_base
430 for (a_base, a_size), (b_base, _) in zip(sized_regions, regions[1:])), \
431 "MMIO regions must be sorted and disjoint"
432
433 entries = [(idx, base, size) for idx, (base, size) in enumerate(sized_regions)
434 ]
435
436 def build_tree(node_entries):
437 if len(node_entries) == 1:
438 return node_entries[0][0]
439
440 lowest_fixed_bit = max(size.bit_length() - 1 for _, _, size in node_entries)
441 candidates = []
442 for bit in range(31, lowest_fixed_bit - 1, -1):
443 zeros = [entry for entry in node_entries if not (entry[1] >> bit) & 1]
444 if zeros and len(zeros) != len(node_entries):
445 candidates.append(
446 (abs(len(node_entries) - 2 * len(zeros)), -bit, bit, zeros))
447 assert candidates, "unable to distinguish disjoint MMIO regions"
448 _, _, bit, zeros = min(candidates)
449 zero_indices = {entry[0] for entry in zeros}
450 ones = [entry for entry in node_entries if entry[0] not in zero_indices]
451 return bit, build_tree(zeros), build_tree(ones)
452
453 tree = build_tree(entries)
454
455 class MMIOPrefixRouterImpl(Module):
456 clk = Clock()
457 rst = Reset()
458 inp = Input(Channel(esi.MMIOReadWriteCmdType))
459 for idx in range(len(regions)):
460 locals()[f"output_{idx}"] = Output(Channel(esi.MMIOReadWriteCmdType))
461
462 @generator
463 def build(ports):
464 """Route through one registered address-bit branch per tree level."""
465
466 def new_demux(command_channel: ChannelSignal, select, name: str):
467 Demux = ChannelDemuxN_HalfStage_ReadyBlocking(esi.MMIOReadWriteCmdType,
468 num_outs=2,
469 next_sel_width=0)
470 demux_input = command_channel.transform(
471 lambda cmd, _sel=select, _type=Demux.InPayloadType: _type({
472 "sel": _sel(cmd),
473 "next_sel": Bits(0)(0),
474 "data": cmd
475 }))
476 return Demux(clk=ports.clk,
477 rst=ports.rst,
478 inp=demux_input,
479 instance_name=name)
480
481 def route(node, command_channel: ChannelSignal, path: str):
482 if isinstance(node, int):
483 _, size = regions[node]
484 local_width = size.bit_length() - 1
485
486 def localize(cmd):
487 local_offset = cmd.offset.as_bits()[:local_width].pad_or_truncate(
488 32).as_uint()
489 return esi.MMIOReadWriteCmdType({
490 "write": cmd.write,
491 "offset": local_offset,
492 "data": cmd.data
493 })
494
495 setattr(ports, f"output_{node}", command_channel.transform(localize))
496 return
497
498 bit, zero_node, one_node = node
499 demux = new_demux(command_channel,
500 lambda cmd, _bit=bit: cmd.offset.as_bits()[_bit],
501 f"prefix_{path}_bit{bit}")
502 route(zero_node,
503 demux.get_out(0).transform(lambda p: p.data), path + "0")
504 route(one_node,
505 demux.get_out(1).transform(lambda p: p.data), path + "1")
506
507 client_channel = ports.inp
508 if open_ended_base is not None:
509 # Peel off the open-ended region first: any address bit above the sized
510 # regions selects it. Its base isn't a bit slice away from the local
511 # offset (the region is unbounded), so subtract it explicitly.
512 split_bit = open_ended_base.bit_length() - 1
513 split = new_demux(
514 ports.inp, lambda cmd: cmd.offset.as_bits()[split_bit:].or_reduce(),
515 f"prefix_above_bit{split_bit}")
516
517 def localize_open_ended(payload):
518 cmd = payload.data
519 local_offset = (cmd.offset - UInt(32)(open_ended_base)).as_uint(32)
520 return esi.MMIOReadWriteCmdType({
521 "write": cmd.write,
522 "offset": local_offset,
523 "data": cmd.data
524 })
525
526 setattr(ports, f"output_{len(regions) - 1}",
527 split.get_out(1).transform(localize_open_ended))
528 client_channel = split.get_out(0).transform(lambda p: p.data)
529
530 route(tree, client_channel, "")
531
532 def get_out(self, index: int) -> ChannelSignal:
533 return getattr(self, f"output_{index}")
534
535 return MMIOPrefixRouterImpl
536
537
538@modparams
540 delay_cycles: int) -> type["DesignResetControllerImpl"]:
541 """Counts `delay_cycles` clock cycles after a reset request is observed, then
542 asserts `design_reset` for one cycle. This module must be driven by the
543 *external* reset only (not the reset it generates) so that the countdown is
544 not disturbed by the reset it produces.
545
546 `reset_pending` is asserted from the moment a reset is requested until it
547 fires. It is intended to be used to quiesce the design (e.g. stop accepting
548 new transactions) so that nothing is in flight when the reset is asserted."""
549
550 if delay_cycles < 1:
551 raise ValueError("'delay_cycles' must be at least 1.")
552
553 counter_width = max(clog2(delay_cycles), 1)
554
555 class DesignResetControllerImpl(Module):
556 clk = Clock()
557 rst = Reset()
558 reset_request = Input(Bits(1))
559 design_reset = Output(Bits(1))
560 # High from the cycle a reset is requested until it fires. Use this to stop
561 # accepting new work so in-flight transactions can drain before the reset.
562 reset_pending = Output(Bits(1))
563
564 @generator
565 def build(ports):
566 fire = Wire(Bits(1))
567 # Latch that a reset has been requested until we fire the reset.
568 pending = ControlReg(clk=ports.clk,
569 rst=ports.rst,
570 asserts=[ports.reset_request],
571 resets=[fire],
572 name="reset_pending")
573 # Count cycles while a reset is pending.
574 count = Counter(counter_width)(clk=ports.clk,
575 rst=ports.rst,
576 clear=fire | ~pending,
577 increment=pending,
578 instance_name="reset_delay_counter")
579 fire.assign(pending &
580 (count.out == UInt(counter_width)(delay_cycles - 1)))
581 ports.design_reset = fire
582 ports.reset_pending = pending
583
584 return DesignResetControllerImpl
585
586
587class ChannelMMIO(esi.ServiceImplementation):
588 """MMIO service implementation with MMIO bundle interfaces. Should be
589 relatively easy to adapt to physical interfaces by wrapping the wires to
590 channels then bundles. Allows the implementation to be shared and (hopefully)
591 platform independent.
592
593 Whether or not to support unaligned accesses is up to the clients. The header
594 and manifest do not support unaligned accesses and throw away the lower three
595 bits.
596
597 Only allows one outstanding request at a time. This is enforced in hardware
598 by a `MaxOutstandingLimiter` on the command channel, which stalls incoming
599 commands until the previous response has been consumed. If a client fails to
600 return a response, the MMIO service will hang. TODO: add some kind of
601 timeout.
602
603 Implementation-defined MMIO layout:
604 - 0x0: 0 constant
605 - 0x8: Magic number (0x207D98E5_E5100E51)
606 - 0x10: ESI version number (1)
607 - 0x18: Location of the manifest ROM (absolute address)
608
609 - 0x100: Start of MMIO space for requests. Mapping is contained in the
610 manifest so can be dynamically queried.
611
612 - Directly above the last client allocation (rounded up to a power of two):
613 Start of the manifest ROM. It matches any address with a bit set above
614 the client space, so it is never truncated by its window.
615 - addr(Manifest ROM) + 0: Size of compressed manifest
616 - addr(Manifest ROM) + 8: Start of compressed manifest
617
618 This layout _should_ be pretty standard, but different BSPs may have various
619 different restrictions. Any BSP which uses this service implementation will
620 have this layout, possibly with an offset or address window.
621 """
622
623 clk = Clock()
624 rst = Input(Bits(1))
625
626 cmd = Input(esi.MMIO.read_write.type)
627
628 # Asserted for one cycle when the host requests a design reset via an MMIO
629 # write to the header. Propagates up to the BSP which performs the reset.
630 reset_request = Output(Bits(1))
631
632 # Amount of MMIO space a client gets when it doesn't request a size. Must be
633 # at least large enough for the header block, which uses eight 64-bit slots.
634 DefaultRegionSpace = 0x100
635
636 # Start at this address for assigning MMIO addresses to service requests.
637 initial_offset: int = DefaultRegionSpace
638
639 @generator
640 def generate(ports, bundles: esi._ServiceGeneratorBundles):
641 table, manifest_loc = ChannelMMIO.build_table(bundles)
642 ChannelMMIO.build_read(ports, manifest_loc, table)
643 return True
644
645 @staticmethod
647 bundles) -> Tuple[Dict[int, Tuple[Optional[int], AssignableSignal]], int]:
648 """Build a table of read and write addresses to BundleSignals."""
649
650 def align(value: int, alignment: int) -> int:
651 return (value + alignment - 1) // alignment * alignment
652
653 offset = ChannelMMIO.initial_offset
654 table: Dict[int, Tuple[Optional[int], AssignableSignal]] = {}
655 for bundle in bundles.to_client_reqs:
656 requested_size = bundle.options.get("size",
657 ChannelMMIO.DefaultRegionSpace)
658 if isinstance(requested_size,
659 bool) or not isinstance(requested_size, int):
660 raise ValueError("MMIO request option 'size' must be an integer")
661 if requested_size <= 0:
662 raise ValueError("MMIO request option 'size' must be positive")
663 # Round the allocation up to a power of two and align the base to it so
664 # that the address decode is a pure prefix match and the client-local
665 # offset is a bit slice.
666 size = 1 << (max(requested_size, MMIOWordBytes) - 1).bit_length()
667 offset = align(offset, size)
668 next_offset = offset + size
669 if next_offset >= 1 << 32:
670 raise ValueError("MMIO address allocation exceeds the 32-bit space")
671
672 if bundle.port == 'read':
673 table[offset] = size, bundle
674 bundle.add_record(details={
675 "offset": offset,
676 "size": size,
677 "type": "ro"
678 })
679 elif bundle.port == 'read_write':
680 table[offset] = size, bundle
681 bundle.add_record(details={
682 "offset": offset,
683 "size": size,
684 "type": "rw"
685 })
686 else:
687 raise ValueError(f"Unrecognized MMIO port name: {bundle.port}")
688 offset = next_offset
689
690 # The manifest sits directly above the clients and claims every address
691 # with a bit set above them. Its size isn't known until CIRCT builds the
692 # manifest ROM, so leaving the region open-ended keeps it from overflowing
693 # while letting the BAR be only as large as the manifest actually needs.
694 manifest_loc = 1 << max(offset - 1, 0).bit_length()
695 if manifest_loc >= 1 << 32:
696 raise ValueError(
697 "MMIO address allocation leaves no room for the manifest")
698 return table, manifest_loc
699
700 @staticmethod
701 def build_read(ports, manifest_loc: int,
702 table: Dict[int, Tuple[Optional[int], AssignableSignal]]):
703 """Builds the read side of the MMIO service."""
704
705 # Instantiate the header and manifest ROM. Fill in the read_table with
706 # bundle wires to be assigned identically to the other MMIO clients.
707 header_bundle_wire = Wire(esi.MMIO.read_write.type)
708 table[0] = ChannelMMIO.DefaultRegionSpace, header_bundle_wire
709 header = HeaderMMIO(manifest_loc)(clk=ports.clk,
710 rst=ports.rst,
711 read=header_bundle_wire)
712
713 mani_bundle_wire = Wire(esi.MMIO.read.type)
714 # 'None' size: the manifest claims every address above the clients.
715 table[manifest_loc] = None, mani_bundle_wire
716 ESI_Manifest_ROM_Wrapper(clk=ports.clk, read=mani_bundle_wire)
717
718 # Unpack the cmd bundle.
719 data_resp_channel = Wire(Channel(esi.MMIODataType))
720 counted_output = Wire(Channel(esi.MMIODataType))
721 cmd_channel = ports.cmd.unpack(data=counted_output)["cmd"]
722 counted_output.assign(data_resp_channel)
723
724 # Enforce the single-outstanding-transaction invariant in hardware: hold
725 # off accepting a new command until the response to the previous command
726 # has been consumed by the host. Snoop the response wire for the
727 # completion pulse.
728 resp_xact, _ = counted_output.snoop_xact()
729 cmd_limiter = MaxOutstandingLimiter(cmd_channel.type.inner_type,
730 max_outstanding=1)(
731 clk=ports.clk,
732 rst=ports.rst,
733 in_=cmd_channel,
734 complete=resp_xact,
735 instance_name="cmd_rate_limiter",
736 )
737 cmd_channel = cmd_limiter.out
738
739 sorted_table = sorted(table.items())
740 command_router = MMIOPrefixRouter(
741 tuple((base, size) for base, (size, _) in sorted_table))(
742 clk=ports.clk,
743 rst=ports.rst,
744 inp=cmd_channel,
745 instance_name="command_router",
746 )
747
748 client_data_channels = []
749 for idx, (_, (_, bundle_wire)) in enumerate(sorted_table):
750 client_cmd_channel = command_router.get_out(idx)
751 bundle_type = bundle_wire.type
752 if bundle_type == esi.MMIO.read.type:
753 offset = client_cmd_channel.transform(lambda cmd: cmd.offset)
754 bundle, bundle_froms = esi.MMIO.read.type.pack(offset=offset)
755 elif bundle_type == esi.MMIO.read_write.type:
756 bundle, bundle_froms = esi.MMIO.read_write.type.pack(
757 cmd=client_cmd_channel)
758 else:
759 assert False, "Unrecognized bundle type."
760 bundle_wire.assign(bundle)
761 client_data_channels.append(bundle_froms["data"])
762 # `cmd_rate_limiter` above caps the design at one outstanding MMIO command,
763 # and `client_cmd_demux` routes that one command to exactly one client. So
764 # provided each client only asserts its response `valid` in reply to a
765 # command it was given -- see `ChannelMergeOneValid` for why that second
766 # half matters, and note it is required by `ChannelMux` too -- at most one
767 # client response is ever valid, and arbitration is unnecessary.
768 resp_channel = esi.ChannelMergeOneValid(client_data_channels, ports.clk,
769 ports.rst)
770 data_resp_channel.assign(resp_channel)
771
772 # The header surfaces a reset request when the host writes the reset magic
773 # number to slot 7. Propagate it up to the caller (the BSP).
774 ports.reset_request = header.reset_request
775
776
777class TelemetryMMIO(esi.ServiceImplementation):
778 """An ESI service implementation which provides telemetry data through an MMIO
779 region. Each client request is assigned a register in the MMIO space. When a
780 read request is received for the assigned address, it gets routed to the
781 assigned client. When a write request is received, it is discarded. The
782 assignment table is stored in the manifest.
783
784 **REQUIREMENTS.** Both are needed to make the response merge safe:
785
786 1. The `MMIO` service implementation this connects to must not issue a read
787 command while a previous read's response is still outstanding.
788 2. Every telemetry client must assert its `data` channel's `valid` only in
789 response to a `get`. `Telemetry.report_signal` does this; a client which
790 holds `valid` high permanently -- legal ESI, and the natural way to
791 express an always-available counter -- does not.
792
793 Given both, each command is demuxed to exactly one client and at most one
794 client is offering a response at a time, so the responses can be merged with
795 `ChannelMergeOneValid` instead of arbitrated -- which keeps the response path
796 from building a combinational cone across every telemetry client.
797
798 Nothing here enforces either, and violating either loses responses. Note (2)
799 is not specific to this merge: `ChannelMux2` is fixed-priority, so under an
800 arbiter a permanently-valid client starves every client behind it."""
801
802 clk = Clock()
803 rst = Reset()
804
805 @generator
806 def generate(ports, bundles: esi._ServiceGeneratorBundles) -> bool:
807 if len(bundles.to_client_reqs) == 0:
808 # No clients to connect to, so we don't need to do anything.
809 return True
810
811 # Assign each telemetry client a register offset in MMIO space.
812 offset = 0
813 table: Dict[int, AssignableSignal] = {}
814 for bundle in bundles.to_client_reqs:
815 # Only support 'report' port for telemetry.
816 if bundle.port == 'report':
817 table[offset] = bundle
818 bundle.add_record(details={"offset": offset, "type": "mmio"})
819 offset += MMIOWordBytes
820 else:
821 raise ValueError(f"Unrecognized port name: {bundle.port}")
822
823 # Request exactly the space the register table occupies rather than relying
824 # on the MMIO service's default allocation.
825 mmio_cmd = esi.MMIO.read_write(esi.AppID("__telemetry_mmio"),
826 options={"size": offset})
827
828 # Unpack the cmd bundle.
829 data_resp_channel = Wire(Channel(esi.MMIODataType), "telemetry_data_resp")
830 counted_output = Wire(Channel(esi.MMIODataType), "telemetry_counted_output")
831 cmd_channel = mmio_cmd.unpack(data=counted_output)["cmd"]
832 counted_output.assign(data_resp_channel)
833
834 # Decode the address to select the client.
835 cmd_ready_wire = Wire(Bits(1), "telemetry_cmd_ready")
836 cmd, cmd_valid = cmd_channel.unwrap(cmd_ready_wire)
837 client_addr_chan, client_addr_ready = Channel(Bits(0)).wrap(
838 Bits(0)(0), cmd_valid)
839 cmd_ready_wire.assign(client_addr_ready)
840
841 # Build the demux/mux and assign the results of each appropriately.
842 read_clients_clog2 = clog2(len(table))
843 chan_sel = cmd.offset.as_bits()[3:read_clients_clog2 + 3]
844 client_cmd_channels = esi.ChannelDemux(
845 sel=chan_sel,
846 input=client_addr_chan,
847 num_outs=len(table),
848 instance_name="telemetry_client_cmd_demux")
849 client_data_channels = []
850 for (idx, offset) in enumerate(sorted(table.keys())):
851 bundle_wire = table[offset]
852 bundle_type = bundle_wire.type
853 # For telemetry, the client expects a 'get' channel and returns 'data'.
854 offset_chan = client_cmd_channels[idx]
855 bundle, bundle_froms = bundle_type.pack(get=offset_chan)
856
857 bundle_wire.assign(bundle)
858 client_data_channels.append(
859 bundle_froms["data"].transform(lambda m: m.as_bits(64)))
860 # The demux above routes each command to exactly one client, so -- given
861 # both requirements in this class' docs -- at most one client is offering a
862 # response at a time and no arbitration is needed to merge them.
863 resp_channel = esi.ChannelMergeOneValid(
864 client_data_channels,
865 ports.clk,
866 ports.rst,
867 instance_name="telemetry_resp_merge")
868 data_resp_channel.assign(resp_channel)
869 return True
870
871
873 """Some platforms do not support MMIO space greater than a certain size (e.g.
874 Vitis 2022's limit is 4k). This module implements a level of indirection to
875 provide access to a full 32-bit address space.
876
877 MMIO addresses:
878 - 0x0: 0 constant
879 - 0x8: 64 bit ESI magic number for Indirect MMIO (0x312bf0cc_E5100E51)
880 - 0x10: Version number for Indirect MMIO (0)
881 - 0x18: Location of read/write in the virtual MMIO space.
882 - 0x20: A read from this location will initiate a read in the virtual MMIO
883 space specified by the address stored in 0x18 and return the result.
884 A write to this location will initiate a write into the virtual MMIO
885 space to the virtual address specified in 0x18.
886 """
887 clk = Clock()
888 rst = Reset()
889
890 upstream = Input(esi.MMIO.read_write.type)
891 downstream = Output(esi.MMIO.read_write.type)
892
893 @generator
894 def build(ports):
895 # This implementation assumes there is only one outstanding upstream MMIO
896 # transaction in flight at once. TODO: enforce this or make it more robust.
897
898 reg_bits = 8
899 location_reg = UInt(reg_bits)(0x18)
900 indirect_mmio_reg = UInt(reg_bits)(0x20)
901 virt_address = Wire(UInt(32))
902
903 # Set up the upstream MMIO interface. Capture last upstream command in a
904 # mailbox which never empties to give access to the last command for all
905 # time.
906 upstream_resp_chan_wire = Wire(Channel(esi.MMIODataType))
907 upstream_cmd_chan = ports.upstream.unpack(
908 data=upstream_resp_chan_wire)["cmd"]
909 _, _, upstream_cmd_data = upstream_cmd_chan.snoop()
910
911 # Set up a channel demux to separate the MMIO commands which get processed
912 # locally with ones which should be transformed and fowarded downstream.
913 phys_loc = upstream_cmd_data.offset.as_uint(reg_bits)
914 fwd_upstream = NamedWire(phys_loc == indirect_mmio_reg, "fwd_upstream")
915 local_reg_cmd_chan, downstream_cmd_channel = esi.ChannelDemux(
916 upstream_cmd_chan, fwd_upstream, 2, "upstream_demux")
917
918 # Set up the downstream MMIO interface.
919 downstream_cmd_channel = downstream_cmd_channel.transform(
920 lambda cmd: esi.MMIOReadWriteCmdType({
921 "write": cmd.write,
922 "offset": virt_address,
923 "data": cmd.data
924 }))
925 ports.downstream, froms = esi.MMIO.read_write.type.pack(
926 cmd=downstream_cmd_channel)
927 downstream_data_chan = froms["data"]
928
929 # Process local regs.
930 (local_reg_cmd_valid, local_reg_cmd_ready,
931 local_reg_cmd) = local_reg_cmd_chan.snoop()
932 write_virt_address = (local_reg_cmd_valid & local_reg_cmd_ready &
933 local_reg_cmd.write & (phys_loc == location_reg))
934 virt_address.assign(
935 local_reg_cmd.data.as_uint(32).reg(
936 name="virt_address",
937 clk=ports.clk,
938 ce=write_virt_address,
939 ))
940
941 # Build the pysical MMIO register space.
942 local_reg_resp_array = Array(Bits(64), 4)([
943 0x0, # 0x0
944 IndirectionMagicNumber, # 0x8
945 IndirectionVersionNumber, # 0x10
946 virt_address.as_bits(64), # 0x18
947 ])
948 local_reg_resp_chan = local_reg_cmd_chan.transform(
949 lambda cmd: local_reg_resp_array[cmd.offset.as_uint(2)])
950
951 # Mux together the local register responses and the downstream data to
952 # create the upstream response.
953 upstream_resp = esi.ChannelMux([local_reg_resp_chan, downstream_data_chan])
954 upstream_resp_chan_wire.assign(upstream_resp)
955
956
957@modparams
958def SliceReadGearbox(input_bitwidth: int,
959 output_bitwidth: int) -> type["SliceReadGearboxImpl"]:
960 """Narrow one engine word to a single-message client element no wider than the
961 word (``OUT <= IN``). The element sits in the word's low bits, so the datapath
962 is a slice; ``valid_bytes`` is unused (a single element is never a partial
963 word). Wider single elements use `ConcatReadGearbox`; packed list reads use
964 `DepackReadGearbox`/`ShiftReadGearbox`."""
965
966 if input_bitwidth <= 0 or input_bitwidth % 8 != 0:
967 raise ValueError("engine word width must be a positive multiple of 8 bits")
968 if not 0 < output_bitwidth <= input_bitwidth:
969 raise ValueError("SliceReadGearbox requires 0 < output <= input")
970
971 in_bytes = input_bitwidth // 8
972 vb_width = max(1, clog2(in_bytes))
973
974 class SliceReadGearboxImpl(Module):
975 clk = Clock()
976 rst = Reset()
977 in_ = InputChannel(
978 StructType([
979 ("tag", esi.HostMem.TagType),
980 ("data", Bits(input_bitwidth)),
981 ("valid_bytes", UInt(vb_width)),
982 ("last", Bits(1)),
983 ]))
984 out = OutputChannel(
985 StructType([
986 ("tag", esi.HostMem.TagType),
987 ("data", Bits(output_bitwidth)),
988 ("last", Bits(1)),
989 ]))
990
991 @generator
992 def build(ports):
993 up_ready = Wire(Bits(1), name="up_ready")
994 up, up_valid = ports.in_.unwrap(up_ready)
995 client_channel, client_ready = SliceReadGearboxImpl.out.type.wrap(
996 {
997 "tag": up.tag,
998 "data": up.data[:output_bitwidth],
999 "last": up.last,
1000 }, up_valid)
1001 up_ready.assign(client_ready)
1002 ports.out = client_channel
1003
1004 return SliceReadGearboxImpl
1005
1006
1007@modparams
1008def ConcatReadGearbox(input_bitwidth: int,
1009 output_bitwidth: int) -> type["ConcatReadGearboxImpl"]:
1010 """Concatenate ``ceil(OUT/IN)`` consecutive engine words into one client
1011 element wider than the word (``OUT > IN``). Serves single-message reads (any
1012 ``OUT > IN``; the low ``OUT`` bits of the concatenation are the element) and
1013 contiguous list reads whose element is a whole number of output_bitwidth
1014 (``OUT % IN == 0``, so elements never straddle). ``valid_bytes`` is unused:
1015 such lists have no partial words and a single element is one flit. Straddling
1016 lists use `ShiftReadGearbox`."""
1017
1018 if input_bitwidth <= 0 or input_bitwidth % 8 != 0:
1019 raise ValueError("engine word width must be a positive multiple of 8 bits")
1020 if output_bitwidth <= input_bitwidth:
1021 raise ValueError("ConcatReadGearbox requires output > input")
1022
1023 in_bytes = input_bitwidth // 8
1024 vb_width = max(1, clog2(in_bytes))
1025
1026 class ConcatReadGearboxImpl(Module):
1027 clk = Clock()
1028 rst = Reset()
1029 in_ = InputChannel(
1030 StructType([
1031 ("tag", esi.HostMem.TagType),
1032 ("data", Bits(input_bitwidth)),
1033 ("valid_bytes", UInt(vb_width)),
1034 ("last", Bits(1)),
1035 ]))
1036 out = OutputChannel(
1037 StructType([
1038 ("tag", esi.HostMem.TagType),
1039 ("data", Bits(output_bitwidth)),
1040 ("last", Bits(1)),
1041 ]))
1042
1043 @generator
1044 def build(ports):
1045 ready_for_upstream = Wire(Bits(1), name="ready_for_upstream")
1046 # Register the input for fmax; the ESI channel buffer keeps the handshake
1047 # elastic.
1048 in_reg = ports.in_.buffer(ports.clk, ports.rst, stages=1)
1049 up, upstream_valid = in_reg.unwrap(ready_for_upstream)
1050 upstream_data = up.data
1051 upstream_last = up.last
1052 upstream_xact = ready_for_upstream & upstream_valid
1053
1054 # Registers accumulate `chunks` upstream words into one client element;
1055 # the output is their concatenation. For a list, elements stream back to
1056 # back and 'last' rides the final word of the burst's final element.
1057 chunks = ceil(output_bitwidth / input_bitwidth)
1058 counter_width = clog2(chunks)
1059 reg_ces = [Wire(Bits(1)) for _ in range(chunks)]
1060 regs = [
1061 upstream_data.reg(ports.clk,
1062 ports.rst,
1063 ce=reg_ces[idx],
1064 name=f"chunk_reg_{idx}") for idx in range(chunks)
1065 ]
1066 client_data_bits = BitsSignal.concat(reversed(regs))[:output_bitwidth]
1067
1068 # Pair-index counter: the word accepted this cycle is written to
1069 # chunk_reg[counter]. 'Counter' clears in preference to incrementing, so
1070 # mask the clear with the accept -- a consume and an accept on the same
1071 # cycle means the accepted word is chunk 0 of the *next* element, so the
1072 # index must land on 1, not 0. 'chunks' need not be a power of two, so
1073 # wrap explicitly rather than relying on the counter's natural rollover.
1074 counter = Wire(UInt(counter_width), name="chunk_counter")
1075 client_xact = Wire(Bits(1))
1076 set_client_valid = counter == UInt(counter_width)(chunks - 1)
1077 counter.assign(
1078 Counter(counter_width)(clk=ports.clk,
1079 rst=ports.rst,
1080 clear=(upstream_xact & set_client_valid) |
1081 (client_xact & ~upstream_xact),
1082 increment=upstream_xact,
1083 instance_name="chunk_counter").out)
1084 client_valid = ControlReg(ports.clk, ports.rst,
1085 [set_client_valid & upstream_xact],
1086 [client_xact])
1087 for idx, reg_ce in enumerate(reg_ces):
1088 reg_ce.assign(upstream_xact & (counter == UInt(counter_width)(idx)))
1089 # 'last' of the final engine word that completes this client flit.
1090 client_last = upstream_last.reg(ports.clk,
1091 ports.rst,
1092 ce=upstream_xact,
1093 name="last_reg")
1094 tag_reg = up.tag.reg(ports.clk,
1095 ports.rst,
1096 ce=upstream_xact,
1097 name="tag_reg")
1098
1099 client_channel, client_ready = ConcatReadGearboxImpl.out.type.wrap(
1100 {
1101 "tag": tag_reg,
1102 "data": client_data_bits,
1103 "last": client_last,
1104 }, client_valid)
1105 client_xact.assign(client_valid & client_ready)
1106 ready_for_upstream.assign(~client_valid | client_ready)
1107 ports.out = client_channel
1108
1109 return ConcatReadGearboxImpl
1110
1111
1112@modparams
1113def DepackReadGearbox(input_bitwidth: int,
1114 output_bitwidth: int) -> type["DepackReadGearboxImpl"]:
1115 """Unpack a byte-aligned element that divides the engine word
1116 (``OUT % 8 == 0`` and ``IN % OUT == 0``) from a contiguous list response. Each
1117 word holds ``IN/OUT`` gap-free elements that never straddle, so a counter
1118 drives a parts:1 element mux -- no shifter (e.g. 32b/64b, 64b/256b).
1119 ``valid_bytes`` locates the last element in the burst's (possibly partial)
1120 final word. Straddling relationships use `ShiftReadGearbox`."""
1121
1122 if input_bitwidth % 8 != 0:
1123 raise ValueError("engine word width must be a multiple of 8 bits")
1124 if output_bitwidth == 0 or output_bitwidth % 8 != 0 \
1125 or input_bitwidth % output_bitwidth != 0:
1126 raise ValueError(
1127 "DepackReadGearbox requires a byte-aligned element that divides the "
1128 "engine word")
1129
1130 in_bytes = input_bitwidth // 8
1131 # 'valid_bytes' is the real byte count minus 1; a word always has >= 1 byte.
1132 vb_width = max(1, clog2(in_bytes))
1133 count_width = clog2(in_bytes + 1)
1134 parts = input_bitwidth // output_bitwidth
1135 elem_bytes = output_bitwidth // 8
1136
1137 class DepackReadGearboxImpl(Module):
1138 clk = Clock()
1139 rst = Reset()
1140 in_ = InputChannel(
1141 StructType([
1142 ("tag", esi.HostMem.TagType),
1143 ("data", Bits(input_bitwidth)),
1144 ("valid_bytes", UInt(vb_width)),
1145 ("last", Bits(1)),
1146 ]))
1147 out = OutputChannel(
1148 StructType([
1149 ("tag", esi.HostMem.TagType),
1150 ("data", Bits(output_bitwidth)),
1151 ("last", Bits(1)),
1152 ]))
1153
1154 @generator
1155 def build(ports):
1156 client_ready = Wire(Bits(1), name="client_ready")
1157 up_ready = Wire(Bits(1), name="up_ready")
1158 # Register the input for fmax; the ESI channel buffer keeps the handshake
1159 # elastic and decouples the ready path.
1160 in_reg = ports.in_.buffer(ports.clk, ports.rst, stages=1)
1161 up, up_valid = in_reg.unwrap(up_ready)
1162 client_xact = up_valid & client_ready
1163
1164 if parts == 1:
1165 # One element per word; nothing to select.
1166 last_in_word = Bits(1)(1)
1167 client_data = up.data
1168 else:
1169 idx_width = clog2(parts)
1170 idx = Reg(UInt(idx_width),
1171 clk=ports.clk,
1172 rst=ports.rst,
1173 rst_value=0,
1174 ce=client_xact,
1175 name="idx")
1176 # (idx + 1) * elem_bytes == real valid bytes marks the word's last
1177 # element (the final word may hold fewer than `parts`); add 1 back to
1178 # the biased 'valid_bytes' to recover the real count.
1179 real_valid_bytes = (up.valid_bytes + UInt(1)(1)).as_uint(count_width)
1180 consumed = ((idx + UInt(1)(1)) *
1181 UInt(count_width)(elem_bytes)).as_uint(count_width)
1182 last_in_word = consumed == real_valid_bytes
1183 # parts:1 element-select mux -- the entire datapath, no shifter.
1184 word_parts = Array(Bits(output_bitwidth), parts)([
1185 up.data[k * output_bitwidth:(k + 1) * output_bitwidth]
1186 for k in range(parts)
1187 ])
1188 client_data = word_parts[idx]
1189 idx.assign(
1190 Mux(last_in_word, (idx + UInt(1)(1)).as_uint(idx_width),
1191 UInt(idx_width)(0)))
1192
1193 # Consume the buffered word as its last element leaves.
1194 up_ready.assign(client_xact & last_in_word)
1195 client_channel, client_ready_sig = DepackReadGearboxImpl.out.type.wrap(
1196 {
1197 "tag": up.tag,
1198 "data": client_data,
1199 "last": (up.last & last_in_word).as_bits(),
1200 }, up_valid)
1201 client_ready.assign(client_ready_sig)
1202 ports.out = client_channel
1203
1204 return DepackReadGearboxImpl
1205
1206
1207@modparams
1208def ShiftReadGearbox(input_bitwidth: int,
1209 output_bitwidth: int) -> type["ShiftReadGearboxImpl"]:
1210 """Universal fallback: unpack a contiguous, byte-packed element stream (a
1211 `read_list` response) for ANY ``(input_bitwidth, output_bitwidth)`` pair.
1212
1213 Elements are packed at their natural byte stride ``stride = ceil(OUT/8)``
1214 bytes, so element k begins at wire bit ``k*stride*8`` and, in general,
1215 straddles engine-word boundaries at an arbitrary bit offset. A byte-addressed
1216 shift-register accumulator realigns each element across words. This is correct
1217 for every width relationship; `SliceReadGearbox`, `ConcatReadGearbox` and
1218 `DepackReadGearbox` are optimizations that avoid this barrel shifter for the
1219 regular (non-straddling) cases.
1220
1221 Each input word carries ``valid_bytes`` (how many of its bytes are real) and
1222 ``last``. Both are framed to one whole `read_list` request rather than to the
1223 transport: `HostMemReadReqSplitter` drops the per-chunk framing of the reads
1224 it issues and re-derives these from the request's total length, so only the
1225 request's final word is ever partial. That length is ``num_elements *
1226 stride``, so tracking real bytes lets the gearbox emit exactly the right
1227 elements and place the list-terminating ``last`` on the final one -- no
1228 padding element is ever emitted."""
1229
1230 if input_bitwidth % 8 != 0:
1231 raise ValueError("engine word width must be a multiple of 8 bits")
1232 if output_bitwidth <= 0:
1233 raise ValueError("client element width must be positive")
1234 in_bytes = input_bitwidth // 8
1235 stride_bytes = (output_bitwidth + 7) // 8
1236 stride_bits = stride_bytes * 8
1237 # Hold at most one partial element plus one freshly accepted word.
1238 buf_bytes = stride_bytes + in_bytes
1239 buf_bits = buf_bytes * 8
1240 # 'valid_bytes' is the real byte count minus 1; a word always has >= 1 byte.
1241 vb_width = max(1, clog2(in_bytes))
1242 cnt_width = clog2(buf_bytes + 1)
1243 # The append offset is only ever in [0, stride_bytes] (has_room), so the shift
1244 # index needs fewer bits than the full count -- see `build`.
1245 offset_width = clog2(stride_bytes + 1)
1246
1247 class ShiftReadGearboxImpl(Module):
1248 clk = Clock()
1249 rst = Reset()
1250 in_ = InputChannel(
1251 StructType([
1252 ("tag", esi.HostMem.TagType),
1253 ("data", Bits(input_bitwidth)),
1254 ("valid_bytes", UInt(vb_width)),
1255 ("last", Bits(1)),
1256 ]))
1257 out = OutputChannel(
1258 StructType([
1259 ("tag", esi.HostMem.TagType),
1260 ("data", Bits(output_bitwidth)),
1261 ("last", Bits(1)),
1262 ]))
1263
1264 @generator
1265 def build(ports):
1266 client_ready = Wire(Bits(1), name="client_ready")
1267 up_ready = Wire(Bits(1), name="up_ready")
1268 # Register the input for fmax; the ESI channel buffer keeps the handshake
1269 # elastic and decouples the ready path.
1270 in_reg = ports.in_.buffer(ports.clk, ports.rst, stages=1)
1271 up, up_valid = in_reg.unwrap(up_ready)
1272
1273 from pycde.circt.dialects import comb
1274
1275 # Byte-addressed accumulator: `buffer` holds `count` valid bytes packed
1276 # from bit 0 up; the element being emitted is buffer[0:OUT].
1277 buffer = Reg(Bits(buf_bits),
1278 clk=ports.clk,
1279 rst=ports.rst,
1280 rst_value=0,
1281 name="buffer")
1282 count = Reg(UInt(cnt_width),
1283 clk=ports.clk,
1284 rst=ports.rst,
1285 rst_value=0,
1286 name="count")
1287 saw_last = Wire(Bits(1), name="saw_last")
1288
1289 # Accept a whole engine word only when there's room, and never while
1290 # draining a finished burst -- otherwise the next burst's bytes would mix
1291 # into this one's buffer.
1292 has_room = count <= UInt(cnt_width)(buf_bytes - in_bytes)
1293 up_ready.assign(has_room & ~saw_last)
1294 up_xact = up_ready & up_valid
1295
1296 # Emit an element once a full stride slot is buffered.
1297 client_valid = count >= UInt(cnt_width)(stride_bytes)
1298 client_xact = client_valid & client_ready
1299
1300 # The burst's final word sets `saw_last`; the emit that drains the buffer
1301 # to empty terminates the list. These never coincide: emitting needs a
1302 # slot buffered by a prior cycle's accept, so `after_emit == 0` on an
1303 # accept cycle is impossible.
1304 added = Mux(up_xact,
1305 UInt(cnt_width)(0), (up.valid_bytes.as_uint(cnt_width) +
1306 UInt(1)(1)).as_uint(cnt_width))
1307 after_add = (count + added).as_uint(cnt_width)
1308 after_emit = (after_add -
1309 UInt(cnt_width)(stride_bytes)).as_uint(cnt_width)
1310 set_saw_last = (up_xact & up.last).as_bits()
1311 is_final_slot = (after_emit == UInt(cnt_width)(0))
1312 burst_ending = saw_last | set_saw_last
1313 client_last = client_valid & burst_ending & is_final_slot
1314 final_emit = client_xact & client_last
1315
1316 # Append the accepted word at bit offset count*8 (dynamic left shift);
1317 # then, if we emit this cycle, drop the consumed slot (constant right
1318 # shift by the stride). The append offset is <= stride_bytes when
1319 # accepting, so bound the shift index to that range: its high bits are
1320 # constant 0, which lets constant-propagation prune the upper barrel-
1321 # shifter stages (synthesis won't infer this bound from the count reg).
1322 append_off = count.as_bits()[0:offset_width]
1323 shamt = BitsSignal.concat([append_off,
1324 Bits(3)(0)]).pad_or_truncate(buf_bits)
1325 word_ext = up.data.pad_or_truncate(buf_bits)
1326 shifted_word = BitsSignal(
1327 comb.ShlOp(word_ext.value, shamt.value).result, Bits(buf_bits))
1328 appended = buffer | Mux(up_xact, Bits(buf_bits)(0), shifted_word)
1329 drained = appended[stride_bits:buf_bits].pad_or_truncate(buf_bits)
1330 buffer.assign(
1331 Mux(final_emit, Mux(client_xact, appended, drained),
1332 Bits(buf_bits)(0)))
1333
1334 # count += accepted real bytes (biased 'valid_bytes' + 1); -= stride on
1335 # emit.
1336 count.assign(Mux(client_xact, after_add, after_emit))
1337
1338 saw_last.assign(
1339 ControlReg(ports.clk, ports.rst, [set_saw_last], [final_emit]))
1340
1341 tag_reg = up.tag.reg(ports.clk, ports.rst, ce=up_xact, name="tag_reg")
1342 client_channel, client_ready_sig = ShiftReadGearboxImpl.out.type.wrap(
1343 {
1344 "tag": tag_reg,
1345 "data": buffer[0:output_bitwidth],
1346 "last": client_last,
1347 }, client_valid)
1348 client_ready.assign(client_ready_sig)
1349 ports.out = client_channel
1350
1351 return ShiftReadGearboxImpl
1352
1353
1354def select_read_gearbox(is_list: bool, input_bitwidth: int,
1355 output_bitwidth: int):
1356 """Pick the read-gearbox module for a client of the given kind and width
1357 relationship. Every gearbox shares the {tag, data, valid_bytes, last} input
1358 (from `HostMemReadReqSplitter`) and the {tag, data, last} output, so callers
1359 wire them identically. `ShiftReadGearbox` is the correct-for-everything
1360 fallback; the others avoid its barrel shifter for regular relationships."""
1361 if not is_list:
1362 # A single element starts at bit 0 and never straddles at a bit offset.
1363 if output_bitwidth <= input_bitwidth:
1364 return SliceReadGearbox(input_bitwidth, output_bitwidth)
1365 return ConcatReadGearbox(input_bitwidth, output_bitwidth)
1366 if output_bitwidth > input_bitwidth:
1367 # Super-word list: a whole-word-multiple element never straddles.
1368 if output_bitwidth % input_bitwidth == 0:
1369 return ConcatReadGearbox(input_bitwidth, output_bitwidth)
1370 return ShiftReadGearbox(input_bitwidth, output_bitwidth)
1371 # Sub-word list: a byte-aligned element that divides the word never straddles.
1372 if input_bitwidth % output_bitwidth == 0 and output_bitwidth % 8 == 0:
1373 return DepackReadGearbox(input_bitwidth, output_bitwidth)
1374 return ShiftReadGearbox(input_bitwidth, output_bitwidth)
1375
1376
1377# Maximum size, in bytes, of a single upstream read request. Reads larger than
1378# this are split by the requester into multiple requests. The default is a
1379# conservative PCIe-derived cap (Max_Read_Request_Size tops out at 4096 bytes,
1380# but root ports often negotiate less); it mirrors kPcieMaxReadRequestBytes in
1381# the Cosim backend (cpp/lib/backends/Cosim.cpp).
1382DEFAULT_MAX_READ_REQUEST_BYTES = 64 * 4 # 64 double words
1383
1384# Maximum size, in bytes, of a single upstream write transaction; an element
1385# whose write payload is wider is split into multiple <= this-size transactions.
1386# The default is a conservative PCIe-derived Max-Payload-Size cap.
1387DEFAULT_MAX_WRITE_PAYLOAD_BYTES = 256
1388
1389
1390@modparams
1391def HostMemReadReqSplitter(req_channel_type: Channel,
1392 resp_channel_type: Channel,
1393 max_chunk_bytes: int,
1394 max_outstanding: int = 1,
1395 round_read_lengths: bool = True):
1396 """Split oversized host memory read requests into request-sized chunks before
1397 arbitration and reassemble the per-chunk responses into a single logical
1398 burst per request.
1399
1400 A burst read (`read_list`) can request many more bytes than a single upstream
1401 read request can carry. This module breaks such a request into
1402 `max_chunk_bytes`-sized (word-aligned) chunks addressed sequentially from the
1403 base. Splitting here -- *before* the requests
1404 are arbitrated onto the shared upstream read channel -- lets each client's
1405 chunks interleave with other clients' requests, so one large burst does not
1406 monopolize host memory bandwidth.
1407
1408 On the response path the per-chunk end-of-list markers are dropped and a
1409 single burst-final `last` (plus the final word's `valid_bytes`) is re-derived
1410 from each logical request's length, so the gearbox and client see one
1411 contiguous response stream per request, identical to an unsplit read.
1412
1413 Up to `max_outstanding` logical requests may be in flight at once. On accept,
1414 each request's {word count, final-word valid bytes} is queued in a metadata
1415 FIFO (depth `max_outstanding`); the response path frames each burst from the
1416 head of that FIFO. This **requires the upstream to return response words in
1417 request order** (no interleaving of different requests' words) -- the
1418 response `tag` is passed through from `resp_in` and is not used for matching.
1419 A new request is accepted whenever the chunk emitter is free (or is issuing
1420 its final chunk this cycle) and fewer than `max_outstanding` requests are in
1421 flight; draining responses does not block accepting. Throughput: a
1422 single-chunk request can be accepted every cycle, chunks are issued one per
1423 cycle, and responses flow one word per cycle with no bubble between bursts.
1424 No latency is added over the single-request version. There is no
1425 combinational path from `resp_in` to `req_in`'s ready; with
1426 `max_outstanding` > 1, `req_out`'s ready feeds `req_in`'s ready.
1427
1428 Zero-length requests are not supported.
1429
1430 req_channel_type: channel of the upstream read request {address, length
1431 (bytes), tag}.
1432 resp_channel_type: channel of the upstream response {tag, data, last}.
1433 max_chunk_bytes: largest per-chunk byte count; must be > 0 and a multiple of
1434 the response word size.
1435 max_outstanding: maximum number of logical requests in flight (accepted
1436 but whose final response word has not yet been delivered). The default of 1
1437 serializes requests.
1438 round_read_lengths: round chunk lengths up to a whole response word for
1439 transports requiring word-granular reads. Disable for byte-granular
1440 transports such as cosim, which zero-pad a partial final response word.
1441 """
1442 assert max_chunk_bytes > 0
1443 assert max_outstanding >= 1
1444
1445 req_struct = req_channel_type.inner_type
1446 resp_struct = resp_channel_type.inner_type
1447 req_fields = dict(req_struct.fields)
1448 addr_width = req_fields["address"].bitwidth
1449 length_width = req_fields["length"].bitwidth
1450 tag_type = req_fields["tag"]
1451 word_bytes = dict(resp_struct.fields)["data"].bitwidth // 8
1452 word_shift = clog2(word_bytes)
1453 words_width = length_width - word_shift
1454 # The response is augmented with a per-word 'valid_bytes': the number of real
1455 # bytes in the (possibly partial) final word, biased by -1. A burst word
1456 # always has >= 1 real byte, so encoding count-1 fits in one fewer bit.
1457 vb_width = max(1, clog2(word_bytes))
1458 resp_fields = dict(resp_struct.fields)
1459 resp_out_struct = StructType([
1460 ("tag", resp_fields["tag"]),
1461 ("data", resp_fields["data"]),
1462 ("valid_bytes", UInt(vb_width)),
1463 ("last", Bits(1)),
1464 ])
1465 resp_out_channel_type = Channel(resp_out_struct)
1466
1467 # Per-request response metadata, computed on accept. 'words_m1' is the
1468 # request's word count minus one, 'final_vb' the valid bytes in its final
1469 # word (biased by -1) and 'single' flags a one-word request.
1470 meta_struct = StructType([
1471 ("words_m1", UInt(words_width)),
1472 ("final_vb", UInt(vb_width)),
1473 ("single", Bits(1)),
1474 ])
1475 inflight_width = clog2(max_outstanding + 1)
1476
1477 class HostMemReadReqSplitterImpl(Module):
1478 clk = Clock()
1479 rst = Reset()
1480 req_in = Input(req_channel_type)
1481 req_out = Output(req_channel_type)
1482 resp_in = Input(resp_channel_type)
1483 resp_out = Output(resp_out_channel_type)
1484
1485 @generator
1486 def build(ports):
1487 clk = ports.clk
1488 rst = ports.rst
1489
1490 # --- Request intake and splitting ---
1491 emit_busy = Wire(Bits(1), name="emit_busy") # issuing chunk requests
1492 cur_addr = Wire(UInt(addr_width), name="cur_addr")
1493 remaining = Wire(UInt(length_width), name="remaining") # req bytes left
1494 # Registered 'remaining <= max_chunk': the chunk being issued is the
1495 # request's last.
1496 last_chunk = Wire(Bits(1), name="last_chunk")
1497 tag_reg = Wire(tag_type, name="tag_reg")
1498 inflight_full = Wire(Bits(1), name="inflight_full")
1499
1500 max_chunk = UInt(length_width)(max_chunk_bytes)
1501 chunk_len = Mux(last_chunk, max_chunk, remaining)
1502
1503 # Round the emitted read length up to a whole word. The reader response
1504 # is word-granular and 'valid_bytes' still carries the real trailing byte
1505 # count, so the reassembled element count is unchanged; this just keeps
1506 # every read word-aligned for single-flit HostMem transports that reject
1507 # sub-word read lengths.
1508 if word_shift == 0 or not round_read_lengths:
1509 chunk_len_out = chunk_len
1510 else:
1511 chunk_words = (chunk_len + UInt(length_width)(word_bytes - 1)
1512 ).as_bits()[word_shift:].as_uint(words_width)
1513 chunk_len_out = BitsSignal.concat(
1514 [chunk_words.as_bits(), Bits(word_shift)(0)]).as_uint(length_width)
1515
1516 req_out_ch, req_out_ready = req_channel_type.wrap(
1517 req_struct({
1518 "address": cur_addr,
1519 "length": chunk_len_out,
1520 "tag": tag_reg,
1521 }), emit_busy)
1522 ports.req_out = req_out_ch
1523 chunk_xact = emit_busy & req_out_ready
1524 emit_done = chunk_xact & last_chunk
1525
1526 # With more than one request allowed in flight, accept the next request
1527 # in the cycle the current one issues its final chunk so single-chunk
1528 # requests stream at one per cycle. (With one, 'inflight_full' blocks
1529 # that case anyway.)
1530 if max_outstanding > 1:
1531 emitter_free = ~emit_busy | emit_done
1532 else:
1533 emitter_free = ~emit_busy
1534 req_ready = Wire(Bits(1))
1535 req_payload, req_valid = ports.req_in.unwrap(req_ready)
1536 req_ready.assign(emitter_free & ~inflight_full)
1537 accept = req_valid & req_ready
1538
1539 emit_busy.assign(
1540 ControlReg(clk, rst, [accept], [emit_done], name="emit_busy_reg"))
1541
1542 # cur_addr / remaining: load on accept; on each non-final chunk advance by
1543 # a full chunk. (After the final chunk they are dead until the next
1544 # accept, so the constant-step update is exact.) Datapath only: no reset.
1545 cur_addr_incr = (cur_addr +
1546 UInt(addr_width)(max_chunk_bytes)).as_uint(addr_width)
1547 cur_addr.assign(
1548 Mux(accept, cur_addr_incr,
1549 req_payload.address).reg(clk,
1550 ce=accept | chunk_xact,
1551 name="cur_addr_reg"))
1552 remaining_dec = (remaining - max_chunk).as_uint(length_width)
1553 remaining.assign(
1554 Mux(accept, remaining_dec,
1555 req_payload.length).reg(clk,
1556 ce=accept | chunk_xact,
1557 name="remaining_reg"))
1558 # The next chunk is the last iff what is left after this one fits in a
1559 # chunk: remaining - max_chunk <= max_chunk.
1560 two_chunks = min(2 * max_chunk_bytes, (1 << length_width) - 1)
1561 last_chunk.assign(
1562 Mux(accept, remaining <= UInt(length_width)(two_chunks),
1563 req_payload.length <= max_chunk).reg(clk,
1564 ce=accept | chunk_xact,
1565 name="last_chunk_reg"))
1566 tag_reg.assign(req_payload.tag.reg(clk, ce=accept, name="tag_reg_r"))
1567
1568 # --- Per-request response metadata. Elements need not tile evenly into
1569 # words: with len_m1 = length - 1, the request spans (len_m1 >> shift) + 1
1570 # words and its final word holds (len_m1 % word_bytes) + 1 bytes. ---
1571 len_m1 = (req_payload.length -
1572 UInt(length_width)(1)).as_uint(length_width)
1573 if word_shift == 0:
1574 words_m1 = len_m1.as_uint(words_width)
1575 final_vb = UInt(vb_width)(0)
1576 else:
1577 words_m1 = len_m1.as_bits()[word_shift:].as_uint(words_width)
1578 final_vb = len_m1.as_bits()[:word_shift].as_uint(vb_width)
1579 meta_in = meta_struct({
1580 "words_m1": words_m1,
1581 "final_vb": final_vb,
1582 "single": req_payload.length <= UInt(length_width)(word_bytes),
1583 })
1584
1585 # Metadata store between accept and the response framer. A request
1586 # accepted while the store is empty and the framer is free bypasses the
1587 # store (see 'bypass' below).
1588 meta_push = Wire(Bits(1), name="meta_push")
1589 meta_pop = Wire(Bits(1), name="meta_pop")
1590 if max_outstanding == 1:
1591 # At most one request in flight: a single holding register suffices.
1592 meta_head = meta_in.reg(clk, ce=meta_push, name="meta_reg")
1593 meta_empty = ~ControlReg(
1594 clk, rst, [meta_push], [meta_pop], name="meta_valid")
1595 else:
1596 meta_fifo = SeqFIFO(meta_struct, max_outstanding, clk, rst)
1597 meta_fifo.push(meta_in, meta_push)
1598 meta_head = meta_fifo.pop(meta_pop)
1599 meta_empty = meta_fifo.empty
1600
1601 # --- Response framing: re-derive the burst-final 'last' and the byte
1602 # count of the (possibly partial) final word. The current burst's
1603 # metadata is held in registers loaded from the metadata store head:
1604 # 'cur_words' counts the words after the current one and 'cur_last'
1605 # flags the current word as the burst's final word. ---
1606 cur_valid = Wire(Bits(1), name="cur_valid")
1607 cur_words = Wire(UInt(words_width), name="cur_words")
1608 cur_last = Wire(Bits(1), name="cur_last")
1609 cur_vb = Wire(UInt(vb_width), name="cur_vb")
1610
1611 # The response handshake is not gated on 'cur_valid': a request's
1612 # metadata is in the framer by the cycle its first chunk is issued, so
1613 # no response word can precede it.
1614 resp_ready = Wire(Bits(1))
1615 resp_payload, resp_valid = ports.resp_in.unwrap(resp_ready)
1616 resp_out_ch, resp_out_ready = resp_out_channel_type.wrap(
1617 resp_out_struct({
1618 "tag":
1619 resp_payload.tag,
1620 "data":
1621 resp_payload.data,
1622 "valid_bytes":
1623 Mux(cur_last,
1624 UInt(vb_width)(word_bytes - 1), cur_vb),
1625 "last":
1626 cur_last,
1627 }), resp_valid)
1628 ports.resp_out = resp_out_ch
1629 resp_ready.assign(resp_out_ready)
1630 resp_xact = resp_valid & resp_out_ready
1631 burst_done = resp_xact & cur_last
1632
1633 # Load the next burst's metadata when idle or as the current burst ends,
1634 # so back-to-back bursts need no bubble: from the store head if it holds
1635 # anything, otherwise straight from a request accepted this cycle.
1636 framer_free = ~cur_valid | burst_done
1637 load_head = ~meta_empty & framer_free
1638 bypass = accept & meta_empty & framer_free
1639 load = load_head | bypass
1640 meta_pop.assign(load_head)
1641 meta_push.assign(accept & ~bypass)
1642 next_meta = Mux(load_head, meta_in, meta_head)
1643 cur_valid.assign(
1644 ControlReg(clk, rst, [load], [burst_done], name="cur_valid_reg"))
1645 cur_words_dec = (cur_words - UInt(words_width)(1)).as_uint(words_width)
1646 cur_words.assign(
1647 Mux(load, cur_words_dec,
1648 next_meta["words_m1"]).reg(clk,
1649 ce=load | resp_xact,
1650 name="cur_words_reg"))
1651 cur_last.assign(
1652 Mux(load, cur_words == UInt(words_width)(1),
1653 next_meta["single"]).reg(clk,
1654 ce=load | resp_xact,
1655 name="cur_last_reg"))
1656 cur_vb.assign(next_meta["final_vb"].reg(clk, ce=load, name="cur_vb_reg"))
1657
1658 # --- In-flight request accounting: accepted, final word not yet
1659 # delivered. 'inflight_full' is registered so 'req_in' ready has no
1660 # combinational dependence on the response path. ---
1661 inflight = Wire(UInt(inflight_width), name="inflight")
1662 one = UInt(inflight_width)(1)
1663 inflight_inc = (inflight + one).as_uint(inflight_width)
1664 inflight_dec = (inflight - one).as_uint(inflight_width)
1665 inflight_next = Mux(accept, Mux(burst_done, inflight, inflight_dec),
1666 Mux(burst_done, inflight_inc, inflight))
1667 inflight.assign(
1668 inflight_next.reg(clk, rst, rst_value=0, name="inflight_reg"))
1669 inflight_full.assign(
1670 (inflight_next == UInt(inflight_width)(max_outstanding)).reg(
1671 clk, rst, rst_value=0, name="inflight_full_reg"))
1672
1673 return HostMemReadReqSplitterImpl
1674
1675
1677 read_width: int,
1678 hostmem_module,
1679 reqs: List[esi._OutputBundleSetter],
1680 max_read_request_bytes: int = DEFAULT_MAX_READ_REQUEST_BYTES,
1681 max_outstanding_reads: int = 1,
1682 round_read_lengths: bool = True):
1683 """Construct a host memory read request module to orchestrate the the read
1684 connections. Responsible for both gearboxing the data, multiplexing the
1685 requests, reassembling out-of-order responses and routing the responses to the
1686 correct clients.
1687
1688 `max_outstanding_reads` bounds the number of logical read requests each
1689 client may have in flight (see `HostMemReadReqSplitter`). Values above 1
1690 require the upstream to return each client's responses in request order.
1691
1692 Generate this module dynamically to allow for multiple read clients of
1693 multiple types to be directly accomodated."""
1694
1695 class HostmemReadProcessorImpl(Module):
1696 clk = Clock()
1697 rst = Reset()
1698
1699 # Add an output port for each read client.
1700 reqPortMap: Dict[esi._OutputBundleSetter, str] = {}
1701 for req in reqs:
1702 name = "client_" + req.client_name_str
1703 locals()[name] = Output(req.type)
1704 reqPortMap[req] = name
1705
1706 # And then the port which goes to the host.
1707 upstream = Output(hostmem_module.read.type)
1708
1709 @generator
1710 def build(ports):
1711 """Build the read side of the HostMem service."""
1712
1713 # If there's no read clients, just return a no-op read bundle.
1714 if len(reqs) == 0:
1715 upstream_req_channel, _ = Channel(hostmem_module.UpstreamReadReq).wrap(
1716 {
1717 "tag": 0,
1718 "length": 0,
1719 "address": 0
1720 }, 0)
1721 upstream_read_bundle, _ = hostmem_module.read.type.pack(
1722 req=upstream_req_channel)
1723 ports.upstream = upstream_read_bundle
1724 return
1725
1726 # Since we use the tag to identify the client, we can't have more than 256
1727 # read clients. Supporting more than 256 clients would require
1728 # tag-rewriting, which we'll probably have to implement at some point.
1729 # TODO: Implement tag-rewriting.
1730 assert len(reqs) <= 256, "More than 256 read clients not supported."
1731
1732 # Pack the upstream bundle and leave the request as a wire.
1733 upstream_req_channel = Wire(Channel(hostmem_module.UpstreamReadReq))
1734 upstream_read_bundle, froms = hostmem_module.read.type.pack(
1735 req=upstream_req_channel)
1736 ports.upstream = upstream_read_bundle
1737 upstream_resp_channel = froms["resp"]
1738
1739 # Demux the upstream response frames {tag, data, last} to each client by
1740 # tag. Each client's stream then flows through a `HostMemReadReqSplitter`
1741 # (which annotates per-word 'valid_bytes' and the burst-final 'last') into
1742 # the leaf gearbox chosen by `select_read_gearbox`.
1743 demux = esi.TaggedDemux(len(reqs), upstream_resp_channel.type)(
1744 clk=ports.clk, rst=ports.rst, in_=upstream_resp_channel)
1745
1746 word_bytes = read_width // 8
1747 tagged_client_reqs = []
1748 for idx, client in enumerate(reqs):
1749 # Find the response channel in the request bundle.
1750 resp_type = [
1751 c.channel for c in client.type.channels if c.name == 'resp'
1752 ][0]
1753 demuxed_upstream_channel = demux.get_out(idx)
1754
1755 # TODO: Should responses come back out-of-order (interleaved tags),
1756 # re-order them here so the gearbox doesn't get confused. (Longer term.)
1757 # For now, each client's responses must come back in request order
1758 # (the splitter frames bursts in order, allowing up to
1759 # `max_outstanding_reads` in flight). This has the additional benefit
1760 # of letting the upstream tag be the client identifier.
1761 client_type = resp_type.inner_type
1762 is_list = isinstance(client_type, Window)
1763
1764 # A read_list response is a parallel window over
1765 # struct{tag, data: list<element>} (num_items=1), lowering to
1766 # struct{tag, data: element, data_size, last}; a single read carries the
1767 # element directly. Pull the element width out of whichever shape.
1768 if is_list:
1769 lowered = client_type.lowered_type
1770 lowered_fields = dict(lowered.fields)
1771 element_type = lowered_fields["data"]
1772 element_bits = element_type.bitwidth
1773 data_size_type = lowered_fields["data_size"]
1774 if element_bits == 0:
1775 raise ValueError("read_list element type cannot be zero-width.")
1776 else:
1777 if client_type.data.bitwidth == 0:
1778 raise ValueError("Client data type cannot be zero-width. Use a "
1779 "single-bit type if no data is needed.")
1780 element_bits = client_type.data.bitwidth
1781 # Elements are packed contiguously in host memory at their natural byte
1782 # size, independent of the engine word width.
1783 elem_stride_bytes = (element_bits + 7) // 8
1784
1785 # Both single-message and list reads flow demux -> splitter -> gearbox
1786 # with a uniform {tag, data, valid_bytes, last} interface. The splitter
1787 # chunks oversized requests (so even a wide single element is
1788 # request-chunked) and annotates each word with 'valid_bytes' plus the
1789 # burst-final 'last'; `select_read_gearbox` picks the leaf gearbox for
1790 # this (is_list, read_width, element_bits). 'splitter_resp' breaks the
1791 # request/response construction cycle (the client request is derived
1792 # from the gearbox's response bundle).
1793 max_chunk_bytes = (max_read_request_bytes // word_bytes) * word_bytes
1794 gearbox_mod = select_read_gearbox(is_list, read_width, element_bits)
1795 splitter_resp = Wire(gearbox_mod.in_.type)
1796 gearbox = gearbox_mod(clk=ports.clk, rst=ports.rst, in_=splitter_resp)
1797
1798 if is_list:
1799 # Propagate 'last', then re-wrap the element as the response window.
1800 client_resp_channel = gearbox.out.transform(
1801 lambda m, lowered=lowered, element_type=element_type,
1802 data_size_type=data_size_type, client_type=client_type:
1803 client_type.wrap(
1804 lowered({
1805 "tag": m.tag,
1806 "data": m.data.bitcast(element_type),
1807 "data_size": data_size_type(0),
1808 "last": m.last,
1809 })))
1810 client_bundle, froms = client.type.pack(resp=client_resp_channel)
1811 client_req = froms["req"]
1812 logical_req = client_req.transform(
1813 lambda r, idx=idx, elem_stride_bytes=elem_stride_bytes:
1814 hostmem_module.UpstreamReadReq({
1815 "address":
1816 r.address,
1817 "length": (r.length * UInt(64)
1818 (elem_stride_bytes)).as_uint(32),
1819 "tag":
1820 idx,
1821 }))
1822 else:
1823 # Single-message read: one element; discard the 'last' burst marker.
1824 client_resp_channel = gearbox.out.transform(
1825 lambda m, client_type=client_type: client_type({
1826 "tag": m.tag,
1827 "data": m.data.bitcast(client_type.data)
1828 }))
1829 client_bundle, froms = client.type.pack(resp=client_resp_channel)
1830 client_req = froms["req"]
1831 logical_req = client_req.transform(
1832 lambda r, idx=idx, elem_stride_bytes=elem_stride_bytes:
1833 hostmem_module.UpstreamReadReq({
1834 "address": r.address,
1835 "length": UInt(32)(elem_stride_bytes),
1836 # TODO: Change this once we support tag-rewriting.
1837 "tag": idx,
1838 }))
1839
1840 splitter = HostMemReadReqSplitter(
1841 logical_req.type, demuxed_upstream_channel.type, max_chunk_bytes,
1842 max_outstanding_reads,
1843 round_read_lengths)(clk=ports.clk,
1844 rst=ports.rst,
1845 req_in=logical_req,
1846 resp_in=demuxed_upstream_channel)
1847 splitter_resp.assign(splitter.resp_out)
1848 tagged_client_req = splitter.req_out
1849
1850 tagged_client_reqs.append(tagged_client_req)
1851
1852 # Set the port for the client request.
1853 setattr(ports, HostmemReadProcessorImpl.reqPortMap[client],
1854 client_bundle)
1855
1856 # Assign the multiplexed read request to the upstream request. Use the
1857 # list-aware, pipelined ChannelArbiter (vs. the combinational ChannelMux)
1858 # for a registered N:1 mux that closes timing at high client fan-in. Read
1859 # requests are single-flit, so list-awareness is a no-op here.
1860 # `mux_pipeline_levels=2` retimes the wide payload selection mux, whose
1861 # depth otherwise grows as log2(num_clients); the added latency is
1862 # absorbed by the arbiter's output FIFO / credit counter.
1863 # TODO: Don't release a request until the client is ready to accept
1864 # the response otherwise the system could deadlock. A stalled client
1865 # blocks the shared response demux for every other client; with
1866 # `max_outstanding_reads` > 1 each client can have more reads in flight,
1867 # so more response data can be queued behind a stalled client.
1868 muxed_client_reqs = ChannelArbiter(tagged_client_reqs,
1869 ports.clk,
1870 ports.rst,
1871 mux_pipeline_levels=2,
1872 pipelined_scheduler=True,
1873 telemetry=False)
1874 upstream_req_channel.assign(muxed_client_reqs)
1875 HostmemReadProcessorImpl.reqPortMap.clear()
1876
1877 return HostmemReadProcessorImpl
1878
1879
1880@modparams
1881def TaggedWriteGearbox(input_bitwidth: int, output_bitwidth: int,
1882 max_burst_bytes: int) -> type["TaggedWriteGearboxImpl"]:
1883 """Build a gearbox to convert the client data to upstream write chunks.
1884 Assumes a struct {address, tag, data} and only gearboxes the data. Tag is
1885 stored separately and the struct is re-assembled later on.
1886
1887 'max_burst_bytes' caps a single contiguous upstream write transaction (a
1888 max-payload-size analog): when an element spans more than 'max_burst_bytes',
1889 its engine words are split into multiple <= 'max_burst_bytes' transactions by
1890 emitting the framing 'last' at each boundary. 0 disables the cap."""
1891
1892 if output_bitwidth % 8 != 0:
1893 raise ValueError("Output bitwidth must be a multiple of 8.")
1894 input_pad_bits = 0
1895 if input_bitwidth % 8 != 0:
1896 input_pad_bits = 8 - (input_bitwidth % 8)
1897 input_padded_bitwidth = input_bitwidth + input_pad_bits
1898 valid_bytes_type = Bits(clog2(output_bitwidth // 8 + 1))
1899
1900 # Number of engine words per capped transaction (0 = uncapped).
1901 max_burst_words = (max_burst_bytes //
1902 (output_bitwidth // 8)) if max_burst_bytes else 0
1903 if max_burst_words:
1904 assert (max_burst_words & (max_burst_words - 1)) == 0, \
1905 "max_burst_bytes / (output_bitwidth // 8) must be a power of two"
1906
1907 class TaggedWriteGearboxImpl(Module):
1908 clk = Clock()
1909 rst = Reset()
1910 in_ = InputChannel(
1911 StructType([
1912 ("address", UInt(64)),
1913 ("tag", esi.HostMem.TagType),
1914 ("data", Bits(input_bitwidth)),
1915 ]))
1916 out = OutputChannel(
1917 StructType([
1918 ("address", UInt(64)),
1919 ("tag", esi.HostMem.TagType),
1920 ("data", Bits(output_bitwidth)),
1921 ("valid_bytes", valid_bytes_type),
1922 ("last", Bits(1)),
1923 ]))
1924
1925 num_chunks = ceil(input_padded_bitwidth / output_bitwidth)
1926
1927 @generator
1928 def build(ports):
1929 upstream_ready = Wire(Bits(1))
1930 ready_for_client = Wire(Bits(1))
1931 client_tag_and_data, client_valid = ports.in_.unwrap(ready_for_client)
1932 client_data = client_tag_and_data.data
1933 if input_pad_bits > 0:
1934 client_data = client_data.pad_or_truncate(input_padded_bitwidth)
1935 client_xact = ready_for_client & client_valid
1936 input_bitwidth_bytes = input_padded_bitwidth // 8
1937 output_bitwidth_bytes = output_bitwidth // 8
1938
1939 # Determine if gearboxing is necessary and whether it needs to be
1940 # gearboxed up or just sliced down.
1941 if output_bitwidth == input_padded_bitwidth:
1942 upstream_data_bits = client_data
1943 upstream_valid = client_valid
1944 ready_for_client.assign(upstream_ready)
1945 tag = client_tag_and_data.tag
1946 address = client_tag_and_data.address
1947 valid_bytes = valid_bytes_type(input_bitwidth_bytes)
1948 last = Bits(1)(1)
1949 elif output_bitwidth > input_padded_bitwidth:
1950 upstream_data_bits = client_data.as_bits(output_bitwidth)
1951 upstream_valid = client_valid
1952 ready_for_client.assign(upstream_ready)
1953 tag = client_tag_and_data.tag
1954 address = client_tag_and_data.address
1955 valid_bytes = valid_bytes_type(input_bitwidth_bytes)
1956 last = Bits(1)(1)
1957 else:
1958 # Create registers equal to the number of upstream transactions needed
1959 # to complete the transmission.
1960 num_chunks = TaggedWriteGearboxImpl.num_chunks
1961 num_chunks_idx_bitwidth = clog2(num_chunks)
1962 if input_padded_bitwidth % output_bitwidth == 0:
1963 padding_numbits = 0
1964 else:
1965 padding_numbits = output_bitwidth - (input_padded_bitwidth %
1966 output_bitwidth)
1967 client_data_padded = BitsSignal.concat(
1968 [Bits(padding_numbits)(0), client_data])
1969 chunks = [
1970 client_data_padded[i * output_bitwidth:(i + 1) * output_bitwidth]
1971 for i in range(num_chunks)
1972 ]
1973 chunk_regs = [
1974 c.reg(ports.clk, ce=client_xact, name=f"chunk_{idx}")
1975 for idx, c in enumerate(chunks)
1976 ]
1977 increment = Wire(Bits(1))
1978 clear = Wire(Bits(1))
1979 counter = Counter(num_chunks_idx_bitwidth)(clk=ports.clk,
1980 rst=ports.rst,
1981 increment=increment,
1982 clear=clear)
1983 # A tree of 2:1 muxes avoids dynamic wide packed-array selects, which
1984 # crash Verilator 5.040 above its 2048-bit expansion limit.
1985 mux_values = chunk_regs + [chunk_regs[-1]] * (
1986 (1 << num_chunks_idx_bitwidth) - num_chunks)
1987 for level in range(num_chunks_idx_bitwidth):
1988 mux_values = [
1989 Mux(counter.out.as_bits()[level], mux_values[i],
1990 mux_values[i + 1]) for i in range(0, len(mux_values), 2)
1991 ]
1992 upstream_data_bits = mux_values[0]
1993 upstream_valid = ControlReg(ports.clk, ports.rst, [client_xact],
1994 [clear])
1995 upstream_xact = upstream_valid & upstream_ready
1996 clear.assign(upstream_xact & (counter.out == (num_chunks - 1)))
1997 increment.assign(upstream_xact)
1998 ready_for_client.assign(~upstream_valid)
1999 address_padding_bits = clog2(output_bitwidth_bytes)
2000 counter_bytes = BitsSignal.concat(
2001 [counter.out.as_bits(),
2002 Bits(address_padding_bits)(0)]).as_uint()
2003
2004 # Construct the output channel. Shared logic across all three cases.
2005 tag_reg = client_tag_and_data.tag.reg(ports.clk,
2006 ce=client_xact,
2007 name="tag_reg")
2008 addr_reg = client_tag_and_data.address.reg(ports.clk,
2009 ce=client_xact,
2010 name="address_reg")
2011 address = (addr_reg + counter_bytes).as_uint(64)
2012 tag = tag_reg
2013 elem_end = counter.out == (num_chunks - 1)
2014 valid_bytes = Mux(
2015 elem_end, valid_bytes_type(output_bitwidth_bytes),
2016 valid_bytes_type((output_bitwidth - padding_numbits) // 8))
2017 if max_burst_words and num_chunks > max_burst_words:
2018 # Max-payload-size cap: end the upstream write transaction at the
2019 # element end OR every max_burst_words engine words, whichever comes
2020 # first, so a wide element's write is split into <= max_burst_bytes
2021 # transactions. Each word keeps its own sequential address; only the
2022 # transaction-framing 'last' changes.
2023 burst_shift = clog2(max_burst_words)
2024 burst_end = (counter.out.as_bits()[:burst_shift].and_reduce()
2025 if burst_shift else Bits(1)(1))
2026 last = elem_end | burst_end
2027 else:
2028 last = elem_end
2029
2030 upstream_channel, upstrm_ready_sig = TaggedWriteGearboxImpl.out.type.wrap(
2031 {
2032 "address": address,
2033 "tag": tag,
2034 "data": upstream_data_bits,
2035 "valid_bytes": valid_bytes,
2036 "last": last,
2037 }, upstream_valid)
2038 upstream_ready.assign(upstrm_ready_sig)
2039 ports.out = upstream_channel
2040
2041 return TaggedWriteGearboxImpl
2042
2043
2044@modparams
2045def EmitEveryN(message_type: Type, N: int) -> type['EmitEveryNImpl']:
2046 """Emit (forward) one message for every N input messages. The emitted message
2047 is the last one of the N received. N must be >= 1."""
2048
2049 if N < 1:
2050 raise ValueError("N must be >= 1")
2051
2052 class EmitEveryNImpl(Module):
2053 clk = Clock()
2054 rst = Reset()
2055 in_ = InputChannel(message_type)
2056 out = OutputChannel(message_type)
2057
2058 @generator
2059 def build(ports):
2060 ready_for_in = Wire(Bits(1))
2061 in_data, in_valid = ports.in_.unwrap(ready_for_in)
2062 xact = in_valid & ready_for_in
2063
2064 # Fast path: N == 1 -> pass-through.
2065 if N == 1:
2066 out_chan, out_ready = EmitEveryNImpl.out.type.wrap(in_data, in_valid)
2067 ready_for_in.assign(out_ready)
2068 ports.out = out_chan
2069 return
2070
2071 counter_width = clog2(N)
2072 counter_clear = Wire(Bits(1))
2073 counter = Counter(counter_width)(clk=ports.clk,
2074 rst=ports.rst,
2075 increment=xact,
2076 clear=counter_clear)
2077
2078 # Capture last message of the group.
2079 last_msg = in_data.reg(ports.clk, ports.rst, ce=xact, name="last_msg")
2080 # Clear the counter.
2081 hit_last = (counter.out == UInt(counter_width)(N - 1)) & xact
2082 counter_clear.assign(hit_last)
2083
2084 emit_accepted = Wire(Bits(1))
2085 out_valid = ControlReg(ports.clk, ports.rst, [hit_last], [emit_accepted])
2086
2087 out_chan, out_ready = EmitEveryNImpl.out.type.wrap(last_msg, out_valid)
2088 # Stall input while waiting for downstream to accept the aggregated output.
2089 ready_for_in.assign(~(out_valid & ~out_ready))
2090 emit_accepted.assign(out_valid & out_ready) # Output consumed downstream.
2091
2092 ports.out = out_chan
2093
2094 return EmitEveryNImpl
2095
2096
2098 write_width: int,
2099 hostmem_module,
2100 reqs: List[esi._OutputBundleSetter],
2101 max_write_payload_bytes: int = DEFAULT_MAX_WRITE_PAYLOAD_BYTES
2102) -> type["HostMemWriteProcessorImpl"]:
2103 """Construct a host memory write request module to orchestrate the the write
2104 connections. Responsible for both gearboxing the data, multiplexing the
2105 requests, reassembling out-of-order responses and routing the responses to the
2106 correct clients.
2107
2108 Generate this module dynamically to allow for multiple write clients of
2109 multiple types to be directly accomodated."""
2110
2111 class HostMemWriteProcessorImpl(Module):
2112
2113 clk = Clock()
2114 rst = Reset()
2115
2116 # Add an output port for each read client.
2117 reqPortMap: Dict[esi._OutputBundleSetter, str] = {}
2118 for req in reqs:
2119 name = "client_" + req.client_name_str
2120 locals()[name] = Output(req.type)
2121 reqPortMap[req] = name
2122
2123 # And then the port which goes to the host.
2124 upstream = Output(hostmem_module.write.type)
2125
2126 @generator
2127 def build(ports):
2128 clk = ports.clk
2129 rst = ports.rst
2130
2131 # Width of the frame's 'data_size' field: log2 of the number of bytes per
2132 # engine word. It holds (valid_bytes - 1) for the final (possibly partial)
2133 # word of a write.
2134 size_width = clog2(write_width // 8)
2135
2136 # If there's no write clients, just create a no-op write bundle
2137 if len(reqs) == 0:
2138 req, _ = Channel(hostmem_module.UpstreamWriteReq).wrap(
2139 {
2140 "address": 0,
2141 "tag": 0,
2142 "data": 0,
2143 "data_size": 0,
2144 "last": 0,
2145 }, 0)
2146 write_bundle, _ = hostmem_module.write.type.pack(req=req)
2147 ports.upstream = write_bundle
2148 return
2149
2150 assert len(reqs) <= 256, "More than 256 write clients not supported."
2151
2152 upstream_req_channel = Wire(Channel(hostmem_module.UpstreamWriteReq))
2153 upstream_write_bundle, froms = hostmem_module.write.type.pack(
2154 req=upstream_req_channel)
2155 ports.upstream = upstream_write_bundle
2156 upstream_ack_tag = froms["ackTag"]
2157
2158 demuxed_acks = esi.TaggedDemux(len(reqs), upstream_ack_tag.type)(
2159 clk=ports.clk, rst=ports.rst, in_=upstream_ack_tag)
2160
2161 # TODO: re-write the tags and store the client and client tag.
2162
2163 # Build the write request channels and ack wires.
2164 write_channels: List[ChannelSignal] = []
2165 for idx, req in enumerate(reqs):
2166 # Get the request channel and its data type.
2167 reqch = [c.channel for c in req.type.channels if c.name == 'req'][0]
2168 client_type = reqch.inner_type
2169 input_flit_ack = Wire(upstream_ack_tag.type)
2170
2171 if isinstance(client_type, Window):
2172 # Windowed (list) write: the client streams a list of elements to be
2173 # written to sequential addresses from a base. Lowered frame:
2174 # struct{address, tag, data: elem[num_items], data_size, last}. One
2175 # element per frame (num_items=1 here).
2176 bundle_sig, wfroms = req.type.pack(ackTag=input_flit_ack)
2177 windowed_req = wfroms["req"]
2178 lowered = client_type.lowered_type
2179 array_type = dict(lowered.fields)["data"]
2180 element_bits = array_type.element_type.bitwidth
2181 # Elements are packed contiguously in host memory at their natural
2182 # byte size, independent of the engine word width (matches the
2183 # read_list path). Each per-element write is byte-enabled via
2184 # data_size, so a sub-word element writes only its own bytes.
2185 elem_stride = (element_bits + 7) // 8
2186
2187 gearbox_mod = TaggedWriteGearbox(element_bits, write_width,
2188 max_write_payload_bytes)
2189 gearbox_in_type = gearbox_mod.in_.type.inner_type
2190
2191 # Unwrap the window frames; compute a base+offset address from a
2192 # per-burst element counter (reset after each burst's final element).
2193 ready_for_frame = Wire(Bits(1))
2194 frame_win, frame_valid = windowed_req.unwrap(ready_for_frame)
2195 frame = frame_win.unwrap()
2196 frame_xact = frame_valid & ready_for_frame
2197 elem_clear = Wire(Bits(1))
2198 elem_counter = Counter(64)(clk=ports.clk,
2199 rst=ports.rst,
2200 clear=elem_clear,
2201 increment=frame_xact)
2202 elem_clear.assign(frame_xact & frame["last"])
2203 elem_addr = (frame["address"] +
2204 elem_counter.out * UInt(64)(elem_stride)).as_uint(64)
2205 gearbox_in_chan, gearbox_in_ready = Channel(gearbox_in_type).wrap(
2206 gearbox_in_type({
2207 "tag": frame["tag"],
2208 "address": elem_addr,
2209 "data": frame["data"][0].bitcast(gearbox_in_type.data),
2210 }), frame_valid)
2211 ready_for_frame.assign(gearbox_in_ready)
2212 gearbox = gearbox_mod(clk=ports.clk,
2213 rst=ports.rst,
2214 in_=gearbox_in_chan)
2215 else:
2216 # Single-message write.
2217 write_req_bundle_type = esi.HostMem.write_req_bundle_type(
2218 client_type.data)
2219 bundle_sig, sfroms = write_req_bundle_type.pack(ackTag=input_flit_ack)
2220 gearbox_mod = TaggedWriteGearbox(client_type.data.bitwidth,
2221 write_width, max_write_payload_bytes)
2222 gearbox_in_type = gearbox_mod.in_.type.inner_type
2223 bitcast_client_req = sfroms["req"].transform(
2224 lambda m, git=gearbox_in_type: git({
2225 "tag": m.tag,
2226 "address": m.address,
2227 "data": m.data.bitcast(git.data)
2228 }))
2229 gearbox = gearbox_mod(clk=ports.clk,
2230 rst=ports.rst,
2231 in_=bitcast_client_req)
2232
2233 write_channels.append(
2234 gearbox.out.transform(
2235 lambda m, idx=idx: hostmem_module.UpstreamWriteReq({
2236 "address":
2237 m.address,
2238 "tag":
2239 idx,
2240 "data":
2241 m.data,
2242 "data_size": (m.valid_bytes.as_uint() - UInt(size_width + 1)
2243 (1)).as_bits()[:size_width],
2244 "last":
2245 m.last,
2246 })))
2247
2248 # Count the number of acks received from hostmem for this client
2249 # and only send one back to the client per input.
2250 ack_every_n = EmitEveryN(upstream_ack_tag.type, gearbox_mod.num_chunks)(
2251 clk=clk, rst=rst, in_=demuxed_acks.get_out(idx))
2252 input_flit_ack.assign(ack_every_n.out)
2253
2254 # Set the port for the client request.
2255 setattr(ports, HostMemWriteProcessorImpl.reqPortMap[req], bundle_sig)
2256
2257 # Multiplex the write requests onto the single upstream channel with the
2258 # list-aware, pipelined ChannelArbiter (matching the read side). A real
2259 # windowed write (multi-word client flits) engages the arbiter's list-
2260 # awareness -- via the frame's 'last' -- to keep a client's words
2261 # contiguous; single-word (<= engine width) clients emit one message per
2262 # word, for which single-flit arbitration is correct.
2263 # `mux_pipeline_levels=2` retimes the (wide -- a full engine word plus
2264 # address) payload selection mux; the added latency is absorbed by the
2265 # arbiter's output FIFO / credit counter.
2266 muxed_write_channel = ChannelArbiter(write_channels,
2267 ports.clk,
2268 ports.rst,
2269 mux_pipeline_levels=2,
2270 pipelined_scheduler=True,
2271 telemetry=False)
2272 upstream_req_channel.assign(muxed_write_channel)
2273
2274 return HostMemWriteProcessorImpl
2275
2276
2277@modparams
2279 read_width: int,
2280 write_width: int,
2281 max_read_request_bytes: int = DEFAULT_MAX_READ_REQUEST_BYTES,
2282 max_write_payload_bytes: int = DEFAULT_MAX_WRITE_PAYLOAD_BYTES,
2283 max_outstanding_reads: int = 1,
2284 round_read_lengths: bool = True) -> typing.Type['ChannelHostMemImpl']:
2285 """HostMem service multiplexing every client onto one read and one write
2286 bundle. `max_outstanding_reads` is the per-client limit on logical read
2287 requests in flight (see `HostMemReadReqSplitter`); values above 1 require the
2288 upstream to return each client's read responses in request order.
2289 `round_read_lengths` selects word-granular upstream reads (the default);
2290 byte-granular transports can disable it to avoid reading padding bytes."""
2291
2292 class ChannelHostMemImpl(esi.ServiceImplementation):
2293 """Builds a HostMem service which multiplexes multiple HostMem clients into
2294 two (read and write) bundles of the given data width."""
2295
2296 clk = Clock()
2297 rst = Reset()
2298
2299 UpstreamReadReq = StructType([
2300 ("address", UInt(64)),
2301 ("length", UInt(32)), # In bytes.
2302 ("tag", UInt(8)),
2303 ])
2304 read = Output(
2305 Bundle([
2306 BundledChannel("req", ChannelDirection.TO, UpstreamReadReq),
2307 BundledChannel(
2308 "resp", ChannelDirection.FROM,
2309 StructType([
2310 ("tag", esi.HostMem.TagType),
2311 ("data", Bits(read_width)),
2312 ("last", Bits(1)),
2313 ])),
2314 ]))
2315
2316 if write_width % 8 != 0:
2317 raise ValueError("Write width must be a multiple of 8.")
2318 UpstreamWriteReq = StructType([
2319 ("address", UInt(64)),
2320 ("tag", UInt(8)),
2321 ("data", Bits(write_width)),
2322 ("data_size", Bits(clog2(write_width // 8))),
2323 ("last", Bits(1)),
2324 ])
2325 write = Output(
2326 Bundle([
2327 BundledChannel("req", ChannelDirection.TO, UpstreamWriteReq),
2328 BundledChannel("ackTag", ChannelDirection.FROM, UInt(8)),
2329 ]))
2330
2331 @generator
2332 def generate(ports, bundles: esi._ServiceGeneratorBundles):
2333 # Split the read side out into a separate module. Must assign the output
2334 # ports to the clients since we can't service a request in a different
2335 # module.
2336 read_reqs = [
2337 req for req in bundles.to_client_reqs
2338 if req.port in ('read', 'read_list')
2339 ]
2340 read_proc_module = HostmemReadProcessor(read_width, ChannelHostMemImpl,
2341 read_reqs, max_read_request_bytes,
2342 max_outstanding_reads,
2343 round_read_lengths)
2344 read_proc = read_proc_module(clk=ports.clk, rst=ports.rst)
2345 ports.read = read_proc.upstream
2346 for req in read_reqs:
2347 req.assign(getattr(read_proc, read_proc_module.reqPortMap[req]))
2348
2349 # The write side.
2350 write_reqs = [
2351 req for req in bundles.to_client_reqs if req.port == 'write'
2352 ]
2353 write_proc_module = HostMemWriteProcessor(write_width, ChannelHostMemImpl,
2354 write_reqs,
2355 max_write_payload_bytes)
2356 write_proc = write_proc_module(clk=ports.clk, rst=ports.rst)
2357 ports.write = write_proc.upstream
2358 for req in write_reqs:
2359 req.assign(getattr(write_proc, write_proc_module.reqPortMap[req]))
2360
2361 return ChannelHostMemImpl
2362
2363
2364@modparams
2365def DummyToHostEngine(client_type: Type) -> type['DummyToHostEngineImpl']:
2366 """Create a fake DMA engine which just throws everything away."""
2367
2368 class DummyToHostEngineImpl(esi.EngineModule):
2369
2370 @property
2371 def TypeName(self):
2372 return "DummyToHostEngine"
2373
2374 clk = Clock()
2375 rst = Reset()
2376 input_channel = InputChannel(client_type)
2377
2378 @generator
2379 def build(ports):
2380 pass
2381
2382 return DummyToHostEngineImpl
2383
2384
2385@modparams
2386def DummyFromHostEngine(client_type: Type) -> type['DummyFromHostEngineImpl']:
2387 """Create a fake DMA engine which just never produces messages."""
2388
2389 class DummyFromHostEngineImpl(esi.EngineModule):
2390
2391 @property
2392 def TypeName(self):
2393 return "DummyFromHostEngine"
2394
2395 clk = Clock()
2396 rst = Reset()
2397 output_channel = OutputChannel(client_type)
2398
2399 @generator
2400 def build(ports):
2401 valid = Bits(1)(0)
2402 data = Bits(client_type.bitwidth)(0).bitcast(client_type)
2403 channel, ready = Channel(client_type).wrap(data, valid)
2404 ports.output_channel = channel
2405
2406 return DummyFromHostEngineImpl
2407
2408
2409def _resolve_engine_pair(path: str) -> Tuple[Callable, Callable]:
2410 """Resolve a dotted Python import path to a
2411 `(to_host_engine_gen, from_host_engine_gen)` tuple, used to override the
2412 default engine pair for a specific service request.
2413
2414 The path may point at either:
2415 - a module-level 2-tuple attribute, e.g.
2416 `"mypkg.mymod.MyEnginePair"` where `MyEnginePair` is
2417 `(MyToHost, MyFromHost)`; or
2418 - a zero-arg factory callable returning such a tuple.
2419 """
2420 import importlib
2421 if not isinstance(path, str):
2422 raise TypeError(
2423 "Engine override path must be a dotted 'pkg.mod.attr' string; "
2424 f"got {type(path).__name__}")
2425 module_path, _, attr_path = path.rpartition(".")
2426 if not module_path or not attr_path:
2427 raise ValueError(
2428 "Engine override path must be a dotted 'pkg.mod.attr' string; "
2429 f"got {path!r}")
2430 obj = importlib.import_module(module_path)
2431 for part in attr_path.split("."):
2432 obj = getattr(obj, part)
2433 if callable(obj):
2434 obj = obj()
2435 if not (isinstance(obj, tuple) and len(obj) == 2):
2436 raise TypeError(
2437 f"Engine override {path!r} must resolve to a 2-tuple "
2438 f"(to_host_engine_gen, from_host_engine_gen); got {type(obj).__name__}")
2439 if not (callable(obj[0]) and callable(obj[1])):
2440 raise TypeError(
2441 f"Engine override {path!r} must resolve to a 2-tuple of callables; got "
2442 f"({type(obj[0]).__name__}, {type(obj[1]).__name__})")
2443 return obj
2444
2445
2447 to_host_engine_gen: Callable,
2448 from_host_engine_gen: Callable) -> type['ChannelEngineService']:
2449 """Returns a channel service implementation which calls
2450 to_host_engine_gen(<client_type>) or from_host_engine_gen(<client_type>) to
2451 generate the to_host and from_host engines for each channel. Does not support
2452 engines which can service multiple clients at once.
2453
2454 Individual service requests may override the default engine pair by passing
2455 `options={"engine": "pkg.mod.attr"}` at the service-request call site (e.g.
2456 `HostComms.some_bundle(AppID(...), options={"engine": "..."})`). The path
2457 is resolved by `_resolve_engine_pair` and must yield a
2458 `(to_host_engine_gen, from_host_engine_gen)` tuple with the same call shape
2459 as the defaults; the override applies to every channel of that request's
2460 bundle.
2461 """
2462
2463 class ChannelEngineService(esi.ServiceImplementation):
2464 """Service implementation which services the clients via a per-channel DMA
2465 engine."""
2466
2467 clk = Clock()
2468 rst = Reset()
2469
2470 @generator
2471 def build(ports, bundles: esi._ServiceGeneratorBundles):
2472 clk = ports.clk
2473 rst = ports.rst
2474
2475 def build_engine_appid(client_appid: List[esi.AppID],
2476 channel_name: str) -> str:
2477 appid_strings = [str(appid) for appid in client_appid]
2478 return f"{'_'.join(appid_strings)}.{channel_name}"
2479
2480 def build_engine(bc: BundledChannel,
2481 bundle_to_host_gen: Callable,
2482 bundle_from_host_gen: Callable,
2483 input_channel=None) -> Type:
2484 idbase = build_engine_appid(bundle.client_name, bc.name)
2485 eng_appid = esi.AppID(idbase)
2486 # DMA engines require at least 1 byte of data; substitute Bits(8)
2487 # for zero-width (void) channel types so the engine never sees a
2488 # zero-length transfer.
2489 engine_client_type = bc.channel.inner_type
2490 is_void = (engine_client_type.bitwidth == 0)
2491 if is_void:
2492 engine_client_type = Bits(8)
2493 if bc.direction == ChannelDirection.FROM:
2494 engine_mod = bundle_to_host_gen(engine_client_type)
2495 else:
2496 engine_mod = bundle_from_host_gen(engine_client_type)
2497 eng_inputs = {
2498 "clk": ports.clk,
2499 "rst": ports.rst,
2500 }
2501 eng_details: Dict[str, object] = {"engine_inst": eng_appid}
2502 if input_channel is not None:
2503 # For void channels, widen the 0-bit input to the 8-bit
2504 # placeholder the engine expects.
2505 if is_void:
2506 input_channel = input_channel.transform(lambda _: Bits(8)(0))
2507 if (engine_mod.input_channel.type.signaling
2508 != input_channel.type.signaling):
2509 input_channel = input_channel.buffer(
2510 clk,
2511 rst,
2512 stages=1,
2513 output_signaling=engine_mod.input_channel.type.signaling)
2514 eng_inputs["input_channel"] = input_channel
2515 if hasattr(engine_mod, "mmio"):
2516 mmio_appid = esi.AppID(idbase + ".mmio")
2517 eng_inputs["mmio"] = esi.MMIO.read_write(mmio_appid)
2518 eng_details["mmio"] = mmio_appid
2519 if hasattr(engine_mod, "hostmem_write"):
2520 eng_inputs["hostmem_write"] = esi.HostMem.write_from_bundle(
2521 esi.AppID(idbase + ".hostmem_write"),
2522 engine_mod.hostmem_write.type,
2523 options=bundle.options)
2524 if hasattr(engine_mod, "hostmem_read"):
2525 eng_inputs["hostmem_read"] = esi.HostMem.read_from_bundle(
2526 esi.AppID(idbase + ".hostmem_read"),
2527 engine_mod.hostmem_read.type,
2528 options=bundle.options)
2529 if hasattr(engine_mod, "hostmem_read_list"):
2530 eng_inputs["hostmem_read_list"] = esi.HostMem.read_list_from_bundle(
2531 esi.AppID(idbase + ".hostmem_read_list"),
2532 engine_mod.hostmem_read_list.type,
2533 options=bundle.options)
2534 engine = engine_mod(appid=eng_appid, **eng_inputs)
2535 engine_rec = bundles.emit_engine(engine, details=eng_details)
2536 engine_rec.add_record(bundle, {bc.name: {}})
2537 return engine
2538
2539 for bundle in bundles.to_client_reqs:
2540 # Per-request engine override: if the client's service request carries
2541 # an `"engine"` option, use that engine pair instead of the defaults
2542 # for every channel of this bundle. This is purely a hardware-side
2543 # substitution.
2544 engine_override = bundle.options.get("engine")
2545 if engine_override is None:
2546 bundle_to_host_gen = to_host_engine_gen
2547 bundle_from_host_gen = from_host_engine_gen
2548 else:
2549 bundle_to_host_gen, bundle_from_host_gen = _resolve_engine_pair(
2550 engine_override)
2551
2552 bundle_type = bundle.type
2553 to_channels = {}
2554 # Create a DMA engine for each channel headed TO the client (from the host).
2555 for bc in bundle_type.channels:
2556 if bc.direction == ChannelDirection.TO:
2557 engine = build_engine(bc, bundle_to_host_gen, bundle_from_host_gen)
2558 out_chan = engine.output_channel
2559 # For void channels, narrow the 8-bit placeholder back to 0-bit.
2560 if bc.channel.inner_type.bitwidth == 0:
2561 out_chan = out_chan.transform(lambda _: Bits(0)(0))
2562 to_channels[bc.name] = out_chan
2563
2564 client_bundle_sig, froms = bundle_type.pack(**to_channels)
2565 bundle.assign(client_bundle_sig)
2566
2567 # Create a DMA engine for each channel headed FROM the client (to the host).
2568 for bc in bundle_type.channels:
2569 if bc.direction == ChannelDirection.FROM:
2570 build_engine(bc, bundle_to_host_gen, bundle_from_host_gen,
2571 froms[bc.name])
2572
2573 return ChannelEngineService
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))
@ Input
Definition HW.h:42
@ Output
Definition HW.h:42
build_read(ports, int manifest_loc, Dict[int, Tuple[Optional[int], AssignableSignal]] table)
Definition common.py:702
Tuple[Dict[int, Tuple[Optional[int], AssignableSignal]], int] build_table(bundles)
Definition common.py:647
generate(ports, esi._ServiceGeneratorBundles bundles)
Definition common.py:640
bool generate(ports, esi._ServiceGeneratorBundles bundles)
Definition common.py:806
type["ChannelDemuxNImpl"] ChannelDemuxN_HalfStage_ReadyBlocking(Type data_type, int num_outs, int next_sel_width)
Definition common.py:176
type["ChannelDemuxTree"] ChannelDemuxTree_HalfStage_ReadyBlocking(Type data_type, int num_outs, int branching_factor_log2)
Definition common.py:269
type["ShiftReadGearboxImpl"] ShiftReadGearbox(int input_bitwidth, int output_bitwidth)
Definition common.py:1209
select_read_gearbox(bool is_list, int input_bitwidth, int output_bitwidth)
Definition common.py:1355
type["DesignResetControllerImpl"] DesignResetController(int delay_cycles)
Definition common.py:540
Tuple[Callable, Callable] _resolve_engine_pair(str path)
Definition common.py:2409
type["MMIOPrefixRouterImpl"] MMIOPrefixRouter(Tuple[Tuple[int, Optional[int]],...] regions)
Definition common.py:379
type["SliceReadGearboxImpl"] SliceReadGearbox(int input_bitwidth, int output_bitwidth)
Definition common.py:959
type[ 'ChannelEngineService'] ChannelEngineService(Callable to_host_engine_gen, Callable from_host_engine_gen)
Definition common.py:2448
Module HeaderMMIO(int manifest_loc)
Definition common.py:75
type["ConcatReadGearboxImpl"] ConcatReadGearbox(int input_bitwidth, int output_bitwidth)
Definition common.py:1009
type[ 'DummyToHostEngineImpl'] DummyToHostEngine(Type client_type)
Definition common.py:2365
HostmemReadProcessor(int read_width, hostmem_module, List[esi._OutputBundleSetter] reqs, int max_read_request_bytes=DEFAULT_MAX_READ_REQUEST_BYTES, int max_outstanding_reads=1, bool round_read_lengths=True)
Definition common.py:1682
type[ 'DummyFromHostEngineImpl'] DummyFromHostEngine(Type client_type)
Definition common.py:2386
type["TaggedWriteGearboxImpl"] TaggedWriteGearbox(int input_bitwidth, int output_bitwidth, int max_burst_bytes)
Definition common.py:1882
type["DepackReadGearboxImpl"] DepackReadGearbox(int input_bitwidth, int output_bitwidth)
Definition common.py:1114
HostMemReadReqSplitter(Channel req_channel_type, Channel resp_channel_type, int max_chunk_bytes, int max_outstanding=1, bool round_read_lengths=True)
Definition common.py:1395
type[ 'EmitEveryNImpl'] EmitEveryN(Type message_type, int N)
Definition common.py:2045
type["HostMemWriteProcessorImpl"] HostMemWriteProcessor(int write_width, hostmem_module, List[esi._OutputBundleSetter] reqs, int max_write_payload_bytes=DEFAULT_MAX_WRITE_PAYLOAD_BYTES)
Definition common.py:2102
typing.Type[ 'ChannelHostMemImpl'] ChannelHostMem(int read_width, int write_width, int max_read_request_bytes=DEFAULT_MAX_READ_REQUEST_BYTES, int max_write_payload_bytes=DEFAULT_MAX_WRITE_PAYLOAD_BYTES, int max_outstanding_reads=1, bool round_read_lengths=True)
Definition common.py:2284