Round-robin and priority arbiters

Build a round-robin arbiter from a pointer and two priority chains, count its fairness in transfers, and keep a stalled grant in place.

Lesson
R5.4
Time
30 min
Runs on
Your browser
Check
5 questions

Four requesters shared one bus through a fixed-priority arbiter. Requester 0 always had something to send, so it won every cycle. Requesters 1 to 3 waited for the whole run, and in the real system their queues overflowed. The arbiter did exactly what its code said; the system was unfair.

Fixed priority with four busy requestersAll four ask in every cycle. Each transfer takes two cycles, so the resource is ready in every other cycle.
01234567clkreq1111readygrant0001
  1. Cycle 0

    Cycle e2: all four ask. The lowest request wins, so grant is 0001.

  2. Cycle 1

    Cycle e3: ready is 1, so requester 0 makes its transfer at edge 4.

  3. Cycle 2

    Cycle e4: requester 0 asks again at once and wins again. Requesters 1 to 3 never see a grant.

Measured on the lab bench, cycles e2 to e9. Over 16 cycles requester 0 makes all 8 transfers.

Fixed priority: small, and unfair under load

An arbiter picks one of several requesters in each cycle and tells it so with a grant. Here the grant is one-hot: one bit per requester, at most one bit set. A transfer happens at an edge where a grant is set and the shared resource is ready.

A fixed-priority arbiter grants the lowest-numbered request. It is a priority chain: test request 0, then 1, then 2, and the first one set wins. It needs no state and only 5 cells and 2 levels of logic on this flow.

Its only promise is order. If requester 0 asks in every cycle, nobody else is ever served. That is starvation: a requester that keeps asking and never gets a transfer.

Round robin: a pointer and two chains

A round-robin arbiter remembers who is first in line in a small register, the pointer. It grants the first request at or after the pointer, wrapping round to requester 0. After a transfer, the pointer moves to the requester just after the winner.

One round-robin grantArrows carry values inside one clock cycle, except the pointer update, which happens at the edge.
One round-robin grantArrows carry values inside one clock cycle, except the pointer update, which happens at the edge. input req[3:0] who is asking flop ptr_q first in line (flop) logic Chain 1 requests at or afterptr logic Chain 2 all requests, from 0 logic Pick chain 1 if it foundone output grant[3:0] one-hot req & maskmaskreqfirstfirstgrantwinner + 1 at atransfer
  1. Mask

    With ptr_q = 2 and all four asking, the mask 1100 keeps requests 2 and 3 for chain 1.

  2. Two chains

    Chain 1 finds request 2. Chain 2 would find request 0, but it is used only when chain 1 finds nothing.

  3. Grant

    The grant is 0100. When ready is 1, the transfer happens at the edge.

    Follow the calls, values and waits
    1. data grant 0100: Pick → grant[3:0]
  4. Move the pointer

    At that edge the pointer becomes 3, the requester after the winner. Next time chain 1 starts at requester 3, then chain 2 wraps to 0.

    Follow the calls, values and waits
    1. data ptr = 3: grant[3:0] → ptr_q
Read the diagram as text
  • req[3:0]: who is asking
  • ptr_q: first in line (flop)
  • Chain 1: requests at or after ptr
  • Chain 2: all requests, from 0
  • Pick: chain 1 if it found one
  • grant[3:0]: one-hot
  • req[3:0] → Chain 1: req & mask
  • ptr_q → Chain 1: mask
  • req[3:0] → Chain 2: req
  • Chain 1 → Pick: first
  • Chain 2 → Pick: first
  • Pick → grant[3:0]: grant
  • grant[3:0] → ptr_q: winner + 1 at a transfer

Chain 2 is the wrap-around: it serves the requesters below the pointer when nobody at or above it asks.

The round-robin arbiter, same ports as the lab. The function first is the fixed-priority chain; it is used twice.
// arb4: one grant per cycle among four requesters, in round-robin order.
// A transfer happens at an edge where grant is not 0000 and ready is 1.
module arb4 (
  input  logic       clk,
  input  logic       rst_n,      // active-low, asynchronous
  input  logic [3:0] req,        // req[i] = 1: requester i has a transfer to make
  input  logic       ready,      // 1 when the resource takes the granted transfer
  output logic [3:0] grant       // one-hot, or 0000 when nobody asks
);
  logic [1:0] ptr_q;             // the requester first in line

  // A priority chain: the lowest set bit of v wins.
  function automatic logic [3:0] first(input logic [3:0] v);
    casez (v)
      4'b???1: return 4'b0001;
      4'b??10: return 4'b0010;
      4'b?100: return 4'b0100;
      4'b1000: return 4'b1000;
      default: return 4'b0000;
    endcase
  endfunction

  logic [3:0] upper;             // the requests at or after the pointer
  always_comb begin
    upper = req & (4'b1111 << ptr_q);
    if (upper != 4'b0000) grant = first(upper);   // chain 1: from the pointer up
    else                  grant = first(req);     // chain 2: wrap round to requester 0
  end

  logic [1:0] win;               // the number of the granted requester
  assign win = {grant[3] | grant[2], grant[3] | grant[1]};

  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n)                          ptr_q <= 2'd0;
    else if (grant != 4'b0000 && ready)  ptr_q <= win + 2'd1;   // taken: the next one is first in line
    else if (grant != 4'b0000)           ptr_q <= win;          // not taken: the winner stays first
endmodule
The same bench with round robinAll four ask; ready is 1 in every other cycle. The grant moves on after each transfer.
01234567clkreadygrant0001001001001000
  1. Cycle 1

    Requester 0 makes its transfer at edge 4. The pointer becomes 1.

  2. Cycle 2

    Chain 1 starts at requester 1, so grant is 0010.

  3. Cycle 7

    Requester 3 transfers at edge 10; the pointer wraps to 0 and the round starts again.

Measured on the lab bench, cycles e2 to e9. Over 16 cycles each requester makes 2 transfers.

ArbiterTransfers per requester (16 cycles)Longest waitTransfers to others during it
fixed priority (the lab as shipped)8, 0, 0, 016 cycles, and still waiting8
round robin2, 2, 2, 27 cycles3

Measured on the lab bench, part 1. With round robin a requester that keeps asking waits for at most 3 transfers to the others: N - 1 for N = 4.

Move the pointer only on a transfer

Fairness is counted in transfers, never in grants. A grant that is offered but not taken has served nobody. Watch what happens when the pointer moves on every grant instead.

Pointer moves on every grant

else if (grant != 4'b0000)
  ptr_q <= win + 2'd1;

Requesters 0 and 2 are offered a grant only in cycles where ready is 0. Transfers: 0, 4, 0, 4.

Pointer moves on a transfer

else if (grant != 4'b0000 && ready)
  ptr_q <= win + 2'd1;
else if (grant != 4'b0000)
  ptr_q <= win;

A stalled winner stays first in line. Transfers: 2, 2, 2, 2.

Pointer moved on every grantThe same bench. The grant rotates every cycle, in step with ready.
01234567clkreadygrant00010010010010000001001001001000
  1. Cycle 0

    Cycle e2: requester 0 is granted, but ready is 0. The pointer moves anyway.

  2. Cycle 1

    Cycle e3: requester 1 is granted and ready is 1. It transfers.

  3. Cycle 2

    Requester 2 gets its grant in a cycle where ready is 0, every time. It never transfers.

Measured. Every grant count is 4, but the transfers are 0, 4, 0, 4: the bench reports a longest wait of 16 cycles.

The second rule is about the stalled grant itself. A grant drives the select of the mux that carries the winner's data to the resource. Once that data is offered, it must stay until it is taken. AXI-Stream states it for its own valid: once asserted, it stays asserted with the same data until the handshake.

Part 2 of the bench tests this. Requester 3 asks alone and is granted, but ready is 0. In the next cycle requester 2 starts asking, and it sits between the pointer and the winner.

A newcomer during a stallPart 2 of the lab bench, cycles e18 to e24. grant (hold) is the round robin above; grant (no hold) lacks the line that keeps a stalled winner first.
0123456clkreq00001000110001000000readygrant (hold)0000100001000000grant (no hold)00001000010010000000
  1. Cycle 1

    Cycle e19: only requester 3 asks. Both versions grant it, and ready is 0.

  2. Cycle 2

    Cycle e20: requester 2 arrives. With the hold, the pointer sits on 3 and the grant stays. Without it, chain 1 starts at 0 and finds 2 first.

  3. Cycle 3

    Cycle e21: ready is 1. The held version takes requester 3, whose data waited two cycles. The other takes requester 2 and abandons requester 3's offer.

Measured. The bench's last line, "grant moved during a stall", reads 0 with the hold and 1 without it. Fixed priority also reads 1.

What fairness costs

DesignCells (flops)Levels of logicPart 1 transfersGrant moved in a stall
fixed priority5 (0)28, 0, 0, 01
round robin, pointer on every grant27 (2)80, 4, 0, 410
round robin, pointer on a transfer, no hold28 (2)82, 2, 2, 21
round robin, pointer on a transfer, hold (above)32 (2)92, 2, 2, 20
the same rules, with the held grant in its own register56 (7)142, 2, 2, 20

Cells and levels measured with Yosys 0.70 on this flow; the last two columns on the lab bench.

Fairness costs state and a second chain: 32 cells against 5, and 9 levels of logic against 2. The last row keeps the same rules in a different shape and costs 24 cells and 5 levels more. Two ways of writing the same behaviour can differ that much, so measure both.

The lab

Design brief
PortDirectionWidthMeaning
clkinput1clock
rst_ninput1reset, asynchronous, active-low
reqinput[3:0]req[i] = 1: requester i has a transfer to make
readyinput11 when the resource takes the granted transfer
grantoutput[3:0]one-hot or 0000, in the same cycle as req; a transfer happens at an edge where grant is not 0000 and ready is 1
Clock
rising edge
Reset
rst_n asynchronous, active-low; after reset requester 0 is first in line
Corner cases
nobody asks; one requester alone; a newcomer during a stall; the pointer wrapping from 3 to 0. Fairness: a requester that keeps asking is served within 3 transfers to the others. Stall: while a grant is not taken and its requester still asks, the grant does not change

Run the lab as shipped: requester 0 makes all 8 transfers in part 1, and the grant moves once in part 2. Lint warns that clk, rst_n and ready are unused, which is true of fixed priority. Paste the round-robin module above over it and run again: 2, 2, 2, 2, and no grant moves. Then try the left-hand pointer line of the compare block and read the counts.

Asked in interviews
  1. Fixed priority or round robin?

    Fixed priority is small and gives a strict order, which suits interrupts or a requester that must never wait. Under load it starves the low requesters. Round robin costs a pointer and a second chain, and bounds every wait at N - 1 transfers to the others.

  2. Implement a round-robin arbiter.

    Keep a pointer to the requester first in line. Mask the requests at or above it and take the lowest one; if there is none, take the lowest request overall. Move the pointer past the winner only when a transfer happens.

  3. What is starvation, and how do you test for it?

    A requester that keeps asking and is never served. Hold every request on, count transfers per requester over a window, and check that no requester sees N transfers to others while it waits.

Try it

Each line is one cycle, named after the edge that starts it; "transfer" means the granted requester transfers at the next edge. In part 1 requester 0 wins every cycle. In part 2 requester 2 arrives at e20 and takes the grant from requester 3 in the middle of its stall.

With the round-robin module the grant in part 1 runs 0001, 0001, 0010, 0010, 0100, 0100, 1000, 1000 and repeats. The summary reads part 1 transfers: 2 2 2 2, part 1 longest wait: 7 cycles, 3 transfers to others and grant moved during a stall: 0. Synthesize reports 32 cells, 2 flops and 9 levels of logic.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e2 req=1111 ready=0 grant=0001 e3 req=1111 ready=1 grant=0001 transfer e4 req=1111 ready=0 grant=0001 e5 req=1111 ready=1 grant=0001 transfer e6 req=1111 ready=0 grant=0001 e7 req=1111 ready=1 grant=0001 transfer e8 req=1111 ready=0 grant=0001 e9 req=1111 ready=1 grant=0001 transfer e10 req=1111 ready=0 grant=0001 e11 req=1111 ready=1 grant=0001 transfer e12 req=1111 ready=0 grant=0001 e13 req=1111 ready=1 grant=0001 transfer e14 req=1111 ready=0 grant=0001 e15 req=1111 ready=1 grant=0001 transfer e16 req=1111 ready=0 grant=0001 e17 req=1111 ready=1 grant=0001 transfer e18 req=0000 ready=0 grant=0000 e19 req=1000 ready=0 grant=1000 e20 req=1100 ready=0 grant=0100 e21 req=1100 ready=1 grant=0100 transfer e22 req=1000 ready=0 grant=1000 e23 req=1000 ready=1 grant=1000 transfer e24 req=0000 ready=0 grant=0000 part 1 transfers: 8 0 0 0 part 1 longest wait: 16 cycles, 8 transfers to others grant moved during a stall: 1

Original design · 5 cells · 0 flip-flops · 0 latches · 2 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic [3:0] req = 4'b0000, pending = 4'b0000;
  logic       ready = 1'b0;
  logic [3:0] grant;
  int got[4];                            // part 1: transfers per requester
  int wait_c[4], wait_g[4];              // cycles waited, and transfers to the others meanwhile
  int longest_c = 0, longest_g = 0, moved = 0;
  logic [3:0] stalled = 4'b0000;         // last cycle's grant, if it was not taken
  arb4 dut (.clk(clk), .rst_n(rst_n), .req(req), .ready(ready), .grant(grant));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (edge_n >= 2) begin
      if (edge_n <= 17) begin                // part 1
        req = 4'b1111; ready = (edge_n % 2 == 1);
      end else begin                         // part 2: each requester asks until its transfer
        if (edge_n == 19) pending[3] = 1'b1;
        if (edge_n == 20) pending[2] = 1'b1;
        req = pending; ready = (edge_n == 21 || edge_n == 23);
      end
      #1;
      if (stalled != 4'b0000 && (req & stalled) != 4'b0000 && grant != stalled) moved++;
      if (edge_n <= 17)
        for (int i = 0; i < 4; i++)
          if (grant[i] && ready) begin got[i]++; wait_c[i] = 0; wait_g[i] = 0; end
          else if (req[i]) begin
            wait_c[i]++; if (ready && grant != 4'b0000) wait_g[i]++;
            if (wait_c[i] > longest_c) longest_c = wait_c[i];
            if (wait_g[i] > longest_g) longest_g = wait_g[i];
          end
      stalled = ready ? 4'b0000 : grant;
      if (edge_n > 17 && ready) pending = pending & ~grant;
      if (ready && grant != 4'b0000) $display("e%0d req=%b ready=%b grant=%b  transfer", edge_n, req, ready, grant);
      else                           $display("e%0d req=%b ready=%b grant=%b", edge_n, req, ready, grant);
    end
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    repeat (23) tick();
    $display("part 1 transfers: %0d %0d %0d %0d", got[0], got[1], got[2], got[3]);
    $display("part 1 longest wait: %0d cycles, %0d transfers to others", longest_c, longest_g);
    $display("grant moved during a stall: %0d", moved);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe round-robin arbiter of this lesson, just out of reset, with all four requesters asking and ready held at 1. The program lists the granted requester in each of five cycles. What does it print?
// arb4: one grant per cycle among four requesters, in round-robin order.
// A transfer happens at an edge where grant is not 0000 and ready is 1.
module arb4 (
  input  logic       clk,
  input  logic       rst_n,      // active-low, asynchronous
  input  logic [3:0] req,        // req[i] = 1: requester i has a transfer to make
  input  logic       ready,      // 1 when the resource takes the granted transfer
  output logic [3:0] grant       // one-hot, or 0000 when nobody asks
);
  logic [1:0] ptr_q;             // the requester first in line

  // A priority chain: the lowest set bit of v wins.
  function automatic logic [3:0] first(input logic [3:0] v);
    casez (v)
      4'b???1: return 4'b0001;
      4'b??10: return 4'b0010;
      4'b?100: return 4'b0100;
      4'b1000: return 4'b1000;
      default: return 4'b0000;
    endcase
  endfunction

  logic [3:0] upper;             // the requests at or after the pointer
  always_comb begin
    upper = req & (4'b1111 << ptr_q);
    if (upper != 4'b0000) grant = first(upper);   // chain 1: from the pointer up
    else                  grant = first(req);     // chain 2: wrap round to requester 0
  end

  logic [1:0] win;               // the number of the granted requester
  assign win = {grant[3] | grant[2], grant[3] | grant[1]};

  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n)                          ptr_q <= 2'd0;
    else if (grant != 4'b0000 && ready)  ptr_q <= win + 2'd1;   // taken: the next one is first in line
    else if (grant != 4'b0000)           ptr_q <= win;          // not taken: the winner stays first
endmodule

`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic [3:0] req = 4'b0000;
  logic       ready = 1'b1;
  logic [3:0] grant;
  string s = "grants:";
  arb4 dut (.clk(clk), .rst_n(rst_n), .req(req), .ready(ready), .grant(grant));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (edge_n >= 2) begin
      req = 4'b1111; #1;
      s = {s, $sformatf(" %0d", grant[1] ? 1 : grant[2] ? 2 : grant[3] ? 3 : 0)};
    end
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    repeat (5) tick();
    $display("%s", s);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule
Spot the bugEach option is the pointer update after a transfer by requester win (a 2-bit number). The bench holds all four requests and ready at 1 for 12 cycles and expects 3 transfers each. Which update fails?
ConceptWhat does a fixed-priority arbiter guarantee to requester 3 of 4 when the others are busy?
ConceptEvery arbiter here gives a one-hot grant. What does that buy the data path that follows it?
Read the waveformThe round robin on the lab bench. At which edge does requester 2 make its first transfer?
Round robin, part 1
01234567readygrant0001001001001000

Cycles e2 to e9, left to right. A transfer happens at the edge after a cycle where ready is 1 and a grant is set.

Remember

  • An arbiter grants one requester per cycle; a one-hot grant lets the data mux be a plain AND-OR.
  • Fixed priority is a priority chain: 5 cells here, and it starves every requester below a busy one.
  • Round robin keeps a pointer to the requester first in line and uses two chains: from the pointer up, then from 0. A requester that keeps asking waits for at most N - 1 transfers.
  • Move the pointer only when a transfer happens, and keep a stalled winner first in line, so a grant never moves while its data waits.