An arbiter decides who may use a shared resource next. Its rules are short and precise, which makes it an ideal block for formal: at most one grant, no grant without a request, and every requester served within a bounded time. The last rule is where round-robin arbiters fail, because the bug only shows under a particular pattern of competing requests. This article checks a 4-requester round-robin arbiter and finds a pointer bug that starves one requester.

9 min read | Intermediate

What You'll Learn

  • Write the three standard arbiter properties: mutual exclusion, validity and fairness
  • Express fairness as a bounded wait with a fairness assumption on the requesters
  • Find a round-robin pointer bug that starves a requester
  • Measure the worst-case wait of a correct arbiter

Standard Arbiter Properties


// mutual exclusion: at most one grant
a_onehot: assert property (@(posedge clk) disable iff (rst) $onehot0(gnt));
// validity: a grant only goes to a requester that asked
a_valid:  assert property (@(posedge clk) disable iff (rst) ##1 (gnt & ~$past(req)) == 4'b0000);
// fairness: a waiting requester is served within 4 cycles (one property per requester)
a_fair3:  assert property (@(posedge clk) disable iff (rst) req[3] |-> ##[0:4] gnt[3]);
// fairness needs well-behaved requesters: a request is held until granted
m_hold3:  assume property (@(posedge clk) disable iff (rst) req[3] && !gnt[3] |=> req[3]);

Mutual exclusion and validity are safety rules from the invariants article. Fairness is a bounded liveness rule from the safety and liveness article. It needs the hold assumption: a requester that gives up cannot complain about not being served.

The Design: a Round-Robin Arbiter

A round-robin arbiter remembers the last requester it granted and searches the others in order, starting after that one. That way every requester gets a turn:


module rr_arb (input clk, input rst, input [3:0] req, output reg [3:0] gnt);
  reg [1:0] last;                       // index of the last granted requester
  reg [3:0] nxt;
  integer i;
  always @(*) begin
    nxt = 4'b0000;
    for (i = 3; i >= 1; i = i - 1)      // search last+1, last+2, last+3, then last itself
      if (req[(last + i) % 4]) nxt = 4'b0001 << ((last + i) % 4);
    if (nxt == 4'b0000 && req[last]) nxt = 4'b0001 << last;
  end
  always @(posedge clk)
    if (rst) begin
      gnt <= 4'b0000; last <= 2'd3;
    end else begin
      gnt <= nxt;
      if (nxt != 4'b0000) last <= 2'd0;   // bug: should remember the index of the grant
    end
endmodule

The loop runs from i = 3 down to 1, so the last match it finds is the nearest requester after last, which wins. The bug is in the pointer update: it always stores 0 instead of the requester that was granted. The search therefore always starts at requester 1, and the arbiter behaves like a fixed-priority arbiter with the order 1, 2, 3, 0.

Running It in ChipVerify Lab

In the lab form, a helper counter per requester measures how long it has waited:


`ifdef FORMAL
  reg init = 1'b1;
  always @(posedge clk) init <= 1'b0;
  always @(*) if (init) assume (rst);
  // requests are held until granted
  genvar k;
  generate for (k = 0; k < 4; k = k + 1) begin : hold
    always @(posedge clk) if (!init && !$past(rst) && $past(req[k]) && !gnt[k]) assume (req[k]);
  end endgenerate
  // at most one grant, and only to a requester that asked
  always @(*) if (!init) assert ($onehot0(gnt));
  always @(posedge clk) if (!init && !$past(rst)) assert ((gnt & ~$past(req)) == 4'b0000);
  // fairness: every requester is served within 4 cycles of asking
  reg [2:0] w0 = 0, w1 = 0, w2 = 0, w3 = 0;
  always @(posedge clk) begin
    w0 <= (rst || gnt[0] || !req[0]) ? 3'd0 : w0 + 3'd1;
    w1 <= (rst || gnt[1] || !req[1]) ? 3'd0 : w1 + 3'd1;
    w2 <= (rst || gnt[2] || !req[2]) ? 3'd0 : w2 + 3'd1;
    w3 <= (rst || gnt[3] || !req[3]) ? 3'd0 : w3 + 3'd1;
  end
  always @(*) if (!init) assert (w0 <= 3'd4 && w1 <= 3'd4 && w2 <= 3'd4 && w3 <= 3'd4);
`endif

The run fails at step 6:

Requester 3 asks from step 1. The tool keeps requesters 1 and 2 busy with new requests. Because the pointer never moves, they always come first in the search, and requester 3 is granted only in step 6, five cycles after asking. The two safety rules hold in this trace: the bug is only visible through fairness.

Quick Check

Q: Would a test with all four requesters asking all the time find this bug?

Show Answer

Probably not quickly. With all requests high, the search from requester 1 grants 1 every time, so requesters 0, 2 and 3 all starve, and a test that only checks "someone gets a grant" passes. A fairness check would catch it, but the formal tool finds the shortest pattern without anyone designing it.

The Fix and the Worst Case

The pointer must store the index of the granted requester:


if (nxt[0]) last <= 2'd0;
if (nxt[1]) last <= 2'd1;
if (nxt[2]) last <= 2'd2;
if (nxt[3]) last <= 2'd3;

With the fix, the run passes at depth 12. Lowering the bound in the fairness assertion measures the real worst case: a bound of 3 fails, so a requester of this arbiter can wait 4 cycles, one for each requester ahead of it in the round plus the registered grant. That number belongs in the specification of the block.

Quick Check

Q: Why does the fairness check need the assumption that requests are held?

Show Answer

Without it, a requester could drop its request just before its turn and raise it again just after, and the arbiter would correctly skip it each time. The wait counter would grow although the arbiter is fair. The assumption models the protocol that the specification promises fairness for.

Common Mistakes

Mistake #1: Checking only mutual exclusion

The buggy arbiter never grants two requesters at once and never grants one that did not ask. Without a fairness rule, it passes.

Mistake #2: Writing fairness without a bound

"Eventually granted" cannot fail in a bounded run. Use the bound from the specification, and measure the real worst case by lowering it.

Mistake #3: Forgetting the hold assumption

Fairness is promised to requesters that wait. Without the assumption, the check fails on legal arbiters, or passes for the wrong reasons.

Practice Exercise

Challenge: Fix the Pointer and Find the Worst Case

Open the example, run it, and fix the pointer update. Run at depth 12 (PRO) or 10. Then lower the bound from 4 to 3, and to 2, and find the smallest bound that still passes.

Hint

Change all four 3'd4 in the fairness assertion together.

Solution

The fixed arbiter passes with a bound of 4. A bound of 3 fails at step 5, and a bound of 2 fails at step 4. The worst-case wait is therefore 4 cycles: a requester asks just after its turn, waits for the three others, and the grant is registered one cycle later.

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

  • Arbiters need mutual exclusion, validity and bounded fairness, with a hold assumption on the requests.
  • A round-robin pointer that always reset to 0 turned the arbiter into a fixed-priority one, and requester 3 waited 5 cycles.
  • The safety rules passed on the buggy design; only fairness found the bug.
  • Lowering the bound measured the worst-case wait of the fixed arbiter: 4 cycles.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.