CIRCT 23.0.0git
Loading...
Searching...
No Matches
serialization_probes.py
Go to the documentation of this file.
1"""Hardware design for serialization-correctness probes.
2
3This design exercises the on-the-wire layout invariants of the ESI runtime
4serializer/deserializer pair against a hardware implementation that is
5deliberately picky about byte order, sign extension, struct field order,
6sub-byte field packing, and array element order. Each function is small and
7self-checking: the host sends a value with distinguishable bytes/fields/bits,
8the hardware applies a position-revealing transform, and the host asserts
9the exact expected bytes/fields/bits come back. A mismatch in any of those
10five invariants between the host serializer and the hardware wire format
11yields a wrong (rather than coincidentally correct) answer.
12"""
13
14import sys
15
16import pycde.esi as esi
17from pycde import AppID, Clock, Module, Reset, System, generator
18from esiaccel.bsp import get_bsp
19from pycde.constructs import Mux, Wire
20from pycde.signals import BitsSignal, Struct
21from pycde.types import Bits, Channel, SInt, UInt
22
23
24class ByteRotate1(Module):
25 """Function ``byte_rotate1``: ui64 -> ui64.
26
27 Rotates the input left by one byte position. Sending
28 ``0x0102030405060708`` yields ``0x0203040506070801``: the MSB byte wraps
29 around to the LSB. Unlike a byte *swap*, a rotate is **not** its own
30 inverse, so a host that mis-orders bytes symmetrically on the send and
31 receive paths (the classic symmetric-serdes bug that passes loopback)
32 produces a wrong-and-distinguishable answer here.
33 """
34
35 clk = Clock()
36 rst = Reset()
37
38 @generator
39 def construct(ports):
40 result_wire = Wire(Channel(UInt(64)))
41 args = esi.FuncService.get_call_chans(AppID("byte_rotate1"),
42 arg_type=UInt(64),
43 result=result_wire)
44
45 ready = Wire(Bits(1))
46 arg, valid = args.unwrap(ready)
47
48 # Numerically: result = (arg << 8) | (arg >> 56).
49 # In bit-slice terms: the bottom 56 bits of arg become the top 56 bits
50 # of the result and the top byte of arg wraps around to the bottom.
51 arg_bits = arg.as_bits(64)
52 rotated = BitsSignal.concat([arg_bits[0:56], arg_bits[56:64]]).as_uint()
53
54 out_chan, out_ready = Channel(UInt(64)).wrap(rotated, valid)
55 ready.assign(out_ready)
56 result_wire.assign(out_chan)
57
58
59PatternArray = UInt(8) * 8
60
61# A deliberately asymmetric byte sequence: the wire ordering is unambiguous
62# in either direction (no palindrome / no monotone), and every nibble is
63# distinct so a single wrong byte is easy to spot in failure messages.
64_BYTE_PATTERN = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]
65
66
67class BytePatternConst(Module):
68 """Function ``byte_pattern_const``: ui8 -> array<ui8, 8>.
69
70 Ignores the trigger byte and returns the constant pattern
71 ``[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]`` (in PyCDE / MLIR
72 index order). The driver consumes the result as **raw wire bytes**
73 rather than through the typed deserializer, so any host-side decoding bug
74 (including a symmetric one) is taken out of the loop entirely. A test
75 failure here points at a mismatch between the runtime's wire convention
76 and the spec, not at a SW round-trip.
77 """
78
79 clk = Clock()
80 rst = Reset()
81
82 @generator
83 def construct(ports):
84 result_wire = Wire(Channel(PatternArray))
85 args = esi.FuncService.get_call_chans(AppID("byte_pattern_const"),
86 arg_type=UInt(8),
87 result=result_wire)
88
89 ready = Wire(Bits(1))
90 _, valid = args.unwrap(ready)
91
92 pattern = PatternArray([UInt(8)(b) for b in _BYTE_PATTERN])
93 out_chan, out_ready = Channel(PatternArray).wrap(pattern, valid)
94 ready.assign(out_ready)
95 result_wire.assign(out_chan)
96
97
98class BytePatternEchoEq(Module):
99 """Function ``byte_pattern_echo_eq``: array<ui8, 8> -> ui8.
100
101 Compares the incoming 8-byte array against the same constant pattern as
102 :class:`BytePatternConst` and returns ``1`` if every element matches,
103 ``0`` otherwise. The driver writes **raw wire bytes** straight into the
104 arg channel, taking the host-side serializer out of the loop. A test
105 failure means the bytes the runtime put on the wire disagree with the
106 spec, not that the host SW ``de``serializer also misread them.
107 """
108
109 clk = Clock()
110 rst = Reset()
111
112 @generator
113 def construct(ports):
114 result_wire = Wire(Channel(UInt(8)))
115 args = esi.FuncService.get_call_chans(AppID("byte_pattern_echo_eq"),
116 arg_type=PatternArray,
117 result=result_wire)
118
119 ready = Wire(Bits(1))
120 arg, valid = args.unwrap(ready)
121
122 matches = [arg[i] == UInt(8)(b) for i, b in enumerate(_BYTE_PATTERN)]
123 all_match = matches[0]
124 for m in matches[1:]:
125 all_match = all_match & m
126 # ``Mux(sel, false_val, true_val)``: when ``sel`` is 1, pick the last arg.
127 result = Mux(all_match, UInt(8)(0), UInt(8)(1))
128
129 out_chan, out_ready = Channel(UInt(8)).wrap(result, valid)
130 ready.assign(out_ready)
131 result_wire.assign(out_chan)
132
133
134class SignResult(Struct):
135 plus_one: SInt(16)
136 neg: SInt(16)
137 sign_bit: UInt(1)
138
139
140class SignResult13(Struct):
141 plus_one: SInt(13)
142 neg: SInt(13)
143 sign_bit: UInt(1)
144
145
146class SignProbe(Module):
147 """Function ``sign_probe``: si16 -> {plus_one, neg, sign_bit}.
148
149 Returns ``arg+1``, ``-arg`` and ``arg<0`` in a single struct. Exercises
150 two's-complement addition, negation and MSB extraction at a sub-32-bit
151 width. A host that confuses signed/unsigned encoding or sign-extends
152 incorrectly will see a wrong ``plus_one`` near the boundaries
153 (``INT16_MIN``/``INT16_MAX``).
154 """
155
156 clk = Clock()
157 rst = Reset()
158
159 @generator
160 def construct(ports):
161 result_wire = Wire(Channel(SignResult))
162 args = esi.FuncService.get_call_chans(AppID("sign_probe"),
163 arg_type=SInt(16),
164 result=result_wire)
165
166 ready = Wire(Bits(1))
167 arg, valid = args.unwrap(ready)
168
169 plus_one = (arg + SInt(16)(1)).as_sint(16)
170 neg = (-arg).as_sint(16)
171 sign_bit = arg.as_bits(16)[15].as_uint(1)
172 result = SignResult(plus_one=plus_one, neg=neg, sign_bit=sign_bit)
173 out_chan, out_ready = Channel(SignResult).wrap(result, valid)
174 ready.assign(out_ready)
175 result_wire.assign(out_chan)
176
177
178class SignProbe13(Module):
179 """Function ``sign_probe13``: si13 -> {plus_one: si13, neg: si13, sign_bit: ui1}.
180
181 The non-byte-aligned cousin of :class:`SignProbe`. si13 occupies a 2-byte
182 wire slot but only bit 12 carries the sign, with bits 13..15 acting as
183 padding. A host that:
184
185 - uses bit 15 (instead of bit 12) as the sign bit, or
186 - forgets to sign-extend the padding bits on read, or
187 - leaves uninitialised garbage in the padding bits on write,
188
189 will produce wrong ``plus_one`` / ``neg`` values for negative inputs and
190 for inputs near the si13 boundaries (-4096 / 4095). This catches
191 width-bounded sign-extension bugs that si16 would never trip.
192 """
193
194 clk = Clock()
195 rst = Reset()
196
197 @generator
198 def construct(ports):
199 result_wire = Wire(Channel(SignResult13))
200 args = esi.FuncService.get_call_chans(AppID("sign_probe13"),
201 arg_type=SInt(13),
202 result=result_wire)
203
204 ready = Wire(Bits(1))
205 arg, valid = args.unwrap(ready)
206
207 plus_one = (arg + SInt(13)(1)).as_sint(13)
208 neg = (-arg).as_sint(13)
209 sign_bit = arg.as_bits(13)[12].as_uint(1)
210 result = SignResult13(plus_one=plus_one, neg=neg, sign_bit=sign_bit)
211 out_chan, out_ready = Channel(SignResult13).wrap(result, valid)
212 ready.assign(out_ready)
213 result_wire.assign(out_chan)
214
215
216class PackStruct(Struct):
217 a: UInt(8)
218 b: UInt(16)
219 c: UInt(8)
220 d: UInt(32)
221
222
223class PackProbe(Module):
224 """Function ``pack_probe``: PackStruct -> PackStruct.
225
226 XORs each field with a unique sentinel of the same width. Sentinels
227 (``0xA0``, ``0xB000``, ``0xC0``, ``0xD0000000``) make every byte of the
228 reply unique and per-field-distinguishable, so a host that swaps fields
229 during packing produces a visibly wrong reply rather than a same-bit-width
230 false positive. Catches struct-field order and inter-field padding bugs;
231 CIRCT structs are LSB-first on the wire.
232 """
233
234 clk = Clock()
235 rst = Reset()
236
237 @generator
238 def construct(ports):
239 result_wire = Wire(Channel(PackStruct))
240 args = esi.FuncService.get_call_chans(AppID("pack_probe"),
241 arg_type=PackStruct,
242 result=result_wire)
243
244 ready = Wire(Bits(1))
245 arg, valid = args.unwrap(ready)
246
247 a = (arg["a"].as_bits(8) ^ Bits(8)(0xA0)).as_uint(8)
248 b = (arg["b"].as_bits(16) ^ Bits(16)(0xB000)).as_uint(16)
249 c = (arg["c"].as_bits(8) ^ Bits(8)(0xC0)).as_uint(8)
250 d = (arg["d"].as_bits(32) ^ Bits(32)(0xD0000000)).as_uint(32)
251 result = PackStruct(a=a, b=b, c=c, d=d)
252 out_chan, out_ready = Channel(PackStruct).wrap(result, valid)
253 ready.assign(out_ready)
254 result_wire.assign(out_chan)
255
256
257class BitPackArg(Struct):
258 x: UInt(3)
259 y: UInt(5)
260 z: UInt(4)
261 w: UInt(4)
262
263
264class BitPackResult(Struct):
265 # Same widths as BitPackArg, but rotated: the original ``x`` lands in the
266 # ``w_field`` slot, etc. Distinct field names so the rotation cannot be
267 # confused for an identity transform by accident.
268 w_field: UInt(3)
269 z_field: UInt(5)
270 y_field: UInt(4)
271 x_field: UInt(4)
272
273
274class BitPackProbe(Module):
275 """Function ``bit_pack_probe``: BitPackArg -> BitPackResult.
276
277 Returns ``{w, z, y, x}`` packed back into a 16-bit-wide struct that uses
278 the same widths as the argument but in reverse field order. Each field has
279 a unique width, and the chosen sentinel values used by the test driver
280 (``x=0b001``, ``y=0b10001``, ``z=0b1001``, ``w=0b1100``) are unique within
281 their own width, so a misaligned shift or wrong field offset produces a
282 wrong number rather than a coincidentally-matching one.
283 """
284
285 clk = Clock()
286 rst = Reset()
287
288 @generator
289 def construct(ports):
290 result_wire = Wire(Channel(BitPackResult))
291 args = esi.FuncService.get_call_chans(AppID("bit_pack_probe"),
292 arg_type=BitPackArg,
293 result=result_wire)
294
295 ready = Wire(Bits(1))
296 arg, valid = args.unwrap(ready)
297
298 # Move each input field into the output slot whose width matches.
299 result = BitPackResult(
300 w_field=arg["x"], # 3 bits
301 z_field=arg["y"], # 5 bits
302 y_field=arg["z"], # 4 bits
303 x_field=arg["w"], # 4 bits
304 )
305 out_chan, out_ready = Channel(BitPackResult).wrap(result, valid)
306 ready.assign(out_ready)
307 result_wire.assign(out_chan)
308
309
310ArrayProbeArg = UInt(8) * 4
311ArrayProbeResult = UInt(8) * 4
312
313
314class ArrayProbe(Module):
315 """Function ``array_probe``: array<ui8, 4> -> array<ui8, 4>.
316
317 Returns ``[arg[0]+10, arg[1]+20, arg[2]+30, arg[3]+40]``. CIRCT arrays
318 serialize element-reversed on the wire while lists do not, so this
319 exercises a serializer path distinct from the list-based tests. A host
320 that forgets to reverse on (de)serialize will see, e.g., ``arg=[1,2,3,4]``
321 echoed back as ``[41, 32, 23, 14]`` instead of ``[11, 22, 33, 44]``.
322 """
323
324 clk = Clock()
325 rst = Reset()
326
327 @generator
328 def construct(ports):
329 result_wire = Wire(Channel(ArrayProbeResult))
330 args = esi.FuncService.get_call_chans(AppID("array_probe"),
331 arg_type=ArrayProbeArg,
332 result=result_wire)
333
334 ready = Wire(Bits(1))
335 arg, valid = args.unwrap(ready)
336
337 # Per-index sentinels (10, 20, 30, 40) make the element order observable.
338 # Build the output in increasing-index order; the C++ host driver
339 # asserts ``out[i] == arg[i] + 10*(i+1)``.
340 plus = [(arg[i] + UInt(8)(10 * (i + 1))).as_uint(8) for i in range(4)]
341 out_array = ArrayProbeResult(plus)
342 out_chan, out_ready = Channel(ArrayProbeResult).wrap(out_array, valid)
343 ready.assign(out_ready)
344 result_wire.assign(out_chan)
345
346
347class Top(Module):
348 clk = Clock()
349 rst = Reset()
350
351 @generator
352 def construct(ports):
353 ByteRotate1(clk=ports.clk, rst=ports.rst, appid=AppID("byte_rotate1_inst"))
354 BytePatternConst(clk=ports.clk,
355 rst=ports.rst,
356 appid=AppID("byte_pattern_const_inst"))
357 BytePatternEchoEq(clk=ports.clk,
358 rst=ports.rst,
359 appid=AppID("byte_pattern_echo_eq_inst"))
360 SignProbe(clk=ports.clk, rst=ports.rst, appid=AppID("sign_probe_inst"))
361 SignProbe13(clk=ports.clk, rst=ports.rst, appid=AppID("sign_probe13_inst"))
362 PackProbe(clk=ports.clk, rst=ports.rst, appid=AppID("pack_probe_inst"))
363 BitPackProbe(clk=ports.clk,
364 rst=ports.rst,
365 appid=AppID("bit_pack_probe_inst"))
366 ArrayProbe(clk=ports.clk, rst=ports.rst, appid=AppID("array_probe_inst"))
367
368
369if __name__ == "__main__":
370 bsp = get_bsp(sys.argv[2] if len(sys.argv) > 2 else None)
371 s = System(bsp(Top), name="SerializationProbes", output_directory=sys.argv[1])
372 s.compile()
373 s.package()
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))