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