Verilog Modules

Modules are the fundamental building blocks of Verilog designs. Every piece of hardware, from a simple gate to a complete processor, is described as a module.

Module Structure

A Verilog module has three main parts:

  1. Module declaration – Name and port list
  2. Port definitions – Input/output specifications
  3. Module body – The actual hardware description
Basic Module Structure
module module_name (
    // Port list
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  data_in,
    output reg  [7:0]  data_out
);
    // Internal signals
    wire [7:0] internal_wire;
    reg  [7:0] internal_reg;
    // Module body: logic, instantiations, etc.
    assign internal_wire = data_in;
    always @(posedge clk) begin
        data_out <= internal_wire;
    end
endmodule

Module Instantiation

To use a module inside another, you instantiate it. This is like placing a component on a circuit board:

Module Instantiation
// Define a simple adder module
module adder (
    input  wire [7:0] a,
    input  wire [7:0] b,
    output wire [8:0] sum
);
    assign sum = a + b;
endmodule
// Use it in a top-level module
module top (
    input  wire [7:0] x,
    input  wire [7:0] y,
    output wire [8:0] result
);
    // Named port connection (recommended)
    adder u_adder (
        .a   (x),
        .b   (y),
        .sum (result)
    );
endmodule

Naming Convention

Instance names typically start with u_ or i_ (e.g., u_adder, i_fifo). This makes it easy to identify instances in simulation waveforms.

Port Connection Styles

There are two ways to connect ports when instantiating a module:

Port Connection Methods
module full_adder (
    input  wire a, b, cin,
    output wire sum, cout
);
    assign sum  = a ^ b ^ cin;
    assign cout = (a & b) | (cin & (a ^ b));
endmodule
module top;
    wire x, y, c_in, s, c_out;
    // Method 1: Named port connection (RECOMMENDED)
    full_adder u_fa1 (
        .a    (x),
        .b    (y),
        .cin  (c_in),
        .sum  (s),
        .cout (c_out)
    );
    // Method 2: Positional connection (NOT recommended)
    // Order must match the module definition exactly
    full_adder u_fa2 (x, y, c_in, s, c_out);
endmodule

Always use named port connections – they are self-documenting and prevent errors when port order changes.

Parameterized Modules

Parameters make modules reusable with different configurations:

Parameterized Module
// Generic counter with configurable width
module counter #(
    parameter WIDTH = 8,
    parameter MAX_VAL = (1 << WIDTH) - 1
) (
    input  wire             clk,
    input  wire             rst_n,
    input  wire             enable,
    output reg  [WIDTH-1:0] count
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 0;
        else if (enable) begin
            if (count == MAX_VAL)
                count <= 0;
            else
                count <= count + 1;
        end
    end
endmodule
// Instantiation with parameter override
module top;
    wire clk, rst_n, en;
    wire [15:0] wide_count;
    wire [3:0]  narrow_count;
    // 16-bit counter
    counter #(
        .WIDTH   (16),
        .MAX_VAL (1000)
    ) u_wide_counter (
        .clk    (clk),
        .rst_n  (rst_n),
        .enable (en),
        .count  (wide_count)
    );
    // 4-bit counter (default MAX_VAL)
    counter #(.WIDTH(4)) u_narrow_counter (
        .clk    (clk),
        .rst_n  (rst_n),
        .enable (en),
        .count  (narrow_count)
    );
endmodule

Hierarchical Design

Complex designs are built by nesting modules in a hierarchy:

Design Hierarchy Example
// Level 3: Basic gates
module and_gate (input a, b, output y);
    assign y = a & b;
endmodule
// Level 2: Half adder using gates
module half_adder (
    input  wire a, b,
    output wire sum, carry
);
    xor u_xor (sum, a, b);      // Built-in primitive
    and_gate u_and (.a(a), .b(b), .y(carry));
endmodule
// Level 1: Full adder using half adders
module full_adder (
    input  wire a, b, cin,
    output wire sum, cout
);
    wire s1, c1, c2;
    half_adder u_ha1 (.a(a), .b(b), .sum(s1), .carry(c1));
    half_adder u_ha2 (.a(s1), .b(cin), .sum(sum), .carry(c2));
    or u_or (cout, c1, c2);
endmodule
// Top: 4-bit ripple carry adder
module adder_4bit (
    input  wire [3:0] a, b,
    input  wire       cin,
    output wire [3:0] sum,
    output wire       cout
);
    wire [3:0] c;  // Internal carries
    full_adder fa0 (.a(a[0]), .b(b[0]), .cin(cin),  .sum(sum[0]), .cout(c[0]));
    full_adder fa1 (.a(a[1]), .b(b[1]), .cin(c[0]), .sum(sum[1]), .cout(c[1]));
    full_adder fa2 (.a(a[2]), .b(b[2]), .cin(c[1]), .sum(sum[2]), .cout(c[2]));
    full_adder fa3 (.a(a[3]), .b(b[3]), .cin(c[2]), .sum(sum[3]), .cout(cout));
endmodule

Common Interview Questions

  1. What is the difference between module definition and instantiation?

    Definition describes the module's behavior; instantiation creates an instance (copy) of it in another module.

  2. Can a module instantiate itself?

    No, Verilog doesn't support recursive instantiation. Use generate blocks for iterative structures.

  3. What happens if you leave a port unconnected?

    Inputs default to high-impedance (Z); outputs are left floating. Use .port() or .port(floating) explicitly.