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.
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).
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.
The quality of your RTL directly impacts:
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.
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 declarationinput/output — Port directionwire — Data type (combinational)assign — Continuous assignmentendmodule — Module terminationANSI 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.
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.
Understanding when to use wire vs reg is fundamental to writing correct RTL. These data types carry semantic meaning about how logic is synthesized.
A wire represents a combinational connection between logic gates — it has no memory and must be continuously driven.
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:
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.
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:
assign (combinational); no memory; always drivenalways blocks (procedural); may hold values across clock cycleswire synthesizes to combinational gates; reg synthesizes to flip-flops or latchesreg for combinational output or wire in always blocks causes synthesis errors or incorrect behavior
The always block is where sequential and combinational logic is described procedurally in Verilog. The sensitivity list (event trigger) controls when the block executes.
For combinational logic, use always @(*) to include all input sensitivity automatically:
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 logicreg (procedural variable), not a flip-flopdefault case prevents latches (incomplete assignment)For sequential logic, trigger on clock edge. Use posedge clk (rising edge) or negedge clk (falling edge):
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:
<= ) — critical for correct synthesis
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 assignments execute sequentially — the right-hand side is evaluated and assigned immediately, blocking further statements until complete. Use only in combinational always blocks:
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 conditionsalways @(posedge clk)
q = d; // Blocking in sequential block — WRONG!
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:
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:
| 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 |
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.
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 allow you to instantiate multiple instances or conditionally include logic. Common in datapath design:
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:
genvar i) are elaboration-time onlyNot all valid Verilog synthesizes equally. Writing synthesis-friendly code directly impacts timing closure, area efficiency, and power consumption.
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:
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:
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
Synthesis tools must meet timing constraints. Very deep combinational logic (many gates in series) limits maximum clock frequency. Solution: insert pipeline stages:
// 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.
Every sequential element should have explicit reset logic (usually asynchronous, active-low):
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:
A practical example combining modules, ports, sequential logic, non-blocking assignments, parameters, and reset strategy.
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:
count is sequential (flip-flops), overflow is combinational (wire)// 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)
);
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.
Design Mealy and Moore FSMs with proper state encoding, reset strategy, and timing optimization. FSMs are the foundation of most digital controllers.
Master safe signal transfer between asynchronous clock domains using synchronizers, handshake protocols, and Gray code. Critical for multi-clock designs.
Build high-throughput, high-frequency designs by strategic pipeline insertion. Learn hazard detection, forwarding, and retiming techniques.
Extend your Verilog skills with SystemVerilog interfaces, assertions, and advanced parameterization for complex designs.