Post

[Verilog] Verilog and FPGA: Basic Syntax for Verilog

[Verilog] Verilog and FPGA: Basic Syntax for Verilog

This post is based on lecture notes: Digital System Design: Verilog and FPGA by Hyokeun Lee Ph.D.

1. Module

A module is the fundamental unit of design in Verilog. It is common (but not strictly required) to keep one module per source file (e.g., source_code.v). A module typically encapsulates two main types of items:

  • Declarations: These define the module’s interface (ports) and internal elements (nets, variables, functions, tasks).
  • Statements: These describe the module’s behavior through operations like continuous assignments, procedural blocks, and/or instantiated submodules.

2. Tokens

In Verilog, statement is composed of tokens, which are the smallest lexically meaningful units. These include keywords, identifiers, operators, and literals.

  • Keywords (Reserved Words)
    • These are predefined names in the language (e.g., module, input, output, assign, reg, wire, etc.).
    • They cannot be used as user-defined identifiers.
  • Identifiers
    • These are user-defined names for modules, signals, variables, and other elements.
    • Verilog identifiers are case-sensitive.
    • Rules:
      • Must begin with a letter or an underscore (_).
      • Subsequent characters may include letters, digits, underscores, and dollar signs ($).
      • Examples: my_var, _temp, data$reg
1
2
3
4
5
6
7
8
9
10
// Simple AND gate example module
module example(result, in1, in2);
  input  in1, in2;
  output result;

  // For single-bit inputs, '+' performs 1-bit addition and discards the carry.
  // Using '&' (bitwise AND) is often a clearer first example:
  // assign result = in1 & in2;
  assign result = in1 + in2;
endmodule
  • Letter Case Conventions
    • Lower case: Typically used for signals, variables, and port names.
    • Upper case: Commonly used for constants and parameters (and user-defined types in SystemVerilog).
  • Naming Conventions (Project-Dependent)
    • _r or _q: Denotes a registered (flip-flop) output.
    • _d or _next: Indicates a next-state (combinational) value.
    • _n or _L: Signifies an active-low signal.
    • _i, _o, _io: Used to indicate input, output, and inout port directions, respectively.

3. Numbers

Verilog employs four-valued logic, which is crucial for hardware description:

ValueMeaning
0Logic 0 (false)
1Logic 1 (true)
zHigh-impedance (tri-state).
- In numeric literals, ? is treated as z (e.g., 4'b01??4'b01zz).
- Commonly found on tri-stated buses and inout ports.
xUnknown.
- In simulation, x values propagate. In synthesis, they may be treated as “don’t care” depending on context.
- Also arises from uninitialized values or conflicting multiple drivers.

Bitwise Operators

OperatorOperation
&AND
|OR
^XOR
^~, ~^XNOR
~NOT
  • Literal Formats
    • Sized: <size>'<base><digits>
      • e.g., 4'b1001 (4-bit binary 1001), 16'habcd (16-bit hexadecimal abcd)
    • Unsized:
      • Decimal numbers like 2009 are treated as 32-bit signed integers by default.
      • Base-specified numbers like 'habc are treated as 32-bit unsigned integers by default.
  • Base Specifiers:
    • b or B: Binary
    • o or O: Octal
    • h or H: Hexadecimal
    • d or D: Decimal
  • Readability
    • Use underscores (_) as digit separators to improve readability; they are ignored by the parser:
      • 16'b0101_1001_1110_0000

4. Data Types: Nets

  • Nets (e.g., wire) model physical connections between hardware blocks.
  • They do not store a value; instead, their value is the resolved result of their drivers (e.g., assign statements, module outputs).
  • The default net type for implicitly declared nets is wire.

    Best Practice: Avoid implicit net declarations. It is highly recommended to explicitly declare all nets. You can enforce this by adding `default_nettype none at the top of your Verilog files.

  • Other net types exist for tri-state or special wiring (tri, tri0, tri1, wand, wor, supply0/1, etc.); some are not synthesizable in typical FPGA/ASIC flows (e.g., internal tri-states).

  • Nets are driven by continuous assignments using the assign keyword, or by connecting them to module/primitive outputs.
1
2
3
4
5
module example;
  wire a;
  wire b;
  assign b = ~a; // Continuous assignment to net 'b'
endmodule

Tip: Internal tri-state buses are generally not synthesizable on FPGAs or are highly inefficient. Tri-state logic is typically restricted to top-level I/O pins. Inside the FPGA fabric, it’s better to implement tri-state behavior using multiplexers (muxes).

5. Data Types: Variables

  • Variables (e.g., reg) represent storage elements; they retain their last assigned value until updated.

  • Commonly used variable types in Verilog:
    • reg: A scalar or vector variable used in procedural blocks (always, initial). Crucially, the reg keyword does NOT inherently imply a flip-flop or a physical register. It simply means a variable that can hold a value.
    • integer: A 32-bit signed variable (often used for loop counters or indices; tool support for synthesis varies but is widely available).
    • Simulation-only types: real, realtime, time (these are non-synthesizable).
  • Variables are assigned values within procedural blocks (always/initial) using procedural assignments.
1
2
3
4
5
6
7
8
9
module example();
  wire a;
  reg  b; // 'b' is a variable that can store a value

  // This procedural block executes whenever 'a' changes
  always @* begin
    b = ~a; // Procedural assignment to variable 'b'
  end
endmodule

Important: Whether a reg variable synthesizes into a flip-flop, a latch, or just combinational logic is determined by your procedural code style (e.g., sensitivity list, conditional coverage, and the use of blocking = vs. non-blocking <= assignments), not by the reg keyword itself. This is a common source of confusion for beginners.

6. Port Types

  • Verilog modules have three types of ports: input, output, and inout.
    • input: Signals consumed by the module.
      • Input ports are always nets (e.g., wire) and are driven from outside the module. You cannot assign a value to an input port from within the module.
    • output: Signals produced by the module.
      • Output ports may be nets (output wire) or variables (output reg).
      • Output ports can also be read inside the module.
    • inout: Bidirectional signals (typically used for tri-state I/O).
      • inout ports must be of a net type (e.g., wire).
1
2
3
4
5
6
module example (G1, G2, G3, A, Y);
  input       G1, G2, G3;     // Equivalent to: input wire G1, G2, G3;
  input wire  [2:0] A;        // Explicitly declared as wire
  output reg  [7:0] Y;        // Output declared as reg
  // ...
endmodule

Best Practice: In older non-ANSI Verilog styles, a port listed without an explicit declaration would implicitly become a wire. To avoid ambiguity and potential errors, always declare the type of all ports explicitly. Using `default_nettype none is highly recommended to prevent implicit net declarations.

7. Parameters

  • Purpose: Parameters represent constants (e.g., bit widths, array sizes, specific values) that allow the same module to be reused with different configurations without modifying its source code.
  • Syntax: parameter <identifier> = <value>; (Conventionally, use UPPERCASE names for parameters).

Example (Parameterized Module Definition):

1
2
3
4
5
6
7
8
9
module example #(
  parameter WIDTH_IN  = 4,
  parameter WIDTH_OUT = 10
) (
  input  [WIDTH_IN-1:0]  in,
  output [WIDTH_OUT-1:0] out
);
// ...
endmodule

Override on Instantiation (Named Association):

1
2
3
4
5
6
7
8
9
10
11
12
module top;
  input  [4:0]  in_top;
  output [17:0] out_top;

  example #(
    .WIDTH_IN (5),
    .WIDTH_OUT(18)
  ) example_uut (
    .in (in_top),
    .out(out_top)
  );
endmodule

8. Vectors

8.1. Usage & Declarations

  • Vectors represent multi-bit data and can be used for nets, variables, and constants.
  • Declaration Format: [MSB:LSB] specifies the bit range. The MSB (Most Significant Bit) does not necessarily have to be numerically greater than the LSB (Least Significant Bit), but [MSB:LSB] is the common convention.
    • wire [15:0] data_bus;
    • reg [7:0] address;
  • Example (16 bits, with digit separators for readability):
    1
    2
    3
    4
    
    wire [15:0] Zbus;
    assign Zbus = 16'b1000_0110_1100_1010;
    // Zbus[15] is the MSB (value 1)
    // Zbus[0]  is the LSB (value 0)
    

8.2.Operations

  • Selections (Indexing): vec[i] selects a single bit; vec[a:b] selects a contiguous range of bits (a slice).
  • Concatenation: {A, B, C, ...} combines multiple signals or parts of signals into a larger vector (e.g., {2'b00, 2'b11}4'b0011).
  • Replication: {N{DATA}} repeats a DATA value N times (e.g., {4{2'b10}}8'b10101010).
  • Direction Note: The direction of a part-select (slice) must match the declaration direction of the vector. For a vector declared as [MSB:LSB], a part-select must also be in the form [upper:lower] where upper >= lower. For example, if you have wire [15:0] data;, then data[7:0] is valid, but data[0:7] is not.

  • Reduction Operators: These operators reduce a vector to a single bit result. They are prefix forms: & (AND), | (OR), ^ (XOR), ^~ or ~^ (XNOR).
    • &DATA: Returns 1'b1 if all bits in DATA are 1.
    • |DATA: Returns 1'b1 if any bit in DATA is 1.
    • ^DATA: Returns 1'b1 if there is an odd number of 1s in DATA.

  • Arithmetic Operators: Various arithmetic operations are supported.
  • Basic operations (+, -, *) are generally synthesizable and often map efficiently to dedicated DSP (Digital Signal Processing) resources on FPGAs.
  • Higher-complexity operations (/, %, **) are often not synthesizable or require significant logic, making them impractical for hardware implementation without specialized libraries.

    OperatorOperationSynthesizable
    +AdditionMostly maps to DSP in FPGA
    -Subtraction
    *Multiplication
    /DivisionGenerally not synthesizable
    %Modulus
    **Exponentiation
    <<(Logical) shift left
    >>(Logical) shift right
    <<<Arithmetic shift leftOptionally synthesizable
    >>>Arithmetic shift right

  • Signed Arithmetic (Verilog-2001)
    • Both nets and variables can be declared with the signed keyword; negative values are represented using two’s complement.
    • Be cautious of implicit unsigned casting when mixing signed and unsigned operands. Use the built-in system functions $signed(...) and $unsigned(...) for explicit type casting when needed.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    module multiply(
      input  signed [2:0] in_1,
      input         [2:0] in_2,
      output signed [5:0] out
    );
      // To multiply a signed and an unsigned number, the unsigned operand must be cast to signed.
      // Here, we explicitly add a sign bit (0) and use $signed() to perform a signed multiplication.
      assign out = in_1 * $signed({1'b0, in_2}); // consider overflow
      // assign out = in_1 * in_2; /* incorrect, as in_2 is unsigned */
    endmodule
    

9. Arrays

  • Until Verilog-1995, only one-dimensional vectors/memories were supported. Multi-dimensional arrays became available from Verilog-2001, simplifying the description of memory-like structures (e.g., LUTs).
  • Declaration Formats:
    • reg [7:0] my_memory [0:255]; // An array of 256 elements, each 8-bit wide (canonical form)
    • reg my_memory [0:255][7:0]; // Also valid, but less common for hardware description
  • Physical Mapping: Small arrays often map to combinational logic (LUTs). Depending on EDA tools and coding style, larger arrays can be inferred as memory (e.g., block RAMs) in FPGAs.

Example

1
2
3
4
5
6
7
8
9
10
11
12
// Declare a memory with 256 elements, each 8-bit wide
reg [7:0] memory [0:255];

// Write example (within a procedural block)
always @(posedge clk) begin
  if (write_enable) begin
    memory[address] <= data_in; // Procedural assignment to an array element
  end
end

// Read example (continuous assignment)
assign data_out = memory[address]; // Continuous assignment from an array element

10. Logical Expressions

10.1. Truth Values

  • Logical expressions produce a single-bit truth value.
    • True: 1'b1
    • False: 1'b0
  • For multi-bit operands, any non-zero value is considered TRUE; only a value where all bits are zero is considered FALSE.
    • 4'b0000 → FALSE
    • 4'b0010 → TRUE

      Note: While this lecture’s convention treats x or z as false in logical expressions, in reality, x values can propagate through logical operations, resulting in an x output if an operand is x.

10.2. Logical Operators

  • C-style Logical Operators:
    • Logical Evaluation: && (logical AND), || (logical OR), ! (logical NOT)
    • Equality Operators: == (logical equality), != (logical inequality)
      • The result becomes x if either operand contains x or z.
    • Magnitude (Relational) Operators: > (greater than), >= (greater than or equal), < (less than), <= (less than or equal)
OperatorOperation
&&Logical AND
||Logical OR
!Logical NOT
==Logical Equality
!=Logical Inequality
>Greater Than
>=Greater Than or Equal
<Less Than
<=Less Than or Equal

Important Distinction: Do not confuse bitwise operators (&, |, ~) with logical operators (&&, ||, !). Bitwise operators perform operations on each bit of their operands, while logical operators treat their operands as Boolean values (true/false) and produce a single-bit result.

10.3. Conditional Operator

  • The conditional (ternary) operator selects one of two values based on a logical expression. It is often used to implement multiplexers (muxes).
  • Format: <logical-expr> ? <true-expr> : <false-expr>

    1
    2
    3
    4
    5
    6
    7
    
    // 2-to-1 Multiplexer
    assign out = sel ? in1 : in0;
    
    // Nested conditional operator for a 4-to-1 Multiplexer
    assign mux_out = (sel == 2'b00) ? d0 :
                     (sel == 2'b01) ? d1 :
                     (sel == 2'b10) ? d2 : d3;
    

  • Case-Equality Operators: ===, !==
    • These operators compare every bit, treating x and z as specific values to be matched, unlike == and !=.
    • They are not synthesizable but are highly useful in simulation for checking unknown or high-impedance conditions.
    1
    2
    
    // Example: Detect if a bus contains any unknown bits
    if (data === 8'bxxxx_xxxx) $display("All bits of 'data' are unknown.");
    

11. Functions and Tasks

11.1. Function

  • A function is a reusable block of code defined within a module.
  • Features:
    • Can be used in place of an expression (on the RHS of an assignment).
    • Implicitly returns a value via its name.
    • Cannot contain timing control statements (#, @). Therefore, functions execute as combinational logic.
    • Cannot call a task.
    • Generally synthesizable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module test;
  input  [7:0] A;
  input  [7:0] B;
  output [7:0] F1, F2;

  // Function definition
  function [7:0] sum;
    input [7:0] a, b;
    sum = a + b;   // Implicit return via function name
  endfunction

  assign F1 = sum(A, B);       // Function call
  assign F2 = sum(F1, 8'hEE);  // Another function call
endmodule

11.2. Task

  • A task is a more general subroutine than a function.
  • Features:
    • Can be used in place of a statement.
    • May include timing control statements (@, #, wait).
    • Can call functions and other tasks.
    • Can be defined as static (variables shared across invocations) or automatic (variables re-allocated per call).
    • Due to timing controls, tasks are mostly not synthesizable and are primarily recommended for simulation and testbenches.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Static task:                     |     Automatic task:
variable 'i' is shared across calls |     'i' is independent per call*/
module test;                        |     module test;
  initial display();                |       initial display();
  initial display();                |       initial display();
  initial display();                |       initial display();
                                    |
  task display();                   |       task automatic display();
    integer i = 0;                  |         integer i = 0;
    i = i + 1;                      |         i = i + 1;
    $display("i=%0d", i);           |         $display("i=%0d", i);
  endtask                           |       endtask
endmodule                           |     endmodule
------------------------------------|--------------------------------------
result:                             |     result:
i=1                                 |     i=1
i=2                                 |     i=1
i=3                                 |     i=1

initial block: Executes statements only once, concurrently with other initial and always blocks.


11.3. System Functions and Tasks

  • These are built-in functions/tasks whose names start with a dollar sign ($).
  • Useful ones include:
  • $display: Prints a formatted text to the console (with a newline).
  • $write: Prints a formatted string to a file or console (without a newline).
    • Related tasks: $fopen, $fclose for file handling.
  • $time: Returns the current simulation time value.
  • $stop: Suspends simulation.
  • $finish: Terminates simulation.
  • $monitor: Displays values of its parameters whenever any argument changes.

12. Compiler Directives

  • These are predefined commands that instruct the Verilog compiler to perform certain tasks. They begin with a backtick (`).
  • `include <filename>
    • The specified file’s content is read and processed immediately, as if its contents were part of the current file.
  • `define <identifier> <text/value>
    • Similar to a macro in C++, the compiler textually replaces each occurrence of the identifier with the defined text/value.
  • `timescale
    • Sets the simulation time unit and precision. (Explained in the Simulation section.)

13. Connecting Different Modules

13.1. Port Connection

  • Named Association (Recommended)
    • Connects ports by their names, similar to keyword arguments in Python. This method is robust against changes in port order and improves readability.
    1
    2
    3
    4
    5
    6
    7
    
    shift_reg shift_reg_inst (          // Module name and instance name
      .clk          (clk_50),        // Connects module port '.clk' to wire 'clk_50'
      .reset_n      (reset_n),       // The '.' denotes the module port
      .data_ena     (data_ena),      // The value in parentheses is the connecting signal/wire
      .serial_data  (serial_data),
      .parallel_data(shift_reg_out)  // Wires and pins do not have to match names
    );
    

  • Positional Association (Avoid in Real Designs)
    • Connects ports based on their order in the module definition. This method is prone to errors and hard to maintain, especially in large designs.

      1
      2
      
      // Not recommended in large designs due to maintenance issues
      shift_reg shift_reg_1(clk_50, reset_n, data_ena, serial_data, shift_reg_out);
      

  • Built-in gate primitives (e.g., and, or, xor) support positional connections only:

    1
    2
    
    xor xor1 (s, x, y); // Output 's', inputs 'x' and 'y'
    and and1 (c, x, y); // Output 'c', inputs 'x' and 'y'
    

13.2. Case Study

  • When instantiating modules:
    • Always use named association for clarity and maintainability.
    • Parameterize bit widths for flexible, modular connections.
    • Ensure bit-widths match between connected wires and ports to avoid truncation or sign-extension issues.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module top;
  input  i_data;
  output o_data;

  // Interconnect wires
  wire p2i_w, i2m_w, wpath_w, rpath_w;

  // Interface module (parameterized), connected by name
  interface #( /* ... parameters ... */ ) itf_0 (
    .m2s0 (i2m_w),
    .s2m0 (p2i_w),
    .m2s1 (o_data),
    .s2m1 (i_data)
  );

  // Processing unit module
  process_unit #( /* ... parameters ... */ ) pu_0 (
    .p2i  (p2i_w),
    .wmem (wpath_w),
    .rmem (rpath_w)
  );

  // Memory block module
  memory #( /* ... parameters ... */ ) mem_0 (
    .wport  (wpath_w),
    .rport  (rpath_w),
    .wport2 (i2m_w)
  );
endmodule

Checklist: Prefer named connections, keep port and wire widths consistent, and use parameters to avoid hard-coded bit sizes.

14. Simulation

14.1. Testbench (Stimulus)

  • A testbench is a Verilog module that provides stimuli (input vectors) to the design under test (DUT) and observes its outputs to verify correct functionality.
  • Testbenches are not synthesizable and can use simulation-specific constructs like timing controls (#), file I/O, and display functions.

14.2. Timings

  • `timescale <time unit>/<time precision>: Sets the simulation time unit and time precision.
    • Example: `timescale 1ns/10ps means the time unit is 1 nanosecond, and the precision is 10 picoseconds. All delays will be rounded to the nearest 10ps.
  • Delay Operator #: Inserts a time delay. This operator is ignored by synthesis tools.
    • #10;: Waits for 10 time units.
    • assign #5 out = in;: out will be assigned the value of in after a 5 time unit delay.
  • Example and Precision Effect (with `timescale 1ns/10ps):
    • Fractional delays finer than the time precision will be rounded.
      1
      2
      3
      4
      
      `timescale 1ns/10ps
      assign #5.5   foo = tiger;   // 5.5 ns
      assign #5.55  foo = tiger;   // 5.55 ns -> rounded to 5.55 ns (if precision allows)
      assign #5.555 foo = tiger;   // 5.555 ns -> rounded to 5.56 ns (if precision is 10ps)
      

14.3. Case Study: 4-bit Adder Simulation

  • (1) DUT: 4-bit Adder
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    /* Implementation of a 4-bit adder using four full_adder instances */
    module four_bit_adder (
      input  [3:0] x, y,
      input        c_in,
      output [3:0] sum,
      output       c_out
    );
      wire c1, c2, c3; // Internal carry signals
    
      // Instantiate four full_adder modules
      full_adder fa0 (x[0], y[0], c_in, sum[0], c1);
      full_adder fa1 (x[1], y[1], c1,   sum[1], c2);
      full_adder fa2 (x[2], y[2], c2,   sum[2], c3);
      full_adder fa3 (x[3], y[3], c3,   sum[3], c_out);
    endmodule
    
  • (2) Testbench
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    `timescale 1ns/100ps // Time unit: 1ns, Precision: 100ps
    
    module four_bit_adder_tb; // Testbench for the four_bit_adder DUT
      reg  [3:0] x, y;
      reg        c_in;
      wire [3:0] sum;
      wire       c_out;
    
      // Instantiate the Design Under Test (DUT)
      four_bit_adder UUT (.x(x), .y(y), .c_in(c_in), .sum(sum), .c_out(c_out));
    
      // Stimulus generation (input vectors)
      initial begin
        for (i = 0; i <= 8'd255; i = i + 1) begin
          #20;
          x = i[7:4];
          y = i[3:0];
        end
      end
    
      // stop after 6000 ns; print results whenever any argument changes
      initial #6000 $finish;
      initial $monitor($realtime, " ns  %h %h %b  %h", x, y, 1'b0, {c_out, sum});
    endmodule
    
  • Example Console Output
    1
    2
    3
    4
    5
    6
    7
    
    0ns     0 0 0  00
    20ns   0 1 0  01
    40ns   0 2 0  02
    ...
    320ns   1 0 0  01
    340ns   1 1 0  02
    ...
    
This post is licensed under CC BY 4.0 by the author.