Verilog Series · Module 04

Module in Verilog — VLSI Trainers
Verilog Series · Module 04

MODULE in Verilog

The fundamental building block of every Verilog design — understand what a module is, how ports work, and how to instantiate and compose modules into larger systems.

📦 What is a Module?

Every Verilog program begins with the keyword module. A module is the fundamental building block of any Verilog design — it is a named unit of logic that encapsulates its internal implementation and exposes only its interface (its ports) to the outside world.

Think of it like an IC chip: you can see the pins (ports), but the internal circuitry is hidden. You use the chip by connecting to its pins — you don’t need to know how it works internally.

🔌
Has Named Ports
Every module exposes a set of named input/output ports — the only way the outside world interacts with it.
🔒
Hides Internals
Internal signals, registers, and sub-modules are invisible to the parent. Only ports are accessible.
♻️
Reusable
Define a module once, instantiate it as many times as needed — each instance is an independent copy in hardware.
🏗
Hierarchical
Modules can instantiate other modules — enabling hierarchical, top-down or bottom-up design of complex systems.

🔌 Port Types

The ports attached to a module can be of three types:

➡️
input
Signals enter the module through input ports. They are driven from outside and read inside. Cannot be assigned inside the module.
input clk, reset
⬅️
output
Signals exit the module through output ports. They are driven inside the module and read outside. Must be driven by internal logic.
output [7:0] data
↔️
inout
Bidirectional ports — can carry signals both in and out at different times. Used for shared buses (e.g., I2C SDA, memory data bus).
inout [7:0] bus
Default data type: Ports are wire by default. An output port that needs to hold a value across clock edges should be declared as output reg.

Module as a Black Box

A module is always treated as a black box by any module that instantiates it. Only the port list matters from the outside — the internal implementation is completely hidden.

Fig 1 — IC 7430 (8-input NAND gate) as a Verilog module
in[0] in[1] in[2] in[3] in[4] in[5] in[6] in[7] module nand_gate 8-input NAND (IC 7430) ───────────────── assign y = ~(&in); y (output) INPUTS OUTPUT

📝 Module Syntax

Every module follows a consistent structure — header, port list, internal declarations, and body — closed by endmodule.

Fig 2 — Module Template (Annotated)
// ┌─ keyword that starts every module ─────────────────────────┐
module module_name        // user-defined name (identifier)
  #( /* parameters */ )    // optional — for configurable modules
  (
    // Port list
    input              clk,      // 1-bit input
    input              reset,
    input  [7:0]       data_in, // 8-bit input bus
    output [7:0]       data_out,// 8-bit output bus
    output             valid,
    inout  [7:0]       bus      // bidirectional
  );

  // Internal signal declarations
  wire  [7:0] internal_net;
  reg   [7:0] state;

  // Module body — logic description at any level
  always @(posedge clk) begin
    // sequential logic
  end

  assign data_out = internal_net;

endmodule  // ← always required, no semicolon
No semicolon after endmodule. This is one of the most common beginner mistakes — endmodule does not take a semicolon, unlike most Verilog statements.

🔧 Port Declaration Styles

Verilog supports two styles for declaring ports. Both are valid — Verilog-2001 style is preferred in modern code:

Verilog-1995 Style (older)

// Port names listed in header,
// types declared separately inside
module adder (a, b, cin, sum, cout);

  input  [3:0] a, b;
  input        cin;
  output [3:0] sum;
  output       cout;

  assign {cout,sum} = a+b+cin;
endmodule

Verilog-2001 Style (modern ✅)

// Port type declared inline in header
// — more concise, less error-prone
module adder (
  input  [3:0] a,
  input  [3:0] b,
  input        cin,
  output [3:0] sum,
  output       cout
);
  assign {cout,sum} = a+b+cin;
endmodule

🔁 Module Instantiation

Instantiation means using a module inside another module. Each use creates an independent hardware copy. Two connection styles exist:

⚠️ Positional Connection

// Ports connected by position —
// order must match module definition
adder u1 (a_sig, b_sig, c_in,
           sum_out, c_out);
⚠️ Fragile — any reorder in the module definition breaks it silently.

✅ Named Port Connection

// Ports connected by name —
// order doesn't matter
adder u1 (
  .a   (a_sig),
  .b   (b_sig),
  .cin (c_in),
  .sum (sum_out),
  .cout(c_out)
);
✅ Preferred — self-documenting, safe against reorders.

Multiple Instances of the Same Module

// Same module, three different instances — each is independent hardware
adder u1 (.a(a0), .b(b0), .cin(1'b0), .sum(s0), .cout(c0));
adder u2 (.a(a1), .b(b1), .cin(c0),   .sum(s1), .cout(c1));
adder u3 (.a(a2), .b(b2), .cin(c1),   .sum(s2), .cout(c2));
// Above: three 4-bit adders chained into a 12-bit ripple-carry adder

Unconnected Ports

// Leave a port unconnected using empty connection
adder u4 (.a(x), .b(y), .cin(1'b0),
           .sum(result), .cout(/*unconnected*/));
Unconnected outputs are legal — the signal is simply floating. Unconnected inputs default to z (high-impedance), which often behaves as x inside gates. Always be intentional about unconnected ports.

🌳 Module Hierarchy

Complex designs are built by composing modules — a top-level module instantiates mid-level modules, which in turn instantiate leaf modules. This forms a tree called the design hierarchy.

Fig 3 — Example SoC Module Hierarchy
soc_top  (top-level)
├── cpu_core  u_cpu
├── alu  u_alu
├── register_file  u_rf
└── ctrl_unit  u_ctrl
├── mem_ctrl  u_mem
├── sram_16k  u_sram
└── cache  u_cache
└── uart  u_uart
├── uart_tx  u_tx
└── uart_rx  u_rx
Hierarchical path example:  soc_top.u_cpu.u_alu.carry_out
Design tip: Keep hierarchy shallow (3–5 levels) for large designs. Flat netlists are hard to debug; too many levels make timing closure difficult. Each level should represent a meaningful design partition.

⚙️ Parameterized Modules

Parameters make modules configurable and reusable. You define default values in the module and override them at instantiation time — without changing the module source.

Fig 4 — Parameterized N-bit Adder
// Module with parameters — configurable bit width
module adder_n #(
  parameter N = 8           // default: 8-bit
) (
  input  [N-1:0] a, b,
  input          cin,
  output [N-1:0] sum,
  output         cout
);
  assign {cout, sum} = a + b + cin;
endmodule

// Instantiate with different widths
adder_n            u8  (...); // uses default N=8
adder_n #(.N(16))  u16 (...); // 16-bit adder
adder_n #(.N(32))  u32 (...); // 32-bit adder
adder_n #(.N(64))  u64 (...); // 64-bit adder

Multiple Parameters

module fifo #(
  parameter DATA_WIDTH = 8,
  parameter DEPTH      = 16,
  parameter ADDR_WIDTH = $clog2(DEPTH)  // derived parameter
) (
  input                    clk, rst,
  input  [DATA_WIDTH-1:0] wdata,
  output [DATA_WIDTH-1:0] rdata,
  input                    wr_en, rd_en,
  output                   full, empty
);
  // ... FIFO logic using DATA_WIDTH and DEPTH ...
endmodule

// Override at instantiation
fifo #(.DATA_WIDTH(32), .DEPTH(64)) u_fifo (...);
defparam (legacy): Parameters can also be overridden using defparam instance.PARAM = value; — but this is discouraged in modern code. Use the #() syntax instead.

📋 Module Rules

Rule Detail Allowed?
Defined once Each module is defined exactly once in the design. Redefining with the same name causes a compile error. One definition only
Nested definition A module cannot be defined inside another module. All module definitions are at the top level. Not allowed
Multiple instances A module can be instantiated any number of times inside other modules, each with a unique instance name. Allowed ✓
Recursive instantiation A module cannot instantiate itself (directly or indirectly). Verilog does not support recursion. Not allowed
No semicolon on endmodule endmodule does not take a semicolon at the end. Common beginner mistake. No semicolon
Port direction defaults All ports are wire type by default. Declare output reg explicitly if the output is driven from an always block. wire default
Instance name required Every instantiation must have a unique instance name (e.g., u1, u_adder). Unnamed instances are not allowed. Name required

💡 Complete Examples

Fig 5 — D Flip-Flop Module

Sequential module — output port declared as reg
module dff (
  input      clk,
  input      rst_n,    // active-low reset
  input      d,
  output reg q         // reg because driven from always block
);
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
      q <= 1'b0;
    else
      q <= d;
  end
endmodule

// Instantiate 8 flip-flops to make an 8-bit register
module reg8 (
  input        clk, rst_n,
  input  [7:0] d,
  output [7:0] q
);
  genvar i;
  generate
    for (i=0; i<8; i++) begin : bit
      dff u (.clk(clk), .rst_n(rst_n), .d(d[i]), .q(q[i]));
    end
  endgenerate
endmodule

Fig 6 — 2-to-1 Multiplexer with Inout Bus

Demonstrates input, output, and inout port types
// 2-to-1 mux + bidirectional data bus
module mux_bus #(
  parameter W = 8
) (
  input           sel,        // select line
  input  [W-1:0]  a, b,       // data inputs
  output [W-1:0]  y,          // mux output
  input           oe,         // output enable for bus
  inout  [W-1:0]  data_bus    // bidirectional bus
);
  // Mux logic
  assign y = sel ? a : b;

  // Tri-state driver: drive bus when oe=1, else float (z)
  assign data_bus = oe ? y : {W{1'bz}};

endmodule

Fig 7 — Top-Level Module Composing Sub-modules

Structural composition — modules wired together
module top (
  input        clk, rst_n,
  input  [7:0] data_a, data_b,
  output [7:0] result,
  output       carry
);
  // Internal wires connecting sub-modules
  wire [7:0] sum_raw;
  wire       cout_raw;

  // Sub-module 1: 8-bit adder
  adder_n #(.N(8)) u_add (
    .a   (data_a),
    .b   (data_b),
    .cin (1'b0),
    .sum (sum_raw),
    .cout(cout_raw)
  );

  // Sub-module 2: 8-bit register to pipeline the result
  reg8 u_reg (
    .clk  (clk),
    .rst_n(rst_n),
    .d    (sum_raw),
    .q    (result)
  );

  // Register the carry too
  dff u_carry (
    .clk  (clk),
    .rst_n(rst_n),
    .d    (cout_raw),
    .q    (carry)
  );

endmodule
Key takeaway: The top module above has no logic of its own — it only wires sub-modules together. This is called structural modeling and is the standard approach for top-level integration in real VLSI projects. Each sub-module can be independently verified before integration.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top