Priority Arbiter in Verilog: Fixed Priority + Mask Trick
When several blocks want the same resource at the same time — a shared memory port, a single bus master slot, one write channel — something has to pick exactly one winner each cycle. That picker is an arbiter. The simplest and most common flavour is the fixed-priority arbiter: every requester is assigned a rank, and if two or more raise their request lines together, the highest-ranked one wins. This guide walks through working, runnable priority arbiter Verilog code — a parameterized fixed-priority design, the elegant “mask trick” one-liner, and a self-checking testbench — all in plain Verilog-2005 that simulates in Icarus Verilog.
The interface is a classic request/grant handshake. A req bus carries one bit per requester (a 1 means “I want the resource”). The arbiter drives a one-hot grant bus: at most one bit is high, marking the winner. We make bit 0 the highest priority, so if req = 0110, the lower of the two set bits — bit 1 — wins.
The fixed-priority arbiter code
Here is the core module. It is purely combinational, scans from the lowest index upward, and stops at the first request it finds. The WIDTH parameter lets you reuse the same RTL for 2, 4, 8, or any number of requesters.
// Fixed-priority arbiter: bit 0 = highest priority, parameterized width
module priority_arbiter #(
parameter WIDTH = 4
)(
input wire [WIDTH-1:0] req,
output reg [WIDTH-1:0] grant
);
integer i;
reg found;
always @(*) begin
grant = {WIDTH{1'b0}};
found = 1'b0;
for (i = 0; i < WIDTH; i = i + 1) begin
if (!found && req[i]) begin
grant[i] = 1'b1;
found = 1'b1;
end
end
end
endmodule
module tb_priority_arbiter;
localparam WIDTH = 4;
reg [WIDTH-1:0] req;
wire [WIDTH-1:0] grant;
priority_arbiter #(.WIDTH(WIDTH)) dut (.req(req), .grant(grant));
integer k;
initial begin
$display(" req -> grant (bit0 = highest priority)");
for (k = 0; k < 16; k = k + 1) begin
req = k[WIDTH-1:0];
#5;
$display(" %b -> %b", req, grant);
end
$finish;
end
endmodulereq -> grant (bit0 = highest priority) 0000 -> 0000 0001 -> 0001 0010 -> 0010 0011 -> 0001 0100 -> 0100 0101 -> 0001 0110 -> 0010 0111 -> 0001 1000 -> 1000 1001 -> 0001 1010 -> 0010 1011 -> 0001 1100 -> 0100 1101 -> 0001 1110 -> 0010 1111 -> 0001 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp450p10us/fp.v:37: $finish called at 80 (1s)
How the code works
The for loop is unrolled by the synthesis tool into combinational logic — it is not a clocked loop. The found flag is the key idea: once any request is granted, found becomes 1 and the if (!found && req[i]) guard blocks every later bit from also being granted. That guarantees a one-hot output. Because the loop runs from i = 0 upward, the lowest index always wins ties, which is exactly fixed priority with bit 0 on top.
Notice grant and found are reset to 0 at the top of every evaluation of the always @(*) block. This is essential: it makes the logic combinational and latch-free. If you forget to clear grant, the synthesizer infers a latch because the output would hold its old value when no request is active.
The mask trick: a one-line arbiter
For a fixed-priority arbiter where bit 0 is highest, there is a famous bit-manipulation shortcut that isolates the lowest set bit of a vector:
grant = req & (~req + 1)
In two’s complement, ~req + 1 is simply -req. ANDing a number with its own negation leaves only the least-significant 1 standing — every higher set bit is cleared. That single expression is a fixed-priority arbiter, with no loop and no flag. The module below includes both the mask version and the loop version, and the testbench proves they produce identical grants across a sweep of inputs.
// Mask-trick arbiter: grant = req & (~req + 1) isolates the lowest set bit
module arbiter_mask #(
parameter WIDTH = 8
)(
input wire [WIDTH-1:0] req,
output wire [WIDTH-1:0] grant
);
assign grant = req & (~req + 1'b1);
endmodule
// Reference loop version used only to prove equivalence in the testbench
module arbiter_loop #(
parameter WIDTH = 8
)(
input wire [WIDTH-1:0] req,
output reg [WIDTH-1:0] grant
);
integer i;
reg found;
always @(*) begin
grant = {WIDTH{1'b0}};
found = 1'b0;
for (i = 0; i < WIDTH; i = i + 1)
if (!found && req[i]) begin
grant[i] = 1'b1;
found = 1'b1;
end
end
endmodule
module tb_arbiter_mask;
localparam WIDTH = 8;
reg [WIDTH-1:0] req;
wire [WIDTH-1:0] g_mask, g_loop;
integer k, errors;
arbiter_mask #(.WIDTH(WIDTH)) m (.req(req), .grant(g_mask));
arbiter_loop #(.WIDTH(WIDTH)) l (.req(req), .grant(g_loop));
initial begin
errors = 0;
$display(" req mask loop match");
for (k = 0; k < 256; k = k + 37) begin
req = k[WIDTH-1:0];
#2;
$display("%b %b %b %s",
req, g_mask, g_loop, (g_mask === g_loop) ? "ok" : "NO");
if (g_mask !== g_loop) errors = errors + 1;
end
if (errors == 0)
$display("RESULT: mask trick matches loop arbiter on all vectors");
else
$display("RESULT: %0d mismatches", errors);
$finish;
end
endmodulereq mask loop match 00000000 00000000 00000000 ok 00100101 00000001 00000001 ok 01001010 00000010 00000010 ok 01101111 00000001 00000001 ok 10010100 00000100 00000100 ok 10111001 00000001 00000001 ok 11011110 00000010 00000010 ok RESULT: mask trick matches loop arbiter on all vectors /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp9r6yl_d5/mask.v:54: $finish called at 14 (1s)
The mask trick is compact and synthesizes to a clean carry chain, but the loop form is easier to extend — for round-robin arbitration you would add a rotating priority mask on top of exactly this structure.
Adding a binary index and a valid flag
Downstream logic often wants the winner as a binary number (to index a register file or steer a mux) plus a single-bit valid that tells you whether any request was present. This version emits all three outputs. It uses $clog2(WIDTH) to size the index bus automatically.
// Fixed-priority arbiter returning a one-hot grant, a binary index and a valid flag
module priority_arbiter_idx #(
parameter WIDTH = 4
)(
input wire [WIDTH-1:0] req,
output reg [WIDTH-1:0] grant,
output reg [$clog2(WIDTH)-1:0] grant_idx,
output reg valid
);
integer i;
reg found;
always @(*) begin
grant = {WIDTH{1'b0}};
grant_idx = {$clog2(WIDTH){1'b0}};
valid = 1'b0;
found = 1'b0;
for (i = 0; i < WIDTH; i = i + 1) begin
if (!found && req[i]) begin
grant[i] = 1'b1;
grant_idx = i[$clog2(WIDTH)-1:0];
valid = 1'b1;
found = 1'b1;
end
end
end
endmodule
module tb_priority_arbiter_idx;
localparam WIDTH = 4;
reg [WIDTH-1:0] req;
wire [WIDTH-1:0] grant;
wire [$clog2(WIDTH)-1:0] grant_idx;
wire valid;
priority_arbiter_idx #(.WIDTH(WIDTH)) dut (
.req(req), .grant(grant), .grant_idx(grant_idx), .valid(valid)
);
initial begin
$monitor("t=%0t req=%b grant=%b idx=%0d valid=%b",
$time, req, grant, grant_idx, valid);
req = 4'b0000; #5;
req = 4'b1010; #5;
req = 4'b1100; #5;
req = 4'b1000; #5;
req = 4'b0110; #5;
$finish;
end
endmodulet=0 req=0000 grant=0000 idx=0 valid=0 t=5 req=1010 grant=0010 idx=1 valid=1 t=10 req=1100 grant=0100 idx=2 valid=1 t=15 req=1000 grant=1000 idx=3 valid=1 t=20 req=0110 grant=0010 idx=1 valid=1 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmprp58w1_p/idx.v:47: $finish called at 25 (1s)
In the log, the first vector 0000 drives valid = 0 and idx = 0; always check valid before trusting grant_idx, because an index of 0 looks identical whether bit 0 won or nothing was requested.
Common mistakes
- Forgetting the default assignment and inferring a latch. Every combinational
always @(*)must assigngranton all paths. Clearing it to0at the top solves this. - Dropping the priority guard. Writing
if (req[i]) grant[i] = 1'b1;without thefoundcheck grants every active requester at once — the output is no longer one-hot, and two masters collide on the bus. - Making bit 0 the lowest priority by accident. Loop direction decides who wins ties. Count up for “bit 0 highest”; count down for “bit 0 lowest.”
- Expecting fairness. A fixed-priority arbiter can starve low-priority requesters forever if a high-priority one keeps asking. Use round-robin when fairness matters.
- Sizing the index with a magic number. Hard-coding the width of
grant_idxbreaks whenWIDTHchanges; let$clog2compute it. - Using the mask trick for the wrong polarity.
req & (~req + 1)always picks the lowest set bit. If your highest priority is the top bit, you must reverse the vector or use the loop form.
Quick check
Quick check
For req = 4'b1010 with bit 0 as highest priority, what is the one-hot grant?
4'b0010. Bits 1 and 3 are set; bit 1 is the lower index, so it wins and all higher set bits are cleared.
Why is grant assigned to 0 at the start of the always @(*) block?
To keep the logic purely combinational. Without a default on every path, the synthesizer infers a latch to hold the previous value, which is almost never intended.
What does the expression req & (~req + 1) compute?
It isolates the least-significant set bit of req. Since ~req + 1 equals -req in two’s complement, ANDing a value with its negation leaves only the lowest 1, giving a one-hot fixed-priority grant.
Why can a fixed-priority arbiter starve a requester, and what is the fix?
If a higher-priority requester asserts its line every cycle, lower-priority ones never win. A round-robin arbiter, which rotates which requester has top priority after each grant, provides fairness.
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