75 """Construct the ESI header MMIO adhering to the MMIO layout specified in
76 the ChannelMMIO service implementation."""
80 read = Input(esi.MMIO.read_write.type)
83 reset_request = Output(Bits(1))
89 data_chan_wire = Wire(Channel(esi.MMIODataType))
90 input_bundles = ports.read.unpack(data=data_chan_wire)
91 cmd_chan = input_bundles[
'cmd']
96 cmd_ready = Wire(Bits(1))
97 s1_to_s2_xact = Wire(Bits(1))
98 cmd_raw, cmd_valid = cmd_chan.unwrap(cmd_ready)
101 s1_load = cmd_valid & cmd_ready
102 cmd = cmd_raw.reg(clk, rst, ce=s1_load, name=
"cmd")
103 s1_valid = ControlReg(clk,
106 resets=[s1_to_s2_xact],
109 cmd_ready.assign(~s1_valid)
111 address_words = cmd.offset.as_bits()[3:]
112 slot = address_words[:3]
114 cycles = Counter(64)(clk=ports.clk,
117 increment=Bits(1)(1),
118 instance_name=
"cycle_counter")
121 core_freq = System.current().core_freq
122 if core_freq
is None:
124 header = Array(Bits(64), 8)([
130 cycles.out.as_bits(),
134 header.name =
"header"
137 s2_valid = Wire(Bits(1))
138 data_chan_ready = Wire(Bits(1))
139 s2_xact = s2_valid & data_chan_ready
141 s1_to_s2_xact.assign(s1_valid & ~s2_valid)
143 header_out = header[slot].reg(clk=clk,
150 asserts=[s1_to_s2_xact],
152 name=
"header_out_valid"))
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)
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
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."""
179 assert num_outs >= 1,
"num_outs must be at least 1."
181 class ChannelDemuxNImpl(Module):
186 InPayloadType = StructType([
187 (
"sel", Bits(clog2(num_outs))),
188 (
"next_sel", Bits(next_sel_width)),
191 inp = Input(Channel(InPayloadType))
192 OutPayloadType = StructType([
193 (
"next_sel", Bits(next_sel_width)),
197 for i
in range(num_outs):
198 locals()[f
"output_{i}"] = Output(Channel(OutPayloadType))
201 def generate(ports) -> None:
206 sel_width = clog2(num_outs)
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
218 valid_regs: List[BitsSignal] = []
219 selected_valid_expr = Bits(1)(0)
221 for i
in range(num_outs):
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)
228 out_msg_reg = ChannelDemuxNImpl.OutPayloadType({
229 "next_sel": in_next_sel,
231 }).reg(clk=clk, rst=rst, ce=will_write, name=f
"out{i}_msg_reg")
234 consume = Wire(Bits(1), name=f
"consume_{i}")
235 valid_reg = ControlReg(
238 asserts=[will_write],
240 name=f
"out{i}_valid_reg",
242 valid_regs.append(valid_reg)
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)
251 selected_valid_expr = selected_valid_expr | (
252 (in_sel == Bits(sel_width)(i)) & valid_reg)
255 input_ready.assign(selected_valid_expr ^ Bits(1)(1))
257 def get_out(self, index: int) -> ChannelSignal:
258 return getattr(self, f
"output_{index}")
260 return ChannelDemuxNImpl
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.
273 root_sel_width = clog2(num_outs)
275 num_outs = 2**root_sel_width
276 sel_width = branching_factor_log2
277 fanout = 2**sel_width
279 class ChannelDemuxTree(Module):
283 InPayloadType = StructType([
284 (
"sel", Bits(clog2(num_outs))),
287 inp = Input(Channel(InPayloadType))
290 for i
in range(num_outs):
291 locals()[f
"output_{i}"] = Output(Channel(data_type))
294 def build(ports) -> None:
295 assert branching_factor_log2 > 0
298 setattr(ports,
"output_0", ports.inp.transform(
lambda p: p.data))
301 def payload_type(sel_width: int, next_sel_width: int) -> Type:
303 (
"sel", Bits(sel_width)),
304 (
"next_sel", Bits(next_sel_width)),
308 def next_sel_width_calc(curr_sel_width) -> int:
309 return max(curr_sel_width - sel_width, 0)
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."""
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)
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,
330 current_channels: List[ChannelSignal] = [
331 ports.inp.transform(
lambda m: payload_type(0, root_sel_width)({
338 curr_sel_width = root_sel_width
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):
346 num_outs=level_num_outs,
347 next_sel_width=next_sel_width_calc(curr_sel_width),
351 inp=c.transform(payload_next),
352 instance_name=f
"demux_l{level}_i{i}",
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
360 for i
in range(num_outs):
365 current_channels[i].transform(
lambda p: p.data),
368 def get_out(self, index: int) -> ChannelSignal:
369 return getattr(self, f
"output_{index}")
371 return ChannelDemuxTree
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`."""
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")
784 in_bytes = input_bitwidth // 8
785 vb_width = clog2(in_bytes)
787 class ConcatReadGearboxImpl(Module):
792 (
"tag", esi.HostMem.TagType),
793 (
"data", Bits(input_bitwidth)),
794 (
"valid_bytes", UInt(vb_width)),
799 (
"tag", esi.HostMem.TagType),
800 (
"data", Bits(output_bitwidth)),
806 ready_for_upstream = Wire(Bits(1), name=
"ready_for_upstream")
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
818 chunks = ceil(output_bitwidth / input_bitwidth)
819 counter_width = clog2(chunks)
820 reg_ces = [Wire(Bits(1))
for _
in range(chunks)]
822 upstream_data.reg(ports.clk,
825 name=f
"chunk_reg_{idx}")
for idx
in range(chunks)
827 client_data_bits = BitsSignal.concat(reversed(regs))[:output_bitwidth]
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)
839 Counter(counter_width)(clk=ports.clk,
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],
848 for idx, reg_ce
in enumerate(reg_ces):
849 reg_ce.assign(upstream_xact & (counter == UInt(counter_width)(idx)))
851 client_last = upstream_last.reg(ports.clk,
855 tag_reg = up.tag.reg(ports.clk,
860 client_channel, client_ready = ConcatReadGearboxImpl.out.type.wrap(
863 "data": client_data_bits,
866 client_xact.assign(client_valid & client_ready)
867 ready_for_upstream.assign(~client_valid | client_ready)
868 ports.out = client_channel
870 return ConcatReadGearboxImpl
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`."""
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:
888 "DepackReadGearbox requires a byte-aligned element that divides the "
891 in_bytes = input_bitwidth // 8
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
898 class DepackReadGearboxImpl(Module):
903 (
"tag", esi.HostMem.TagType),
904 (
"data", Bits(input_bitwidth)),
905 (
"valid_bytes", UInt(vb_width)),
910 (
"tag", esi.HostMem.TagType),
911 (
"data", Bits(output_bitwidth)),
917 client_ready = Wire(Bits(1), name=
"client_ready")
918 up_ready = Wire(Bits(1), name=
"up_ready")
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
927 last_in_word = Bits(1)(1)
928 client_data = up.data
930 idx_width = clog2(parts)
931 idx = Reg(UInt(idx_width),
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
945 word_parts = Array(Bits(output_bitwidth), parts)([
946 up.data[k * output_bitwidth:(k + 1) * output_bitwidth]
947 for k
in range(parts)
949 client_data = word_parts[idx]
951 Mux(last_in_word, (idx + UInt(1)(1)).as_uint(idx_width),
955 up_ready.assign(client_xact & last_in_word)
956 client_channel, client_ready_sig = DepackReadGearboxImpl.out.type.wrap(
960 "last": (up.last & last_in_word).as_bits(),
962 client_ready.assign(client_ready_sig)
963 ports.out = client_channel
965 return DepackReadGearboxImpl
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.
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.
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."""
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
999 buf_bytes = stride_bytes + in_bytes
1000 buf_bits = buf_bytes * 8
1002 vb_width = clog2(in_bytes)
1003 cnt_width = clog2(buf_bytes + 1)
1006 offset_width = clog2(stride_bytes + 1)
1008 class ShiftReadGearboxImpl(Module):
1013 (
"tag", esi.HostMem.TagType),
1014 (
"data", Bits(input_bitwidth)),
1015 (
"valid_bytes", UInt(vb_width)),
1018 out = OutputChannel(
1020 (
"tag", esi.HostMem.TagType),
1021 (
"data", Bits(output_bitwidth)),
1027 client_ready = Wire(Bits(1), name=
"client_ready")
1028 up_ready = Wire(Bits(1), name=
"up_ready")
1031 in_reg = ports.in_.buffer(ports.clk, ports.rst, stages=1)
1032 up, up_valid = in_reg.unwrap(up_ready)
1034 from pycde.circt.dialects
import comb
1038 buffer = Reg(Bits(buf_bits),
1043 count = Reg(UInt(cnt_width),
1048 saw_last = Wire(Bits(1), name=
"saw_last")
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
1058 client_valid = count >= UInt(cnt_width)(stride_bytes)
1059 client_xact = client_valid & client_ready
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
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)
1092 Mux(final_emit, Mux(client_xact, appended, drained),
1097 count.assign(Mux(client_xact, after_add, after_emit))
1100 ControlReg(ports.clk, ports.rst, [set_saw_last], [final_emit]))
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(
1106 "data": buffer[0:output_bitwidth],
1107 "last": client_last,
1109 client_ready.assign(client_ready_sig)
1110 ports.out = client_channel
1112 return ShiftReadGearboxImpl
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
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.
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
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.
1177 req_channel_type: channel of the upstream read request {address, length
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.
1183 assert max_chunk_bytes > 0
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
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)),
1205 resp_out_channel_type = Channel(resp_out_struct)
1207 class HostMemReadReqSplitterImpl(Module):
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)
1222 emit_busy = Wire(Bits(1), name=
"emit_busy")
1223 resp_busy = Wire(Bits(1), name=
"resp_busy")
1224 cur_addr = Wire(UInt(addr_width), name=
"cur_addr")
1225 remaining = Wire(UInt(length_width), name=
"remaining")
1226 tag_reg = Wire(tag_type, name=
"tag_reg")
1227 words_left = Wire(UInt(words_width), name=
"words_left")
1229 idle = (~emit_busy) & (~resp_busy)
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)
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
1247 chunk_len_out = chunk_len
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)
1254 req_out_ch, req_out_ready = req_channel_type.wrap(
1256 "address": cur_addr,
1257 "length": chunk_len_out,
1260 ports.req_out = req_out_ch
1261 chunk_xact = emit_busy & req_out_ready
1265 rst, [accept], [chunk_xact & last_chunk],
1266 name=
"emit_busy_reg"))
1269 cur_addr_incr = (cur_addr +
1270 chunk_len.as_uint(addr_width)).as_uint(addr_width)
1272 Mux(accept, Mux(chunk_xact, cur_addr, cur_addr_incr),
1273 req_payload.address).reg(clk,
1276 ce=accept | chunk_xact,
1277 name=
"cur_addr_reg"))
1280 remaining_dec = (remaining - chunk_len).as_uint(length_width)
1282 Mux(accept, Mux(chunk_xact, remaining, remaining_dec),
1283 req_payload.length).reg(clk,
1286 ce=accept | chunk_xact,
1287 name=
"remaining_reg"))
1289 tag_reg.assign(req_payload.tag.reg(clk, rst, ce=accept, name=
"tag_reg_r"))
1294 total_words = ((req_payload.length + UInt(length_width)(word_bytes - 1)
1295 ).as_bits()[word_shift:]).as_uint(words_width)
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(
1316 UInt(vb_width)(word_bytes - 1), final_valid_bytes),
1320 ports.resp_out = resp_out_ch
1321 resp_ready.assign(resp_out_ready)
1322 resp_xact = resp_valid & resp_out_ready
1325 words_dec = (words_left - UInt(words_width)(1)).as_uint(words_width)
1327 Mux(accept, Mux(resp_xact, words_left, words_dec),
1328 total_words).reg(clk,
1331 ce=accept | resp_xact,
1332 name=
"words_left_reg"))
1336 rst, [accept], [resp_xact & is_final_word],
1337 name=
"resp_busy_reg"))
1339 return HostMemReadReqSplitterImpl
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
1352 Generate this module dynamically to allow for multiple read clients of
1353 multiple types to be directly accomodated."""
1355 class HostmemReadProcessorImpl(Module):
1360 reqPortMap: Dict[esi._OutputBundleSetter, str] = {}
1362 name =
"client_" + req.client_name_str
1363 locals()[name] = Output(req.type)
1364 reqPortMap[req] = name
1367 upstream = Output(hostmem_module.read.type)
1371 """Build the read side of the HostMem service."""
1375 upstream_req_channel, _ = Channel(hostmem_module.UpstreamReadReq).
wrap(
1381 upstream_read_bundle, _ = hostmem_module.read.type.pack(
1382 req=upstream_req_channel)
1383 ports.upstream = upstream_read_bundle
1390 assert len(reqs) <= 256,
"More than 256 read clients not supported."
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"]
1403 demux = esi.TaggedDemux(len(reqs), upstream_resp_channel.type)(
1404 clk=ports.clk, rst=ports.rst, in_=upstream_resp_channel)
1406 word_bytes = read_width // 8
1407 tagged_client_reqs = []
1408 for idx, client
in enumerate(reqs):
1411 c.channel
for c
in client.type.channels
if c.name ==
'resp'
1413 demuxed_upstream_channel = demux.get_out(idx)
1420 client_type = resp_type.inner_type
1421 is_list = isinstance(client_type, Window)
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.")
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
1442 elem_stride_bytes = (element_bits + 7) // 8
1452 max_chunk_bytes = (max_read_request_bytes // word_bytes) * word_bytes
1454 splitter_resp = Wire(gearbox_mod.in_.type)
1455 gearbox = gearbox_mod(clk=ports.clk, rst=ports.rst, in_=splitter_resp)
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:
1465 "data": m.data.bitcast(element_type),
1466 "data_size": data_size_type(0),
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({
1476 "length": (r.length * UInt(64)
1477 (elem_stride_bytes)).as_uint(32),
1483 client_resp_channel = gearbox.out.transform(
1484 lambda m, client_type=client_type: client_type({
1486 "data": m.data.bitcast(client_type.data)
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),
1500 logical_req.type, demuxed_upstream_channel.type,
1501 max_chunk_bytes)(clk=ports.clk,
1504 resp_in=demuxed_upstream_channel)
1505 splitter_resp.assign(splitter.resp_out)
1506 tagged_client_req = splitter.req_out
1508 tagged_client_reqs.append(tagged_client_req)
1511 setattr(ports, HostmemReadProcessorImpl.reqPortMap[client],
1520 muxed_client_reqs = ChannelArbiter(tagged_client_reqs,
1524 upstream_req_channel.assign(muxed_client_reqs)
1525 HostmemReadProcessorImpl.reqPortMap.clear()
1527 return HostmemReadProcessorImpl
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.
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."""
1542 if output_bitwidth % 8 != 0:
1543 raise ValueError(
"Output bitwidth must be a multiple of 8.")
1545 if input_bitwidth % 8 != 0:
1546 input_pad_bits = 8 - (input_bitwidth % 8)
1547 input_padded_bitwidth = input_bitwidth + input_pad_bits
1550 max_burst_words = (max_burst_bytes //
1551 (output_bitwidth // 8))
if max_burst_bytes
else 0
1553 assert (max_burst_words & (max_burst_words - 1)) == 0, \
1554 "max_burst_bytes / (output_bitwidth // 8) must be a power of two"
1556 class TaggedWriteGearboxImpl(Module):
1561 (
"address", UInt(64)),
1562 (
"tag", esi.HostMem.TagType),
1563 (
"data", Bits(input_bitwidth)),
1565 out = OutputChannel(
1567 (
"address", UInt(64)),
1568 (
"tag", esi.HostMem.TagType),
1569 (
"data", Bits(output_bitwidth)),
1570 (
"valid_bytes", Bits(8)),
1574 num_chunks = ceil(input_padded_bitwidth / output_bitwidth)
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
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)
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)
1609 num_chunks = TaggedWriteGearboxImpl.num_chunks
1610 num_chunks_idx_bitwidth = clog2(num_chunks)
1611 if input_padded_bitwidth % output_bitwidth == 0:
1614 padding_numbits = output_bitwidth - (input_padded_bitwidth %
1616 client_data_padded = BitsSignal.concat(
1617 [Bits(padding_numbits)(0), client_data])
1619 client_data_padded[i * output_bitwidth:(i + 1) * output_bitwidth]
1620 for i
in range(num_chunks)
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)
1626 increment = Wire(Bits(1))
1627 clear = Wire(Bits(1))
1628 counter = Counter(num_chunks_idx_bitwidth)(clk=ports.clk,
1630 increment=increment,
1632 upstream_data_bits = chunk_regs[counter.out]
1633 upstream_valid = ControlReg(ports.clk, ports.rst, [client_xact],
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()
1645 tag_reg = client_tag_and_data.tag.reg(ports.clk,
1648 addr_reg = client_tag_and_data.address.reg(ports.clk,
1651 address = (addr_reg + counter_bytes).as_uint(64)
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:
1663 burst_shift = clog2(max_burst_words)
1664 burst_end = counter.out.as_bits()[:burst_shift].and_reduce()
1665 last = elem_end | burst_end
1669 upstream_channel, upstrm_ready_sig = TaggedWriteGearboxImpl.out.type.wrap(
1673 "data": upstream_data_bits,
1674 "valid_bytes": valid_bytes,
1677 upstream_ready.assign(upstrm_ready_sig)
1678 ports.out = upstream_channel
1680 return TaggedWriteGearboxImpl
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."""
1689 raise ValueError(
"N must be >= 1")
1691 class EmitEveryNImpl(Module):
1694 in_ = InputChannel(message_type)
1695 out = OutputChannel(message_type)
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
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
1710 counter_width = clog2(N)
1711 counter_clear = Wire(Bits(1))
1712 counter = Counter(counter_width)(clk=ports.clk,
1715 clear=counter_clear)
1718 last_msg = in_data.reg(ports.clk, ports.rst, ce=xact, name=
"last_msg")
1720 hit_last = (counter.out == UInt(counter_width)(N - 1)) & xact
1721 counter_clear.assign(hit_last)
1723 emit_accepted = Wire(Bits(1))
1724 out_valid = ControlReg(ports.clk, ports.rst, [hit_last], [emit_accepted])
1726 out_chan, out_ready = EmitEveryNImpl.out.type.wrap(last_msg, out_valid)
1728 ready_for_in.assign(~(out_valid & ~out_ready))
1729 emit_accepted.assign(out_valid & out_ready)
1731 ports.out = out_chan
1733 return EmitEveryNImpl
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
1747 Generate this module dynamically to allow for multiple write clients of
1748 multiple types to be directly accomodated."""
1750 class HostMemWriteProcessorImpl(Module):
1756 reqPortMap: Dict[esi._OutputBundleSetter, str] = {}
1758 name =
"client_" + req.client_name_str
1759 locals()[name] = Output(req.type)
1760 reqPortMap[req] = name
1763 upstream = Output(hostmem_module.write.type)
1773 size_width = clog2(write_width // 8)
1777 req, _ = Channel(hostmem_module.UpstreamWriteReq).
wrap(
1785 write_bundle, _ = hostmem_module.write.type.pack(req=req)
1786 ports.upstream = write_bundle
1789 assert len(reqs) <= 256,
"More than 256 write clients not supported."
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"]
1797 demuxed_acks = esi.TaggedDemux(len(reqs), upstream_ack_tag.type)(
1798 clk=ports.clk, rst=ports.rst, in_=upstream_ack_tag)
1803 write_channels: List[ChannelSignal] = []
1804 for idx, req
in enumerate(reqs):
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)
1810 if isinstance(client_type, Window):
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
1824 elem_stride = (element_bits + 7) // 8
1827 max_write_payload_bytes)
1828 gearbox_in_type = gearbox_mod.in_.type.inner_type
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,
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(
1846 "tag": frame[
"tag"],
1847 "address": elem_addr,
1848 "data": frame[
"data"][0].bitcast(gearbox_in_type.data),
1850 ready_for_frame.assign(gearbox_in_ready)
1851 gearbox = gearbox_mod(clk=ports.clk,
1853 in_=gearbox_in_chan)
1856 write_req_bundle_type = esi.HostMem.write_req_bundle_type(
1858 bundle_sig, sfroms = write_req_bundle_type.pack(ackTag=input_flit_ack)
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({
1865 "address": m.address,
1866 "data": m.data.bitcast(git.data)
1868 gearbox = gearbox_mod(clk=ports.clk,
1870 in_=bitcast_client_req)
1872 write_channels.append(
1873 gearbox.out.transform(
1874 lambda m, idx=idx: hostmem_module.UpstreamWriteReq({
1881 "data_size": (m.valid_bytes.as_uint() - UInt(8)
1882 (1)).as_bits()[:size_width],
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)
1894 setattr(ports, HostMemWriteProcessorImpl.reqPortMap[req], bundle_sig)
1902 muxed_write_channel = ChannelArbiter(write_channels,
1906 upstream_req_channel.assign(muxed_write_channel)
1908 return HostMemWriteProcessorImpl
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']:
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."""
1926 UpstreamReadReq = StructType([
1927 (
"address", UInt(64)),
1928 (
"length", UInt(32)),
1933 BundledChannel(
"req", ChannelDirection.TO, UpstreamReadReq),
1935 "resp", ChannelDirection.FROM,
1937 (
"tag", esi.HostMem.TagType),
1938 (
"data", Bits(read_width)),
1943 if write_width % 8 != 0:
1944 raise ValueError(
"Write width must be a multiple of 8.")
1945 UpstreamWriteReq = StructType([
1946 (
"address", UInt(64)),
1948 (
"data", Bits(write_width)),
1949 (
"data_size", Bits(clog2(write_width // 8))),
1954 BundledChannel(
"req", ChannelDirection.TO, UpstreamWriteReq),
1955 BundledChannel(
"ackTag", ChannelDirection.FROM, UInt(8)),
1959 def generate(ports, bundles: esi._ServiceGeneratorBundles):
1964 req
for req
in bundles.to_client_reqs
1965 if req.port
in (
'read',
'read_list')
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]))
1976 req
for req
in bundles.to_client_reqs
if req.port ==
'write'
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]))
1986 return ChannelHostMemImpl
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.
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
2088 class ChannelEngineService(esi.ServiceImplementation):
2089 """Service implementation which services the clients via a per-channel DMA
2096 def build(ports, bundles: esi._ServiceGeneratorBundles):
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}"
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)
2114 engine_client_type = bc.channel.inner_type
2115 is_void = (engine_client_type.bitwidth == 0)
2117 engine_client_type = Bits(8)
2118 if bc.direction == ChannelDirection.FROM:
2119 engine_mod = bundle_to_host_gen(engine_client_type)
2121 engine_mod = bundle_from_host_gen(engine_client_type)
2126 eng_details: Dict[str, object] = {
"engine_inst": eng_appid}
2127 if input_channel
is not None:
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(
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(
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: {}})
2156 for bundle
in bundles.to_client_reqs:
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
2169 bundle_type = bundle.type
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
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
2181 client_bundle_sig, froms = bundle_type.pack(**to_channels)
2182 bundle.assign(client_bundle_sig)
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,
2190 return ChannelEngineService