Capstone: Formal Sign-Off of the AXI4-Lite Register Block
Take one block from plan to signed page: bench, properties, covers, proof, a case split, the fault matrix and a mutation score, every number measured.
- Lesson
- F9.1
- Time
- 45 min
- Runs on
- SymbiYosys
- Check
- 4 questions
A register block went to review with 31 proven properties, and nobody asked what they did not read. The byte-strobe path was in no property's cone, so a block that wrote all four lanes on a one-lane write passed every proof. A mutation run would have shown it in an afternoon; this lesson is that afternoon.
The Plan, Filled
| Field | axil_regs, the P2.15 register block |
|---|---|
| 1 Scope | the whole block: five channels, four 32-bit words, five address bits. Out: AxPROT, and bursts (Lite has none) |
| 2 Goal | sign-off |
| 3 Spec rows | VALID holds and payload stays stable on each channel (IHI 0022L A2.3); responses OKAY, SLVERR, DECERR by the map; IDs reflected (IHI 0022H.c B1); RW lanes, RO words, reset values |
| 4 Properties | a_reset, a_b_hold, a_r_hold, a_b_resp, a_r_resp, a_r_zero, a_r_data |
| 5 Assumptions | reset in step 0; AW, W and AR hold until accepted. Checked in the system by the same three lines as assertions (F7.4) |
| 6 Covers | an OKAY write of lane 1 only (wstrb = 0010): step 4; a SLVERR write: step 4; a DECERR read: step 3; a written word read back on the checked path: step 6; W accepted before its AW: step 3; B held two cycles under backpressure: step 7 |
| 7 Abstractions | none needed. A case split on the watched word is shown and measured |
| 8 Depth budget | full proof. Required depth 8 from the deepest cover; every fault fails by step 6 |
| 9 Sign-off | the page below |
The nine fields of F7.5, filled for this block. Every number is measured.
The block is P2.15's register block, copied for the open tools: five address bits, two-bit IDs, no protection bits. CTRL and DATA are read/write, STATUS and ID read-only, and anything at 0x10 or above is outside the map.
The Bench
Read the diagram as text
- AW, W, AR in: assume: VALID holds, payload stable
- axil_regs: CTRL, DATA (RW); STATUS, ID (RO); 215 flop bits
- B, R out: assert: VALID holds, payload stable, response and ID right
- write scoreboard: accepted AW and W beats in order, two deep
- watched word: `$anyconst` index; four byte-lane shadows
- read in flight: address, ID, shadow value at acceptance
- AW, W, AR in → axil_regs
- axil_regs → B, R out
- write scoreboard → watched word: B taken
- watched word → read in flight: AR taken
- read in flight → B, R out: compare
The whole model, block and bench: 498 flop bits. The bench reads only the block's ports.
| Spec row | Property | Kind |
|---|---|---|
| AW, W, AR: VALID holds and payload stable until accepted (A2.3) | 3 lines on the inputs | assume |
| B, R: VALID holds and payload stable until accepted (A2.3) | a_b_hold, a_r_hold | assert |
| B answers an accepted AW and W, with its ID and the map's response | a_b_resp | assert |
| R answers an accepted AR, with its ID; DECERR outside, data 0 | a_r_resp, a_r_zero | assert |
| A read returns the word as the strobed writes left it; RO words their reset value | a_r_data | assert |
| No response is valid in the first cycle after reset | a_reset | assert |
Each spec row names its property. Nothing restricts bready or rready: the manager may stall a response forever, and the block must hold it.
The manager's rules become three assumptions; the block's rules become assertions. Nothing is assumed about bready. A promise that the manager takes B within two cycles would hide a block that drops bvalid early, and no spec row asks for it.
The register rows use one watched word, any of the four, fixed for the whole trace. Its four lane shadows apply each answered write lane by lane. A read accepted with no write in flight must return the shadow.
Covers Before the Proof
| Cover | What it shows is reachable | Reached at step |
|---|---|---|
c_strb | an OKAY write of lane 1 only (wstrb = 0010) | 4 |
c_slverr | a SLVERR write | 4 |
c_decerr | a DECERR read | 3 |
c_raw | a written word read back on the checked path | 6 |
c_w_first | W accepted before its AW | 3 |
c_b_stall | B held two cycles under backpressure | 7 |
One mode cover run, depth 20: Cover PASS, last cover reached at step 7, 0.8 s.
Every behaviour the assertions talk about is reachable under the three assumptions, so none of them passes vacuously. The deepest cover, step 7, sets the required depth: 8 (F7.3).
The Proof, and One Case Split
| Run | Engine | Result |
|---|---|---|
| Whole set, immediate form | aiger rIC3 | Prove PASS (rIC3), 1.4 s |
| Whole set, concurrent props.sv | aiger rIC3 | Prove PASS (rIC3), 1.8 s |
| Whole set, immediate form | abc pdr | Prove PASS (abc pdr), 8.2 s |
| Whole set, k-induction | smtbmc yices, k = 6 | UNKNOWN at k = 6 (base case passed, induction step failed); a_b_resp |
Case 0: restrict(f_word == 0) | abc pdr | Prove PASS (abc pdr), 3.4 s |
Case 1: restrict(f_word == 1) | abc pdr | Prove PASS (abc pdr), 3.3 s |
Case 2: restrict(f_word == 2) | abc pdr | Prove PASS (abc pdr), 0.8 s |
Case 3: restrict(f_word == 3) | abc pdr | Prove PASS (abc pdr), 0.8 s |
Reset assumed in step 0. Four split cases take 8.4 s together against 8.2 s whole; each case drops three words' shadows from the solver's view.
Two IC3 engines close the whole set. abc pdr is the implementation F2.5 taught. rIC3 is a newer one in the same suite: the same algorithm on the same AIGER model, faster here. k-induction at k = 6 does not. Its induction step starts from a state where the scoreboard and the block disagree, so it would need helper invariants (F4.6). The sign-off uses the IC3 proofs.
The split restricts the watched word, a value fixed for the whole trace, so four cases cover every trace. Splitting on awaddr would not: addresses change between transactions, and a write to DATA corrupting CTRL lives in no single-address case (F4.3).
Six Faults, Seven Checks
| Fault | reset | b_hold | r_hold | b_resp | r_resp | r_zero | r_data |
|---|---|---|---|---|---|---|---|
F_RO_OKAY: a write to STATUS or ID returns OKAY and is applied | - | - | - | 4 | - | - | 6 |
F_NO_DECERR: a read above the sixteen bytes returns OKAY | - | - | - | - | 3 | - | - |
F_RVALID_DROP: rvalid falls before rready | - | - | 4 | - | - | - | - |
F_RDATA_CHANGE: rdata re-reads the register while rvalid waits | - | - | 5 | - | - | - | 5 |
F_STRB_ALL: a write applies all four lanes, whatever wstrb says | - | - | - | - | - | - | 6 |
F_BID_ZERO: bid is 0, not the accepted awid | - | - | - | 4 | - | - | - |
Each fault against each check alone. A dash is Prove PASS (aiger rIC3); a number is the reported failing step at bmc depth 12, the shortest trace. F_RO_OKAY and F_RDATA_CHANGE are caught by two checks each.
Measured, bmc depth 12: FAIL at reported step 6 on a_r_data. The write's strobes select fewer lanes than the read returns changed.
Each faulty block differs from the correct one in one line. Every fault fails within depth 8; the strobe fault needs a write and a read of the same word, and only a_r_data sees it.
The Sign-Off Page
FORMAL SIGN-OFF axil_regs (P2.15) 2026-11-08
Constraints reset in step 0; AW, W, AR hold until accepted (A2.3)
3 assumptions, asserted in the system bench (F7.4)
Checkers 7 properties, every spec row named
fault matrix: 6 of 6 faults caught (deepest at step 6)
mutation: 34 of 37 killed (91%), 2 equivalent set aside;
39 edits of axil_regs and a control, seed 9, floor 90%
Complexity full proof: aiger rIC3 1.4 s, abc pdr 8.2 s
k-induction k = 6: UNKNOWN (not used)
498 flop bits with the bench, 215 in the block
Coverage 6 of 6 covers reached, deepest at step 7
cone map: 17 block registers, each in some check's cone;
white logic: none
required depth 8; the proof is unbounded
Open items 3 surviving edits: each stops a channel accepting;
no safety check sees it; the covers do
Tools SBY v0.70-9-g79fc9ed, Yosys 0.70+290| Mutation result | Count |
|---|---|
| Killed | 34 |
first failing check a_r_data | 25 |
first failing check a_b_resp | 5 |
first failing check a_r_resp | 2 |
first failing check a_b_hold | 1 |
first failing check a_r_hold | 1 |
| Equivalent (a miter proves no visible change) | 2 |
| Survived: a channel never accepts | 3 |
39 edits of axil_regs and the unedited control, the bench never edited. Each runs the property set with rIC3; the ones it passes then run a miter (F7.2). The campaign ran 44.9 s.
| The survivors, written by hand | Property set | Covers unreached |
|---|---|---|
arready tied low | Prove PASS (rIC3) | c_decerr, c_raw |
| the AW handshake ignored | Prove PASS (rIC3) | c_b_stall, c_raw, c_slverr, c_strb |
All three survivors stop the block accepting on one channel. AXI lets a subordinate wait forever, so no safety row can object; the covers can.
A block that never answers breaks no safety rule: this is F7.2's deaf write path again. The covers are part of the sign-off for exactly this reason. The floor is 90%; a revision whose campaign kills fewer goes back for review. A floor belongs to its fault model, seed and sample size: a campaign that edits only storage cells, or draws four entries instead of forty, sets its own.
The six faults and the score are separate claims, and neither proves every possible fault is caught.
Asked in Interviews
| Question | Answer |
|---|---|
| Walk me through a formal sign-off. | Plan, assumptions with who checks them, properties by spec row, covers reached, abstraction with its argument, proof or bounded proof with the required depth, mutation score, one page. |
| Why not assume the manager takes responses quickly? | No spec row asks for it, and it would hide a block that drops VALID before READY. |
| When is a case split sound? | When the cases cover every trace: split on a value fixed for the whole trace, and check the cases' OR is always true. |
Try It
The lab is the capstone's bench with the concurrent property file: Prove PASS (rIC3), 1.8 s. The twins run the immediate form, the covers, the six faults and the four split cases.
props.sv: the handshake pair, given
assume property (@(posedge clk) disable iff (!aresetn) awvalid && !awready |=> awvalid && awaddr == awaddr_q && awid == awid_q);
assert property (@(posedge clk) disable iff (!aresetn) bvalid && !bready |=> bvalid && $stable(bresp) && $stable(bid));
Assume on the manager's side, assert on the block's.
props.sv: the rows you write
assert property (@(posedge clk) disable iff (!aresetn) bvalid |-> f_awn != 0 && f_wn != 0 && bid == e_id && bresp == e_resp);
assert property (@(posedge clk) disable iff (!aresetn) rvalid && f_check |-> rdata == f_rsh);
The scoreboard and the shadow are in the bench; the rows only compare.
mode bmc, depth 12, smtbmc yices.[options]
mode prove
[engines]
aiger rIC3
[script]
read_verilog -formal -sv design.sv props.sv
prep -top axil_bench
[files]
design.sv
props.svFill the plan
Write the nine fields for this block before you open the bench.
Write the rows
Keep the given handshake pair; write the response and register rows in props.sv.
Covers, then the proof
Run the covers (deepest at step 7), then
provewith rIC3.Split and argue
Run the four split tasks and write one line on why their union is the whole proof.
The fault matrix
Run each fault at depth 12 and record the check and step.
Sign it
Fill the sign-off page from your runs and compare it with the one above.
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 sign-off is a plan filled from measured runs: rows, properties, assumptions, covers, abstraction, depth, mutation, one page.
- Assume only what a spec row gives the manager; never a quick READY on the block's responses.
- Split on a value fixed for the whole trace, and argue the union.
- The fault matrix shows which rows carry the proof; the mutation score samples what the rows miss.