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

Fieldaxil_regs, the P2.15 register block
1 Scopethe whole block: five channels, four 32-bit words, five address bits. Out: AxPROT, and bursts (Lite has none)
2 Goalsign-off
3 Spec rowsVALID 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 Propertiesa_reset, a_b_hold, a_r_hold, a_b_resp, a_r_resp, a_r_zero, a_r_data
5 Assumptionsreset in step 0; AW, W and AR hold until accepted. Checked in the system by the same three lines as assertions (F7.4)
6 Coversan 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 Abstractionsnone needed. A case split on the watched word is shown and measured
8 Depth budgetfull proof. Required depth 8 from the deepest cover; every fault fails by step 6
9 Sign-offthe 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

The Bench Around the BlockThe manager drives AW, W and AR: its promises are assumptions. The block drives B and R: its rules are assertions. The scoreboard and the lane shadows remember what the map says the answers must be.
The bench around the blockThe manager drives AW, W and AR: its promises are assumptions. The block drives B and R: its rules are assertions. The scoreboard and the lane shadows remember what the map says the answers must be. assume AW, W, AR in assume: VALID holds,payload stable design axil_regs CTRL, DATA (RW);STATUS, ID (RO);215 flop bits assert B, R out assert: VALID holds,payload stable,response and ID right model write scoreboard accepted AW and Wbeats in order, twodeep model watched word `$anyconst` index;four byte-laneshadows model read in flight address, ID, shadowvalue at acceptance B takenAR takencompare
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 rowPropertyKind
AW, W, AR: VALID holds and payload stable until accepted (A2.3)3 lines on the inputsassume
B, R: VALID holds and payload stable until accepted (A2.3)a_b_hold, a_r_holdassert
B answers an accepted AW and W, with its ID and the map's responsea_b_respassert
R answers an accepted AR, with its ID; DECERR outside, data 0a_r_resp, a_r_zeroassert
A read returns the word as the strobed writes left it; RO words their reset valuea_r_dataassert
No response is valid in the first cycle after reseta_resetassert

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

CoverWhat it shows is reachableReached at step
c_strban OKAY write of lane 1 only (wstrb = 0010)4
c_slverra SLVERR write4
c_decerra DECERR read3
c_rawa written word read back on the checked path6
c_w_firstW accepted before its AW3
c_b_stallB held two cycles under backpressure7

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

RunEngineResult
Whole set, immediate formaiger rIC3Prove PASS (rIC3), 1.4 s
Whole set, concurrent props.svaiger rIC3Prove PASS (rIC3), 1.8 s
Whole set, immediate formabc pdrProve PASS (abc pdr), 8.2 s
Whole set, k-inductionsmtbmc yices, k = 6UNKNOWN at k = 6 (base case passed, induction step failed); a_b_resp
Case 0: restrict(f_word == 0)abc pdrProve PASS (abc pdr), 3.4 s
Case 1: restrict(f_word == 1)abc pdrProve PASS (abc pdr), 3.3 s
Case 2: restrict(f_word == 2)abc pdrProve PASS (abc pdr), 0.8 s
Case 3: restrict(f_word == 3)abc pdrProve 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

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

F_STRB_ALL: a strobed write, then a read
0123456clkfreeawvalidfreeawaddr14040004001000freewvalidfreewstrb100000001000000010000000freewdata800000000000000180000000000000008000000000000000bvalidfreearvalidfreearaddr101C04180400rvalidrdata0000000000000001R carries all four lanesreported step 6

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

The one-page sign-off, generated from the measured runs. A reviewer reads it top to bottom and asks for the run behind any line.
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 resultCount
Killed34
first failing check a_r_data25
first failing check a_b_resp5
first failing check a_r_resp2
first failing check a_b_hold1
first failing check a_r_hold1
Equivalent (a miter proves no visible change)2
Survived: a channel never accepts3

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 handProperty setCovers unreached
arready tied lowProve PASS (rIC3)c_decerr, c_raw
the AW handshake ignoredProve 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

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

The whole task. The fault runs use 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.sv
  1. Fill the plan

    Write the nine fields for this block before you open the bench.

  2. Write the rows

    Keep the given handshake pair; write the response and register rows in props.sv.

  3. Covers, then the proof

    Run the covers (deepest at step 7), then prove with rIC3.

  4. Split and argue

    Run the four split tasks and write one line on why their union is the whole proof.

  5. The fault matrix

    Run each fault at depth 12 and record the check and step.

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

Predict the outputaxil_bench with F_STRB_ALL, mode bmc, depth 12, smtbmc yices, aresetn assumed low in step 0 and high after, the immediate checks. At which reported step does it fail?
module axil_regs(input clk, aresetn,
  input awvalid, input [4:0] awaddr, input [1:0] awid, output awready,
  input wvalid, input [31:0] wdata, input [3:0] wstrb, output wready,
  output reg bvalid = 0, output reg [1:0] bresp = 0, output reg [1:0] bid = 0, input bready,
  input arvalid, input [4:0] araddr, input [1:0] arid, output arready,
  output reg rvalid = 0, output reg [1:0] rresp = 0, output reg [1:0] rid = 0, output reg [31:0] rdata = 0, input rready);
  reg [31:0] ctrl = 0, data = 0, status = 32'h1, id = 32'h4c495445;
  reg aw_v = 0, w_v = 0;                 // an address or a data beat held, waiting for its partner
  reg [4:0] aw_a = 0, rd_a = 0;
  reg [1:0] aw_i = 0;
  reg [31:0] w_d = 0;
  reg [3:0] w_s = 0;
  assign awready = !aw_v;
  assign wready = !w_v;
  assign arready = !rvalid || rready;
  wire commit = aw_v && w_v && (!bvalid || bready);   // both halves held, B channel free
  wire [3:0] ln = 4'hF;   // fault F_STRB_ALL
  wire [31:0] m = {{8{ln[3]}}, {8{ln[2]}}, {8{ln[1]}}, {8{ln[0]}}};   // lanes the write may change
  function [31:0] word(input [1:0] a);
    word = a == 2'd0 ? ctrl : a == 2'd1 ? data : a == 2'd2 ? status : id;
  endfunction
  always @(posedge clk)
    if (!aresetn) begin
      aw_v <= 0; w_v <= 0; bvalid <= 0; rvalid <= 0;
      ctrl <= 0; data <= 0; status <= 32'h1; id <= 32'h4c495445;
    end else begin
      if (awvalid && awready) begin aw_v <= 1; aw_a <= awaddr; aw_i <= awid; end
      if (wvalid && wready) begin w_v <= 1; w_d <= wdata; w_s <= wstrb; end
      if (bvalid && bready) bvalid <= 0;
      if (commit) begin
        aw_v <= 0; w_v <= 0; bvalid <= 1;
        bid <= aw_i;
        bresp <= aw_a[4] ? 2'd3 : aw_a[3] ? 2'd2 : 2'd0;   // DECERR, SLVERR, OKAY
        if (!aw_a[4]) case (aw_a[3:2])
          2'd0: ctrl <= (ctrl & ~m) | (w_d & m);
          2'd1: data <= (data & ~m) | (w_d & m);
          default: ;
        endcase
      end
      if (rvalid && rready) rvalid <= 0;
      if (arvalid && arready) begin
        rvalid <= 1; rid <= arid; rd_a <= araddr;
        rresp <= araddr[4] ? 2'd3 : 2'd0;
        rdata <= araddr[4] ? 32'd0 : word(araddr[3:2]);
      end
    end
endmodule
module axil_bench(input clk, aresetn,
  input awvalid, input [4:0] awaddr, input [1:0] awid,
  input wvalid, input [31:0] wdata, input [3:0] wstrb, input bready,
  input arvalid, input [4:0] araddr, input [1:0] arid, input rready);
  wire awready, wready, bvalid, arready, rvalid;
  wire [1:0] bresp, bid, rresp, rid;
  wire [31:0] rdata;
  axil_regs dut(.clk(clk), .aresetn(aresetn), .awvalid(awvalid), .awaddr(awaddr), .awid(awid), .awready(awready),
    .wvalid(wvalid), .wdata(wdata), .wstrb(wstrb), .wready(wready), .bvalid(bvalid), .bresp(bresp), .bid(bid), .bready(bready),
    .arvalid(arvalid), .araddr(araddr), .arid(arid), .arready(arready), .rvalid(rvalid), .rresp(rresp), .rid(rid), .rdata(rdata), .rready(rready));
  always @(posedge clk) begin
    if ($initstate) begin assume(!aresetn); end else begin assume(aresetn); end
  end
  wire aw_hs = awvalid && awready, w_hs = wvalid && wready, b_hs = bvalid && bready;
  wire ar_hs = arvalid && arready, r_hs = rvalid && rready;
  // registered copies: last cycle's inputs and outputs (the engine refuses $past on an input)
  reg aw_wait = 0, w_wait = 0, ar_wait = 0, b_wait = 0, r_wait = 0, live = 0;
  reg [4:0] awaddr_q = 0, araddr_q = 0;
  reg [1:0] awid_q = 0, arid_q = 0, bresp_q = 0, bid_q = 0, rresp_q = 0, rid_q = 0;
  reg [31:0] wdata_q = 0, rdata_q = 0;
  reg [3:0] wstrb_q = 0;
  always @(posedge clk) begin
    aw_wait <= aresetn && awvalid && !awready; w_wait <= aresetn && wvalid && !wready; ar_wait <= aresetn && arvalid && !arready;
    b_wait <= aresetn && bvalid && !bready; r_wait <= aresetn && rvalid && !rready; live <= aresetn;
    awaddr_q <= awaddr; awid_q <= awid; wdata_q <= wdata; wstrb_q <= wstrb; araddr_q <= araddr; arid_q <= arid;
    bresp_q <= bresp; bid_q <= bid; rresp_q <= rresp; rid_q <= rid; rdata_q <= rdata;
  end
  // the manager's promises (IHI 0022L A2.3): VALID holds, and its payload is stable, until the handshake
  always @(posedge clk) if (aresetn) begin
    if (aw_wait) assume(awvalid && awaddr == awaddr_q && awid == awid_q);
    if (w_wait) assume(wvalid && wdata == wdata_q && wstrb == wstrb_q);
    if (ar_wait) assume(arvalid && araddr == araddr_q && arid == arid_q);
  end
  // write scoreboard: accepted AW and W beats, in order, until their B is taken
  reg [1:0] f_awn = 0, f_wn = 0;
  reg [6:0] f_aw0 = 0, f_aw1 = 0;        // {address, id}
  reg [31:0] f_wd0 = 0, f_wd1 = 0;       // data
  reg [3:0] f_ws0 = 0, f_ws1 = 0;         // strobes
  wire [4:0] e_a = f_aw0[6:2];
  wire [1:0] e_id = f_aw0[1:0];
  wire [1:0] e_resp = e_a[4] ? 2'd3 : e_a[3] ? 2'd2 : 2'd0;   // the response the map requires
  always @(posedge clk)
    if (!aresetn) begin f_awn <= 0; f_wn <= 0; end
    else begin
      if (b_hs) begin f_aw0 <= f_aw1; f_wd0 <= f_wd1; f_ws0 <= f_ws1; end
      if (aw_hs) begin if (f_awn - b_hs == 0) f_aw0 <= {awaddr, awid}; else f_aw1 <= {awaddr, awid}; end
      if (w_hs) begin if (f_wn - b_hs == 0) begin f_wd0 <= wdata; f_ws0 <= wstrb; end else begin f_wd1 <= wdata; f_ws1 <= wstrb; end end
      f_awn <= f_awn + aw_hs - b_hs;
      f_wn <= f_wn + w_hs - b_hs;
    end
  // the watched word: any of the four, fixed for the whole trace, and its four byte lanes
  wire [1:0] f_word = $anyconst;
  reg [7:0] f_sh0 = 0, f_sh1 = 0, f_sh2 = 0, f_sh3 = 0;
  wire f_hit = b_hs && !e_a[4] && !e_a[3] && !f_word[1] && e_a[2] == f_word[0];   // a write to it is answered
  always @(posedge clk)
    if (!aresetn) {f_sh3, f_sh2, f_sh1, f_sh0} <= f_word == 2'd2 ? 32'h1 : f_word == 2'd3 ? 32'h4c495445 : 32'h0;
    else if (f_hit) begin
      if (f_ws0[0]) f_sh0 <= f_wd0[7:0];
      if (f_ws0[1]) f_sh1 <= f_wd0[15:8];
      if (f_ws0[2]) f_sh2 <= f_wd0[23:16];
      if (f_ws0[3]) f_sh3 <= f_wd0[31:24];
    end
  // the read in flight: its address, id, and the watched word's value when it was accepted
  reg f_rn = 0, f_quiet = 0;
  reg [4:0] f_ra = 0;
  reg [1:0] f_rid = 0;
  reg [31:0] f_rsh = 0;
  wire f_rout = f_ra[4];                                        // the read was outside the map
  wire [1:0] f_rresp = f_rout ? 2'd3 : 2'd0;
  wire f_check = f_quiet && !f_ra[4] && f_ra[3:2] == f_word;   // a read of the watched word, no write in flight
  always @(posedge clk)
    if (!aresetn) f_rn <= 0;
    else begin
      f_rn <= ar_hs || f_rn && !r_hs;
      if (ar_hs) begin f_ra <= araddr; f_rid <= arid; f_rsh <= {f_sh3, f_sh2, f_sh1, f_sh0}; f_quiet <= f_awn == 0 && f_wn == 0; end
    end
  always @(posedge clk) if (aresetn) begin
    if (!live) a_reset: assert(!bvalid && !rvalid);                                       // reset
    if (b_wait) a_b_hold: assert(bvalid && bresp == bresp_q && bid == bid_q);              // B holds
    if (r_wait) a_r_hold: assert(rvalid && rresp == rresp_q && rid == rid_q && rdata == rdata_q);   // R holds
    if (bvalid) a_b_resp: assert(f_awn != 0 && f_wn != 0 && bid == e_id && bresp == e_resp);
    if (rvalid) a_r_resp: assert(f_rn && rid == f_rid && rresp == f_rresp);
    if (rvalid && f_rout) a_r_zero: assert(rdata == 32'd0);
    if (rvalid && f_check) a_r_data: assert(rdata == f_rsh);                              // the lane shadows
  end
endmodule
ConceptWhich plan entry would a reviewer send back?
ConceptWhy is splitting on the watched word sound here, when splitting on awaddr would not be?
Read the waveformIn the F_RVALID_DROP trace, what happens in step 3?
F_RVALID_DROP
01234clkfreearvalidarreadyrvalidfreerreadyrvalid falls, rready lowreported step 4

Measured, bmc depth 12: FAIL at reported step 4 on a_r_hold.

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.