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
endmoduleHow the rotating pointer works
The trick is a two-stage fixed-priority search combined with a mask. Read the grant network top to bottom:
- Build the mask.
hi_sel = ~(ptr - 1)keeps the pointer bit and every bit above it. Becauseptris one-hot,ptr - 1sets all the lower bits; inverting it leaves the pointer position and the higher lanes as the “high-priority group”. - Pick the lowest set bit. The idiom
x & (~x + 1)(two’s-complement negate-and-AND) isolates the least-significant1in a vector. Applied to the masked requests it returns the first requester at or above the pointer. - Wrap around. If nobody in the high group is asking (
|hi_reqis 0), the mux falls back togrant_lo, the lowest set bit of the raw request vector. That is the wrap from the top of the vector back to lane 0. - 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:
| Cycle | ptr (one-hot) | Highest priority lane | grant |
|---|---|---|---|
| 0 | 0001 | 0 | 0001 |
| 1 | 0010 | 1 | 0010 |
| 2 | 0100 | 2 | 0100 |
| 3 | 1000 | 3 | 1000 |
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
endmodulecycle 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
endmodulereq=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
ptrunconditionally 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_hiand thegrant_lofallback, selected by|hi_req. - Using a plain incrementing counter as the pointer. Blindly cycling
0,1,2,3wastes 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
grantis an encoded index, index 0 is ambiguous with “no grant”. A one-hot vector plus a separate|grantvalid signal avoids that trap. - Hardcoding the width. Writing
4'b0001for the reset value breaks whenNchanges. UseN-relative constants such as{{(N-1){1'b0}}, 1'b1}so the module truly scales. - Feeding
grantcombinationally back intoreq. That creates a zero-delay loop. Keep the pointer registered, as shown, so the grant depends only on the currentreqand 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.
Share your question in comments or talk to our mentor team for batch guidance.
Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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