VLSI Front-End

RTL Design
with Verilog HDL

Register Transfer Level (RTL) design is the front-end of the ASIC flow — where hardware behavior is described in Verilog or SystemVerilog before synthesis converts it into gates. This guide covers everything from module structure to synthesis-safe FSM coding.

Verilog HDL
Synthesis Ready
FSM Design
Pitfall Avoidance
Section 01

Verilog Module Structure

Every design in Verilog is organized as a module — the fundamental building block of HDL design, equivalent to a black box with ports.

Port Types

input / output / inout

Ports declare the interface. input drives into the module, output drives out, inout is bidirectional (used for bus interfaces).

Data Types

wire vs reg

wire is a net driven continuously. reg is a variable holding its last assigned value. Despite the name, reg does not always infer a flip-flop.

Parameters

Parameterized Modules

Use parameter to make modules reusable with configurable widths, depths, or modes — avoiding duplicate code for different bus sizes.

Verilog
module reg_file #(
    parameter WIDTH = 8,
    parameter DEPTH = 16
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire             wr_en,
    input  wire [$clog2(DEPTH)-1:0] wr_addr,
    input  wire [WIDTH-1:0] wr_data,
    input  wire [$clog2(DEPTH)-1:0] rd_addr,
    output reg  [WIDTH-1:0] rd_data
);

    reg [WIDTH-1:0] mem [0:DEPTH-1];

    // Write port
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            integer i;
            for (i = 0; i < DEPTH; i = i + 1)
                mem[i] <= {WIDTH{1'b0}};
        end else if (wr_en) begin
            mem[wr_addr] <= wr_data;
        end
    end

    // Read port (async)
    always @(*) begin
        rd_data = mem[rd_addr];
    end

endmodule
Section 02

Blocking vs Non-Blocking Assignments

The most critical distinction in Verilog RTL. Mixing the two incorrectly produces simulations that diverge from synthesized hardware.

PropertyBlocking ( = )Non-Blocking ( <= )
Execution orderSequential — each line waits for the previousParallel — RHS evaluated together, LHS updated together
Hardware targetCombinational logic (assign, always @(*))Sequential logic (always @(posedge clk))
Race condition riskHigh if used in clocked blocksNone — designed for clocked blocks
Synthesis inferenceWire / combinational logicFlip-flop register
Simulation modelImmediate update in the active regionUpdate deferred to NBA (Non-Blocking Assignment) region
Golden Rule: Always use <= inside clocked always blocks. Always use = inside combinational always @(*) blocks. Never mix both in the same always block.
CORRECT — Sequential
always @(posedge clk) begin
    a <= b;   // non-blocking
    b <= a;   // swaps a and b
end
// Both RHS evaluated before any LHS update
// Result: a gets old b, b gets old a
WRONG — Blocking in clocked
always @(posedge clk) begin
    a = b;   // blocking — a updated now
    b = a;   // b gets new a, not old a
end
// Both end up with original value of b
// Simulation & synthesis mismatch!
Section 03

Combinational & Sequential Logic

RTL design uses two always block templates — one for pure combinational logic, one for clocked registers. Keeping them separate is a synthesis best practice.

Combinational
// Combinational MUX
always @(*) begin
    case (sel)
        2'b00: out = in0;
        2'b01: out = in1;
        2'b10: out = in2;
        default: out = in3; // REQUIRED
    endcase
end

// Equivalent continuous assign
assign out = (sel == 2'b00) ? in0 :
             (sel == 2'b01) ? in1 :
             (sel == 2'b10) ? in2 : in3;
Sequential
// D Flip-Flop with sync reset
always @(posedge clk) begin
    if (rst_n == 1'b0)
        q <= 1'b0;
    else
        q <= d;
end

// With clock enable
always @(posedge clk) begin
    if (!rst_n)
        q <= 1'b0;
    else if (en)
        q <= d;
    // else q holds — no latch,
    // because this is clocked
end
always @(*) vs always @(posedge clk): Use always @(*) for combinational — the simulator automatically tracks all signals read inside. Never manually write sensitivity lists for combinational blocks; missing a signal causes simulation-synthesis mismatch.
Section 04

Latch Inference — How & How to Avoid

Unintended latches are one of the most common RTL bugs. They are level-sensitive storage elements that synthesis tools create when a combinational block has incomplete output assignments.

Infers a Latch
always @(*) begin
    if (en) begin
        out = data_in;  // only assigned when en=1
    end
    // When en=0, 'out' not assigned
    // → synthesis infers a latch to hold value
end
No Latch — Default Assignment
always @(*) begin
    out = 1'b0;       // default at top
    if (en) begin
        out = data_in; // overrides when en=1
    end
    // All paths assign 'out' → pure combo
end
CauseExampleFix
Incomplete if without elseif (sel) y = a;Add else y = 0; or default at top
case without defaultcase(op) ... endcaseAdd default: y = 0;
Output not assigned all pathsOutput only in one branchAssign default before the if/case
Missing signal in sensitivity listalways @(a) — misses bUse always @(*)
Section 05

Finite State Machine (FSM) Design

FSMs are the backbone of control logic in RTL. The two-always-block template (state register + next-state/output logic) is the industry standard for synthesis-clean FSMs.

Mealy Machine

Output depends on state + input

Output is computed in the combinational next-state block. Responds one cycle faster than Moore but output can glitch with input noise.

Moore Machine

Output depends only on state

Output is registered — cleaner, glitch-free. Preferred for most ASIC control logic. Requires one extra state but is more robust.

FSM — Two Always Block Template
// State encoding — use parameter for readability
parameter IDLE  = 2'b00,
          FETCH = 2'b01,
          EXEC  = 2'b10,
          DONE  = 2'b11;

reg [1:0] state, next_state;

// Block 1: State register (sequential)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= next_state;
end

// Block 2: Next-state + output logic (combinational)
always @(*) begin
    next_state = state;   // default: stay in state
    out        = 1'b0;    // default output

    case (state)
        IDLE: begin
            if (start)
                next_state = FETCH;
        end
        FETCH: begin
            next_state = EXEC;
        end
        EXEC: begin
            out = 1'b1;
            if (done)
                next_state = DONE;
        end
        DONE: begin
            next_state = IDLE;
        end
        default: next_state = IDLE;
    endcase
end
Binary Encoding

Fewest Flip-Flops

Uses log₂(N) FFs for N states. More complex next-state logic. Good for large FSMs in area-constrained ASIC designs.

One-Hot Encoding

Fastest Combinational

One FF per state. Simplest next-state logic — just check one bit. Preferred for FPGAs and high-speed ASIC control paths.

Gray Encoding

Low Switching Power

Adjacent states differ by one bit — reduces glitches and switching activity. Used in counters crossing clock domains.

Section 06

Synthesis-Safe Coding Guidelines

RTL written without these rules may simulate correctly but synthesize into hardware that behaves differently — or fail timing closure.

RuleWhat to DoWhy
ClockingOne clock per always blockAvoids undefined behavior with multiple edges
Reset styleSynchronous reset preferredEasier timing closure; async reset needs special care in CDC
Assignments<= in clocked, = in combinationalMatches hardware semantics; prevents sim/synth mismatch
Sensitivity listUse always @(*) for comboAuto-updates, prevents incomplete sensitivity list bugs
DelaysNo #delay in RTLDelays are ignored by synthesis; simulation-only construct
LatchesAlways assign defaultsPrevents unintended latch inference
Initial blocksAvoid in synthesizable RTLNot supported in all synthesis flows; use reset instead
Case statementsAlways include defaultPrevents priority encoder inference and latch creation
ArithmeticMind bit-width on overflowSynthesis matches the declared width — truncation can corrupt values
GenerateUse generate for repetitive structuresKeeps code scalable and readable; synthesizes correctly
Interactive Lab

RTL Pattern Explorer

Select a common RTL scenario to see the correct vs incorrect coding pattern and what hardware each infers.

RTL Coding Pattern Comparison
Click any topic to see the correct and incorrect pattern side by side.
✓ Correct — use <= in clocked block
always @(posedge clk) begin
    q1 <= d;        // FF
    q2 <= q1;       // FF — pipeline
end
// q1 and q2 are two pipeline stages
// Infers: 2 flip-flops
✗ Wrong — blocking in clocked block
always @(posedge clk) begin
    q1 = d;         // q1 updated NOW
    q2 = q1;        // gets new q1, not old
end
// q2 immediately equals d — no pipeline!
// Simulation ≠ Synthesis behavior

Non-blocking assignments model pipelined registers correctly. Both RHS values are sampled before any LHS is updated — this is the fundamental flip-flop behavior.

✓ Correct — default prevents latch
always @(*) begin
    y = 1'b0;        // default
    if (sel)
        y = data;
end
// All paths assign y → pure combinational
// Infers: MUX, no latch
✗ Wrong — incomplete assignment
always @(*) begin
    if (sel)
        y = data;
    // No else — y not assigned when sel=0
end
// y must "remember" its last value
// Infers: latch (level-sensitive storage)

Latches cause timing closure problems — they are transparent, not edge-triggered, and STA tools cannot easily constrain them. Always assign a default.

✓ FSM with safe default state
always @(*) begin
    next_state = IDLE; // default
    case (state)
        IDLE:  if (req) next_state = ACTIVE;
        ACTIVE: next_state = DONE;
        DONE:  next_state = IDLE;
        default: next_state = IDLE; // safety
    endcase
end
// No latch — all states covered
✗ FSM without default
always @(*) begin
    case (state)
        IDLE:   next_state = ACTIVE;
        ACTIVE: next_state = DONE;
        DONE:   next_state = IDLE;
        // no default!
    endcase
end
// Undefined states → latch inferred
// Or: X-propagation in simulation

The default assignment before the case statement AND the default case inside both serve different purposes. Use both for a fully robust FSM.

✓ Async reset — correct sensitivity list
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;    // async reset
    else
        q <= d;
end
// rst_n in sensitivity list → async behavior
// Reset takes effect immediately (no clock needed)
✓ Sync reset — simpler timing
always @(posedge clk) begin
    if (!rst_n)
        q <= 1'b0;    // sync reset
    else
        q <= d;
end
// rst_n NOT in sensitivity list → sync behavior
// Reset only takes effect on clock edge
// Easier timing closure — preferred in ASIC

Both are valid. Sync reset is easier to close timing on. Async reset is useful when the chip must reset without a running clock (power-on, brownout). In CDC designs, async reset must be synchronized before release.

FAQ

Common RTL Questions

Answers to the most frequently asked RTL design questions in VLSI interviews and design reviews.

wire is a net — it must be driven at all times by a continuous assignment or module output port. reg is a variable that retains its last assigned value between always block executions. In synthesis: reg inside always @(posedge clk) infers a flip-flop; reg inside always @(*) infers combinational logic (wire). The name "reg" is misleading — it does not necessarily mean a hardware register.
Blocking (=) executes sequentially — each statement completes before the next starts. Non-blocking (<=) evaluates all right-hand sides first, then updates all left-hand sides simultaneously at the end of the time step. Use = in combinational blocks and <= in clocked blocks. Mixing them in the same clocked block causes simulation-synthesis mismatch and race conditions.
Always assign a default value at the top of every combinational always block — before any if/case statement. This ensures the output is assigned for all possible input conditions. Also ensure every case statement includes a default branch. Tools like Synopsys Design Compiler will warn you about latch inference — treat these warnings as errors.
The two-always-block style: one clocked always block for the state register, and one combinational always @(*) block for next-state and output logic. Keep outputs registered when possible (Moore) to avoid glitches. Use binary encoding for area-constrained designs, one-hot for speed-critical control logic. Always include a default case and reset to a known safe state.
Synchronous reset is preferred for ASIC designs — it is sampled on the clock edge, so timing tools can analyze it like any other data path. Asynchronous reset asserts immediately without a clock, which is useful for power-on reset scenarios but requires a reset synchronizer circuit for safe deassertion across clock domains. Mixing async and sync resets in the same design is a common source of metastability and should be avoided.