Register tables are usually reviewed in a document, where a duplicated offset or a reset value that does not fit its field hides in plain sight until a driver trips over it. The same table as data can be checked exhaustively in milliseconds: alignment, uniqueness, width, reset fit and access-type sanity for every row. This is the register-level twin of the map checker from the design topic, and it runs before any RTL or header is generated from the table.
Five rules cover almost every specification bug. Offsets are word aligned and unique within their block. Every field fits inside its declared register width and no two fields in one register overlap. The reset value fits inside the implemented width. The access type is one of the five the checker knows: read-write, read-only, write-1-to-clear, write-only, or mixed, where a register's fields carry different types and are checked field by field. And a write-only register still declares a width and a reset behaviour, usually acts-and-forgets, so the documentation never promises a readback. The checker below encodes the DMA and SBR tables from the design specification, validates all five rules, then proves it is awake by catching a row whose reset value is wider than its field.
# (block, register, offset, fields, access, width_bits, reset)
SPEC = [
("DMA", "CTRL", 0x00, [(0, 0, "GO"), (1, 1, "IRQ_EN")], "RW", 2, 0x0),
("DMA", "STATUS", 0x04, [(0, 0, "BUSY"), (1, 1, "DONE"), (2, 2, "ERR")], "MIX", 3, 0x0),
("DMA", "LEN", 0x08, [(15, 0, "WORDS")], "RW", 16, 0x0),
("DMA", "KEY", 0x0C, [(31, 0, "KEY")], "RW", 32, 0x0),
("DMA", "COUNT", 0x10, [(15, 0, "WORDS")], "RO", 16, 0x0),
("SBR", "SRC_ADDR", 0x00, [(31, 0, "ADDR")], "RW", 32, 0x2000_0000),
("SBR", "DST_ADDR", 0x04, [(31, 0, "ADDR")], "RW", 32, 0x2001_0000),
("SBR", "LEN", 0x08, [(15, 0, "WORDS")], "RW", 16, 0x0),
("SBR", "CTRL", 0x0C, [(0, 0, "START"), (1, 1, "IRQ_EN"), (2, 2, "NS")], "RW", 3, 0x4),
("SBR", "STATUS", 0x10, [(0, 0, "BUSY"), (1, 1, "DONE"), (2, 2, "ERR")], "MIX", 3, 0x0),
]
ACCESSES = {"RW", "RO", "W1C", "WO", "MIX"}
def check(spec):
problems = []
seen = set()
for block, name, off, fields, acc, width, reset in spec:
if off % 4 != 0:
problems.append(f"{block}.{name}: offset {off:#x} not word aligned")
if (block, off) in seen:
problems.append(f"{block}.{name}: duplicate offset {off:#x}")
seen.add((block, off))
if acc not in ACCESSES:
problems.append(f"{block}.{name}: unknown access {acc}")
if width > 32 or reset >= (1 << width):
problems.append(f"{block}.{name}: reset {reset:#x} exceeds width {width}")
used = 0
for hi, lo, _fname in fields:
if hi < lo or hi > 31:
problems.append(f"{block}.{name}: bad field [{hi}:{lo}]")
if hi >= width:
problems.append(f"{block}.{name}: field [{hi}:{lo}] exceeds width {width}")
bits = ((1 << (hi - lo + 1)) - 1) << lo
if used & bits:
problems.append(f"{block}.{name}: overlapping fields")
used |= bits
return problems
assert check(SPEC) == []
bad = SPEC + [("SBR", "BROKEN", 0x14, [(7, 0, "X")], "RW", 8, 0x1FF)]
hits = check(bad)
assert len(hits) == 1 and "exceeds width" in hits[0]
print("PASS: DMA + SBR tables valid; oversized reset value caught")The checker output for the current specification version is attached to the integration review, like the map checker output.
Every block in every slot appears in the table; a block with no rows is a finding, not an omission.
Fields named in prose documentation exist in the table with the same widths, because both are generated from it.
Run the checker whenever the table changes, and treat a clean run as the entry ticket for generating RTL, headers and documentation. When it reports a problem, fix the table, never the generated file. The failure it cannot see is semantic: a correct-width field with the wrong meaning, which is why the next lesson builds the behaviour and the verification topic exercises every access type from software.
The reset-fit rule: 0x0001_0000 needs 17 bits and does not fit in a 16-bit field, so the stored value would silently truncate at reset. Either widen the field or correct the reset value in the table.