CIRCT 23.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 pycde.common import AppID, Constant, InputChannel, Output, OutputChannel
26from pycde.constructs import ControlReg, Counter, Mux, NamedWire, Reg, Wire
27from pycde.module import Metadata
28from pycde.testing import print_info
29from pycde.types import Bits, Channel, ChannelSignaling, UInt
30
31
32class CallbackTest(Module):
33 """Call a function on the host when an MMIO write is received at offset
34 0x10."""
35
36 clk = Clock()
37 rst = Reset()
38
39 @generator
40 def construct(ports):
41 clk = ports.clk
42 rst = ports.rst
43
44 mmio_bundle = esi.MMIO.read_write(appid=AppID("cmd"))
45 data_resp_chan = Wire(Channel(Bits(64)))
46 mmio_cmd_chan = mmio_bundle.unpack(data=data_resp_chan)["cmd"]
47 cb_trigger, mmio_cmd_chan_fork = mmio_cmd_chan.fork(clk=clk, rst=rst)
48
49 data_resp_chan.assign(
50 mmio_cmd_chan_fork.transform(lambda cmd: Bits(64)(cmd.data)))
51
52 cb_trigger_ready = Wire(Bits(1))
53 cb_trigger_cmd, cb_trigger_valid = cb_trigger.unwrap(cb_trigger_ready)
54 trigger = cb_trigger_valid & (cb_trigger_cmd.offset == UInt(32)(0x10))
55 data_reg = cb_trigger_cmd.data.reg(clk, rst, ce=trigger)
56 cb_chan, cb_trigger_ready_sig = Channel(Bits(64)).wrap(
57 data_reg, trigger.reg(clk, rst))
58 cb_trigger_ready.assign(cb_trigger_ready_sig)
59 esi.CallService.call(AppID("cb"), cb_chan, Bits(0))
60
61
62class LoopbackInOutAdd(Module):
63 """Exposes a function which adds the 'add_amt' constant to the argument."""
64
65 clk = Clock()
66 rst = Reset()
67
68 add_amt = Constant(UInt(16), 11)
69
70 @generator
71 def construct(ports):
72 loopback = Wire(Channel(UInt(16), signaling=ChannelSignaling.FIFO))
73 args = esi.FuncService.get_call_chans(AppID("add"),
74 arg_type=UInt(24),
75 result=loopback)
76
77 ready = Wire(Bits(1))
78 data, valid = args.unwrap(ready)
79 plus7 = data + LoopbackInOutAdd.add_amt.value
80 data_chan, data_ready = Channel(UInt(16), ChannelSignaling.ValidReady).wrap(
81 plus7.as_uint(16), valid)
82 data_chan_buffered = data_chan.buffer(ports.clk, ports.rst, 1,
83 ChannelSignaling.FIFO)
84 ready.assign(data_ready)
85 loopback.assign(data_chan_buffered)
86
87
88@modparams
89def StreamingAdder(numItems: int):
90 """Creates a StreamingAdder module parameterized by the number of items per
91 window frame. The module exposes a function which has an argument of struct
92 {add_amt, list<uint32>}. It then adds add_amt to each element of the list in
93 parallel (numItems at a time) and returns the resulting list.
94 """
95
96 class StreamingAdder(Module):
97 clk = Clock()
98 rst = Reset()
99
100 @generator
101 def construct(ports):
102 from pycde.types import StructType, List, Window
103
104 # Define the argument type: struct { add_amt: UInt(32), list: List<UInt(32)> }
105 arg_struct_type = StructType([("add_amt", UInt(32)),
106 ("input", List(UInt(32)))])
107
108 # Create a windowed version with numItems parallel elements
109 arg_window_type = Window(
110 "arg_window", arg_struct_type,
111 [Window.Frame(None, ["add_amt", ("input", numItems)])])
112
113 # Result is also a List with numItems parallel elements
114 result_struct_type = StructType([("data", List(UInt(32)))])
115 result_window_type = Window("result_window", result_struct_type,
116 [Window.Frame(None, [("data", numItems)])])
117
118 result_chan = Wire(Channel(result_window_type))
119 args = esi.FuncService.get_call_chans(AppID("streaming_add"),
120 arg_type=arg_window_type,
121 result=result_chan)
122
123 # Unwrap the argument channel
124 ready = Wire(Bits(1))
125 arg_data, arg_valid = args.unwrap(ready)
126
127 # Unwrap the window to get the lowered struct
128 # Lowered type: struct { add_amt, input: array[numItems], input_size, last }
129 arg_unwrapped = arg_data.unwrap()
130
131 # Extract add_amt and input array from the struct
132 add_amt = arg_unwrapped["add_amt"]
133 input_arr = arg_unwrapped["input"]
134
135 # Perform all additions in parallel
136 result_arr = [
137 (add_amt + input_arr[i]).as_uint(32) for i in range(numItems)
138 ]
139
140 # Build the result lowered type
141 # Lowered type: struct { data: array[numItems], data_size, last }
142 lowered_val = result_window_type.lowered_type({
143 "data": result_arr,
144 "data_size": arg_unwrapped["input_size"],
145 "last": arg_unwrapped["last"]
146 })
147
148 result_window = result_window_type.wrap(lowered_val)
149
150 # Wrap the result into a channel
151 result_chan_internal, result_ready = Channel(result_window_type).wrap(
152 result_window, arg_valid)
153 ready.assign(result_ready)
154 result_chan.assign(result_chan_internal)
155
156 return StreamingAdder
157
158
159class CoordTranslator(Module):
160 """Exposes a function which takes a struct of {x_translation, y_translation,
161 coords: list<struct{x, y}>} and adds the translation to each coordinate,
162 returning the translated list of coordinates.
163 """
164
165 clk = Clock()
166 rst = Reset()
167
168 @generator
169 def construct(ports):
170 from pycde.types import StructType, List, Window
171
172 # Define the coordinate type: struct { x: UInt(32), y: UInt(32) }
173 coord_type = StructType([("x", UInt(32)), ("y", UInt(32))])
174
175 # Define the argument type: struct { x_translation, y_translation, coords }
176 arg_struct_type = StructType([("x_translation", UInt(32)),
177 ("y_translation", UInt(32)),
178 ("coords", List(coord_type))])
179
180 # Create a windowed version of the argument struct for streaming
181 arg_window_type = Window.default_of(arg_struct_type)
182
183 # Result is also a List of coordinates
184 result_type = List(coord_type)
185 result_window_type = Window.default_of(result_type)
186
187 result_chan = Wire(Channel(result_window_type))
188 args = esi.FuncService.get_call_chans(AppID("translate_coords"),
189 arg_type=arg_window_type,
190 result=result_chan)
191
192 # Unwrap the argument channel
193 ready = Wire(Bits(1))
194 arg_data, arg_valid = args.unwrap(ready)
195
196 # Unwrap the window to get the struct/union
197 arg_unwrapped = arg_data.unwrap()
198
199 # Extract translations and coordinates from the struct
200 x_translation = arg_unwrapped["x_translation"]
201 y_translation = arg_unwrapped["y_translation"]
202 input_coord = arg_unwrapped["coords"]
203
204 # Add translations to each coordinate
205 result_x = (x_translation + input_coord["x"]).as_uint(32)
206 result_y = (y_translation + input_coord["y"]).as_uint(32)
207
208 # Create the result coordinate struct
209 result_coord = coord_type({"x": result_x, "y": result_y})
210
211 result_window = result_window_type.wrap(
212 result_window_type.lowered_type({
213 "data": result_coord,
214 "last": arg_unwrapped.last
215 }))
216
217 # Wrap the result into a channel
218 result_chan_internal, result_ready = Channel(result_window_type).wrap(
219 result_window, arg_valid)
220 ready.assign(result_ready)
221 result_chan.assign(result_chan_internal)
222
223
225 """Like CoordTranslator, but uses the serial (bulk-transfer) list encoding.
226
227 Input wire format is a window with two frames:
228 - "header": {x_translation, y_translation, coords_count}
229 - "data": {coords[1]} (one coordinate per frame)
230
231 Output wire format is also a window with two frames:
232 - "header": {coords_count}
233 - "data": {coords[1]} (one coordinate per frame)
234
235 In bulk-transfer encoding, the sender may transmit multiple header/data
236 sequences to extend a list. A common pattern is to set coords_count=64 and
237 re-send a new header every 64 items; the final header has coords_count=0.
238 This module passes the header count through and translates each coordinate.
239 """
240
241 clk = Clock()
242 rst = Reset()
243
244 @generator
245 def construct(ports):
246 from pycde.types import List, StructType, Window
247
248 clk = ports.clk
249 rst = ports.rst
250
251 bulk_count_width = 16
252 items_per_frame = 1
253
254 coord_type = StructType([("x", Bits(32)), ("y", Bits(32))])
255
256 # ----- Input window type (serial/bulk transfer) -----
257 arg_struct_type = StructType([
258 ("x_translation", Bits(32)),
259 ("y_translation", Bits(32)),
260 ("coords", List(coord_type)),
261 ])
262 arg_window_type = Window(
263 "serial_coord_args",
264 arg_struct_type,
265 [
266 Window.Frame(
267 "header",
268 [
269 "x_translation",
270 "y_translation",
271 ("coords", 0, bulk_count_width),
272 ],
273 ),
274 Window.Frame(
275 "data",
276 [("coords", items_per_frame, 0)],
277 ),
278 ],
279 )
280
281 # ----- Output window type (serial/bulk transfer) -----
282 result_struct_type = StructType([("coords", List(coord_type))])
283 result_window_type = Window(
284 "serial_coord_result",
285 result_struct_type,
286 [
287 Window.Frame(
288 "header",
289 [("coords", 0, bulk_count_width)],
290 ),
291 Window.Frame(
292 "data",
293 [("coords", items_per_frame, 0)],
294 ),
295 ],
296 )
297
298 result_chan = Wire(Channel(result_window_type))
299 args = esi.FuncService.get_call_chans(
300 AppID("translate_coords_serial"),
301 arg_type=arg_window_type,
302 result=result_chan,
303 )
304
305 # Unwrap the argument channel.
306 in_ready = Wire(Bits(1))
307 in_window, in_valid = args.unwrap(in_ready)
308 in_union = in_window.unwrap()
309
310 hdr_frame = in_union["header"]
311 data_frame = in_union["data"]
312
313 hdr_x = hdr_frame["x_translation"].as_uint(32)
314 hdr_y = hdr_frame["y_translation"].as_uint(32)
315 hdr_count_bits = hdr_frame["coords_count"]
316 hdr_count = hdr_count_bits.as_uint(bulk_count_width)
317
318 out_hdr_struct_ty = result_window_type.lowered_type.header
319 out_data_struct_ty = result_window_type.lowered_type.data
320
321 # Output channel (built below) drives readiness/backpressure.
322 out_ready_wire = Wire(Bits(1))
323 handshake = in_valid & out_ready_wire
324
325 # Track which frame we're currently expecting.
326 in_is_header = Reg(
327 Bits(1),
328 clk=clk,
329 rst=rst,
330 rst_value=1,
331 ce=handshake,
332 name="in_is_header",
333 )
334 # Only log the frame count when the handshake is for a header frame.
335 hdr_handshake = handshake & in_is_header
336 hdr_handshake.when_true(
337 lambda: print_info("Received frame count=%d", hdr_count_bits))
338
339 # Latch the most recent header count for re-use when emitting the output
340 # header (do not rely on union extracts during data frames).
341 hdr_is_zero = hdr_count == UInt(bulk_count_width)(0)
342 footer_handshake = hdr_handshake & hdr_is_zero
343 start_handshake = hdr_handshake & ~hdr_is_zero
344 message_active = ControlReg(
345 clk,
346 rst,
347 asserts=[start_handshake],
348 resets=[footer_handshake],
349 name="message_active",
350 )
351 count_reg = Reg(
352 UInt(bulk_count_width),
353 clk=clk,
354 rst=rst,
355 rst_value=0,
356 ce=hdr_handshake,
357 name="coords_count",
358 )
359 count_reg.assign(hdr_count)
360
361 data_handshake = handshake & ~in_is_header
362 data_count = Counter(bulk_count_width)(
363 clk=clk,
364 rst=rst,
365 clear=hdr_handshake,
366 increment=data_handshake,
367 instance_name="data_count",
368 ).out
369
370 # Latch translations only on the first header of a message.
371 x_translation_reg = Reg(
372 UInt(32),
373 clk=clk,
374 rst=rst,
375 rst_value=0,
376 ce=start_handshake & ~message_active,
377 name="x_translation",
378 )
379 y_translation_reg = Reg(
380 UInt(32),
381 clk=clk,
382 rst=rst,
383 rst_value=0,
384 ce=start_handshake & ~message_active,
385 name="y_translation",
386 )
387 x_translation_reg.assign(hdr_x)
388 y_translation_reg.assign(hdr_y)
389
390 # Next-state logic for header/data tracking.
391 count_minus_one = (count_reg -
392 UInt(bulk_count_width)(1)).as_uint(bulk_count_width)
393 data_last = data_count == count_minus_one
394 next_is_header = Mux(in_is_header, data_last, hdr_is_zero)
395 in_is_header.assign(next_is_header)
396
397 # Build output frames.
398 out_hdr_struct = out_hdr_struct_ty(
399 {"coords_count": hdr_count.as_bits(bulk_count_width)})
400
401 in_coord = data_frame["coords"][0]
402 in_x = in_coord["x"].as_uint(32)
403 in_y = in_coord["y"].as_uint(32)
404 translated_x = (x_translation_reg + in_x).as_uint(32)
405 translated_y = (y_translation_reg + in_y).as_uint(32)
406 out_coord = coord_type({
407 "x": translated_x.as_bits(32),
408 "y": translated_y.as_bits(32),
409 })
410 out_data_struct = out_data_struct_ty({"coords": [out_coord]})
411
412 out_union_hdr = result_window_type.lowered_type(("header", out_hdr_struct))
413 out_union_data = result_window_type.lowered_type(("data", out_data_struct))
414 out_union = Mux(in_is_header, out_union_data, out_union_hdr)
415 out_window = result_window_type.wrap(out_union)
416
417 out_chan, out_ready = Channel(result_window_type).wrap(out_window, in_valid)
418 out_ready_wire.assign(out_ready)
419
420 in_ready.assign(out_ready)
421 result_chan.assign(out_chan)
422
423
425 """Like CoordTranslator, but exposes the function with the serial
426 (bulk-transfer) list encoding on both the argument and result. Internally,
427 the serial input is converted to the parallel one-item-per-message form via
428 `ListWindowToParallel`, the per-coordinate translation is applied, and the
429 parallel result is converted back to the serial wire form via
430 `ListWindowToSerial`.
431
432 This exercises the automatic serial<->parallel conversion modules instead of
433 building the frame state machine by hand (as `SerialCoordTranslator` does).
434 """
435
436 clk = Clock()
437 rst = Reset()
438
439 @generator
440 def construct(ports):
441 from pycde.types import StructType, List, Window
442 from pycde.esi import ListWindowToParallel, ListWindowToSerial
443
444 bulk_count_width = 16
445 items_per_frame = 1
446 # Intentionally tiny FIFO so the host's coord lists (which can be much
447 # larger than this) get split across many bulk transfers, exercising the
448 # multi-burst code paths in `ListWindowToSerial` (drain-on-full bursts
449 # interleaved with the producer, plus the count==0 terminator).
450 fifo_depth = 4
451
452 # ---- Externally-visible (serial) function arg/result types. ----
453 # NOTE: use Bits for coord/translation fields. The window lowering for
454 # bulk-transfer encoding currently strips signedness from union variant
455 # fields, which causes type mismatches when the underlying struct uses
456 # UInt; SerialCoordTranslator hits the same constraint.
457 coord_type = StructType([("x", Bits(32)), ("y", Bits(32))])
458
459 arg_struct_type = StructType([("x_translation", Bits(32)),
460 ("y_translation", Bits(32)),
461 ("coords", List(coord_type))])
462 arg_window_type = Window.serial_of(arg_struct_type, bulk_count_width,
463 items_per_frame)
464
465 result_type = List(coord_type)
466 result_window_type = Window.serial_of(result_type, bulk_count_width,
467 items_per_frame)
468
469 # Result channel back to FuncService is the serial output of the
470 # parallel->serial converter (assigned at the end).
471 result_chan = Wire(Channel(result_window_type))
472 args = esi.FuncService.get_call_chans(AppID("translate_coords_auto_serial"),
473 arg_type=arg_window_type,
474 result=result_chan)
475
476 # ---- Convert the serial argument stream into a parallel one. ----
477 s2p = ListWindowToParallel(arg_window_type)(clk=ports.clk,
478 rst=ports.rst,
479 serial_in=args)
480 parallel_arg = s2p.parallel_out
481
482 # ---- Apply the per-coordinate translation. ----
483 par_ready = Wire(Bits(1))
484 par_window, par_valid = parallel_arg.unwrap(par_ready)
485 par_struct = par_window.unwrap()
486
487 x_translation = par_struct["x_translation"].as_uint(32)
488 y_translation = par_struct["y_translation"].as_uint(32)
489 input_coord = par_struct["coords"]
490 last_bit = par_struct["last"]
491
492 result_x = (x_translation +
493 input_coord["x"].as_uint(32)).as_uint(32).as_bits(32)
494 result_y = (y_translation +
495 input_coord["y"].as_uint(32)).as_uint(32).as_bits(32)
496 result_coord = coord_type({"x": result_x, "y": result_y})
497
498 parallel_result_window_type = Window.default_of(result_type)
499 parallel_result_struct = parallel_result_window_type.lowered_type({
500 "data": result_coord,
501 "last": last_bit,
502 })
503 parallel_result_window = parallel_result_window_type.wrap(
504 parallel_result_struct)
505
506 parallel_result_chan, parallel_result_ready = Channel(
507 parallel_result_window_type).wrap(parallel_result_window, par_valid)
508 par_ready.assign(parallel_result_ready)
509
510 # ---- Convert the parallel result stream back into a serial one. ----
511 p2s = ListWindowToSerial(parallel_result_window_type, bulk_count_width,
512 items_per_frame,
513 fifo_depth)(clk=ports.clk,
514 rst=ports.rst,
515 parallel_in=parallel_result_chan)
516 result_chan.assign(p2s.serial_out)
517
518
519@modparams
520def MMIOAdd(add_amt: int) -> Type[Module]:
521
522 class MMIOAdd(Module):
523 """Exposes an MMIO address space wherein MMIO reads return the <address
524 offset into its space> + add_amt."""
525
526 metadata = Metadata(
527 name="MMIOAdd",
528 misc={"add_amt": add_amt},
529 )
530
531 add_amt_const = Constant(UInt(32), add_amt)
532
533 @generator
534 def build(ports):
535 mmio_read_bundle = esi.MMIO.read(appid=AppID("mmio_client", add_amt))
536
537 address_chan_wire = Wire(Channel(UInt(32)))
538 address, address_valid = address_chan_wire.unwrap(1)
539 response_data = (address.as_uint() + add_amt).as_bits(64)
540 response_chan, response_ready = Channel(Bits(64)).wrap(
541 response_data, address_valid)
542
543 address_chan = mmio_read_bundle.unpack(data=response_chan)["offset"]
544 address_chan_wire.assign(address_chan)
545
546 return MMIOAdd
547
548
549@modparams
550def AddressCommand(width: int):
551
552 class AddressCommand(Module):
553 """Constructs an module which takes MMIO commands and issues host memory
554 commands based on those commands. Tracks the number of cycles to issue
555 addresses and get all of the expected responses.
556
557 MMIO offsets:
558 0x10: Starting address for host memory operations.
559 0x18: Number of flits to read/write.
560 0x20: Start read/write operation.
561 """
562
563 clk = Clock()
564 rst = Reset()
565
566 # Number of flits left to issue.
567 flits_left = Output(UInt(64))
568 # Signal to start the operation.
569 command_go = Output(Bits(1))
570
571 # Channel which issues hostmem addresses. Must be transformed into
572 # read/write requests by the instantiator.
573 hostmem_cmd_address = OutputChannel(UInt(64))
574
575 # Channel which indicates when the read/write operation is done.
576 hostmem_cmd_done = InputChannel(Bits(0))
577
578 @generator
579 def construct(ports):
580 # MMIO command channel setup.
581 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
582 resp_ready_wire = Wire(Bits(1))
583 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
584 mmio_xact = cmd_valid & resp_ready_wire
585
586 # Write enables.
587 start_addr_we = (mmio_xact & cmd.write & (cmd.offset == UInt(32)(0x10)))
588 flits_we = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0x18))
589 start_op_we = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0x20))
590 ports.command_go = start_op_we
591
592 # Registers for start address and number of flits.
593 start_addr = cmd.data.as_uint().reg(
594 clk=ports.clk,
595 rst=ports.rst,
596 rst_value=0,
597 ce=start_addr_we,
598 name="start_addr",
599 )
600 flits_total = cmd.data.as_uint().reg(
601 clk=ports.clk,
602 rst=ports.rst,
603 rst_value=0,
604 ce=flits_we,
605 name="flits_total",
606 )
607
608 # Response counter.
609 responses_incr = Wire(Bits(1))
610 responses_cnt = Counter(64)(
611 clk=ports.clk,
612 rst=ports.rst,
613 clear=start_op_we,
614 increment=responses_incr,
615 instance_name="addr_cmd_responses_cnt",
616 )
617
618 operation_done = responses_cnt.out.as_uint() == flits_total
619 operation_active = ControlReg(
620 clk=ports.clk,
621 rst=ports.rst,
622 asserts=[start_op_we],
623 resets=[operation_done],
624 name="operation_active",
625 )
626 # Cycle counter while active.
627 cycles_cnt = Counter(64)(
628 clk=ports.clk,
629 rst=ports.rst,
630 clear=start_op_we,
631 increment=operation_active,
632 instance_name="addr_cmd_cycle_counter",
633 )
634 # Latch final cycle count at completion.
635 final_cycles = Reg(
636 UInt(64),
637 clk=ports.clk,
638 rst=ports.rst,
639 rst_value=0,
640 ce=operation_done,
641 name="addr_cmd_cycles",
642 )
643 final_cycles.assign(cycles_cnt.out.as_uint())
644
645 # Issue counter.
646 issue_incr = Wire(Bits(1))
647 issue_cnt = Counter(64)(
648 clk=ports.clk,
649 rst=ports.rst,
650 clear=start_op_we,
651 increment=issue_incr,
652 )
653
654 # Increment by number of bytes per flit, rounded up to nearest 32
655 # bits (double word).
656 incr_bytes = UInt(64)(((width + 31) // 32) * 4)
657
658 # Generate current address.
659 current_addr = (start_addr +
660 (issue_cnt.out.as_uint() * incr_bytes)).as_uint(64)
661
662 # Valid when active and still have flits to issue.
663 addr_valid = operation_active & (issue_cnt.out.as_uint() < flits_total)
664 addr_chan, addr_ready = Channel(UInt(64),
665 ChannelSignaling.ValidReady).wrap(
666 current_addr, addr_valid)
667 issue_xact = addr_valid & addr_ready
668 issue_incr.assign(issue_xact)
669
670 # Consume hostmem_cmd_done (Bits(0) channel) for completed responses.
671 _, done_valid = ports.hostmem_cmd_done.unwrap(Bits(1)(1))
672 responses_incr.assign(done_valid)
673
674 # flits_left = total - responses received.
675 flits_left_val = (flits_total - responses_cnt.out.as_uint()).as_uint(64)
676 ports.flits_left = flits_left_val # direct assignment
677
678 # Drive output channel.
679 ports.hostmem_cmd_address = addr_chan # direct assignment
680
681 # MMIO read response: return flits_left.
682 response_data = flits_left_val.as_bits(64)
683 response_chan, response_ready = Channel(Bits(64)).wrap(
684 response_data, cmd_valid)
685 resp_ready_wire.assign(response_ready)
686
687 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd", width))
688 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
689 cmd_chan_wire.assign(mmio_rw_cmd_chan)
690
691 # Report telemetry.
692 esi.Telemetry.report_signal(
693 ports.clk,
694 ports.rst,
695 esi.AppID("addrCmdCycles"),
696 final_cycles,
697 )
698 esi.Telemetry.report_signal(
699 ports.clk,
700 ports.rst,
701 esi.AppID("addrCmdIssued"),
702 issue_cnt.out,
703 )
704 esi.Telemetry.report_signal(
705 ports.clk,
706 ports.rst,
707 esi.AppID("addrCmdResponses"),
708 responses_cnt.out,
709 )
710
711 return AddressCommand
712
713
714@modparams
715def ReadMem(width: int):
716
717 class ReadMem(Module):
718 """Host memory read test module.
719
720 Function:
721 Issues a sequence of host memory read requests using an internal
722 address/control submodule which is configured via MMIO writes. Each
723 read returns 'width' bits; the low 64 bits of the most recent
724 response are latched and exported as telemetry (lastReadLSB).
725
726 Flit width:
727 'width' is the number of payload data bits per read flit. The address
728 stride between successive requests is ceil(width/32) 32-bit words
729 (= ceil(width/32) * 4 bytes). Non–power‑of‑two widths are supported
730 and packed into the minimum whole 32‑bit word count.
731
732 MMIO command interface:
733 0x10 Write: Starting base address for the read operation.
734 0x18 Write: Number of flits (read transactions) to perform.
735 0x20 Write: Start the operation (assert once to launch).
736 Reads return the current flits_left (remaining responses).
737
738 Operation:
739 After 0x20 is written, sequential addresses are generated:
740 addr = start_addr + i * ceil(width/32) (i = 0 .. flits-1)
741 Each address produces one host memory read request.
742
743 Telemetry (AppID -> signal):
744 addrCmdCycles Total cycles elapsed during the active window.
745 addrCmdIssued Count of host memory commands issued.
746 addrCmdResponses Count of host memory responses received.
747 lastReadLSB Low 64 bits of the most recently received read data.
748
749 Notes:
750 Backpressure on the read response channel naturally throttles issue.
751 Completion occurs when responses == requested flits.
752 """
753
754 clk = Clock()
755 rst = Reset()
756
757 width_bits = Constant(UInt(32), width)
758
759 @generator
760 def construct(ports):
761 clk = ports.clk
762 rst = ports.rst
763
764 address_cmd_resp = Wire(Channel(Bits(0)))
765 addresses = AddressCommand(width)(
766 clk=clk,
767 rst=rst,
768 hostmem_cmd_done=address_cmd_resp,
769 instance_name="address_command",
770 )
771
772 read_cmd_chan = addresses.hostmem_cmd_address.transform(
773 lambda addr: esi.HostMem.ReadReqType({
774 "tag": UInt(8)(0),
775 "address": addr
776 }))
777 read_responses = esi.HostMem.read(
778 appid=AppID("host"),
779 data_type=Bits(width),
780 req=read_cmd_chan,
781 )
782 # Signal completion to AddressCommand (each response -> Bits(0)).
783 address_cmd_resp.assign(read_responses.transform(lambda resp: Bits(0)(0)))
784 # Snoop the response channel to capture the low 64 bits without consuming it.
785 read_resp_valid, _, read_resp_data = read_responses.snoop()
786 last_read_lsb = Reg(
787 UInt(64),
788 clk=ports.clk,
789 rst=ports.rst,
790 rst_value=0,
791 ce=read_resp_valid,
792 name="last_read_lsb",
793 )
794 last_read_lsb.assign(read_resp_data.data.as_uint(64))
795 esi.Telemetry.report_signal(
796 ports.clk,
797 ports.rst,
798 esi.AppID("lastReadLSB"),
799 last_read_lsb,
800 )
801
802 return ReadMem
803
804
805@modparams
806def WriteMem(width: int) -> Type[Module]:
807
808 class WriteMem(Module):
809 """Host memory write test module.
810
811 Function:
812 Issues sequential host memory write requests produced by an internal
813 address/control submodule configured via MMIO writes. Data for each
814 flit is the current free‑running 32‑bit cycle counter value
815 (zero‑extended or truncated to 'width').
816
817 Flit width:
818 'width' is the number of payload data bits per write flit. The address
819 stride between successive writes is ceil(width/32) 32‑bit words
820 (= ceil(width/32) * 4 bytes). Wider payloads span multiple words;
821 narrower payloads still consume one word of address space per flit.
822
823 MMIO command interface:
824 0x10 Write: Starting base address for the write operation.
825 0x18 Write: Number of flits (write transactions) to perform.
826 0x20 Write: Start the operation (assert once to launch).
827 Reads return the current flits_left (remaining responses).
828
829 Data pattern:
830 data[i] = cycle_counter sampled when the write command for flit i is formed.
831
832 Telemetry (AppID -> signal):
833 addrCmdCycles Total cycles elapsed during the active window.
834 addrCmdIssued Count of host memory commands issued.
835 addrCmdResponses Count of host memory responses received.
836
837 Notes:
838 No additional telemetry beyond the above signals is generated here.
839 Completion occurs when write responses == requested flits.
840 """
841
842 clk = Clock()
843 rst = Reset()
844
845 width_bits = Constant(UInt(32), width)
846
847 @generator
848 def construct(ports):
849 clk = ports.clk
850 rst = ports.rst
851
852 cycle_counter_reset = Wire(Bits(1))
853 cycle_counter = Counter(32)(
854 clk=clk,
855 rst=rst,
856 clear=Bits(1)(0),
857 increment=Bits(1)(1),
858 )
859
860 address_cmd_resp = Wire(Channel(Bits(0)))
861 addresses = AddressCommand(width)(
862 clk=clk,
863 rst=rst,
864 hostmem_cmd_done=address_cmd_resp,
865 instance_name="address_command",
866 )
867 cycle_counter_reset.assign(addresses.command_go)
868
869 write_cmd_chan = addresses.hostmem_cmd_address.transform(
870 lambda addr: esi.HostMem.write_req_channel_type(UInt(width))({
871 "tag": UInt(8)(0),
872 "address": addr,
873 "data": cycle_counter.out.as_uint(width),
874 }))
875
876 write_responses = esi.HostMem.write(
877 appid=AppID("host"),
878 req=write_cmd_chan,
879 )
880 # Signal completion to AddressCommand (each response -> Bits(0)).
881 address_cmd_resp.assign(
882 write_responses.transform(lambda resp: Bits(0)(0)))
883
884 return WriteMem
885
886
887@modparams
888def ToHostDMATest(width: int):
889 """Construct a module that sends the write count over a channel to the host
890 the specified number of times. Exercises any DMA engine."""
891
892 class ToHostDMATest(Module):
893 """Transmit a 32-bit cycle counter value to the host a programmed number of times.
894
895 Functionality:
896 A free-running 32-bit counter advances only on successful channel
897 handshakes. A write to MMIO offset 0x0 programs 'write_count' (number
898 of messages to send). Each message’s payload is the counter value
899 constrained to 'width':
900 width < 32 -> lower 'width' bits (truncated)
901 width >= 32 -> zero-extended to 'width' bits
902 One message is emitted per handshake until 'write_count' messages have
903 been transferred. A new write to 0x0 re-arms after completion.
904
905 Width:
906 Selects how the 32-bit counter is represented on the output channel
907 (truncate or zero-extend as above).
908
909 MMIO command interface:
910 0x0 Write: Set write_count (messages to transmit). Starts a new
911 sequence if idle/completed.
912 0x0 Read: Returns constant 0.
913
914 Telemetry:
915 totalWrites (AppID "totalWrites"): Count of messages successfully sent.
916 toHostCycles (AppID "toHostCycles"): Cycle count from write_count programming
917 (start) through completion (inclusive of active cycles).
918
919 Notes:
920 Backpressure governs pacing. Completion when totalWrites == write_count.
921 """
922
923 clk = Clock()
924 rst = Reset()
925
926 width_bits = Constant(UInt(32), width)
927
928 @generator
929 def construct(ports):
930 count_reached = Wire(Bits(1))
931 count_valid = Wire(Bits(1))
932 out_xact = Wire(Bits(1))
933 cycle_counter = Counter(32)(
934 clk=ports.clk,
935 rst=ports.rst,
936 clear=Bits(1)(0),
937 increment=out_xact,
938 )
939
940 write_cntr_incr = ~count_reached & count_valid & out_xact
941 write_counter = Counter(32)(
942 clk=ports.clk,
943 rst=ports.rst,
944 clear=count_reached,
945 increment=write_cntr_incr,
946 )
947 num_writes = write_counter.out
948
949 # Get the MMIO space for commands.
950 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
951 resp_ready_wire = Wire(Bits(1))
952 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
953 mmio_xact = cmd_valid & resp_ready_wire
954 response_data = Bits(64)(0)
955 response_chan, response_ready = Channel(response_data.type).wrap(
956 response_data, cmd_valid)
957 resp_ready_wire.assign(response_ready)
958
959 # write_count is the specified number of times to send the cycle count.
960 write_count_ce = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0))
961 write_count = cmd.data.as_uint().reg(clk=ports.clk,
962 rst=ports.rst,
963 rst_value=0,
964 ce=write_count_ce)
965 count_reached.assign(num_writes == write_count)
966 count_valid.assign(
967 ControlReg(
968 clk=ports.clk,
969 rst=ports.rst,
970 asserts=[write_count_ce],
971 resets=[count_reached],
972 ))
973
974 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd"))
975 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
976 cmd_chan_wire.assign(mmio_rw_cmd_chan)
977
978 # Output channel.
979 out_channel, out_channel_ready = Channel(UInt(width)).wrap(
980 cycle_counter.out.as_uint(width), count_valid)
981 out_xact.assign(out_channel_ready & count_valid)
982 esi.ChannelService.to_host(name=AppID("out"), chan=out_channel)
983
984 total_write_counter = Counter(64)(
985 clk=ports.clk,
986 rst=ports.rst,
987 clear=Bits(1)(0),
988 increment=write_cntr_incr,
989 )
990 esi.Telemetry.report_signal(
991 ports.clk,
992 ports.rst,
993 esi.AppID("totalWrites"),
994 total_write_counter.out,
995 )
996
997 # Cycle telemetry: count cycles while sequence active.
998 tohost_cycle_cnt = Counter(64)(
999 clk=ports.clk,
1000 rst=ports.rst,
1001 clear=write_count_ce,
1002 increment=count_valid,
1003 instance_name="tohost_cycle_counter",
1004 )
1005 tohost_final_cycles = Reg(
1006 UInt(64),
1007 clk=ports.clk,
1008 rst=ports.rst,
1009 rst_value=0,
1010 ce=count_reached,
1011 name="tohost_cycles",
1012 )
1013 tohost_final_cycles.assign(tohost_cycle_cnt.out.as_uint())
1014 esi.Telemetry.report_signal(
1015 ports.clk,
1016 ports.rst,
1017 esi.AppID("toHostCycles"),
1018 tohost_final_cycles,
1019 )
1020
1021 return ToHostDMATest
1022
1023
1024@modparams
1025def FromHostDMATest(width: int):
1026 """Construct a module that receives the write count over a channel from the
1027 host the specified number of times. Exercises any DMA engine."""
1028
1029 class FromHostDMATest(Module):
1030 """Receive test data from the host a programmed number of times.
1031
1032 Functionality:
1033 A write to MMIO offset 0x0 programs 'read_count', the number of messages
1034 to accept from the host. The input channel (AppID "in") is marked ready
1035 while the number of received messages is less than 'read_count'. Each
1036 received width-bit payload is latched; the most recent value is exposed
1037 on MMIO reads.
1038
1039 Width:
1040 'width' is the payload bit width of each received message. The latched
1041 value is widened/truncated to 64 bits for MMIO read-back (lower 64 bits
1042 if width > 64).
1043
1044 MMIO command interface:
1045 0x0 Write: Set read_count (number of messages to receive). Clears the
1046 internal receive counter.
1047 0x0 Read: Returns the last received value (Bits(64), derived from the
1048 width-bit payload).
1049
1050 Telemetry:
1051 fromHostCycles (AppID "fromHostCycles"): Cycle count from read_count programming
1052 (start) through completion of the programmed receive sequence.
1053
1054 Notes:
1055 Completion is when received messages == programmed read_count; another
1056 write to 0x0 re-arms for a new sequence.
1057 """
1058
1059 clk = Clock()
1060 rst = Reset()
1061
1062 width_bits = Constant(UInt(32), width)
1063
1064 @generator
1065 def build(ports):
1066 last_read = Wire(UInt(width))
1067
1068 # Get the MMIO space for commands.
1069 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
1070 resp_ready_wire = Wire(Bits(1))
1071 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
1072 mmio_xact = cmd_valid & resp_ready_wire
1073 response_data = last_read.as_bits(64)
1074 response_chan, response_ready = Channel(response_data.type).wrap(
1075 response_data, cmd_valid)
1076 resp_ready_wire.assign(response_ready)
1077
1078 # read_count is the specified number of times to recieve data.
1079 read_count_ce = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0))
1080 read_count = cmd.data.as_uint().reg(clk=ports.clk,
1081 rst=ports.rst,
1082 rst_value=0,
1083 ce=read_count_ce)
1084 in_data_xact = NamedWire(Bits(1), "in_data_xact")
1085 read_counter = Counter(32)(
1086 clk=ports.clk,
1087 rst=ports.rst,
1088 clear=read_count_ce,
1089 increment=in_data_xact,
1090 )
1091
1092 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd"))
1093 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
1094 cmd_chan_wire.assign(mmio_rw_cmd_chan)
1095
1096 in_chan = esi.ChannelService.from_host(name=AppID("in"), type=UInt(width))
1097 in_ready = NamedWire(read_counter.out < read_count, "in_ready")
1098 in_data, in_valid = in_chan.unwrap(in_ready)
1099 NamedWire(in_data, "in_data")
1100 in_data_xact.assign(in_valid & in_ready)
1101
1102 last_read.assign(
1103 in_data.reg(
1104 clk=ports.clk,
1105 rst=ports.rst,
1106 ce=in_data_xact,
1107 name="last_read",
1108 ))
1109
1110 # Cycle telemetry: detect completion and count active cycles.
1111 fromhost_count_reached = Wire(Bits(1))
1112 fromhost_count_reached.assign(read_counter.out == read_count)
1113 fromhost_cycle_valid = ControlReg(
1114 clk=ports.clk,
1115 rst=ports.rst,
1116 asserts=[read_count_ce],
1117 resets=[fromhost_count_reached],
1118 name="fromhost_cycle_active",
1119 )
1120 fromhost_cycle_cnt = Counter(64)(
1121 clk=ports.clk,
1122 rst=ports.rst,
1123 clear=read_count_ce,
1124 increment=fromhost_cycle_valid,
1125 instance_name="fromhost_cycle_counter",
1126 )
1127 fromhost_final_cycles = Reg(
1128 UInt(64),
1129 clk=ports.clk,
1130 rst=ports.rst,
1131 rst_value=0,
1132 ce=fromhost_count_reached,
1133 name="fromhost_cycles",
1134 )
1135 fromhost_final_cycles.assign(fromhost_cycle_cnt.out.as_uint())
1136 esi.Telemetry.report_signal(
1137 ports.clk,
1138 ports.rst,
1139 esi.AppID("fromHostCycles"),
1140 fromhost_final_cycles,
1141 )
1142
1143 return FromHostDMATest
1144
1145
1146class ChannelTest(Module):
1147 """Test the ChannelService with a to_host producer and a from_host loopback.
1148
1149 The 'producer' to_host port sends incrementing UInt(32) values. The number
1150 of values to send is specified via an MMIO write to offset 0x0. Reading MMIO
1151 returns the remaining count.
1152
1153 The 'loopback_in'/'loopback_out' pair forwards from_host data back to_host."""
1154
1155 clk = Clock()
1156 rst = Reset()
1157
1158 @generator
1159 def construct(ports):
1160 clk = ports.clk
1161 rst = ports.rst
1162
1163 # MMIO interface for triggering the producer.
1164 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
1165
1166 # State: remaining count and current value.
1167 remaining = Reg(UInt(32), clk=clk, rst=rst, rst_value=0)
1168 cur_value = Reg(UInt(32), clk=clk, rst=rst, rst_value=0)
1169
1170 # Handle MMIO commands.
1171 cmd_ready = Wire(Bits(1))
1172 cmd, cmd_valid = cmd_chan_wire.unwrap(cmd_ready)
1173 is_write = cmd.write & cmd_valid
1174 # On write to offset 0x0, load the count and reset the current value.
1175 load_count = is_write & (cmd.offset == UInt(32)(0))
1176
1177 # to_host: send incrementing values while remaining > 0.
1178 has_data = remaining != UInt(32)(0)
1179 data_chan, data_ready = Channel(UInt(32)).wrap(cur_value, has_data)
1180 sent = data_ready & has_data
1181
1182 # Compute next state: load from MMIO takes priority, then decrement on send.
1183 next_remaining = Mux(
1184 load_count, Mux(sent, remaining, (remaining - UInt(32)(1)).as_uint(32)),
1185 cmd.data.as_uint(32))
1186 next_cur_value = Mux(
1187 load_count, Mux(sent, cur_value, (cur_value + UInt(32)(1)).as_uint(32)),
1188 UInt(32)(0))
1189 remaining.assign(next_remaining)
1190 cur_value.assign(next_cur_value)
1191
1192 # MMIO read response: return remaining count.
1193 response_chan, response_ready = Channel(Bits(64)).wrap(
1194 remaining.as_bits(64), cmd_valid)
1195 cmd_ready.assign(response_ready)
1196
1197 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd"))
1198 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
1199 cmd_chan_wire.assign(mmio_rw_cmd_chan)
1200
1201 esi.ChannelService.to_host(AppID("producer"), data_chan)
1202
1203 # from_host -> to_host loopback.
1204 loopback_in = esi.ChannelService.from_host(AppID("loopback_in"), UInt(32))
1205 esi.ChannelService.to_host(AppID("loopback_out"), loopback_in)
1206
1207
1208class EsiTester(Module):
1209 """Top-level ESI test harness module.
1210
1211 Contains submodules:
1212 CallbackTest (single instance) – host callback via MMIO write (offset 0x10).
1213 LoopbackInOutAdd (single instance) – function service adding constant 11.
1214 ChannelTest (single instance) – ChannelService to_host and from_host loopback.
1215 MMIOAdd(add_amt) instances for add_amt in {4, 9, 14} – MMIO read returns offset + add_amt.
1216 ReadMem(width) for widths: 32, 64, 128, 256, 512, 534 – host memory read tests.
1217 WriteMem(width) for widths: 32, 64, 128, 256, 512, 534 – host memory write tests.
1218 ToHostDMATest(width) for widths: 32, 64, 128, 256, 512, 534 – DMA to host, cycle & count telemetry.
1219 FromHostDMATest(width) for widths: 32, 64, 128, 256, 512, 534 – DMA from host, cycle telemetry.
1220
1221 Width set used across Read/Write/DMA tests:
1222 widths = [32, 64, 128, 256, 512, 534]
1223
1224 Purpose:
1225 Aggregates all functional, MMIO, host memory, and DMA tests into one image
1226 for comprehensive accelerator validation and telemetry collection.
1227 """
1228
1229 clk = Clock()
1230 rst = Reset()
1231
1232 @generator
1233 def construct(ports):
1235 clk=ports.clk,
1236 rst=ports.rst,
1237 instance_name="cb_test",
1238 appid=AppID("cb_test"),
1239 )
1241 clk=ports.clk,
1242 rst=ports.rst,
1243 instance_name="loopback",
1244 appid=AppID("loopback"),
1245 )
1247 clk=ports.clk,
1248 rst=ports.rst,
1249 instance_name="channel_test",
1250 appid=AppID("channel_test"),
1251 )
1252 StreamingAdder(1)(
1253 clk=ports.clk,
1254 rst=ports.rst,
1255 instance_name="streaming_adder",
1256 appid=AppID("streaming_adder"),
1257 )
1259 clk=ports.clk,
1260 rst=ports.rst,
1261 instance_name="coord_translator",
1262 appid=AppID("coord_translator"),
1263 )
1265 clk=ports.clk,
1266 rst=ports.rst,
1267 instance_name="coord_translator_serial",
1268 appid=AppID("coord_translator_serial"),
1269 )
1271 clk=ports.clk,
1272 rst=ports.rst,
1273 instance_name="coord_translator_auto_serial",
1274 appid=AppID("coord_translator_auto_serial"),
1275 )
1276
1277 for i in range(4, 18, 5):
1278 MMIOAdd(i)(instance_name=f"mmio_add_{i}", appid=AppID("mmio_add", i))
1279
1280 for width in [32, 64, 128, 256, 512, 534]:
1281 ReadMem(width)(
1282 instance_name=f"readmem_{width}",
1283 appid=esi.AppID("readmem", width),
1284 clk=ports.clk,
1285 rst=ports.rst,
1286 )
1287 WriteMem(width)(
1288 instance_name=f"writemem_{width}",
1289 appid=AppID("writemem", width),
1290 clk=ports.clk,
1291 rst=ports.rst,
1292 )
1293 ToHostDMATest(width)(
1294 instance_name=f"tohostdma_{width}",
1295 appid=AppID("tohostdma", width),
1296 clk=ports.clk,
1297 rst=ports.rst,
1298 )
1299 FromHostDMATest(width)(
1300 instance_name=f"fromhostdma_{width}",
1301 appid=AppID("fromhostdma", width),
1302 clk=ports.clk,
1303 rst=ports.rst,
1304 )
1305
1306 for i in range(3):
1307 ReadMem(512)(
1308 instance_name=f"readmem_{i}",
1309 appid=esi.AppID(f"readmem_{i}", 512),
1310 clk=ports.clk,
1311 rst=ports.rst,
1312 )
1313 WriteMem(512)(
1314 instance_name=f"writemem_{i}",
1315 appid=AppID(f"writemem_{i}", 512),
1316 clk=ports.clk,
1317 rst=ports.rst,
1318 )
1319 ToHostDMATest(512)(
1320 instance_name=f"tohostdma_{i}",
1321 appid=AppID(f"tohostdma_{i}", 512),
1322 clk=ports.clk,
1323 rst=ports.rst,
1324 )
1325 FromHostDMATest(512)(
1326 instance_name=f"fromhostdma_{i}",
1327 appid=AppID(f"fromhostdma_{i}", 512),
1328 clk=ports.clk,
1329 rst=ports.rst,
1330 )
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))
FromHostDMATest(int width)
Type[Module] MMIOAdd(int add_amt)
Definition esitester.py:520
Type[Module] WriteMem(int width)
Definition esitester.py:806
ReadMem(int width)
Definition esitester.py:715
ToHostDMATest(int width)
Definition esitester.py:888
StreamingAdder(int numItems)
Definition esitester.py:89
AddressCommand(int width)
Definition esitester.py:550