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
endmodule
req  -> 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
endmodule
req     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
endmodule
t=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 assign grant on all paths. Clearing it to 0 at the top solves this.
  • Dropping the priority guard. Writing if (req[i]) grant[i] = 1'b1; without the found check 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_idx breaks when WIDTH changes; let $clog2 compute 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.

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