Arbitration, fairness and atomic access

Choose how several initiators share one target, recognise starvation in a trace, and explain the two ways a bus makes a read-modify-write safe.

Lesson
P1.9
Time
30 min
Runs on
Your browser
Check
5 questions

A DMA engine was given the lowest priority on a shared memory port, because its transfers were "not urgent". The core next to it issued a request three cycles out of four, and an accelerator filled most of the gaps. The DMA engine waited for 12,000 cycles without a single grant, its buffer overflowed, and the audio stream it was feeding dropped out every few seconds.

Fixed priority: the DMA engine never winsThree requesters, one grant per cycle. The arbiter always picks the lowest-numbered requester that asks.
reqgrantownerCorerequester 0Acceleratorrequester 1DMA enginerequester 2Arbiterone grant per cycleMemorythe shared target

A lit req wire is a request; a lit grant wire is this cycle's winner; the pill names who owns the memory.

01234567clkreq0req1req2g0g1g2ownerI0I1I0I1
  1. Cycle 0

    All three ask. The core is requester 0, so it wins.

  2. Cycle 3

    The core pauses for a cycle. The accelerator wins; the DMA engine is still waiting.

  3. Cycle 4

    The core is back and wins again. The DMA engine has asked every cycle and been refused every cycle.

  4. Cycle 7

    Eight cycles, zero grants for requester 2. With these requesters it will never be served: that is starvation.

Fixed priority is simple and fast, and it promises nothing to the lowest requester.

One target, several initiators

When two initiators want the same target in the same cycle, something must choose. An arbiter takes a request from each initiator and returns one grant per cycle; the winner drives the target, the others hold their requests. The choice is the arbiter's policy, and the policy decides who waits and for how long.

Two policies cover most designs. Fixed priority picks the lowest-numbered requester that asks. Round robin starts each search just after the requester granted last time, so a requester that keeps asking is served within three grants when there are three requesters. Many arbiters mix them: a weighted round robin gives one requester two turns per round.

Round robin and fairness

Round robin: the same requests, a different winner each timeThe search starts after the previous winner, so the grant moves around the ring.
reqgrantownerCorerequester 0Acceleratorrequester 1DMA enginerequester 2Arbiterone grant per cycleMemorythe shared target

The pill moves around the ring: after a grant to requester k the search starts at k+1.

01234567clkreq0req1req2g0g1g2ownerI0I2I0I1I2I0I1I2
  1. Cycle 0

    The first search starts at requester 0, which asks and wins.

  2. Cycle 1

    The search starts at requester 1, which is quiet, so requester 2 wins. The DMA engine is served on its second cycle.

  3. Cycle 4

    While all three ask, each wins every third cycle.

  4. Cycle 7

    Eight cycles: three, two and three grants. Nobody waits more than two cycles while asking.

Round robin bounds the wait of every requester. The price is that an urgent requester waits like everyone else.

A policy is fair when a requester that keeps asking is granted within a known number of cycles. Round robin is fair; fixed priority is fair only for the top requester. Starvation is the failure of fairness: a requester that asks forever and is never served. The bench measures it as the longest wait while asking.

Fixed priority is the right choice when one requester truly must not wait, such as a refresh controller or a real-time core, and the rest can tolerate gaps. Round robin is the right choice when every requester has a deadline. The usual mistake is the opening story: a priority list written by importance rather than by deadline.

Where the arbiter lives

BusWho arbitratesBetween what
AHBa central arbitermanagers sharing one address and data bus, one at a time
AXIthe interconnect, per targetrequests from several managers to the same subordinate
Network-on-chipevery routerpackets contending for one output link
PCIeeach switch portpackets, by virtual channel and traffic class

On a shared bus the arbiter is a block of its own. On a point-to-point interconnect it sits at every meeting point.

Making a read-modify-write safe

Arbitration shares a target cycle by cycle, and that is exactly the problem for a read-modify-write. Two cores that each increment a shared counter both read 5, both compute 6, both write 6. One increment is lost, and neither core broke a rule of the bus.

A lost updateTwo cores increment the same counter. The lit slots are the counter's value.
ABCore Acount = count + 1Core Bcount = count + 1Counterstarts at 5

The counter should end at 7. It ends at 6.

012345clka_readb_reada_writeb_writecount_bar1111100011111100
  1. Cycle 0

    Core A reads the counter: 5.

  2. Cycle 1

    Core B reads it too: 5. Nothing stops B, because A holds nothing.

  3. Cycle 3

    A writes 6.

  4. Cycle 4

    B writes 6 as well. The second increment overwrote the first instead of adding to it.

  5. Cycle 5

    Two increments, one counted. A lock or an exclusive pair would have caught this.

Each core did a correct read and a correct write. The gap between them is the bug.

Buses offer two cures. A locked access keeps the grant with one initiator from its read to its write: the arbiter serves nobody else in between. It is simple and it stalls everyone, so AXI4 dropped it and AHB keeps it. An exclusive access leaves the bus free. The initiator marks its read as exclusive and the target records the address and the initiator. The matching exclusive write succeeds only if no other write touched that address since. If one did, the write fails with a plain response, and the initiator reads again and retries. Newer buses add atomic operations, where the target does the whole read-modify-write itself.

MechanismWho guarantees itCostWhere
Locked accessthe arbiter: no other grant until the lock is releasedeverybody waitsAHB, AXI3
Exclusive accessthe target's monitor: the write fails if the location changeda retry when it failsAXI4, AXI5, AHB5
Atomic operationthe target performs read, modify and write in one transactiona wider targetAXI5, TileLink, PCIe

Three ways to keep a read-modify-write whole, from the oldest to the newest.

Asked in interviews: arbitration and atomic access
  1. Priority or round robin, when?

    Fixed priority when one requester must never wait and the others can tolerate gaps. Round robin when every requester has a deadline, because it bounds everyone's wait.

  2. What is starvation?

    A requester that keeps asking and is never granted. Fixed priority starves the lowest requester whenever the higher ones ask often enough.

  3. What is the difference between a locked and an exclusive access?

    A lock holds the bus from the read to the write, so nobody else is served. An exclusive pair leaves the bus free and lets the write fail if the location changed, so the initiator retries.

  4. Where does arbitration happen on AXI?

    Inside the interconnect, at every subordinate that several managers can reach. There is no central arbiter and no shared bus.

  5. How do you verify fairness?

    Count grants per requester under a request pattern where all keep asking, and measure the longest wait while asking. A bounded wait is fairness; an unbounded one is starvation.

Try it

The core asks three cycles in four, the accelerator two in four and the DMA engine every cycle. With fixed priority the DMA engine gets nothing in twelve cycles. Change POLICY(0) to POLICY(1) and run again: the shares become 5, 3 and 4, and nobody waits more than three cycles.

Then make the core ask every cycle with ask(3'b111) four times, and see which policy still serves the others.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: grants: I0 I0 I0 I1 I0 I0 I0 I1 I0 I0 I0 I1 shares over 12 cycles: I0 9, I1 3, I2 0 longest wait while asking: I0 0, I1 2, I2 12
Read included file: arbiter.sv
// arbiter.sv: one grant per cycle among three requesters (read-only in this lesson).
// grant is one-hot in the same cycle as req, or 0 when nobody asks.
//   POLICY 0, fixed priority: requester 0 wins whenever it asks, then 1, then 2.
//   POLICY 1, round robin: the search starts just after the requester granted last time, so a
//   requester that keeps asking is served within three grants.
module arbiter #(parameter int POLICY = 0) (
  input  logic       clk,
  input  logic       rst_n,
  input  logic [2:0] req,
  output logic [2:0] grant
);
  logic [1:0] last;                 // round robin: the requester granted most recently
  int i;
  always_comb begin
    grant = 3'b000;
    if (POLICY == 0) begin
      casez (req)
        3'b??1:  grant = 3'b001;
        3'b?10:  grant = 3'b010;
        3'b100:  grant = 3'b100;
        default: grant = 3'b000;
      endcase
    end else begin
      for (int k = 1; k <= 3; k++) begin
        i = (last + k) % 3;
        if (grant == 3'b000 && req[i]) grant = 3'b001 << i;
      end
    end
  end
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) last <= 2'd2;         // the first round-robin search starts at requester 0
    else if (grant != 3'b000) last <= grant[0] ? 2'd0 : grant[1] ? 2'd1 : 2'd2;
endmodule
Read included file: arb_tasks.svh
// arb_tasks.svh: drive the three requesters cycle by cycle and keep the score (read-only).
// `cycle` counts rising edges from 0. The grant is sampled at every edge.
int cycle = -1;
always @(posedge clk) cycle = cycle + 1;
bit quiet = 0;                       // 1: print only the shares line
int granted [3];                     // grants per requester
int waiting [3];                     // cycles spent asking since the last grant
int longest [3];                     // the longest such wait
string history = "";
int cycles = 0;
initial for (int k = 0; k < 3; k++) begin granted[k] = 0; waiting[k] = 0; longest[k] = 0; end

// Hold one request pattern (bit i for requester i) for one cycle.
task automatic ask(input logic [2:0] r);
  req = r;
  @(posedge clk); #1;
endtask

always @(posedge clk) begin
  logic [2:0] g, r; logic active;
  g = grant; r = req; active = rst_n;
  #1 if (active) begin
    cycles++;
    history = {history, " ", g[0] ? "I0" : g[1] ? "I1" : g[2] ? "I2" : "--"};
    for (int k = 0; k < 3; k++) begin
      if (g[k]) begin granted[k]++; waiting[k] = 0; end
      else if (r[k]) begin waiting[k]++; if (waiting[k] > longest[k]) longest[k] = waiting[k]; end
    end
  end
end

task automatic report();
  #1;                                // let the monitor count the last edge first
  if (!quiet) $display("grants:%s", history);
  $display("shares over %0d cycles: I0 %0d, I1 %0d, I2 %0d", cycles, granted[0], granted[1], granted[2]);
  if (!quiet) $display("longest wait while asking: I0 %0d, I1 %0d, I2 %0d", longest[0], longest[1], longest[2]);
endtask

Check yourself

Predict the outputA round-robin arbiter sees the request patterns 111, 011, 110 twice over. Only the shares line is printed. What is it?
`timescale 1ns/1ps
module tb;
  logic clk = 0, rst_n = 0;
  always #5 clk = ~clk;                 // rising edges at 5, 15, 25, ...
  logic [2:0] req = 3'b000, grant;
  // POLICY 0 is fixed priority (requester 0 first); POLICY 1 is round robin.
  arbiter #(.POLICY(1)) arb (.clk(clk), .rst_n(rst_n), .req(req), .grant(grant));
  `include "arb_tasks.svh"
  initial quiet = 1;
  initial begin
    repeat (2) @(posedge clk); #1 rst_n = 1;   // reset released after edge 1
    repeat (2) begin ask(3'b111); ask(3'b011); ask(3'b110); end
    req = 3'b000;
    report();
    $finish;
  end
endmodule
Read included file: arbiter.sv
// arbiter.sv: one grant per cycle among three requesters (read-only in this lesson).
// grant is one-hot in the same cycle as req, or 0 when nobody asks.
//   POLICY 0, fixed priority: requester 0 wins whenever it asks, then 1, then 2.
//   POLICY 1, round robin: the search starts just after the requester granted last time, so a
//   requester that keeps asking is served within three grants.
module arbiter #(parameter int POLICY = 0) (
  input  logic       clk,
  input  logic       rst_n,
  input  logic [2:0] req,
  output logic [2:0] grant
);
  logic [1:0] last;                 // round robin: the requester granted most recently
  int i;
  always_comb begin
    grant = 3'b000;
    if (POLICY == 0) begin
      casez (req)
        3'b??1:  grant = 3'b001;
        3'b?10:  grant = 3'b010;
        3'b100:  grant = 3'b100;
        default: grant = 3'b000;
      endcase
    end else begin
      for (int k = 1; k <= 3; k++) begin
        i = (last + k) % 3;
        if (grant == 3'b000 && req[i]) grant = 3'b001 << i;
      end
    end
  end
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) last <= 2'd2;         // the first round-robin search starts at requester 0
    else if (grant != 3'b000) last <= grant[0] ? 2'd0 : grant[1] ? 2'd1 : 2'd2;
endmodule
Read included file: arb_tasks.svh
// arb_tasks.svh: drive the three requesters cycle by cycle and keep the score (read-only).
// `cycle` counts rising edges from 0. The grant is sampled at every edge.
int cycle = -1;
always @(posedge clk) cycle = cycle + 1;
bit quiet = 0;                       // 1: print only the shares line
int granted [3];                     // grants per requester
int waiting [3];                     // cycles spent asking since the last grant
int longest [3];                     // the longest such wait
string history = "";
int cycles = 0;
initial for (int k = 0; k < 3; k++) begin granted[k] = 0; waiting[k] = 0; longest[k] = 0; end

// Hold one request pattern (bit i for requester i) for one cycle.
task automatic ask(input logic [2:0] r);
  req = r;
  @(posedge clk); #1;
endtask

always @(posedge clk) begin
  logic [2:0] g, r; logic active;
  g = grant; r = req; active = rst_n;
  #1 if (active) begin
    cycles++;
    history = {history, " ", g[0] ? "I0" : g[1] ? "I1" : g[2] ? "I2" : "--"};
    for (int k = 0; k < 3; k++) begin
      if (g[k]) begin granted[k]++; waiting[k] = 0; end
      else if (r[k]) begin waiting[k]++; if (waiting[k] > longest[k]) longest[k] = waiting[k]; end
    end
  end
end

task automatic report();
  #1;                                // let the monitor count the last edge first
  if (!quiet) $display("grants:%s", history);
  $display("shares over %0d cycles: I0 %0d, I1 %0d, I2 %0d", cycles, granted[0], granted[1], granted[2]);
  if (!quiet) $display("longest wait while asking: I0 %0d, I1 %0d, I2 %0d", longest[0], longest[1], longest[2]);
endtask
Spot the bugEach option is the grant function of an arbiter. All three requesters ask for nine cycles, and the bench expects three grants each. Which option fails?
ConceptA memory port is shared by a display controller, which fails visibly if it waits more than 20 cycles, and two cores. Which policy fits?
ConceptTwo cores each do an exclusive read of a counter, then an exclusive write. Core B writes between A's read and A's write. What happens to A's write?
ConceptWhat does a bench measure to show that an arbiter is fair?

Remember

  • An arbiter returns one grant per cycle among several requesters; its policy decides who waits.
  • Fixed priority serves the lowest-numbered requester and can starve the rest. Round robin starts after the last winner and bounds every wait.
  • Pick fixed priority for a requester that must never wait, round robin when everyone has a deadline, and measure the longest wait while asking.
  • Two reads then two writes lose an update. A locked access holds the bus for the pair; an exclusive pair leaves the bus free and fails the write if the location changed.
  • On AXI the arbiter is inside the interconnect at every subordinate; on AHB it is a block of its own.

Practice