Verilog serves as the primary hardware description language for FPGA development. This guide covers the core language constructs you'll encounter in everyday coding work.
Data Types
Verilog defines two fundamental variable types: wire and reg.
A wire represents a physical connection—it simply passes signals without storing them. A reg type holds state information, functioning like a register in actual hardware.
For instance, when declaring output reg data_out;, the variable can retain its value across clock cycles. Conversely, wire types update continuously based on their connected sources.
Blocking vs Non-blocking Assignmant
Verilog provides two assignment operators with distinct behaviors:
Blocking assignment (=) executes immediately. When the statement completes, the target variable holds the new value:
q = d; // Evaluated and assigned right awayNon-blocking assignment (<=) schedules the update. The target variable receives its new value only at the end of the current time step, typically on a clock edge:
q <= d; // Scheduled, takes effect at clock edgeCombinational logic requires blocking assignments since outputs must react instantly to input changes. Sequential logic, driven by clock edges, requires non-blocking assignments to prevent race conditions.
Bit Width Declarations
Variables default to 1-bit width when unspecified. Always explicitly declare widths for clarity:
wire [15:0] data_bus; // 16-bit bus
reg [31:0] counter; // 32-bit registerFor arrays or memories, include the depth dimension:
reg [7:0] memory [0:255]; // 256 locations, each 8 bits wideWhen assigning values, match the declared width. Using a specific width notation ensures proper bit sizing:
assign data_bus = 16'hFF00; // Correct: matches declared width
assign data_bus = 65535; // Works: system truncates 32-bit value to 16 bitsModule interconnection often involves single-bit signals that can be used implicitly without explicit declaration. While legal, this practice reduces code readability and should be avoided in production designs.
Parameters provide symbolic constants, improving maintainability. By convnetion, use uppercase for parameter names:
parameter MAX_COUNT = 100;Procedural vs Continuous Assignment
Verilog offers two assignment mechanisms with different synthesis implications.
Continuous Assignment (assign)
Applied exclusively to wire types, assign creates combinational logic:
assign result = a & b;The statement executes whenever any operand changes. Only one assignment can target each wire.
Procedural Assignment (always)
The always block handles both combinational and sequential logic through sensitivity lists:
Level-triggered (combinational):
always @(*) begin y = a & b;endThe @(*) wildcard automatically includes all referenced signals. Equivalent forms include always @(a, b) and always @(a or b).
Edge-triggered (sequential):
always @(posedge clk) begin q <= d;endOnly one always block should drive any particular reg variable to avoid synthesis conflicts.
A practical example demonstrating both assignment styles:
module assignment_demo( input clk, input a, b, output wire c, // continuous assignment output reg c1, // combinational procedural output reg c2 // sequential procedural);assign c = a & b; // combinational outputalways @(*) begin c1 = a & b; // same function, procedural styleendalways @(posedge clk) begin c2 <= a & b; // sequential outputendendmodule### Initial Blocks
The initial statement initializes simulation values. Since synthesis tools often ignore it, reserve initial for testbenches:
initial begin clk = 0; data_in = 0; #100; data_in = 8'hAA;endOnly reg variables can receive assignments in initial blocks.
For sequential logic in actual hardware, initialization occurs through reset signals rather than initial statements. When initializing registers during synthesis, blocking assignments work correctly.
Operators
Arithmetic Operators
+ - * / %Avoid heavy operators like multiplication, division, and modulus in synthesizable code unless absolutely necessary. These consume significant hardware resources.
Relational Operators
> < >= <= == !=These return boolean values (1 or 0). The <= operator serves dual purposes—comparison in expressions and non-blocking assignment in procedural blocks.
Logical Operators
&& || !These operate on boolean values and return true or false. Distinguish from bitwise operators which operate per-bit.
Bitwise Operators
& | ~ ^ ~^These perform operations on corresponding bits of multi-bit operands.
Conditional Operator
condition ? true_value : false_valueFunctions like an if-then-else expression in a single line.
Shift Operators
<< >>Logical shifts pad with zeros. Note that repeated left shifts can overflow—4'b0001 << 4 yeilds 4'b0000.
Concatenation Operator
{bits1, bits2, ...}Combines multiple signals into a wider vector. This operator enables efficient rotate operations:
module rotate_example( input clk, input rst, output reg [7:0] rotated = 8'b00000001);always @(posedge clk) begin if (rst) rotated <= 8'b00000001; else rotated <= {rotated[6:0], rotated[7]}; // circular left shiftendendmoduleCompare this to the alternative approach that requires explicit boundary checking:
always @(posedge clk) begin if (rst) shifted <= 8'b00000001; else if (shifted == 8'b10000000) shifted <= 8'b00000001; else shifted <= shifted << 1; // needs extra comparisonendConcatenation achieves rotation in a single assignment.
Conditional Statements
Both if-else and case constructs belong inside always blocks.
if-else Guidelines
Several practical rules improve if-else usage:
Limit nesting depth. More than 8 levels causes excessive combinational path delay, potentially causing timing violations. Timing violations occur when signals don't stabilize within the clock period—typically from overly complex combinational logic.
Consider priority. if-else chains implement priority encoders, where earlier conditions take precedence. For parallel evaluation, use case.
Complete with else. Unmatched conditions create unintended latches. Always provide default paths:
always @(*) begin if (enable) data_out = data_in; else data_out = 8'b0; // default assignment prevents latchesendGrouping multiple statements requires begin-end blocks:
always @(posedge clk) begin if (rst) counter <= 0; else counter <= counter + 1;endSingle-statement branches can omit the block delimiters, though including them improves consistency.
case Statement
case implements parallel multiplexers, offering faster evaluation than priority-encoded if-else chains when many mutually exclusive conditions exist:
always @(posedge clk) begin case (state) 2'b00: data_out <= 8'h01; 2'b01: data_out <= 8'h02; 2'b10: data_out <= 8'h04; default: data_out <= 8'h00; // required when coverage is incomplete endcaseend``default clauses become mandatory when the case doesn't cover all possible values. Always include them for synthesis compatibility.
Multi-statement case branches can use begin-end:
case (op_code) 3'd0: begin result <= a + b; flag <= 1; end 3'd1: result <= a - b; default: result <= 0;endcaseLiteral Separators
Underscores improve readability of long numbers but require explicit width notation:
parameter PERIOD = 26'd49_999_999; // valid
parameter PERIOD = 49_999_999; // invalid: no width specifiedWithout declared width, the system interprets numbers as 32-bit decimal values, where separators aren't permitted.
Testbench Patterns
Common testbench constructions include:
Random stimulus generation:
data = {$random} % 256; // generates 0-255Reset sequence:
initial begin clk = 0; rst_n = 0; #100; rst_n = 1;endClock generation:
always #20 clk = ~clk; // 25MHz clock from 50% duty cycleSimulation delays:
#200; // wait 200 time unitsThese primitives form the foundation for creating comprehensive verification environments.