CIRCT 23.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, Mux, Reg, Wire
10from pycde.esi import (ChannelService, CallService, FuncService, MMIO,
11 MMIOReadWriteCmdType)
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):
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 mmio_read_bundle = MMIO.read(appid=AppID("mmio_client", add_amt))
91
92 address_chan_wire = Wire(Channel(UInt(32)))
93 address, address_valid = address_chan_wire.unwrap(1)
94 response_data = (address + add_amt).as_bits(64)
95 response_chan, response_ready = Channel(Bits(64)).wrap(
96 response_data, address_valid)
97
98 address_chan = mmio_read_bundle.unpack(data=response_chan)['offset']
99 address_chan_wire.assign(address_chan)
100
101 return MMIOClient
102
103
105 clk = Clock()
106 rst = Reset()
107
108 @generator
109 def build(ports):
110 mmio_read_write_bundle = MMIO.read_write(appid=AppID("mmio_rw_client"))
111
112 cmd_chan_wire = Wire(Channel(MMIOReadWriteCmdType))
113 resp_ready_wire = Wire(Bits(1))
114 cmd, cmd_valid = cmd_chan_wire.unwrap(resp_ready_wire)
115
116 add_amt = Reg(UInt(64),
117 name="add_amt",
118 clk=ports.clk,
119 rst=ports.rst,
120 rst_value=0,
121 ce=cmd_valid & cmd.write & (cmd.offset == 0x8).as_bits())
122 add_amt.assign(cmd.data.as_uint())
123 response_data = Mux(
124 cmd.write,
125 (cmd.offset + add_amt).as_bits(64),
126 Bits(64)(0),
127 )
128 response_chan, response_ready = Channel(Bits(64)).wrap(
129 response_data, cmd_valid)
130 resp_ready_wire.assign(response_ready)
131
132 cmd_chan = mmio_read_write_bundle.unpack(data=response_chan)['cmd']
133 cmd_chan_wire.assign(cmd_chan)
134
135
136class ConstProducer(Module):
137 clk = Clock()
138 rst = Reset()
139
140 @generator
141 def construct(ports):
142 const = UInt(32)(42)
143 xact = Wire(Bits(1))
144 valid = ~ControlReg(ports.clk, ports.rst, [xact], [Bits(1)(0)])
145 ch, ready = Channel(UInt(32)).wrap(const, valid)
146 xact.assign(ready & valid)
147 ChannelService.to_host(AppID("const_producer"), ch)
148
149
150class JoinAddFunc(Func):
151 # This test is broken since the DC dialect flow is broken. Leaving the code
152 # here in case it gets fixed in the future.
153 # https://github.com/llvm/circt/issues/7949 is the latest layer of the onion.
154
155 a = Input(UInt(32))
156 b = Input(UInt(32))
157 x = Output(UInt(32))
158
159 @generator
160 def construct(ports):
161 ports.x = (ports.a + ports.b).as_uint(32)
162
163
164class Join(Module):
165 # This test is broken since the JoinAddFunc function is broken.
166 clk = Clock()
167 rst = Reset()
168
169 @generator
170 def construct(ports):
171 a = ChannelService.from_host(AppID("join_a"), UInt(32))
172 b = ChannelService.from_host(AppID("join_b"), UInt(32))
173 f = JoinAddFunc(clk=ports.clk, rst=ports.rst, a=a, b=b)
174 ChannelService.to_host(AppID("join_x"), f.x)
175
176
177# Define the struct with four fields
178FourFieldStruct = StructType({
179 "a": Bits(32),
180 "b": Bits(32),
181 "c": Bits(32),
182 "d": Bits(32),
183})
184
185# Create a window that divides the struct into two frames
186windowed_struct = Window(
187 "four_field_window", FourFieldStruct,
188 [Window.Frame("frame1", ["a", "b"]),
189 Window.Frame("frame2", ["c", "d"])])
190
191
192class WindowToStructFunc(Module):
193 """Exposes a function that accepts a windowed struct (four fields split into
194 two frames) and returns the reassembled struct without windowing.
195
196 The input struct has four UInt(32) fields: a, b, c, d.
197 The window divides these into two frames:
198 - Frame 1: fields a and b
199 - Frame 2: fields c and d
200
201 Frames arrive in-order. The function reads both frames, reassembles the
202 complete struct, and outputs it.
203 """
204
205 clk = Clock()
206 rst = Reset()
207
208 @generator
209 def construct(ports):
210
211 # Result is the complete struct (no windowing)
212 result_chan = Wire(Channel(FourFieldStruct))
213 args = FuncService.get_call_chans(AppID("struct_from_window"),
214 arg_type=windowed_struct,
215 result=result_chan)
216
217 # State register to track which frame we're expecting (0 = frame1, 1 = frame2)
218 expecting_frame2 = Reg(Bits(1),
219 name="expecting_frame2",
220 clk=ports.clk,
221 rst=ports.rst,
222 rst_value=0)
223
224 # Registers to hold the values from frame1
225 a_reg = Reg(Bits(32),
226 name="a_reg",
227 clk=ports.clk,
228 rst=ports.rst,
229 rst_value=0)
230 b_reg = Reg(Bits(32),
231 name="b_reg",
232 clk=ports.clk,
233 rst=ports.rst,
234 rst_value=0)
235
236 # Unwrap the incoming channel
237 ready = Wire(Bits(1))
238 window_data, window_valid = args.unwrap(ready)
239
240 # Unwrap the window to get the union of frames
241 frame_union = window_data.unwrap()
242
243 # Extract data from both frames (only one is valid at a time based on state)
244 # Access the frame structs through the union - the data is reinterpreted
245 # based on which frame we're expecting
246 frame1_data = frame_union["frame1"]
247 frame2_data = frame_union["frame2"]
248
249 # When we receive frame1, store a and b
250 got_frame1 = window_valid & ~expecting_frame2
251 a_reg.assign(Mux(got_frame1, a_reg, frame1_data.a))
252 b_reg.assign(Mux(got_frame1, b_reg, frame1_data.b))
253
254 # When we receive frame2, we can output the complete struct
255 got_frame2 = window_valid & expecting_frame2
256
257 # Update state: after receiving frame1, expect frame2; after frame2, expect frame1
258 expecting_frame2.assign(
259 Mux(window_valid, expecting_frame2, ~expecting_frame2))
260
261 # Output the reassembled struct when we have frame2
262 output_struct = FourFieldStruct({
263 "a": a_reg,
264 "b": b_reg,
265 "c": frame2_data["c"],
266 "d": frame2_data["d"]
267 })
268 result_internal, result_ready = Channel(FourFieldStruct).wrap(
269 output_struct, got_frame2)
270
271 # We're ready to accept when either:
272 # - We're waiting for frame1 (always ready)
273 # - We're waiting for frame2 and downstream is ready
274 ready.assign(~expecting_frame2 | result_ready)
275 result_chan.assign(result_internal)
276
277
278class StructToWindowFunc(Module):
279 """Exposes a function that accepts a complete struct and returns it as a
280 windowed struct split into two frames.
281
282 This is the inverse of WindowedStructFunc.
283
284 The input struct has four Bits(32) fields: a, b, c, d.
285 The output window divides these into two frames:
286 - Frame 1: fields a and b
287 - Frame 2: fields c and d
288
289 The function reads the complete struct, then outputs two frames in order.
290 """
291
292 clk = Clock()
293 rst = Reset()
294
295 @generator
296 def construct(ports):
297 # Result is the windowed struct
298 result_chan = Wire(Channel(windowed_struct))
299 args = FuncService.get_call_chans(AppID("struct_to_window"),
300 arg_type=FourFieldStruct,
301 result=result_chan)
302
303 # State register to track which frame we're sending (0 = frame1, 1 = frame2)
304 sending_frame2 = Reg(Bits(1),
305 name="sending_frame2",
306 clk=ports.clk,
307 rst=ports.rst,
308 rst_value=0)
309
310 # Register to indicate we have a valid struct to send
311 have_struct = Reg(Bits(1),
312 name="have_struct",
313 clk=ports.clk,
314 rst=ports.rst,
315 rst_value=0)
316
317 # Registers to hold the input struct fields
318 a_reg = Reg(Bits(32),
319 name="a_reg",
320 clk=ports.clk,
321 rst=ports.rst,
322 rst_value=0)
323 b_reg = Reg(Bits(32),
324 name="b_reg",
325 clk=ports.clk,
326 rst=ports.rst,
327 rst_value=0)
328 c_reg = Reg(Bits(32),
329 name="c_reg",
330 clk=ports.clk,
331 rst=ports.rst,
332 rst_value=0)
333 d_reg = Reg(Bits(32),
334 name="d_reg",
335 clk=ports.clk,
336 rst=ports.rst,
337 rst_value=0)
338
339 # Unwrap the incoming channel
340 ready = Wire(Bits(1))
341 struct_data, struct_valid = args.unwrap(ready)
342
343 # Get the lowered type (a union of frame structs)
344 lowered_type = windowed_struct.lowered_type
345
346 # Create frame1 and frame2 data
347 frame1_struct = lowered_type.frame1({"a": a_reg, "b": b_reg})
348 frame2_struct = lowered_type.frame2({"c": c_reg, "d": d_reg})
349
350 # Select which frame to output based on state
351 frame1_union = lowered_type(("frame1", frame1_struct))
352 frame2_union = lowered_type(("frame2", frame2_struct))
353
354 # Mux between frames based on state
355 output_union = Mux(sending_frame2, frame1_union, frame2_union)
356 output_window = windowed_struct.wrap(output_union)
357
358 # Output is valid when we have a struct to send
359 output_valid = have_struct
360 result_internal, result_ready = Channel(windowed_struct).wrap(
361 output_window, output_valid)
362
363 # Compute state transitions
364 frame_sent = output_valid & result_ready
365 store_struct = struct_valid & ~have_struct
366 done_sending = frame_sent & sending_frame2
367
368 # Store the incoming struct when we receive it and aren't busy
369 a_reg.assign(Mux(store_struct, a_reg, struct_data["a"]))
370 b_reg.assign(Mux(store_struct, b_reg, struct_data["b"]))
371 c_reg.assign(Mux(store_struct, c_reg, struct_data["c"]))
372 d_reg.assign(Mux(store_struct, d_reg, struct_data["d"]))
373
374 # have_struct: set when storing, clear when done sending both frames
375 have_struct.assign(
376 Mux(store_struct, Mux(done_sending, have_struct,
377 Bits(1)(0)),
378 Bits(1)(1)))
379
380 # sending_frame2: set after sending frame1, clear after sending frame2
381 sending_frame2.assign(
382 Mux(frame_sent & ~sending_frame2,
383 Mux(done_sending, sending_frame2,
384 Bits(1)(0)),
385 Bits(1)(1)))
386
387 # We're ready to accept a new struct when we don't have one
388 ready.assign(~have_struct)
389 result_chan.assign(result_internal)
390
391
392class Top(Module):
393 clk = Clock()
394 rst = Reset()
395
396 @generator
397 def construct(ports):
398 CallbackTest(clk=ports.clk, rst=ports.rst, appid=AppID("callback"))
399 LoopbackInOutAdd(clk=ports.clk, rst=ports.rst, appid=AppID("loopback"))
400 for i in range(4, 18, 5):
401 MMIOClient(i)()
402 MMIOReadWriteClient(clk=ports.clk, rst=ports.rst)
403 ConstProducer(clk=ports.clk, rst=ports.rst)
404 WindowToStructFunc(clk=ports.clk, rst=ports.rst)
405 StructToWindowFunc(clk=ports.clk, rst=ports.rst)
406
407 # Disable broken test.
408 # Join(clk=ports.clk, rst=ports.rst)
409
410
411if __name__ == "__main__":
412 bsp = get_bsp(sys.argv[2] if len(sys.argv) > 2 else None)
413 s = pycde.System(bsp(Top),
414 name="ESILoopback",
415 output_directory=sys.argv[1],
416 core_clock_frequency_hz=20_000_000)
417 s.compile()
418 s.package()
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))
construct(ports)
Definition esi_test.py:170
construct(ports)
Definition esi_test.py:397
FourFieldStruct
Definition esi_test.py:178
MMIOClient(int add_amt)
Definition esi_test.py:82