Hardware Description Language

Verilog HDL
The Language of Digital Design

Verilog is the industry-standard Hardware Description Language (HDL) used to describe, simulate, and synthesize digital circuits for ASICs and FPGAs. This comprehensive guide covers everything from basic module structure to advanced synthesis-ready coding patterns, equipping you with the skills needed to write efficient, optimizable RTL code that passes timing closure and area constraints in production designs.

Modules & Hierarchy
Synthesis-Ready Code
Best Practices
Production-Grade Design

What is Verilog?

Verilog is a Hardware Description Language created in 1984 by Prabhu Goel and popularized in the 1990s as an alternative to VHDL. Today, Verilog (and its successor, SystemVerilog) is used in the vast majority of digital design projects worldwide to describe circuits at the Register Transfer Level (RTL).

The Three Abstraction Levels in Verilog

Verilog can describe circuits at multiple levels of abstraction:

Level Description Use Case
Behavioral (Algorithmic) Describes what a design does, using high-level constructs like loops and conditions Specification, simulation, prototyping
Dataflow (RTL) Describes how data flows between registers through combinational logic; maps directly to hardware ASIC/FPGA synthesis, production designs
Structural (Gate-Level) Describes the exact gates and interconnects; mapped from standard cells Post-synthesis verification, simulation

For synthesis (converting Verilog to gates), only the RTL subset of Verilog is usable — not all language constructs synthesize. Throughout this guide, we focus on synthesis-friendly Verilog.

Why Verilog Matters for VLSI Engineers

The quality of your RTL directly impacts:

  • Timing closure: Poor RTL with deep combinational paths fails timing at higher frequencies
  • Area efficiency: Inefficient logic synthesis leads to unnecessary gate count and power waste
  • Verification complexity: Ambiguous RTL behavior can hide bugs that escape simulation but fail in silicon
  • Testability: Non-synthesizable or simulation-only code cannot be properly scanned or tested
  • Maintainability: Clean RTL code is easier to modify, reuse, and port across technologies

Module Structure & Port Declarations

In Verilog, a module is the fundamental design unit. It defines a hardware block with inputs, outputs, and internal logic. Modules are hierarchical — larger designs are built by instantiating smaller modules.

Anatomy of a Verilog Module

Example: Simple 2-to-1 Multiplexer module mux2to1 ( input wire a, // Input A input wire b, // Input B input wire sel, // Select line output wire out // Output ); // Continuous assignment (combinational logic) assign out = sel ? b : a; endmodule

Key elements:

  • module <name> (...) — Module declaration
  • input/output — Port direction
  • wire — Data type (combinational)
  • assign — Continuous assignment
  • endmodule — Module termination

Port Declaration Styles

ANSI Style (Recommended for RTL) — Modern, cleaner, used in production:

module adder_8bit ( input [7:0] a, b, output [8:0] sum ); assign sum = a + b; endmodule

Non-ANSI (Traditional) Style — Legacy, harder to read:

module adder_8bit (a, b, sum); input [7:0] a, b; output [8:0] sum; assign sum = a + b; endmodule

Best Practice: Use ANSI port declaration style in all new RTL designs. It's more readable, reduces portability bugs, and is the standard in modern design flows.

Port Width and Bus Declarations

module processor ( input wire clk, // Single-bit input input wire [31:0] data_in, // 32-bit bus (bits 0 to 31) input wire [15:0] addr, // 16-bit address output reg [31:0] data_out, // 32-bit output register output wire valid, // Single-bit valid flag input wire reset_n // Active-low reset ); // Internal logic here endmodule

Bit ordering convention: In Verilog, [31:0] means bit 31 (MSB) down to bit 0 (LSB). This matches hardware convention and is universally used in RTL design.

Data Types: Wire vs Reg

Understanding when to use wire vs reg is fundamental to writing correct RTL. These data types carry semantic meaning about how logic is synthesized.

Wire: Combinational Logic

A wire represents a combinational connection between logic gates — it has no memory and must be continuously driven.

Wire Usage Example module logic_example ( input wire [7:0] a, b, output wire [7:0] result ); // Wire assignments use 'assign' (continuous assignment) assign result = a & b; // Bitwise AND (combinational) endmodule

When to use wire:

  • Output of combinational logic (assign blocks, gates)
  • Internal signals that are purely combinational
  • Module port outputs driven by combinational logic
  • Nets in continuous assignments

Reg: Sequential Logic

A reg represents a storage element (flip-flop or memory). The name "reg" is misleading — it doesn't mean it must hold a value; rather, it's a procedural data type used in always blocks.

Reg Usage Example module counter ( input wire clk, input wire reset, output reg [7:0] count ); always @(posedge clk) begin if (reset) count <= 8'b0; else count <= count + 1; // Sequential update end endmodule

When to use reg:

  • Variables inside always blocks
  • Flip-flop outputs (sequential logic)
  • Memory arrays (RAM, registers)
  • Accumulator variables, counters

Critical Distinction

  • Wire: Used with assign (combinational); no memory; always driven
  • Reg: Used in always blocks (procedural); may hold values across clock cycles
  • Synthesis: wire synthesizes to combinational gates; reg synthesizes to flip-flops or latches
  • Misuse: Using reg for combinational output or wire in always blocks causes synthesis errors or incorrect behavior

Always Blocks & Procedural Logic

The always block is where sequential and combinational logic is described procedurally in Verilog. The sensitivity list (event trigger) controls when the block executes.

Combinational Always Block

For combinational logic, use always @(*) to include all input sensitivity automatically:

Combinational Logic with Always module decoder_2to4 ( input wire [1:0] sel, output reg [3:0] out ); always @(*) begin case (sel) 2'b00: out = 4'b0001; 2'b01: out = 4'b0010; 2'b10: out = 4'b0100; 2'b11: out = 4'b1000; default: out = 4'b0000; endcase end endmodule

Key points:

  • @(*) triggers whenever any input changes — synthesizes to combinational logic
  • Output is a reg (procedural variable), not a flip-flop
  • default case prevents latches (incomplete assignment)

Sequential Always Block

For sequential logic, trigger on clock edge. Use posedge clk (rising edge) or negedge clk (falling edge):

Sequential Logic with Always module shift_register ( input wire clk, input wire reset_n, input wire data_in, output reg [3:0] shift_out ); always @(posedge clk) begin if (!reset_n) shift_out <= 4'b0; else shift_out <= {shift_out[2:0], data_in}; // Shift left end endmodule

Sequential block characteristics:

  • Triggered only on specified clock edge (or reset)
  • Synthesizes to flip-flops with clock and reset inputs
  • Use non-blocking assignment (<= ) — critical for correct synthesis
  • Always include reset logic for initialization

Blocking vs Non-Blocking Assignments

This is the single most important concept in RTL design. Misuse of blocking (=) vs non-blocking (<= ) assignments is the root cause of many simulation-synthesis mismatches and functional bugs.

Blocking Assignment (=)

Blocking assignments execute sequentially — the right-hand side is evaluated and assigned immediately, blocking further statements until complete. Use only in combinational always blocks:

Correct Use: Combinational Logic always @(*) begin temp = a & b; // Blocking: immediate evaluation result = temp | c; // Uses updated temp value end

⚠️ DO NOT use blocking assignment in sequential blocks:

// WRONG — causes race conditions
always @(posedge clk)
q = d; // Blocking in sequential block — WRONG!

Non-Blocking Assignment (<=)

Non-blocking assignments are evaluated in parallel — all right-hand sides are computed using the current value, then all left-hand sides are updated simultaneously. Use always in sequential always blocks:

Correct Use: Sequential Logic always @(posedge clk) begin next_q <= d; // Non-blocking: parallel evaluation q <= next_q; // Both use OLD values of next_q and q // After clock, assignments execute in parallel end

Why non-blocking is correct for sequential:

  • Mimics actual flip-flop behavior (all updates synchronous)
  • Eliminates order-dependent bugs
  • Ensures simulation matches post-synthesis behavior
Feature Blocking (=) Non-Blocking (<=)
Evaluation Sequential Parallel
Use in Combinational always @(*) Sequential always @(clk)
Continuous assign Blocked assignments also use = Never use <= with assign
Race Conditions Possible if misused in sequential None — inherently safe
Simulation-Synthesis Match High risk of mismatch if misused Always matches post-synthesis

The Golden Rule

  • Combinational always @(*): Use blocking (=)
  • Sequential always @(posedge/negedge): Use non-blocking (<=)
  • Continuous assign: Always use = (not <=)
  • Never mix: Don't use both = and <= to the same variable in the same always block

Parameterized Modules & Reusability

Parameters allow modules to be configured at instantiation time, enabling reusable, scalable RTL blocks. A 32-bit adder and a 64-bit adder can use the same parameterized module.

Parameter Declaration & Usage

Parameterized Adder Module module adder #( parameter WIDTH = 8 // Default 8-bit, can be overridden ) ( input wire [WIDTH-1:0] a, b, output wire [WIDTH:0] sum ); assign sum = a + b; endmodule

Instantiation with different widths:

// 8-bit adder (default) adder u_add8 ( .a(data_a[7:0]), .b(data_b[7:0]), .sum(sum8) ); // 32-bit adder (override WIDTH) adder #(.WIDTH(32)) u_add32 ( .a(data_a[31:0]), .b(data_b[31:0]), .sum(sum32) );

Parameters must be constant — they're determined at elaboration (before simulation/synthesis), not runtime.

Generate Blocks for Iteration

Generate blocks allow you to instantiate multiple instances or conditionally include logic. Common in datapath design:

Generate Block: Bitwise Adder Chain module adder_chain #( parameter WIDTH = 8 ) ( input wire [WIDTH-1:0] a, b, output wire [WIDTH:0] sum ); wire [WIDTH:0] carry; assign carry[0] = 1'b0; genvar i; generate for (i = 0; i < WIDTH; i = i + 1) begin : bit_adder assign sum[i] = a[i] ^ b[i] ^ carry[i]; assign carry[i+1] = (a[i] & b[i]) | (carry[i] & (a[i] ^ b[i])); end endgenerate assign sum[WIDTH] = carry[WIDTH]; endmodule

Generate advantages:

  • Eliminates repetitive manual instantiation
  • Easy to scale designs with one parameter change
  • Generate variables (like genvar i) are elaboration-time only
  • Can include conditional logic with generate if/else

Synthesis-Friendly Coding Practices

Not all valid Verilog synthesizes equally. Writing synthesis-friendly code directly impacts timing closure, area efficiency, and power consumption.

Avoid Latches — Always Specify Default Values

A latch is an undesirable transparent storage element that can cause timing issues. It occurs when an output is not assigned in all conditional paths:

❌ LATCH INFERRAL (Bad) always @(*) begin if (sel) out = a; // out not assigned when sel=0 // Synthesizer infers a latch to hold the previous value end

Fix: Always assign in all paths:

✓ CORRECT (Good) always @(*) begin if (sel) out = a; else out = b; // All paths assigned end // Or use default: always @(*) begin out = b; // Default value if (sel) out = a; end

Deep Combinational Paths Cause Timing Failures

Synthesis tools must meet timing constraints. Very deep combinational logic (many gates in series) limits maximum clock frequency. Solution: insert pipeline stages:

Pipelining for Timing Closure // BEFORE: Deep combinational path wire [31:0] deep_result; assign deep_result = ((a + b) * c) | (d & e & f); // AFTER: Pipelined with intermediate registers reg [31:0] stage1, stage2; always @(posedge clk) begin stage1 <= (a + b) * c; // Stage 1 stage2 <= stage1 | (d & e & f); // Stage 2 end assign deep_result = stage2;

Pipelining adds latency but increases frequency and throughput — a worthwhile tradeoff in high-performance designs.

Reset Strategy

Every sequential element should have explicit reset logic (usually asynchronous, active-low):

Proper Reset Design always @(posedge clk or negedge reset_n) begin if (!reset_n) q <= 1'b0; // Asynchronous reset else q <= next_state; // Normal operation end

Reset best practices:

  • Use asynchronous active-low reset (industry standard)
  • Reset to known, safe states (usually all zeros)
  • Hold reset for at least 2-3 clock cycles during power-up
  • Avoid resetting to non-zero values unless necessary

Common Synthesis Pitfalls

  • Incomplete sensitivity lists → missing signals in combinational blocks
  • Loops without termination → infinite elaboration time
  • Real numbers (float) → not synthesizable; use fixed-point or integers
  • $display and $monitor → simulation-only; removed during synthesis
  • Delays (#delay) → simulation-only; ignored in synthesis
  • Initial blocks → synthesis-dependent; avoid in production RTL

Complete Example: Synthesizable Counter

A practical example combining modules, ports, sequential logic, non-blocking assignments, parameters, and reset strategy.

Production-Quality Counter Design module counter #( parameter WIDTH = 8, // Counter width (bits) parameter INIT_VAL = 0 // Initial count value ) ( input wire clk, input wire reset_n, input wire enable, input wire load, input wire [WIDTH-1:0] load_val, output reg [WIDTH-1:0] count, output wire overflow // Asserted when count rolls over ); // Overflow when count reaches maximum and will increment assign overflow = (count == {WIDTH{1'b1}}) && enable; always @(posedge clk or negedge reset_n) begin if (!reset_n) count <= INIT_VAL; // Asynchronous reset else if (load) count <= load_val; // Load operation else if (enable) count <= count + 1; // Count increment // else: hold current count (no change) end endmodule

Design analysis:

Module Instantiation Example

// Instantiate a 16-bit counter starting at 0 counter #( .WIDTH(16), .INIT_VAL(0) ) u_counter ( .clk(sys_clk), .reset_n(sys_reset_n), .enable(cnt_enable), .load(cnt_load), .load_val(load_data), .count(counter_out), .overflow(cnt_overflow) );

Mastery & Advanced Topics

Now that you understand Verilog fundamentals, you're ready to tackle advanced RTL design patterns: finite state machines, pipelining, clock domain crossing, and synthesis-specific optimization techniques.

Recommended Learning Path

1
Finite State Machines (FSM)

Design Mealy and Moore FSMs with proper state encoding, reset strategy, and timing optimization. FSMs are the foundation of most digital controllers.

2
Clock Domain Crossing (CDC)

Master safe signal transfer between asynchronous clock domains using synchronizers, handshake protocols, and Gray code. Critical for multi-clock designs.

3
Pipelining & Datapath Design

Build high-throughput, high-frequency designs by strategic pipeline insertion. Learn hazard detection, forwarding, and retiming techniques.

4
SystemVerilog for RTL

Extend your Verilog skills with SystemVerilog interfaces, assertions, and advanced parameterization for complex designs.

Key Takeaways

Core Principles

  • Module hierarchy: Build larger designs from smaller, reusable blocks
  • Data types matter: Wire for combinational, reg for sequential; synthesis depends on correct usage
  • Always blocks: Use @(*) for combinational, @(posedge clk) for sequential
  • Blocking vs non-blocking: The single most important distinction in RTL design
  • Synthesis-aware: Write code with timing closure, area, and verifiability in mind
  • Parameterization: Make designs reusable and scalable with parameters and generate blocks