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 resp_channel = esi.ChannelMux(client_data_channels)
588 data_resp_channel.assign(resp_channel)
589
590 # The header surfaces a reset request when the host writes the reset magic
591 # number to slot 7. Propagate it up to the caller (the BSP).
592 ports.reset_request = header.reset_request
593
594 @staticmethod
595 def build_addr_read(read_addr_chan: ChannelSignal, num_clients: int,
596 manifest_loc: int) -> Tuple[BitsSignal, ChannelSignal]:
597 """Build a channel for the address read request. Returns the index to select
598 the client and a channel for the masked address to be passed to the
599 clients."""
600
601 # Decoding the selection bits is very simple as of now. This might need to
602 # change to support more flexibility in addressing. Not clear if what we're
603 # doing now it sufficient or not.
604
605 manifest_loc_const = UInt(32)(manifest_loc)
606
607 cmd_ready_wire = Wire(Bits(1))
608 cmd, cmd_valid = read_addr_chan.unwrap(cmd_ready_wire)
609 is_manifest_read = cmd.offset >= manifest_loc_const
610 sel_bits = NamedWire(Bits(32 - ChannelMMIO.RegisterSpaceBits), "sel_bits")
611 # If reading the manifest, override the selection to select the manifest instead.
612 sel_bits.assign(
613 Mux(is_manifest_read,
614 cmd.offset.as_bits()[ChannelMMIO.RegisterSpaceBits:],
615 Bits(32 - ChannelMMIO.RegisterSpaceBits)(num_clients - 1)))
616 regular_client_offset = (cmd.offset.as_bits() &
617 Bits(32)(ChannelMMIO.AddressMask)).as_uint()
618 offset = Mux(is_manifest_read, regular_client_offset,
619 (cmd.offset - manifest_loc_const).as_uint(32))
620 client_cmd = NamedWire(esi.MMIOReadWriteCmdType, "client_cmd")
621 client_cmd.assign(
622 esi.MMIOReadWriteCmdType({
623 "write": cmd.write,
624 "offset": offset,
625 "data": cmd.data
626 }))
627 client_addr_chan, client_addr_ready = Channel(
628 esi.MMIOReadWriteCmdType).wrap(client_cmd, cmd_valid)
629 cmd_ready_wire.assign(client_addr_ready)
630 return sel_bits, client_addr_chan
631
632
633class MMIOIndirection(Module):
634 """Some platforms do not support MMIO space greater than a certain size (e.g.
635 Vitis 2022's limit is 4k). This module implements a level of indirection to
636 provide access to a full 32-bit address space.
637
638 MMIO addresses:
639 - 0x0: 0 constant
640 - 0x8: 64 bit ESI magic number for Indirect MMIO (0x312bf0cc_E5100E51)
641 - 0x10: Version number for Indirect MMIO (0)
642 - 0x18: Location of read/write in the virtual MMIO space.
643 - 0x20: A read from this location will initiate a read in the virtual MMIO
644 space specified by the address stored in 0x18 and return the result.
645 A write to this location will initiate a write into the virtual MMIO
646 space to the virtual address specified in 0x18.
647 """
648 clk = Clock()
649 rst = Reset()
650
651 upstream = Input(esi.MMIO.read_write.type)
652 downstream = Output(esi.MMIO.read_write.type)
653
654 @generator
655 def build(ports):
656 # This implementation assumes there is only one outstanding upstream MMIO
657 # transaction in flight at once. TODO: enforce this or make it more robust.
658
659 reg_bits = 8
660 location_reg = UInt(reg_bits)(0x18)
661 indirect_mmio_reg = UInt(reg_bits)(0x20)
662 virt_address = Wire(UInt(32))
663
664 # Set up the upstream MMIO interface. Capture last upstream command in a
665 # mailbox which never empties to give access to the last command for all
666 # time.
667 upstream_resp_chan_wire = Wire(Channel(esi.MMIODataType))
668 upstream_cmd_chan = ports.upstream.unpack(
669 data=upstream_resp_chan_wire)["cmd"]
670 _, _, upstream_cmd_data = upstream_cmd_chan.snoop()
671
672 # Set up a channel demux to separate the MMIO commands which get processed
673 # locally with ones which should be transformed and fowarded downstream.
674 phys_loc = upstream_cmd_data.offset.as_uint(reg_bits)
675 fwd_upstream = NamedWire(phys_loc == indirect_mmio_reg, "fwd_upstream")
676 local_reg_cmd_chan, downstream_cmd_channel = esi.ChannelDemux(
677 upstream_cmd_chan, fwd_upstream, 2, "upstream_demux")
678
679 # Set up the downstream MMIO interface.
680 downstream_cmd_channel = downstream_cmd_channel.transform(
681 lambda cmd: esi.MMIOReadWriteCmdType({
682 "write": cmd.write,
683 "offset": virt_address,
684 "data": cmd.data
685 }))
686 ports.downstream, froms = esi.MMIO.read_write.type.pack(
687 cmd=downstream_cmd_channel)
688 downstream_data_chan = froms["data"]
689
690 # Process local regs.
691 (local_reg_cmd_valid, local_reg_cmd_ready,
692 local_reg_cmd) = local_reg_cmd_chan.snoop()
693 write_virt_address = (local_reg_cmd_valid & local_reg_cmd_ready &
694 local_reg_cmd.write & (phys_loc == location_reg))
695 virt_address.assign(
696 local_reg_cmd.data.as_uint(32).reg(
697 name="virt_address",
698 clk=ports.clk,
699 ce=write_virt_address,
700 ))
701
702 # Build the pysical MMIO register space.
703 local_reg_resp_array = Array(Bits(64), 4)([
704 0x0, # 0x0
705 IndirectionMagicNumber, # 0x8
706 IndirectionVersionNumber, # 0x10
707 virt_address.as_bits(64), # 0x18
708 ])
709 local_reg_resp_chan = local_reg_cmd_chan.transform(
710 lambda cmd: local_reg_resp_array[cmd.offset.as_uint(2)])
711
712 # Mux together the local register responses and the downstream data to
713 # create the upstream response.
714 upstream_resp = esi.ChannelMux([local_reg_resp_chan, downstream_data_chan])
715 upstream_resp_chan_wire.assign(upstream_resp)
716
717
718@modparams
719def SliceReadGearbox(input_bitwidth: int,
720 output_bitwidth: int) -> type["SliceReadGearboxImpl"]:
721 """Narrow one engine word to a single-message client element no wider than the
722 word (``OUT <= IN``). The element sits in the word's low bits, so the datapath
723 is a slice; ``valid_bytes`` is unused (a single element is never a partial
724 word). Wider single elements use `ConcatReadGearbox`; packed list reads use
725 `DepackReadGearbox`/`ShiftReadGearbox`."""
726
727 if input_bitwidth <= 0 or input_bitwidth % 8 != 0:
728 raise ValueError("engine word width must be a positive multiple of 8 bits")
729 if not 0 < output_bitwidth <= input_bitwidth:
730 raise ValueError("SliceReadGearbox requires 0 < output <= input")
731
732 in_bytes = input_bitwidth // 8
733 vb_width = clog2(in_bytes)
734
735 class SliceReadGearboxImpl(Module):
736 clk = Clock()
737 rst = Reset()
738 in_ = InputChannel(
739 StructType([
740 ("tag", esi.HostMem.TagType),
741 ("data", Bits(input_bitwidth)),
742 ("valid_bytes", UInt(vb_width)),
743 ("last", Bits(1)),
744 ]))
745 out = OutputChannel(
746 StructType([
747 ("tag", esi.HostMem.TagType),
748 ("data", Bits(output_bitwidth)),
749 ("last", Bits(1)),
750 ]))
751
752 @generator
753 def build(ports):
754 up_ready = Wire(Bits(1), name="up_ready")
755 up, up_valid = ports.in_.unwrap(up_ready)
756 client_channel, client_ready = SliceReadGearboxImpl.out.type.wrap(
757 {
758 "tag": up.tag,
759 "data": up.data[:output_bitwidth],
760 "last": up.last,
761 }, up_valid)
762 up_ready.assign(client_ready)
763 ports.out = client_channel
764
765 return SliceReadGearboxImpl
766
767
768@modparams
769def ConcatReadGearbox(input_bitwidth: int,
770 output_bitwidth: int) -> type["ConcatReadGearboxImpl"]:
771 """Concatenate ``ceil(OUT/IN)`` consecutive engine words into one client
772 element wider than the word (``OUT > IN``). Serves single-message reads (any
773 ``OUT > IN``; the low ``OUT`` bits of the concatenation are the element) and
774 contiguous list reads whose element is a whole number of output_bitwidth
775 (``OUT % IN == 0``, so elements never straddle). ``valid_bytes`` is unused:
776 such lists have no partial words and a single element is one flit. Straddling
777 lists use `ShiftReadGearbox`."""
778
779 if input_bitwidth <= 0 or input_bitwidth % 8 != 0:
780 raise ValueError("engine word width must be a positive multiple of 8 bits")
781 if output_bitwidth <= input_bitwidth:
782 raise ValueError("ConcatReadGearbox requires output > input")
783
784 in_bytes = input_bitwidth // 8
785 vb_width = clog2(in_bytes)
786
787 class ConcatReadGearboxImpl(Module):
788 clk = Clock()
789 rst = Reset()
790 in_ = InputChannel(
791 StructType([
792 ("tag", esi.HostMem.TagType),
793 ("data", Bits(input_bitwidth)),
794 ("valid_bytes", UInt(vb_width)),
795 ("last", Bits(1)),
796 ]))
797 out = OutputChannel(
798 StructType([
799 ("tag", esi.HostMem.TagType),
800 ("data", Bits(output_bitwidth)),
801 ("last", Bits(1)),
802 ]))
803
804 @generator
805 def build(ports):
806 ready_for_upstream = Wire(Bits(1), name="ready_for_upstream")
807 # Register the input for fmax; the ESI channel buffer keeps the handshake
808 # elastic.
809 in_reg = ports.in_.buffer(ports.clk, ports.rst, stages=1)
810 up, upstream_valid = in_reg.unwrap(ready_for_upstream)
811 upstream_data = up.data
812 upstream_last = up.last
813 upstream_xact = ready_for_upstream & upstream_valid
814
815 # Registers accumulate `chunks` upstream words into one client element;
816 # the output is their concatenation. For a list, elements stream back to
817 # back and 'last' rides the final word of the burst's final element.
818 chunks = ceil(output_bitwidth / input_bitwidth)
819 counter_width = clog2(chunks)
820 reg_ces = [Wire(Bits(1)) for _ in range(chunks)]
821 regs = [
822 upstream_data.reg(ports.clk,
823 ports.rst,
824 ce=reg_ces[idx],
825 name=f"chunk_reg_{idx}") for idx in range(chunks)
826 ]
827 client_data_bits = BitsSignal.concat(reversed(regs))[:output_bitwidth]
828
829 # Pair-index counter: the word accepted this cycle is written to
830 # chunk_reg[counter]. 'Counter' clears in preference to incrementing, so
831 # mask the clear with the accept -- a consume and an accept on the same
832 # cycle means the accepted word is chunk 0 of the *next* element, so the
833 # index must land on 1, not 0. 'chunks' need not be a power of two, so
834 # wrap explicitly rather than relying on the counter's natural rollover.
835 counter = Wire(UInt(counter_width), name="chunk_counter")
836 client_xact = Wire(Bits(1))
837 set_client_valid = counter == UInt(counter_width)(chunks - 1)
838 counter.assign(
839 Counter(counter_width)(clk=ports.clk,
840 rst=ports.rst,
841 clear=(upstream_xact & set_client_valid) |
842 (client_xact & ~upstream_xact),
843 increment=upstream_xact,
844 instance_name="chunk_counter").out)
845 client_valid = ControlReg(ports.clk, ports.rst,
846 [set_client_valid & upstream_xact],
847 [client_xact])
848 for idx, reg_ce in enumerate(reg_ces):
849 reg_ce.assign(upstream_xact & (counter == UInt(counter_width)(idx)))
850 # 'last' of the final engine word that completes this client flit.
851 client_last = upstream_last.reg(ports.clk,
852 ports.rst,
853 ce=upstream_xact,
854 name="last_reg")
855 tag_reg = up.tag.reg(ports.clk,
856 ports.rst,
857 ce=upstream_xact,
858 name="tag_reg")
859
860 client_channel, client_ready = ConcatReadGearboxImpl.out.type.wrap(
861 {
862 "tag": tag_reg,
863 "data": client_data_bits,
864 "last": client_last,
865 }, client_valid)
866 client_xact.assign(client_valid & client_ready)
867 ready_for_upstream.assign(~client_valid | client_ready)
868 ports.out = client_channel
869
870 return ConcatReadGearboxImpl
871
872
873@modparams
874def DepackReadGearbox(input_bitwidth: int,
875 output_bitwidth: int) -> type["DepackReadGearboxImpl"]:
876 """Unpack a byte-aligned element that divides the engine word
877 (``OUT % 8 == 0`` and ``IN % OUT == 0``) from a contiguous list response. Each
878 word holds ``IN/OUT`` gap-free elements that never straddle, so a counter
879 drives a parts:1 element mux -- no shifter (e.g. 32b/64b, 64b/256b).
880 ``valid_bytes`` locates the last element in the burst's (possibly partial)
881 final word. Straddling relationships use `ShiftReadGearbox`."""
882
883 if input_bitwidth % 8 != 0:
884 raise ValueError("engine word width must be a multiple of 8 bits")
885 if output_bitwidth == 0 or output_bitwidth % 8 != 0 \
886 or input_bitwidth % output_bitwidth != 0:
887 raise ValueError(
888 "DepackReadGearbox requires a byte-aligned element that divides the "
889 "engine word")
890
891 in_bytes = input_bitwidth // 8
892 # 'valid_bytes' is the real byte count minus 1; a word always has >= 1 byte.
893 vb_width = clog2(in_bytes)
894 count_width = clog2(in_bytes + 1)
895 parts = input_bitwidth // output_bitwidth
896 elem_bytes = output_bitwidth // 8
897
898 class DepackReadGearboxImpl(Module):
899 clk = Clock()
900 rst = Reset()
901 in_ = InputChannel(
902 StructType([
903 ("tag", esi.HostMem.TagType),
904 ("data", Bits(input_bitwidth)),
905 ("valid_bytes", UInt(vb_width)),
906 ("last", Bits(1)),
907 ]))
908 out = OutputChannel(
909 StructType([
910 ("tag", esi.HostMem.TagType),
911 ("data", Bits(output_bitwidth)),
912 ("last", Bits(1)),
913 ]))
914
915 @generator
916 def build(ports):
917 client_ready = Wire(Bits(1), name="client_ready")
918 up_ready = Wire(Bits(1), name="up_ready")
919 # Register the input for fmax; the ESI channel buffer keeps the handshake
920 # elastic and decouples the ready path.
921 in_reg = ports.in_.buffer(ports.clk, ports.rst, stages=1)
922 up, up_valid = in_reg.unwrap(up_ready)
923 client_xact = up_valid & client_ready
924
925 if parts == 1:
926 # One element per word; nothing to select.
927 last_in_word = Bits(1)(1)
928 client_data = up.data
929 else:
930 idx_width = clog2(parts)
931 idx = Reg(UInt(idx_width),
932 clk=ports.clk,
933 rst=ports.rst,
934 rst_value=0,
935 ce=client_xact,
936 name="idx")
937 # (idx + 1) * elem_bytes == real valid bytes marks the word's last
938 # element (the final word may hold fewer than `parts`); add 1 back to
939 # the biased 'valid_bytes' to recover the real count.
940 real_valid_bytes = (up.valid_bytes + UInt(1)(1)).as_uint(count_width)
941 consumed = ((idx + UInt(1)(1)) *
942 UInt(count_width)(elem_bytes)).as_uint(count_width)
943 last_in_word = consumed == real_valid_bytes
944 # parts:1 element-select mux -- the entire datapath, no shifter.
945 word_parts = Array(Bits(output_bitwidth), parts)([
946 up.data[k * output_bitwidth:(k + 1) * output_bitwidth]
947 for k in range(parts)
948 ])
949 client_data = word_parts[idx]
950 idx.assign(
951 Mux(last_in_word, (idx + UInt(1)(1)).as_uint(idx_width),
952 UInt(idx_width)(0)))
953
954 # Consume the buffered word as its last element leaves.
955 up_ready.assign(client_xact & last_in_word)
956 client_channel, client_ready_sig = DepackReadGearboxImpl.out.type.wrap(
957 {
958 "tag": up.tag,
959 "data": client_data,
960 "last": (up.last & last_in_word).as_bits(),
961 }, up_valid)
962 client_ready.assign(client_ready_sig)
963 ports.out = client_channel
964
965 return DepackReadGearboxImpl
966
967
968@modparams
969def ShiftReadGearbox(input_bitwidth: int,
970 output_bitwidth: int) -> type["ShiftReadGearboxImpl"]:
971 """Universal fallback: unpack a contiguous, byte-packed element stream (a
972 `read_list` response) for ANY ``(input_bitwidth, output_bitwidth)`` pair.
973
974 Elements are packed at their natural byte stride ``stride = ceil(OUT/8)``
975 bytes, so element k begins at wire bit ``k*stride*8`` and, in general,
976 straddles engine-word boundaries at an arbitrary bit offset. A byte-addressed
977 shift-register accumulator realigns each element across words. This is correct
978 for every width relationship; `SliceReadGearbox`, `ConcatReadGearbox` and
979 `DepackReadGearbox` are optimizations that avoid this barrel shifter for the
980 regular (non-straddling) cases.
981
982 Each input word carries ``valid_bytes`` (how many of its bytes are real) and
983 ``last``. Both are framed to one whole `read_list` request rather than to the
984 transport: `HostMemReadReqSplitter` drops the per-chunk framing of the reads
985 it issues and re-derives these from the request's total length, so only the
986 request's final word is ever partial. That length is ``num_elements *
987 stride``, so tracking real bytes lets the gearbox emit exactly the right
988 elements and place the list-terminating ``last`` on the final one -- no
989 padding element is ever emitted."""
990
991 if input_bitwidth % 8 != 0:
992 raise ValueError("engine word width must be a multiple of 8 bits")
993 if output_bitwidth <= 0:
994 raise ValueError("client element width must be positive")
995 in_bytes = input_bitwidth // 8
996 stride_bytes = (output_bitwidth + 7) // 8
997 stride_bits = stride_bytes * 8
998 # Hold at most one partial element plus one freshly accepted word.
999 buf_bytes = stride_bytes + in_bytes
1000 buf_bits = buf_bytes * 8
1001 # 'valid_bytes' is the real byte count minus 1; a word always has >= 1 byte.
1002 vb_width = clog2(in_bytes)
1003 cnt_width = clog2(buf_bytes + 1)
1004 # The append offset is only ever in [0, stride_bytes] (has_room), so the shift
1005 # index needs fewer bits than the full count -- see `build`.
1006 offset_width = clog2(stride_bytes + 1)
1007
1008 class ShiftReadGearboxImpl(Module):
1009 clk = Clock()
1010 rst = Reset()
1011 in_ = InputChannel(
1012 StructType([
1013 ("tag", esi.HostMem.TagType),
1014 ("data", Bits(input_bitwidth)),
1015 ("valid_bytes", UInt(vb_width)),
1016 ("last", Bits(1)),
1017 ]))
1018 out = OutputChannel(
1019 StructType([
1020 ("tag", esi.HostMem.TagType),
1021 ("data", Bits(output_bitwidth)),
1022 ("last", Bits(1)),
1023 ]))
1024
1025 @generator
1026 def build(ports):
1027 client_ready = Wire(Bits(1), name="client_ready")
1028 up_ready = Wire(Bits(1), name="up_ready")
1029 # Register the input for fmax; the ESI channel buffer keeps the handshake
1030 # elastic and decouples the ready path.
1031 in_reg = ports.in_.buffer(ports.clk, ports.rst, stages=1)
1032 up, up_valid = in_reg.unwrap(up_ready)
1033
1034 from pycde.circt.dialects import comb
1035
1036 # Byte-addressed accumulator: `buffer` holds `count` valid bytes packed
1037 # from bit 0 up; the element being emitted is buffer[0:OUT].
1038 buffer = Reg(Bits(buf_bits),
1039 clk=ports.clk,
1040 rst=ports.rst,
1041 rst_value=0,
1042 name="buffer")
1043 count = Reg(UInt(cnt_width),
1044 clk=ports.clk,
1045 rst=ports.rst,
1046 rst_value=0,
1047 name="count")
1048 saw_last = Wire(Bits(1), name="saw_last")
1049
1050 # Accept a whole engine word only when there's room, and never while
1051 # draining a finished burst -- otherwise the next burst's bytes would mix
1052 # into this one's buffer.
1053 has_room = count <= UInt(cnt_width)(buf_bytes - in_bytes)
1054 up_ready.assign(has_room & ~saw_last)
1055 up_xact = up_ready & up_valid
1056
1057 # Emit an element once a full stride slot is buffered.
1058 client_valid = count >= UInt(cnt_width)(stride_bytes)
1059 client_xact = client_valid & client_ready
1060
1061 # The burst's final word sets `saw_last`; the emit that drains the buffer
1062 # to empty terminates the list. These never coincide: emitting needs a
1063 # slot buffered by a prior cycle's accept, so `after_emit == 0` on an
1064 # accept cycle is impossible.
1065 added = Mux(up_xact,
1066 UInt(cnt_width)(0), (up.valid_bytes.as_uint(cnt_width) +
1067 UInt(1)(1)).as_uint(cnt_width))
1068 after_add = (count + added).as_uint(cnt_width)
1069 after_emit = (after_add -
1070 UInt(cnt_width)(stride_bytes)).as_uint(cnt_width)
1071 set_saw_last = (up_xact & up.last).as_bits()
1072 is_final_slot = (after_emit == UInt(cnt_width)(0))
1073 burst_ending = saw_last | set_saw_last
1074 client_last = client_valid & burst_ending & is_final_slot
1075 final_emit = client_xact & client_last
1076
1077 # Append the accepted word at bit offset count*8 (dynamic left shift);
1078 # then, if we emit this cycle, drop the consumed slot (constant right
1079 # shift by the stride). The append offset is <= stride_bytes when
1080 # accepting, so bound the shift index to that range: its high bits are
1081 # constant 0, which lets constant-propagation prune the upper barrel-
1082 # shifter stages (synthesis won't infer this bound from the count reg).
1083 append_off = count.as_bits()[0:offset_width]
1084 shamt = BitsSignal.concat([append_off,
1085 Bits(3)(0)]).pad_or_truncate(buf_bits)
1086 word_ext = up.data.pad_or_truncate(buf_bits)
1087 shifted_word = BitsSignal(
1088 comb.ShlOp(word_ext.value, shamt.value).result, Bits(buf_bits))
1089 appended = buffer | Mux(up_xact, Bits(buf_bits)(0), shifted_word)
1090 drained = appended[stride_bits:buf_bits].pad_or_truncate(buf_bits)
1091 buffer.assign(
1092 Mux(final_emit, Mux(client_xact, appended, drained),
1093 Bits(buf_bits)(0)))
1094
1095 # count += accepted real bytes (biased 'valid_bytes' + 1); -= stride on
1096 # emit.
1097 count.assign(Mux(client_xact, after_add, after_emit))
1098
1099 saw_last.assign(
1100 ControlReg(ports.clk, ports.rst, [set_saw_last], [final_emit]))
1101
1102 tag_reg = up.tag.reg(ports.clk, ports.rst, ce=up_xact, name="tag_reg")
1103 client_channel, client_ready_sig = ShiftReadGearboxImpl.out.type.wrap(
1104 {
1105 "tag": tag_reg,
1106 "data": buffer[0:output_bitwidth],
1107 "last": client_last,
1108 }, client_valid)
1109 client_ready.assign(client_ready_sig)
1110 ports.out = client_channel
1111
1112 return ShiftReadGearboxImpl
1113
1114
1115def select_read_gearbox(is_list: bool, input_bitwidth: int,
1116 output_bitwidth: int):
1117 """Pick the read-gearbox module for a client of the given kind and width
1118 relationship. Every gearbox shares the {tag, data, valid_bytes, last} input
1119 (from `HostMemReadReqSplitter`) and the {tag, data, last} output, so callers
1120 wire them identically. `ShiftReadGearbox` is the correct-for-everything
1121 fallback; the others avoid its barrel shifter for regular relationships."""
1122 if not is_list:
1123 # A single element starts at bit 0 and never straddles at a bit offset.
1124 if output_bitwidth <= input_bitwidth:
1125 return SliceReadGearbox(input_bitwidth, output_bitwidth)
1126 return ConcatReadGearbox(input_bitwidth, output_bitwidth)
1127 if output_bitwidth > input_bitwidth:
1128 # Super-word list: a whole-word-multiple element never straddles.
1129 if output_bitwidth % input_bitwidth == 0:
1130 return ConcatReadGearbox(input_bitwidth, output_bitwidth)
1131 return ShiftReadGearbox(input_bitwidth, output_bitwidth)
1132 # Sub-word list: a byte-aligned element that divides the word never straddles.
1133 if input_bitwidth % output_bitwidth == 0 and output_bitwidth % 8 == 0:
1134 return DepackReadGearbox(input_bitwidth, output_bitwidth)
1135 return ShiftReadGearbox(input_bitwidth, output_bitwidth)
1136
1137
1138# Maximum size, in bytes, of a single upstream read request. Reads larger than
1139# this are split by the requester into multiple requests. The default is a
1140# conservative PCIe-derived cap (Max_Read_Request_Size tops out at 4096 bytes,
1141# but root ports often negotiate less); it mirrors kPcieMaxReadRequestBytes in
1142# the Cosim backend (cpp/lib/backends/Cosim.cpp).
1143DEFAULT_MAX_READ_REQUEST_BYTES = 64 * 4 # 64 double words
1144
1145# Maximum size, in bytes, of a single upstream write transaction; an element
1146# whose write payload is wider is split into multiple <= this-size transactions.
1147# The default is a conservative PCIe-derived Max-Payload-Size cap.
1148DEFAULT_MAX_WRITE_PAYLOAD_BYTES = 256
1149
1150
1151@modparams
1152def HostMemReadReqSplitter(req_channel_type: Channel,
1153 resp_channel_type: Channel, max_chunk_bytes: int):
1154 """Split oversized host memory read requests into request-sized chunks before
1155 arbitration and reassemble the per-chunk responses into a single logical
1156 burst.
1157
1158 A burst read (`read_list`) can request many more bytes than a single upstream
1159 read request can carry. This module breaks such a request into
1160 `max_chunk_bytes`-sized (word-aligned) chunks addressed sequentially from the
1161 base. Splitting here -- *before* the requests
1162 are arbitrated onto the shared upstream read channel -- lets each client's
1163 chunks interleave with other clients' requests, so one large burst does not
1164 monopolize host memory bandwidth.
1165
1166 On the response path the per-chunk end-of-list markers are dropped and a
1167 single burst-final `last` is re-derived from the total transfer length, so the
1168 gearbox and client see one contiguous response stream identical to an unsplit
1169 read.
1170
1171 Only one logical request is in flight at a time (matching the read processor's
1172 one-outstanding-transaction-per-client model): a new request is not accepted
1173 until the current burst's chunks have all been issued and its responses have
1174 fully drained. This will be a performance limiter.
1175 TODO: make this able to issue >1 one read at a time.
1176
1177 req_channel_type: channel of the upstream read request {address, length
1178 (bytes), tag}.
1179 resp_channel_type: channel of the upstream response {tag, data, last}.
1180 max_chunk_bytes: largest per-chunk byte count; must be > 0 and a multiple of
1181 the response word size.
1182 """
1183 assert max_chunk_bytes > 0
1184
1185 req_struct = req_channel_type.inner_type
1186 resp_struct = resp_channel_type.inner_type
1187 req_fields = dict(req_struct.fields)
1188 addr_width = req_fields["address"].bitwidth
1189 length_width = req_fields["length"].bitwidth
1190 tag_type = req_fields["tag"]
1191 word_bytes = dict(resp_struct.fields)["data"].bitwidth // 8
1192 word_shift = clog2(word_bytes)
1193 words_width = length_width - word_shift
1194 # The response is augmented with a per-word 'valid_bytes': the number of real
1195 # bytes in the (possibly partial) final word, biased by -1. A burst word
1196 # always has >= 1 real byte, so encoding count-1 fits in one fewer bit.
1197 vb_width = clog2(word_bytes)
1198 resp_fields = dict(resp_struct.fields)
1199 resp_out_struct = StructType([
1200 ("tag", resp_fields["tag"]),
1201 ("data", resp_fields["data"]),
1202 ("valid_bytes", UInt(vb_width)),
1203 ("last", Bits(1)),
1204 ])
1205 resp_out_channel_type = Channel(resp_out_struct)
1206
1207 class HostMemReadReqSplitterImpl(Module):
1208 clk = Clock()
1209 rst = Reset()
1210 req_in = Input(req_channel_type)
1211 req_out = Output(req_channel_type)
1212 resp_in = Input(resp_channel_type)
1213 resp_out = Output(resp_out_channel_type)
1214
1215 @generator
1216 def build(ports):
1217 clk = ports.clk
1218 rst = ports.rst
1219
1220 # Burst state shared by the request-splitting and response-reassembly
1221 # FSMs. One logical request is processed at a time.
1222 emit_busy = Wire(Bits(1), name="emit_busy") # issuing chunk requests
1223 resp_busy = Wire(Bits(1), name="resp_busy") # responses still draining
1224 cur_addr = Wire(UInt(addr_width), name="cur_addr")
1225 remaining = Wire(UInt(length_width), name="remaining") # req bytes left
1226 tag_reg = Wire(tag_type, name="tag_reg")
1227 words_left = Wire(UInt(words_width), name="words_left") # resp words left
1228
1229 idle = (~emit_busy) & (~resp_busy)
1230
1231 # --- Request intake and splitting ---
1232 req_ready = Wire(Bits(1))
1233 req_payload, req_valid = ports.req_in.unwrap(req_ready)
1234 accept = idle & req_valid
1235 req_ready.assign(accept)
1236
1237 max_chunk = UInt(length_width)(max_chunk_bytes)
1238 chunk_len = Mux(remaining > max_chunk, remaining, max_chunk)
1239 last_chunk = remaining <= max_chunk
1240
1241 # Round the emitted read length up to a whole word. The reader response
1242 # is word-granular and 'valid_bytes' still carries the real trailing byte
1243 # count, so total_words and the reassembled element count are unchanged;
1244 # this just keeps every read word-aligned for single-flit HostMem
1245 # transports that reject sub-word read lengths.
1246 if word_shift == 0:
1247 chunk_len_out = chunk_len
1248 else:
1249 chunk_words = (chunk_len + UInt(length_width)(word_bytes - 1)
1250 ).as_bits()[word_shift:].as_uint(words_width)
1251 chunk_len_out = BitsSignal.concat(
1252 [chunk_words.as_bits(), Bits(word_shift)(0)]).as_uint(length_width)
1253
1254 req_out_ch, req_out_ready = req_channel_type.wrap(
1255 req_struct({
1256 "address": cur_addr,
1257 "length": chunk_len_out,
1258 "tag": tag_reg,
1259 }), emit_busy)
1260 ports.req_out = req_out_ch
1261 chunk_xact = emit_busy & req_out_ready
1262
1263 emit_busy.assign(
1264 ControlReg(clk,
1265 rst, [accept], [chunk_xact & last_chunk],
1266 name="emit_busy_reg"))
1267
1268 # cur_addr: load base on accept, advance by the chunk on each issue.
1269 cur_addr_incr = (cur_addr +
1270 chunk_len.as_uint(addr_width)).as_uint(addr_width)
1271 cur_addr.assign(
1272 Mux(accept, Mux(chunk_xact, cur_addr, cur_addr_incr),
1273 req_payload.address).reg(clk,
1274 rst,
1275 rst_value=0,
1276 ce=accept | chunk_xact,
1277 name="cur_addr_reg"))
1278
1279 # remaining: load length on accept, subtract each issued chunk.
1280 remaining_dec = (remaining - chunk_len).as_uint(length_width)
1281 remaining.assign(
1282 Mux(accept, Mux(chunk_xact, remaining, remaining_dec),
1283 req_payload.length).reg(clk,
1284 rst,
1285 rst_value=0,
1286 ce=accept | chunk_xact,
1287 name="remaining_reg"))
1288
1289 tag_reg.assign(req_payload.tag.reg(clk, rst, ce=accept, name="tag_reg_r"))
1290
1291 # --- Response reassembly: re-derive the burst-final 'last' and the byte
1292 # count of the (possibly partial) final word. Elements need not tile
1293 # evenly into words, so count words with ceil(length / word_bytes). ---
1294 total_words = ((req_payload.length + UInt(length_width)(word_bytes - 1)
1295 ).as_bits()[word_shift:]).as_uint(words_width)
1296 # Bytes valid in the final word = length - (total_words - 1) * word_bytes.
1297 words_before_last = (total_words -
1298 UInt(words_width)(1)).as_uint(words_width)
1299 bytes_before_last = BitsSignal.concat(
1300 [words_before_last.as_bits(),
1301 Bits(word_shift)(0)]).as_uint(length_width)
1302 final_valid_bytes = (req_payload.length - bytes_before_last -
1303 UInt(length_width)(1)).as_uint(vb_width).reg(
1304 clk, rst, ce=accept, name="final_valid_bytes")
1305 resp_ready = Wire(Bits(1))
1306 resp_payload, resp_valid = ports.resp_in.unwrap(resp_ready)
1307 is_final_word = words_left == UInt(words_width)(1)
1308 resp_out_ch, resp_out_ready = resp_out_channel_type.wrap(
1309 resp_out_struct({
1310 "tag":
1311 resp_payload.tag,
1312 "data":
1313 resp_payload.data,
1314 "valid_bytes":
1315 Mux(is_final_word,
1316 UInt(vb_width)(word_bytes - 1), final_valid_bytes),
1317 "last":
1318 is_final_word,
1319 }), resp_valid)
1320 ports.resp_out = resp_out_ch
1321 resp_ready.assign(resp_out_ready)
1322 resp_xact = resp_valid & resp_out_ready
1323
1324 # words_left: load total on accept, decrement per received word.
1325 words_dec = (words_left - UInt(words_width)(1)).as_uint(words_width)
1326 words_left.assign(
1327 Mux(accept, Mux(resp_xact, words_left, words_dec),
1328 total_words).reg(clk,
1329 rst,
1330 rst_value=0,
1331 ce=accept | resp_xact,
1332 name="words_left_reg"))
1333
1334 resp_busy.assign(
1335 ControlReg(clk,
1336 rst, [accept], [resp_xact & is_final_word],
1337 name="resp_busy_reg"))
1338
1339 return HostMemReadReqSplitterImpl
1340
1341
1343 read_width: int,
1344 hostmem_module,
1345 reqs: List[esi._OutputBundleSetter],
1346 max_read_request_bytes: int = DEFAULT_MAX_READ_REQUEST_BYTES):
1347 """Construct a host memory read request module to orchestrate the the read
1348 connections. Responsible for both gearboxing the data, multiplexing the
1349 requests, reassembling out-of-order responses and routing the responses to the
1350 correct clients.
1351
1352 Generate this module dynamically to allow for multiple read clients of
1353 multiple types to be directly accomodated."""
1354
1355 class HostmemReadProcessorImpl(Module):
1356 clk = Clock()
1357 rst = Reset()
1358
1359 # Add an output port for each read client.
1360 reqPortMap: Dict[esi._OutputBundleSetter, str] = {}
1361 for req in reqs:
1362 name = "client_" + req.client_name_str
1363 locals()[name] = Output(req.type)
1364 reqPortMap[req] = name
1365
1366 # And then the port which goes to the host.
1367 upstream = Output(hostmem_module.read.type)
1368
1369 @generator
1370 def build(ports):
1371 """Build the read side of the HostMem service."""
1372
1373 # If there's no read clients, just return a no-op read bundle.
1374 if len(reqs) == 0:
1375 upstream_req_channel, _ = Channel(hostmem_module.UpstreamReadReq).wrap(
1376 {
1377 "tag": 0,
1378 "length": 0,
1379 "address": 0
1380 }, 0)
1381 upstream_read_bundle, _ = hostmem_module.read.type.pack(
1382 req=upstream_req_channel)
1383 ports.upstream = upstream_read_bundle
1384 return
1385
1386 # Since we use the tag to identify the client, we can't have more than 256
1387 # read clients. Supporting more than 256 clients would require
1388 # tag-rewriting, which we'll probably have to implement at some point.
1389 # TODO: Implement tag-rewriting.
1390 assert len(reqs) <= 256, "More than 256 read clients not supported."
1391
1392 # Pack the upstream bundle and leave the request as a wire.
1393 upstream_req_channel = Wire(Channel(hostmem_module.UpstreamReadReq))
1394 upstream_read_bundle, froms = hostmem_module.read.type.pack(
1395 req=upstream_req_channel)
1396 ports.upstream = upstream_read_bundle
1397 upstream_resp_channel = froms["resp"]
1398
1399 # Demux the upstream response frames {tag, data, last} to each client by
1400 # tag. Each client's stream then flows through a `HostMemReadReqSplitter`
1401 # (which annotates per-word 'valid_bytes' and the burst-final 'last') into
1402 # the leaf gearbox chosen by `select_read_gearbox`.
1403 demux = esi.TaggedDemux(len(reqs), upstream_resp_channel.type)(
1404 clk=ports.clk, rst=ports.rst, in_=upstream_resp_channel)
1405
1406 word_bytes = read_width // 8
1407 tagged_client_reqs = []
1408 for idx, client in enumerate(reqs):
1409 # Find the response channel in the request bundle.
1410 resp_type = [
1411 c.channel for c in client.type.channels if c.name == 'resp'
1412 ][0]
1413 demuxed_upstream_channel = demux.get_out(idx)
1414
1415 # TODO: Should responses come back out-of-order (interleaved tags),
1416 # re-order them here so the gearbox doesn't get confused. (Longer term.)
1417 # For now, only support one outstanding transaction at a time. This has
1418 # the additional benefit of letting the upstream tag be the client
1419 # identifier. TODO: Implement the gating logic here.
1420 client_type = resp_type.inner_type
1421 is_list = isinstance(client_type, Window)
1422
1423 # A read_list response is a parallel window over
1424 # struct{tag, data: list<element>} (num_items=1), lowering to
1425 # struct{tag, data: element, data_size, last}; a single read carries the
1426 # element directly. Pull the element width out of whichever shape.
1427 if is_list:
1428 lowered = client_type.lowered_type
1429 lowered_fields = dict(lowered.fields)
1430 element_type = lowered_fields["data"]
1431 element_bits = element_type.bitwidth
1432 data_size_type = lowered_fields["data_size"]
1433 if element_bits == 0:
1434 raise ValueError("read_list element type cannot be zero-width.")
1435 else:
1436 if client_type.data.bitwidth == 0:
1437 raise ValueError("Client data type cannot be zero-width. Use a "
1438 "single-bit type if no data is needed.")
1439 element_bits = client_type.data.bitwidth
1440 # Elements are packed contiguously in host memory at their natural byte
1441 # size, independent of the engine word width.
1442 elem_stride_bytes = (element_bits + 7) // 8
1443
1444 # Both single-message and list reads flow demux -> splitter -> gearbox
1445 # with a uniform {tag, data, valid_bytes, last} interface. The splitter
1446 # chunks oversized requests (so even a wide single element is
1447 # request-chunked) and annotates each word with 'valid_bytes' plus the
1448 # burst-final 'last'; `select_read_gearbox` picks the leaf gearbox for
1449 # this (is_list, read_width, element_bits). 'splitter_resp' breaks the
1450 # request/response construction cycle (the client request is derived
1451 # from the gearbox's response bundle).
1452 max_chunk_bytes = (max_read_request_bytes // word_bytes) * word_bytes
1453 gearbox_mod = select_read_gearbox(is_list, read_width, element_bits)
1454 splitter_resp = Wire(gearbox_mod.in_.type)
1455 gearbox = gearbox_mod(clk=ports.clk, rst=ports.rst, in_=splitter_resp)
1456
1457 if is_list:
1458 # Propagate 'last', then re-wrap the element as the response window.
1459 client_resp_channel = gearbox.out.transform(
1460 lambda m, lowered=lowered, element_type=element_type,
1461 data_size_type=data_size_type, client_type=client_type:
1462 client_type.wrap(
1463 lowered({
1464 "tag": m.tag,
1465 "data": m.data.bitcast(element_type),
1466 "data_size": data_size_type(0),
1467 "last": m.last,
1468 })))
1469 client_bundle, froms = client.type.pack(resp=client_resp_channel)
1470 client_req = froms["req"]
1471 logical_req = client_req.transform(
1472 lambda r, idx=idx, elem_stride_bytes=elem_stride_bytes:
1473 hostmem_module.UpstreamReadReq({
1474 "address":
1475 r.address,
1476 "length": (r.length * UInt(64)
1477 (elem_stride_bytes)).as_uint(32),
1478 "tag":
1479 idx,
1480 }))
1481 else:
1482 # Single-message read: one element; discard the 'last' burst marker.
1483 client_resp_channel = gearbox.out.transform(
1484 lambda m, client_type=client_type: client_type({
1485 "tag": m.tag,
1486 "data": m.data.bitcast(client_type.data)
1487 }))
1488 client_bundle, froms = client.type.pack(resp=client_resp_channel)
1489 client_req = froms["req"]
1490 logical_req = client_req.transform(
1491 lambda r, idx=idx, elem_stride_bytes=elem_stride_bytes:
1492 hostmem_module.UpstreamReadReq({
1493 "address": r.address,
1494 "length": UInt(32)(elem_stride_bytes),
1495 # TODO: Change this once we support tag-rewriting.
1496 "tag": idx,
1497 }))
1498
1499 splitter = HostMemReadReqSplitter(
1500 logical_req.type, demuxed_upstream_channel.type,
1501 max_chunk_bytes)(clk=ports.clk,
1502 rst=ports.rst,
1503 req_in=logical_req,
1504 resp_in=demuxed_upstream_channel)
1505 splitter_resp.assign(splitter.resp_out)
1506 tagged_client_req = splitter.req_out
1507
1508 tagged_client_reqs.append(tagged_client_req)
1509
1510 # Set the port for the client request.
1511 setattr(ports, HostmemReadProcessorImpl.reqPortMap[client],
1512 client_bundle)
1513
1514 # Assign the multiplexed read request to the upstream request. Use the
1515 # list-aware, pipelined ChannelArbiter (vs. the combinational ChannelMux)
1516 # for a registered N:1 mux that closes timing at high client fan-in. Read
1517 # requests are single-flit, so list-awareness is a no-op here.
1518 # TODO: Don't release a request until the client is ready to accept
1519 # the response otherwise the system could deadlock.
1520 muxed_client_reqs = ChannelArbiter(tagged_client_reqs,
1521 ports.clk,
1522 ports.rst,
1523 telemetry=False)
1524 upstream_req_channel.assign(muxed_client_reqs)
1525 HostmemReadProcessorImpl.reqPortMap.clear()
1526
1527 return HostmemReadProcessorImpl
1528
1529
1530@modparams
1531def TaggedWriteGearbox(input_bitwidth: int, output_bitwidth: int,
1532 max_burst_bytes: int) -> type["TaggedWriteGearboxImpl"]:
1533 """Build a gearbox to convert the client data to upstream write chunks.
1534 Assumes a struct {address, tag, data} and only gearboxes the data. Tag is
1535 stored separately and the struct is re-assembled later on.
1536
1537 'max_burst_bytes' caps a single contiguous upstream write transaction (a
1538 max-payload-size analog): when an element spans more than 'max_burst_bytes',
1539 its engine words are split into multiple <= 'max_burst_bytes' transactions by
1540 emitting the framing 'last' at each boundary. 0 disables the cap."""
1541
1542 if output_bitwidth % 8 != 0:
1543 raise ValueError("Output bitwidth must be a multiple of 8.")
1544 input_pad_bits = 0
1545 if input_bitwidth % 8 != 0:
1546 input_pad_bits = 8 - (input_bitwidth % 8)
1547 input_padded_bitwidth = input_bitwidth + input_pad_bits
1548
1549 # Number of engine words per capped transaction (0 = uncapped).
1550 max_burst_words = (max_burst_bytes //
1551 (output_bitwidth // 8)) if max_burst_bytes else 0
1552 if max_burst_words:
1553 assert (max_burst_words & (max_burst_words - 1)) == 0, \
1554 "max_burst_bytes / (output_bitwidth // 8) must be a power of two"
1555
1556 class TaggedWriteGearboxImpl(Module):
1557 clk = Clock()
1558 rst = Reset()
1559 in_ = InputChannel(
1560 StructType([
1561 ("address", UInt(64)),
1562 ("tag", esi.HostMem.TagType),
1563 ("data", Bits(input_bitwidth)),
1564 ]))
1565 out = OutputChannel(
1566 StructType([
1567 ("address", UInt(64)),
1568 ("tag", esi.HostMem.TagType),
1569 ("data", Bits(output_bitwidth)),
1570 ("valid_bytes", Bits(8)),
1571 ("last", Bits(1)),
1572 ]))
1573
1574 num_chunks = ceil(input_padded_bitwidth / output_bitwidth)
1575
1576 @generator
1577 def build(ports):
1578 upstream_ready = Wire(Bits(1))
1579 ready_for_client = Wire(Bits(1))
1580 client_tag_and_data, client_valid = ports.in_.unwrap(ready_for_client)
1581 client_data = client_tag_and_data.data
1582 if input_pad_bits > 0:
1583 client_data = client_data.pad_or_truncate(input_padded_bitwidth)
1584 client_xact = ready_for_client & client_valid
1585 input_bitwidth_bytes = input_padded_bitwidth // 8
1586 output_bitwidth_bytes = output_bitwidth // 8
1587
1588 # Determine if gearboxing is necessary and whether it needs to be
1589 # gearboxed up or just sliced down.
1590 if output_bitwidth == input_padded_bitwidth:
1591 upstream_data_bits = client_data
1592 upstream_valid = client_valid
1593 ready_for_client.assign(upstream_ready)
1594 tag = client_tag_and_data.tag
1595 address = client_tag_and_data.address
1596 valid_bytes = Bits(8)(input_bitwidth_bytes)
1597 last = Bits(1)(1)
1598 elif output_bitwidth > input_padded_bitwidth:
1599 upstream_data_bits = client_data.as_bits(output_bitwidth)
1600 upstream_valid = client_valid
1601 ready_for_client.assign(upstream_ready)
1602 tag = client_tag_and_data.tag
1603 address = client_tag_and_data.address
1604 valid_bytes = Bits(8)(input_bitwidth_bytes)
1605 last = Bits(1)(1)
1606 else:
1607 # Create registers equal to the number of upstream transactions needed
1608 # to complete the transmission.
1609 num_chunks = TaggedWriteGearboxImpl.num_chunks
1610 num_chunks_idx_bitwidth = clog2(num_chunks)
1611 if input_padded_bitwidth % output_bitwidth == 0:
1612 padding_numbits = 0
1613 else:
1614 padding_numbits = output_bitwidth - (input_padded_bitwidth %
1615 output_bitwidth)
1616 client_data_padded = BitsSignal.concat(
1617 [Bits(padding_numbits)(0), client_data])
1618 chunks = [
1619 client_data_padded[i * output_bitwidth:(i + 1) * output_bitwidth]
1620 for i in range(num_chunks)
1621 ]
1622 chunk_regs = Array(Bits(output_bitwidth), num_chunks)([
1623 c.reg(ports.clk, ce=client_xact, name=f"chunk_{idx}")
1624 for idx, c in enumerate(chunks)
1625 ])
1626 increment = Wire(Bits(1))
1627 clear = Wire(Bits(1))
1628 counter = Counter(num_chunks_idx_bitwidth)(clk=ports.clk,
1629 rst=ports.rst,
1630 increment=increment,
1631 clear=clear)
1632 upstream_data_bits = chunk_regs[counter.out]
1633 upstream_valid = ControlReg(ports.clk, ports.rst, [client_xact],
1634 [clear])
1635 upstream_xact = upstream_valid & upstream_ready
1636 clear.assign(upstream_xact & (counter.out == (num_chunks - 1)))
1637 increment.assign(upstream_xact)
1638 ready_for_client.assign(~upstream_valid)
1639 address_padding_bits = clog2(output_bitwidth_bytes)
1640 counter_bytes = BitsSignal.concat(
1641 [counter.out.as_bits(),
1642 Bits(address_padding_bits)(0)]).as_uint()
1643
1644 # Construct the output channel. Shared logic across all three cases.
1645 tag_reg = client_tag_and_data.tag.reg(ports.clk,
1646 ce=client_xact,
1647 name="tag_reg")
1648 addr_reg = client_tag_and_data.address.reg(ports.clk,
1649 ce=client_xact,
1650 name="address_reg")
1651 address = (addr_reg + counter_bytes).as_uint(64)
1652 tag = tag_reg
1653 elem_end = counter.out == (num_chunks - 1)
1654 valid_bytes = Mux(elem_end,
1655 Bits(8)(output_bitwidth_bytes),
1656 Bits(8)((output_bitwidth - padding_numbits) // 8))
1657 if max_burst_words and num_chunks > max_burst_words:
1658 # Max-payload-size cap: end the upstream write transaction at the
1659 # element end OR every max_burst_words engine words, whichever comes
1660 # first, so a wide element's write is split into <= max_burst_bytes
1661 # transactions. Each word keeps its own sequential address; only the
1662 # transaction-framing 'last' changes.
1663 burst_shift = clog2(max_burst_words)
1664 burst_end = counter.out.as_bits()[:burst_shift].and_reduce()
1665 last = elem_end | burst_end
1666 else:
1667 last = elem_end
1668
1669 upstream_channel, upstrm_ready_sig = TaggedWriteGearboxImpl.out.type.wrap(
1670 {
1671 "address": address,
1672 "tag": tag,
1673 "data": upstream_data_bits,
1674 "valid_bytes": valid_bytes,
1675 "last": last,
1676 }, upstream_valid)
1677 upstream_ready.assign(upstrm_ready_sig)
1678 ports.out = upstream_channel
1679
1680 return TaggedWriteGearboxImpl
1681
1682
1683@modparams
1684def EmitEveryN(message_type: Type, N: int) -> type['EmitEveryNImpl']:
1685 """Emit (forward) one message for every N input messages. The emitted message
1686 is the last one of the N received. N must be >= 1."""
1687
1688 if N < 1:
1689 raise ValueError("N must be >= 1")
1690
1691 class EmitEveryNImpl(Module):
1692 clk = Clock()
1693 rst = Reset()
1694 in_ = InputChannel(message_type)
1695 out = OutputChannel(message_type)
1696
1697 @generator
1698 def build(ports):
1699 ready_for_in = Wire(Bits(1))
1700 in_data, in_valid = ports.in_.unwrap(ready_for_in)
1701 xact = in_valid & ready_for_in
1702
1703 # Fast path: N == 1 -> pass-through.
1704 if N == 1:
1705 out_chan, out_ready = EmitEveryNImpl.out.type.wrap(in_data, in_valid)
1706 ready_for_in.assign(out_ready)
1707 ports.out = out_chan
1708 return
1709
1710 counter_width = clog2(N)
1711 counter_clear = Wire(Bits(1))
1712 counter = Counter(counter_width)(clk=ports.clk,
1713 rst=ports.rst,
1714 increment=xact,
1715 clear=counter_clear)
1716
1717 # Capture last message of the group.
1718 last_msg = in_data.reg(ports.clk, ports.rst, ce=xact, name="last_msg")
1719 # Clear the counter.
1720 hit_last = (counter.out == UInt(counter_width)(N - 1)) & xact
1721 counter_clear.assign(hit_last)
1722
1723 emit_accepted = Wire(Bits(1))
1724 out_valid = ControlReg(ports.clk, ports.rst, [hit_last], [emit_accepted])
1725
1726 out_chan, out_ready = EmitEveryNImpl.out.type.wrap(last_msg, out_valid)
1727 # Stall input while waiting for downstream to accept the aggregated output.
1728 ready_for_in.assign(~(out_valid & ~out_ready))
1729 emit_accepted.assign(out_valid & out_ready) # Output consumed downstream.
1730
1731 ports.out = out_chan
1732
1733 return EmitEveryNImpl
1734
1735
1737 write_width: int,
1738 hostmem_module,
1739 reqs: List[esi._OutputBundleSetter],
1740 max_write_payload_bytes: int = DEFAULT_MAX_WRITE_PAYLOAD_BYTES
1741) -> type["HostMemWriteProcessorImpl"]:
1742 """Construct a host memory write request module to orchestrate the the write
1743 connections. Responsible for both gearboxing the data, multiplexing the
1744 requests, reassembling out-of-order responses and routing the responses to the
1745 correct clients.
1746
1747 Generate this module dynamically to allow for multiple write clients of
1748 multiple types to be directly accomodated."""
1749
1750 class HostMemWriteProcessorImpl(Module):
1751
1752 clk = Clock()
1753 rst = Reset()
1754
1755 # Add an output port for each read client.
1756 reqPortMap: Dict[esi._OutputBundleSetter, str] = {}
1757 for req in reqs:
1758 name = "client_" + req.client_name_str
1759 locals()[name] = Output(req.type)
1760 reqPortMap[req] = name
1761
1762 # And then the port which goes to the host.
1763 upstream = Output(hostmem_module.write.type)
1764
1765 @generator
1766 def build(ports):
1767 clk = ports.clk
1768 rst = ports.rst
1769
1770 # Width of the frame's 'data_size' field: log2 of the number of bytes per
1771 # engine word. It holds (valid_bytes - 1) for the final (possibly partial)
1772 # word of a write.
1773 size_width = clog2(write_width // 8)
1774
1775 # If there's no write clients, just create a no-op write bundle
1776 if len(reqs) == 0:
1777 req, _ = Channel(hostmem_module.UpstreamWriteReq).wrap(
1778 {
1779 "address": 0,
1780 "tag": 0,
1781 "data": 0,
1782 "data_size": 0,
1783 "last": 0,
1784 }, 0)
1785 write_bundle, _ = hostmem_module.write.type.pack(req=req)
1786 ports.upstream = write_bundle
1787 return
1788
1789 assert len(reqs) <= 256, "More than 256 write clients not supported."
1790
1791 upstream_req_channel = Wire(Channel(hostmem_module.UpstreamWriteReq))
1792 upstream_write_bundle, froms = hostmem_module.write.type.pack(
1793 req=upstream_req_channel)
1794 ports.upstream = upstream_write_bundle
1795 upstream_ack_tag = froms["ackTag"]
1796
1797 demuxed_acks = esi.TaggedDemux(len(reqs), upstream_ack_tag.type)(
1798 clk=ports.clk, rst=ports.rst, in_=upstream_ack_tag)
1799
1800 # TODO: re-write the tags and store the client and client tag.
1801
1802 # Build the write request channels and ack wires.
1803 write_channels: List[ChannelSignal] = []
1804 for idx, req in enumerate(reqs):
1805 # Get the request channel and its data type.
1806 reqch = [c.channel for c in req.type.channels if c.name == 'req'][0]
1807 client_type = reqch.inner_type
1808 input_flit_ack = Wire(upstream_ack_tag.type)
1809
1810 if isinstance(client_type, Window):
1811 # Windowed (list) write: the client streams a list of elements to be
1812 # written to sequential addresses from a base. Lowered frame:
1813 # struct{address, tag, data: elem[num_items], data_size, last}. One
1814 # element per frame (num_items=1 here).
1815 bundle_sig, wfroms = req.type.pack(ackTag=input_flit_ack)
1816 windowed_req = wfroms["req"]
1817 lowered = client_type.lowered_type
1818 array_type = dict(lowered.fields)["data"]
1819 element_bits = array_type.element_type.bitwidth
1820 # Elements are packed contiguously in host memory at their natural
1821 # byte size, independent of the engine word width (matches the
1822 # read_list path). Each per-element write is byte-enabled via
1823 # data_size, so a sub-word element writes only its own bytes.
1824 elem_stride = (element_bits + 7) // 8
1825
1826 gearbox_mod = TaggedWriteGearbox(element_bits, write_width,
1827 max_write_payload_bytes)
1828 gearbox_in_type = gearbox_mod.in_.type.inner_type
1829
1830 # Unwrap the window frames; compute a base+offset address from a
1831 # per-burst element counter (reset after each burst's final element).
1832 ready_for_frame = Wire(Bits(1))
1833 frame_win, frame_valid = windowed_req.unwrap(ready_for_frame)
1834 frame = frame_win.unwrap()
1835 frame_xact = frame_valid & ready_for_frame
1836 elem_clear = Wire(Bits(1))
1837 elem_counter = Counter(64)(clk=ports.clk,
1838 rst=ports.rst,
1839 clear=elem_clear,
1840 increment=frame_xact)
1841 elem_clear.assign(frame_xact & frame["last"])
1842 elem_addr = (frame["address"] +
1843 elem_counter.out * UInt(64)(elem_stride)).as_uint(64)
1844 gearbox_in_chan, gearbox_in_ready = Channel(gearbox_in_type).wrap(
1845 gearbox_in_type({
1846 "tag": frame["tag"],
1847 "address": elem_addr,
1848 "data": frame["data"][0].bitcast(gearbox_in_type.data),
1849 }), frame_valid)
1850 ready_for_frame.assign(gearbox_in_ready)
1851 gearbox = gearbox_mod(clk=ports.clk,
1852 rst=ports.rst,
1853 in_=gearbox_in_chan)
1854 else:
1855 # Single-message write.
1856 write_req_bundle_type = esi.HostMem.write_req_bundle_type(
1857 client_type.data)
1858 bundle_sig, sfroms = write_req_bundle_type.pack(ackTag=input_flit_ack)
1859 gearbox_mod = TaggedWriteGearbox(client_type.data.bitwidth,
1860 write_width, max_write_payload_bytes)
1861 gearbox_in_type = gearbox_mod.in_.type.inner_type
1862 bitcast_client_req = sfroms["req"].transform(
1863 lambda m, git=gearbox_in_type: git({
1864 "tag": m.tag,
1865 "address": m.address,
1866 "data": m.data.bitcast(git.data)
1867 }))
1868 gearbox = gearbox_mod(clk=ports.clk,
1869 rst=ports.rst,
1870 in_=bitcast_client_req)
1871
1872 write_channels.append(
1873 gearbox.out.transform(
1874 lambda m, idx=idx: hostmem_module.UpstreamWriteReq({
1875 "address":
1876 m.address,
1877 "tag":
1878 idx,
1879 "data":
1880 m.data,
1881 "data_size": (m.valid_bytes.as_uint() - UInt(8)
1882 (1)).as_bits()[:size_width],
1883 "last":
1884 m.last,
1885 })))
1886
1887 # Count the number of acks received from hostmem for this client
1888 # and only send one back to the client per input.
1889 ack_every_n = EmitEveryN(upstream_ack_tag.type, gearbox_mod.num_chunks)(
1890 clk=clk, rst=rst, in_=demuxed_acks.get_out(idx))
1891 input_flit_ack.assign(ack_every_n.out)
1892
1893 # Set the port for the client request.
1894 setattr(ports, HostMemWriteProcessorImpl.reqPortMap[req], bundle_sig)
1895
1896 # Multiplex the write requests onto the single upstream channel with the
1897 # list-aware, pipelined ChannelArbiter (matching the read side). A real
1898 # windowed write (multi-word client flits) engages the arbiter's list-
1899 # awareness -- via the frame's 'last' -- to keep a client's words
1900 # contiguous; single-word (<= engine width) clients emit one message per
1901 # word, for which single-flit arbitration is correct.
1902 muxed_write_channel = ChannelArbiter(write_channels,
1903 ports.clk,
1904 ports.rst,
1905 telemetry=False)
1906 upstream_req_channel.assign(muxed_write_channel)
1907
1908 return HostMemWriteProcessorImpl
1909
1910
1911@modparams
1912def ChannelHostMem(
1913 read_width: int,
1914 write_width: int,
1915 max_read_request_bytes: int = DEFAULT_MAX_READ_REQUEST_BYTES,
1916 max_write_payload_bytes: int = DEFAULT_MAX_WRITE_PAYLOAD_BYTES
1917) -> typing.Type['ChannelHostMemImpl']:
1918
1919 class ChannelHostMemImpl(esi.ServiceImplementation):
1920 """Builds a HostMem service which multiplexes multiple HostMem clients into
1921 two (read and write) bundles of the given data width."""
1922
1923 clk = Clock()
1924 rst = Reset()
1925
1926 UpstreamReadReq = StructType([
1927 ("address", UInt(64)),
1928 ("length", UInt(32)), # In bytes.
1929 ("tag", UInt(8)),
1930 ])
1931 read = Output(
1932 Bundle([
1933 BundledChannel("req", ChannelDirection.TO, UpstreamReadReq),
1934 BundledChannel(
1935 "resp", ChannelDirection.FROM,
1936 StructType([
1937 ("tag", esi.HostMem.TagType),
1938 ("data", Bits(read_width)),
1939 ("last", Bits(1)),
1940 ])),
1941 ]))
1942
1943 if write_width % 8 != 0:
1944 raise ValueError("Write width must be a multiple of 8.")
1945 UpstreamWriteReq = StructType([
1946 ("address", UInt(64)),
1947 ("tag", UInt(8)),
1948 ("data", Bits(write_width)),
1949 ("data_size", Bits(clog2(write_width // 8))),
1950 ("last", Bits(1)),
1951 ])
1952 write = Output(
1953 Bundle([
1954 BundledChannel("req", ChannelDirection.TO, UpstreamWriteReq),
1955 BundledChannel("ackTag", ChannelDirection.FROM, UInt(8)),
1956 ]))
1957
1958 @generator
1959 def generate(ports, bundles: esi._ServiceGeneratorBundles):
1960 # Split the read side out into a separate module. Must assign the output
1961 # ports to the clients since we can't service a request in a different
1962 # module.
1963 read_reqs = [
1964 req for req in bundles.to_client_reqs
1965 if req.port in ('read', 'read_list')
1966 ]
1967 read_proc_module = HostmemReadProcessor(read_width, ChannelHostMemImpl,
1968 read_reqs, max_read_request_bytes)
1969 read_proc = read_proc_module(clk=ports.clk, rst=ports.rst)
1970 ports.read = read_proc.upstream
1971 for req in read_reqs:
1972 req.assign(getattr(read_proc, read_proc_module.reqPortMap[req]))
1973
1974 # The write side.
1975 write_reqs = [
1976 req for req in bundles.to_client_reqs if req.port == 'write'
1977 ]
1978 write_proc_module = HostMemWriteProcessor(write_width, ChannelHostMemImpl,
1979 write_reqs,
1980 max_write_payload_bytes)
1981 write_proc = write_proc_module(clk=ports.clk, rst=ports.rst)
1982 ports.write = write_proc.upstream
1983 for req in write_reqs:
1984 req.assign(getattr(write_proc, write_proc_module.reqPortMap[req]))
1985
1986 return ChannelHostMemImpl
1987
1988
1989@modparams
1990def DummyToHostEngine(client_type: Type) -> type['DummyToHostEngineImpl']:
1991 """Create a fake DMA engine which just throws everything away."""
1992
1993 class DummyToHostEngineImpl(esi.EngineModule):
1994
1995 @property
1996 def TypeName(self):
1997 return "DummyToHostEngine"
1998
1999 clk = Clock()
2000 rst = Reset()
2001 input_channel = InputChannel(client_type)
2002
2003 @generator
2004 def build(ports):
2005 pass
2006
2007 return DummyToHostEngineImpl
2008
2009
2010@modparams
2011def DummyFromHostEngine(client_type: Type) -> type['DummyFromHostEngineImpl']:
2012 """Create a fake DMA engine which just never produces messages."""
2013
2014 class DummyFromHostEngineImpl(esi.EngineModule):
2015
2016 @property
2017 def TypeName(self):
2018 return "DummyFromHostEngine"
2019
2020 clk = Clock()
2021 rst = Reset()
2022 output_channel = OutputChannel(client_type)
2023
2024 @generator
2025 def build(ports):
2026 valid = Bits(1)(0)
2027 data = Bits(client_type.bitwidth)(0).bitcast(client_type)
2028 channel, ready = Channel(client_type).wrap(data, valid)
2029 ports.output_channel = channel
2030
2031 return DummyFromHostEngineImpl
2032
2033
2034def _resolve_engine_pair(path: str) -> Tuple[Callable, Callable]:
2035 """Resolve a dotted Python import path to a
2036 `(to_host_engine_gen, from_host_engine_gen)` tuple, used to override the
2037 default engine pair for a specific service request.
2038
2039 The path may point at either:
2040 - a module-level 2-tuple attribute, e.g.
2041 `"mypkg.mymod.MyEnginePair"` where `MyEnginePair` is
2042 `(MyToHost, MyFromHost)`; or
2043 - a zero-arg factory callable returning such a tuple.
2044 """
2045 import importlib
2046 if not isinstance(path, str):
2047 raise TypeError(
2048 "Engine override path must be a dotted 'pkg.mod.attr' string; "
2049 f"got {type(path).__name__}")
2050 module_path, _, attr_path = path.rpartition(".")
2051 if not module_path or not attr_path:
2052 raise ValueError(
2053 "Engine override path must be a dotted 'pkg.mod.attr' string; "
2054 f"got {path!r}")
2055 obj = importlib.import_module(module_path)
2056 for part in attr_path.split("."):
2057 obj = getattr(obj, part)
2058 if callable(obj):
2059 obj = obj()
2060 if not (isinstance(obj, tuple) and len(obj) == 2):
2061 raise TypeError(
2062 f"Engine override {path!r} must resolve to a 2-tuple "
2063 f"(to_host_engine_gen, from_host_engine_gen); got {type(obj).__name__}")
2064 if not (callable(obj[0]) and callable(obj[1])):
2065 raise TypeError(
2066 f"Engine override {path!r} must resolve to a 2-tuple of callables; got "
2067 f"({type(obj[0]).__name__}, {type(obj[1]).__name__})")
2068 return obj
2069
2070
2071def ChannelEngineService(
2072 to_host_engine_gen: Callable,
2073 from_host_engine_gen: Callable) -> type['ChannelEngineService']:
2074 """Returns a channel service implementation which calls
2075 to_host_engine_gen(<client_type>) or from_host_engine_gen(<client_type>) to
2076 generate the to_host and from_host engines for each channel. Does not support
2077 engines which can service multiple clients at once.
2078
2079 Individual service requests may override the default engine pair by passing
2080 `options={"engine": "pkg.mod.attr"}` at the service-request call site (e.g.
2081 `HostComms.some_bundle(AppID(...), options={"engine": "..."})`). The path
2082 is resolved by `_resolve_engine_pair` and must yield a
2083 `(to_host_engine_gen, from_host_engine_gen)` tuple with the same call shape
2084 as the defaults; the override applies to every channel of that request's
2085 bundle.
2086 """
2087
2088 class ChannelEngineService(esi.ServiceImplementation):
2089 """Service implementation which services the clients via a per-channel DMA
2090 engine."""
2091
2092 clk = Clock()
2093 rst = Reset()
2094
2095 @generator
2096 def build(ports, bundles: esi._ServiceGeneratorBundles):
2097 clk = ports.clk
2098 rst = ports.rst
2099
2100 def build_engine_appid(client_appid: List[esi.AppID],
2101 channel_name: str) -> str:
2102 appid_strings = [str(appid) for appid in client_appid]
2103 return f"{'_'.join(appid_strings)}.{channel_name}"
2104
2105 def build_engine(bc: BundledChannel,
2106 bundle_to_host_gen: Callable,
2107 bundle_from_host_gen: Callable,
2108 input_channel=None) -> Type:
2109 idbase = build_engine_appid(bundle.client_name, bc.name)
2110 eng_appid = esi.AppID(idbase)
2111 # DMA engines require at least 1 byte of data; substitute Bits(8)
2112 # for zero-width (void) channel types so the engine never sees a
2113 # zero-length transfer.
2114 engine_client_type = bc.channel.inner_type
2115 is_void = (engine_client_type.bitwidth == 0)
2116 if is_void:
2117 engine_client_type = Bits(8)
2118 if bc.direction == ChannelDirection.FROM:
2119 engine_mod = bundle_to_host_gen(engine_client_type)
2120 else:
2121 engine_mod = bundle_from_host_gen(engine_client_type)
2122 eng_inputs = {
2123 "clk": ports.clk,
2124 "rst": ports.rst,
2125 }
2126 eng_details: Dict[str, object] = {"engine_inst": eng_appid}
2127 if input_channel is not None:
2128 # For void channels, widen the 0-bit input to the 8-bit
2129 # placeholder the engine expects.
2130 if is_void:
2131 input_channel = input_channel.transform(lambda _: Bits(8)(0))
2132 if (engine_mod.input_channel.type.signaling
2133 != input_channel.type.signaling):
2134 input_channel = input_channel.buffer(
2135 clk,
2136 rst,
2137 stages=1,
2138 output_signaling=engine_mod.input_channel.type.signaling)
2139 eng_inputs["input_channel"] = input_channel
2140 if hasattr(engine_mod, "mmio"):
2141 mmio_appid = esi.AppID(idbase + ".mmio")
2142 eng_inputs["mmio"] = esi.MMIO.read_write(mmio_appid)
2143 eng_details["mmio"] = mmio_appid
2144 if hasattr(engine_mod, "hostmem_write"):
2145 eng_inputs["hostmem_write"] = esi.HostMem.write_from_bundle(
2146 esi.AppID(idbase + ".hostmem_write"),
2147 engine_mod.hostmem_write.type)
2148 if hasattr(engine_mod, "hostmem_read"):
2149 eng_inputs["hostmem_read"] = esi.HostMem.read_from_bundle(
2150 esi.AppID(idbase + ".hostmem_read"), engine_mod.hostmem_read.type)
2151 engine = engine_mod(appid=eng_appid, **eng_inputs)
2152 engine_rec = bundles.emit_engine(engine, details=eng_details)
2153 engine_rec.add_record(bundle, {bc.name: {}})
2154 return engine
2155
2156 for bundle in bundles.to_client_reqs:
2157 # Per-request engine override: if the client's service request carries
2158 # an `"engine"` option, use that engine pair instead of the defaults
2159 # for every channel of this bundle. This is purely a hardware-side
2160 # substitution.
2161 engine_override = bundle.options.get("engine")
2162 if engine_override is None:
2163 bundle_to_host_gen = to_host_engine_gen
2164 bundle_from_host_gen = from_host_engine_gen
2165 else:
2166 bundle_to_host_gen, bundle_from_host_gen = _resolve_engine_pair(
2167 engine_override)
2168
2169 bundle_type = bundle.type
2170 to_channels = {}
2171 # Create a DMA engine for each channel headed TO the client (from the host).
2172 for bc in bundle_type.channels:
2173 if bc.direction == ChannelDirection.TO:
2174 engine = build_engine(bc, bundle_to_host_gen, bundle_from_host_gen)
2175 out_chan = engine.output_channel
2176 # For void channels, narrow the 8-bit placeholder back to 0-bit.
2177 if bc.channel.inner_type.bitwidth == 0:
2178 out_chan = out_chan.transform(lambda _: Bits(0)(0))
2179 to_channels[bc.name] = out_chan
2180
2181 client_bundle_sig, froms = bundle_type.pack(**to_channels)
2182 bundle.assign(client_bundle_sig)
2183
2184 # Create a DMA engine for each channel headed FROM the client (to the host).
2185 for bc in bundle_type.channels:
2186 if bc.direction == ChannelDirection.FROM:
2187 build_engine(bc, bundle_to_host_gen, bundle_from_host_gen,
2188 froms[bc.name])
2189
2190 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:596
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:1346
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:970
select_read_gearbox(bool is_list, int input_bitwidth, int output_bitwidth)
Definition common.py:1116
Tuple[Callable, Callable] _resolve_engine_pair(str path)
Definition common.py:2034
type["SliceReadGearboxImpl"] SliceReadGearbox(int input_bitwidth, int output_bitwidth)
Definition common.py:720
Module HeaderMMIO(int manifest_loc)
Definition common.py:72
type["ConcatReadGearboxImpl"] ConcatReadGearbox(int input_bitwidth, int output_bitwidth)
Definition common.py:770
type[ 'DummyToHostEngineImpl'] DummyToHostEngine(Type client_type)
Definition common.py:1990
type[ 'DummyFromHostEngineImpl'] DummyFromHostEngine(Type client_type)
Definition common.py:2011
type["TaggedWriteGearboxImpl"] TaggedWriteGearbox(int input_bitwidth, int output_bitwidth, int max_burst_bytes)
Definition common.py:1532
type["DepackReadGearboxImpl"] DepackReadGearbox(int input_bitwidth, int output_bitwidth)
Definition common.py:875
type[ 'EmitEveryNImpl'] EmitEveryN(Type message_type, int N)
Definition common.py:1684
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:1741
HostMemReadReqSplitter(Channel req_channel_type, Channel resp_channel_type, int max_chunk_bytes)
Definition common.py:1153