Every property belongs to one of two families. A safety property says that something bad never happens: two grants at once, a lost word, a count above the limit. A liveness property says that something good eventually happens: every request is granted, every job finishes. The two need different checks and produce different counterexamples. This article explains the difference, shows why bounded model checking handles liveness through bounded deadlines, and finds a starvation bug in an arbiter.
9 min read | Intermediate
What You'll Learn
- Tell safety properties from liveness properties
- Explain why a liveness counterexample is an endless loop, not a finite trace
- Turn a liveness rule into a bounded deadline that BMC can check
- Find a starvation bug in a fixed-priority arbiter
Safety: Nothing Bad Happens
All the properties in this chapter so far are safety properties. They have one thing in common: when they fail, they fail at a specific cycle, and the trace up to that cycle proves it. A counterexample is a finite list of cycles that ends in the bad state.
a_safe: assert property (@(posedge clk) disable iff (rst) gnt != 2'b11);
Bounded model checking is built for safety. It looks for a bad state within the depth, and if it finds one, the trace is the counterexample.
Liveness: Something Good Eventually Happens
A liveness property has no deadline:
a_live: assert property (@(posedge clk) disable iff (rst) req[1] |-> s_eventually gnt[1]);
s_eventually means "at some point in the future, with no limit". The s_ stands for strong: the property fails if the good event never happens, even after infinitely many cycles.
Such a property can never fail at a specific cycle. After any number of cycles without a grant, the grant could still come in the next one. The only way to show that it never comes is a trace that runs forever without it. Formal tools show this as a loop: a path from reset into a cycle of states that repeats forever, and in which the grant never happens. Such a counterexample is called a lasso, after its shape.
Commercial tools can prove and refute liveness properties with special engines. Bounded model checking cannot, because it only looks at finite traces. In practice, most engineers check liveness as a bounded deadline anyway, for a good reason: a grant that comes after a million cycles satisfies the liveness property but is useless in a real system.
Quick Check
Q: Is "the FIFO never overflows" a safety or a liveness property? And "every word written into the FIFO is eventually read out"?
Show Answer
The first is safety: an overflow happens at a specific cycle. The second is liveness: there is no point in time at which you can say the word will never come out. As a deadline, "read within N cycles", it becomes safety.
Bounded Liveness
A bounded version of the rule gives the good event a deadline:
a_bound: assert property (@(posedge clk) disable iff (rst) req[1] |-> ##[0:4] gnt[1]);
Now the rule fails at a definite cycle: the fifth cycle after a request without a grant. It is a safety property again, and BMC can check it. It is also stronger than the liveness rule, because it demands a fast answer, not just an eventual one.
Liveness rules usually need a fairness assumption about the environment. An arbiter cannot grant a requester that gives up, so the rule only makes sense if the request stays up until it is granted:
m_hold: assume property (@(posedge clk) disable iff (rst) req[1] && !gnt[1] |=> req[1]);
The Design: a Two-Requester Arbiter
This arbiter gives a one-cycle grant to one of two requesters. Requester 0 always wins:
module arb2 (input clk, input rst, input [1:0] req, output reg [1:0] gnt);
always @(posedge clk)
if (rst) gnt <= 2'b00;
else if (req[0]) gnt <= 2'b01; // bug: fixed priority, requester 1 can starve
else if (req[1]) gnt <= 2'b10;
else gnt <= 2'b00;
endmodule
Fixed priority is not wrong in itself. But if requester 0 keeps asking, requester 1 is never served. That is called starvation. The specification here says every requester is served within 4 cycles, so this design breaks it.
Running It in ChipVerify Lab
The deadline becomes a helper counter of cycles waited, as in the sequences and implication article:
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// a request is held until it is granted
always @(posedge clk) if (!init && !$past(rst) && $past(req[0]) && !gnt[0]) assume (req[0]);
always @(posedge clk) if (!init && !$past(rst) && $past(req[1]) && !gnt[1]) assume (req[1]);
// bounded liveness: req[1] |-> ##[0:4] gnt[1]
reg [2:0] wait1 = 3'd0;
always @(posedge clk) wait1 <= (rst || gnt[1] || !req[1]) ? 3'd0 : wait1 + 3'd1;
always @(posedge clk) if (!init) assert (wait1 <= 3'd4);
// safety: never two grants
always @(posedge clk) if (!init) assert (gnt != 2'b11);
`endif
The run fails at step 7:
Both requesters ask from step 1. Requester 0 wins in every cycle. In step 5 it finally drops its request, and requester 1 is granted in step 6, five cycles after it asked. A longer trace with requester 0 asking forever would be the lasso counterexample of the liveness rule. The deadline turns it into a finite trace of 8 cycles.
The safety rule passes: this arbiter never grants both. Safety and liveness are independent. A design can be perfectly safe and still starve a requester, and a design that grants everything at once never starves anybody but is unsafe.
Quick Check
Q: Why does the check need the assumption that requests are held until granted?
Show Answer
Without it, the tool could raise req[1] only in cycles where requester 0 also asks and drop it otherwise. The rule as written resets the wait when the request drops, so the check would no longer describe a requester that is waiting. The assumption models a well-behaved requester, which is what the specification promises service to.
Common Mistakes
Mistake #1: Expecting BMC to find a liveness bug with an unbounded property
An unbounded "eventually" cannot fail on a finite trace, so a bounded run always passes it. Give the rule a deadline from the specification, or use a tool with liveness engines.
Mistake #2: Forgetting the fairness assumption
Without "requests are held", many liveness rules fail on environments that give up, or pass for the wrong reason. State what the environment promises, and only that.
Mistake #3: Checking only safety for an arbiter
"At most one grant" passes on an arbiter that never grants anything, or that always grants the same requester. Every arbiter, scheduler and flow-control block needs a progress rule as well.
Practice Exercise
Challenge: Fix the Starvation
Open the example and run it. Then make the arbiter fair: when both requesters ask, grant the one that did not get the last grant. Run again. Finally, find the smallest deadline for which your fair arbiter still passes.
Hint
The current grant register remembers who was served last. On a tie, give the grant to the other requester. When only one asks, grant that one.
Solution
always @(posedge clk)
if (rst) gnt <= 2'b00;
else if (req == 2'b11) gnt <= (gnt == 2'b01) ? 2'b10 : 2'b01; // round robin on a tie
else gnt <= req;
With this round-robin rule the run passes with the deadline 4. Lowering the bound shows the real worst case: wait1 <= 3'd2 passes, and wait1 <= 3'd1 fails at step 4. A requester of the fair arbiter waits at most 2 cycles, which you could now write into the specification.
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
- Safety: nothing bad happens. Its counterexamples are finite traces, and BMC finds them.
- Liveness: something good eventually happens, written with
s_eventually. Its counterexamples are endless loops, which BMC cannot show. - A bounded deadline such as
##[0:4]turns liveness into safety, and is usually what the specification really needs. - Liveness rules need fairness assumptions about the environment, such as requests held until granted.
- A fixed-priority arbiter passed the safety rule and starved requester 1 for 5 cycles. Round robin brought the worst case down to 2.