Problem: decode a register address
Specify and build a register decoder that rejects misaligned, out-of-range and unknown addresses, then test it at every edge.
- Lesson
- 1.6
- Time
- 25 min
- Runs on
- Your browser
- Check
- 5 questions
A timer test reports a DUT bug, because a write to 0x1001 came back with an error response, but the DUT was right. The reference decoder in the testbench computed (addr - 32'h1000) >> 2, which rounds 0x1001 down to register 0, so the model predicted a normal write to CTRL. A wrong model costs as much time as a wrong DUT, because someone always debugs the RTL first.
The model rejects addresses outside the block but never checks alignment.
The register map
The timer has four 32-bit registers, and because each one is 4 bytes wide, consecutive registers sit 4 addresses apart, starting at 0x1000.
| Address | Index | Register |
|---|---|---|
| 0x1000 | 0 | CTRL |
| 0x1004 | 1 | STATUS |
| 0x1008 | 2 | LOAD |
| 0x100C | 3 | COUNT |
Timer register map.
Write a function decode_reg(addr) that returns the index 0 to 3 for these four addresses and -1 for anything else. Anything else covers three groups: addresses outside the block, addresses inside it that are not multiples of 4, and addresses with unknown bits.
The DUT answers an invalid access with an error response on pslverr, and your model has to predict that error. So -1 is as important an output as a valid index, and a model that never returns it cannot check the error path at all.
Before writing code, state the rule in one sentence: an address is valid when it lies in [0x1000, 0x1010) and is a multiple of 4. Each part of the sentence becomes one check, and each check needs test addresses on both sides of it.
Three checks, then the index
Reject unknown bits
Test
$isunknown(addr)first, whileaddris still four-state. Anxbit means you cannot know which register was meant.Check the range
Subtract the base with
off = addr - 32'h1000. The block covers the half-open range [0x1000, 0x1010), where 0x1000 is inside and 0x1010 is the first address outside, so valid offsets are 0 to 15.0x1008 off = 80x1010 off = 16, rejectedCheck alignment
A register address is a multiple of 4, so the two lowest bits of the offset are 0. If
off[1:0]is anything else, the access is misaligned.0x1001 off[1:0] = 01, rejected0x1008 off[1:0] = 00Compute the index
Shift the offset right by 2, which divides it by 4 and gives the register index.
off = 8 index 2
The order matters, because shifting first throws away the two low bits, and with them the only sign of a misaligned access. That is exactly the bug in the opening story. The unknown-bit check comes first for a similar reason: every later step would treat an x as some ordinary number.
Why the type of the offset matters
Keep addr and off unsigned, for example as logic [31:0]. Then address 0x0FFC gives off = 32'hFFFF_FFFC, a very large number that fails off < 16, so one unsigned comparison checks both ends of the range.
Declare off as int instead and the same subtraction gives -4, which passes the range check because -4 is less than 16. The low bits of -4 are 00, so alignment passes too, and the shift returns 1,073,741,823.
The four-state check must also come before any conversion. If addr were copied into an int unsigned first, 32'h0000_100x would become 0x1000 and decode as CTRL.
Range check, then shift
if (addr < 32'h1000 || addr >= 32'h1010)
return -1;
return (addr - 32'h1000) >> 2;
Addresses 0x1001, 0x1002 and 0x1003 all decode as CTRL.
Unknown, range and alignment checks
if ($isunknown(addr)) return -1;
off = addr - 32'h1000; // logic [31:0]
if (off >= 16) return -1;
if (off[1:0] != 2'b0) return -1;
return off >> 2;
Returns -1 for every misaligned, out-of-range or unknown address.
Choose test addresses at the edges
A decoder that returns 2 for 0x1008 has proved very little, because bugs live at the edges. Pick addresses on both sides of every decision the decoder makes.
| Address | Why it is in the list | Expected |
|---|---|---|
| 0x1000 | first register | 0 |
| 0x100C | last register | 3 |
| 0x0FFC | one register below the block | -1 |
| 0x1010 | first address past the block | -1 |
| 0x1001, 0x1002, 0x1003 | each misaligned low-bit pattern | -1 |
| 0xFFFF_FFFC | huge address; wraps if treated as signed | -1 |
| 0x0000_100x | unknown bit | -1 |
Edge cases for the decoder.
Each row catches a different broken decoder. If you drop a row, one kind of bug can pass the whole test unnoticed.
Write the expected results from the spec before you run the decoder. Expected values copied from the decoder's own output only prove that the decoder agrees with itself.
Let the map grow
Register blocks grow, so suppose the timer gains four more registers next quarter. If the decoder spells out 16 and 0x1010 as literals, every copy must be found and edited, and one missed copy is a new bug.
Derive the numbers instead, with localparam int NREGS = 4; and localparam logic [31:0] SPAN = 4 * NREGS;. The range check becomes off >= SPAN, while the alignment check and the shift stay the same, because each register is still 4 bytes wide.
Half-open ranges help here as well, because the end of the block is simply BASE + SPAN. There is no "minus one" to forget when the size changes.
Try it
Every edge case from the table decodes correctly. Now break the decoder on purpose: delete the alignment line and run it. Rows 0x1001 and 0x1002 should change, and the count of wrong cases should rise to 2.
Then restore it, and change off to int. Two rows change: predict which two, and what they print, before you run it.
00001000 -> 0 00001004 -> 1 00001008 -> 2 0000100c -> 3 00000ffc -> -1 00001010 -> -1 00001001 -> -1 00001002 -> -1 fffffffc -> -1 0000100x -> -1 0 of 10 cases wrongCheck yourself
Remember
- Write the rule as a half-open range plus an alignment check: [0x1000, 0x1010) and the two low bits 0.
- Check for unknown bits while the address is still four-state, before any copy into a two-state type.
- Check alignment before shifting; the shift throws away the bits that show a misaligned access.
- Keep offsets unsigned, so one comparison rejects addresses on both sides of the block.
- Test both sides of every decision: first and last register, just outside, each misaligned pattern, and x.
Practice
- Register index Implement · Easy
- Addresses that catch broken decoders Break it · Medium