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
424@modparams
425def MMIOAdd(add_amt: int) -> Type[Module]:
426
427 class MMIOAdd(Module):
428 """Exposes an MMIO address space wherein MMIO reads return the <address
429 offset into its space> + add_amt."""
430
431 metadata = Metadata(
432 name="MMIOAdd",
433 misc={"add_amt": add_amt},
434 )
435
436 add_amt_const = Constant(UInt(32), add_amt)
437
438 @generator
439 def build(ports):
440 mmio_read_bundle = esi.MMIO.read(appid=AppID("mmio_client", add_amt))
441
442 address_chan_wire = Wire(Channel(UInt(32)))
443 address, address_valid = address_chan_wire.unwrap(1)
444 response_data = (address.as_uint() + add_amt).as_bits(64)
445 response_chan, response_ready = Channel(Bits(64)).wrap(
446 response_data, address_valid)
447
448 address_chan = mmio_read_bundle.unpack(data=response_chan)["offset"]
449 address_chan_wire.assign(address_chan)
450
451 return MMIOAdd
452
453
454@modparams
455def AddressCommand(width: int):
456
457 class AddressCommand(Module):
458 """Constructs an module which takes MMIO commands and issues host memory
459 commands based on those commands. Tracks the number of cycles to issue
460 addresses and get all of the expected responses.
461
462 MMIO offsets:
463 0x10: Starting address for host memory operations.
464 0x18: Number of flits to read/write.
465 0x20: Start read/write operation.
466 """
467
468 clk = Clock()
469 rst = Reset()
470
471 # Number of flits left to issue.
472 flits_left = Output(UInt(64))
473 # Signal to start the operation.
474 command_go = Output(Bits(1))
475
476 # Channel which issues hostmem addresses. Must be transformed into
477 # read/write requests by the instantiator.
478 hostmem_cmd_address = OutputChannel(UInt(64))
479
480 # Channel which indicates when the read/write operation is done.
481 hostmem_cmd_done = InputChannel(Bits(0))
482
483 @generator
484 def construct(ports):
485 # MMIO command channel setup.
486 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
487 resp_ready_wire = Wire(Bits(1))
488 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
489 mmio_xact = cmd_valid & resp_ready_wire
490
491 # Write enables.
492 start_addr_we = (mmio_xact & cmd.write & (cmd.offset == UInt(32)(0x10)))
493 flits_we = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0x18))
494 start_op_we = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0x20))
495 ports.command_go = start_op_we
496
497 # Registers for start address and number of flits.
498 start_addr = cmd.data.as_uint().reg(
499 clk=ports.clk,
500 rst=ports.rst,
501 rst_value=0,
502 ce=start_addr_we,
503 name="start_addr",
504 )
505 flits_total = cmd.data.as_uint().reg(
506 clk=ports.clk,
507 rst=ports.rst,
508 rst_value=0,
509 ce=flits_we,
510 name="flits_total",
511 )
512
513 # Response counter.
514 responses_incr = Wire(Bits(1))
515 responses_cnt = Counter(64)(
516 clk=ports.clk,
517 rst=ports.rst,
518 clear=start_op_we,
519 increment=responses_incr,
520 instance_name="addr_cmd_responses_cnt",
521 )
522
523 operation_done = responses_cnt.out.as_uint() == flits_total
524 operation_active = ControlReg(
525 clk=ports.clk,
526 rst=ports.rst,
527 asserts=[start_op_we],
528 resets=[operation_done],
529 name="operation_active",
530 )
531 # Cycle counter while active.
532 cycles_cnt = Counter(64)(
533 clk=ports.clk,
534 rst=ports.rst,
535 clear=start_op_we,
536 increment=operation_active,
537 instance_name="addr_cmd_cycle_counter",
538 )
539 # Latch final cycle count at completion.
540 final_cycles = Reg(
541 UInt(64),
542 clk=ports.clk,
543 rst=ports.rst,
544 rst_value=0,
545 ce=operation_done,
546 name="addr_cmd_cycles",
547 )
548 final_cycles.assign(cycles_cnt.out.as_uint())
549
550 # Issue counter.
551 issue_incr = Wire(Bits(1))
552 issue_cnt = Counter(64)(
553 clk=ports.clk,
554 rst=ports.rst,
555 clear=start_op_we,
556 increment=issue_incr,
557 )
558
559 # Increment by number of bytes per flit, rounded up to nearest 32
560 # bits (double word).
561 incr_bytes = UInt(64)(((width + 31) // 32) * 4)
562
563 # Generate current address.
564 current_addr = (start_addr +
565 (issue_cnt.out.as_uint() * incr_bytes)).as_uint(64)
566
567 # Valid when active and still have flits to issue.
568 addr_valid = operation_active & (issue_cnt.out.as_uint() < flits_total)
569 addr_chan, addr_ready = Channel(UInt(64),
570 ChannelSignaling.ValidReady).wrap(
571 current_addr, addr_valid)
572 issue_xact = addr_valid & addr_ready
573 issue_incr.assign(issue_xact)
574
575 # Consume hostmem_cmd_done (Bits(0) channel) for completed responses.
576 _, done_valid = ports.hostmem_cmd_done.unwrap(Bits(1)(1))
577 responses_incr.assign(done_valid)
578
579 # flits_left = total - responses received.
580 flits_left_val = (flits_total - responses_cnt.out.as_uint()).as_uint(64)
581 ports.flits_left = flits_left_val # direct assignment
582
583 # Drive output channel.
584 ports.hostmem_cmd_address = addr_chan # direct assignment
585
586 # MMIO read response: return flits_left.
587 response_data = flits_left_val.as_bits(64)
588 response_chan, response_ready = Channel(Bits(64)).wrap(
589 response_data, cmd_valid)
590 resp_ready_wire.assign(response_ready)
591
592 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd", width))
593 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
594 cmd_chan_wire.assign(mmio_rw_cmd_chan)
595
596 # Report telemetry.
597 esi.Telemetry.report_signal(
598 ports.clk,
599 ports.rst,
600 esi.AppID("addrCmdCycles"),
601 final_cycles,
602 )
603 esi.Telemetry.report_signal(
604 ports.clk,
605 ports.rst,
606 esi.AppID("addrCmdIssued"),
607 issue_cnt.out,
608 )
609 esi.Telemetry.report_signal(
610 ports.clk,
611 ports.rst,
612 esi.AppID("addrCmdResponses"),
613 responses_cnt.out,
614 )
615
616 return AddressCommand
617
618
619@modparams
620def ReadMem(width: int):
621
622 class ReadMem(Module):
623 """Host memory read test module.
624
625 Function:
626 Issues a sequence of host memory read requests using an internal
627 address/control submodule which is configured via MMIO writes. Each
628 read returns 'width' bits; the low 64 bits of the most recent
629 response are latched and exported as telemetry (lastReadLSB).
630
631 Flit width:
632 'width' is the number of payload data bits per read flit. The address
633 stride between successive requests is ceil(width/32) 32-bit words
634 (= ceil(width/32) * 4 bytes). Non–power‑of‑two widths are supported
635 and packed into the minimum whole 32‑bit word count.
636
637 MMIO command interface:
638 0x10 Write: Starting base address for the read operation.
639 0x18 Write: Number of flits (read transactions) to perform.
640 0x20 Write: Start the operation (assert once to launch).
641 Reads return the current flits_left (remaining responses).
642
643 Operation:
644 After 0x20 is written, sequential addresses are generated:
645 addr = start_addr + i * ceil(width/32) (i = 0 .. flits-1)
646 Each address produces one host memory read request.
647
648 Telemetry (AppID -> signal):
649 addrCmdCycles Total cycles elapsed during the active window.
650 addrCmdIssued Count of host memory commands issued.
651 addrCmdResponses Count of host memory responses received.
652 lastReadLSB Low 64 bits of the most recently received read data.
653
654 Notes:
655 Backpressure on the read response channel naturally throttles issue.
656 Completion occurs when responses == requested flits.
657 """
658
659 clk = Clock()
660 rst = Reset()
661
662 width_bits = Constant(UInt(32), width)
663
664 @generator
665 def construct(ports):
666 clk = ports.clk
667 rst = ports.rst
668
669 address_cmd_resp = Wire(Channel(Bits(0)))
670 addresses = AddressCommand(width)(
671 clk=clk,
672 rst=rst,
673 hostmem_cmd_done=address_cmd_resp,
674 instance_name="address_command",
675 )
676
677 read_cmd_chan = addresses.hostmem_cmd_address.transform(
678 lambda addr: esi.HostMem.ReadReqType({
679 "tag": UInt(8)(0),
680 "address": addr
681 }))
682 read_responses = esi.HostMem.read(
683 appid=AppID("host"),
684 data_type=Bits(width),
685 req=read_cmd_chan,
686 )
687 # Signal completion to AddressCommand (each response -> Bits(0)).
688 address_cmd_resp.assign(read_responses.transform(lambda resp: Bits(0)(0)))
689 # Snoop the response channel to capture the low 64 bits without consuming it.
690 read_resp_valid, _, read_resp_data = read_responses.snoop()
691 last_read_lsb = Reg(
692 UInt(64),
693 clk=ports.clk,
694 rst=ports.rst,
695 rst_value=0,
696 ce=read_resp_valid,
697 name="last_read_lsb",
698 )
699 last_read_lsb.assign(read_resp_data.data.as_uint(64))
700 esi.Telemetry.report_signal(
701 ports.clk,
702 ports.rst,
703 esi.AppID("lastReadLSB"),
704 last_read_lsb,
705 )
706
707 return ReadMem
708
709
710@modparams
711def WriteMem(width: int) -> Type[Module]:
712
713 class WriteMem(Module):
714 """Host memory write test module.
715
716 Function:
717 Issues sequential host memory write requests produced by an internal
718 address/control submodule configured via MMIO writes. Data for each
719 flit is the current free‑running 32‑bit cycle counter value
720 (zero‑extended or truncated to 'width').
721
722 Flit width:
723 'width' is the number of payload data bits per write flit. The address
724 stride between successive writes is ceil(width/32) 32‑bit words
725 (= ceil(width/32) * 4 bytes). Wider payloads span multiple words;
726 narrower payloads still consume one word of address space per flit.
727
728 MMIO command interface:
729 0x10 Write: Starting base address for the write operation.
730 0x18 Write: Number of flits (write transactions) to perform.
731 0x20 Write: Start the operation (assert once to launch).
732 Reads return the current flits_left (remaining responses).
733
734 Data pattern:
735 data[i] = cycle_counter sampled when the write command for flit i is formed.
736
737 Telemetry (AppID -> signal):
738 addrCmdCycles Total cycles elapsed during the active window.
739 addrCmdIssued Count of host memory commands issued.
740 addrCmdResponses Count of host memory responses received.
741
742 Notes:
743 No additional telemetry beyond the above signals is generated here.
744 Completion occurs when write responses == requested flits.
745 """
746
747 clk = Clock()
748 rst = Reset()
749
750 width_bits = Constant(UInt(32), width)
751
752 @generator
753 def construct(ports):
754 clk = ports.clk
755 rst = ports.rst
756
757 cycle_counter_reset = Wire(Bits(1))
758 cycle_counter = Counter(32)(
759 clk=clk,
760 rst=rst,
761 clear=Bits(1)(0),
762 increment=Bits(1)(1),
763 )
764
765 address_cmd_resp = Wire(Channel(Bits(0)))
766 addresses = AddressCommand(width)(
767 clk=clk,
768 rst=rst,
769 hostmem_cmd_done=address_cmd_resp,
770 instance_name="address_command",
771 )
772 cycle_counter_reset.assign(addresses.command_go)
773
774 write_cmd_chan = addresses.hostmem_cmd_address.transform(
775 lambda addr: esi.HostMem.write_req_channel_type(UInt(width))({
776 "tag": UInt(8)(0),
777 "address": addr,
778 "data": cycle_counter.out.as_uint(width),
779 }))
780
781 write_responses = esi.HostMem.write(
782 appid=AppID("host"),
783 req=write_cmd_chan,
784 )
785 # Signal completion to AddressCommand (each response -> Bits(0)).
786 address_cmd_resp.assign(
787 write_responses.transform(lambda resp: Bits(0)(0)))
788
789 return WriteMem
790
791
792@modparams
793def ToHostDMATest(width: int):
794 """Construct a module that sends the write count over a channel to the host
795 the specified number of times. Exercises any DMA engine."""
796
797 class ToHostDMATest(Module):
798 """Transmit a 32-bit cycle counter value to the host a programmed number of times.
799
800 Functionality:
801 A free-running 32-bit counter advances only on successful channel
802 handshakes. A write to MMIO offset 0x0 programs 'write_count' (number
803 of messages to send). Each message’s payload is the counter value
804 constrained to 'width':
805 width < 32 -> lower 'width' bits (truncated)
806 width >= 32 -> zero-extended to 'width' bits
807 One message is emitted per handshake until 'write_count' messages have
808 been transferred. A new write to 0x0 re-arms after completion.
809
810 Width:
811 Selects how the 32-bit counter is represented on the output channel
812 (truncate or zero-extend as above).
813
814 MMIO command interface:
815 0x0 Write: Set write_count (messages to transmit). Starts a new
816 sequence if idle/completed.
817 0x0 Read: Returns constant 0.
818
819 Telemetry:
820 totalWrites (AppID "totalWrites"): Count of messages successfully sent.
821 toHostCycles (AppID "toHostCycles"): Cycle count from write_count programming
822 (start) through completion (inclusive of active cycles).
823
824 Notes:
825 Backpressure governs pacing. Completion when totalWrites == write_count.
826 """
827
828 clk = Clock()
829 rst = Reset()
830
831 width_bits = Constant(UInt(32), width)
832
833 @generator
834 def construct(ports):
835 count_reached = Wire(Bits(1))
836 count_valid = Wire(Bits(1))
837 out_xact = Wire(Bits(1))
838 cycle_counter = Counter(32)(
839 clk=ports.clk,
840 rst=ports.rst,
841 clear=Bits(1)(0),
842 increment=out_xact,
843 )
844
845 write_cntr_incr = ~count_reached & count_valid & out_xact
846 write_counter = Counter(32)(
847 clk=ports.clk,
848 rst=ports.rst,
849 clear=count_reached,
850 increment=write_cntr_incr,
851 )
852 num_writes = write_counter.out
853
854 # Get the MMIO space for commands.
855 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
856 resp_ready_wire = Wire(Bits(1))
857 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
858 mmio_xact = cmd_valid & resp_ready_wire
859 response_data = Bits(64)(0)
860 response_chan, response_ready = Channel(response_data.type).wrap(
861 response_data, cmd_valid)
862 resp_ready_wire.assign(response_ready)
863
864 # write_count is the specified number of times to send the cycle count.
865 write_count_ce = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0))
866 write_count = cmd.data.as_uint().reg(clk=ports.clk,
867 rst=ports.rst,
868 rst_value=0,
869 ce=write_count_ce)
870 count_reached.assign(num_writes == write_count)
871 count_valid.assign(
872 ControlReg(
873 clk=ports.clk,
874 rst=ports.rst,
875 asserts=[write_count_ce],
876 resets=[count_reached],
877 ))
878
879 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd"))
880 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
881 cmd_chan_wire.assign(mmio_rw_cmd_chan)
882
883 # Output channel.
884 out_channel, out_channel_ready = Channel(UInt(width)).wrap(
885 cycle_counter.out.as_uint(width), count_valid)
886 out_xact.assign(out_channel_ready & count_valid)
887 esi.ChannelService.to_host(name=AppID("out"), chan=out_channel)
888
889 total_write_counter = Counter(64)(
890 clk=ports.clk,
891 rst=ports.rst,
892 clear=Bits(1)(0),
893 increment=write_cntr_incr,
894 )
895 esi.Telemetry.report_signal(
896 ports.clk,
897 ports.rst,
898 esi.AppID("totalWrites"),
899 total_write_counter.out,
900 )
901
902 # Cycle telemetry: count cycles while sequence active.
903 tohost_cycle_cnt = Counter(64)(
904 clk=ports.clk,
905 rst=ports.rst,
906 clear=write_count_ce,
907 increment=count_valid,
908 instance_name="tohost_cycle_counter",
909 )
910 tohost_final_cycles = Reg(
911 UInt(64),
912 clk=ports.clk,
913 rst=ports.rst,
914 rst_value=0,
915 ce=count_reached,
916 name="tohost_cycles",
917 )
918 tohost_final_cycles.assign(tohost_cycle_cnt.out.as_uint())
919 esi.Telemetry.report_signal(
920 ports.clk,
921 ports.rst,
922 esi.AppID("toHostCycles"),
923 tohost_final_cycles,
924 )
925
926 return ToHostDMATest
927
928
929@modparams
930def FromHostDMATest(width: int):
931 """Construct a module that receives the write count over a channel from the
932 host the specified number of times. Exercises any DMA engine."""
933
934 class FromHostDMATest(Module):
935 """Receive test data from the host a programmed number of times.
936
937 Functionality:
938 A write to MMIO offset 0x0 programs 'read_count', the number of messages
939 to accept from the host. The input channel (AppID "in") is marked ready
940 while the number of received messages is less than 'read_count'. Each
941 received width-bit payload is latched; the most recent value is exposed
942 on MMIO reads.
943
944 Width:
945 'width' is the payload bit width of each received message. The latched
946 value is widened/truncated to 64 bits for MMIO read-back (lower 64 bits
947 if width > 64).
948
949 MMIO command interface:
950 0x0 Write: Set read_count (number of messages to receive). Clears the
951 internal receive counter.
952 0x0 Read: Returns the last received value (Bits(64), derived from the
953 width-bit payload).
954
955 Telemetry:
956 fromHostCycles (AppID "fromHostCycles"): Cycle count from read_count programming
957 (start) through completion of the programmed receive sequence.
958
959 Notes:
960 Completion is when received messages == programmed read_count; another
961 write to 0x0 re-arms for a new sequence.
962 """
963
964 clk = Clock()
965 rst = Reset()
966
967 width_bits = Constant(UInt(32), width)
968
969 @generator
970 def build(ports):
971 last_read = Wire(UInt(width))
972
973 # Get the MMIO space for commands.
974 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
975 resp_ready_wire = Wire(Bits(1))
976 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
977 mmio_xact = cmd_valid & resp_ready_wire
978 response_data = last_read.as_bits(64)
979 response_chan, response_ready = Channel(response_data.type).wrap(
980 response_data, cmd_valid)
981 resp_ready_wire.assign(response_ready)
982
983 # read_count is the specified number of times to recieve data.
984 read_count_ce = mmio_xact & cmd.write & (cmd.offset == UInt(32)(0))
985 read_count = cmd.data.as_uint().reg(clk=ports.clk,
986 rst=ports.rst,
987 rst_value=0,
988 ce=read_count_ce)
989 in_data_xact = NamedWire(Bits(1), "in_data_xact")
990 read_counter = Counter(32)(
991 clk=ports.clk,
992 rst=ports.rst,
993 clear=read_count_ce,
994 increment=in_data_xact,
995 )
996
997 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd"))
998 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
999 cmd_chan_wire.assign(mmio_rw_cmd_chan)
1000
1001 in_chan = esi.ChannelService.from_host(name=AppID("in"), type=UInt(width))
1002 in_ready = NamedWire(read_counter.out < read_count, "in_ready")
1003 in_data, in_valid = in_chan.unwrap(in_ready)
1004 NamedWire(in_data, "in_data")
1005 in_data_xact.assign(in_valid & in_ready)
1006
1007 last_read.assign(
1008 in_data.reg(
1009 clk=ports.clk,
1010 rst=ports.rst,
1011 ce=in_data_xact,
1012 name="last_read",
1013 ))
1014
1015 # Cycle telemetry: detect completion and count active cycles.
1016 fromhost_count_reached = Wire(Bits(1))
1017 fromhost_count_reached.assign(read_counter.out == read_count)
1018 fromhost_cycle_valid = ControlReg(
1019 clk=ports.clk,
1020 rst=ports.rst,
1021 asserts=[read_count_ce],
1022 resets=[fromhost_count_reached],
1023 name="fromhost_cycle_active",
1024 )
1025 fromhost_cycle_cnt = Counter(64)(
1026 clk=ports.clk,
1027 rst=ports.rst,
1028 clear=read_count_ce,
1029 increment=fromhost_cycle_valid,
1030 instance_name="fromhost_cycle_counter",
1031 )
1032 fromhost_final_cycles = Reg(
1033 UInt(64),
1034 clk=ports.clk,
1035 rst=ports.rst,
1036 rst_value=0,
1037 ce=fromhost_count_reached,
1038 name="fromhost_cycles",
1039 )
1040 fromhost_final_cycles.assign(fromhost_cycle_cnt.out.as_uint())
1041 esi.Telemetry.report_signal(
1042 ports.clk,
1043 ports.rst,
1044 esi.AppID("fromHostCycles"),
1045 fromhost_final_cycles,
1046 )
1047
1048 return FromHostDMATest
1049
1050
1051class ChannelTest(Module):
1052 """Test the ChannelService with a to_host producer and a from_host loopback.
1053
1054 The 'producer' to_host port sends incrementing UInt(32) values. The number
1055 of values to send is specified via an MMIO write to offset 0x0. Reading MMIO
1056 returns the remaining count.
1057
1058 The 'loopback_in'/'loopback_out' pair forwards from_host data back to_host."""
1059
1060 clk = Clock()
1061 rst = Reset()
1062
1063 @generator
1064 def construct(ports):
1065 clk = ports.clk
1066 rst = ports.rst
1067
1068 # MMIO interface for triggering the producer.
1069 cmd_chan_wire = Wire(Channel(esi.MMIOReadWriteCmdType))
1070
1071 # State: remaining count and current value.
1072 remaining = Reg(UInt(32), clk=clk, rst=rst, rst_value=0)
1073 cur_value = Reg(UInt(32), clk=clk, rst=rst, rst_value=0)
1074
1075 # Handle MMIO commands.
1076 cmd_ready = Wire(Bits(1))
1077 cmd, cmd_valid = cmd_chan_wire.unwrap(cmd_ready)
1078 is_write = cmd.write & cmd_valid
1079 # On write to offset 0x0, load the count and reset the current value.
1080 load_count = is_write & (cmd.offset == UInt(32)(0))
1081
1082 # to_host: send incrementing values while remaining > 0.
1083 has_data = remaining != UInt(32)(0)
1084 data_chan, data_ready = Channel(UInt(32)).wrap(cur_value, has_data)
1085 sent = data_ready & has_data
1086
1087 # Compute next state: load from MMIO takes priority, then decrement on send.
1088 next_remaining = Mux(
1089 load_count, Mux(sent, remaining, (remaining - UInt(32)(1)).as_uint(32)),
1090 cmd.data.as_uint(32))
1091 next_cur_value = Mux(
1092 load_count, Mux(sent, cur_value, (cur_value + UInt(32)(1)).as_uint(32)),
1093 UInt(32)(0))
1094 remaining.assign(next_remaining)
1095 cur_value.assign(next_cur_value)
1096
1097 # MMIO read response: return remaining count.
1098 response_chan, response_ready = Channel(Bits(64)).wrap(
1099 remaining.as_bits(64), cmd_valid)
1100 cmd_ready.assign(response_ready)
1101
1102 mmio_rw = esi.MMIO.read_write(appid=AppID("cmd"))
1103 mmio_rw_cmd_chan = mmio_rw.unpack(data=response_chan)["cmd"]
1104 cmd_chan_wire.assign(mmio_rw_cmd_chan)
1105
1106 esi.ChannelService.to_host(AppID("producer"), data_chan)
1107
1108 # from_host -> to_host loopback.
1109 loopback_in = esi.ChannelService.from_host(AppID("loopback_in"), UInt(32))
1110 esi.ChannelService.to_host(AppID("loopback_out"), loopback_in)
1111
1112
1113class EsiTester(Module):
1114 """Top-level ESI test harness module.
1115
1116 Contains submodules:
1117 CallbackTest (single instance) – host callback via MMIO write (offset 0x10).
1118 LoopbackInOutAdd (single instance) – function service adding constant 11.
1119 ChannelTest (single instance) – ChannelService to_host and from_host loopback.
1120 MMIOAdd(add_amt) instances for add_amt in {4, 9, 14} – MMIO read returns offset + add_amt.
1121 ReadMem(width) for widths: 32, 64, 128, 256, 512, 534 – host memory read tests.
1122 WriteMem(width) for widths: 32, 64, 128, 256, 512, 534 – host memory write tests.
1123 ToHostDMATest(width) for widths: 32, 64, 128, 256, 512, 534 – DMA to host, cycle & count telemetry.
1124 FromHostDMATest(width) for widths: 32, 64, 128, 256, 512, 534 – DMA from host, cycle telemetry.
1125
1126 Width set used across Read/Write/DMA tests:
1127 widths = [32, 64, 128, 256, 512, 534]
1128
1129 Purpose:
1130 Aggregates all functional, MMIO, host memory, and DMA tests into one image
1131 for comprehensive accelerator validation and telemetry collection.
1132 """
1133
1134 clk = Clock()
1135 rst = Reset()
1136
1137 @generator
1138 def construct(ports):
1140 clk=ports.clk,
1141 rst=ports.rst,
1142 instance_name="cb_test",
1143 appid=AppID("cb_test"),
1144 )
1146 clk=ports.clk,
1147 rst=ports.rst,
1148 instance_name="loopback",
1149 appid=AppID("loopback"),
1150 )
1152 clk=ports.clk,
1153 rst=ports.rst,
1154 instance_name="channel_test",
1155 appid=AppID("channel_test"),
1156 )
1157 StreamingAdder(1)(
1158 clk=ports.clk,
1159 rst=ports.rst,
1160 instance_name="streaming_adder",
1161 appid=AppID("streaming_adder"),
1162 )
1164 clk=ports.clk,
1165 rst=ports.rst,
1166 instance_name="coord_translator",
1167 appid=AppID("coord_translator"),
1168 )
1170 clk=ports.clk,
1171 rst=ports.rst,
1172 instance_name="coord_translator_serial",
1173 appid=AppID("coord_translator_serial"),
1174 )
1175
1176 for i in range(4, 18, 5):
1177 MMIOAdd(i)(instance_name=f"mmio_add_{i}", appid=AppID("mmio_add", i))
1178
1179 for width in [32, 64, 128, 256, 512, 534]:
1180 ReadMem(width)(
1181 instance_name=f"readmem_{width}",
1182 appid=esi.AppID("readmem", width),
1183 clk=ports.clk,
1184 rst=ports.rst,
1185 )
1186 WriteMem(width)(
1187 instance_name=f"writemem_{width}",
1188 appid=AppID("writemem", width),
1189 clk=ports.clk,
1190 rst=ports.rst,
1191 )
1192 ToHostDMATest(width)(
1193 instance_name=f"tohostdma_{width}",
1194 appid=AppID("tohostdma", width),
1195 clk=ports.clk,
1196 rst=ports.rst,
1197 )
1198 FromHostDMATest(width)(
1199 instance_name=f"fromhostdma_{width}",
1200 appid=AppID("fromhostdma", width),
1201 clk=ports.clk,
1202 rst=ports.rst,
1203 )
1204
1205 for i in range(3):
1206 ReadMem(512)(
1207 instance_name=f"readmem_{i}",
1208 appid=esi.AppID(f"readmem_{i}", 512),
1209 clk=ports.clk,
1210 rst=ports.rst,
1211 )
1212 WriteMem(512)(
1213 instance_name=f"writemem_{i}",
1214 appid=AppID(f"writemem_{i}", 512),
1215 clk=ports.clk,
1216 rst=ports.rst,
1217 )
1218 ToHostDMATest(512)(
1219 instance_name=f"tohostdma_{i}",
1220 appid=AppID(f"tohostdma_{i}", 512),
1221 clk=ports.clk,
1222 rst=ports.rst,
1223 )
1224 FromHostDMATest(512)(
1225 instance_name=f"fromhostdma_{i}",
1226 appid=AppID(f"fromhostdma_{i}", 512),
1227 clk=ports.clk,
1228 rst=ports.rst,
1229 )
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))
FromHostDMATest(int width)
Definition esitester.py:930
Type[Module] MMIOAdd(int add_amt)
Definition esitester.py:425
Type[Module] WriteMem(int width)
Definition esitester.py:711
ReadMem(int width)
Definition esitester.py:620
ToHostDMATest(int width)
Definition esitester.py:793
StreamingAdder(int numItems)
Definition esitester.py:89
AddressCommand(int width)
Definition esitester.py:455