Home / Learn Hub / Code examples
CODE EXAMPLES

Working Verilog & SystemVerilog, explained line by line

16 examples of the circuits and testbench patterns that come up again and again in RTL and verification interviews. Each one shows the code, why it's written that way, and how interviewers extend it. The 14 Verilog examples were simulated and self-checked before being published.

16EXAMPLES
14SIMULATED & SELF-CHECKED
CopyEVERY BLOCK IS SELECTABLE
Tested, not just typed. Every example marked “Simulated” was compiled with Icarus Verilog and run against a self-checking testbench before publishing.
Read it, then rewrite it. Copy is fine for study, but in interviews you'll write these from memory. Close the page and try.
Looking for something else? Tutorials teach step by step, guides go deep on one topic, and the RTL questions test you.
BasicsBeginner✓ Simulated & self-checked

Flip-flops: asynchronous vs synchronous reset

The same D flip-flop written two ways, so you can see exactly how the reset styles differ.

dff.v
// Two D flip-flops side by side: asynchronous vs synchronous active-low reset
module dff_resets (
  input  wire clk,
  input  wire rst_n,
  input  wire d,
  output reg  q_async,   // clears immediately when rst_n falls
  output reg  q_sync     // clears on the next rising clock edge
);
  always @(posedge clk or negedge rst_n)
    if (!rst_n) q_async <= 1'b0;
    else        q_async <= d;

  always @(posedge clk)
    if (!rst_n) q_sync <= 1'b0;
    else        q_sync <= d;
endmodule

WHY IT'S WRITTEN THIS WAY

  • q_async has negedge rst_n in its sensitivity list, so it clears the instant reset falls, even with no clock.
  • q_sync only looks at rst_n on a clock edge, so reset behaves like any other data input and is timed by STA.
  • Non-blocking <= in both: every clocked block should use it.
Interview angle: Be ready to say why the release of an asynchronous reset is the risky part, and how a reset synchronizer fixes it (see below).
BasicsBeginner✓ Simulated & self-checked

Parameterized up/down counter with load and enable

A reusable counter: set the width once, load any value, count in either direction.

counter.v
// Parameterized up/down counter with synchronous load and enable
module updown_counter #(parameter W = 8) (
  input  wire         clk,
  input  wire         rst_n,
  input  wire         en,
  input  wire         load,
  input  wire         up,        // 1 = count up, 0 = count down
  input  wire [W-1:0] load_val,
  output reg  [W-1:0] count
);
  always @(posedge clk or negedge rst_n)
    if (!rst_n)      count <= {W{1'b0}};
    else if (load)   count <= load_val;          // load has priority over counting
    else if (en)     count <= up ? count + 1'b1 : count - 1'b1;
endmodule

WHY IT'S WRITTEN THIS WAY

  • parameter W makes one module serve every width. Instantiate with #(.W(16)).
  • The if / else if order sets priority: reset, then load, then count. Say so explicitly in interviews.
  • Wrap-around is automatic: an 8-bit counter at 255 counting up goes to 0. The testbench checks 250 + 7 → 1.
Interview angle: Interviewers often extend this live: "now make it stop at a limit" or "add a terminal-count output".
BasicsIntermediate✓ Simulated & self-checked

8-bit maximal-length LFSR

A linear-feedback shift register that cycles through all 255 non-zero states: a cheap pseudo-random source for BIST, scramblers and test patterns.

lfsr.v
// 8-bit maximal-length Fibonacci LFSR: x^8 + x^6 + x^5 + x^4 + 1 (period 255)
module lfsr8 (
  input  wire       clk,
  input  wire       rst_n,
  input  wire       en,
  output reg  [7:0] q
);
  wire fb = q[7] ^ q[5] ^ q[4] ^ q[3];   // taps 8,6,5,4 -> bit indices 7,5,4,3
  always @(posedge clk or negedge rst_n)
    if (!rst_n)  q <= 8'h01;             // any non-zero seed; all-zeros is a lock-up state
    else if (en) q <= {q[6:0], fb};
endmodule

WHY IT'S WRITTEN THIS WAY

  • Taps 8, 6, 5, 4 come from a primitive polynomial, which is what makes the period the maximum 2⁸ − 1 = 255. Our testbench counts it.
  • The seed must be non-zero. All zeros is a lock-up state, because XOR of zeros is zero forever.
  • It uses only flip-flops and a few XOR gates, which is why LFSRs appear in DFT (LBIST pattern generators) and serial links.
Interview angle: Expect "why can't the state be all zeros?" and "how would you get a 256-state sequence?"
BasicsBeginner✓ Simulated & self-checked

Rising and falling edge detector

Turns a level change into a one-clock-cycle pulse. It's used everywhere, from button presses to start signals.

edge.v
// Rising- and falling-edge detector: one-cycle pulses, input must already be synchronous
module edge_detect (
  input  wire clk,
  input  wire rst_n,
  input  wire sig,
  output wire rise,
  output wire fall
);
  reg sig_d;
  always @(posedge clk or negedge rst_n)
    if (!rst_n) sig_d <= 1'b0;
    else        sig_d <= sig;

  assign rise =  sig & ~sig_d;
  assign fall = ~sig &  sig_d;
endmodule

WHY IT'S WRITTEN THIS WAY

  • The trick is comparing the signal with its own value one cycle ago (sig_d).
  • The outputs are combinational (assign), so the pulse appears in the same cycle the change is seen.
  • The input must already be synchronous to clk. For an asynchronous input, add a 2-flop synchronizer first.
Interview angle: A common follow-up: "what happens if sig comes from another clock domain?"
ClockingIntermediate✓ Simulated & self-checked

Clock-enable (tick) generator instead of a divided clock

Produces a one-cycle tick every N clocks, so slow logic can run on the main clock using an enable.

tick.v
// Clock-enable generator: one-cycle 'tick' every N clocks (use instead of a divided clock)
module tick_gen #(parameter N = 10) (
  input  wire clk,
  input  wire rst_n,
  output reg  tick
);
  localparam CW = (N > 1) ? $clog2(N) : 1;
  reg [CW-1:0] cnt;
  always @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      cnt  <= {CW{1'b0}};
      tick <= 1'b0;
    end else if (cnt == N-1) begin
      cnt  <= {CW{1'b0}};
      tick <= 1'b1;
    end else begin
      cnt  <= cnt + 1'b1;
      tick <= 1'b0;
    end
endmodule

WHY IT'S WRITTEN THIS WAY

  • Dividing a clock with a counter bit creates a new clock that CTS and STA must handle, and it can cause skew between domains. An enable keeps everything in one clock domain.
  • The counter width is computed with $clog2(N), so it adapts to any N.
  • Downstream logic uses if (tick) ... inside its normal always @(posedge clk) block.
Interview angle: This is the answer interviewers want when they ask "how do you run part of a design at 1/10 the speed?"
ClockingIntermediate✓ Simulated & self-checked

Reset synchronizer

Asynchronous assertion, synchronous de-assertion: the standard way to deliver reset into each clock domain.

rstsync.v
// Reset synchronizer: asynchronous assertion, synchronous de-assertion
module reset_sync (
  input  wire clk,
  input  wire arst_n,     // raw asynchronous reset, active low
  output wire rst_n       // safe to use in the clk domain
);
  reg r1, r2;
  always @(posedge clk or negedge arst_n)
    if (!arst_n) {r2, r1} <= 2'b00;          // assert immediately, no clock needed
    else         {r2, r1} <= {r1, 1'b1};     // release after two clean edges
  assign rst_n = r2;
endmodule

WHY IT'S WRITTEN THIS WAY

  • Reset asserts immediately through the asynchronous clear, even if the clock is stopped.
  • Reset releases only after two clean clock edges, so every flop in the domain leaves reset in the same cycle, away from the timing window.
  • Use one synchronizer per clock domain. Never share one synchronized reset across unrelated clocks.
Interview angle: Pairs with the flip-flop example above. Interviewers love the "why not just use the raw reset?" follow-up.
InterfacesIntermediate✓ Simulated & self-checked

Push-button debouncer

Mechanical buttons bounce for a few milliseconds. This module ignores the bounces and outputs one clean transition.

debounce.v
// Push-button debouncer: output changes only after the input is stable for STABLE clocks
module debounce #(parameter STABLE = 16) (
  input  wire clk,
  input  wire rst_n,
  input  wire btn_raw,    // asynchronous, bouncy
  output reg  btn_clean
);
  // 1) synchronize the raw input (it is asynchronous to clk)
  reg s1, s2;
  always @(posedge clk or negedge rst_n)
    if (!rst_n) {s2, s1} <= 2'b00;
    else        {s2, s1} <= {s1, btn_raw};

  // 2) count how long the synchronized input has differed from the output
  reg [$clog2(STABLE+1)-1:0] cnt;
  always @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      cnt <= 0; btn_clean <= 1'b0;
    end else if (s2 == btn_clean) begin
      cnt <= 0;                          // agrees with output: nothing to do
    end else if (cnt == STABLE-1) begin
      btn_clean <= s2;                   // stable long enough: accept the new level
      cnt <= 0;
    end else begin
      cnt <= cnt + 1'b1;
    end
endmodule

WHY IT'S WRITTEN THIS WAY

  • Step 1 synchronizes the raw button, which is asynchronous to the clock, through two flops.
  • Step 2 only accepts a new level after it has been stable for STABLE clocks. Choose STABLE for about 5–20 ms at your clock rate.
  • The testbench toggles the input six times rapidly (ignored), then holds it (accepted).
Interview angle: It shows you think about the real world: asynchronous inputs, metastability, and noise.
MemoriesIntermediate✓ Simulated & self-checked

Synchronous FIFO with correct full/empty flags

The single most-asked RTL design question. This version's full flag stays correct after the pointers wrap around.

fifo.v
// Synchronous FIFO: DEPTH must be a power of two. Pointers carry one extra wrap bit.
module sync_fifo #(parameter WIDTH = 8, parameter DEPTH = 8) (
  input  wire             clk,
  input  wire             rst_n,
  input  wire             wr_en,
  input  wire [WIDTH-1:0] din,
  input  wire             rd_en,
  output reg  [WIDTH-1:0] dout,
  output wire             full,
  output wire             empty
);
  localparam AW = $clog2(DEPTH);
  reg [WIDTH-1:0] mem [0:DEPTH-1];
  reg [AW:0]      wr_ptr, rd_ptr;              // AW+1 bits: the MSB counts wraps

  assign empty = (wr_ptr == rd_ptr);
  assign full  = (wr_ptr[AW] != rd_ptr[AW]) &&  // wrapped a different number of times
                 (wr_ptr[AW-1:0] == rd_ptr[AW-1:0]);

  wire do_wr = wr_en && !full;
  wire do_rd = rd_en && !empty;

  always @(posedge clk) if (do_wr) mem[wr_ptr[AW-1:0]] <= din;

  always @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      wr_ptr <= 0; rd_ptr <= 0; dout <= {WIDTH{1'b0}};
    end else begin
      if (do_wr) wr_ptr <= wr_ptr + 1'b1;
      if (do_rd) begin
        dout   <= mem[rd_ptr[AW-1:0]];
        rd_ptr <= rd_ptr + 1'b1;
      end
    end
endmodule

WHY IT'S WRITTEN THIS WAY

  • Each pointer has one extra MSB (the wrap bit). Empty means the pointers are equal. Full means the address bits are equal but the wrap bits differ.
  • Writes are blocked when full and reads are blocked when empty (do_wr and do_rd), so the FIFO protects itself.
  • A common bug we fixed here: full = (wr_ptr - rd_ptr) == DEPTH looks right, but Verilog evaluates it at 32 bits, so after a wrap the difference comes out negative and full never sets. Our simulation confirmed the old version overflowed.
  • DEPTH must be a power of two for the wrap-bit trick.
Interview angle: Be ready for the corner cases: simultaneous read and write when full or empty, and what changes for a non-power-of-two depth.
VerificationIntermediate✓ Simulated & self-checked

Self-checking testbench for the FIFO

A testbench that decides PASS or FAIL by itself, using a SystemVerilog queue as the reference model.

tb_fifo.v
// Self-checking testbench for sync_fifo: a queue is the reference model
`timescale 1ns/1ps
module tb_sync_fifo;
  reg        clk = 0, rst_n = 0, wr_en = 0, rd_en = 0;
  reg  [7:0] din = 0;
  wire [7:0] dout;
  wire       full, empty;
  reg  [7:0] expected [$];            // reference model
  integer    errors = 0, n;

  sync_fifo #(.WIDTH(8), .DEPTH(8)) dut (.*);
  always #5 clk = ~clk;

  task push(input [7:0] v);
    begin
      wr_en = 1; din = v; @(posedge clk); #1 wr_en = 0;
      expected.push_back(v);
    end
  endtask

  task pop_and_check;
    reg [7:0] exp;
    begin
      rd_en = 1; @(posedge clk); #1 rd_en = 0;
      exp = expected.pop_front();
      if (dout !== exp) begin
        errors = errors + 1;
        $display("ERROR: read %h, expected %h", dout, exp);
      end
    end
  endtask

  initial begin
    repeat (2) @(posedge clk); #1 rst_n = 1;
    for (n = 0; n < 5; n = n + 1) begin      // repeat so the pointers wrap
      while (!full && expected.size() < 8) push($random);
      if (!full) begin errors = errors + 1; $display("ERROR: full not set"); end
      repeat (3) pop_and_check();
    end
    while (expected.size() > 0) pop_and_check();   // drain by the model, not the DUT
    if (!empty) begin errors = errors + 1; $display("ERROR: empty not set"); end
    if (errors == 0) $display("PASS");
    else             $display("FAIL: %0d errors", errors);
    $finish;
  end
endmodule

WHY IT'S WRITTEN THIS WAY

  • The queue mirrors what the FIFO should contain: push_back on every write, pop_front and compare on every read.
  • The loop fills the FIFO five times over, so the pointers wrap, which is exactly where the classic full-flag bug hides.
  • The final drain is driven by the model, not the DUT's empty flag, so a broken flag gives a clean FAIL instead of a hang. We checked this by breaking the DUT on purpose.
  • .* connects every port to a same-named signal, which keeps the instantiation short.
Interview angle: Talking about how you know your design works is as important as the design itself.
VerificationAdvancedNeeds an SVA/UVM simulator · not simulated here

SystemVerilog assertions for a req/gnt handshake

Three protocol assertions and a cover, attached to the design with bind, so the RTL file is never edited.

sva.sv
// SVA checker for a req/gnt handshake, attached to the design with 'bind'
module handshake_checker (
  input logic clk,
  input logic rst_n,
  input logic req,
  input logic gnt
);
  default clocking cb @(posedge clk); endclocking
  default disable iff (!rst_n);

  // every new request is granted within 1 to 8 cycles
  a_req_gets_gnt: assert property ($rose(req) |-> ##[1:8] gnt)
    else $error("req not granted within 8 cycles");

  // a request stays up until it is granted (no withdrawing)
  a_req_held: assert property (req && !gnt |=> req)
    else $error("req dropped before gnt");

  // no grant without a request
  a_no_spurious_gnt: assert property (gnt |-> req)
    else $error("gnt without req");

  // prove the interesting case actually happens in simulation
  c_back_to_back: cover property (gnt ##1 $rose(req));
endmodule

// attach to every instance of 'arbiter_port' without editing the RTL
bind arbiter_port handshake_checker u_chk (.clk(clk), .rst_n(rst_n), .req(req), .gnt(gnt));

WHY IT'S WRITTEN THIS WAY

  • default clocking and default disable iff remove repetition from every property.
  • $rose(req) makes the first check start once per request, not on every cycle that req is high.
  • The cover property proves the back-to-back case really happens. An assertion that never triggers proves nothing.
Interview angle: Shows you understand assertions as living specifications, and how bind keeps verification code separate.
VerificationAdvancedNeeds an SVA/UVM simulator · not simulated here

UVM transaction with constraints, and a write-then-read sequence

A realistic UVM sequence item with layered constraints, and a sequence that writes an address and then reads it back.

uvm_seq.sv
// UVM transaction with constraints, and a sequence that uses it
class bus_txn extends uvm_sequence_item;
  rand bit [31:0] addr;
  rand bit [31:0] data;
  rand bit        write;
  rand bit [3:0]  burst_len;

  constraint c_align   { addr[1:0] == 2'b00; }                 // word aligned
  constraint c_region  { addr inside {[32'h0000_0000 : 32'h0000_FFFF]}; }
  constraint c_burst   { burst_len inside {[1:8]};
                         write -> burst_len <= 4; }            // writes are short
  constraint c_mix     { write dist { 1 := 60, 0 := 40 }; }

  `uvm_object_utils_begin(bus_txn)
    `uvm_field_int(addr,      UVM_ALL_ON | UVM_HEX)
    `uvm_field_int(data,      UVM_ALL_ON | UVM_HEX)
    `uvm_field_int(write,     UVM_ALL_ON)
    `uvm_field_int(burst_len, UVM_ALL_ON)
  `uvm_object_utils_end

  function new(string name = "bus_txn");
    super.new(name);
  endfunction
endclass

class write_then_read_seq extends uvm_sequence #(bus_txn);
  `uvm_object_utils(write_then_read_seq)
  rand int unsigned n = 10;
  constraint c_n { n inside {[5:20]}; }

  function new(string name = "write_then_read_seq");
    super.new(name);
  endfunction

  task body();
    bus_txn wr, rd;
    repeat (n) begin
      wr = bus_txn::type_id::create("wr");
      start_item(wr);
      if (!wr.randomize() with { write == 1; })
        `uvm_error("RAND", "write randomization failed")
      finish_item(wr);

      rd = bus_txn::type_id::create("rd");
      start_item(rd);
      if (!rd.randomize() with { write == 0; addr == wr.addr; })   // read back the same address
        `uvm_error("RAND", "read randomization failed")
      finish_item(rd);
    end
  endtask
endclass

WHY IT'S WRITTEN THIS WAY

  • Constraints stack: alignment, address range, burst length (with an implication for writes) and a read/write mix using dist.
  • Randomization happens between start_item and finish_item, and every randomize() result is checked.
  • The inline constraint addr == wr.addr ties the read to the previous write, which gives a self-checkable scenario.
Interview angle: Walk through the handshake line by line. It's the most common UVM coding question.
Clock domain crossingAdvanced✓ Simulated & self-checked

Toggle-based pulse synchronizer (CDC)

Moves a single-cycle pulse from a fast clock domain into a slow one without losing it.

pulsesync.v
// Pulse synchronizer (toggle-based): moves a single-cycle pulse from clk_a to clk_b.
// Works from a fast to a slow domain as long as pulses are spaced >= ~3 clk_b cycles apart.
module pulse_sync (
  input  wire clk_a, rst_a_n, pulse_a,
  input  wire clk_b, rst_b_n,
  output wire pulse_b
);
  // 1) turn each pulse into a level change (toggle) in the source domain
  reg tgl_a;
  always @(posedge clk_a or negedge rst_a_n)
    if (!rst_a_n)     tgl_a <= 1'b0;
    else if (pulse_a) tgl_a <= ~tgl_a;

  // 2) synchronize the level into clk_b, then 3) detect its edges
  reg s1, s2, s3;
  always @(posedge clk_b or negedge rst_b_n)
    if (!rst_b_n) {s3, s2, s1} <= 3'b000;
    else          {s3, s2, s1} <= {s2, s1, tgl_a};
  assign pulse_b = s2 ^ s3;          // one clk_b pulse per toggle
endmodule

WHY IT'S WRITTEN THIS WAY

  • A 1-cycle pulse in the fast domain can fall between two slow-clock edges and vanish. Converting it to a toggle (a level change) means it can't be missed.
  • The toggle passes through a 2-flop synchronizer, and a third flop detects each change, which becomes one pulse in the slow domain.
  • Limitation: pulses must be spaced at least about three destination cycles apart. For bursts, use a handshake or an async FIFO. Our testbench sends 20 pulses from a 250 MHz to a ~71 MHz domain and receives all 20.
Interview angle: This is the textbook answer to "a pulse from a 100 MHz domain is sometimes missed in a 25 MHz domain. Why, and how do you fix it?"
Clock domain crossingBeginner✓ Simulated & self-checked

Binary ↔ Gray code converters

Gray code changes exactly one bit per increment. That's what makes async FIFO pointers safe to synchronize.

gray.v
// Binary <-> Gray converters (parameterized, purely combinational)
module bin2gray #(parameter W = 4) (input wire [W-1:0] bin, output wire [W-1:0] gray);
  assign gray = bin ^ (bin >> 1);
endmodule

module gray2bin #(parameter W = 4) (input wire [W-1:0] gray, output reg [W-1:0] bin);
  integer i;
  always @(*) begin
    bin[W-1] = gray[W-1];
    for (i = W-2; i >= 0; i = i - 1)
      bin[i] = bin[i+1] ^ gray[i];      // each bit = XOR of all higher Gray bits
  end
endmodule

WHY IT'S WRITTEN THIS WAY

  • Binary to Gray is a single expression: bin ^ (bin >> 1).
  • Gray to binary is a chain: each binary bit is the XOR of all the Gray bits above it, done here with a loop in always @(*).
  • The testbench checks the round trip for all 16 values, and that consecutive Gray codes differ in exactly one bit.
Interview angle: Know why Gray code works for counters but not for arbitrary multi-bit data.
ControlAdvanced✓ Simulated & self-checked

4-way round-robin arbiter

Grants a shared resource fairly: whoever was served last goes to the back of the line.

arbiter.v
// 4-way round-robin arbiter: the most recently granted requester gets lowest priority next
module rr_arbiter (
  input  wire       clk,
  input  wire       rst_n,
  input  wire [3:0] req,
  output reg  [3:0] gnt          // one-hot, registered
);
  reg  [1:0] last;               // index of the last grant
  reg  [3:0] next_gnt;
  integer    i;
  reg  [1:0] idx;

  always @(*) begin
    next_gnt = 4'b0000;
    // search starting just after 'last', wrapping around
    for (i = 1; i <= 4; i = i + 1) begin
      idx = last + i[1:0];
      if (req[idx] && next_gnt == 4'b0000)
        next_gnt[idx] = 1'b1;
    end
  end

  always @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      gnt  <= 4'b0000;
      last <= 2'd3;              // so requester 0 has priority first
    end else begin
      gnt <= next_gnt;
      if (next_gnt[0]) last <= 2'd0;
      else if (next_gnt[1]) last <= 2'd1;
      else if (next_gnt[2]) last <= 2'd2;
      else if (next_gnt[3]) last <= 2'd3;
    end
endmodule

WHY IT'S WRITTEN THIS WAY

  • last remembers the previous winner. The search starts just after it and wraps around.
  • Only one grant is ever issued (next_gnt == 0 check), and the grant is registered for clean timing.
  • With all four requesting, the testbench sees grants rotate evenly (at least 9 each in 40 cycles) and always one-hot.
Interview angle: Follow-ups: scaling to N requesters, weighted round-robin, and the critical path of the priority search.
ControlIntermediate✓ Simulated & self-checked

Moore FSM: 1011 sequence detector (overlapping)

Detects the serial pattern 1011, including overlapping matches (so 1011011 contains two matches).

seqdet.v
// Moore FSM: detects the serial pattern 1011, overlapping matches allowed
module seq_1011 (
  input  wire clk,
  input  wire rst_n,
  input  wire din,
  output wire found            // high for one cycle after the last '1' of 1011
);
  localparam S0 = 3'd0,  // nothing matched
             S1 = 3'd1,  // "1"
             S2 = 3'd2,  // "10"
             S3 = 3'd3,  // "101"
             S4 = 3'd4;  // "1011" -> output state
  reg [2:0] state, next;

  always @(posedge clk or negedge rst_n)
    if (!rst_n) state <= S0;
    else        state <= next;

  always @(*) begin
    case (state)
      S0: next = din ? S1 : S0;
      S1: next = din ? S1 : S2;   // "11": the last '1' can still start a match
      S2: next = din ? S3 : S0;   // "100": no suffix matches a prefix of 1011
      S3: next = din ? S4 : S2;   // "1010": suffix "10" is a prefix
      S4: next = din ? S1 : S2;   // overlap: "10110" ends in "10", "10111" ends in "1"
      default: next = S0;
    endcase
  end
  assign found = (state == S4);   // Moore: output depends on state only
endmodule

WHY IT'S WRITTEN THIS WAY

  • Each state records how much of the pattern has matched so far, using named localparam states.
  • The overlap transitions (S3 → S2 and S4 → S1/S2) come from asking which longest suffix of the input is still a prefix of 1011.
  • Separate sequential (state register) and combinational (next state) blocks, with a default to avoid latches.
  • The testbench feeds 1011011 and expects exactly two detections.
Interview angle: Almost every fresher interview asks for a sequence detector. Practise drawing the state diagram before coding it.
ControlBeginner✓ Simulated & self-checked

Parameterized priority encoder

Returns the index of the highest-priority (highest-numbered) active request.

prienc.v
// Parameterized priority encoder: highest-index set bit wins
module prio_enc #(parameter N = 8) (
  input  wire [N-1:0]         req,
  output reg  [$clog2(N)-1:0] idx,
  output wire                 valid
);
  integer i;
  assign valid = |req;
  always @(*) begin
    idx = 0;
    for (i = 0; i < N; i = i + 1)
      if (req[i]) idx = i;        // later (higher) iterations overwrite earlier ones
  end
endmodule

WHY IT'S WRITTEN THIS WAY

  • In a combinational for loop, later iterations overwrite earlier ones, so the last set bit found wins.
  • Synthesis unrolls the loop into a priority chain. For large N, a tree-based structure is faster.
  • valid is separate, because an index of 0 is ambiguous: bit 0 set, or nothing set?
Interview angle: Explain why the valid output is needed. It's a subtle point interviewers check.
PRACTISE

Try these without looking

Close this page and write each one from memory, then simulate it. Free simulators such as Icarus Verilog or Verilator (or EDA Playground in the browser) are all you need.

WARM-UP

Modify the counter

Add a terminal-count output that pulses when the counter wraps. Then make the counter saturate at a limit instead of wrapping.

CORE

Detect a different sequence

Change the FSM to detect 1101 (overlapping), and write a testbench that counts the matches in a random stream.

STRETCH

Make the FIFO asynchronous

Use the Gray converters and synchronizers from this page to give the FIFO separate read and write clocks.