Automatic Checks

Generate covers and assertions from the RTL itself, read two kinds of verdict in one log, and see what lint can and cannot tell you about the same lines.

Lesson
F5.2
Time
15 min
Runs on
SymbiYosys
Check
4 questions

A controller listed five of its eight state encodings and sent the rest to a default that assigned x, "because it can never happen". Lint printed one informational note. Nobody asked the only useful question: can the state register reach an unlisted encoding, and what happens if it does?

The missing arm: S4 falls into the x default
0123456clkfreerstfreegofreemodestate012460S4: no arm, default runsstate = 6SBY reports FAIL

Measured with smtbmc yices at depth 20. The default writes 3'bxxx, which this engine turns into a free value; the solver picks 6. Sampled at step 5, reported at 6.

Properties the Code Implies

Five RTL Lines, Five Generated StatementsAn automatic check reads a construct in the RTL and writes the property that construct implies. Arrows mean "generates".
Five RTL lines, five generated statementsAn automatic check reads a construct in the RTL and writes the property that construct implies. Arrows mean "generates". component case (state) witha default listed states S0 toS4 component mem[idx] eight entries, afour-bit index component acc <= acc + in a 9-bit sum stored in8 bits component cover(state ==Sk) one per listed state:reachable? component assert(state <=S4) never an unlistedencoding component assert(!rd || idx <8) every read in range component assert(!acc_en ||!sum[8]) no carry dropped
Read the diagram as text
  • case (state) with a default: listed states S0 to S4
  • mem[idx]: eight entries, a four-bit index
  • acc <= acc + in: a 9-bit sum stored in 8 bits
  • cover(state == Sk): one per listed state: reachable?
  • assert(state <= S4): never an unlisted encoding
  • assert(!rd || idx < 8): every read in range
  • assert(!acc_en || !sum[8]): no carry dropped
  • case (state) with a default → cover(state == Sk)
  • case (state) with a default → assert(state <= S4)
  • mem[idx] → assert(!rd || idx < 8)
  • acc <= acc + in → assert(!acc_en || !sum[8])

No spec was read. Each statement follows from the code, which is both the strength and the limit of the method.

Some properties need no spec. A case implies its state stays among the listed values. An array implies its index stays in range. A narrower destination implies the sum fits. A tool can write these statements for every such line in a design and run them all.

Two kinds of statement come out. Reachability is a cover: each listed state should be reachable. Safety is an assert: no index, encoding or carry should go wrong. One log holds both verdicts.

Two Verdicts in One Log

CoverCovers, x defaultCovers, default to S0
state == S0reached, reported step 2reached, reported step 2
state == S1reached, reported step 3reached, reported step 3
state == S2reached, reported step 4reached, reported step 4
state == S3reached, reported step 4reached, reported step 4
state == S4reached, reported step 5reached, reported step 5
state > S4reached, reported step 6unreached within 20

Mode cover, depth 20, smtbmc yices, reset assumed in step 0. 582 ms and 579 ms on this machine.

Every listed state is reachable in both runs. The extra cover, state > S4, is the one that tells them apart: with the x default it is reached at step 6, with default: state <= S0 it is unreached. One arm changed (F1.3).

The asserts find the other two bugs. The index check fails at reported step 10: eight reads from reset take idx to 8. The carry check fails at step 3, the second add. With all three fixed, the set gives BMC PASS to depth 20 and abc pdr proves it in 496 ms.

Lint Says Could, Formal Says Does

RTL constructLint (Verilator, default warnings)Formal (generated property)
mem[idx], four-bit indexWIDTHTRUNC, on the buggy and the fixed RTL alikewrap at 9: FAIL at reported step 10; wrap at 7: BMC PASS
S4 arm missing, default assigns xnothingFAIL at reported step 6
9-bit sum stored in 8 bitsnothing (-Wall reports sum[8] unused)FAIL at reported step 3
each listed statenothingfive covers reached by reported step 5

Verilator 5.055 devel, --lint-only. Lint reads text and says a line could be wrong; formal reads the state space and says whether it is.

Lint warned about the index on both the buggy and the fixed RTL, because the text is the same: four bits for eight entries. Only the state space shows that the wrap at 9 reaches entry 8. Lint said nothing about the missing arm or the dropped carry.

The limit cuts the other way too. A generated check proves the code agrees with itself. If the spec wanted wrap-around arithmetic, the carry assert is noise; if a state should never be entered, its cover being reached is the bug. Read every generated verdict against the spec.

Asked in Interviews

QuestionAnswer
What do automatic formal checks find?Reachable out-of-range indexes, x assignments that matter, dropped carries, unlisted case values and unreachable states, from properties generated out of the RTL.
Why run them before writing properties?They need no spec, they find real bugs and dead logic, and a reached cover per state shows the clock and reset setup works.
How is this different from lint?Lint flags a line that could be wrong. Formal shows whether the bad state is reachable, with a trace.

Try It

The generated set in SVA

cover property (@(posedge clk) !rst && state == S3);
assert property (@(posedge clk) disable iff (rst) state inside {S0, S1, S2, S3, S4});
assert property (@(posedge clk) disable iff (rst) rd |-> idx < 4'd8);
assert property (@(posedge clk) disable iff (rst) acc_en |-> !sum[8]);

A cover per state, then one assert per construct. inside is the IEEE form of the legal-state check.

As this engine runs them

  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  // generated: state legal, index in range, no dropped carry
  always @(posedge clk) if (!rst) begin
    a_state: assert(state <= S4);
    a_index: assert(!rd || idx < 4'd8);
    a_carry: assert(!acc_en || !sum[8]);
  end

Clocked immediate statements; the labels name each one in the log.

The cover task. The assert runs use mode bmc with the same script.
[options]
mode cover
depth 20
[engines]
smtbmc yices
[script]
read_verilog -formal -sv design.sv
prep -top lint5
[files]
design.sv
  1. Run the covers

    Run as it is: Cover reached, reported step 6. Read which covers the log names and at which step.

  2. Fix the default

    Change the default to state <= S0 and run: Cover unreached within 20. Which cover is unreached now, and why is that good news?

  3. Run the asserts

    Replace the covers with the three asserts and run with the index bug, then with the overflow bug. Read the step at which each fails.

  4. Fix and prove

    With all three fixed, switch to mode prove with abc 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

Predict the outputlint5 with only the index bug (the read pointer wraps at 9), mode bmc, default depth 20, smtbmc yices, reset assumed in step 0, the three generated asserts. At which reported step does it fail?
module lint5(input clk, rst, go, rd, acc_en, input [1:0] mode, input [7:0] in,
  output reg [7:0] out=0, output reg [7:0] acc=0);
  localparam S0=3'd0, S1=3'd1, S2=3'd2, S3=3'd3, S4=3'd4;
  reg [2:0] state=S0;
  reg [3:0] idx=0;                 // read pointer: four bits for an eight-entry table
  reg [7:0] mem [0:7];
  wire [8:0] sum = acc + in;
  always @(posedge clk)
    if (rst) begin state <= S0; idx <= 0; acc <= 0; out <= 0; end
    else begin
      case (state)
        S0: if (go) state <= S1;
        S1: state <= mode[0] ? S2 : S3;
        S2: state <= S4;
        S3: state <= S4;
        S4: state <= S0;
        default: state <= S0;
      endcase
      if (rd) begin
        out <= mem[idx];
        idx <= (idx == 4'd9) ? 4'd0 : idx + 4'd1;   // bug: wraps at 9, not 7
      end
      if (acc_en) acc <= sum[8] ? 8'hFF : sum[7:0];   // saturates
    end
`ifdef FORMAL
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  // generated: state legal, index in range
  always @(posedge clk) if (!rst) begin
    a_state: assert(state <= S4);
    a_index: assert(!rd || idx < 4'd8);
  end
`endif
endmodule
ConceptA generated cover(state == S3) is unreached at depth 20. What do you know?
ConceptLint warns that mem[idx] uses a four-bit index for eight entries. What does the generated assert add?
Read the waveformIn the x-default trace, what does state hold one step before it leaves the listed encodings?
The missing arm: S4 falls into the x default
0123456clkfreerstfreegofreemodestate012460S4: no arm, default runsstate = 6SBY reports FAIL

Measured with smtbmc yices at depth 20. The default writes 3'bxxx, which this engine turns into a free value; the solver picks 6. Sampled at step 5, reported at 6.

Remember

  • An automatic check is a property generated from an RTL construct: a cover per listed state, a legal-state assert, an index bound, a carry check.
  • Covers and asserts come back in one log: reached or unreached, PASS or FAIL with a trace.
  • Lint flags text that could be wrong; formal shows whether the bad state is reachable.
  • Generated checks prove the code agrees with itself. Read each verdict against the spec.