The symptom: one requester gets every grant while the other three wait, and the structural checks, one-hot and no grant without a request, all pass. Structural checks are blind to starvation; only a bound on waiting sees it. This lesson makes you derive the bound from the arbiter's behaviour, prove it, and read what each planted fault does to it.
The arbiter below, with the BUG switch (W1 to W3) and the HELPER switch for the four helper assertions.
The property set: one-hot, no phantom grant, work conservation, four wait-bound properties on counters, four helpers, and six covers.
The symptoms of W1 to W3 in the brief; read the BUG == lines of the listing last, after you have read each trace.
No assumption on the requesters. The bound holds for every request pattern, so the fairness register for this design is empty; true liveness is out of reach of this toolchain and the lab uses the bounded form.
// formal: prove 4 expect pass slang top rr_arbiter
`define FORMAL
// rr_arbiter: 4-requester round-robin arbiter. The grant is combinational from req and the 2-bit
// pointer last (the index granted most recently). The search starts at last + 1 and wraps. A grant is
// given in the cycle a request is seen and lasts one cycle (no lock). Synchronous active-high reset:
// last = 3, so requester 0 has priority first. BUG selects a variant (0 is the design as specified);
// HELPER=0 removes the helper assertions.
module rr_arbiter #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire [3:0] req,
output wire [3:0] grant
);
reg [1:0] last;
wire [3:0] hi_mask = (last == 2'd0) ? 4'b1110 :
(last == 2'd1) ? 4'b1100 :
(last == 2'd2) ? 4'b1000 : 4'b0000;
wire [3:0] hi_req = req & hi_mask;
wire [3:0] lo_req = (BUG == 3) ? (req | (4'b0001 << last)) : req;
function [3:0] lowest; // lowest set bit, one-hot (0 if none)
input [3:0] v;
lowest = v & (~v + 4'd1);
endfunction
wire [3:0] g_hi = lowest(hi_req);
wire [3:0] g_lo = lowest(lo_req) & {4{(hi_req == 4'b0) || (BUG == 2)}};
assign grant = g_hi | g_lo;
wire [1:0] gidx = grant[0] ? 2'd0 : grant[1] ? 2'd1 : grant[2] ? 2'd2 : 2'd3;
always @(posedge clk) begin
if (rst) last <= 2'd3;
else if (|grant && !(BUG == 1 && grant[0])) last <= gidx;
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// wt[i]: consecutive cycles requester i has requested without being granted
reg [2:0] wt0, wt1, wt2, wt3;
always @(posedge clk) begin
if (rst) begin wt0 <= 3'd0; wt1 <= 3'd0; wt2 <= 3'd0; wt3 <= 3'd0; end
else begin
wt0 <= (req[0] && !grant[0]) ? wt0 + 3'd1 : 3'd0;
wt1 <= (req[1] && !grant[1]) ? wt1 + 3'd1 : 3'd0;
wt2 <= (req[2] && !grant[2]) ? wt2 + 3'd1 : 3'd0;
wt3 <= (req[3] && !grant[3]) ? wt3 + 3'd1 : 3'd0;
end
end
always @(*) if (!init) begin
a_onehot0: assert ((grant & (grant - 4'd1)) == 4'd0);
a_no_phantom: assert ((grant & ~req) == 4'd0);
a_work_cons: assert (!(|req) || (|grant));
// bounded response (safety form of starvation freedom): nobody waits more than 3 cycles
a_wt0: assert (wt0 <= 3'd3);
a_wt1: assert (wt1 <= 3'd3);
a_wt2: assert (wt2 <= 3'd3);
a_wt3: assert (wt3 <= 3'd3);
end
// helper invariants for the induction step of the wait bounds
function [1:0] ahead; // how many grants can still precede requester i
input [1:0] i;
ahead = i - last - 2'd1; // 0..3 (mod 4)
endfunction
generate if (HELPER) begin : g_helper
always @(*) if (!init) begin
h_wt0: assert (wt0 + {1'b0, ahead(2'd0)} <= 3'd3);
h_wt1: assert (wt1 + {1'b0, ahead(2'd1)} <= 3'd3);
h_wt2: assert (wt2 + {1'b0, ahead(2'd2)} <= 3'd3);
h_wt3: assert (wt3 + {1'b0, ahead(2'd3)} <= 3'd3);
end
end endgenerate
// covers
always @(*) if (!init) begin
c_g0: cover (grant[0]);
c_g1: cover (grant[1]);
c_g2: cover (grant[2]);
c_g3: cover (grant[3]);
c_contend: cover (req == 4'b1111 && wt3 == 3'd3);
c_idle: cover (req == 4'b0000 && grant == 4'd0);
end
`endif
endmoduleExpected outcome: proved. The variants, by header edit. W1 is run twice, with and without the helpers.
Variant | Edit the header of the block above to | Expected category |
|---|---|---|
W1, helpers on |
| counterexample |
W1, helpers off |
| counterexample |
W2 |
| counterexample |
W3 |
| counterexample |
The wait bound, derived with the model below and not typed in, and a statement that it is exact (a bound one smaller fails).
For each variant: the failing property, step and smallest failing depth; for W1, both the early failure and the requirement failure, and why they differ.
The covers reached, with the contention cover's step.
The rows: structural properties, the wait bound, the covers.
// formal: cover 6 expect reached slang top rr_arbiter
`define FORMAL
// rr_arbiter: 4-requester round-robin arbiter. The grant is combinational from req and the 2-bit
// pointer last (the index granted most recently). The search starts at last + 1 and wraps. A grant is
// given in the cycle a request is seen and lasts one cycle (no lock). Synchronous active-high reset:
// last = 3, so requester 0 has priority first. BUG selects a variant (0 is the design as specified);
// HELPER=0 removes the helper assertions.
module rr_arbiter #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire [3:0] req,
output wire [3:0] grant
);
reg [1:0] last;
wire [3:0] hi_mask = (last == 2'd0) ? 4'b1110 :
(last == 2'd1) ? 4'b1100 :
(last == 2'd2) ? 4'b1000 : 4'b0000;
wire [3:0] hi_req = req & hi_mask;
wire [3:0] lo_req = (BUG == 3) ? (req | (4'b0001 << last)) : req;
function [3:0] lowest; // lowest set bit, one-hot (0 if none)
input [3:0] v;
lowest = v & (~v + 4'd1);
endfunction
wire [3:0] g_hi = lowest(hi_req);
wire [3:0] g_lo = lowest(lo_req) & {4{(hi_req == 4'b0) || (BUG == 2)}};
assign grant = g_hi | g_lo;
wire [1:0] gidx = grant[0] ? 2'd0 : grant[1] ? 2'd1 : grant[2] ? 2'd2 : 2'd3;
always @(posedge clk) begin
if (rst) last <= 2'd3;
else if (|grant && !(BUG == 1 && grant[0])) last <= gidx;
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// wt[i]: consecutive cycles requester i has requested without being granted
reg [2:0] wt0, wt1, wt2, wt3;
always @(posedge clk) begin
if (rst) begin wt0 <= 3'd0; wt1 <= 3'd0; wt2 <= 3'd0; wt3 <= 3'd0; end
else begin
wt0 <= (req[0] && !grant[0]) ? wt0 + 3'd1 : 3'd0;
wt1 <= (req[1] && !grant[1]) ? wt1 + 3'd1 : 3'd0;
wt2 <= (req[2] && !grant[2]) ? wt2 + 3'd1 : 3'd0;
wt3 <= (req[3] && !grant[3]) ? wt3 + 3'd1 : 3'd0;
end
end
always @(*) if (!init) begin
a_onehot0: assert ((grant & (grant - 4'd1)) == 4'd0);
a_no_phantom: assert ((grant & ~req) == 4'd0);
a_work_cons: assert (!(|req) || (|grant));
// bounded response (safety form of starvation freedom): nobody waits more than 3 cycles
a_wt0: assert (wt0 <= 3'd3);
a_wt1: assert (wt1 <= 3'd3);
a_wt2: assert (wt2 <= 3'd3);
a_wt3: assert (wt3 <= 3'd3);
end
// helper invariants for the induction step of the wait bounds
function [1:0] ahead; // how many grants can still precede requester i
input [1:0] i;
ahead = i - last - 2'd1; // 0..3 (mod 4)
endfunction
generate if (HELPER) begin : g_helper
always @(*) if (!init) begin
h_wt0: assert (wt0 + {1'b0, ahead(2'd0)} <= 3'd3);
h_wt1: assert (wt1 + {1'b0, ahead(2'd1)} <= 3'd3);
h_wt2: assert (wt2 + {1'b0, ahead(2'd2)} <= 3'd3);
h_wt3: assert (wt3 + {1'b0, ahead(2'd3)} <= 3'd3);
end
end endgenerate
// covers
always @(*) if (!init) begin
c_g0: cover (grant[0]);
c_g1: cover (grant[1]);
c_g2: cover (grant[2]);
c_g3: cover (grant[3]);
c_contend: cover (req == 4'b1111 && wt3 == 3'd3);
c_idle: cover (req == 4'b0000 && grant == 4'd0);
end
`endif
endmoduledef grant(req, last):
# the search starts at last + 1 and wraps; req is a list of four booleans
for off in range(1, 5):
i = (last + off) % 4
if req[i]:
return i
return None
def trace(masks, last=3):
out = []
for m in masks:
g = grant([bool(m >> i & 1) for i in range(4)], last)
out.append(0 if g is None else 1 << g)
if g is not None:
last = g
return out
# the recorded behaviour of the design: ten cycles of requests and the one-hot grants
assert trace([15, 15, 15, 15, 15, 0, 10, 10, 10, 1]) == [1, 2, 4, 8, 1, 0, 2, 8, 2, 1]
def max_wait(masks, last=3):
wait, worst = [0] * 4, 0
for m in masks:
g = grant([bool(m >> i & 1) for i in range(4)], last)
for i in range(4):
wait[i] = wait[i] + 1 if (m >> i & 1) and g != i else 0
worst = max(worst, wait[i])
if g is not None:
last = g
return worst
BOUND = max_wait([15] * 40) # derived: every requester requests in every cycle
assert BOUND == 3, BOUND
seed, worst = 7, 0 # a deterministic pseudo-random sweep over request patterns
for _ in range(3000):
masks = []
for _ in range(12):
seed = (seed * 1103515245 + 12345) % (2 ** 31)
masks.append(seed >> 8 & 15)
worst = max(worst, max_wait(masks, last=seed % 4))
assert worst <= BOUND, "no pattern waits longer than the derived bound"
assert BOUND - 1 < max_wait([15] * 40), "a bound one smaller would be violated: the bound is exact"
print("PASS: wait bound %d derived from the model, exact, and never exceeded by 3000 patterns" % BOUND)# ---- formal lab results ledger, engine v2: every row cites a typed run (kind, depth, outcome[, design]) ----
STATUS = ("proven", "bounded", "cex", "cover-reached", "inconclusive")
FROM_RUN = {"proven": {("prove", "proved")},
"bounded": {("bmc", "bounded")},
"cex": {("bmc", "counterexample"), ("prove", "counterexample")},
"cover-reached": {("cover", "reached")},
"inconclusive": {("prove", "unproven"), ("cover", "not-reached")}}
NEED = {"sync_fifo": 9, "rr_arbiter": 5, "hs_bridge": 4, "csr_block": 3,
"pipe3": 5, "pipe3_eq": 5, "mod10_counter": 15, "toggle_demo": 4}
KEYS = ("property", "design", "status", "bound", "why", "run", "assumes", "cover", "kills", "rev")
REV = 3
ALWAYS = "reset-at-step-0"
TRACKED = "tracked-constant-stable"
def row_problems(e, runs, tracked):
n = e.get("property", "?")
miss = [k for k in KEYS if k not in e]
if miss:
return ["missing-field:%s:%s" % (n, ",".join(miss))]
if e["status"] not in STATUS:
return ["bad-status:" + n]
p = []
if e["rev"] != REV:
p.append("stale:" + n)
if e["run"] not in runs:
return p + ["no-such-run:" + n]
kind, depth, outcome = runs[e["run"]][:3]
if len(runs[e["run"]]) > 3 and runs[e["run"]][3] != e["design"]:
p.append("run-of-another-design:" + n) # a run typed with its design may be cited only by that design's rows
if (kind, outcome) not in FROM_RUN[e["status"]]:
p.append("status-not-from-run:" + n)
if e["bound"] != depth:
p.append("bound-not-run-depth:" + n)
has_cover = bool(str(e["cover"]).strip())
if e["status"] in ("proven", "bounded") and not (has_cover and e["kills"]):
p.append("no-evidence:" + n)
if e["status"] == "cover-reached" and not has_cover:
p.append("no-evidence:" + n)
if e["status"] == "cex" and not e["kills"]:
p.append("no-evidence:" + n)
if e["status"] == "bounded":
if e["design"] not in NEED:
p.append("unknown-design:" + n)
elif depth < NEED[e["design"]]:
p.append("short-bound:" + n)
if len(str(e["why"]).strip()) < 12:
p.append("unjustified:" + n)
if ALWAYS not in e["assumes"]:
p.append("hidden-assume:%s:%s" % (n, ALWAYS))
if n in tracked and TRACKED not in e["assumes"]:
p.append("hidden-assume:%s:%s" % (n, TRACKED))
return p
def ledger_ok(ledger, runs, required, mutants, register, tracked, final=True):
names = [e.get("property") for e in ledger]
bad = ["duplicate:" + n for n in sorted({n for n in names if names.count(n) > 1})]
bad += ["missing-row:" + r for r in sorted(set(required) - set(names))]
bad += [q for e in ledger for q in row_problems(e, runs, tracked)]
good = [e for e in ledger if not row_problems(e, runs, tracked)] # only a row without complaints counts as evidence
solid = {e["property"] for e in good if e["status"] in ("proven", "cover-reached")}
for a, rows in sorted(register.items()):
if rows is None:
if final:
bad.append("deferred-assume:" + a) # a stage may defer a discharge; a final package may not
elif not (rows and all(r in solid for r in rows)):
bad.append("open-assume:" + a)
bad += ["unregistered-assume:%s:%s" % (e.get("property"), a) for e in ledger
for a in e.get("assumes", []) if a not in register]
killed = {m for e in good if e["status"] == "cex" for m in e["kills"]}
bad += ["survivor:" + m for m in sorted(set(mutants) - killed)]
bad += ["unsupported-kill:%s:%s" % (e.get("property"), m) for e in ledger
if e.get("status") in ("proven", "bounded") for m in e.get("kills", []) if m not in killed]
return bad
def mutation_score(ledger, mutants):
assert mutants, "no mutants registered"
killed = {m for e in ledger if e.get("status") == "cex" for m in e.get("kills", [])}
return len(killed & set(mutants)) / len(mutants)
RUNS = {
"arb-prove": ("prove", 4, "proved"),
"arb-cover": ("cover", 6, "reached"),
}
REQUIRED = ["arbiter-structural", "arbiter-wait-bound", "arbiter-covers"]
MUTANTS = ["arb-W%d" % i for i in range(1, 4)]
REGISTER = {"reset-at-step-0": None} # discharged by the pair rows of lesson 3
YOUR_ROWS = [] # fill from your own runs
bad = ledger_ok(YOUR_ROWS, RUNS, REQUIRED, MUTANTS, REGISTER, set(), final=False)
if YOUR_ROWS:
assert bad == [], bad
print("PASS: stage 2 arbiter package consistent")
else:
expected = ["missing-row:" + r for r in REQUIRED] + ["survivor:" + m for m in MUTANTS]
assert sorted(bad) == sorted(expected), bad
print("PASS: the empty package is rejected with %d complaints" % len(bad))The good design is proved and the covers are reached.
The model derives the bound, reproduces the recorded grants, and the bound is exact.
Each variant gives a counterexample, with the smallest failing depth found by lowering the depth.
The package check passes for your rows.
Hint 1: the structural properties pass on one of the variants, so the fault is in time, not in shape: look at the wait properties.
Hint 2: run the variant with and without the helpers and compare the failing steps; ask which condition is already unrecoverable before any requirement is broken.
Hint 3: keep all four requests high from step 1 and count the ungranted cycles of each requester.
Hint 4: list the signals the failing property reads, find the logic that drives each of them in the listing, and compare it with the sentence of the header comment that governs it.
Derive the bound before proving it. If the proof fails on the good design, check that the wait counters clear on a grant and on a request release: a counter that never clears is a harness fault shaped like starvation. Confirm each grant property against the cover that witnesses its trigger.
A continuously requesting line is granted in the fourth cycle, so it has waited through three: one grant per rival. Four would allow a full extra rotation, which round robin never needs.
The step of each failure and which other properties fail at which step: one fault can be seen by several properties at different steps. The reference reading is in the Solutions topic.