1"""Hardware design for serialization-correctness probes.
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.
16import pycde.esi
as esi
17from pycde
import AppID, Clock, Module, Reset, System, generator
19from pycde.constructs
import Mux, Wire
20from pycde.signals
import BitsSignal, Struct
21from pycde.types
import Bits, Channel, SInt, UInt
25 """Function ``byte_rotate1``: ui64 -> ui64.
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.
40 result_wire = Wire(Channel(UInt(64)))
41 args = esi.FuncService.get_call_chans(AppID(
"byte_rotate1"),
46 arg, valid = args.unwrap(ready)
51 arg_bits = arg.as_bits(64)
52 rotated = BitsSignal.concat([arg_bits[0:56], arg_bits[56:64]]).as_uint()
54 out_chan, out_ready = Channel(UInt(64)).
wrap(rotated, valid)
55 ready.assign(out_ready)
56 result_wire.assign(out_chan)
59PatternArray = UInt(8) * 8
64_BYTE_PATTERN = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]
68 """Function ``byte_pattern_const``: ui8 -> array<ui8, 8>.
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.
84 result_wire = Wire(Channel(PatternArray))
85 args = esi.FuncService.get_call_chans(AppID(
"byte_pattern_const"),
90 _, valid = args.unwrap(ready)
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)
99 """Function ``byte_pattern_echo_eq``: array<ui8, 8> -> ui8.
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.
114 result_wire = Wire(Channel(UInt(8)))
115 args = esi.FuncService.get_call_chans(AppID(
"byte_pattern_echo_eq"),
116 arg_type=PatternArray,
119 ready = Wire(Bits(1))
120 arg, valid = args.unwrap(ready)
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
127 result = Mux(all_match, UInt(8)(0), UInt(8)(1))
129 out_chan, out_ready = Channel(UInt(8)).
wrap(result, valid)
130 ready.assign(out_ready)
131 result_wire.assign(out_chan)
147 """Function ``sign_probe``: si16 -> {plus_one, neg, sign_bit}.
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``).
161 result_wire = Wire(Channel(SignResult))
162 args = esi.FuncService.get_call_chans(AppID(
"sign_probe"),
166 ready = Wire(Bits(1))
167 arg, valid = args.unwrap(ready)
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)
179 """Function ``sign_probe13``: si13 -> {plus_one: si13, neg: si13, sign_bit: ui1}.
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:
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,
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.
199 result_wire = Wire(Channel(SignResult13))
200 args = esi.FuncService.get_call_chans(AppID(
"sign_probe13"),
204 ready = Wire(Bits(1))
205 arg, valid = args.unwrap(ready)
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)
224 """Function ``pack_probe``: PackStruct -> PackStruct.
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.
239 result_wire = Wire(Channel(PackStruct))
240 args = esi.FuncService.get_call_chans(AppID(
"pack_probe"),
244 ready = Wire(Bits(1))
245 arg, valid = args.unwrap(ready)
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)
252 out_chan, out_ready = Channel(PackStruct).
wrap(result, valid)
253 ready.assign(out_ready)
254 result_wire.assign(out_chan)
275 """Function ``bit_pack_probe``: BitPackArg -> BitPackResult.
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.
290 result_wire = Wire(Channel(BitPackResult))
291 args = esi.FuncService.get_call_chans(AppID(
"bit_pack_probe"),
295 ready = Wire(Bits(1))
296 arg, valid = args.unwrap(ready)
305 out_chan, out_ready = Channel(BitPackResult).
wrap(result, valid)
306 ready.assign(out_ready)
307 result_wire.assign(out_chan)
310ArrayProbeArg = UInt(8) * 4
311ArrayProbeResult = UInt(8) * 4
315 """Function ``array_probe``: array<ui8, 4> -> array<ui8, 4>.
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]``.
329 result_wire = Wire(Channel(ArrayProbeResult))
330 args = esi.FuncService.get_call_chans(AppID(
"array_probe"),
331 arg_type=ArrayProbeArg,
334 ready = Wire(Bits(1))
335 arg, valid = args.unwrap(ready)
340 plus = [(arg[i] + UInt(8)(10 * (i + 1))).as_uint(8)
for i
in range(4)]
342 out_chan, out_ready = Channel(ArrayProbeResult).
wrap(out_array, valid)
343 ready.assign(out_ready)
344 result_wire.assign(out_chan)
353 ByteRotate1(clk=ports.clk, rst=ports.rst, appid=AppID(
"byte_rotate1_inst"))
356 appid=AppID(
"byte_pattern_const_inst"))
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"))
365 appid=AppID(
"bit_pack_probe_inst"))
366 ArrayProbe(clk=ports.clk, rst=ports.rst, appid=AppID(
"array_probe_inst"))
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])
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))