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.

Model and DUT disagree on a misaligned write
01234pselpaddr100010041001100c1010pslverr (DUT)model_idx0103-1model: CTRL; DUT: error

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.

AddressIndexRegister
0x10000CTRL
0x10041STATUS
0x10082LOAD
0x100C3COUNT

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

  1. Reject unknown bits

    Test $isunknown(addr) first, while addr is still four-state. An x bit means you cannot know which register was meant.

  2. 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.

    0x1008off = 8
    0x1010off = 16, rejected
  3. Check 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.

    0x1001off[1:0] = 01, rejected
    0x1008off[1:0] = 00
  4. Compute the index

    Shift the offset right by 2, which divides it by 4 and gives the register index.

    off = 8index 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.

AddressWhy it is in the listExpected
0x1000first register0
0x100Clast register3
0x0FFCone register below the block-1
0x1010first address past the block-1
0x1001, 0x1002, 0x1003each misaligned low-bit pattern-1
0xFFFF_FFFChuge address; wraps if treated as signed-1
0x0000_100xunknown 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.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: 00001000 -> 0 00001004 -> 1 00001008 -> 2 0000100c -> 3 00000ffc -> -1 00001010 -> -1 00001001 -> -1 00001002 -> -1 fffffffc -> -1 0000100x -> -1 0 of 10 cases wrong

Check yourself

Predict the outputThis decoder checks the range but not alignment. Predict the exact output.
module tb;
  function automatic int naive_decode(logic [31:0] addr);
    if (addr < 32'h1000 || addr >= 32'h1010) return -1;
    return (addr - 32'h1000) >> 2;
  endfunction
  initial $display("%0d", naive_decode(32'h100B));
endmodule
ConceptIs 0x1010 a valid address for this timer?
Read the waveformThe reference model and the DUT disagree in one cycle. Which statement is correct?
Decoder model against the DUT
0123paddr1004100c10061008pslverr (DUT)model_idx1312

pslverr = 1 means the DUT rejected the access.

Spot the bugEach option finishes decode_reg after the unknown and range checks, where off holds addr - 32'h1000 and is below 16. Which option accepts a misaligned address?
ConceptA monitor passes the address to the decoder as an int unsigned. The DUT drives 32'h0000_100x. What does the decoder return?

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