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.

Modules: The LEGO Bricks of Silicon

If you've ever played with LEGOs, you already understand how Verilog works. A Module is like a single LEGO brick.

  • You design the brick once (the module definition).
  • You can then use that same brick hundreds of times to build a giant castle (the instantiation).

In the professional world, we don't write one giant 1-million-line file for a chip. We write small, manageable modules (like an Adder, a Multiplier, or a Memory block) and then "plug" them together.

The "Black Box" Concept

The beauty of a module is that it is a Black Box. If you are using a "Timer" module designed by a coworker, you don't need to know how the gears turn inside.

You only need to know what the Ports (the plugs) are. As long as you connect the right wires to the right plugs, the module will work perfectly. This is how massive teams build complex CPUs—everyone focuses on their own "box."

Ports: Oneway vs. Twoway Streets

Signals move through Ports like cars on a street. There are three types of "streets" in Verilog:

  • INPUT: A one-way street coming into your module. Your module can "listen" to this wire but cannot change it.
  • OUTPUT: A one-way street going out of your module. Your module "drives" this wire to tell the rest of the chip what happened.
  • INOUT: A two-way street. This is like a walkie-talkie. Only one person can talk at a time, or the signals will "crash" (creating an X value). These are usually used for physical pins on the outside of the chip.

Module Structure

A Verilog module has three main parts:

  1. Module declaration – Giving the brick a name.
  2. Port definitions – Identifying the plugs (Input/Output).
  3. Module body – Drawing the circuit inside the box.
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.