A module is a block of Verilog code that implements a certain functionality. Modules can be embedded within other modules and a higher level module can communicate with its lower level modules using their input and output ports. They are the fundamental building blocks of digital hardware design, enabling modular, reusable, and hierarchical design methodologies.

What You'll Learn

  • Understand Verilog module syntax, port declarations, and instantiation techniques
  • Build hierarchical designs by nesting modules to create complex systems
  • Differentiate between top-level and sub-modules in design and testbench contexts
  • Use hierarchical naming conventions to access signals across module boundaries

Syntax

A module should be enclosed within module and endmodule keywords. Name of the module should be given right after the module keyword and an optional list of ports may be declared as well. Note that ports declared in the list of port declarations cannot be redeclared within the body of the module.


	// Basic module syntax with port list
	module <name> ([port_list]);
		// Variable declarations (wires, regs, integers, etc.)
		// Dataflow statements (assign, always, initial blocks)
		// Function and task definitions
		// Sub-module instantiations
	endmodule

	// A module can have an empty portlist (typically for testbenches)
	module testbench_top;
		// Testbench logic without external ports
	endmodule

All variable declarations, dataflow statements, functions or tasks and lower module instances if any, must be defined within the module and endmodule keywords. There can be multiple modules with different names in the same file and can be defined in any order.

AspectModule CharacteristicsNotes
ScopeAll code must be within module...endmoduleNo executable code allowed outside modules
Multiple ModulesOne file can contain multiple modulesDefined in any order; simulator resolves dependencies
Port ListOptional (can be empty for testbenches)Declared ports cannot be redeclared in module body
ReusabilitySame module can be instantiated multiple timesEach instance has unique identifier
HierarchyModules can contain sub-module instancesForms tree structure with top-level root

Example

dff ports

The module dff represents a D flip flop which has three input ports d, clk, rstn and one output port q. Contents of the module describe how a D flip flop should behave for different combinations of inputs. Here, input d is always assigned to output q at positive edge of clock if rstn is high because it is an active low reset.


// Module called "dff" has 3 inputs and 1 output port
module dff (  input 			d,       // Data input
              input 			clk,     // Clock signal
              input 			rstn,    // Active-low synchronous reset
              output reg	q);      // Registered output (must be 'reg' for sequential logic)

	// Sequential logic block: executes on every positive clock edge
	always @ (posedge clk) begin
		if (!rstn)             // Reset condition (active-low: rstn=0 triggers reset)
			q <= 0;              // Non-blocking assignment: reset output to 0
		else
			q <= d;              // Non-blocking assignment: capture input data
	end
endmodule

Hardware Schematic

This module will be converted into the following digital circuit during synthesis.

Note that you cannot have any code written outside a module !

What is the purpose of a module ?

A module represents a design unit that implements certain behavioral characteristics and will get converted into a digital circuit during synthesis. Any combination of inputs can be given to the module and it will provide a corresponding output. This allows the same module to be reused to form bigger modules that implement more complex hardware.

For example, the DFF shown above can be chained to form a shift register.


// 4-bit shift register built from four DFF module instances
module shift_reg ( 	input 	d,       // Serial data input
										input 	clk,     // Clock signal shared by all DFFs
										input 	rstn,    // Reset signal shared by all DFFs
										output 	q);      // Serial data output (final stage)

	// Internal wires connecting adjacent DFF stages
	wire [2:0] q_net;  // q_net[0] = u0 output, q_net[1] = u1 output, q_net[2] = u2 output

	// Module instantiation using named port mapping
	dff u0 (.d(d),          .clk(clk), .rstn(rstn), .q(q_net[0]));  // Stage 0: input -> q_net[0]
	dff u1 (.d(q_net[0]),   .clk(clk), .rstn(rstn), .q(q_net[1]));  // Stage 1: q_net[0] -> q_net[1]
	dff u2 (.d(q_net[1]),   .clk(clk), .rstn(rstn), .q(q_net[2]));  // Stage 2: q_net[1] -> q_net[2]
	dff u3 (.d(q_net[2]),   .clk(clk), .rstn(rstn), .q(q));         // Stage 3: q_net[2] -> output

endmodule

Hardware Schematic

Note that the dff instances are connected together with wires as described by the Verilog RTL module.

Instead of building up from smaller blocks to form bigger design blocks, the reverse can also be done. Consider the breakdown of a simple GPU engine into smaller components such that each can be represented as a module that implements a specific feature. The GPU engine shown below can be divided into five different sub-blocks where each perform a specific functionality. The bus interface unit gets data from outside into the design, which gets processed by another unit to extract instructions. Other units down the line process data provided by previous unit.

gpu modules

Each sub-block can be represented as a module with a certain set of input and output signals for communication with other modules and each sub-block can be further divided into more finer blocks as required.

What are top-level modules ?

A top-level module is one which contains all other modules. A top-level module is not instantiated within any other module.

For example, design modules are normally instantiated within top level testbench modules so that simulation can be run by providing input stimulus. But, the testbench is not instantiated within any other module because it is a block that encapsulates everything else and hence is the top-level module.

Design Top Level

The design code shown below has a top-level module called design_top. This is because it contains all other sub-modules required to make the design complete. The submodules can have more nested sub-modules like mod3 inside mod1 and mod4 inside mod2. Anyhow, all these are included into the top level module when mod1 and mod2 are instantiated. So this makes the design complete and is the top-level module for the design.


	//---------------------------------
	// Leaf-level modules (lowest in hierarchy)
	//---------------------------------
	module mod3 ( [port_list] );
		reg c;                     // Internal register storage
		// Low-level functional logic
	endmodule

	module mod4 ( [port_list] );
		wire a;                    // Internal wire connection
		// Low-level functional logic
	endmodule

	//---------------------------------
	// Mid-level modules (contain leaf modules)
	//---------------------------------
	module mod1 ( [port_list] );    // Mid-level module containing mod3 instances
		wire y;                       // Internal signal within mod1

		// Instantiate two mod3 modules with unique instance names
		mod3 mod_inst1 ( ... );       // First mod3 instance
		mod3 mod_inst2 ( ... );       // Second mod3 instance (reusing mod3 module)
	endmodule

	module mod2 ( [port_list] );    // Mid-level module containing mod4 instances
		// Instantiate two mod4 modules with unique instance names
		mod4 mod_inst1 ( ... );       // First mod4 instance
		mod4 mod_inst2 ( ... );       // Second mod4 instance (reusing mod4 module)
	endmodule

	//---------------------------------
	// Top-level module (root of hierarchy)
	//---------------------------------
	module design_top ( [port_list] );  // TOP-LEVEL: contains all design sub-modules
		wire _net;                    // Internal signal for inter-module communication

		// Instantiate mid-level modules (which contain mod3/mod4 instances)
		mod1 mod_inst1 ( ... );       // mod1 instance (brings in 2x mod3 instances)
		mod2 mod_inst2 ( ... );       // mod2 instance (brings in 2x mod4 instances)
	endmodule
	// Design hierarchy: design_top -> {mod1, mod2} -> {mod3, mod3, mod4, mod4}

Testbench Top Level

The testbench module contains stimulus to check functionality of the design and is primarily used for functional verification using simulation tools. Hence the design is instantiated and called d0 inside the testbench module. From a simulator perspective, testbench is the top level module.


	//-----------------------------------------------------------
	// Testbench code - TOP-LEVEL MODULE for simulation
	// From simulation perspective, this is the root of the hierarchy
	// because 'design_top' module is instantiated within this module
	//-----------------------------------------------------------
	module testbench;                             // No ports (testbench is self-contained)

		// Instantiate the design-under-test (DUT)
		design_top d0 ( [port_list_connections] );     // Connect testbench signals to design ports

		// Testbench components:
		// - Clock generation (initial/always blocks)
		// - Stimulus generation (initial blocks with test vectors)
		// - Response checking (always blocks with assertions)
		// - Waveform dumping ($dumpfile, $dumpvars)
	endmodule
	// Simulation hierarchy: testbench -> design_top -> {mod1, mod2} -> {mod3, mod3, mod4, mod4}

Hierarchical Names

A hierarchical structure is formed when modules can be instantiated inside one another, and hence the top level module is called the root. Since each lower module instantiation within a given module is required to have different identifier names, there will not be any ambiguity in accessing signals. A hierarchical name is constructed by a list of these identifiers separated by dots . for each level of the hierarchy. Any signal can be accessed within any module using the hierarchical path to that particular signal.


	// Hierarchical path examples using the design structure above

	// Access module instances (intermediate nodes in hierarchy tree)
	design_top.mod_inst1                    // Access mod1 instance within design module
	design_top.mod_inst2.mod_inst1          // Access first mod4 instance within mod2

	// Access signals through hierarchical paths (leaf nodes)
	design_top.mod_inst1.y                  // Access wire "y" inside mod1 instance
	design_top.mod_inst2.mod_inst2.a        // Access wire "a" within second mod4 instance

	// Access design signals from testbench scope
	testbench.d0._net                   // Access wire "_net" within design module from TB
	testbench.d0.mod_inst1.mod_inst2.c  // Access reg "c" in second mod3 instance from TB

	// Hierarchical paths enable cross-module signal probing in:
	// - Assertions (checking internal design state)
	// - Waveform dumps (selective signal dumping)
	// - Forced signals for debug ($force, force/release)

Hierarchical names are invaluable for testbench debugging, allowing you to probe or force internal signals without modifying the design. Use them with $dumpvars for selective waveform dumping or $monitor for targeted signal tracking.

Real-World Application: CPU Core Design

Modern CPU cores are designed using deeply hierarchical module structures. A typical RISC-V processor might have a top-level core module containing sub-modules like fetch_unit, decode_unit, execute_unit, memory_unit, and writeback_unit. Each of these units further contains specialized modules--for example, the execute_unit instantiates an alu module, branch_unit, and multiplier. This hierarchical decomposition enables:

  • Team collaboration: Different engineers work on independent modules (ALU, FPU, cache)
  • IP reuse: The same ALU module can be reused in GPU designs or DSP pipelines
  • Incremental verification: Each module is verified in isolation before integration
  • Design scalability: Adding SIMD units or co-processors involves instantiating new modules

Real-World Application: SoC Integration

System-on-Chip (SoC) designs for mobile processors or IoT devices use modules as IP blocks. A typical SoC top-level module instantiates:

  • cpu_subsystem (ARM Cortex-M4 or RISC-V core with cache hierarchy)
  • gpu_subsystem (graphics processing units with shader cores)
  • memory_controller (DDR4/LPDDR5 interface with arbiter logic)
  • peripheral_subsystem (UART, SPI, I2C, GPIO controllers)
  • interconnect (AXI/AHB bus matrix for module communication)

Each subsystem is developed independently, possibly by third-party IP vendors, and integrated via standardized interfaces. Module instantiation enables "plug-and-play" IP integration without modifying individual blocks.

Common Beginner Mistakes

Mistake #1: Using the same instance name for multiple module instantiations


module top;
  dff u0 (.d(a), .clk(clk), .rstn(rstn), .q(q1));
  dff u0 (.d(b), .clk(clk), .rstn(rstn), .q(q2));  // ERROR: duplicate instance name "u0"
endmodule

Why it's problematic: Each module instance must have a unique identifier within the parent module. Duplicate instance names create ambiguity in hierarchical paths and cause compilation errors. The simulator cannot differentiate between top.u0 referring to the first or second instance.

Best Practice: Use descriptive, unique instance names: dff_stage0, dff_stage1, or u0, u1 for sequential numbering.

Mistake #2: Redeclaring ports inside the module body


module counter (input clk, input rst, output reg [3:0] count);
  input clk;      // ERROR: port 'clk' already declared in port list
  output count;   // ERROR: port 'count' already declared in port list
  // ...
endmodule

Why it's problematic: When using ANSI-style port declarations (Verilog-2001), ports are fully declared in the module header with direction and type. Redeclaring them in the module body violates syntax rules and causes compilation errors.

Best Practice: Use ANSI-style ports exclusively for modern designs: module name (input wire a, output reg b); Do not redeclare ports inside the module.

Mistake #3: Writing executable code outside module boundaries


module dff (...);
  // module contents
endmodule

assign global_wire = 1'b0;  // ERROR: code outside module scope

module testbench;
  // testbench contents
endmodule

Why it's problematic: Verilog requires all executable code (assign statements, always blocks, initial blocks) to be encapsulated within module...endmodule boundaries. Code outside modules has no execution context and will cause syntax errors. Only compiler directives (`define, `include, `timescale) are allowed outside modules.

Best Practice: Ensure all logic is within module scope. Use packages (SystemVerilog) for shared parameters and type definitions.

Mistake #4: Confusing module definition with module instantiation


module top;
  module dff (input d, input clk, output reg q);  // ERROR: defining module inside another
    always @(posedge clk) q <= d;
  endmodule
endmodule

Why it's problematic: Module definitions (the structural template) cannot be nested inside other modules. Verilog only supports module instantiation (creating instances of already-defined modules). Attempting to define a module inside another causes "illegal nested module definition" errors.

Best Practice: Define all modules separately at the top level of your file. Inside parent modules, only instantiate pre-defined modules: dff u0 (.d(data), .clk(clk), .q(output));

Summary

In this tutorial, you learned:

  • Module syntax: Modules are defined with module...endmodule keywords, encapsulating ports, variables, logic, and sub-module instances
  • Module instantiation: Reuse modules by creating multiple instances with unique names and named/positional port connections
  • Hierarchical design: Build complex systems by nesting modules, forming tree structures from leaf-level primitives to top-level integration
  • Top-level modules: Design top-level contains all RTL sub-modules; testbench top-level instantiates the design for simulation
  • Hierarchical names: Access signals across module boundaries using dot notation (parent.child.signal) for debugging and verification
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5