Modules, ports, hierarchy and parameters

Learning objectives
  • Define modules with clear ports
  • Instantiate hierarchy using named connections
  • Use parameters and localparams
  • Identify unconnected and multiply driven nets
  • Explain elaboration
1 Objective→ 2 Learn→ 3 Example→ 4 Practice→ 5 Verify→ 6 Continue
How to study this lessonUnderstand the idea first. Use the example to see the behaviour, practise where provided, then make sure you can explain the result before continuing.
top
u_ctrlu_datapathu_fifo
Concept diagram: Modules, ports, hierarchy and parameters

Explanation

A module is a hardware boundary

A useful module has one clear responsibility, an explicit interface, and a name that reflects its function. This parameterized adder makes both width and carry visible:

module add_unsigned #(
    parameter WIDTH = 8
) (
    input  wire [WIDTH-1:0] a,
    input  wire [WIDTH-1:0] b,
    output wire [WIDTH:0]   sum
);
    assign sum = {1'b0, a} + {1'b0, b};
endmodule

The parameter is applied during elaboration. It is not a register and cannot change while the simulated hardware runs.

Port declarations and time units

Prefer ANSI-style declarations, where each port's direction and width appear beside its name:

module and_gate (
    input  wire a,
    input  wire b,
    output wire y
);
    assign y = a & b;
endmodule

Classic Verilog places names in the module header and declares directions in the body. You may encounter it in older code; check that every header name has one matching declaration before editing it. Keep testbench time units explicit with a directive such as `timescale 1ns/1ps. The first value sets the delay unit and the second sets precision; #5 then means five time units in that source context. Time directives affect simulation interpretation, not physical implementation delay. Use the project's supported compilation mode when source files have different time directives.

Named connections are reviewable

wire [16:0] result;

add_unsigned #(.WIDTH(16)) u_add (
    .a   (operand_a),
    .b   (operand_b),
    .sum (result)
);

Named connections expose intent and reduce errors when interfaces evolve. Verify direction, width, signedness, and meaning at every boundary. Do not leave an input floating unless the interface explicitly permits it.

Hierarchy and ownership

An instance creates a child below its parent. A signal should have one clear owner. Multiple ordinary RTL drivers often mean that two blocks are trying to control the same hardware. Tri-state buses require deliberate technology and interface rules.

Use consistent instance names such as u_fifo, u_ctrl, and u_datapath. They become paths in logs and waveforms:

tb.dut.u_ctrl.state

Parameters and local constants

Use a parameter when the parent may configure the value. Use localparam for an internal constant that must not be overridden.

localparam ADDR_WIDTH = 4;
localparam IDLE = 2'b00;

Reject meaningless configurations through project checks or elaboration-time testbench guards. For example, a FIFO depth may need to be at least two and compatible with its pointer calculation.

Avoid implicit nets

A misspelled signal can silently become a one-bit implicit wire in permissive Verilog. Add this at the start of source files or through an approved common header:

`default_nettype none

Restore the project default at the end if required:

`default_nettype wire

With implicit nets disabled, spelling mistakes become compile errors.

Worked example

A parent connects an 8-bit signal to a 16-bit child input. Compilation may only warn and automatically extend the value. The code is still ambiguous: was extension intended, and should it be zero or sign extension? Fix the boundary with an explicit conversion and document the numeric meaning.

Check yourself

Why can a parameter change hardware structure but not runtime behavior? What four properties should you review at a port connection? How does disabling implicit nets improve debug?