CIRCT 24.0.0git
Loading...
Searching...
No Matches
esitester.py
Go to the documentation of this file.
1# ===- esitester.py - accelerator for testing ESI functionality -----------===//
2#
3# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4# See https://llvm.org/LICENSE.txt for license information.
5# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6#
7# ===----------------------------------------------------------------------===//
8#
9# This design is used for testing ESI functionality. It is distribed in the
10# esiaccel package for BSP developers to exercise new BSPs, boards, and
11# features. It is compatible with the distributed esitester application.
12#
13# Importantly, it is not a standalone application -- merely a collection of
14# test modules and top level. The user must write a main function which builds
15# the system using this module as a library.
16#
17# ===----------------------------------------------------------------------===//
18
19import sys
20from typing import Type
21
22import pycde.esi as esi
23from pycde import Clock, Module, Reset, System, generator, modparams
24from esiaccel.bsp import get_bsp
25from esiaccel.components.mmio import MmioRegistry, mmio_write_we
26from pycde.common import AppID, Constant, Input, InputChannel, Output, OutputChannel
27from pycde.constructs import ControlReg, Counter, Mux, NamedWire, Reg, Wire
28from pycde.module import Metadata
29from pycde.signals import BitsSignal
30from pycde.testing import print_info
31from pycde.types import Array, Bits, Channel, ChannelSignaling, UInt
32
33# Fixed 64-bit seed for the hostmem burst data pattern. WriteMem fills every
34# byte of each element with (seed ^ index) tiled across bytes and XORed with a
35# distinct per-position mask, and ReadMem folds every received byte into a
36# checksum, so the host tests verify the full-width data landed at / was fetched
37# from the right bytes -- independent of the backend's engine word width.
38_ESITESTER_SEQ_SEED = 0x5A5A5A5A5A5A5A5A
39
40# ---------------------------------------------------------------------------
41# Reusable building blocks for timing-friendly MMIO-controlled test modules.
42#
43# Both helpers keep the BSP's MMIO mux and the consumer-facing channel inside
44# this user module isolated from each other's combinational paths:
45#
46# * `MmioRegistry` drives the MMIO bundle's `cmd_ready` and response `valid`
47# from a single `resp_pending` ControlReg. The BSP MMIO mux therefore
48# only sees FF outputs, and the user module's internal write-enable
49# strobes are 1-cycle-late registered pulses gated by `write_cmd_xact_r`.
50#
51# * `IterationGate` exposes a counter+limit "run for N iterations" widget
52# whose `active` output is a ControlReg (FF) and whose only consumer-
53# driven input (`iter_xact`) terminates at the counter's clock enable.
54# The consumer's channel-ready signal never re-emerges combinationally
55# from this module. It also always reports `cycles` telemetry for the
56# active window.
57#
58# Use both together to build a test that drives a single channel with N
59# iterations under MMIO control without exposing the BSP arbitration mux to
60# wide internal combinational logic.
61# ---------------------------------------------------------------------------
62
63
64@modparams
65def IterationGate(count_width: int):
66 """Run an internal counter for `limit` iterations gated by `iter_xact`,
67 and always report cycle telemetry for the active window.
68
69 `start_pulse` asserts `active` (and clears the counters) for one cycle.
70 `iter_xact` is the per-iteration handshake strobe; it feeds only a
71 Counter clock enable. `active` clears the cycle after `iter_count`
72 reaches `limit`.
73
74 The consumer's channel-ready signal can flow into `iter_xact` without
75 re-emerging combinationally through any output: only Counters (FFs)
76 consume it.
77
78 Telemetry reported under this instance's AppID:
79 cycles : ui64
80 Cycles `active` was asserted between `start_pulse` and
81 `count_reached`; latched on `count_reached` so the host always
82 reads the final value of the most recent run.
83
84 Ports:
85 start_pulse : Input Bits(1)
86 limit : Input UInt(count_width)
87 iter_xact : Input Bits(1)
88 active : Output Bits(1) -- ControlReg, FF output
89 count_reached : Output Bits(1) -- iter_count == limit
90 iter_count : Output UInt(count_width)
91 iters_left : Output UInt(count_width)
92 """
93
94 class IterationGate(Module):
95 clk = Clock()
96 rst = Reset()
97
98 start_pulse = Input(Bits(1))
99 limit = Input(UInt(count_width))
100 iter_xact = Input(Bits(1))
101
102 active = Output(Bits(1))
103 count_reached = Output(Bits(1))
104 iter_count = Output(UInt(count_width))
105 iters_left = Output(UInt(count_width))
106
107 @generator
108 def construct(ports):
109 clk = ports.clk
110 rst = ports.rst
111
112 # `count_reached_sig` fires combinationally on the last
113 # `iter_xact` (when `iter_count` is about to become
114 # `limit`), so `active` drops the cycle the consumer would
115 # have issued the (limit+1)th transaction. Without this,
116 # there is a 2-cycle race between `iter_count` reaching
117 # `limit` and the ControlReg dropping `active`, during
118 # which the consumer issues one extra transaction -- and
119 # for `limit=1` the host-visible counters jump straight
120 # from 0 to 2, so any host poll for "== 1" never catches.
121 # `count_reached` is combinational on `iter_xact` (which is
122 # consumer-driven), but it only feeds the ControlReg reset
123 # (a flop) and telemetry, so the consumer's ready signal
124 # does not re-emerge combinationally on `active`.
125 count_reached_wire = Wire(Bits(1))
126 active_r = ControlReg(
127 clk=clk,
128 rst=rst,
129 asserts=[ports.start_pulse],
130 resets=[count_reached_wire],
131 name="active_r",
132 )
133 counter = Counter(count_width)(
134 clk=clk,
135 rst=rst,
136 clear=ports.start_pulse,
137 increment=ports.iter_xact,
138 instance_name="iter_counter",
139 )
140 last_iter = (counter.out.as_uint() == (
141 ports.limit - UInt(count_width)(1)).as_uint(count_width)).as_bits(1)
142 count_reached_sig = ports.iter_xact & last_iter
143 count_reached_wire.assign(count_reached_sig)
144
145 ports.active = active_r
146 ports.count_reached = count_reached_sig
147 ports.iter_count = counter.out.as_uint()
148 # Elements remaining in the *active* window: 0 when no run is in flight
149 # (after `limit` is set but before `start_pulse`, or after completion),
150 # else `limit - iter_count`.
151 remaining = (ports.limit - counter.out.as_uint()).as_uint(count_width)
152 ports.iters_left = Mux(active_r, UInt(count_width)(0), remaining)
153
154 cycles_cnt = Counter(64)(
155 clk=clk,
156 rst=rst,
157 clear=ports.start_pulse,
158 increment=active_r,
159 instance_name="cycle_counter",
160 )
161 final_cycles = Reg(
162 UInt(64),
163 clk=clk,
164 rst=rst,
165 rst_value=0,
166 ce=count_reached_sig,
167 name="final_cycles",
168 )
169 final_cycles.assign(cycles_cnt.out.as_uint())
170 esi.Telemetry.report_signal(clk, rst, AppID("cycles"), final_cycles)
171
172 return IterationGate
173
174
175class CallbackTest(Module):
176 """Call a function on the host when an MMIO write is received at offset
177 0x10."""
178
179 clk = Clock()
180 rst = Reset()
181
182 @generator
183 def construct(ports):
184 clk = ports.clk
185 rst = ports.rst
186
187 mmio_bundle = esi.MMIO.read_write(appid=AppID("cmd"))
188 data_resp_chan = Wire(Channel(Bits(64)))
189 mmio_cmd_chan = mmio_bundle.unpack(data=data_resp_chan)["cmd"]
190 cb_trigger, mmio_cmd_chan_fork = mmio_cmd_chan.fork(clk=clk, rst=rst)
191
192 data_resp_chan.assign(
193 mmio_cmd_chan_fork.transform(lambda cmd: Bits(64)(cmd.data)))
194
195 cb_trigger_ready = Wire(Bits(1))
196 cb_trigger_cmd, cb_trigger_valid = cb_trigger.unwrap(cb_trigger_ready)
197 trigger = cb_trigger_valid & (cb_trigger_cmd.offset == UInt(32)(0x10))
198 data_reg = cb_trigger_cmd.data.reg(clk, rst, ce=trigger)
199 cb_chan, cb_trigger_ready_sig = Channel(Bits(64)).wrap(
200 data_reg, trigger.reg(clk, rst))
201 cb_trigger_ready.assign(cb_trigger_ready_sig)
202 esi.CallService.call(AppID("cb"), cb_chan, Bits(0))
203
204
205class LoopbackInOutAdd(Module):
206 """Exposes a function which adds the 'add_amt' constant to the argument."""
207
208 clk = Clock()
209 rst = Reset()
210
211 add_amt = Constant(UInt(16), 11)
212
213 @generator
214 def construct(ports):
215 loopback = Wire(Channel(UInt(16), signaling=ChannelSignaling.FIFO))
216 args = esi.FuncService.get_call_chans(AppID("add"),
217 arg_type=UInt(24),
218 result=loopback)
219
220 ready = Wire(Bits(1))
221 data, valid = args.unwrap(ready)
222 plus7 = data + LoopbackInOutAdd.add_amt.value
223 data_chan, data_ready = Channel(UInt(16), ChannelSignaling.ValidReady).wrap(
224 plus7.as_uint(16), valid)
225 data_chan_buffered = data_chan.buffer(ports.clk, ports.rst, 1,
226 ChannelSignaling.FIFO)
227 ready.assign(data_ready)
228 loopback.assign(data_chan_buffered)
229
230
231@modparams
232def StreamingAdder(numItems: int):
233 """Creates a StreamingAdder module parameterized by the number of items per
234 window frame. The module exposes a function which has an argument of struct
235 {add_amt, list<uint32>}. It then adds add_amt to each element of the list in
236 parallel (numItems at a time) and returns the resulting list.
237 """
238
239 class StreamingAdder(Module):
240 clk = Clock()
241 rst = Reset()
242
243 @generator
244 def construct(ports):
245 from pycde.types import StructType, List, Window
246
247 # Define the argument type: struct { add_amt: UInt(32), list: List<UInt(32)> }
248 arg_struct_type = StructType([("add_amt", UInt(32)),
249 ("input", List(UInt(32)))])
250
251 # Create a windowed version with numItems parallel elements
252 arg_window_type = Window(
253 "arg_window", arg_struct_type,
254 [Window.Frame(None, ["add_amt", ("input", numItems)])])
255
256 # Result is also a List with numItems parallel elements
257 result_struct_type = StructType([("data", List(UInt(32)))])
258 result_window_type = Window("result_window", result_struct_type,
259 [Window.Frame(None, [("data", numItems)])])
260
261 result_chan = Wire(Channel(result_window_type))
262 args = esi.FuncService.get_call_chans(AppID("streaming_add"),
263 arg_type=arg_window_type,
264 result=result_chan)
265
266 # Unwrap the argument channel
267 ready = Wire(Bits(1))
268 arg_data, arg_valid = args.unwrap(ready)
269
270 # Unwrap the window to get the lowered struct
271 # Lowered type: struct { add_amt, input: array[numItems], input_size, last }
272 arg_unwrapped = arg_data.unwrap()
273
274 # Extract add_amt and input array from the struct
275 add_amt = arg_unwrapped["add_amt"]
276 input_arr = arg_unwrapped["input"]
277
278 # Perform all additions in parallel
279 result_arr = [
280 (add_amt + input_arr[i]).as_uint(32) for i in range(numItems)
281 ]
282
283 # Build the result lowered type
284 # Lowered type: struct { data: array[numItems], data_size, last }
285 lowered_val = result_window_type.lowered_type({
286 "data": result_arr,
287 "data_size": arg_unwrapped["input_size"],
288 "last": arg_unwrapped["last"]
289 })
290
291 result_window = result_window_type.wrap(lowered_val)
292
293 # Wrap the result into a channel
294 result_chan_internal, result_ready = Channel(result_window_type).wrap(
295 result_window, arg_valid)
296 ready.assign(result_ready)
297 result_chan.assign(result_chan_internal)
298
299 return StreamingAdder
300
301
302class CoordTranslator(Module):
303 """Exposes a function which takes a struct of {x_translation, y_translation,
304 coords: list<struct{x, y}>} and adds the translation to each coordinate,
305 returning the translated list of coordinates.
306 """
307
308 clk = Clock()
309 rst = Reset()
310
311 @generator
312 def construct(ports):
313 from pycde.types import StructType, List, Window
314
315 # Define the coordinate type: struct { x: UInt(32), y: UInt(32) }
316 coord_type = StructType([("x", UInt(32)), ("y", UInt(32))])
317
318 # Define the argument type: struct { x_translation, y_translation, coords }
319 arg_struct_type = StructType([("x_translation", UInt(32)),
320 ("y_translation", UInt(32)),
321 ("coords", List(coord_type))])
322
323 # Create a windowed version of the argument struct for streaming
324 arg_window_type = Window.default_of(arg_struct_type)
325
326 # Result is also a List of coordinates
327 result_type = List(coord_type)
328 result_window_type = Window.default_of(result_type)
329
330 result_chan = Wire(Channel(result_window_type))
331 args = esi.FuncService.get_call_chans(AppID("translate_coords"),
332 arg_type=arg_window_type,
333 result=result_chan)
334
335 # Unwrap the argument channel
336 ready = Wire(Bits(1))
337 arg_data, arg_valid = args.unwrap(ready)
338
339 # Unwrap the window to get the struct/union
340 arg_unwrapped = arg_data.unwrap()
341
342 # Extract translations and coordinates from the struct
343 x_translation = arg_unwrapped["x_translation"]
344 y_translation = arg_unwrapped["y_translation"]
345 input_coord = arg_unwrapped["coords"]
346
347 # Add translations to each coordinate
348 result_x = (x_translation + input_coord["x"]).as_uint(32)
349 result_y = (y_translation + input_coord["y"]).as_uint(32)
350
351 # Create the result coordinate struct
352 result_coord = coord_type({"x": result_x, "y": result_y})
353
354 result_window = result_window_type.wrap(
355 result_window_type.lowered_type({
356 "data": result_coord,
357 "last": arg_unwrapped.last
358 }))
359
360 # Wrap the result into a channel
361 result_chan_internal, result_ready = Channel(result_window_type).wrap(
362 result_window, arg_valid)
363 ready.assign(result_ready)
364 result_chan.assign(result_chan_internal)
365
366
368 """Like CoordTranslator, but uses the serial (bulk-transfer) list encoding.
369
370 Input wire format is a window with two frames:
371 - "header": {x_translation, y_translation, coords_count}
372 - "data": {coords[1]} (one coordinate per frame)
373
374 Output wire format is also a window with two frames:
375 - "header": {coords_count}
376 - "data": {coords[1]} (one coordinate per frame)
377
378 In bulk-transfer encoding, the sender may transmit multiple header/data
379 sequences to extend a list. A common pattern is to set coords_count=64 and
380 re-send a new header every 64 items; the final header has coords_count=0.
381 This module passes the header count through and translates each coordinate.
382 """
383
384 clk = Clock()
385 rst = Reset()
386
387 @generator
388 def construct(ports):
389 from pycde.types import List, StructType, Window
390
391 clk = ports.clk
392 rst = ports.rst
393
394 bulk_count_width = 16
395 items_per_frame = 1
396
397 coord_type = StructType([("x", Bits(32)), ("y", Bits(32))])
398
399 # ----- Input window type (serial/bulk transfer) -----
400 arg_struct_type = StructType([
401 ("x_translation", Bits(32)),
402 ("y_translation", Bits(32)),
403 ("coords", List(coord_type)),
404 ])
405 arg_window_type = Window(
406 "serial_coord_args",
407 arg_struct_type,
408 [
409 Window.Frame(
410 "header",
411 [
412 "x_translation",
413 "y_translation",
414 ("coords", 0, bulk_count_width),
415 ],
416 ),
417 Window.Frame(
418 "data",
419 [("coords", items_per_frame, 0)],
420 ),
421 ],
422 )
423
424 # ----- Output window type (serial/bulk transfer) -----
425 result_struct_type = StructType([("coords", List(coord_type))])
426 result_window_type = Window(
427 "serial_coord_result",
428 result_struct_type,
429 [
430 Window.Frame(
431 "header",
432 [("coords", 0, bulk_count_width)],
433 ),
434 Window.Frame(
435 "data",
436 [("coords", items_per_frame, 0)],
437 ),
438 ],
439 )
440
441 result_chan = Wire(Channel(result_window_type))
442 args = esi.FuncService.get_call_chans(
443 AppID("translate_coords_serial"),
444 arg_type=arg_window_type,
445 result=result_chan,
446 )
447
448 # Unwrap the argument channel.
449 in_ready = Wire(Bits(1))
450 in_window, in_valid = args.unwrap(in_ready)
451 in_union = in_window.unwrap()
452
453 hdr_frame = in_union["header"]
454 data_frame = in_union["data"]
455
456 hdr_x = hdr_frame["x_translation"].as_uint(32)
457 hdr_y = hdr_frame["y_translation"].as_uint(32)
458 hdr_count_bits = hdr_frame["coords_count"]
459 hdr_count = hdr_count_bits.as_uint(bulk_count_width)
460
461 out_hdr_struct_ty = result_window_type.lowered_type.header
462 out_data_struct_ty = result_window_type.lowered_type.data
463
464 # Output channel (built below) drives readiness/backpressure.
465 out_ready_wire = Wire(Bits(1))
466 handshake = in_valid & out_ready_wire
467
468 # Track which frame we're currently expecting.
469 in_is_header = Reg(
470 Bits(1),
471 clk=clk,
472 rst=rst,
473 rst_value=1,
474 ce=handshake,
475 name="in_is_header",
476 )
477 # Only log the frame count when the handshake is for a header frame.
478 hdr_handshake = handshake & in_is_header
479 hdr_handshake.when_true(
480 lambda: print_info("Received frame count=%d", hdr_count_bits))
481
482 # Latch the most recent header count for re-use when emitting the output
483 # header (do not rely on union extracts during data frames).
484 hdr_is_zero = hdr_count == UInt(bulk_count_width)(0)
485 footer_handshake = hdr_handshake & hdr_is_zero
486 start_handshake = hdr_handshake & ~hdr_is_zero
487 message_active = ControlReg(
488 clk,
489 rst,
490 asserts=[start_handshake],
491 resets=[footer_handshake],
492 name="message_active",
493 )
494 count_reg = Reg(
495 UInt(bulk_count_width),
496 clk=clk,
497 rst=rst,
498 rst_value=0,
499 ce=hdr_handshake,
500 name="coords_count",
501 )
502 count_reg.assign(hdr_count)
503
504 data_handshake = handshake & ~in_is_header
505 data_count = Counter(bulk_count_width)(
506 clk=clk,
507 rst=rst,
508 clear=hdr_handshake,
509 increment=data_handshake,
510 instance_name="data_count",
511 ).out
512
513 # Latch translations only on the first header of a message.
514 x_translation_reg = Reg(
515 UInt(32),
516 clk=clk,
517 rst=rst,
518 rst_value=0,
519 ce=start_handshake & ~message_active,
520 name="x_translation",
521 )
522 y_translation_reg = Reg(
523 UInt(32),
524 clk=clk,
525 rst=rst,
526 rst_value=0,
527 ce=start_handshake & ~message_active,
528 name="y_translation",
529 )
530 x_translation_reg.assign(hdr_x)
531 y_translation_reg.assign(hdr_y)
532
533 # Next-state logic for header/data tracking.
534 count_minus_one = (count_reg -
535 UInt(bulk_count_width)(1)).as_uint(bulk_count_width)
536 data_last = data_count == count_minus_one
537 next_is_header = Mux(in_is_header, data_last, hdr_is_zero)
538 in_is_header.assign(next_is_header)
539
540 # Build output frames.
541 out_hdr_struct = out_hdr_struct_ty(
542 {"coords_count": hdr_count.as_bits(bulk_count_width)})
543
544 in_coord = data_frame["coords"][0]
545 in_x = in_coord["x"].as_uint(32)
546 in_y = in_coord["y"].as_uint(32)
547 translated_x = (x_translation_reg + in_x).as_uint(32)
548 translated_y = (y_translation_reg + in_y).as_uint(32)
549 out_coord = coord_type({
550 "x": translated_x.as_bits(32),
551 "y": translated_y.as_bits(32),
552 })
553 out_data_struct = out_data_struct_ty({"coords": [out_coord]})
554
555 out_union_hdr = result_window_type.lowered_type(("header", out_hdr_struct))
556 out_union_data = result_window_type.lowered_type(("data", out_data_struct))
557 out_union = Mux(in_is_header, out_union_data, out_union_hdr)
558 out_window = result_window_type.wrap(out_union)
559
560 out_chan, out_ready = Channel(result_window_type).wrap(out_window, in_valid)
561 out_ready_wire.assign(out_ready)
562
563 in_ready.assign(out_ready)
564 result_chan.assign(out_chan)
565
566
568 """Like CoordTranslator, but exposes the function with the serial
569 (bulk-transfer) list encoding on both the argument and result. Internally,
570 the serial input is converted to the parallel one-item-per-message form via
571 `ListWindowToParallel`, the per-coordinate translation is applied, and the
572 parallel result is converted back to the serial wire form via
573 `ListWindowToSerial`.
574
575 This exercises the automatic serial<->parallel conversion modules instead of
576 building the frame state machine by hand (as `SerialCoordTranslator` does).
577 """
578
579 clk = Clock()
580 rst = Reset()
581
582 @generator
583 def construct(ports):
584 from pycde.types import StructType, List, Window
585 from pycde.esi import ListWindowToParallel, ListWindowToSerial
586
587 bulk_count_width = 16
588 items_per_frame = 1
589 # Intentionally tiny FIFO so the host's coord lists (which can be much
590 # larger than this) get split across many bulk transfers, exercising the
591 # multi-burst code paths in `ListWindowToSerial` (drain-on-full bursts
592 # interleaved with the producer, plus the count==0 terminator).
593 fifo_depth = 4
594
595 # ---- Externally-visible (serial) function arg/result types. ----
596 # NOTE: use Bits for coord/translation fields. The window lowering for
597 # bulk-transfer encoding currently strips signedness from union variant
598 # fields, which causes type mismatches when the underlying struct uses
599 # UInt; SerialCoordTranslator hits the same constraint.
600 coord_type = StructType([("x", Bits(32)), ("y", Bits(32))])
601
602 arg_struct_type = StructType([("x_translation", Bits(32)),
603 ("y_translation", Bits(32)),
604 ("coords", List(coord_type))])
605 arg_window_type = Window.serial_of(arg_struct_type, bulk_count_width,
606 items_per_frame)
607
608 result_type = List(coord_type)
609 result_window_type = Window.serial_of(result_type, bulk_count_width,
610 items_per_frame)
611
612 # Result channel back to FuncService is the serial output of the
613 # parallel->serial converter (assigned at the end).
614 result_chan = Wire(Channel(result_window_type))
615 args = esi.FuncService.get_call_chans(AppID("translate_coords_auto_serial"),
616 arg_type=arg_window_type,
617 result=result_chan)
618
619 # ---- Convert the serial argument stream into a parallel one. ----
620 s2p = ListWindowToParallel(arg_window_type)(clk=ports.clk,
621 rst=ports.rst,
622 serial_in=args)
623 parallel_arg = s2p.parallel_out
624
625 # ---- Apply the per-coordinate translation. ----
626 par_ready = Wire(Bits(1))
627 par_window, par_valid = parallel_arg.unwrap(par_ready)
628 par_struct = par_window.unwrap()
629
630 x_translation = par_struct["x_translation"].as_uint(32)
631 y_translation = par_struct["y_translation"].as_uint(32)
632 input_coord = par_struct["coords"]
633 last_bit = par_struct["last"]
634
635 result_x = (x_translation +
636 input_coord["x"].as_uint(32)).as_uint(32).as_bits(32)
637 result_y = (y_translation +
638 input_coord["y"].as_uint(32)).as_uint(32).as_bits(32)
639 result_coord = coord_type({"x": result_x, "y": result_y})
640
641 parallel_result_window_type = Window.default_of(result_type)
642 parallel_result_struct = parallel_result_window_type.lowered_type({
643 "data": result_coord,
644 "last": last_bit,
645 })
646 parallel_result_window = parallel_result_window_type.wrap(
647 parallel_result_struct)
648
649 parallel_result_chan, parallel_result_ready = Channel(
650 parallel_result_window_type).wrap(parallel_result_window, par_valid)
651 par_ready.assign(parallel_result_ready)
652
653 # ---- Convert the parallel result stream back into a serial one. ----
654 p2s = ListWindowToSerial(parallel_result_window_type, bulk_count_width,
655 items_per_frame,
656 fifo_depth)(clk=ports.clk,
657 rst=ports.rst,
658 parallel_in=parallel_result_chan)
659 result_chan.assign(p2s.serial_out)
660
661
662@modparams
663def MMIOAdd(add_amt: int) -> Type[Module]:
664
665 class MMIOAdd(Module):
666 """Exposes an MMIO address space wherein MMIO reads return the <address
667 offset into its space> + add_amt."""
668
669 metadata = Metadata(
670 name="MMIOAdd",
671 misc={"add_amt": add_amt},
672 )
673
674 add_amt_const = Constant(UInt(32), add_amt)
675
676 @generator
677 def build(ports):
678 mmio_read_bundle = esi.MMIO.read(appid=AppID("mmio_client", add_amt))
679
680 address_chan_wire = Wire(Channel(UInt(32)))
681 address, address_valid = address_chan_wire.unwrap(1)
682 response_data = (address.as_uint() + add_amt).as_bits(64)
683 response_chan, response_ready = Channel(Bits(64)).wrap(
684 response_data, address_valid)
685
686 address_chan = mmio_read_bundle.unpack(data=response_chan)["offset"]
687 address_chan_wire.assign(address_chan)
688
689 return MMIOAdd
690
691
692@modparams
693def BurstCommand(width: int):
694 """MMIO-controlled single-burst command surface. Replaces AddressCommand's
695 per-flit address stream: exposes one {address, tag, length} burst request
696 per command -- for a ``read_list`` or a windowed (list) write -- and tracks
697 completion.
698
699 MMIO register map (8-byte stride):
700 0x00 Read : flits_left (elements remaining in the active command).
701 0x08 Write: base address.
702 0x10 Write: list length (flits).
703 0x18 Write: start.
704 """
705
706 class BurstCommand(Module):
707 clk = Clock()
708 rst = Reset()
709
710 # Remaining elements (for MMIO read-back).
711 flits_left = Output(UInt(64))
712 # Single {address, tag, length} burst request, held valid until
713 # accepted.
714 burst_req = OutputChannel(esi.HostMem.read_req_burst_type())
715 # One Bits(0) completion token per received element / write ack.
716 hostmem_cmd_done = InputChannel(Bits(0))
717
718 @generator
719 def construct(ports):
720 clk = ports.clk
721 rst = ports.rst
722
723 # Register map (RO < RW < WO, 8-byte stride):
724 # 0x00 flits_left (RO, client-updated read-back)
725 # 0x08 start_addr (RW, host-written base address)
726 # 0x10 flits_total (RW, host-written list length)
727 # 0x18 start (WO, host write triggers the operation)
728 flits_left_data = Wire(Bits(64))
729 mmio = MmioRegistry(num_ro=1, num_rw=2, num_wo=1)(
730 clk=clk,
731 rst=rst,
732 read_reg_ce=Array(Bits(1), 3)([Bits(1)(1),
733 Bits(1)(0),
734 Bits(1)(0)]),
735 read_reg_data=Array(Bits(64),
736 3)([flits_left_data,
737 Bits(64)(0),
738 Bits(64)(0)]),
739 instance_name="mmio",
740 appid=AppID("mmio", width),
741 )
742
743 start_addr = mmio.read_reg_value[1].as_uint()
744 flits_total = mmio.read_reg_value[2].as_uint()
745 start_op_we = mmio_write_we(mmio, 0x18)
746
747 # Response side: count completed elements; auto-reports cycles.
748 _, done_valid = ports.hostmem_cmd_done.unwrap(Bits(1)(1))
749 resp_gate = IterationGate(64)(
750 clk=clk,
751 rst=rst,
752 start_pulse=start_op_we,
753 limit=flits_total,
754 iter_xact=done_valid,
755 instance_name="resp_gate",
756 appid=AppID("addrCmdResp"),
757 )
758 ports.flits_left = resp_gate.iters_left
759 # The RO flits_left register mirrors the live remaining count.
760 flits_left_data.assign(resp_gate.iters_left.as_bits(64))
761
762 # Single burst request, held valid from start until accepted.
763 burst_accepted = Wire(Bits(1))
764 burst_pending = ControlReg(
765 clk=clk,
766 rst=rst,
767 asserts=[start_op_we],
768 resets=[burst_accepted],
769 name="burst_pending",
770 )
771 burst_req_t = esi.HostMem.read_req_burst_type()
772 burst_chan, burst_ready = Channel(burst_req_t).wrap(
773 burst_req_t({
774 "address": start_addr,
775 "tag": UInt(8)(0),
776 "length": flits_total,
777 }),
778 burst_pending,
779 )
780 burst_accepted.assign((burst_pending & burst_ready).as_bits())
781 ports.burst_req = burst_chan
782
783 # Issue side: one issued command per accepted burst.
784 issue_cnt = Counter(64)(
785 clk=clk,
786 rst=rst,
787 clear=start_op_we,
788 increment=burst_accepted,
789 )
790
791 esi.Telemetry.report_signal(clk, rst, esi.AppID("addrCmdIssued"),
792 issue_cnt.out)
793 esi.Telemetry.report_signal(clk, rst, esi.AppID("addrCmdResponses"),
794 resp_gate.iter_count)
795
796 return BurstCommand
797
798
799@modparams
800def ReadMem(width: int):
801
802 class ReadMem(Module):
803 """Host memory burst (list) read test module.
804
805 Issues a single ``read_list`` burst of 'flits' elements starting at the
806 base address (both configured via MMIO) and receives the elements back
807 as a windowed list (num_items=1 -> one element per frame). The low 64
808 bits of the most recent element are exported as telemetry (lastReadLSB),
809 and every byte of every element is folded into a byte-position-sensitive
810 integrity checksum (readChecksum).
811
812 MMIO command interface (via BurstCommand):
813 0x00 Read : remaining element count (flits_left).
814 0x08 Write: base address for the read.
815 0x10 Write: number of list elements (flits) to read.
816 0x18 Write: start the operation.
817
818 Telemetry (AppID -> signal):
819 addrCmdIssued Count of burst commands issued (1 per command).
820 addrCmdResponses Count of list elements received.
821 lastReadLSB Low 64 bits of the most recent element.
822 """
823
824 clk = Clock()
825 rst = Reset()
826
827 width_bits = Constant(UInt(32), width)
828
829 @generator
830 def construct(ports):
831 clk = ports.clk
832 rst = ports.rst
833
834 done_wire = Wire(Channel(Bits(0)))
835 cmd = BurstCommand(width)(
836 clk=clk,
837 rst=rst,
838 hostmem_cmd_done=done_wire,
839 instance_name="burst_command",
840 )
841
842 # One read_list request per command; elements come back as a
843 # windowed list.
844 read_responses = esi.HostMem.read_list(
845 appid=AppID("host"),
846 req=cmd.burst_req,
847 element_type=Bits(width),
848 num_items=1,
849 )
850 # Each received element -> one completion token to BurstCommand.
851 done_wire.assign(read_responses.transform(lambda resp: Bits(0)(0)))
852 # Snoop each received element without consuming it.
853 read_resp_valid_snoop, read_resp_data = read_responses.snoop_xact()
854 read_elem = read_resp_data.unwrap()["data"][0]
855 read_elem_lsb = read_elem.as_uint(64)
856 last_read_lsb = Reg(
857 UInt(64),
858 clk=ports.clk,
859 rst=ports.rst,
860 rst_value=0,
861 ce=read_resp_valid_snoop,
862 name="last_read_lsb",
863 )
864 last_read_lsb.assign(read_elem_lsb)
865 esi.Telemetry.report_signal(
866 ports.clk,
867 ports.rst,
868 esi.AppID("lastReadLSB"),
869 last_read_lsb,
870 )
871 # Byte-position-sensitive integrity checksum over all `width` bits: fold
872 # each 64-bit chunk of the element into 64 bits with a per-chunk rotate
873 # (so word/byte misplacement doesn't cancel), then XOR across elements.
874 num_chunks = (width + 63) // 64
875 elem_fold = Bits(64)(0)
876 for c in range(num_chunks):
877 hi = min(64 * c + 64, width)
878 chunk = read_elem[64 * c:hi]
879 if hi - 64 * c < 64:
880 chunk = chunk.as_uint().as_uint(64).as_bits()
881 r = (8 * c) % 64
882 if r != 0:
883 chunk = BitsSignal.concat([chunk[0:64 - r], chunk[64 - r:64]])
884 elem_fold = elem_fold ^ chunk
885 read_checksum = Wire(UInt(64))
886 read_checksum.assign((read_checksum.as_bits() ^ elem_fold).as_uint().reg(
887 ports.clk,
888 ports.rst,
889 rst_value=0,
890 ce=read_resp_valid_snoop,
891 name="read_checksum",
892 ))
893 esi.Telemetry.report_signal(
894 ports.clk,
895 ports.rst,
896 esi.AppID("readChecksum"),
897 read_checksum,
898 )
899
900 return ReadMem
901
902
903@modparams
904def WriteMem(width: int) -> Type[Module]:
905
906 class WriteMem(Module):
907 """Host memory burst (list) write test module.
908
909 Issues a single windowed (list) write of 'flits' elements to sequential
910 addresses starting at the base address (both configured via MMIO). The
911 elements are streamed as a windowed list (num_items=1 -> one element per
912 frame, 'last' on the final); each element's payload is a byte-level
913 pattern derived from its frame index (see _ESITESTER_SEQ_SEED).
914
915 MMIO command interface (via BurstCommand):
916 0x00 Read : remaining element count (flits_left).
917 0x08 Write: base address for the write.
918 0x10 Write: number of list elements (flits) to write.
919 0x18 Write: start the operation.
920
921 Telemetry (AppID -> signal):
922 addrCmdIssued Count of burst commands issued (1 per command).
923 addrCmdResponses Count of write acks received.
924 addrCmdResp/cycles Active-window cycle count.
925 """
926
927 clk = Clock()
928 rst = Reset()
929
930 width_bits = Constant(UInt(32), width)
931
932 @generator
933 def construct(ports):
934 clk = ports.clk
935 rst = ports.rst
936
937 done_wire = Wire(Channel(Bits(0)))
938 cmd = BurstCommand(width)(
939 clk=clk,
940 rst=rst,
941 hostmem_cmd_done=done_wire,
942 instance_name="burst_command",
943 )
944
945 # Windowed (list) write: consume the single {base, tag, length}
946 # burst request and stream 'length' elements to sequential
947 # addresses (num_items=1 -> one element per frame).
948 write_win = esi.HostMem.write_window(Bits(width), 1)
949 lowered = write_win.lowered_type
950
951 streaming = Wire(Bits(1))
952 frame_xact = Wire(Bits(1))
953 burst_ready = (~streaming).as_bits()
954 burst, burst_valid = cmd.burst_req.unwrap(burst_ready)
955 burst_accept = (burst_valid & ~streaming).as_bits()
956 base_addr = burst.address.reg(
957 clk=clk,
958 rst=rst,
959 rst_value=0,
960 ce=burst_accept,
961 name="base_addr",
962 )
963 total = burst.length.reg(
964 clk=clk,
965 rst=rst,
966 rst_value=0,
967 ce=burst_accept,
968 name="total",
969 )
970
971 frame_counter = Counter(64)(
972 clk=clk,
973 rst=rst,
974 clear=burst_accept,
975 increment=frame_xact,
976 )
977 is_last = (frame_counter.out == (total -
978 UInt(64)(1)).as_uint(64)).as_bits(1)
979 last_accept = (frame_xact & is_last).as_bits()
980 streaming.assign(
981 ControlReg(
982 clk=clk,
983 rst=rst,
984 asserts=[burst_accept],
985 resets=[last_accept],
986 name="streaming",
987 ))
988
989 # Byte-level data pattern: tile (seed ^ frame index) across the element's
990 # bytes and XOR each byte with a distinct per-position mask so every byte
991 # is unique. A read that fetches the wrong bytes is then caught at byte
992 # granularity by the hostmembw data-integrity check.
993 seq64 = frame_counter.out.as_bits() ^ Bits(64)(_ESITESTER_SEQ_SEED)
994 num_bytes = (width + 7) // 8
995 elem_bytes = [
996 seq64[8 * (j % 8):8 * (j % 8) + 8] ^ Bits(8)((j * 0x9D) & 0xFF)
997 for j in range(num_bytes)
998 ]
999 element = BitsSignal.concat(list(reversed(elem_bytes)))[0:width]
1000 frame_val = lowered({
1001 "address": base_addr,
1002 "tag": UInt(8)(0),
1003 "data": [element],
1004 "data_size": Bits(0)(0),
1005 "last": is_last,
1006 })
1007 frame_chan, frame_ready = Channel(write_win).wrap(
1008 write_win.wrap(frame_val), streaming)
1009 frame_xact.assign((streaming & frame_ready).as_bits())
1010
1011 write_responses = esi.HostMem.write(
1012 appid=AppID("host"),
1013 req=frame_chan,
1014 )
1015 # Each write ack -> one completion token to BurstCommand.
1016 done_wire.assign(write_responses.transform(lambda resp: Bits(0)(0)))
1017
1018 return WriteMem
1019
1020
1021@modparams
1022def ToHostDMATest(width: int):
1023 """Construct a module that sends the write count over a channel to the host
1024 the specified number of times. Exercises any DMA engine."""
1025
1026 class ToHostDMATest(Module):
1027 """Transmit patterned values to the host a programmed number of times.
1028
1029 A write to MMIO offset 0x0 programs `write_count`. Each message carries
1030 a byte-level pattern derived from its per-command transfer index (see
1031 ``_ESITESTER_SEQ_SEED``). The index advances on a successful channel
1032 handshake and resets for every command. The payload's final byte is
1033 truncated when `width` is not a multiple of eight.
1034 """
1035
1036 clk = Clock()
1037 rst = Reset()
1038
1039 width_bits = Constant(UInt(32), width)
1040
1041 @generator
1042 def construct(ports):
1043 count_reached = Wire(Bits(1))
1044 count_valid = Wire(Bits(1))
1045 out_xact = Wire(Bits(1))
1046
1047 write_cntr_incr = ~count_reached & count_valid & out_xact
1048 write_counter = Counter(32)(
1049 clk=ports.clk,
1050 rst=ports.rst,
1051 clear=count_reached,
1052 increment=write_cntr_incr,
1053 )
1054 num_writes = write_counter.out
1055
1056 # Get the MMIO space for commands.
1057 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
1058 resp_ready_wire = Wire(Bits(1))
1059 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
1060 mmio_xact = cmd_valid & resp_ready_wire
1061 response_data = Bits(64)(0)
1062 response_chan, response_ready = Channel(response_data.type).wrap(
1063 response_data, cmd_valid)
1064 resp_ready_wire.assign(response_ready)
1065
1066 # write_count is the specified number of times to send the cycle count.
1067 write_count_ce = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0))
1068 write_count = cmd.data.as_uint().reg(clk=ports.clk,
1069 rst=ports.rst,
1070 rst_value=0,
1071 ce=write_count_ce)
1072 count_reached.assign(num_writes == write_count)
1073 count_valid.assign(
1074 ControlReg(
1075 clk=ports.clk,
1076 rst=ports.rst,
1077 asserts=[write_count_ce],
1078 resets=[count_reached],
1079 ))
1080
1081 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd"))
1082 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
1083 cmd_chan_wire.assign(mmio_rw_cmd_chan)
1084
1085 # Output one byte-level pattern per command transfer. This lets the host
1086 # verify the complete payload, including every byte of wide messages.
1087 sequence_counter = Counter(64)(
1088 clk=ports.clk,
1089 rst=ports.rst,
1090 clear=write_count_ce,
1091 increment=out_xact,
1092 )
1093 seq64 = sequence_counter.out.as_bits() ^ Bits(64)(_ESITESTER_SEQ_SEED)
1094 num_bytes = (width + 7) // 8
1095 payload_bytes = [
1096 seq64[8 * (j % 8):8 * (j % 8) + 8] ^ Bits(8)((j * 0x9D) & 0xFF)
1097 for j in range(num_bytes)
1098 ]
1099 payload = BitsSignal.concat(list(reversed(payload_bytes)))[0:width]
1100 out_channel, out_channel_ready = Channel(UInt(width)).wrap(
1101 payload.as_uint(width), count_valid)
1102 out_xact.assign(out_channel_ready & count_valid)
1103 esi.ChannelService.to_host(name=AppID("out"), chan=out_channel)
1104
1105 total_write_counter = Counter(64)(
1106 clk=ports.clk,
1107 rst=ports.rst,
1108 clear=Bits(1)(0),
1109 increment=write_cntr_incr,
1110 )
1111 esi.Telemetry.report_signal(
1112 ports.clk,
1113 ports.rst,
1114 esi.AppID("totalWrites"),
1115 total_write_counter.out,
1116 )
1117
1118 # Cycle telemetry: count cycles while sequence active.
1119 tohost_cycle_cnt = Counter(64)(
1120 clk=ports.clk,
1121 rst=ports.rst,
1122 clear=write_count_ce,
1123 increment=count_valid,
1124 instance_name="tohost_cycle_counter",
1125 )
1126 tohost_final_cycles = Reg(
1127 UInt(64),
1128 clk=ports.clk,
1129 rst=ports.rst,
1130 rst_value=0,
1131 ce=count_reached,
1132 name="tohost_cycles",
1133 )
1134 tohost_final_cycles.assign(tohost_cycle_cnt.out.as_uint())
1135 esi.Telemetry.report_signal(
1136 ports.clk,
1137 ports.rst,
1138 esi.AppID("toHostCycles"),
1139 tohost_final_cycles,
1140 )
1141
1142 return ToHostDMATest
1143
1144
1145@modparams
1146def FromHostDMATest(width: int):
1147 """Construct a module that receives the write count over a channel from the
1148 host the specified number of times. Exercises any DMA engine."""
1149
1150 class FromHostDMATest(Module):
1151 """Receive test data from the host a programmed number of times.
1152
1153 Functionality:
1154 A write to MMIO offset 0x0 programs 'read_count', the number of messages
1155 to accept from the host. The input channel (AppID "in") is marked ready
1156 while the number of received messages is less than 'read_count'. Each
1157 received width-bit payload is latched; the most recent value is exposed
1158 on MMIO reads.
1159
1160 Width:
1161 'width' is the payload bit width of each received message. The latched
1162 value is widened/truncated to 64 bits for MMIO read-back (lower 64 bits
1163 if width > 64).
1164
1165 MMIO command interface:
1166 0x0 Write: Set read_count (number of messages to receive). Clears the
1167 internal receive counter.
1168 0x0 Read: Returns the last received value (Bits(64), derived from the
1169 width-bit payload).
1170
1171 Telemetry:
1172 fromHostCycles (AppID "fromHostCycles"): Cycle count from read_count programming
1173 (start) through completion of the programmed receive sequence.
1174 fromHostChecksum (AppID "fromHostChecksum"): Byte-position-sensitive
1175 fold of all payloads accepted during the programmed receive sequence.
1176
1177 Notes:
1178 Completion is when received messages == programmed read_count; another
1179 write to 0x0 re-arms for a new sequence.
1180 """
1181
1182 clk = Clock()
1183 rst = Reset()
1184
1185 width_bits = Constant(UInt(32), width)
1186
1187 @generator
1188 def build(ports):
1189 last_read = Wire(UInt(width))
1190
1191 # Get the MMIO space for commands.
1192 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
1193 resp_ready_wire = Wire(Bits(1))
1194 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
1195 mmio_xact = cmd_valid & resp_ready_wire
1196 response_data = last_read.as_bits(64)
1197 response_chan, response_ready = Channel(response_data.type).wrap(
1198 response_data, cmd_valid)
1199 resp_ready_wire.assign(response_ready)
1200
1201 # read_count is the specified number of times to recieve data.
1202 read_count_ce = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0))
1203 read_count = cmd.data.as_uint().reg(clk=ports.clk,
1204 rst=ports.rst,
1205 rst_value=0,
1206 ce=read_count_ce)
1207 in_data_xact = NamedWire(Bits(1), "in_data_xact")
1208 read_counter = Counter(32)(
1209 clk=ports.clk,
1210 rst=ports.rst,
1211 clear=read_count_ce,
1212 increment=in_data_xact,
1213 )
1214
1215 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd"))
1216 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
1217 cmd_chan_wire.assign(mmio_rw_cmd_chan)
1218
1219 in_chan = esi.ChannelService.from_host(name=AppID("in"), type=UInt(width))
1220 in_ready = NamedWire(read_counter.out < read_count, "in_ready")
1221 in_data, in_valid = in_chan.unwrap(in_ready)
1222 NamedWire(in_data, "in_data")
1223 in_data_xact.assign(in_valid & in_ready)
1224
1225 last_read.assign(
1226 in_data.reg(
1227 clk=ports.clk,
1228 rst=ports.rst,
1229 ce=in_data_xact,
1230 name="last_read",
1231 ))
1232
1233 # Fold every received payload so the host can verify all transferred
1234 # bytes, rather than only the low 64 bits of the final payload.
1235 in_bits = in_data.as_bits()
1236 num_chunks = (width + 63) // 64
1237 item_fold = Bits(64)(0)
1238 for c in range(num_chunks):
1239 hi = min(64 * c + 64, width)
1240 chunk = in_bits[64 * c:hi]
1241 if hi - 64 * c < 64:
1242 chunk = chunk.as_uint().as_uint(64).as_bits()
1243 r = (8 * c) % 64
1244 if r != 0:
1245 chunk = BitsSignal.concat([chunk[0:64 - r], chunk[64 - r:64]])
1246 item_fold = item_fold ^ chunk
1247 from_host_checksum = Wire(UInt(64))
1248 checksum_next = Mux(
1249 read_count_ce,
1250 (from_host_checksum.as_bits() ^ item_fold).as_uint(),
1251 UInt(64)(0),
1252 )
1253 from_host_checksum.assign(
1254 checksum_next.reg(
1255 clk=ports.clk,
1256 rst=ports.rst,
1257 rst_value=0,
1258 ce=read_count_ce | in_data_xact,
1259 name="from_host_checksum",
1260 ))
1261 esi.Telemetry.report_signal(
1262 ports.clk,
1263 ports.rst,
1264 esi.AppID("fromHostChecksum"),
1265 from_host_checksum,
1266 )
1267
1268 # Cycle telemetry: detect completion and count active cycles.
1269 fromhost_count_reached = Wire(Bits(1))
1270 fromhost_count_reached.assign(read_counter.out == read_count)
1271 fromhost_cycle_valid = ControlReg(
1272 clk=ports.clk,
1273 rst=ports.rst,
1274 asserts=[read_count_ce],
1275 resets=[fromhost_count_reached],
1276 name="fromhost_cycle_active",
1277 )
1278 fromhost_cycle_cnt = Counter(64)(
1279 clk=ports.clk,
1280 rst=ports.rst,
1281 clear=read_count_ce,
1282 increment=fromhost_cycle_valid,
1283 instance_name="fromhost_cycle_counter",
1284 )
1285 fromhost_final_cycles = Reg(
1286 UInt(64),
1287 clk=ports.clk,
1288 rst=ports.rst,
1289 rst_value=0,
1290 ce=fromhost_count_reached,
1291 name="fromhost_cycles",
1292 )
1293 fromhost_final_cycles.assign(fromhost_cycle_cnt.out.as_uint())
1294 esi.Telemetry.report_signal(
1295 ports.clk,
1296 ports.rst,
1297 esi.AppID("fromHostCycles"),
1298 fromhost_final_cycles,
1299 )
1300
1301 return FromHostDMATest
1302
1303
1304# Factory returning the same (to_host, from_host) engine pair the cosim_dma
1305# BSP wires in by default. Used below to exercise the per-request engine
1306# override on `ChannelService` requests: the resolver imports this path,
1307# calls it, and the returned pair replaces the default pair for just that
1308# one request's channel. Kept module-scope so it is importable as
1309# 'esiaccel.esitester._one_item_buffers_pair' from a service-request
1310# `options={"engine": ...}` value.
1312 from .bsp.dma import OneItemBuffersToHost, OneItemBuffersFromHost
1313 return (OneItemBuffersToHost, OneItemBuffersFromHost)
1314
1315
1316class ChannelTest(Module):
1317 """Test the ChannelService with a to_host producer and a from_host loopback.
1318
1319 The 'producer' to_host port sends incrementing UInt(32) values. The number
1320 of values to send is specified via an MMIO write to offset 0x0. Reading MMIO
1321 returns the remaining count.
1322
1323 The 'loopback_in'/'loopback_out' pair forwards from_host data back to_host."""
1324
1325 clk = Clock()
1326 rst = Reset()
1327
1328 @generator
1329 def construct(ports):
1330 clk = ports.clk
1331 rst = ports.rst
1332
1333 # MMIO interface for triggering the producer.
1334 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
1335
1336 # State: remaining count and current value.
1337 remaining = Reg(UInt(32), clk=clk, rst=rst, rst_value=0)
1338 cur_value = Reg(UInt(32), clk=clk, rst=rst, rst_value=0)
1339
1340 # Handle MMIO commands.
1341 cmd_ready = Wire(Bits(1))
1342 cmd, cmd_valid = cmd_chan_wire.unwrap(cmd_ready)
1343 is_write = cmd.write & cmd_valid
1344 # On write to offset 0x0, load the count and reset the current value.
1345 load_count = is_write & (cmd.offset == UInt(32)(0))
1346
1347 # to_host: send incrementing values while remaining > 0.
1348 has_data = remaining != UInt(32)(0)
1349 data_chan, data_ready = Channel(UInt(32)).wrap(cur_value, has_data)
1350 sent = data_ready & has_data
1351
1352 # Compute next state: load from MMIO takes priority, then decrement on send.
1353 next_remaining = Mux(
1354 load_count, Mux(sent, remaining, (remaining - UInt(32)(1)).as_uint(32)),
1355 cmd.data.as_uint(32))
1356 next_cur_value = Mux(
1357 load_count, Mux(sent, cur_value, (cur_value + UInt(32)(1)).as_uint(32)),
1358 UInt(32)(0))
1359 remaining.assign(next_remaining)
1360 cur_value.assign(next_cur_value)
1361
1362 # MMIO read response: return remaining count.
1363 response_chan, response_ready = Channel(Bits(64)).wrap(
1364 remaining.as_bits(64), cmd_valid)
1365 cmd_ready.assign(response_ready)
1366
1367 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd"))
1368 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
1369 cmd_chan_wire.assign(mmio_rw_cmd_chan)
1370
1371 # Per-request engine override: on cosim_dma this resolves to the same
1372 # (OneItemBuffersToHost, OneItemBuffersFromHost) pair the BSP already
1373 # uses by default, so this exercises the resolver + substitution path
1374 # end-to-end without altering the observed runtime behavior.
1375 esi.ChannelService.to_host(
1376 AppID("producer"),
1377 data_chan,
1378 options={"engine": "esiaccel.esitester._one_item_buffers_pair"})
1379
1380 # from_host -> to_host loopback.
1381 loopback_in = esi.ChannelService.from_host(AppID("loopback_in"), UInt(32))
1382 esi.ChannelService.to_host(AppID("loopback_out"), loopback_in)
1383
1384
1385class EsiTester(Module):
1386 """Top-level ESI test harness module.
1387
1388 Contains submodules:
1389 CallbackTest (single instance) – host callback via MMIO write (offset 0x10).
1390 LoopbackInOutAdd (single instance) – function service adding constant 11.
1391 ChannelTest (single instance) – ChannelService to_host and from_host loopback.
1392 MMIOAdd(add_amt) instances for add_amt in {4, 9, 14} – MMIO read returns offset + add_amt.
1393 ReadMem(width) for widths: 24, 32, 64, 72, 128, 256, 512, 534 – host memory read tests.
1394 WriteMem(width) for widths: 24, 32, 64, 72, 128, 256, 512, 534 – host memory write tests.
1395 ToHostDMATest(width) for widths: 24, 32, 64, 72, 128, 256, 512, 534 – DMA to host, cycle & count telemetry.
1396 FromHostDMATest(width) for widths: 24, 32, 64, 72, 128, 256, 512, 534 – DMA from host, cycle telemetry.
1397
1398 Width set used across Read/Write/DMA tests:
1399 widths = [24, 32, 64, 72, 128, 256, 512, 534]
1400
1401 Purpose:
1402 Aggregates all functional, MMIO, host memory, and DMA tests into one image
1403 for comprehensive accelerator validation and telemetry collection.
1404 """
1405
1406 clk = Clock()
1407 rst = Reset()
1408
1409 @generator
1410 def construct(ports):
1412 clk=ports.clk,
1413 rst=ports.rst,
1414 instance_name="cb_test",
1415 appid=AppID("cb_test"),
1416 )
1418 clk=ports.clk,
1419 rst=ports.rst,
1420 instance_name="loopback",
1421 appid=AppID("loopback"),
1422 )
1424 clk=ports.clk,
1425 rst=ports.rst,
1426 instance_name="channel_test",
1427 appid=AppID("channel_test"),
1428 )
1429 StreamingAdder(1)(
1430 clk=ports.clk,
1431 rst=ports.rst,
1432 instance_name="streaming_adder",
1433 appid=AppID("streaming_adder"),
1434 )
1436 clk=ports.clk,
1437 rst=ports.rst,
1438 instance_name="coord_translator",
1439 appid=AppID("coord_translator"),
1440 )
1442 clk=ports.clk,
1443 rst=ports.rst,
1444 instance_name="coord_translator_serial",
1445 appid=AppID("coord_translator_serial"),
1446 )
1448 clk=ports.clk,
1449 rst=ports.rst,
1450 instance_name="coord_translator_auto_serial",
1451 appid=AppID("coord_translator_auto_serial"),
1452 )
1453
1454 for i in range(4, 18, 5):
1455 MMIOAdd(i)(instance_name=f"mmio_add_{i}", appid=AppID("mmio_add", i))
1456
1457 for width in [24, 32, 64, 72, 128, 256, 512, 534]:
1458 ReadMem(width)(
1459 instance_name=f"readmem_{width}",
1460 appid=esi.AppID("readmem", width),
1461 clk=ports.clk,
1462 rst=ports.rst,
1463 )
1464 WriteMem(width)(
1465 instance_name=f"writemem_{width}",
1466 appid=AppID("writemem", width),
1467 clk=ports.clk,
1468 rst=ports.rst,
1469 )
1470 ToHostDMATest(width)(
1471 instance_name=f"tohostdma_{width}",
1472 appid=AppID("tohostdma", width),
1473 clk=ports.clk,
1474 rst=ports.rst,
1475 )
1476 FromHostDMATest(width)(
1477 instance_name=f"fromhostdma_{width}",
1478 appid=AppID("fromhostdma", width),
1479 clk=ports.clk,
1480 rst=ports.rst,
1481 )
1482
1483 for i in range(3):
1484 ReadMem(512)(
1485 instance_name=f"readmem_{i}",
1486 appid=esi.AppID(f"readmem_{i}", 512),
1487 clk=ports.clk,
1488 rst=ports.rst,
1489 )
1490 WriteMem(512)(
1491 instance_name=f"writemem_{i}",
1492 appid=AppID(f"writemem_{i}", 512),
1493 clk=ports.clk,
1494 rst=ports.rst,
1495 )
1496 ToHostDMATest(512)(
1497 instance_name=f"tohostdma_{i}",
1498 appid=AppID(f"tohostdma_{i}", 512),
1499 clk=ports.clk,
1500 rst=ports.rst,
1501 )
1502 FromHostDMATest(512)(
1503 instance_name=f"fromhostdma_{i}",
1504 appid=AppID(f"fromhostdma_{i}", 512),
1505 clk=ports.clk,
1506 rst=ports.rst,
1507 )
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))
FromHostDMATest(int width)
Type[Module] MMIOAdd(int add_amt)
Definition esitester.py:663
Type[Module] WriteMem(int width)
Definition esitester.py:904
ReadMem(int width)
Definition esitester.py:800
ToHostDMATest(int width)
IterationGate(int count_width)
Definition esitester.py:65
BurstCommand(int width)
Definition esitester.py:693
StreamingAdder(int numItems)
Definition esitester.py:232