A chip has 4,000 registers. The specification lists them in a document, the designers type them into RTL, the verification team types them into a register model, and the software team types them into header files. Four copies of the same table, typed by four teams. Every mismatch between two copies is a bug, and most of them are typing errors, not design decisions.
When a specification is really a table, how should it be written and checked?
The obvious approach writes the table once in the document and lets each team implement and check it by hand. Hand copies drift: a reset value updated in the RTL but not in the document, a field width changed in the header but not in the register model. Checking one hand copy against another finds some errors and enshrines others, and every change must be made four times, correctly.
Machine-readable source: the register map (or state table, opcode table) is written once in a structured format; IP-XACT and SystemRDL are standard formats for register descriptions (fact V4).
Generate everything else: documentation, RTL register blocks, the verification register model and software headers all come from the source.
Check against the source: a generic checker reads the table and checks the design's behaviour, with no register-specific code to get wrong.
One change, one place: updating the source updates every consumer.
Part of a specification | As a table | Checked by |
|---|---|---|
Register map | Address, name, access type, reset value, fields, side effects | A table-driven register checker |
State machine | State, input, next state, outputs | A table-driven transition checker or formal |
Opcode or command set | Code, operands, expected effect | A model that dispatches on the table |
Error responses | Condition, response code, counters affected | Error-injection tests with expected codes from the table |
DCRouter's register map (rules R9 to R12) as a table, and a checker that replays a log of APB transactions against it. The log contains a CTRL read after reset returning 0x37, as Bug Zoo BUG_17 would produce, and a read of an unmapped address answered without pslverr.
"""A table-driven register checker: the register map as data, not as code.
DCRouter's register map (rules R9 to R12) is written once as a table: address, name,
access, reset value, mask, and what a write does. The checker replays a log of APB
transactions against the table: it tracks the expected value of every register, applies
write effects, and compares every read. (In a full testbench, counters that hardware
updates would be predicted by the reference model of lesson 2.3.) The log below contains one planted mistake, a CTRL read after reset returning 0x37
instead of 0x3F (Bug Zoo BUG_17), and a read of an unmapped address.
"""
TABLE = [ # addr, name, access, reset, mask, on write
(0x00, "CTRL", "RW", 0x3F, 0x3F, "store"),
(0x04, "STATUS", "RO", 0x00, 0x1F, "ignore"),
(0x10, "ERR_CNT0", "RC", 0x00, 0xFF, "clear"),
(0x14, "ERR_CNT1", "RC", 0x00, 0xFF, "clear"),
(0x18, "ERR_CNT2", "RC", 0x00, 0xFF, "clear"),
(0x1C, "ERR_CNT3", "RC", 0x00, 0xFF, "clear"),
(0x20, "PKT_CNT0", "RC", 0x0000, 0xFFFF, "clear"),
(0x24, "PKT_CNT1", "RC", 0x0000, 0xFFFF, "clear"),
(0x28, "PKT_CNT2", "RC", 0x0000, 0xFFFF, "clear"),
(0x2C, "PKT_CNT3", "RC", 0x0000, 0xFFFF, "clear"),
]
BY_ADDR = {row[0]: row for row in TABLE}
# a log: (op, addr, data, pslverr); "read" data is what the design returned
LOG = [
("read", 0x00, 0x37, 0), # CTRL after reset: expected 0x3F
("write", 0x00, 0x2F, 0), # priority off
("read", 0x00, 0x2F, 0),
("read", 0x14, 0x00, 0),
("write", 0x14, 0x00, 0), # clear ERR_CNT1
("read", 0x14, 0x00, 0),
("read", 0x40, 0x00, 0), # unmapped: the design must answer pslverr
]
def check(log):
expect = {a: r[3] for a, r in BY_ADDR.items()}
problems = []
for n, (op, addr, data, err) in enumerate(log):
row = BY_ADDR.get(addr)
if row is None:
if not err:
problems.append(f"#{n} {op} 0x{addr:02X}: unmapped address without pslverr (R12)")
continue
_, name, access, reset, mask, on_write = row
if op == "write":
if on_write == "store":
expect[addr] = data & mask
elif on_write == "clear":
expect[addr] = 0
elif (data & mask) != expect[addr]:
problems.append(f"#{n} read {name}: got 0x{data & mask:X}, table says 0x{expect[addr]:X}")
return problems
problems = check(LOG)
print(f"register table: {len(TABLE)} registers, log of {len(LOG)} transactions")
for p in problems:
print(" " + p)
assert problems == [
"#0 read CTRL: got 0x37, table says 0x3F",
"#6 read 0x40: unmapped address without pslverr (R12)",
]
print("PASS: the table-driven checker found the wrong CTRL reset value and the missing pslverr, with no register-specific code")It prints:
register table: 10 registers, log of 7 transactions
#0 read CTRL: got 0x37, table says 0x3F
#6 read 0x40: unmapped address without pslverr (R12)
PASS: the table-driven checker found the wrong CTRL reset value and the missing pslverr, with no register-specific codeHow to read it: the checker knows nothing about DCRouter beyond the table. It finds the wrong reset value (port 3 disabled) and the missing error response for address 0x40, and accepts the write to CTRL and the clear of ERR_CNT1. Adding a register means adding a row, not writing a new check.
Works well for | Needs more than a table |
|---|---|
Reset values, access types, address decoding | Side effects that depend on hardware events (counters, status) |
Field layouts and masks | Timing between a write and its effect |
Simple write effects (store, clear, ignore) | Behaviour that depends on history or other registers |
Hardware-updated values, such as DCRouter's error and packet counters, need the reference model (lesson 2.3) to predict them; the table says how they are accessed, the model says what they should contain.
Practice | Purpose |
|---|---|
Register descriptions in IP-XACT or SystemRDL | One source for documents, RTL, register models and headers |
Generated register models in the testbench (for example the UVM register layer, fact V4) | Table-driven checks of every register |
State tables reviewed as data | Missing and duplicate transitions are visible |
A project (illustrative) typed its 2,000-register map into RTL and into its verification model by hand. A late change moved a field in one register; the RTL was updated, the model was not, and the document was updated a week later. The verification model flagged a mismatch, the team "fixed" the RTL to match the stale model, and the software, written from the updated document, failed in the lab. Generating all three from one source would have made the inconsistency impossible.
The expected reset value is a row in the table; the generic checker compares every register's first read with its row.
The values of hardware-updated registers (ERR_CNT, PKT_CNT, STATUS), which depend on traffic, not on writes.
Much of a specification is data: registers, states, opcodes, error codes.
Write it once as a machine-readable table and generate documents, RTL, models and checkers from it.
Table-driven checkers cover access and reset behaviour; reference models cover hardware-updated values.