CIRCT 23.0.0git
Loading...
Searching...
No Matches
esi_test.py
Go to the documentation of this file.
1# REQUIRES: esi-runtime, esi-cosim, rtl-sim
2# RUN: rm -rf %t
3# RUN: mkdir %t && cd %t
4# RUN: %PYTHON% %s %t 2>&1
5# RUN: esi-cosim.py --source %t -- %PYTHON% %S/test_software/esi_test.py cosim env
6# RUN: ESI_COSIM_MANIFEST_MMIO=1 esi-cosim.py --source %t -- %PYTHON% %S/test_software/esi_test.py cosim env
7
8import pycde
9from pycde import (AppID, Clock, Module, Reset, modparams, generator)
10from pycde.bsp import get_bsp
11from pycde.common import Constant, Input, Output
12from pycde.constructs import ControlReg, Mux, Reg, Wire
13from pycde.esi import (ChannelService, CallService, FuncService, MMIO,
14 MMIOReadWriteCmdType)
15from pycde.testing import print_info
16from pycde.types import (Bits, Channel, ChannelSignaling, StructType, UInt,
17 Window)
18from pycde.handshake import Func
19
20import sys
21
22
23class LoopbackInOutAdd(Module):
24 """Loopback the request from the host, adding 7 to the first 15 bits."""
25 clk = Clock()
26 rst = Reset()
27
28 add_amt = Constant(UInt(16), 11)
29
30 @generator
31 def construct(ports):
32 loopback = Wire(Channel(UInt(16), signaling=ChannelSignaling.FIFO))
33 args = FuncService.get_call_chans(AppID("add"),
34 arg_type=UInt(24),
35 result=loopback)
36
37 ready = Wire(Bits(1))
38 data, valid = args.unwrap(ready)
39 plus7 = data + LoopbackInOutAdd.add_amt.value
40 data_chan, data_ready = Channel(UInt(16), ChannelSignaling.ValidReady).wrap(
41 plus7.as_uint(16), valid)
42 data_chan_buffered = data_chan.buffer(ports.clk, ports.rst, 1,
43 ChannelSignaling.FIFO)
44 ready.assign(data_ready)
45 loopback.assign(data_chan_buffered)
46
47 xact, snooped_data = data_chan_buffered.snoop_xact()
48 xact.when_true(
49 lambda: print_info("LoopbackInOutAdd received: %p", snooped_data))
50
51
52class CallbackTest(Module):
53 """Call a function on the host when an MMIO write is received at offset
54 0x10."""
55 clk = Clock()
56 rst = Reset()
57
58 @generator
59 def construct(ports):
60 clk = ports.clk
61 rst = ports.rst
62
63 mmio_bundle = MMIO.read_write(appid=AppID("cmd"))
64 data_resp_chan = Wire(Channel(Bits(64)))
65 mmio_cmd_chan = mmio_bundle.unpack(data=data_resp_chan)["cmd"]
66 cb_trigger, mmio_cmd_chan_fork = mmio_cmd_chan.fork(clk=clk, rst=rst)
67
68 data_resp_chan.assign(mmio_cmd_chan_fork.transform(lambda cmd: cmd.data))
69
70 cb_trigger_ready = Wire(Bits(1))
71 cb_trigger_cmd, cb_trigger_valid = cb_trigger.unwrap(cb_trigger_ready)
72 trigger = cb_trigger_valid & (cb_trigger_cmd.offset == UInt(32)(0x10))
73 data_reg = cb_trigger_cmd.data.reg(clk, rst, ce=trigger)
74 cb_chan, cb_trigger_ready_sig = Channel(UInt(64)).wrap(
75 data_reg.as_uint(), trigger.reg(clk, rst))
76 cb_trigger_ready.assign(cb_trigger_ready_sig)
77 resp_chan = CallService.call(AppID("cb"), cb_chan, UInt(64))
78 # TODO: Fix snoop_xact to work with unconumed channels.
79 _, _ = resp_chan.unwrap(Bits(1)(1))
80 xact, snooped_data = resp_chan.snoop_xact()
81 xact.when_true(lambda: print_info("Callback received: %p", snooped_data))
82
83
84@modparams
85def MMIOClient(add_amt: int):
86
87 class MMIOClient(Module):
88 """A module which requests an MMIO address space and upon an MMIO read
89 request, returns the <address offset into its space> + add_amt."""
90
91 @generator
92 def build(ports):
93 mmio_read_bundle = MMIO.read(appid=AppID("mmio_client", add_amt))
94
95 address_chan_wire = Wire(Channel(UInt(32)))
96 address, address_valid = address_chan_wire.unwrap(1)
97 response_data = (address + add_amt).as_bits(64)
98 response_chan, response_ready = Channel(Bits(64)).wrap(
99 response_data, address_valid)
100
101 address_chan = mmio_read_bundle.unpack(data=response_chan)['offset']
102 address_chan_wire.assign(address_chan)
103
104 return MMIOClient
105
106
108 clk = Clock()
109 rst = Reset()
110
111 @generator
112 def build(ports):
113 mmio_read_write_bundle = MMIO.read_write(appid=AppID("mmio_rw_client"))
114
115 cmd_chan_wire = Wire(Channel(MMIOReadWriteCmdType))
116 resp_ready_wire = Wire(Bits(1))
117 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
118
119 add_amt = Reg(UInt(64),
120 name="add_amt",
121 clk=ports.clk,
122 rst=ports.rst,
123 rst_value=0,
124 ce=cmd_valid & cmd.write & (cmd.offset == 0x8).as_bits())
125 add_amt.assign(cmd.data.as_uint())
126 response_data = Mux(
127 cmd.write,
128 (cmd.offset + add_amt).as_bits(64),
129 Bits(64)(0),
130 )
131 response_chan, response_ready = Channel(Bits(64)).wrap(
132 response_data, cmd_valid)
133 resp_ready_wire.assign(response_ready)
134
135 cmd_chan = mmio_read_write_bundle.unpack(data=response_chan)['cmd']
136 cmd_chan_wire.assign(cmd_chan)
137
138
139class ConstProducer(Module):
140 clk = Clock()
141 rst = Reset()
142
143 @generator
144 def construct(ports):
145 const = UInt(32)(42)
146 xact = Wire(Bits(1))
147 valid = ~ControlReg(ports.clk, ports.rst, [xact], [Bits(1)(0)])
148 ch, ready = Channel(UInt(32)).wrap(const, valid)
149 xact.assign(ready & valid)
150 ChannelService.to_host(AppID("const_producer"), ch)
151
152
153class JoinAddFunc(Func):
154 # This test is broken since the DC dialect flow is broken. Leaving the code
155 # here in case it gets fixed in the future.
156 # https://github.com/llvm/circt/issues/7949 is the latest layer of the onion.
157
158 a = Input(UInt(32))
159 b = Input(UInt(32))
160 x = Output(UInt(32))
161
162 @generator
163 def construct(ports):
164 ports.x = (ports.a + ports.b).as_uint(32)
165
166
167class Join(Module):
168 # This test is broken since the JoinAddFunc function is broken.
169 clk = Clock()
170 rst = Reset()
171
172 @generator
173 def construct(ports):
174 a = ChannelService.from_host(AppID("join_a"), UInt(32))
175 b = ChannelService.from_host(AppID("join_b"), UInt(32))
176 f = JoinAddFunc(clk=ports.clk, rst=ports.rst, a=a, b=b)
177 ChannelService.to_host(AppID("join_x"), f.x)
178
179
180# Define the struct with four fields
181FourFieldStruct = StructType({
182 "a": Bits(32),
183 "b": Bits(32),
184 "c": Bits(32),
185 "d": Bits(32),
186})
187
188# Create a window that divides the struct into two frames
189windowed_struct = Window(
190 "four_field_window", FourFieldStruct,
191 [Window.Frame("frame1", ["a", "b"]),
192 Window.Frame("frame2", ["c", "d"])])
193
194
195class WindowToStructFunc(Module):
196 """Exposes a function that accepts a windowed struct (four fields split into
197 two frames) and returns the reassembled struct without windowing.
198
199 The input struct has four UInt(32) fields: a, b, c, d.
200 The window divides these into two frames:
201 - Frame 1: fields a and b
202 - Frame 2: fields c and d
203
204 Frames arrive in-order. The function reads both frames, reassembles the
205 complete struct, and outputs it.
206 """
207
208 clk = Clock()
209 rst = Reset()
210
211 @generator
212 def construct(ports):
213
214 # Result is the complete struct (no windowing)
215 result_chan = Wire(Channel(FourFieldStruct))
216 args = FuncService.get_call_chans(AppID("struct_from_window"),
217 arg_type=windowed_struct,
218 result=result_chan)
219
220 # State register to track which frame we're expecting (0 = frame1, 1 = frame2)
221 expecting_frame2 = Reg(Bits(1),
222 name="expecting_frame2",
223 clk=ports.clk,
224 rst=ports.rst,
225 rst_value=0)
226
227 # Registers to hold the values from frame1
228 a_reg = Reg(Bits(32),
229 name="a_reg",
230 clk=ports.clk,
231 rst=ports.rst,
232 rst_value=0)
233 b_reg = Reg(Bits(32),
234 name="b_reg",
235 clk=ports.clk,
236 rst=ports.rst,
237 rst_value=0)
238
239 # Unwrap the incoming channel
240 ready = Wire(Bits(1))
241 window_data, window_valid = args.unwrap(ready)
242
243 # Unwrap the window to get the union of frames
244 frame_union = window_data.unwrap()
245
246 # Extract data from both frames (only one is valid at a time based on state)
247 # Access the frame structs through the union - the data is reinterpreted
248 # based on which frame we're expecting
249 frame1_data = frame_union["frame1"]
250 frame2_data = frame_union["frame2"]
251
252 # When we receive frame1, store a and b
253 got_frame1 = window_valid & ~expecting_frame2
254 a_reg.assign(Mux(got_frame1, a_reg, frame1_data.a))
255 b_reg.assign(Mux(got_frame1, b_reg, frame1_data.b))
256
257 # When we receive frame2, we can output the complete struct
258 got_frame2 = window_valid & expecting_frame2
259
260 # Update state: after receiving frame1, expect frame2; after frame2, expect frame1
261 expecting_frame2.assign(
262 Mux(window_valid, expecting_frame2, ~expecting_frame2))
263
264 # Output the reassembled struct when we have frame2
265 output_struct = FourFieldStruct({
266 "a": a_reg,
267 "b": b_reg,
268 "c": frame2_data["c"],
269 "d": frame2_data["d"]
270 })
271 result_internal, result_ready = Channel(FourFieldStruct).wrap(
272 output_struct, got_frame2)
273
274 # We're ready to accept when either:
275 # - We're waiting for frame1 (always ready)
276 # - We're waiting for frame2 and downstream is ready
277 ready.assign(~expecting_frame2 | result_ready)
278 result_chan.assign(result_internal)
279
280
281class StructToWindowFunc(Module):
282 """Exposes a function that accepts a complete struct and returns it as a
283 windowed struct split into two frames.
284
285 This is the inverse of WindowedStructFunc.
286
287 The input struct has four Bits(32) fields: a, b, c, d.
288 The output window divides these into two frames:
289 - Frame 1: fields a and b
290 - Frame 2: fields c and d
291
292 The function reads the complete struct, then outputs two frames in order.
293 """
294
295 clk = Clock()
296 rst = Reset()
297
298 @generator
299 def construct(ports):
300 # Result is the windowed struct
301 result_chan = Wire(Channel(windowed_struct))
302 args = FuncService.get_call_chans(AppID("struct_to_window"),
303 arg_type=FourFieldStruct,
304 result=result_chan)
305
306 # State register to track which frame we're sending (0 = frame1, 1 = frame2)
307 sending_frame2 = Reg(Bits(1),
308 name="sending_frame2",
309 clk=ports.clk,
310 rst=ports.rst,
311 rst_value=0)
312
313 # Register to indicate we have a valid struct to send
314 have_struct = Reg(Bits(1),
315 name="have_struct",
316 clk=ports.clk,
317 rst=ports.rst,
318 rst_value=0)
319
320 # Registers to hold the input struct fields
321 a_reg = Reg(Bits(32),
322 name="a_reg",
323 clk=ports.clk,
324 rst=ports.rst,
325 rst_value=0)
326 b_reg = Reg(Bits(32),
327 name="b_reg",
328 clk=ports.clk,
329 rst=ports.rst,
330 rst_value=0)
331 c_reg = Reg(Bits(32),
332 name="c_reg",
333 clk=ports.clk,
334 rst=ports.rst,
335 rst_value=0)
336 d_reg = Reg(Bits(32),
337 name="d_reg",
338 clk=ports.clk,
339 rst=ports.rst,
340 rst_value=0)
341
342 # Unwrap the incoming channel
343 ready = Wire(Bits(1))
344 struct_data, struct_valid = args.unwrap(ready)
345
346 # Get the lowered type (a union of frame structs)
347 lowered_type = windowed_struct.lowered_type
348
349 # Create frame1 and frame2 data
350 frame1_struct = lowered_type.frame1({"a": a_reg, "b": b_reg})
351 frame2_struct = lowered_type.frame2({"c": c_reg, "d": d_reg})
352
353 # Select which frame to output based on state
354 frame1_union = lowered_type(("frame1", frame1_struct))
355 frame2_union = lowered_type(("frame2", frame2_struct))
356
357 # Mux between frames based on state
358 output_union = Mux(sending_frame2, frame1_union, frame2_union)
359 output_window = windowed_struct.wrap(output_union)
360
361 # Output is valid when we have a struct to send
362 output_valid = have_struct
363 result_internal, result_ready = Channel(windowed_struct).wrap(
364 output_window, output_valid)
365
366 # Compute state transitions
367 frame_sent = output_valid & result_ready
368 store_struct = struct_valid & ~have_struct
369 done_sending = frame_sent & sending_frame2
370
371 # Store the incoming struct when we receive it and aren't busy
372 a_reg.assign(Mux(store_struct, a_reg, struct_data["a"]))
373 b_reg.assign(Mux(store_struct, b_reg, struct_data["b"]))
374 c_reg.assign(Mux(store_struct, c_reg, struct_data["c"]))
375 d_reg.assign(Mux(store_struct, d_reg, struct_data["d"]))
376
377 # have_struct: set when storing, clear when done sending both frames
378 have_struct.assign(
379 Mux(store_struct, Mux(done_sending, have_struct,
380 Bits(1)(0)),
381 Bits(1)(1)))
382
383 # sending_frame2: set after sending frame1, clear after sending frame2
384 sending_frame2.assign(
385 Mux(frame_sent & ~sending_frame2,
386 Mux(done_sending, sending_frame2,
387 Bits(1)(0)),
388 Bits(1)(1)))
389
390 # We're ready to accept a new struct when we don't have one
391 ready.assign(~have_struct)
392 result_chan.assign(result_internal)
393
394
395class Top(Module):
396 clk = Clock()
397 rst = Reset()
398
399 @generator
400 def construct(ports):
401 CallbackTest(clk=ports.clk, rst=ports.rst, appid=AppID("callback"))
402 LoopbackInOutAdd(clk=ports.clk, rst=ports.rst, appid=AppID("loopback"))
403 for i in range(4, 18, 5):
404 MMIOClient(i)()
405 MMIOReadWriteClient(clk=ports.clk, rst=ports.rst)
406 ConstProducer(clk=ports.clk, rst=ports.rst)
407 WindowToStructFunc(clk=ports.clk, rst=ports.rst)
408 StructToWindowFunc(clk=ports.clk, rst=ports.rst)
409
410 # Disable broken test.
411 # Join(clk=ports.clk, rst=ports.rst)
412
413
414if __name__ == "__main__":
415 bsp = get_bsp(sys.argv[2] if len(sys.argv) > 2 else None)
416 s = pycde.System(bsp(Top),
417 name="ESILoopback",
418 output_directory=sys.argv[1],
419 core_clock_frequency_hz=20_000_000)
420 s.compile()
421 s.package()
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))
construct(ports)
Definition esi_test.py:173
construct(ports)
Definition esi_test.py:400
FourFieldStruct
Definition esi_test.py:181
MMIOClient(int add_amt)
Definition esi_test.py:85