Registers (CSRs)
Generate a fixed set of checks per access type from a register map, run them on an APB completer with the bus rules as assumptions, and catch a W1C field built as plain read-write.
- Lesson
- F5.4
- Time
- 18 min
- Runs on
- SymbiYosys
- Check
- 4 questions
An interrupt-status register was specified write-one-to-clear and built as plain read-write. Every test passed for months, because the tests cleared it by writing zero, which works on both. In the field an interrupt that arrived during the handler was written over and lost.
Measured with smtbmc yices at depth 20, the write-0 row only. The write completes at step 2 with the set bit's data bit at 0; at step 3 irq_stat is 0. Reported at 4.
A Row in the Map, a Few Fixed Checks
| CSV row | Generated property (SVA) |
|---|---|
0x108 IRQ_STAT W1C, write 0 | wr(108) |=> bits written 0 and not set by hardware keep their value |
0x108 IRQ_STAT W1C, write 1 | wr(108) |=> bits written 1 and not set by hardware are 0 |
0x108 IRQ_STAT, hardware set | irq_in != 0 |=> every bit set last cycle is 1 |
0x10C IRQ_EN RW1S | wr(10C) |=> irq_en == $past(irq_en) | data |
0x100 CTRL RW, byte strobes | wr(100) |=> each strobed byte is the data, the others unchanged |
| every field: reset value | !presetn |=> ctrl == 0 && irq_en == 0 |
wr(R) is psel && penable && pwrite && paddr == R: the completion cycle (pready is tied high here). Each check reads the register one cycle later, so every property stays one row long.
A register map is a table: offset, field, bits, access type, reset value. Each access type implies the same small set of checks wherever it appears. RW: a write lands. RO: a write changes nothing and a read returns the source. W1C: a 1 clears, a 0 leaves, hardware wins. RW1S: a 1 sets.
The generator reads the map, not the RTL. A property derived from the RTL would check the RTL against itself; the map is the spec.
Why W1C Needs Three Rows
Read the diagram as text
- bit = 0: nothing pending
- bit = 1: an interrupt pending
- irq_in[i] = 1: hardware sets, and wins over a clearing write
- write with data[i] = 1: firmware clears what it handled
- write with data[i] = 0: must leave the bit alone
- plain RW: data[i] = 0 clears: the forbidden arc the story hit
- bit = 0 → irq_in[i] = 1
- irq_in[i] = 1 → bit = 1
- bit = 1 → write with data[i] = 1
- write with data[i] = 1 → bit = 0
- bit = 1 → write with data[i] = 0
- write with data[i] = 0 → plain RW: data[i] = 0 clears: RW only
Two arcs leave "bit = 1": a clearing write of 1, or nothing. The write-0 property catches the third arc that a plain register adds.
The plain read-write block breaks two of the three W1C rows: a 0 clears and a 1 sets. With every row present the solver reported the first it met, the write of 1. Run the write-0 row alone and it reports the story's bug: FAIL at reported step 4.
Tests that write only zeros cannot tell the two blocks apart, and the property that catches the field bug never mentions the test's data at all.
The Bus Rules Are Assumptions
Requester rules, assumed (IHI 0024E)
if (penable) assume(psel && setup_q);
if (setup_q) assume(psel && penable &&
paddr == paddr_q && pwrite == pwrite_q &&
pwdata == pwdata_q);
ACCESS follows SETUP with the same address, direction and data.
What the completer claims
wr && paddr == 12'h108 |=> ((irq_stat ^ $past(irq_stat)) & ~wdata4_q & ~irq_in_q) == 0
One W1C row, checked on every completed write.
The bus side is the neighbour's promise, so it is assumed (F3.1); the register behaviour is the block's claim, so it is asserted. The covers show the assumptions left every write kind possible: a write to IRQ_STAT, one racing a hardware set, a single-lane CTRL write, all reached by step 3.
| Run | Measured result | Time |
|---|---|---|
| Correct block, clocked immediate | BMC PASS to depth 20 | 686 ms |
| Correct block, concurrent props.sv | BMC PASS to depth 20 | 1009 ms |
| W1C as RW, write-0 row only | FAIL at reported step 4 | 1163 ms |
| W1C as RW, every row | FAIL at reported step 4 | 995 ms |
| CTRL resets to 1 | FAIL at reported step 2 | 748 ms |
| CTRL ignores pstrb (immediate) | FAIL at reported step 4 | 536 ms |
| Correct block, prove k = 4 (immediate) | Prove PASS (k = 4) | 533 ms |
| Correct block, abc pdr (concurrent) | Prove PASS (abc pdr) | 791 ms |
Mode bmc, depth 20, smtbmc yices unless named; presetn assumed low in step 0. The engine refuses $past(pwdata) on an input, so props.sv reads registered copies.
The checks are the easy part; the exceptions column is the work. A read that clears, a write that starts a transfer, two offsets for one register, privilege from pprot, a status field the hardware writes on its own path. Each needs a row of its own or an assumption that switches the path off, and a review of that assumption.
Asked in Interviews
| Question | Answer |
|---|---|
| How are registers verified formally? | From the register map: a fixed check set per access type, generated per field, with the bus protocol as assumptions and covers on each write kind. |
| What is hard about it? | Side effects, aliasing, privilege, byte strobes and hardware write paths: the exceptions column of the map. |
| Why not simulation? | Simulation checks the writes the tests make. The W1C bug passed every test that wrote zero. |
Try It
notes column holds what no fixed check covers.offset,field,bits,access,reset,notes
0x100,CTRL,31:0,RW,0x00000000,byte strobes
0x104,STATUS,3:0,RO,-,reads fcount
0x108,IRQ_STAT,3:0,W1C,0x0,set by irq_in; a hardware set wins
0x10C,IRQ_EN,3:0,RW1S,0x0,
0x110,ID,31:0,RO,0x43505231,constantprops.sv: the concurrent rows
assert property (@(posedge clk) disable iff (!presetn) wr && paddr == 12'h108 |=> ((irq_stat ^ $past(irq_stat)) & ~wdata4_q & ~irq_in_q) == 0);
assert property (@(posedge clk) disable iff (!presetn) wr && paddr == 12'h108 |=> (irq_stat & wdata4_q & ~irq_in_q) == 0);
assert property (@(posedge clk) disable iff (!presetn) irq_in != 0 |=> (irq_stat & irq_in_q) == irq_in_q);
assert property (@(posedge clk) disable iff (!presetn) wr && paddr == 12'h10C |=> irq_en == ($past(irq_en) | wdata4_q));
assert property (@(posedge clk) disable iff (!presetn) wr && paddr == 12'h100 |=> ctrl == (($past(ctrl) & ~lanes_q) | (pwdata_q & lanes_q)));
assert property (@(posedge clk) !presetn |=> ctrl == 0 && irq_en == 0);
History on registers uses $past; history on inputs reads the copies in the bench.
The clocked immediate fallback
wr_en_q <= presetn && wr && paddr == 12'h10C;
live_q <= presetn;
end
always @(posedge clk) if (presetn) begin
if (wr_irq_q) assert(((irq_stat ^ irq_stat_q) & ~pwdata_q[3:0] & ~irq_in_q) == 0); // W1C: a 0 leaves the bit
if (wr_irq_q) assert((irq_stat & pwdata_q[3:0] & ~irq_in_q) == 0); // W1C: a 1 clears it
if (live_q) assert((irq_stat & irq_in_q) == irq_in_q); // a hardware set wins
if (wr_en_q) assert(irq_en == (irq_en_q | pwdata_q[3:0])); // RW1S
if (wr_ctrl_q) assert(ctrl == ((ctrl_q & ~lanes_q) | (pwdata_q & lanes_q))); // RW, byte strobes
if (psel && penable && !pwrite && paddr == 12'h104) assert(prdata == {28'd0, fcount}); // RO
end
always @(posedge clk) if (!live_q && presetn) assert(ctrl == 0 && irq_en == 0); // reset values
Each |=> becomes a flop that remembers the write.
[options]
mode bmc
depth 20
[engines]
smtbmc yices
[script]
read_verilog -formal -sv design.sv props.sv
prep -top csr_apb
[files]
design.sv
props.svRun the write-0 row
Run as it is: FAIL at reported step 4. In the trace find the hardware set, the write and the lost bit.
Run every row
Paste the full props.sv: FAIL at reported step 4. Which W1C row failed this time?
Fix the field
Make the IRQ_STAT write clear the bits written 1: BMC PASS to depth 20.
Add the missing row
The ID register is RO with the constant 32'h4350_5231. Write its check and run the immediate form: BMC PASS to depth 20.
Prove it
Switch to
mode provewithabc pdr: Prove PASS (abc pdr).
design.sv
props.sv
Use assert property (@(posedge clk) ...);. Leave blank for immediate assertions in the design.
Inputs are free unless constrained. Run to find a counterexample or check a proof.
Model: one positive-edge clock, binary data. Sampled history requires declaration-initialized variables; primary-input history is unsupported.
SBY summary and log
Check Yourself
Remember
- A register map is a table, and each access type implies a fixed check set: RW lands, RO ignores writes, W1C clears on 1 and keeps on 0 with hardware winning, RW1S sets.
- Generate the checks from the map, never from the RTL.
- The bus protocol is assumed and covered; the register behaviour is asserted one cycle after the completing write.
- The hard part is the exceptions: side effects, aliasing, privilege, strobes and hardware write paths.