CIRCT 23.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, 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.flow import 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))).as_bits()
165 ports.reset_request = (reset_detect & s1_to_s2_xact).as_bits()
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)).as_bits()
253
254 # Input ready only when selected output has no valid data latched.
255 input_ready.assign((selected_valid_expr ^ Bits(1)(1)).as_bits())
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).as_bits(),
413 increment=pending,
414 instance_name="reset_delay_counter")
415 fire.assign(
416 (pending &
417 (count.out == UInt(counter_width)(delay_cycles - 1))).as_bits())
418 ports.design_reset = fire
419 ports.reset_pending = pending
420
421 return DesignResetControllerImpl
422
423
424class ChannelMMIO(esi.ServiceImplementation):
425 """MMIO service implementation with MMIO bundle interfaces. Should be
426 relatively easy to adapt to physical interfaces by wrapping the wires to
427 channels then bundles. Allows the implementation to be shared and (hopefully)
428 platform independent.
429
430 Whether or not to support unaligned accesses is up to the clients. The header
431 and manifest do not support unaligned accesses and throw away the lower three
432 bits.
433
434 Only allows one outstanding request at a time. This is enforced in hardware
435 by a `MaxOutstandingLimiter` on the command channel, which stalls incoming
436 commands until the previous response has been consumed. If a client fails to
437 return a response, the MMIO service will hang. TODO: add some kind of
438 timeout.
439
440 Implementation-defined MMIO layout:
441 - 0x0: 0 constant
442 - 0x8: Magic number (0x207D98E5_E5100E51)
443 - 0x12: ESI version number (0)
444 - 0x18: Location of the manifest ROM (absolute address)
445
446 - 0x400: Start of MMIO space for requests. Mapping is contained in the
447 manifest so can be dynamically queried.
448
449 - addr(Manifest ROM) + 0: Size of compressed manifest
450 - addr(Manifest ROM) + 8: Start of compressed manifest
451
452 This layout _should_ be pretty standard, but different BSPs may have various
453 different restrictions. Any BSP which uses this service implementation will
454 have this layout, possibly with an offset or address window.
455 """
456
457 clk = Clock()
458 rst = Input(Bits(1))
459
460 cmd = Input(esi.MMIO.read_write.type)
461
462 # Asserted for one cycle when the host requests a design reset via an MMIO
463 # write to the header. Propagates up to the BSP which performs the reset.
464 reset_request = Output(Bits(1))
465
466 # Amount of register space each client gets. This is a GIANT HACK and needs to
467 # be replaced by parameterizable services.
468 # TODO: make the amount of register space each client gets a parameter.
469 # Supporting this will require more address decode logic.
470
471 RegisterSpace = 0x400
472 RegisterSpaceBits = RegisterSpace.bit_length() - 1
473 AddressMask = 0x3FF
474
475 # Start at this address for assigning MMIO addresses to service requests.
476 initial_offset: int = RegisterSpace
477
478 @generator
479 def generate(ports, bundles: esi._ServiceGeneratorBundles):
480 table, manifest_loc = ChannelMMIO.build_table(bundles)
481 ChannelMMIO.build_read(ports, manifest_loc, table)
482 return True
483
484 @staticmethod
485 def build_table(bundles) -> Tuple[Dict[int, AssignableSignal], int]:
486 """Build a table of read and write addresses to BundleSignals."""
487 offset = ChannelMMIO.initial_offset
488 table: Dict[int, AssignableSignal] = {}
489 for bundle in bundles.to_client_reqs:
490 if bundle.port == 'read':
491 table[offset] = bundle
492 bundle.add_record(details={
493 "offset": offset,
494 "size": ChannelMMIO.RegisterSpace,
495 "type": "ro"
496 })
497 offset += ChannelMMIO.RegisterSpace
498 elif bundle.port == 'read_write':
499 table[offset] = bundle
500 bundle.add_record(details={
501 "offset": offset,
502 "size": ChannelMMIO.RegisterSpace,
503 "type": "rw"
504 })
505 offset += ChannelMMIO.RegisterSpace
506 else:
507 assert False, "Unrecognized port name."
508
509 manifest_loc = offset
510 return table, manifest_loc
511
512 @staticmethod
513 def build_read(ports, manifest_loc: int, table: Dict[int, AssignableSignal]):
514 """Builds the read side of the MMIO service."""
515
516 # Instantiate the header and manifest ROM. Fill in the read_table with
517 # bundle wires to be assigned identically to the other MMIO clients.
518 header_bundle_wire = Wire(esi.MMIO.read_write.type)
519 table[0] = header_bundle_wire
520 header = HeaderMMIO(manifest_loc)(clk=ports.clk,
521 rst=ports.rst,
522 read=header_bundle_wire)
523
524 mani_bundle_wire = Wire(esi.MMIO.read.type)
525 table[manifest_loc] = mani_bundle_wire
526 ESI_Manifest_ROM_Wrapper(clk=ports.clk, read=mani_bundle_wire)
527
528 # Unpack the cmd bundle.
529 data_resp_channel = Wire(Channel(esi.MMIODataType))
530 counted_output = Wire(Channel(esi.MMIODataType))
531 cmd_channel = ports.cmd.unpack(data=counted_output)["cmd"]
532 counted_output.assign(data_resp_channel)
533
534 # Enforce the single-outstanding-transaction invariant in hardware: hold
535 # off accepting a new command until the response to the previous command
536 # has been consumed by the host. Snoop the response wire for the
537 # completion pulse.
538 resp_xact, _ = counted_output.snoop_xact()
539 cmd_limiter = MaxOutstandingLimiter(cmd_channel.type.inner_type,
540 max_outstanding=1)(
541 clk=ports.clk,
542 rst=ports.rst,
543 in_=cmd_channel,
544 complete=resp_xact,
545 instance_name="cmd_rate_limiter",
546 )
547 cmd_channel = cmd_limiter.out
548
549 # Get the selection index and the address to hand off to the clients.
550 sel_bits, client_cmd_chan = ChannelMMIO.build_addr_read(
551 cmd_channel, len(table), manifest_loc)
552
553 # Build the demux/mux and assign the results of each appropriately.
554 read_clients_clog2 = clog2(len(table))
555 # Combine selection bits and command channel payload into a struct channel for the demux tree.
556 TreeInType = StructType([
557 ("sel", Bits(read_clients_clog2)),
558 ("data", client_cmd_chan.type.inner_type),
559 ])
560 sel_bits_truncated = sel_bits.pad_or_truncate(read_clients_clog2)
561 combined_cmd_chan = client_cmd_chan.transform(
562 lambda cmd, _sel=sel_bits_truncated: TreeInType({
563 "sel": _sel,
564 "data": cmd
565 }))
567 client_cmd_chan.type.inner_type, len(table), branching_factor_log2=2)(
568 clk=ports.clk,
569 rst=ports.rst,
570 inp=combined_cmd_chan,
571 instance_name="client_cmd_demux",
572 )
573 client_cmd_channels = [demux_inst.get_out(i) for i in range(len(table))]
574 client_data_channels = []
575 for (idx, offset) in enumerate(sorted(table.keys())):
576 bundle_wire = table[offset]
577 bundle_type = bundle_wire.type
578 if bundle_type == esi.MMIO.read.type:
579 offset = client_cmd_channels[idx].transform(lambda cmd: cmd.offset)
580 bundle, bundle_froms = esi.MMIO.read.type.pack(offset=offset)
581 elif bundle_type == esi.MMIO.read_write.type:
582 bundle, bundle_froms = esi.MMIO.read_write.type.pack(
583 cmd=client_cmd_channels[idx])
584 else:
585 assert False, "Unrecognized bundle type."
586 bundle_wire.assign(bundle)
587 client_data_channels.append(bundle_froms["data"])
588 resp_channel = esi.ChannelMux(client_data_channels)
589 data_resp_channel.assign(resp_channel)
590
591 # The header surfaces a reset request when the host writes the reset magic
592 # number to slot 7. Propagate it up to the caller (the BSP).
593 ports.reset_request = header.reset_request
594
595 @staticmethod
596 def build_addr_read(read_addr_chan: ChannelSignal, num_clients: int,
597 manifest_loc: int) -> Tuple[BitsSignal, ChannelSignal]:
598 """Build a channel for the address read request. Returns the index to select
599 the client and a channel for the masked address to be passed to the
600 clients."""
601
602 # Decoding the selection bits is very simple as of now. This might need to
603 # change to support more flexibility in addressing. Not clear if what we're
604 # doing now it sufficient or not.
605
606 manifest_loc_const = UInt(32)(manifest_loc)
607
608 cmd_ready_wire = Wire(Bits(1))
609 cmd, cmd_valid = read_addr_chan.unwrap(cmd_ready_wire)
610 is_manifest_read = cmd.offset >= manifest_loc_const
611 sel_bits = NamedWire(Bits(32 - ChannelMMIO.RegisterSpaceBits), "sel_bits")
612 # If reading the manifest, override the selection to select the manifest instead.
613 sel_bits.assign(
614 Mux(is_manifest_read,
615 cmd.offset.as_bits()[ChannelMMIO.RegisterSpaceBits:],
616 Bits(32 - ChannelMMIO.RegisterSpaceBits)(num_clients - 1)))
617 regular_client_offset = (cmd.offset.as_bits() &
618 Bits(32)(ChannelMMIO.AddressMask)).as_uint()
619 offset = Mux(is_manifest_read, regular_client_offset,
620 (cmd.offset - manifest_loc_const).as_uint(32))
621 client_cmd = NamedWire(esi.MMIOReadWriteCmdType, "client_cmd")
622 client_cmd.assign(
623 esi.MMIOReadWriteCmdType({
624 "write": cmd.write,
625 "offset": offset,
626 "data": cmd.data
627 }))
628 client_addr_chan, client_addr_ready = Channel(
629 esi.MMIOReadWriteCmdType).wrap(client_cmd, cmd_valid)
630 cmd_ready_wire.assign(client_addr_ready)
631 return sel_bits, client_addr_chan
632
633
634class MMIOIndirection(Module):
635 """Some platforms do not support MMIO space greater than a certain size (e.g.
636 Vitis 2022's limit is 4k). This module implements a level of indirection to
637 provide access to a full 32-bit address space.
638
639 MMIO addresses:
640 - 0x0: 0 constant
641 - 0x8: 64 bit ESI magic number for Indirect MMIO (0x312bf0cc_E5100E51)
642 - 0x10: Version number for Indirect MMIO (0)
643 - 0x18: Location of read/write in the virtual MMIO space.
644 - 0x20: A read from this location will initiate a read in the virtual MMIO
645 space specified by the address stored in 0x18 and return the result.
646 A write to this location will initiate a write into the virtual MMIO
647 space to the virtual address specified in 0x18.
648 """
649 clk = Clock()
650 rst = Reset()
651
652 upstream = Input(esi.MMIO.read_write.type)
653 downstream = Output(esi.MMIO.read_write.type)
654
655 @generator
656 def build(ports):
657 # This implementation assumes there is only one outstanding upstream MMIO
658 # transaction in flight at once. TODO: enforce this or make it more robust.
659
660 reg_bits = 8
661 location_reg = UInt(reg_bits)(0x18)
662 indirect_mmio_reg = UInt(reg_bits)(0x20)
663 virt_address = Wire(UInt(32))
664
665 # Set up the upstream MMIO interface. Capture last upstream command in a
666 # mailbox which never empties to give access to the last command for all
667 # time.
668 upstream_resp_chan_wire = Wire(Channel(esi.MMIODataType))
669 upstream_cmd_chan = ports.upstream.unpack(
670 data=upstream_resp_chan_wire)["cmd"]
671 _, _, upstream_cmd_data = upstream_cmd_chan.snoop()
672
673 # Set up a channel demux to separate the MMIO commands which get processed
674 # locally with ones which should be transformed and fowarded downstream.
675 phys_loc = upstream_cmd_data.offset.as_uint(reg_bits)
676 fwd_upstream = NamedWire(phys_loc == indirect_mmio_reg, "fwd_upstream")
677 local_reg_cmd_chan, downstream_cmd_channel = esi.ChannelDemux(
678 upstream_cmd_chan, fwd_upstream, 2, "upstream_demux")
679
680 # Set up the downstream MMIO interface.
681 downstream_cmd_channel = downstream_cmd_channel.transform(
682 lambda cmd: esi.MMIOReadWriteCmdType({
683 "write": cmd.write,
684 "offset": virt_address,
685 "data": cmd.data
686 }))
687 ports.downstream, froms = esi.MMIO.read_write.type.pack(
688 cmd=downstream_cmd_channel)
689 downstream_data_chan = froms["data"]
690
691 # Process local regs.
692 (local_reg_cmd_valid, local_reg_cmd_ready,
693 local_reg_cmd) = local_reg_cmd_chan.snoop()
694 write_virt_address = (local_reg_cmd_valid & local_reg_cmd_ready &
695 local_reg_cmd.write & (phys_loc == location_reg))
696 virt_address.assign(
697 local_reg_cmd.data.as_uint(32).reg(
698 name="virt_address",
699 clk=ports.clk,
700 ce=write_virt_address,
701 ))
702
703 # Build the pysical MMIO register space.
704 local_reg_resp_array = Array(Bits(64), 4)([
705 0x0, # 0x0
706 IndirectionMagicNumber, # 0x8
707 IndirectionVersionNumber, # 0x10
708 virt_address.as_bits(64), # 0x18
709 ])
710 local_reg_resp_chan = local_reg_cmd_chan.transform(
711 lambda cmd: local_reg_resp_array[cmd.offset.as_uint(2)])
712
713 # Mux together the local register responses and the downstream data to
714 # create the upstream response.
715 upstream_resp = esi.ChannelMux([local_reg_resp_chan, downstream_data_chan])
716 upstream_resp_chan_wire.assign(upstream_resp)
717
718
719@modparams
720def TaggedReadGearbox(input_bitwidth: int,
721 output_bitwidth: int) -> type["TaggedReadGearboxImpl"]:
722 """Build a gearbox to convert the upstream data to the client data
723 type. Assumes a struct {tag, data} and only gearboxes the data. Tag is stored
724 separately and the struct is re-assembled later on."""
725
726 class TaggedReadGearboxImpl(Module):
727 clk = Clock()
728 rst = Reset()
729 in_ = InputChannel(
730 StructType([
731 ("tag", esi.HostMem.TagType),
732 ("data", Bits(input_bitwidth)),
733 ]))
734 out = OutputChannel(
735 StructType([
736 ("tag", esi.HostMem.TagType),
737 ("data", Bits(output_bitwidth)),
738 ]))
739
740 @generator
741 def build(ports):
742 ready_for_upstream = Wire(Bits(1), name="ready_for_upstream")
743 upstream_tag_and_data, upstream_valid = ports.in_.unwrap(
744 ready_for_upstream)
745 upstream_data = upstream_tag_and_data.data
746 upstream_xact = ready_for_upstream & upstream_valid
747
748 # Determine if gearboxing is necessary and whether it needs to be
749 # gearboxed up or just sliced down.
750 if output_bitwidth == input_bitwidth:
751 client_data_bits = upstream_data
752 client_valid = upstream_valid
753 elif output_bitwidth < input_bitwidth:
754 client_data_bits = upstream_data[:output_bitwidth]
755 client_valid = upstream_valid
756 else:
757 # Create registers equal to the number of upstream transactions needed
758 # to fill the client data. Set the output to the concatenation of said
759 # registers.
760 chunks = ceil(output_bitwidth / input_bitwidth)
761 reg_ces = [Wire(Bits(1)) for _ in range(chunks)]
762 regs = [
763 upstream_data.reg(ports.clk,
764 ports.rst,
765 ce=reg_ces[idx],
766 name=f"chunk_reg_{idx}") for idx in range(chunks)
767 ]
768 client_data_bits = BitsSignal.concat(reversed(regs))[:output_bitwidth]
769
770 # Use counter to determine to which register to write and determine if
771 # the registers are all full.
772 clear_counter = Wire(Bits(1))
773 counter_width = clog2(chunks)
774 counter = Counter(counter_width)(clk=ports.clk,
775 rst=ports.rst,
776 clear=clear_counter,
777 increment=upstream_xact)
778 set_client_valid = counter.out == chunks - 1
779 client_xact = Wire(Bits(1))
780 client_valid = ControlReg(ports.clk, ports.rst,
781 [set_client_valid & upstream_xact],
782 [client_xact])
783 client_xact.assign(client_valid & ready_for_upstream)
784 clear_counter.assign(client_xact)
785 for idx, reg_ce in enumerate(reg_ces):
786 reg_ce.assign(upstream_xact &
787 (counter.out == UInt(counter_width)(idx)))
788
789 # Construct the output channel. Shared logic across all three cases.
790 tag_reg = upstream_tag_and_data.tag.reg(ports.clk,
791 ports.rst,
792 ce=upstream_xact,
793 name="tag_reg")
794
795 client_channel, client_ready = TaggedReadGearboxImpl.out.type.wrap(
796 {
797 "tag": tag_reg,
798 "data": client_data_bits,
799 }, client_valid)
800 ready_for_upstream.assign(client_ready)
801 ports.out = client_channel
802
803 return TaggedReadGearboxImpl
804
805
806def HostmemReadProcessor(read_width: int, hostmem_module,
807 reqs: List[esi._OutputBundleSetter]):
808 """Construct a host memory read request module to orchestrate the the read
809 connections. Responsible for both gearboxing the data, multiplexing the
810 requests, reassembling out-of-order responses and routing the responses to the
811 correct clients.
812
813 Generate this module dynamically to allow for multiple read clients of
814 multiple types to be directly accomodated."""
815
816 class HostmemReadProcessorImpl(Module):
817 clk = Clock()
818 rst = Reset()
819
820 # Add an output port for each read client.
821 reqPortMap: Dict[esi._OutputBundleSetter, str] = {}
822 for req in reqs:
823 name = "client_" + req.client_name_str
824 locals()[name] = Output(req.type)
825 reqPortMap[req] = name
826
827 # And then the port which goes to the host.
828 upstream = Output(hostmem_module.read.type)
829
830 @generator
831 def build(ports):
832 """Build the read side of the HostMem service."""
833
834 # If there's no read clients, just return a no-op read bundle.
835 if len(reqs) == 0:
836 upstream_req_channel, _ = Channel(hostmem_module.UpstreamReadReq).wrap(
837 {
838 "tag": 0,
839 "length": 0,
840 "address": 0
841 }, 0)
842 upstream_read_bundle, _ = hostmem_module.read.type.pack(
843 req=upstream_req_channel)
844 ports.upstream = upstream_read_bundle
845 return
846
847 # Since we use the tag to identify the client, we can't have more than 256
848 # read clients. Supporting more than 256 clients would require
849 # tag-rewriting, which we'll probably have to implement at some point.
850 # TODO: Implement tag-rewriting.
851 assert len(reqs) <= 256, "More than 256 read clients not supported."
852
853 # Pack the upstream bundle and leave the request as a wire.
854 upstream_req_channel = Wire(Channel(hostmem_module.UpstreamReadReq))
855 upstream_read_bundle, froms = hostmem_module.read.type.pack(
856 req=upstream_req_channel)
857 ports.upstream = upstream_read_bundle
858 upstream_resp_channel = froms["resp"]
859
860 demux = esi.TaggedDemux(len(reqs), upstream_resp_channel.type)(
861 clk=ports.clk, rst=ports.rst, in_=upstream_resp_channel)
862
863 tagged_client_reqs = []
864 for idx, client in enumerate(reqs):
865 # Find the response channel in the request bundle.
866 resp_type = [
867 c.channel for c in client.type.channels if c.name == 'resp'
868 ][0]
869 demuxed_upstream_channel = demux.get_out(idx)
870
871 # TODO: Should responses come back out-of-order (interleaved tags),
872 # re-order them here so the gearbox doesn't get confused. (Longer term.)
873 # For now, only support one outstanding transaction at a time. This has
874 # the additional benefit of letting the upstream tag be the client
875 # identifier. TODO: Implement the gating logic here.
876
877 # Gearbox the data to the client's data type.
878 client_type = resp_type.inner_type
879 if client_type.data.bitwidth == 0:
880 raise ValueError("Client data type cannot be zero-width. Use a "
881 "single-bit type if no data is needed.")
882
883 gearbox = TaggedReadGearbox(read_width, client_type.data.bitwidth)(
884 clk=ports.clk, rst=ports.rst, in_=demuxed_upstream_channel)
885 client_resp_channel = gearbox.out.transform(lambda m: client_type({
886 "tag": m.tag,
887 "data": m.data.bitcast(client_type.data)
888 }))
889
890 # Assign the client response to the correct port.
891 client_bundle, froms = client.type.pack(resp=client_resp_channel)
892 client_req = froms["req"]
893 tagged_client_req = client_req.transform(
894 lambda r: hostmem_module.UpstreamReadReq({
895 "address": r.address,
896 "length": (client_type.data.bitwidth + 7) // 8,
897 # TODO: Change this once we support tag-rewriting.
898 "tag": idx
899 }))
900 tagged_client_reqs.append(tagged_client_req)
901
902 # Set the port for the client request.
903 setattr(ports, HostmemReadProcessorImpl.reqPortMap[client],
904 client_bundle)
905
906 # Assign the multiplexed read request to the upstream request.
907 # TODO: Don't release a request until the client is ready to accept
908 # the response otherwise the system could deadlock.
909 muxed_client_reqs = esi.ChannelMux(tagged_client_reqs)
910 upstream_req_channel.assign(muxed_client_reqs)
911 HostmemReadProcessorImpl.reqPortMap.clear()
912
913 return HostmemReadProcessorImpl
914
915
916@modparams
917def TaggedWriteGearbox(input_bitwidth: int,
918 output_bitwidth: int) -> type["TaggedWriteGearboxImpl"]:
919 """Build a gearbox to convert the client data to upstream write chunks.
920 Assumes a struct {address, tag, data} and only gearboxes the data. Tag is
921 stored separately and the struct is re-assembled later on."""
922
923 if output_bitwidth % 8 != 0:
924 raise ValueError("Output bitwidth must be a multiple of 8.")
925 input_pad_bits = 0
926 if input_bitwidth % 8 != 0:
927 input_pad_bits = 8 - (input_bitwidth % 8)
928 input_padded_bitwidth = input_bitwidth + input_pad_bits
929
930 class TaggedWriteGearboxImpl(Module):
931 clk = Clock()
932 rst = Reset()
933 in_ = InputChannel(
934 StructType([
935 ("address", UInt(64)),
936 ("tag", esi.HostMem.TagType),
937 ("data", Bits(input_bitwidth)),
938 ]))
939 out = OutputChannel(
940 StructType([
941 ("address", UInt(64)),
942 ("tag", esi.HostMem.TagType),
943 ("data", Bits(output_bitwidth)),
944 ("valid_bytes", Bits(8)),
945 ]))
946
947 num_chunks = ceil(input_padded_bitwidth / output_bitwidth)
948
949 @generator
950 def build(ports):
951 upstream_ready = Wire(Bits(1))
952 ready_for_client = Wire(Bits(1))
953 client_tag_and_data, client_valid = ports.in_.unwrap(ready_for_client)
954 client_data = client_tag_and_data.data
955 if input_pad_bits > 0:
956 client_data = client_data.pad_or_truncate(input_padded_bitwidth)
957 client_xact = ready_for_client & client_valid
958 input_bitwidth_bytes = input_padded_bitwidth // 8
959 output_bitwidth_bytes = output_bitwidth // 8
960
961 # Determine if gearboxing is necessary and whether it needs to be
962 # gearboxed up or just sliced down.
963 if output_bitwidth == input_padded_bitwidth:
964 upstream_data_bits = client_data
965 upstream_valid = client_valid
966 ready_for_client.assign(upstream_ready)
967 tag = client_tag_and_data.tag
968 address = client_tag_and_data.address
969 valid_bytes = Bits(8)(input_bitwidth_bytes)
970 elif output_bitwidth > input_padded_bitwidth:
971 upstream_data_bits = client_data.as_bits(output_bitwidth)
972 upstream_valid = client_valid
973 ready_for_client.assign(upstream_ready)
974 tag = client_tag_and_data.tag
975 address = client_tag_and_data.address
976 valid_bytes = Bits(8)(input_bitwidth_bytes)
977 else:
978 # Create registers equal to the number of upstream transactions needed
979 # to complete the transmission.
980 num_chunks = TaggedWriteGearboxImpl.num_chunks
981 num_chunks_idx_bitwidth = clog2(num_chunks)
982 if input_padded_bitwidth % output_bitwidth == 0:
983 padding_numbits = 0
984 else:
985 padding_numbits = output_bitwidth - (input_padded_bitwidth %
986 output_bitwidth)
987 client_data_padded = BitsSignal.concat(
988 [Bits(padding_numbits)(0), client_data])
989 chunks = [
990 client_data_padded[i * output_bitwidth:(i + 1) * output_bitwidth]
991 for i in range(num_chunks)
992 ]
993 chunk_regs = Array(Bits(output_bitwidth), num_chunks)([
994 c.reg(ports.clk, ce=client_xact, name=f"chunk_{idx}")
995 for idx, c in enumerate(chunks)
996 ])
997 increment = Wire(Bits(1))
998 clear = Wire(Bits(1))
999 counter = Counter(num_chunks_idx_bitwidth)(clk=ports.clk,
1000 rst=ports.rst,
1001 increment=increment,
1002 clear=clear)
1003 upstream_data_bits = chunk_regs[counter.out]
1004 upstream_valid = ControlReg(ports.clk, ports.rst, [client_xact],
1005 [clear])
1006 upstream_xact = upstream_valid & upstream_ready
1007 clear.assign(upstream_xact & (counter.out == (num_chunks - 1)))
1008 increment.assign(upstream_xact)
1009 ready_for_client.assign(~upstream_valid)
1010 address_padding_bits = clog2(output_bitwidth_bytes)
1011 counter_bytes = BitsSignal.concat(
1012 [counter.out.as_bits(),
1013 Bits(address_padding_bits)(0)]).as_uint()
1014
1015 # Construct the output channel. Shared logic across all three cases.
1016 tag_reg = client_tag_and_data.tag.reg(ports.clk,
1017 ce=client_xact,
1018 name="tag_reg")
1019 addr_reg = client_tag_and_data.address.reg(ports.clk,
1020 ce=client_xact,
1021 name="address_reg")
1022 address = (addr_reg + counter_bytes).as_uint(64)
1023 tag = tag_reg
1024 valid_bytes = Mux(counter.out == (num_chunks - 1),
1025 Bits(8)(output_bitwidth_bytes),
1026 Bits(8)((output_bitwidth - padding_numbits) // 8))
1027
1028 upstream_channel, upstrm_ready_sig = TaggedWriteGearboxImpl.out.type.wrap(
1029 {
1030 "address": address,
1031 "tag": tag,
1032 "data": upstream_data_bits,
1033 "valid_bytes": valid_bytes
1034 }, upstream_valid)
1035 upstream_ready.assign(upstrm_ready_sig)
1036 ports.out = upstream_channel
1037
1038 return TaggedWriteGearboxImpl
1039
1040
1041@modparams
1042def EmitEveryN(message_type: Type, N: int) -> type['EmitEveryNImpl']:
1043 """Emit (forward) one message for every N input messages. The emitted message
1044 is the last one of the N received. N must be >= 1."""
1045
1046 if N < 1:
1047 raise ValueError("N must be >= 1")
1048
1049 class EmitEveryNImpl(Module):
1050 clk = Clock()
1051 rst = Reset()
1052 in_ = InputChannel(message_type)
1053 out = OutputChannel(message_type)
1054
1055 @generator
1056 def build(ports):
1057 ready_for_in = Wire(Bits(1))
1058 in_data, in_valid = ports.in_.unwrap(ready_for_in)
1059 xact = in_valid & ready_for_in
1060
1061 # Fast path: N == 1 -> pass-through.
1062 if N == 1:
1063 out_chan, out_ready = EmitEveryNImpl.out.type.wrap(in_data, in_valid)
1064 ready_for_in.assign(out_ready)
1065 ports.out = out_chan
1066 return
1067
1068 counter_width = clog2(N)
1069 counter_clear = Wire(Bits(1))
1070 counter = Counter(counter_width)(clk=ports.clk,
1071 rst=ports.rst,
1072 increment=xact,
1073 clear=counter_clear)
1074
1075 # Capture last message of the group.
1076 last_msg = in_data.reg(ports.clk, ports.rst, ce=xact, name="last_msg")
1077 # Clear the counter.
1078 hit_last = (counter.out == UInt(counter_width)(N - 1)) & xact
1079 counter_clear.assign(hit_last)
1080
1081 emit_accepted = Wire(Bits(1))
1082 out_valid = ControlReg(ports.clk, ports.rst, [hit_last], [emit_accepted])
1083
1084 out_chan, out_ready = EmitEveryNImpl.out.type.wrap(last_msg, out_valid)
1085 # Stall input while waiting for downstream to accept the aggregated output.
1086 ready_for_in.assign(~(out_valid & ~out_ready))
1087 emit_accepted.assign(out_valid & out_ready) # Output consumed downstream.
1088
1089 ports.out = out_chan
1090
1091 return EmitEveryNImpl
1092
1093
1095 write_width: int, hostmem_module,
1096 reqs: List[esi._OutputBundleSetter]) -> type["HostMemWriteProcessorImpl"]:
1097 """Construct a host memory write request module to orchestrate the the write
1098 connections. Responsible for both gearboxing the data, multiplexing the
1099 requests, reassembling out-of-order responses and routing the responses to the
1100 correct clients.
1101
1102 Generate this module dynamically to allow for multiple write clients of
1103 multiple types to be directly accomodated."""
1104
1105 class HostMemWriteProcessorImpl(Module):
1106
1107 clk = Clock()
1108 rst = Reset()
1109
1110 # Add an output port for each read client.
1111 reqPortMap: Dict[esi._OutputBundleSetter, str] = {}
1112 for req in reqs:
1113 name = "client_" + req.client_name_str
1114 locals()[name] = Output(req.type)
1115 reqPortMap[req] = name
1116
1117 # And then the port which goes to the host.
1118 upstream = Output(hostmem_module.write.type)
1119
1120 @generator
1121 def build(ports):
1122 clk = ports.clk
1123 rst = ports.rst
1124
1125 # If there's no write clients, just create a no-op write bundle
1126 if len(reqs) == 0:
1127 req, _ = Channel(hostmem_module.UpstreamWriteReq).wrap(
1128 {
1129 "address": 0,
1130 "tag": 0,
1131 "data": 0,
1132 "valid_bytes": 0,
1133 }, 0)
1134 write_bundle, _ = hostmem_module.write.type.pack(req=req)
1135 ports.upstream = write_bundle
1136 return
1137
1138 assert len(reqs) <= 256, "More than 256 write clients not supported."
1139
1140 upstream_req_channel = Wire(Channel(hostmem_module.UpstreamWriteReq))
1141 upstream_write_bundle, froms = hostmem_module.write.type.pack(
1142 req=upstream_req_channel)
1143 ports.upstream = upstream_write_bundle
1144 upstream_ack_tag = froms["ackTag"]
1145
1146 demuxed_acks = esi.TaggedDemux(len(reqs), upstream_ack_tag.type)(
1147 clk=ports.clk, rst=ports.rst, in_=upstream_ack_tag)
1148
1149 # TODO: re-write the tags and store the client and client tag.
1150
1151 # Build the write request channels and ack wires.
1152 write_channels: List[ChannelSignal] = []
1153 for idx, req in enumerate(reqs):
1154 # Get the request channel and its data type.
1155 reqch = [c.channel for c in req.type.channels if c.name == 'req'][0]
1156 client_type = reqch.inner_type
1157 if isinstance(client_type.data, Window):
1158 client_type = client_type.lowered_type
1159
1160 # Pack up the bundle and assign the request channel.
1161 write_req_bundle_type = esi.HostMem.write_req_bundle_type(
1162 client_type.data)
1163 input_flit_ack = Wire(upstream_ack_tag.type)
1164 bundle_sig, froms = write_req_bundle_type.pack(ackTag=input_flit_ack)
1165
1166 gearbox_mod = TaggedWriteGearbox(client_type.data.bitwidth, write_width)
1167 gearbox_in_type = gearbox_mod.in_.type.inner_type
1168 tagged_client_req = froms["req"]
1169 bitcast_client_req = tagged_client_req.transform(
1170 lambda m: gearbox_in_type({
1171 "tag": m.tag,
1172 "address": m.address,
1173 "data": m.data.bitcast(gearbox_in_type.data)
1174 }))
1175
1176 # Gearbox the data to the client's data type.
1177 gearbox = gearbox_mod(clk=ports.clk,
1178 rst=ports.rst,
1179 in_=bitcast_client_req)
1180 write_channels.append(
1181 gearbox.out.transform(lambda m: m.type({
1182 "address": m.address,
1183 "tag": idx,
1184 "data": m.data,
1185 "valid_bytes": m.valid_bytes
1186 })))
1187
1188 # Count the number of acks received from hostmem for this client
1189 # and only send one back to the client per input.
1190 ack_every_n = EmitEveryN(upstream_ack_tag.type, gearbox_mod.num_chunks)(
1191 clk=clk, rst=rst, in_=demuxed_acks.get_out(idx))
1192 input_flit_ack.assign(ack_every_n.out)
1193
1194 # Set the port for the client request.
1195 setattr(ports, HostMemWriteProcessorImpl.reqPortMap[req], bundle_sig)
1196
1197 # Build a channel mux for the write requests.
1198 muxed_write_channel = esi.ChannelMux(write_channels)
1199 upstream_req_channel.assign(muxed_write_channel)
1200
1201 return HostMemWriteProcessorImpl
1202
1203
1204@modparams
1205def ChannelHostMem(read_width: int,
1206 write_width: int) -> typing.Type['ChannelHostMemImpl']:
1207
1208 class ChannelHostMemImpl(esi.ServiceImplementation):
1209 """Builds a HostMem service which multiplexes multiple HostMem clients into
1210 two (read and write) bundles of the given data width."""
1211
1212 clk = Clock()
1213 rst = Reset()
1214
1215 UpstreamReadReq = StructType([
1216 ("address", UInt(64)),
1217 ("length", UInt(32)), # In bytes.
1218 ("tag", UInt(8)),
1219 ])
1220 read = Output(
1221 Bundle([
1222 BundledChannel("req", ChannelDirection.TO, UpstreamReadReq),
1223 BundledChannel(
1224 "resp", ChannelDirection.FROM,
1225 StructType([
1226 ("tag", esi.HostMem.TagType),
1227 ("data", Bits(read_width)),
1228 ])),
1229 ]))
1230
1231 if write_width % 8 != 0:
1232 raise ValueError("Write width must be a multiple of 8.")
1233 UpstreamWriteReq = StructType([
1234 ("address", UInt(64)),
1235 ("tag", UInt(8)),
1236 ("data", Bits(write_width)),
1237 ("valid_bytes", Bits(8)),
1238 ])
1239 write = Output(
1240 Bundle([
1241 BundledChannel("req", ChannelDirection.TO, UpstreamWriteReq),
1242 BundledChannel("ackTag", ChannelDirection.FROM, UInt(8)),
1243 ]))
1244
1245 @generator
1246 def generate(ports, bundles: esi._ServiceGeneratorBundles):
1247 # Split the read side out into a separate module. Must assign the output
1248 # ports to the clients since we can't service a request in a different
1249 # module.
1250 read_reqs = [req for req in bundles.to_client_reqs if req.port == 'read']
1251 read_proc_module = HostmemReadProcessor(read_width, ChannelHostMemImpl,
1252 read_reqs)
1253 read_proc = read_proc_module(clk=ports.clk, rst=ports.rst)
1254 ports.read = read_proc.upstream
1255 for req in read_reqs:
1256 req.assign(getattr(read_proc, read_proc_module.reqPortMap[req]))
1257
1258 # The write side.
1259 write_reqs = [
1260 req for req in bundles.to_client_reqs if req.port == 'write'
1261 ]
1262 write_proc_module = HostMemWriteProcessor(write_width, ChannelHostMemImpl,
1263 write_reqs)
1264 write_proc = write_proc_module(clk=ports.clk, rst=ports.rst)
1265 ports.write = write_proc.upstream
1266 for req in write_reqs:
1267 req.assign(getattr(write_proc, write_proc_module.reqPortMap[req]))
1268
1269 return ChannelHostMemImpl
1270
1271
1272@modparams
1273def DummyToHostEngine(client_type: Type) -> type['DummyToHostEngineImpl']:
1274 """Create a fake DMA engine which just throws everything away."""
1275
1276 class DummyToHostEngineImpl(esi.EngineModule):
1277
1278 @property
1279 def TypeName(self):
1280 return "DummyToHostEngine"
1281
1282 clk = Clock()
1283 rst = Reset()
1284 input_channel = InputChannel(client_type)
1285
1286 @generator
1287 def build(ports):
1288 pass
1289
1290 return DummyToHostEngineImpl
1291
1292
1293@modparams
1294def DummyFromHostEngine(client_type: Type) -> type['DummyFromHostEngineImpl']:
1295 """Create a fake DMA engine which just never produces messages."""
1296
1297 class DummyFromHostEngineImpl(esi.EngineModule):
1298
1299 @property
1300 def TypeName(self):
1301 return "DummyFromHostEngine"
1302
1303 clk = Clock()
1304 rst = Reset()
1305 output_channel = OutputChannel(client_type)
1306
1307 @generator
1308 def build(ports):
1309 valid = Bits(1)(0)
1310 data = Bits(client_type.bitwidth)(0).bitcast(client_type)
1311 channel, ready = Channel(client_type).wrap(data, valid)
1312 ports.output_channel = channel
1313
1314 return DummyFromHostEngineImpl
1315
1316
1317def _resolve_engine_pair(path: str) -> Tuple[Callable, Callable]:
1318 """Resolve a dotted Python import path to a
1319 `(to_host_engine_gen, from_host_engine_gen)` tuple, used to override the
1320 default engine pair for a specific service request.
1321
1322 The path may point at either:
1323 - a module-level 2-tuple attribute, e.g.
1324 `"mypkg.mymod.MyEnginePair"` where `MyEnginePair` is
1325 `(MyToHost, MyFromHost)`; or
1326 - a zero-arg factory callable returning such a tuple.
1327 """
1328 import importlib
1329 if not isinstance(path, str):
1330 raise TypeError(
1331 "Engine override path must be a dotted 'pkg.mod.attr' string; "
1332 f"got {type(path).__name__}")
1333 module_path, _, attr_path = path.rpartition(".")
1334 if not module_path or not attr_path:
1335 raise ValueError(
1336 "Engine override path must be a dotted 'pkg.mod.attr' string; "
1337 f"got {path!r}")
1338 obj = importlib.import_module(module_path)
1339 for part in attr_path.split("."):
1340 obj = getattr(obj, part)
1341 if callable(obj):
1342 obj = obj()
1343 if not (isinstance(obj, tuple) and len(obj) == 2):
1344 raise TypeError(
1345 f"Engine override {path!r} must resolve to a 2-tuple "
1346 f"(to_host_engine_gen, from_host_engine_gen); got {type(obj).__name__}")
1347 if not (callable(obj[0]) and callable(obj[1])):
1348 raise TypeError(
1349 f"Engine override {path!r} must resolve to a 2-tuple of callables; got "
1350 f"({type(obj[0]).__name__}, {type(obj[1]).__name__})")
1351 return obj
1352
1353
1354def ChannelEngineService(
1355 to_host_engine_gen: Callable,
1356 from_host_engine_gen: Callable) -> type['ChannelEngineService']:
1357 """Returns a channel service implementation which calls
1358 to_host_engine_gen(<client_type>) or from_host_engine_gen(<client_type>) to
1359 generate the to_host and from_host engines for each channel. Does not support
1360 engines which can service multiple clients at once.
1361
1362 Individual service requests may override the default engine pair by passing
1363 `options={"engine": "pkg.mod.attr"}` at the service-request call site (e.g.
1364 `HostComms.some_bundle(AppID(...), options={"engine": "..."})`). The path
1365 is resolved by `_resolve_engine_pair` and must yield a
1366 `(to_host_engine_gen, from_host_engine_gen)` tuple with the same call shape
1367 as the defaults; the override applies to every channel of that request's
1368 bundle.
1369 """
1370
1371 class ChannelEngineService(esi.ServiceImplementation):
1372 """Service implementation which services the clients via a per-channel DMA
1373 engine."""
1374
1375 clk = Clock()
1376 rst = Reset()
1377
1378 @generator
1379 def build(ports, bundles: esi._ServiceGeneratorBundles):
1380 clk = ports.clk
1381 rst = ports.rst
1382
1383 def build_engine_appid(client_appid: List[esi.AppID],
1384 channel_name: str) -> str:
1385 appid_strings = [str(appid) for appid in client_appid]
1386 return f"{'_'.join(appid_strings)}.{channel_name}"
1387
1388 def build_engine(bc: BundledChannel,
1389 bundle_to_host_gen: Callable,
1390 bundle_from_host_gen: Callable,
1391 input_channel=None) -> Type:
1392 idbase = build_engine_appid(bundle.client_name, bc.name)
1393 eng_appid = esi.AppID(idbase)
1394 # DMA engines require at least 1 byte of data; substitute Bits(8)
1395 # for zero-width (void) channel types so the engine never sees a
1396 # zero-length transfer.
1397 engine_client_type = bc.channel.inner_type
1398 is_void = (engine_client_type.bitwidth == 0)
1399 if is_void:
1400 engine_client_type = Bits(8)
1401 if bc.direction == ChannelDirection.FROM:
1402 engine_mod = bundle_to_host_gen(engine_client_type)
1403 else:
1404 engine_mod = bundle_from_host_gen(engine_client_type)
1405 eng_inputs = {
1406 "clk": ports.clk,
1407 "rst": ports.rst,
1408 }
1409 eng_details: Dict[str, object] = {"engine_inst": eng_appid}
1410 if input_channel is not None:
1411 # For void channels, widen the 0-bit input to the 8-bit
1412 # placeholder the engine expects.
1413 if is_void:
1414 input_channel = input_channel.transform(lambda _: Bits(8)(0))
1415 if (engine_mod.input_channel.type.signaling
1416 != input_channel.type.signaling):
1417 input_channel = input_channel.buffer(
1418 clk,
1419 rst,
1420 stages=1,
1421 output_signaling=engine_mod.input_channel.type.signaling)
1422 eng_inputs["input_channel"] = input_channel
1423 if hasattr(engine_mod, "mmio"):
1424 mmio_appid = esi.AppID(idbase + ".mmio")
1425 eng_inputs["mmio"] = esi.MMIO.read_write(mmio_appid)
1426 eng_details["mmio"] = mmio_appid
1427 if hasattr(engine_mod, "hostmem_write"):
1428 eng_inputs["hostmem_write"] = esi.HostMem.write_from_bundle(
1429 esi.AppID(idbase + ".hostmem_write"),
1430 engine_mod.hostmem_write.type)
1431 if hasattr(engine_mod, "hostmem_read"):
1432 eng_inputs["hostmem_read"] = esi.HostMem.read_from_bundle(
1433 esi.AppID(idbase + ".hostmem_read"), engine_mod.hostmem_read.type)
1434 engine = engine_mod(appid=eng_appid, **eng_inputs)
1435 engine_rec = bundles.emit_engine(engine, details=eng_details)
1436 engine_rec.add_record(bundle, {bc.name: {}})
1437 return engine
1438
1439 for bundle in bundles.to_client_reqs:
1440 # Per-request engine override: if the client's service request carries
1441 # an `"engine"` option, use that engine pair instead of the defaults
1442 # for every channel of this bundle. This is purely a hardware-side
1443 # substitution.
1444 engine_override = bundle.options.get("engine")
1445 if engine_override is None:
1446 bundle_to_host_gen = to_host_engine_gen
1447 bundle_from_host_gen = from_host_engine_gen
1448 else:
1449 bundle_to_host_gen, bundle_from_host_gen = _resolve_engine_pair(
1450 engine_override)
1451
1452 bundle_type = bundle.type
1453 to_channels = {}
1454 # Create a DMA engine for each channel headed TO the client (from the host).
1455 for bc in bundle_type.channels:
1456 if bc.direction == ChannelDirection.TO:
1457 engine = build_engine(bc, bundle_to_host_gen, bundle_from_host_gen)
1458 out_chan = engine.output_channel
1459 # For void channels, narrow the 8-bit placeholder back to 0-bit.
1460 if bc.channel.inner_type.bitwidth == 0:
1461 out_chan = out_chan.transform(lambda _: Bits(0)(0))
1462 to_channels[bc.name] = out_chan
1463
1464 client_bundle_sig, froms = bundle_type.pack(**to_channels)
1465 bundle.assign(client_bundle_sig)
1466
1467 # Create a DMA engine for each channel headed FROM the client (to the host).
1468 for bc in bundle_type.channels:
1469 if bc.direction == ChannelDirection.FROM:
1470 build_engine(bc, bundle_to_host_gen, bundle_from_host_gen,
1471 froms[bc.name])
1472
1473 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:597
generate(ports, esi._ServiceGeneratorBundles bundles)
Definition common.py:479
Tuple[Dict[int, AssignableSignal], int] build_table(bundles)
Definition common.py:485
build_read(ports, int manifest_loc, Dict[int, AssignableSignal] table)
Definition common.py:513
type["ChannelDemuxNImpl"] ChannelDemuxN_HalfStage_ReadyBlocking(Type data_type, int num_outs, int next_sel_width)
Definition common.py:173
HostmemReadProcessor(int read_width, hostmem_module, List[esi._OutputBundleSetter] reqs)
Definition common.py:807
type["ChannelDemuxTree"] ChannelDemuxTree_HalfStage_ReadyBlocking(Type data_type, int num_outs, int branching_factor_log2)
Definition common.py:266
Tuple[Callable, Callable] _resolve_engine_pair(str path)
Definition common.py:1317
Module HeaderMMIO(int manifest_loc)
Definition common.py:72
type["TaggedWriteGearboxImpl"] TaggedWriteGearbox(int input_bitwidth, int output_bitwidth)
Definition common.py:918
type[ 'DummyToHostEngineImpl'] DummyToHostEngine(Type client_type)
Definition common.py:1273
type[ 'DummyFromHostEngineImpl'] DummyFromHostEngine(Type client_type)
Definition common.py:1294
type[ 'EmitEveryNImpl'] EmitEveryN(Type message_type, int N)
Definition common.py:1042
type["TaggedReadGearboxImpl"] TaggedReadGearbox(int input_bitwidth, int output_bitwidth)
Definition common.py:721
type["HostMemWriteProcessorImpl"] HostMemWriteProcessor(int write_width, hostmem_module, List[esi._OutputBundleSetter] reqs)
Definition common.py:1096