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