Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
    • Flip-Flops and Registers
    • Counters and Timers
    • Shift Registers and LFSRs
    • Pipelining
    • Stalls, Bubbles and Hazards
      • Structural Hazard and Stall Logic
      • Pipeline Flush on Branch Resolve
      • Retiming-Friendly Coding Notes
      • Data Hazards and Forwarding
      • A Round-Robin Arbiter from a Priority Encoder
    • Retiming
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Stalls, Bubbles and Hazards
  1. Topics
  2. RTL Design
  3. Sequential Design
  4. Stalls, Bubbles and Hazards
  5. A Round-Robin Arbiter from a Priority Encoder

Part 3 · Sequential · Intermediate

A Round-Robin Arbiter from a Priority Encoder

Two ways to build round robin from find-first-set: a mask with two priority encoders, and a double-width search.

On this page

    Why this matters

    A priority encoder in front of a shared resource lets requester 0 lock out everyone above it. One pointer register and a second encoder, or a doubled request vector, make it fair with a bound of N - 1 grants.

    After this lesson you can: build a round-robin picker from a mask and two priority encoders or from a double-width search, add the pointer register, and trace the grant for any pointer and request vector.

    Turning a priority encoder into a fair one

    A priority encoder always favours bit 0. Put one in front of a shared resource, keep requester 0 busy, and requester 3 never gets a turn: four requesters asking every cycle give 0, 0, 0, 0 forever.

    Round robin fixes this with one register, last_q, the index of the previous winner. The search starts just above it and wraps. Each requester still asking is served before anyone gets a second turn, so nobody waits more than N - 1 grants. The search itself is plain combinational logic built from the prio_enc of the previous lesson.

    Construction (a): a mask and two priority encoders

    The mask keeps only requests above the last winner. If any survive, the masked priority encoder picks; if none do, the unmasked one wraps around to the lowest requester. The winner's index is loaded into last_q.

    Follow req from the left. The upper path asks who is waiting above the last winner; the lower path is the fallback when nobody is.

    • Mask: {N{1'b1}} << (last + 1) sets every bit strictly above last. When last is N - 1 the shift clears everything, so the search wraps.

    • Two encoders: both are the lowest-index-wins prio_enc. The masked one finds the first requester above last; the unmasked one finds the first requester overall.

    • Mux: valid_m decides. If anyone above last asks, they go first; otherwise the lowest requester wins, which is the wrap-around.

    • Register: last_q loads the winner only when valid is 1.

    rr_pick_mask.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // snippet: construction (a), full listing under Testbench
    module rr_pick_mask #(parameter int N = 4, localparam int W = (N > 1) ? $clog2(N) : 1) (
      input  logic [N-1:0] req,
      input  logic [W-1:0] last,
      output logic [W-1:0] idx,
      output logic         valid
    );
      logic [N-1:0] mask, req_m;
      logic [W-1:0] idx_m, idx_u;
      logic         valid_m, valid_u;
      assign mask  = {N{1'b1}} << (last + 1);   // 1 strictly above last; all 0 when last = N-1
      assign req_m = req & mask;
      prio_enc #(.N(N)) pe_masked   (.req(req_m), .idx(idx_m), .valid(valid_m));
      prio_enc #(.N(N)) pe_unmasked (.req(req),   .idx(idx_u), .valid(valid_u));
      assign idx   = valid_m ? idx_m : idx_u;   // someone above last asks: they go first
      assign valid = valid_u;
    endmodule

    Requests 1101, cycle by cycle

    Requesters 0, 2 and 3 ask every cycle (req = 4'b1101, bit 3 on the left). After reset last_q is 3, so requester 0 has the first turn. These rows are printed by the testbench below.

    Cycle

    last

    mask

    req & mask

    Winner

    Why

    1

    3

    0000

    0000

    0

    Nobody above 3: wrap to the lowest requester

    2

    0

    1110

    1100

    2

    Requester 1 is idle, so 2 is first above 0

    3

    2

    1000

    1000

    3

    Only 3 is above 2

    4

    3

    0000

    0000

    0

    Wrap again

    5

    0

    1110

    1100

    2

    The pattern repeats: 0, 2, 3

    Each requester waits at most two grants, one less than the three requesters asking. That is the fairness bound the bench checks.

    Construction (b): search a doubled vector

    The second construction removes the mux. Write the request vector twice, {req, req}, and start the search at bit last + 1. A search that runs off the top of the lower copy carries on into the upper copy, which is the wrap-around. Folding the two halves together gives a one-hot grant.

    The search uses the lowest-set-bit trick from the priority encoder lesson, shifted to a start position: dbl & ~(dbl - base) keeps the lowest set bit at or above the one-hot base. Subtracting base borrows through the zeros below the first set bit and clears that bit, so the AND with the inverse keeps only it.

    req = 1101

    last = 0

    last = 3

    dbl = {req, req}

    1101_1101

    1101_1101

    base = 1 << (last + 1)

    0000_0010

    0001_0000

    dbl - base

    1101_1011

    1100_1101

    dbl & ~(dbl - base)

    0000_0100

    0001_0000

    Fold the halves: grant

    0100 (requester 2)

    0001 (requester 0, by wrapping)

    rr_pick_dbl.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // snippet: construction (b), full listing under Testbench
    module rr_pick_dbl #(parameter int N = 4, localparam int W = (N > 1) ? $clog2(N) : 1) (
      input  logic [N-1:0] req,
      input  logic [W-1:0] last,
      output logic [W-1:0] idx,
      output logic         valid
    );
      logic [2*N-1:0] dbl, base, first;
      logic [N-1:0]   grant;
      assign dbl   = {req, req};
      assign base  = (2*N)'(1) << (last + 1);   // one-hot start position, at most bit N
      assign first = dbl & ~(dbl - base);       // lowest set bit at or above base
      assign grant = first[N-1:0] | first[2*N-1:N];
      assign valid = |req;
      always_comb begin                         // one-hot to index
        idx = '0;
        for (int i = 0; i < N; i++) if (grant[i]) idx = i[W-1:0];
      end
    endmodule

    A variant rotates req right by last + 1, runs one priority encoder and adds last + 1 back to the index. It is the same idea with a barrel shifter instead of a subtractor.

    The pointer register

    Either picker becomes an arbiter with one register. The rule is short: load the winner, and only when there was one.

    design.sv
    1
    2
    3
    4
    // snippet: from rr_arb, full listing under Testbench
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)     last_q <= W'(N - 1);    // so requester 0 has first turn after reset
        else if (valid) last_q <= idx;          // move only when someone was granted

    This lesson stops at the combinational search and its pointer. The arbitration lesson adds what a bus needs on top: a registered grant held until the transfer is accepted, and an interactive tool for comparing fixed priority with round robin.

    What each costs

    Both pickers were synthesised for N = 8 to generic gates, with the hierarchy flattened, and the longest path counted in gates:

    Design (N = 8)

    Cells

    Longest path (gates)

    (a) rr_pick_mask

    55

    14

    (b) rr_pick_dbl

    77

    25

    rr_arb with (a), including 3 flops for last_q

    71

    16

    rr_arb with (b), including 3 flops for last_q

    88

    27

    Here the mask version is smaller and shallower. The doubled search turns into a 16-bit borrow chain, and generic gates build that as a ripple. A cell library with fast adders, or an FPGA carry chain, changes the picture, so measure on your own target before choosing. For wide N, both shapes are usually rebuilt as trees: encode each half with its valid flag and combine.

    Common mistakes

    Common pitfall

    Moving the pointer when nobody is granted. With last_q <= idx on every cycle, an idle cycle loads index 0, so requester 1 is always first after a gap. With requests 0011 on odd cycles and none on even ones, the correct arbiter grants 0, 1, 0, 1; the buggy one grants 0, 1, 1, 1 and requester 0 never wins again. The twin bug is a mask off by one: << last keeps the last winner in the masked set, so it wins again every time it asks. The bench below catches that one with 4269 mismatches and an 8-grant wait.

    Check yourself N = 8, last_q = 5, req = 8'b0010_0101. Which requester wins, and what is last_q afterwards?

    The mask is 1100_0000, so req & mask is 0 and the unmasked encoder wraps: requester 0 wins and last_q becomes 0. Requester 5 asked too, but it was served last time.

    Check yourself Same requests, next cycle (last_q = 0). Work it with the doubled vector: what are base and the grant?

    base = 1 << 1 = bit 1 of the 16-bit vector. The first set bit at or above bit 1 in {req, req} is bit 2, so requester 2 wins. The order for these requests is 0, 2, 5, 0, 2, 5.

    Testbench

    The bench compares both pickers with a reference that walks from last + 1 upwards and wraps. It covers every request pattern and every last value for N = 4 and N = 8 (64 + 2048 cases), plus 4000 random N = 8 cases. Then it runs two N = 4 arbiters, one per construction, for 5000 cycles of random requests that stay up until granted. It checks that their grants agree and that no waiting requester sees more than N - 1 = 3 grants go to others.

    prio_enc.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    module prio_enc #(parameter int N = 8) (   // N must be at least 1
      input  logic [N-1:0]          req,
      output logic [(N > 1 ? $clog2(N) : 1)-1:0] idx,
      output logic                  valid
    );
      localparam int SW = (N > 1) ? $clog2(N) : 1;
      always_comb begin
        idx   = '0;
        valid = 1'b0;
        for (int i = N-1; i >= 0; i--) begin       // scan from the HIGH end so the LOWEST index wins last
          if (req[i]) begin
            idx   = i[SW-1:0];
            valid = 1'b1;
          end
        end
      end
    endmodule
    
    // (a) Mask and two priority encoders. last = index of the previous grant.
    module rr_pick_mask #(parameter int N = 4, localparam int W = (N > 1) ? $clog2(N) : 1) (
      input  logic [N-1:0] req,
      input  logic [W-1:0] last,
      output logic [W-1:0] idx,
      output logic         valid
    );
      logic [N-1:0] mask, req_m;
      logic [W-1:0] idx_m, idx_u;
      logic         valid_m, valid_u;
      assign mask  = {N{1'b1}} << (last + 1);   // 1 strictly above last; all 0 when last = N-1
      assign req_m = req & mask;
      prio_enc #(.N(N)) pe_masked   (.req(req_m), .idx(idx_m), .valid(valid_m));
      prio_enc #(.N(N)) pe_unmasked (.req(req),   .idx(idx_u), .valid(valid_u));
      assign idx   = valid_m ? idx_m : idx_u;   // someone above last asks: they go first
      assign valid = valid_u;
    endmodule
    
    // (b) Double width: search {req, req} from bit last+1, then fold the halves.
    module rr_pick_dbl #(parameter int N = 4, localparam int W = (N > 1) ? $clog2(N) : 1) (
      input  logic [N-1:0] req,
      input  logic [W-1:0] last,
      output logic [W-1:0] idx,
      output logic         valid
    );
      logic [2*N-1:0] dbl, base, first;
      logic [N-1:0]   grant;
      assign dbl   = {req, req};
      assign base  = (2*N)'(1) << (last + 1);   // one-hot start position, at most bit N
      assign first = dbl & ~(dbl - base);       // lowest set bit at or above base
      assign grant = first[N-1:0] | first[2*N-1:N];
      assign valid = |req;
      always_comb begin                         // one-hot to index
        idx = '0;
        for (int i = 0; i < N; i++) if (grant[i]) idx = i[W-1:0];
      end
    endmodule
    
    // The pointer register that makes either picker round robin.
    module rr_arb #(parameter int N = 4, parameter bit DBL = 0, localparam int W = (N > 1) ? $clog2(N) : 1) (
      input  logic         clk, rst_n,
      input  logic [N-1:0] req,
      output logic [N-1:0] grant,
      output logic [W-1:0] last_q
    );
      logic [W-1:0] idx;
      logic         valid;
      if (DBL) begin : g_dbl
        rr_pick_dbl  #(.N(N)) pick (.req, .last(last_q), .idx, .valid);
      end else begin : g_mask
        rr_pick_mask #(.N(N)) pick (.req, .last(last_q), .idx, .valid);
      end
      assign grant = valid ? (N'(1) << idx) : '0;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)     last_q <= W'(N - 1);    // so requester 0 has first turn after reset
        else if (valid) last_q <= idx;          // move only when someone was granted
    endmodule
    
    Diagram
    $ iverilog -g2012 -s tb_rr -o r rr.sv && vvp r
    cycle 1: last=3 mask=0000 req&mask=0000 grant=0001
    cycle 2: last=0 mask=1110 req&mask=1100 grant=0100
    cycle 3: last=2 mask=1000 req&mask=1000 grant=1000
    cycle 4: last=3 mask=0000 req&mask=0000 grant=0001
    cycle 5: last=0 mask=1110 req&mask=1100 grant=0100
    cases=6112 worst wait=3 grants
    PASS: both pickers match the reference for every request and pointer (N=4, N=8); no requester waits more than N-1 grants

    Remember

    • Mask the requests strictly above the last winner; if any survive take their first, else wrap to the first overall.

    • The double-width form searches {req, req} from bit last + 1 with dbl & ~(dbl - base) and folds the halves.

    • Load the pointer with the winner, and only when someone was granted.

    Rule of thumb: the pointer moves on a grant and only on a grant; the mask starts one above the last winner.

    How this was checked

    Tool: Icarus Verilog 13.0, run on 2026-10-17. Command: iverilog -g2012 -s tb_rr -o r rr.sv && vvp r, where rr.sv is the Testbench listing. Result: cases=6112 (all 64 N = 4 and 2048 N = 8 request and pointer combinations plus 4000 random N = 8 cases), worst wait=3 grants over 5000 random cycles, PASS. The 1101 table rows are the cycle lines it prints.

    Mutation checks on copies of the listing: mask << last instead of << (last + 1) fails with errors=4269 and worst wait 8. Loading last_q on every cycle passes tb_rr (it never idles while a request waits) but a directed run with requests 0011 on odd cycles and 0000 on even ones grants 0001, 0010, 0010, 0010 against 0001, 0010, 0001, 0010 for the correct arbiter.

    Tool: Yosys 0.69+post (git sha1 143eb14f9). Command: yosys -p "read_verilog -sv rtl_only.sv; chparam -set N 8 <top>; synth -flatten -top <top>; stat; ltp -noff" for rr_pick_mask, rr_pick_dbl and rr_arb (DBL 0 and 1). Result: 55 cells and length 14; 77 cells and length 25; 71 and 16; 88 and 27 (the arbiters include 3 $_DFFE_PN1P_ flops).

    Scope and limits: generic gates with no cell library, so cell counts and gate levels show structure, not area or delay; a library adder or FPGA carry chain would change construction (b) most. The fairness check covers one random seed and requests that stay up until granted.

    Keep going

    • Practice: Round-robin arbiter

    • Practice: Priority encoder with parameterised width and valid output

    • Code lab: Round-robin arbiter

    • Interview: run a mock interview on this topic

    Related topics

    • Bus arbitration

    • Priority encoder

    Previous

    85 / 350

    Next: Retiming

    Up next

    Up nextRetiming

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Structural Hazard and Stall Logic
    • Pipeline Flush on Branch Resolve
    • Retiming-Friendly Coding Notes