Some bugs need hundreds or thousands of cycles from reset before they show: a timeout counter that misbehaves when it wraps, a statistics counter that overflows, a credit counter that drifts by one every thousand transactions. No affordable depth reaches them. This article shows a watchdog with such a bug, why a run at depth 100 passes it, and three ways to find it anyway: scaling the design down, starting from an arbitrary state, and full proofs.

9 min read | Intermediate

What You'll Learn

  • Recognize properties whose failures need more cycles than BMC can afford
  • Shrink a design with parameters so a deep bug becomes shallow
  • Start a check from an arbitrary state instead of reset, and know its risk
  • Decide which technique fits which kind of deep bug

The Design: a Watchdog Timer

A watchdog raises tout if software has not "kicked" it for 1000 cycles. The timeout must stay high until the next kick, so that a stuck system is reset for sure:


module timeout (input clk, input rst, input kick, output tout);
  reg [9:0] cnt;
  assign tout = (cnt >= 10'd1000);
  always @(posedge clk)
    if (rst || kick) cnt <= 10'd0;
    else             cnt <= cnt + 10'd1;   // bug: keeps counting and wraps after 1023
endmodule

The counter does not stop at 1000. After 1023 it wraps to 0 and tout drops, although no kick came. A stuck system would get a timeout pulse of 24 cycles instead of a steady signal.

The rule:


a_hold: assert property (@(posedge clk) disable iff (rst) tout && !kick |=> tout);

Why BMC Misses It

From reset, the counter needs 1024 cycles without a kick to wrap. A run at depth 100, the PRO maximum, passes in a few seconds, and the result is correct for what it checked: no failure within 100 cycles. The bug is simply 900 cycles beyond the horizon. Raising the depth to 1030 would find it in principle, but the run time grows with every frame, and most real deep bugs are much deeper than this one.

Quick Check

Q: A 16-bit transaction counter overflows after 65 536 transactions. Is BMC from reset a realistic way to find bugs in its overflow handling?

Show Answer

No. The overflow needs at least 65 536 cycles from reset. A check must either shrink the counter, start from a state close to the overflow, or prove the property for all depths.

Technique 1: Scale the Design Down

The bug does not depend on the numbers 1000 and 1023. It is about what happens when the counter reaches its maximum. Make the width and the limit parameters, and check a small version:


module timeout #(parameter W = 10, parameter [W-1:0] LIMIT = 1000) (input clk, input rst, input kick, output tout);
  reg [W-1:0] cnt;
  assign tout = (cnt >= LIMIT);
  always @(posedge clk)
    if (rst || kick) cnt <= 0;
    else             cnt <= cnt + 1'b1;   // bug: keeps counting and wraps
endmodule

With W = 2 and LIMIT = 2, the counter wraps after 3 and the run fails at step 6, well inside the free depth. With W = 4 and LIMIT = 10 it fails at step 18. The fix, counting only while tout is low, passes for every size.

Scaling down is the most common technique in practice, for counters, FIFOs, memories and buses. Its limitation is that some bugs depend on the exact numbers, such as a comparison with a hard-coded constant that does not scale with the parameter. Design the RTL so that every size-dependent value comes from a parameter, and the small version keeps the logic of the big one.

Technique 2: Start Anywhere

The bug is deep only because the check starts at reset. Remove the reset assumption, and the tool may start the counter at any value, including 1023:

Now the run fails at step 2 on the full 10-bit design:

The tool starts with cnt = 1023, so tout is high, and one step later the counter has wrapped and tout is low. With the fix, the same start-anywhere run passes.

The risk is false failures. A free start state may be one that the design can never reach from reset. Here every count from 0 to 1023 is reachable, so any failure is real. In a design with unreachable states, the tool may start in one of them and report a failure that cannot happen. The usual remedy is to add assumptions or, better, proven invariants that rule out the unreachable start states. That is exactly the problem of induction, and Chapter 6 is about it.

Quick Check

Q: In a start-anywhere run, a FIFO starts with count = 3 but all its entries marked empty. The run fails. Is that a bug?

Show Answer

Probably not. That combination cannot be reached from reset, so the failure comes from an impossible start state. Add the invariant that ties count to the entries, and the false failure disappears.

Technique 3: Prove It for Every Depth

A full proof by induction checks every state that satisfies the properties, not just the states within a depth. For the watchdog, induction would find the wrap in one step, like the start-anywhere run, and would prove the fixed design for all time. Chapter 6 shows how.

Which Technique When

SituationTechnique
The depth comes from a size: counter width, FIFO depth, memory sizeScale the parameters down
The bug is at a specific state far from reset, such as a counter maximumStart anywhere, with invariants for unreachable states
You need a result for all time, not just a bug huntInduction (Chapter 6)
The behavior needs a long, specific scenario, such as a protocol sequenceSimulation, or formal from a recorded state

Running It in ChipVerify Lab

The example below is the 10-bit watchdog. Its formal block starts from reset:


`ifdef FORMAL
  reg init = 1'b1;
  always @(posedge clk) init <= 1'b0;
  // start from reset: the bug is more than 1000 cycles away
  always @(*) if (init) assume (rst);
  // once tout is high, it stays high until a kick or reset
  always @(posedge clk) if (!init && !$past(rst) && $past(tout) && !$past(kick)) assert (tout);
`endif

Run it at depth 10, and with PRO at depth 100: both pass. Then delete the assume (rst) line to start anywhere, and the run fails at step 2.

Common Mistakes

Mistake #1: Accepting a deep pass as a proof

A pass at depth 100 says nothing about cycle 1000. For any rule that involves large counters or long timeouts, use one of the techniques here or say clearly that the result is bounded.

Mistake #2: Scaling a design whose constants do not scale

If the RTL compares with a literal such as 10'd1000 instead of a parameter, the small version is a different design. Make every size-dependent value a parameter first.

Mistake #3: Trusting every start-anywhere failure

A free start state may be unreachable from reset. Before calling a start-anywhere failure a bug, check that its start state can really happen.

Practice Exercise

Challenge: Find the Wrap Two Ways

Open the example. Run it as it is and see it pass. Then delete the reset assumption and run again. Finally restore the reset assumption, make the width and limit parameters, and find a size for which the bug appears within depth 10. Fix the bug and check both ways again.

Hint

With width W, the counter wraps after 2W - 1 counts. Add a reset cycle and a reporting step and keep the total below 10.

Solution

Start anywhere fails at step 2, with cnt = 1023 at the start. With W = 2 and LIMIT = 2 the reset-based run fails at step 6. W = 3 needs depth 11. The fix is else if (!tout) cnt <= cnt + 1'b1;: the counter stops at the limit. Both the start-anywhere run and the scaled runs then pass.

Open the ready-made example in the lab and run the formal check from the formal panel. The free tier allows a depth of up to 10 cycles, and PRO allows up to 100.

Summary

  • Bugs behind large counters and long timeouts can be far beyond any depth BMC can afford. A pass then means little.
  • Scaling the design down with parameters keeps the logic and shortens the path to the bug.
  • Starting from an arbitrary state reached the watchdog wrap in two steps, at the risk of unreachable start states.
  • Induction removes the depth limit altogether and is the subject of Chapter 6.
  • Make every size-dependent value a parameter, so formal can use small versions of the design.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.