Four inputs send DCRouter random packets of random lengths to random outputs, some with corrupted CRCs, some with illegal lengths. Thousands of flits leave the outputs, interleaved in an order decided cycle by cycle by four arbiters. What should have come out? Writing the answer by hand is impossible for one test, let alone a regression. A program has to compute it, from the specification, for any input.
What should a reference model predict, and how precise does it need to be?
The obvious reference model copies the design's behaviour cycle by cycle, so its outputs can be compared flit for flit. Such a model is nearly a second RTL implementation: as hard to write, as likely to contain bugs, and bound to one micro-architecture, so every legal design change breaks it. It also predicts things the specification never promised, such as exactly which input wins an arbitration in which cycle.
Model level | Predicts | Cost | Use when |
|---|---|---|---|
Transaction level | What leaves where, with which content and flags, in which order per flow | Small, fast, close to the specification | The specification promises results and per-flow order, not timing |
Cycle level | Exact values on every signal in every cycle | Large, slow, tied to one implementation | Timing itself is specified (fixed latency pipelines) |
Hybrid | Transaction results plus bounds on timing | Moderate | Results plus latency or throughput limits |
DCRouter's specification promises that each packet leaves on its output whole, in its input's order, with out_err set by rule, and that the counters follow rules R9 and R10. It does not promise how packets from different inputs interleave. So DCRouter's model is transaction level, organised by flow: one ordered list per (input, output) pair.
The model builds packets exactly as the specification describes, checks its own CRC against the catalogued CRC-8/SMBUS check value (fact M6), then predicts a mixed workload of 24 packets with two corrupted CRCs and one zero-length header.
"""DCRouter step 3: a transaction-level reference model.
Given the packets offered on each input, the model predicts what the specification
promises at the outputs (rules R3, R5, R8, R9, R10):
- which output each packet leaves on (its dst), whole and in its input's order
- out_err on its tail when len is 0 or the CRC is wrong
- ERR_CNT per input (saturating at 255) and PKT_CNT per output
It deliberately does not predict how packets from different inputs interleave at an
output: the specification leaves arbitration order to the design, so a transaction-level
model must not pin it down. First the model's CRC is checked against the catalogued
CRC-8/SMBUS check value, then it predicts a small mixed workload.
"""
import random
def crc8_byte(c, b):
c ^= b
for _ in range(8):
c = ((c << 1) ^ 0x07) & 0xFF if c & 0x80 else (c << 1) & 0xFF
return c
def crc8_flits(flits): # each 16-bit flit, high byte first (DCRouter spec)
c = 0
for f in flits:
c = crc8_byte(crc8_byte(c, f >> 8), f & 0xFF)
return c
def make_packet(src, seq, dst, prio, length, payload, bad_crc=False):
header = (dst << 14) | (prio << 13) | (length << 9) | (src << 7) | seq
flits = [header] + payload
c = crc8_flits(flits)
return flits + [(c ^ 0xFF) if bad_crc else c]
def predict(inputs):
"""inputs: list of 4 lists of packets (lists of flits). Returns per-output streams and counters."""
per_flow = {} # (src, dst) -> list of (flits, err) in order
err_cnt = [0] * 4
pkt_cnt = [0] * 4
for src, packets in enumerate(inputs):
for p in packets:
header, tail = p[0], p[-1]
dst, length = header >> 14, (header >> 9) & 0xF
err = length == 0 or (tail & 0xFF) != crc8_flits(p[:-1])
per_flow.setdefault((src, dst), []).append((p, err))
pkt_cnt[dst] += 1
if err:
err_cnt[src] = min(255, err_cnt[src] + 1)
return per_flow, err_cnt, pkt_cnt
check = crc8_flits([0x3132, 0x3334, 0x3536, 0x3738])
check = crc8_byte(check, 0x39)
print(f"CRC-8 of \"123456789\": 0x{check:02X} (catalogued check value 0xF4)")
rng = random.Random(9)
inputs = [[], [], [], []]
for src in range(4):
for seq in range(6):
length = rng.choice([0, 1, 15]) if seq == 0 else rng.randint(1, 15)
inputs[src].append(make_packet(src, seq, rng.randrange(4), rng.randrange(2), length,
[rng.randrange(1 << 16) for _ in range(length)],
bad_crc=(src == 1 and seq in (2, 4))))
flows, err_cnt, pkt_cnt = predict(inputs)
for (src, dst), pkts in sorted(flows.items()):
print(f"input {src} -> output {dst}: {len(pkts)} packet{'s' if len(pkts) != 1 else ' '}, seq " +
" ".join(f"{p[0] & 0x7F}{'!' if e else ''}" for p, e in pkts))
print(f"predicted ERR_CNT {err_cnt} PKT_CNT {pkt_cnt}")
assert check == 0xF4
assert sum(pkt_cnt) == 24 and sum(len(v) for v in flows.values()) == 24
assert err_cnt[1] >= 2
print("PASS: the model's CRC matches CRC-8/SMBUS, and it accounts for all 24 packets, their outputs, errors and counters")It prints:
CRC-8 of "123456789": 0xF4 (catalogued check value 0xF4)
input 0 -> output 0: 3 packets, seq 1 2 3
input 0 -> output 2: 2 packets, seq 0 4
input 0 -> output 3: 1 packet , seq 5
input 1 -> output 0: 1 packet , seq 3
input 1 -> output 1: 2 packets, seq 1 2!
input 1 -> output 2: 2 packets, seq 4! 5
input 1 -> output 3: 1 packet , seq 0
input 2 -> output 0: 2 packets, seq 1 3
input 2 -> output 1: 2 packets, seq 0 2
input 2 -> output 3: 2 packets, seq 4 5
input 3 -> output 0: 4 packets, seq 0! 1 3 5
input 3 -> output 1: 1 packet , seq 2
input 3 -> output 2: 1 packet , seq 4
predicted ERR_CNT [0, 2, 0, 1] PKT_CNT [10, 5, 5, 4]
PASS: the model's CRC matches CRC-8/SMBUS, and it accounts for all 24 packets, their outputs, errors and countersHow to read it: the first line validates the oracle itself, because a model with a wrong CRC would flag every good packet. Each flow line lists the packets that must leave that output from that input, in order, with ! marking those that must carry out_err (corrupted CRC or length 0). The last line gives the counters the registers must show. The model says nothing about how flows interleave at each output; a scoreboard built on it (Part 5) matches packets per flow and so accepts every legal arbitration order.
Validate it independently: against published check values, hand-worked examples, or a second team's numbers.
Write it from the specification, not from the RTL, and by someone other than the designer.
Predict only what is promised: anything extra becomes a false failure when the design legally differs (lesson 2.7).
Keep it simple enough to review: a model as complex as the design is just another design.
Practice | Purpose |
|---|---|
Transaction-level models in Python, C++ or SystemVerilog classes | Fast to write, independent of the micro-architecture |
Models connected through DPI or monitors | Fed the same transactions the design sees |
Algorithm teams' golden models for numerical blocks | An independent source for arithmetic results |
A router team (illustrative) first wrote a cycle-accurate model that predicted exactly which input each arbiter would grant. When designers improved the arbiter's timing, legally, thousands of tests failed overnight with mismatches that were not bugs. Rewriting the model at transaction level, matching per flow, removed every false failure and still caught the real ones, which were packets lost, corrupted or misordered within a flow.
The specification fixes the order within each flow but not how flows interleave at an output, so per-flow lists predict exactly what is promised.
A wrong CRC in the model would flag every correct packet as an error, so the oracle itself must be validated before it judges the design.
A reference model computes the expected outputs for any input: the oracle for data and results.
Model at the level the specification promises: usually transactions and per-flow order, not cycles.
Validate the model independently and keep it simple enough to review.