CIRCT 23.0.0git
Loading...
Searching...
No Matches
test_codegen.py
Go to the documentation of this file.
1"""Hardware design for the codegen + port-kind coverage integration test.
2
3Where ``serialization_probes.py`` exercises wire-format invariants, this design
4exercises the *port-kind* surface area of the ESI runtime + facade codegen.
5Each probe module here is named for the codegen / runtime path it exercises
6so a regression in any single path lights up exactly one driver assertion.
7"""
8
9import sys
10
11import pycde.esi as esi
12from pycde import AppID, Clock, Module, Reset, System, generator
13from esiaccel.bsp import get_bsp
14from pycde.common import Constant
15from pycde.constructs import ControlReg, Counter, Reg, Wire
16from pycde.esi import ListWindowToParallel, ListWindowToSerial
17from pycde.signals import Struct
18from pycde.types import (Array, Bits, Bundle, BundledChannel, Channel,
19 ChannelDirection, ChannelSignaling, List, SInt,
20 StructType, TypeAlias, UInt, Window)
21
22# Custom service declarations for the custom-`@esi.ServiceDecl` raw-channel
23# probe. Provides both Bits(8) and Bits(0) (void) channel widths so the
24# zero-bit elaboration path is exercised even though the C++ driver only
25# drives the byte path today.
26SendI8 = Bundle([BundledChannel("send", ChannelDirection.FROM, Bits(8))])
27RecvI8 = Bundle([BundledChannel("recv", ChannelDirection.TO, Bits(8))])
28SendI0 = Bundle([BundledChannel("send", ChannelDirection.FROM, Bits(0))])
29RecvI0 = Bundle([BundledChannel("recv", ChannelDirection.TO, Bits(0))])
30
31
32@esi.ServiceDecl
34 Send = SendI8
35 Recv = RecvI8
36
37
38@esi.ServiceDecl
40 Send = SendI0
41 Recv = RecvI0
42
43
44class TypedFuncMultiArg(Module):
45 """Typed function with a multi-field argument struct.
46
47 Exercises ``TypedFunction``'s emplace-style ``call(...)`` overload, which
48 forwards its arguments into the generated arg struct's constructor so the
49 C++ driver can call ``connected->call(a, b)`` instead of building the
50 struct itself. The body computes ``a * b`` and returns it as ``ui32``.
51 """
52
53 clk = Clock()
54 rst = Reset()
55
56 @generator
57 def construct(ports):
58 result_wire = Wire(Channel(UInt(32)))
59
60 class Args(Struct):
61 a: UInt(32)
62 b: UInt(32)
63
64 args = esi.FuncService.get_call_chans(AppID("call"),
65 arg_type=Args,
66 result=result_wire)
67 ready = Wire(Bits(1))
68 arg, valid = args.unwrap(ready)
69 product = (arg["a"] * arg["b"]).as_uint(32)
70 out_chan, out_ready = Channel(UInt(32)).wrap(product, valid)
71 ready.assign(out_ready)
72 result_wire.assign(out_chan)
73
74
75class TypedFuncVoidArg(Module):
76 """Typed function with a void argument (typed-result specialization).
77
78 The C++ driver invokes ``connected->call().get()`` and asserts the constant
79 token comes back. Hardware sends ``0xCAFEF00D`` on every call so a wrong-
80 byte-order bug in the result path fails distinguishably.
81 """
82
83 clk = Clock()
84 rst = Reset()
85
86 @generator
87 def construct(ports):
88 result_wire = Wire(Channel(UInt(32)))
89 args = esi.FuncService.get_call_chans(AppID("call"),
90 arg_type=Bits(0),
91 result=result_wire)
92 ready = Wire(Bits(1))
93 _, valid = args.unwrap(ready)
94 token = UInt(32)(0xCAFEF00D)
95 out_chan, out_ready = Channel(UInt(32)).wrap(token, valid)
96 ready.assign(out_ready)
97 result_wire.assign(out_chan)
98
99
100class AckArgs(Struct):
101 tag: UInt(8)
102 seq: UInt(16)
103
104
106 """Typed function with a void result (typed-arg specialization).
107
108 Hardware accepts the request and returns a one-byte zero (the void-result
109 wire encoding). No state is observable other than that the call completes;
110 the test asserts the future resolves without throwing.
111 """
112
113 clk = Clock()
114 rst = Reset()
115
116 @generator
117 def construct(ports):
118 result_wire = Wire(Channel(Bits(0)))
119 args = esi.FuncService.get_call_chans(AppID("call"),
120 arg_type=AckArgs,
121 result=result_wire)
122 ready = Wire(Bits(1))
123 _, valid = args.unwrap(ready)
124 out_chan, out_ready = Channel(Bits(0)).wrap(Bits(0)(0), valid)
125 ready.assign(out_ready)
126 result_wire.assign(out_chan)
127
128
129class NotifyArgs(Struct):
130 tag: UInt(8)
131 payload: UInt(32)
132
133
135 """Hardware-initiated call into the host via `CallService`.
136
137 The trigger is an MMIO write at offset ``0x10`` (whose write data forms
138 the ``payload``) so the driver can deterministically time when the
139 callback fires. The callback returns no payload (``Bits(0)`` is the void
140 encoding); the host-side handler increments a counter so the driver can
141 assert it actually ran.
142 """
143
144 clk = Clock()
145 rst = Reset()
146
147 @generator
148 def construct(ports):
149 clk = ports.clk
150 rst = ports.rst
151
152 mmio_bundle = esi.MMIO.read_write(appid=AppID("trigger"))
153 data_resp_chan = Wire(Channel(Bits(64)))
154 cmd_chan = mmio_bundle.unpack(data=data_resp_chan)["cmd"]
155
156 # Snoop the cmd-channel handshake (xact = valid & ready) combinationally
157 # without consuming it. The echo response below drives the actual
158 # handshake; back-pressure on the CallService never reaches the MMIO bus
159 # because the latch overwrites in place.
160 xact, cmd = cmd_chan.snoop_xact()
161
162 # Echo write data back as the read response.
163 data_resp_chan.assign(cmd_chan.transform(lambda c: Bits(64)(c.data)))
164
165 # trigger_xact: a cmd was accepted this cycle AND its offset is 0x10.
166 # Use it both as the clock enable for the payload latch and as the
167 # assert for the callback's valid flag.
168 is_trigger = (cmd.offset == UInt(32)(0x10))
169 trigger_xact = xact & is_trigger
170
171 # A new trigger overwrites any previous-but-unconsumed payload
172 # (one-outstanding semantics, same as a Mailbox).
173 data_reg = cmd.data.as_uint(32).reg(clk, rst, ce=trigger_xact)
174 notify_args = NotifyArgs(tag=UInt(8)(0xA5), payload=data_reg)
175
176 # ControlReg holds the callback's valid bit: assert on trigger_xact,
177 # clear when the consumer takes the message. ControlReg gives asserts
178 # priority on same-cycle ties, so a new trigger landing exactly on the
179 # consumption cycle correctly keeps valid asserted for the next message.
180 cb_consumed = Wire(Bits(1))
181 cb_valid = ControlReg(clk,
182 rst,
183 asserts=[trigger_xact],
184 resets=[cb_consumed])
185 cb_chan, cb_ready = Channel(NotifyArgs).wrap(notify_args, cb_valid)
186 cb_consumed.assign(cb_valid & cb_ready)
187 esi.CallService.call(AppID("callback"), cb_chan, Bits(0))
188
189
190class EventStruct(Struct):
191 ts: UInt(64)
192 val: SInt(32)
193
194
196 """To-host channel of `EventStruct`: TypedReadPort polling of a struct.
197
198 Hardware pushes a small bounded sequence of distinct events on reset
199 release so the driver can read N items and check exact values. Each event
200 has ``ts = i+1`` and ``val = -(i+1)`` so an off-by-one or sign bug shows
201 up immediately.
202 """
203
204 clk = Clock()
205 rst = Reset()
206
207 num_events = Constant(UInt(8), 4)
208
209 @generator
210 def construct(ports):
211 clk = ports.clk
212 rst = ports.rst
213
214 # One counter that advances exactly when the to-host channel handshakes
215 # (valid & ready). The valid signal is ``count < num_events`` so the
216 # stream goes silent after num_events have been delivered.
217 ready_wire = Wire(Bits(1))
218 increment_wire = Wire(Bits(1))
219 counter = Counter(8)(clk=clk,
220 rst=rst,
221 clear=Bits(1)(0),
222 increment=increment_wire,
223 instance_name="event_counter")
224 valid = counter.out < TypedReadChannelStruct.num_events.value
225 increment_wire.assign(valid & ready_wire)
226
227 one_based = (counter.out + UInt(8)(1)).as_uint(8)
228 ts = one_based.as_uint(64)
229 # val = -ts as si32 -- small enough to fit and reveals sign-extension
230 # bugs on the host side.
231 one_based_u32 = one_based.as_uint(32)
232 neg = (UInt(32)(0) - one_based_u32).as_sint(32)
233 event = EventStruct(ts=ts, val=neg)
234 out_chan, out_ready = Channel(EventStruct).wrap(event, valid)
235 ready_wire.assign(out_ready)
236 esi.ChannelService.to_host(AppID("data"), out_chan)
237
238
240 """From-host channel of ``ui8``: TypedWritePort + MMIO accumulator readback.
241
242 Hardware accepts every byte and XORs each one into a running register; the
243 latest XOR-accumulator value is exposed via the ``accumulator`` MMIO read
244 region so the driver can assert what it sent actually arrived. An always-
245 ready receiver is fine here because the test sends a small known sequence.
246 """
247
248 clk = Clock()
249 rst = Reset()
250
251 @generator
252 def construct(ports):
253 clk = ports.clk
254 rst = ports.rst
255
256 chan = esi.ChannelService.from_host(AppID("data"), Bits(8))
257 always_ready = Bits(1)(1)
258 data, valid = chan.unwrap(always_ready)
259
260 acc = Reg(Bits(8), clk=clk, rst=rst, rst_value=0, ce=valid, name="cmds_acc")
261 acc.assign(acc ^ data)
262
263 # Expose the accumulator on MMIO read so the driver can verify what
264 # arrived without needing an answer channel.
265 mmio_bundle = esi.MMIO.read(appid=AppID("accumulator"))
266 resp_chan = Wire(Channel(Bits(64)))
267 addr_chan = mmio_bundle.unpack(data=resp_chan)["offset"]
268 resp_chan.assign(addr_chan.transform(lambda _: acc.as_bits(64)))
269
270
271class MmioReadWrite(Module):
272 """MMIO read/write region that loops back the most recent write.
273
274 The test writes a value to offset 0x10 and reads it back; whatever was
275 last written at any 8-byte-aligned offset is what the read returns. Stores all
276 writes for simplicity.
277 """
278
279 clk = Clock()
280 rst = Reset()
281
282 @generator
283 def construct(ports):
284 clk = ports.clk
285 rst = ports.rst
286
287 mmio_bundle = esi.MMIO.read_write(appid=AppID("region"))
288 resp_chan = Wire(Channel(Bits(64)))
289 cmd_chan = mmio_bundle.unpack(data=resp_chan)["cmd"]
290
291 cmd_ready = Wire(Bits(1))
292 cmd, cmd_valid = cmd_chan.unwrap(cmd_ready)
293
294 write_handshake = cmd_valid & cmd.write
295 storage = Reg(Bits(64),
296 clk=clk,
297 rst=rst,
298 rst_value=0,
299 ce=write_handshake,
300 name="regs_storage")
301 storage.assign(cmd.data)
302
303 # Reads always echo the most-recently-written value.
304 response, resp_ready = Channel(Bits(64)).wrap(storage, cmd_valid)
305 cmd_ready.assign(resp_ready)
306 resp_chan.assign(response)
307
308
309class TelemetryMetric(Module):
310 """Free-running ``ui64`` cycle counter exposed as a telemetry metric.
311
312 Hardware increments the counter every clock; the host reads it twice and
313 asserts the second read is strictly greater than the first (cycle counts
314 are monotonic between any two host-visible reads).
315 """
316
317 clk = Clock()
318 rst = Reset()
319
320 @generator
321 def construct(ports):
322 cycle_cnt = Counter(64)(clk=ports.clk,
323 rst=ports.rst,
324 clear=Bits(1)(0),
325 increment=Bits(1)(1),
326 instance_name="cycleCounter")
327 esi.Telemetry.report_signal(ports.clk, ports.rst, AppID("cycleCount"),
328 cycle_cnt.out)
329
330
331class IndexedFuncGroup(Module):
332 """Module exposing an indexed array of typed-function ports.
333
334 Instantiates ``num_entries`` ``FuncService`` ports under the same appid name
335 ``call`` with indices 0..N-1 -- the codegen groups them into a single
336 ``IndexedPorts<TypedFunction<...>>`` member, which the C++ driver iterates
337 over (``connected->call[i]``). Each entry returns ``arg + (i+1)``, so the
338 driver can verify it talked to the right index by sending the same arg to
339 every entry and comparing replies.
340 """
341
342 num_entries = Constant(UInt(8), 3)
343
344 clk = Clock()
345 rst = Reset()
346
347 @generator
348 def construct(ports):
349 for i in range(IndexedFuncGroup.num_entries.value):
350 addend = i + 1
351 result_wire = Wire(Channel(UInt(16)))
352 args = esi.FuncService.get_call_chans(AppID("call", i),
353 arg_type=UInt(16),
354 result=result_wire)
355 ready = Wire(Bits(1))
356 arg, valid = args.unwrap(ready)
357 sum_ = (arg + UInt(16)(addend)).as_uint(16)
358 out_chan, out_ready = Channel(UInt(16)).wrap(sum_, valid)
359 ready.assign(out_ready)
360 result_wire.assign(out_chan)
361
362
364 """Custom-`@esi.ServiceDecl` raw-channel loopback.
365
366 Connects ``HostComms.Recv`` -> ``HostComms.Send`` for an 8-bit byte stream
367 (host writes, HW echoes back) and ``VoidComms.Recv`` -> ``VoidComms.Send``
368 for a zero-bit "tick" stream. Both pairs are exposed via custom service
369 decls rather than the standard `ChannelService`. ``Top`` instantiates two
370 copies under indexed appids so the test also covers same-name multi-instance
371 hierarchy resolution.
372 """
373
374 clk = Clock()
375
376 @generator
377 def construct(ports):
378 data_in = HostComms.Recv(AppID("byte_in")).unpack()["recv"]
379 HostComms.Send(AppID("byte_out")).unpack(send=data_in)
380
381 void_in = VoidComms.Recv(AppID("void_in")).unpack()["recv"]
382 VoidComms.Send(AppID("void_out")).unpack(send=void_in)
383
384
385class StructArgs(Struct):
386 a: UInt(16)
387 b: SInt(8)
388
389
390class StructResult(Struct):
391 x: SInt(8)
392 y: SInt(8)
393
394
395class TypedFuncStruct(Module):
396 """Typed function: small struct -> small struct.
397
398 Returns ``{x = b+1, y = b}`` so the host can verify the arithmetic and
399 the order of struct fields end-to-end.
400 """
401
402 clk = Clock()
403 rst = Reset()
404
405 @generator
406 def construct(ports):
407 result_wire = Wire(Channel(StructResult))
408 args = esi.FuncService.get_call_chans(AppID("call"),
409 arg_type=StructArgs,
410 result=result_wire)
411 ready = Wire(Bits(1))
412 arg, valid = args.unwrap(ready)
413 b = arg["b"]
414 plus_one = (b + SInt(8)(1)).as_sint(8)
415 result = StructResult(x=plus_one, y=b)
416 out_chan, out_ready = Channel(StructResult).wrap(result, valid)
417 ready.assign(out_ready)
418 result_wire.assign(out_chan)
419
420
421class OddInner(Struct):
422 p: UInt(8)
423 q: SInt(8)
424 r: UInt(8) * 2
425
426
427class OddStruct(Struct):
428 a: UInt(12)
429 b: SInt(7)
430 inner: OddInner
431
432
434 """Typed function: nested odd-bit-width struct round-trip with arithmetic
435 on every field. Each field gets a distinct addend so a swap of any two
436 fields fails distinguishably."""
437
438 clk = Clock()
439 rst = Reset()
440
441 @generator
442 def construct(ports):
443 result_wire = Wire(Channel(OddStruct))
444 args = esi.FuncService.get_call_chans(AppID("call"),
445 arg_type=OddStruct,
446 result=result_wire)
447 ready = Wire(Bits(1))
448 arg, valid = args.unwrap(ready)
449 a = (arg["a"] + UInt(12)(1)).as_uint(12)
450 b = (arg["b"] + SInt(7)(-3)).as_sint(7)
451 inner = arg["inner"]
452 p = (inner["p"] + UInt(8)(5)).as_uint(8)
453 q = (inner["q"] + SInt(8)(2)).as_sint(8)
454 r0 = (inner["r"][0] + UInt(8)(1)).as_uint(8)
455 r1 = (inner["r"][1] + UInt(8)(2)).as_uint(8)
456 new_inner = OddInner(p=p, q=q, r=[r0, r1])
457 result = OddStruct(a=a, b=b, inner=new_inner)
458 out_chan, out_ready = Channel(OddStruct).wrap(result, valid)
459 ready.assign(out_ready)
460 result_wire.assign(out_chan)
461
462
464 """Typed function: ``si4 -> si4`` identity.
465
466 The driver tests positive, negative, and the si4 boundary values to
467 exercise sign extension at a sub-byte width through the typed facade.
468 """
469
470 clk = Clock()
471 rst = Reset()
472
473 @generator
474 def construct(ports):
475 result_wire = Wire(Channel(SInt(4)))
476 args = esi.FuncService.get_call_chans(AppID("call"),
477 arg_type=SInt(4),
478 result=result_wire)
479 ready = Wire(Bits(1))
480 arg, valid = args.unwrap(ready)
481 out_chan, out_ready = Channel(SInt(4)).wrap(arg, valid)
482 ready.assign(out_ready)
483 result_wire.assign(out_chan)
484
485
486ArrayArg = SInt(8) * 1
487ArrayResult = TypeAlias(SInt(8) * 2, "ArrayResult")
488
489
491 """Typed function with an array result.
492
493 Receives a one-element array and returns a two-element array containing
494 the input element and ``input + 1``. Exercises the typed facade's
495 ``std::array`` path end-to-end.
496 """
497
498 clk = Clock()
499 rst = Reset()
500
501 @generator
502 def construct(ports):
503 result_wire = Wire(Channel(ArrayResult))
504 args = esi.FuncService.get_call_chans(AppID("call"),
505 arg_type=ArrayArg,
506 result=result_wire)
507 ready = Wire(Bits(1))
508 arg, valid = args.unwrap(ready)
509 elem = arg[0]
510 plus_one = (elem + SInt(8)(1)).as_sint(8)
511 # The C++ driver asserts on the wire bytes so the convention is observable.
512 result_array = ArrayResult([elem, plus_one])
513 out_chan, out_ready = Channel(ArrayResult).wrap(result_array, valid)
514 ready.assign(out_ready)
515 result_wire.assign(out_chan)
516
517
518# Typed function over a windowed list payload. Uses the auto serial<->parallel
519# converters from `pycde.esi` so the burst-protocol state machine doesn't
520# have to be hand-rolled here. Each call doubles every list element.
521_TRANSFORM_LIST_BULK_WIDTH = 16
522_TRANSFORM_LIST_ITEMS_PER_FRAME = 1
523
524
525class TransformListItem(Struct):
526 v: Bits(32)
527
528
529_TRANSFORM_LIST_STRUCT = StructType([("data", List(TransformListItem))])
530_transform_list_window = Window.serial_of(_TRANSFORM_LIST_STRUCT,
531 _TRANSFORM_LIST_BULK_WIDTH,
532 _TRANSFORM_LIST_ITEMS_PER_FRAME)
533
534
536 """Typed function: ``window<list<si32>> -> window<list<si32>>``.
537
538 Doubles each element of the input list and emits the result as another
539 serial-burst windowed list. Driving the burst protocol is delegated to
540 `ListWindowToParallel` / `ListWindowToSerial` so this module only has to
541 describe the per-element transform.
542 """
543
544 clk = Clock()
545 rst = Reset()
546
547 @generator
548 def construct(ports):
549 result_chan = Wire(Channel(_transform_list_window))
550 args = esi.FuncService.get_call_chans(AppID("call"),
551 arg_type=_transform_list_window,
552 result=result_chan)
553
554 s2p = ListWindowToParallel(_transform_list_window)(clk=ports.clk,
555 rst=ports.rst,
556 serial_in=args)
557 parallel_in = s2p.parallel_out
558
559 par_ready = Wire(Bits(1))
560 par_window, par_valid = parallel_in.unwrap(par_ready)
561 par_struct = par_window.unwrap()
562
563 in_item = par_struct["data"]
564 last_bit = par_struct["last"]
565 in_v = in_item["v"].as_uint(32)
566 doubled = (in_v + in_v).as_bits(32)
567 out_item = TransformListItem(v=doubled)
568
569 parallel_result_window_type = Window.default_of(_TRANSFORM_LIST_STRUCT)
570 parallel_result_struct = parallel_result_window_type.lowered_type({
571 "data": out_item,
572 "last": last_bit,
573 })
574 parallel_result_window = parallel_result_window_type.wrap(
575 parallel_result_struct)
576 parallel_result_chan, par_result_ready = Channel(
577 parallel_result_window_type).wrap(parallel_result_window, par_valid)
578 par_ready.assign(par_result_ready)
579
580 p2s = ListWindowToSerial(parallel_result_window_type,
581 _TRANSFORM_LIST_BULK_WIDTH,
582 _TRANSFORM_LIST_ITEMS_PER_FRAME,
583 meta_fifo_depth=4)(
584 clk=ports.clk,
585 rst=ports.rst,
586 parallel_in=parallel_result_chan)
587 result_chan.assign(p2s.serial_out)
588
589
590# Window types for the channel-of-window probes. The struct carries a static
591# header (`tag`) plus a list payload (`items`) so the probes exercise the
592# header+list shape that today is only tested via the `FuncService` path.
593_WINDOW_PROBE_TAG = 0xCAFE
594_WINDOW_PROBE_ITEMS = [10, 20, 30, 40]
595_WINDOW_PROBE_BULK_WIDTH = 16
596_WINDOW_PROBE_ITEMS_PER_FRAME = 1
597_window_probe_struct = StructType([("tag", Bits(16)),
598 ("items", List(Bits(32)))])
599_window_probe_window = Window.serial_of(_window_probe_struct,
600 _WINDOW_PROBE_BULK_WIDTH,
601 _WINDOW_PROBE_ITEMS_PER_FRAME)
602
603
605 """HW-initiated callback whose argument is a windowed list with header.
606
607 Combines the callback pattern (``CallService.call``) with the serial-burst
608 windowed list payload. An MMIO write at offset ``0x10`` arms one burst;
609 the HW then sends the same ``{tag=0xCAFE, items=[10,20,30,40]}`` pattern
610 used by the channel probes into the host callback. The host handler
611 verifies the payload and the callback returns void (``Bits(0)``).
612 """
613
614 clk = Clock()
615 rst = Reset()
616
617 @generator
618 def construct(ports):
619 clk = ports.clk
620 rst = ports.rst
621
622 # MMIO trigger: write to offset 0x10 arms one burst.
623 trigger_bundle = esi.MMIO.read_write(appid=AppID("trigger"))
624 resp_chan = Wire(Channel(Bits(64)))
625 cmd_chan = trigger_bundle.unpack(data=resp_chan)["cmd"]
626 cmd_xact, cmd = cmd_chan.snoop_xact()
627 resp_chan.assign(cmd_chan.transform(lambda c: Bits(64)(c.data)))
628 trigger_xact = cmd_xact & (cmd.offset == UInt(32)(0x10))
629
630 n_items = len(_WINDOW_PROBE_ITEMS)
631 burst_end = Wire(Bits(1))
632
633 armed = ControlReg(clk, rst, asserts=[trigger_xact], resets=[burst_end])
634
635 par_ready = Wire(Bits(1))
636 handshake = armed & par_ready
637 idx_counter = Counter(2)(clk=clk,
638 rst=rst,
639 clear=burst_end,
640 increment=handshake,
641 instance_name="cb_window_idx")
642 idx = idx_counter.out
643 last_bit = (idx == UInt(2)(n_items - 1))
644 burst_end.assign(handshake & last_bit)
645
646 item_bits = Array(
647 Bits(32), len(_WINDOW_PROBE_ITEMS))(_WINDOW_PROBE_ITEMS)[idx.as_bits()]
648
649 parallel_window_type = Window.default_of(_window_probe_struct)
650 par_struct = parallel_window_type.lowered_type({
651 "tag": Bits(16)(_WINDOW_PROBE_TAG),
652 "items": item_bits,
653 "last": last_bit,
654 })
655 par_window = parallel_window_type.wrap(par_struct)
656 parallel_chan, parallel_ready = Channel(parallel_window_type).wrap(
657 par_window, armed)
658 par_ready.assign(parallel_ready)
659
660 p2s = ListWindowToSerial(parallel_window_type, _WINDOW_PROBE_BULK_WIDTH,
661 _WINDOW_PROBE_ITEMS_PER_FRAME,
662 4)(clk=clk, rst=rst, parallel_in=parallel_chan)
663
664 esi.CallService.call(AppID("callback"), p2s.serial_out, Bits(0))
665
666
668 """To-host channel of ``window<{tag, list<si32>}>``.
669
670 Exercises the typed read path for windowed-list-with-header on a raw
671 channel (no `TypedFunction` orchestrator on top). The driver writes any
672 value to offset ``0x10`` of the ``trigger`` MMIO region to arm one burst;
673 the HW then emits exactly one burst (``tag = 0xCAFE`` and the four-element
674 list ``[10, 20, 30, 40]``) and goes idle. Free-running emission would
675 unboundedly fill the host runtime's polling queue, so each burst is gated
676 on an explicit trigger.
677 """
678
679 clk = Clock()
680 rst = Reset()
681
682 @generator
683 def construct(ports):
684 clk = ports.clk
685 rst = ports.rst
686
687 # MMIO trigger: any write to offset 0x10 arms one burst. Snoop the
688 # cmd-channel xact and echo the write data as the read response so the
689 # MMIO bus is never back-pressured.
690 trigger_bundle = esi.MMIO.read_write(appid=AppID("trigger"))
691 resp_chan = Wire(Channel(Bits(64)))
692 cmd_chan = trigger_bundle.unpack(data=resp_chan)["cmd"]
693 cmd_xact, cmd = cmd_chan.snoop_xact()
694 resp_chan.assign(cmd_chan.transform(lambda c: Bits(64)(c.data)))
695 trigger_xact = cmd_xact & (cmd.offset == UInt(32)(0x10))
696
697 n_items = len(_WINDOW_PROBE_ITEMS)
698 burst_end = Wire(Bits(1))
699
700 # ``armed`` is high for the duration of one burst: set on a trigger
701 # write, cleared on the cycle the burst's last beat handshakes.
702 armed = ControlReg(clk, rst, asserts=[trigger_xact], resets=[burst_end])
703
704 # Per-beat item index. Increments on a beat handshake, clears at the end
705 # of the burst so the next trigger starts fresh from index 0.
706 par_ready = Wire(Bits(1))
707 handshake = armed & par_ready
708 idx_counter = Counter(2)(clk=clk,
709 rst=rst,
710 clear=burst_end,
711 increment=handshake,
712 instance_name="window_read_idx")
713 idx = idx_counter.out
714 last_bit = (idx == UInt(2)(n_items - 1))
715 burst_end.assign(handshake & last_bit)
716 item_value = Array(
717 Bits(32), len(_WINDOW_PROBE_ITEMS))(_WINDOW_PROBE_ITEMS)[idx.as_bits()]
718
719 # Build the parallel beat. ``Window.default_of`` lowers each beat as
720 # ``{<static fields>, <one item>, last}``.
721 parallel_window_type = Window.default_of(_window_probe_struct)
722 par_struct = parallel_window_type.lowered_type({
723 "tag": Bits(16)(_WINDOW_PROBE_TAG),
724 "items": item_value,
725 "last": last_bit,
726 })
727 par_window = parallel_window_type.wrap(par_struct)
728 parallel_chan, parallel_ready = Channel(parallel_window_type).wrap(
729 par_window, armed)
730 par_ready.assign(parallel_ready)
731
732 p2s = ListWindowToSerial(parallel_window_type, _WINDOW_PROBE_BULK_WIDTH,
733 _WINDOW_PROBE_ITEMS_PER_FRAME,
734 4)(clk=clk, rst=rst, parallel_in=parallel_chan)
735 esi.ChannelService.to_host(AppID("data"), p2s.serial_out)
736
737
738# Multi-burst read: the list is longer than the serial encoder's data FIFO, so
739# `ListWindowToSerial` emits it as several header/data bursts (via its
740# split-on-full path) terminated by a single count==0 footer, rather than one
741# big burst. The host-side `SerialListTypeDeserializer` must stitch those
742# bursts back into a single list. The count field stays 16 bits (byte-aligned,
743# header fills the frame), so this exercises the multi-burst *reassembly*
744# without depending on the sub-byte frame layout the C++ facade codegen does
745# not yet model.
746_MULTIBURST_READ_TAG = 0xF00D
747_MULTIBURST_READ_ITEMS = [0x1000 + i for i in range(10)]
748_MULTIBURST_READ_FIFO_DEPTH = 4
749
750
752 """To-host channel that emits a list split across multiple serial bursts.
753
754 Same shape as `ChannelWindowedListRead`, but the ten-element list exceeds
755 the serial encoder's data FIFO (depth 4), so `ListWindowToSerial` emits the
756 list as several header/data bursts (4 + 4 + 2) followed by a single count==0
757 footer. Verifies that the host-side `SerialListTypeDeserializer` reassembles
758 a multi-burst transfer back into one list. A write to offset ``0x10`` of the
759 ``trigger`` MMIO region arms one transfer.
760 """
761
762 clk = Clock()
763 rst = Reset()
764
765 @generator
766 def construct(ports):
767 clk = ports.clk
768 rst = ports.rst
769
770 # MMIO trigger: any write to offset 0x10 arms one transfer.
771 trigger_bundle = esi.MMIO.read_write(appid=AppID("trigger"))
772 resp_chan = Wire(Channel(Bits(64)))
773 cmd_chan = trigger_bundle.unpack(data=resp_chan)["cmd"]
774 cmd_xact, cmd = cmd_chan.snoop_xact()
775 resp_chan.assign(cmd_chan.transform(lambda c: Bits(64)(c.data)))
776 trigger_xact = cmd_xact & (cmd.offset == UInt(32)(0x10))
777
778 n_items = len(_MULTIBURST_READ_ITEMS)
779 burst_end = Wire(Bits(1))
780
781 # ``armed`` is high for the duration of one list: set on a trigger write,
782 # cleared on the cycle the final item handshakes. The producer streams all
783 # ten items with a single ``last`` on item 9 -- the split into bursts
784 # happens entirely inside `ListWindowToSerial`.
785 armed = ControlReg(clk, rst, asserts=[trigger_xact], resets=[burst_end])
786
787 par_ready = Wire(Bits(1))
788 handshake = armed & par_ready
789 idx_counter = Counter(4)(clk=clk,
790 rst=rst,
791 clear=burst_end,
792 increment=handshake,
793 instance_name="multiburst_read_idx")
794 idx = idx_counter.out
795 last_bit = (idx == UInt(4)(n_items - 1))
796 burst_end.assign(handshake & last_bit)
797 item_value = Array(Bits(32), n_items)(_MULTIBURST_READ_ITEMS)[idx.as_bits()]
798
799 parallel_window_type = Window.default_of(_window_probe_struct)
800 par_struct = parallel_window_type.lowered_type({
801 "tag": Bits(16)(_MULTIBURST_READ_TAG),
802 "items": item_value,
803 "last": last_bit,
804 })
805 par_window = parallel_window_type.wrap(par_struct)
806 parallel_chan, parallel_ready = Channel(parallel_window_type).wrap(
807 par_window, armed)
808 par_ready.assign(parallel_ready)
809
810 p2s = ListWindowToSerial(parallel_window_type, _WINDOW_PROBE_BULK_WIDTH,
811 _WINDOW_PROBE_ITEMS_PER_FRAME,
812 _MULTIBURST_READ_FIFO_DEPTH)(
813 clk=clk, rst=rst, parallel_in=parallel_chan)
814 esi.ChannelService.to_host(AppID("data"), p2s.serial_out)
815
816
818 """From-host channel of ``window<{tag, list<si32>}>``.
819
820 Exercises the typed write path for windowed-list-with-header on a raw
821 channel. Hardware receives one burst, converts it to parallel, and
822 AND-reduces per-beat equality against the same constant pattern as
823 `ChannelWindowedListRead`. The latched match flag is exposed via the
824 ``match`` MMIO region so the driver can verify the burst landed
825 correctly.
826 """
827
828 clk = Clock()
829 rst = Reset()
830
831 @generator
832 def construct(ports):
833 clk = ports.clk
834 rst = ports.rst
835
836 chan = esi.ChannelService.from_host(AppID("data"), _window_probe_window)
837 s2p = ListWindowToParallel(_window_probe_window)(clk=clk,
838 rst=rst,
839 serial_in=chan)
840 par_ready = Wire(Bits(1))
841 par_window, par_valid = s2p.parallel_out.unwrap(par_ready)
842 par_struct = par_window.unwrap()
843 par_ready.assign(Bits(1)(1))
844
845 handshake = par_valid
846 last_bit = par_struct["last"].as_bits(1)
847
848 # Counter cycles 0..N-1, clears on the burst-end beat.
849 n_items = len(_WINDOW_PROBE_ITEMS)
850 idx_clr = (handshake & last_bit).as_bits(1)
851 idx_counter = Counter(2)(clk=clk,
852 rst=rst,
853 clear=idx_clr,
854 increment=handshake,
855 instance_name="window_write_idx")
856 idx = idx_counter.out
857
858 expected_bits = Array(
859 Bits(32), len(_WINDOW_PROBE_ITEMS))(_WINDOW_PROBE_ITEMS)[idx.as_bits()]
860
861 tag_ok = (par_struct["tag"].as_bits(16) == Bits(16)(_WINDOW_PROBE_TAG))
862 item_ok = (par_struct["items"].as_bits(32) == expected_bits)
863 beat_ok = (tag_ok & item_ok).as_bits(1)
864
865 # Running AND-reduce across the burst; latches into ``final_match`` on
866 # the burst-end beat.
867 running_match = Wire(Bits(1))
868 running_match_reg = Reg(Bits(1),
869 clk=clk,
870 rst=rst,
871 rst_value=1,
872 ce=handshake,
873 name="window_match_running")
874 running_match.assign((running_match_reg & beat_ok).as_bits(1))
875 running_match_reg.assign(running_match)
876
877 final_match = Reg(Bits(1),
878 clk=clk,
879 rst=rst,
880 rst_value=0,
881 ce=idx_clr,
882 name="window_match_final")
883 final_match.assign(running_match)
884
885 # Expose the latched flag via MMIO read.
886 mmio_bundle = esi.MMIO.read(appid=AppID("match"))
887 resp_chan = Wire(Channel(Bits(64)))
888 addr_chan = mmio_bundle.unpack(data=resp_chan)["offset"]
889 resp_chan.assign(
890 addr_chan.transform(
891 lambda _: final_match.as_bits(1).pad_or_truncate(64).as_bits(64)))
892
893
894# Multi-burst bare-list window. The bulk count is 8 bits, so each burst can
895# carry at most 255 items; a 256-element list must therefore be split by the
896# host serializer into two bursts (255 + 1). Hardware reassembles the bursts
897# via `ListWindowToParallel` and validates every item in order, proving the
898# write-side burst chunking round-trips through hardware.
899#
900# The window is a *bare* list of byte-sized items (no static header fields)
901# with an 8-bit count, so both the header frame (a single ui8 count) and each
902# data frame (one ui8) fill exactly one byte with no frame padding. That keeps
903# the on-wire layout unambiguous: a narrower (sub-byte) count would introduce
904# sub-byte frame padding, which the C++ facade codegen does not currently
905# model.
906_MULTIBURST_N_ITEMS = 256
907_MULTIBURST_BULK_WIDTH = 8
908_MULTIBURST_ITEMS_PER_FRAME = 1
909_multiburst_struct = StructType([("items", List(Bits(8)))])
910_multiburst_window = Window.serial_of(_multiburst_struct,
911 _MULTIBURST_BULK_WIDTH,
912 _MULTIBURST_ITEMS_PER_FRAME)
913
914
916 """From-host channel of ``window<{list<ui8>}>`` with an 8-bit bulk count.
917
918 The 8-bit bulk count caps each burst at 255 items, so the host splits the
919 256-element list into two bursts (255 + 1). Hardware reassembles the bursts
920 via `ListWindowToParallel`, AND-reduces per-beat equality against the
921 expected ``item[i] == i`` pattern, and latches the result into the ``match``
922 MMIO region. This is the end-to-end check that the host-side multi-burst
923 serialization is correct.
924 """
925
926 clk = Clock()
927 rst = Reset()
928
929 @generator
930 def construct(ports):
931 clk = ports.clk
932 rst = ports.rst
933
934 chan = esi.ChannelService.from_host(AppID("data"), _multiburst_window)
935 s2p = ListWindowToParallel(_multiburst_window)(clk=clk,
936 rst=rst,
937 serial_in=chan)
938 par_ready = Wire(Bits(1))
939 par_window, par_valid = s2p.parallel_out.unwrap(par_ready)
940 par_struct = par_window.unwrap()
941 par_ready.assign(Bits(1)(1))
942
943 handshake = par_valid
944 last_bit = par_struct["last"].as_bits(1)
945
946 # Counter cycles 0..255, clears on the burst-end beat. Each item's value
947 # equals its position, so the expected value is simply the index.
948 idx_clr = (handshake & last_bit).as_bits(1)
949 idx_counter = Counter(8)(clk=clk,
950 rst=rst,
951 clear=idx_clr,
952 increment=handshake,
953 instance_name="multiburst_write_idx")
954 idx = idx_counter.out
955
956 beat_ok = (par_struct["items"].as_bits(8) == idx.as_bits(8)).as_bits(1)
957
958 # Running AND-reduce across the whole (reassembled) list; latches into
959 # ``final_match`` on the last item's beat.
960 running_match = Wire(Bits(1))
961 running_match_reg = Reg(Bits(1),
962 clk=clk,
963 rst=rst,
964 rst_value=1,
965 ce=handshake,
966 name="multiburst_match_running")
967 running_match.assign((running_match_reg & beat_ok).as_bits(1))
968 running_match_reg.assign(running_match)
969
970 final_match = Reg(Bits(1),
971 clk=clk,
972 rst=rst,
973 rst_value=0,
974 ce=idx_clr,
975 name="multiburst_match_final")
976 final_match.assign(running_match)
977
978 # Expose the latched flag via MMIO read.
979 mmio_bundle = esi.MMIO.read(appid=AppID("match"))
980 resp_chan = Wire(Channel(Bits(64)))
981 addr_chan = mmio_bundle.unpack(data=resp_chan)["offset"]
982 resp_chan.assign(
983 addr_chan.transform(
984 lambda _: final_match.as_bits(1).pad_or_truncate(64).as_bits(64)))
985
986
987# Narrow (sub-byte) bulk count with a static header field. The 2-bit count caps
988# each burst at 3 items, so the host serializer splits a 7-element list into
989# three bursts (3 + 3 + 1). Unlike `ChannelMultiBurstListWrite` (8-bit,
990# byte-aligned count where every frame fills whole bytes), the header frame
991# here is `{tag: ui16, items_count: ui2}` -- 18 bits of content that does NOT
992# fill the 32-bit data frame. CIRCT lowers the frame union MSB-first, so on the
993# wire `tag` occupies bits [31:16] and the 2-bit count sits at bits [15:14].
994# The C++ facade codegen must place them at exactly those offsets; the old
995# byte-granular layout put the count in the low bits and the HW would read a
996# garbage (zero) count and stall. This probe therefore exercises the sub-byte /
997# misaligned-static-field frame layout end-to-end.
998_NARROW_TAG = 0xBEEF
999_NARROW_ITEMS = [0x1000 + i for i in range(7)]
1000_NARROW_BULK_WIDTH = 2
1001_NARROW_ITEMS_PER_FRAME = 1
1002_narrow_struct = StructType([("tag", Bits(16)), ("items", List(Bits(32)))])
1003_narrow_window = Window.serial_of(_narrow_struct, _NARROW_BULK_WIDTH,
1004 _NARROW_ITEMS_PER_FRAME)
1005
1006
1008 """From-host ``window<{tag, list<ui32>}>`` with a 2-bit bulk count.
1009
1010 The 2-bit count caps each burst at 3 items, so the host splits the 7-element
1011 list into three bursts (3 + 3 + 1). Because the header content (ui16 tag +
1012 2-bit count = 18 bits) is narrower than the 32-bit data frame, this is the
1013 sub-byte / misaligned-static-field frame layout the byte-granular codegen got
1014 wrong. Hardware reassembles the bursts via `ListWindowToParallel`, checks the
1015 tag and every item, and latches the result into the ``match`` MMIO region.
1016 """
1017
1018 clk = Clock()
1019 rst = Reset()
1020
1021 @generator
1022 def construct(ports):
1023 clk = ports.clk
1024 rst = ports.rst
1025
1026 chan = esi.ChannelService.from_host(AppID("data"), _narrow_window)
1027 s2p = ListWindowToParallel(_narrow_window)(clk=clk, rst=rst, serial_in=chan)
1028 par_ready = Wire(Bits(1))
1029 par_window, par_valid = s2p.parallel_out.unwrap(par_ready)
1030 par_struct = par_window.unwrap()
1031 par_ready.assign(Bits(1)(1))
1032
1033 handshake = par_valid
1034 last_bit = par_struct["last"].as_bits(1)
1035
1036 # Counter cycles 0..N-1 over the reassembled list, clears on the burst-end
1037 # beat. Each item is checked against the expected pattern by index.
1038 n_items = len(_NARROW_ITEMS)
1039 idx_clr = (handshake & last_bit).as_bits(1)
1040 idx_counter = Counter(3)(clk=clk,
1041 rst=rst,
1042 clear=idx_clr,
1043 increment=handshake,
1044 instance_name="narrow_write_idx")
1045 idx = idx_counter.out
1046
1047 expected_bits = Array(Bits(32), n_items)(_NARROW_ITEMS)[idx.as_bits()]
1048 tag_ok = (par_struct["tag"].as_bits(16) == Bits(16)(_NARROW_TAG))
1049 item_ok = (par_struct["items"].as_bits(32) == expected_bits)
1050 beat_ok = (tag_ok & item_ok).as_bits(1)
1051
1052 # Running AND-reduce across the whole (reassembled) list; latches into
1053 # ``final_match`` on the last item's beat.
1054 running_match = Wire(Bits(1))
1055 running_match_reg = Reg(Bits(1),
1056 clk=clk,
1057 rst=rst,
1058 rst_value=1,
1059 ce=handshake,
1060 name="narrow_match_running")
1061 running_match.assign((running_match_reg & beat_ok).as_bits(1))
1062 running_match_reg.assign(running_match)
1063
1064 final_match = Reg(Bits(1),
1065 clk=clk,
1066 rst=rst,
1067 rst_value=0,
1068 ce=idx_clr,
1069 name="narrow_match_final")
1070 final_match.assign(running_match)
1071
1072 # Expose the latched flag via MMIO read.
1073 mmio_bundle = esi.MMIO.read(appid=AppID("match"))
1074 resp_chan = Wire(Channel(Bits(64)))
1075 addr_chan = mmio_bundle.unpack(data=resp_chan)["offset"]
1076 resp_chan.assign(
1077 addr_chan.transform(
1078 lambda _: final_match.as_bits(1).pad_or_truncate(64).as_bits(64)))
1079
1080
1081class Top(Module):
1082 clk = Clock()
1083 rst = Reset()
1084
1085 @generator
1086 def construct(ports):
1087 TypedFuncMultiArg(clk=ports.clk,
1088 rst=ports.rst,
1089 appid=AppID("typed_func_multi_arg_inst"))
1090 TypedFuncVoidArg(clk=ports.clk,
1091 rst=ports.rst,
1092 appid=AppID("typed_func_void_arg_inst"))
1093 TypedFuncVoidResult(clk=ports.clk,
1094 rst=ports.rst,
1095 appid=AppID("typed_func_void_result_inst"))
1096 CallServiceCallback(clk=ports.clk,
1097 rst=ports.rst,
1098 appid=AppID("call_service_callback_inst"))
1099 TypedReadChannelStruct(clk=ports.clk,
1100 rst=ports.rst,
1101 appid=AppID("typed_read_channel_struct_inst"))
1102 TypedWriteChannelByte(clk=ports.clk,
1103 rst=ports.rst,
1104 appid=AppID("typed_write_channel_byte_inst"))
1105 MmioReadWrite(clk=ports.clk,
1106 rst=ports.rst,
1107 appid=AppID("mmio_read_write_inst"))
1108 TelemetryMetric(clk=ports.clk,
1109 rst=ports.rst,
1110 appid=AppID("telemetry_metric_inst"))
1111 IndexedFuncGroup(clk=ports.clk,
1112 rst=ports.rst,
1113 appid=AppID("indexed_func_group_inst"))
1114
1115 # Two CustomServiceDeclChannel instances at indexed appids exercise the
1116 # custom-service-decl path AND same-name multi-instance hierarchy.
1117 CustomServiceDeclChannel(clk=ports.clk,
1118 appid=AppID("custom_service_decl_channel", 0))
1119 CustomServiceDeclChannel(clk=ports.clk,
1120 appid=AppID("custom_service_decl_channel", 1))
1121 TypedFuncStruct(clk=ports.clk,
1122 rst=ports.rst,
1123 appid=AppID("typed_func_struct_inst"))
1124 TypedFuncNestedStruct(clk=ports.clk,
1125 rst=ports.rst,
1126 appid=AppID("typed_func_nested_struct_inst"))
1127 TypedFuncSubByteSigned(clk=ports.clk,
1128 rst=ports.rst,
1129 appid=AppID("typed_func_subbyte_signed_inst"))
1130 TypedFuncArrayResult(clk=ports.clk,
1131 rst=ports.rst,
1132 appid=AppID("typed_func_array_result_inst"))
1133 TypedFuncWindowedList(clk=ports.clk,
1134 rst=ports.rst,
1135 appid=AppID("typed_func_windowed_list_inst"))
1136 ChannelWindowedListRead(clk=ports.clk,
1137 rst=ports.rst,
1138 appid=AppID("channel_windowed_list_read_inst"))
1139 ChannelMultiBurstListRead(clk=ports.clk,
1140 rst=ports.rst,
1141 appid=AppID("channel_multiburst_list_read_inst"))
1142 ChannelWindowedListWrite(clk=ports.clk,
1143 rst=ports.rst,
1144 appid=AppID("channel_windowed_list_write_inst"))
1146 clk=ports.clk,
1147 rst=ports.rst,
1148 appid=AppID("channel_multiburst_list_write_inst"))
1150 clk=ports.clk,
1151 rst=ports.rst,
1152 appid=AppID("channel_narrow_count_list_write_inst"))
1153 CallbackWindowedList(clk=ports.clk,
1154 rst=ports.rst,
1155 appid=AppID("callback_windowed_list_inst"))
1156
1157
1158if __name__ == "__main__":
1159 bsp = get_bsp(sys.argv[2] if len(sys.argv) > 2 else None)
1160 s = System(bsp(Top), name="TestCodegen", output_directory=sys.argv[1])
1161 s.compile()
1162 s.package()
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))