Round-Robin Arbiter in Verilog: Parameterized One-Hot Code

When several masters want the same shared resource at once — a memory port, a crossbar, a NoC link — something has to decide who goes first without letting any requester starve. A round-robin arbiter solves this by handing out access in turns: whoever was served last drops to the back of the line. This tutorial walks through working, runnable round robin arbiter Verilog code that is fully parameterized for N requesters, uses a rotating priority pointer, and emits a clean one-hot grant. Two self-checking testbenches then prove the rotation is fair. Everything here compiles with iverilog -g2005.

The parameterized arbiter module

The whole design fits in one combinational grant network plus a single pointer register. The pointer ptr is a one-hot mask that marks the current highest-priority lane. On every real grant it rotates one step, so the requester just served becomes the lowest priority next time.

// Parameterized round-robin arbiter with a rotating priority pointer.
// Produces a one-hot grant and advances the pointer so the just-served
// requester becomes the lowest priority on the next grant.
module rr_arbiter #(
    parameter N = 4
) (
    input  wire         clk,
    input  wire         rst_n,
    input  wire [N-1:0] req,
    output wire [N-1:0] grant
);
    // ptr is a one-hot mask marking the current highest-priority position.
    reg  [N-1:0] ptr;

    // Select the request bits at or above the pointer (no wrap-around).
    // (ptr-1) sets every bit below the pointer; inverting it keeps the
    // pointer bit and everything above it.
    wire [N-1:0] hi_sel  = ~(ptr - 1'b1);
    wire [N-1:0] hi_req  = req & hi_sel;

    // Isolate the lowest set bit: x & (-x) leaves only the least-significant 1.
    wire [N-1:0] grant_hi = hi_req & (~hi_req + 1'b1);
    wire [N-1:0] grant_lo = req    & (~req    + 1'b1);

    // Prefer a requester in the high group; otherwise wrap to the lowest index.
    assign grant = (|hi_req) ? grant_hi : grant_lo;

    // Rotate the pointer to one position above the granted requester.
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            ptr <= {{(N-1){1'b0}}, 1'b1};      // start priority at index 0
        else if (|grant)
            ptr <= {grant[N-2:0], grant[N-1]}; // left-rotate by one
    end
endmodule

How the rotating pointer works

The trick is a two-stage fixed-priority search combined with a mask. Read the grant network top to bottom:

  1. Build the mask. hi_sel = ~(ptr - 1) keeps the pointer bit and every bit above it. Because ptr is one-hot, ptr - 1 sets all the lower bits; inverting it leaves the pointer position and the higher lanes as the “high-priority group”.
  2. Pick the lowest set bit. The idiom x & (~x + 1) (two’s-complement negate-and-AND) isolates the least-significant 1 in a vector. Applied to the masked requests it returns the first requester at or above the pointer.
  3. Wrap around. If nobody in the high group is asking (|hi_req is 0), the mux falls back to grant_lo, the lowest set bit of the raw request vector. That is the wrap from the top of the vector back to lane 0.
  4. Advance. On a granted cycle the pointer left-rotates by one, placing priority on the lane just after the winner.

Here is the pointer walking through four fully-busy cycles, assuming req = 4'b1111:

Cycleptr (one-hot)Highest priority lanegrant
0000100001
1001010010
2010020100
3100031000

Because the grant is one-hot, downstream logic needs no decoder: each bit directly enables its lane.

Testbench: proving fair rotation

The clearest fairness proof is to make every lane request on every cycle. A correct arbiter must then cycle through all owners in order and give each exactly one turn per N cycles.

// Testbench: all four requesters assert continuously. The grant must
// cycle 0 -> 1 -> 2 -> 3 -> 0 ... proving the pointer rotates fairly.
`timescale 1ns/1ps
module tb_rr_rotate;
    localparam N = 4;
    reg          clk = 1'b0;
    reg          rst_n;
    reg  [N-1:0] req;
    wire [N-1:0] grant;

    rr_arbiter #(.N(N)) dut (
        .clk   (clk),
        .rst_n (rst_n),
        .req   (req),
        .grant (grant)
    );

    always #5 clk = ~clk;   // 10 ns period

    integer i;
    initial begin
        rst_n = 1'b0;
        req   = 4'b1111;    // everyone wants the bus every cycle
        @(negedge clk);
        rst_n = 1'b1;

        for (i = 0; i < 8; i = i + 1) begin
            @(negedge clk);
            $display("cycle %0d : req=%b grant=%b", i, req, grant);
        end

        $display("All requesters busy -> grant rotates evenly across owners.");
        $finish;
    end
endmodule
cycle 0 : req=1111 grant=0010
cycle 1 : req=1111 grant=0100
cycle 2 : req=1111 grant=1000
cycle 3 : req=1111 grant=0001
cycle 4 : req=1111 grant=0010
cycle 5 : req=1111 grant=0100
cycle 6 : req=1111 grant=1000
cycle 7 : req=1111 grant=0001
All requesters busy -> grant rotates evenly across owners.
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpf_sx10ip/tb_rr_rotate.v:33: $finish called at 90000 (1ps)

Notice in the log that the grant visits 0010, 0100, 1000, 0001 and repeats — a perfect four-lane rotation. Over any window of four cycles each lane wins exactly once, which is the formal definition of round-robin fairness.

Sparse requests and an idle bus

Real traffic is bursty. This second bench holds only lanes 1 and 3 active, then drops to an idle bus, then a single request. Two steady requesters must trade the bus rather than one hogging it, and an all-zero request must yield an all-zero grant.

// Testbench: a sparse, changing request pattern. Two steady requesters
// (lanes 1 and 3) must alternate instead of one starving the other, and
// an idle bus (req=0) must produce no grant.
`timescale 1ns/1ps
module tb_rr_fair;
    localparam N = 4;
    reg          clk = 1'b0;
    reg          rst_n;
    reg  [N-1:0] req;
    wire [N-1:0] grant;

    rr_arbiter #(.N(N)) dut (
        .clk   (clk),
        .rst_n (rst_n),
        .req   (req),
        .grant (grant)
    );

    always #5 clk = ~clk;

    initial begin
        rst_n = 1'b0;
        req   = 4'b0000;
        @(negedge clk);
        rst_n = 1'b1;

        req = 4'b1010; @(negedge clk);
        $display("req=%b grant=%b", req, grant);
        req = 4'b1010; @(negedge clk);
        $display("req=%b grant=%b", req, grant);
        req = 4'b1010; @(negedge clk);
        $display("req=%b grant=%b", req, grant);
        req = 4'b0000; @(negedge clk);
        $display("req=%b grant=%b (idle bus)", req, grant);
        req = 4'b0001; @(negedge clk);
        $display("req=%b grant=%b", req, grant);

        $display("Lanes 1 and 3 trade the bus; no lane is starved.");
        $finish;
    end
endmodule
req=1010 grant=1000
req=1010 grant=0010
req=1010 grant=1000
req=0000 grant=0000 (idle bus)
req=0001 grant=0001
Lanes 1 and 3 trade the bus; no lane is starved.
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpw5vlqptv/tb_rr_fair.v:39: $finish called at 60000 (1ps)

The log shows lanes 1 and 3 alternating (1000 then 0010 then 1000), the idle cycle producing 0000, and the lone request winning immediately. Neither steady lane is starved — exactly the behaviour a fixed-priority arbiter would fail.

Common mistakes

  • Advancing the pointer every cycle. If you update ptr unconditionally instead of gating on |grant, the pointer drifts during idle cycles and the “next” lane is no longer the one after the last winner. Only rotate when a grant actually fires.
  • Skipping the wrap-around stage. A single priority search from the pointer upward never services lanes below the pointer, so low-index requesters starve. You need both grant_hi and the grant_lo fallback, selected by |hi_req.
  • Using a plain incrementing counter as the pointer. Blindly cycling 0,1,2,3 wastes a slot whenever the pointed-at lane is not requesting. A proper round-robin skips non-requesters and only rotates past the lane it served.
  • Returning a binary index instead of one-hot. If grant is an encoded index, index 0 is ambiguous with “no grant”. A one-hot vector plus a separate |grant valid signal avoids that trap.
  • Hardcoding the width. Writing 4'b0001 for the reset value breaks when N changes. Use N-relative constants such as {{(N-1){1'b0}}, 1'b1} so the module truly scales.
  • Feeding grant combinationally back into req. That creates a zero-delay loop. Keep the pointer registered, as shown, so the grant depends only on the current req and the stored pointer.

Quick check

Why is the grant output one-hot instead of a binary index?

A one-hot grant drives each lane’s enable directly with no decoder, and it cleanly distinguishes “lane 0 granted” (0001) from “no grant” (0000), which a binary index cannot do without an extra valid bit.

What does the expression x & (~x + 1) compute?

It isolates the lowest set bit of x. ~x + 1 is the two’s-complement negation, and ANDing a value with its negation leaves only the least-significant 1, which is the first requester in priority order.

Why must the pointer only advance when |grant is true?

So that during idle cycles the priority position stays put. If it advanced every clock, the lane after the last winner would be skipped and the round-robin order would break.

What is the purpose of the grant_lo fallback path?

It provides the wrap-around. When no requester sits at or above the pointer, the arbiter grants the lowest-index requester instead, so lanes below the pointer are not starved.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD