Tutorial 02 · Verilog Series

Verilog Module & Port Declaration

Every Verilog design is built from modules. Learn exactly how to declare modules, define ports (input / output / inout), set bit-widths, use wire vs reg, and instantiate one module inside another.

module / endmodule input / output / inout Port Width [n:0] wire vs reg Named Port Connection Module Instantiation
module my_alu internal logic always / assign endmodule a [7:0] input b [7:0] input op [2:0] input result [7:0] output zero output Verilog module — inputs (blue) and outputs (green)
A Verilog module exposes named ports (input / output / inout) to the outside world; all logic lives inside.

1. Anatomy of a Module

A Verilog module always starts with module and ends with endmodule. Between them you declare ports and then describe the logic.

verilog
module module_name (
    // port list
    input  port_a,
    output port_b
);
    // internal signal declarations
    // logic: assign, always, submodule instances

endmodule

2. Port Types: input, output, inout

input

Signal flows into the module. The module can read it but cannot drive it. Internally treated as wire.

output

Signal flows out of the module. The module drives it. Can be wire (driven by assign) or reg (driven by always).

inout

Bidirectional port — driven or read depending on direction logic. Always wire. Needs tri-state enable for proper use.

Note: From the parent module's perspective, what is output in the child is a signal the parent receives. Think of directions always from inside the module looking out.

3. Port Widths & Vectors

A port without a width specifier is 1 bit. Multi-bit ports (vectors) use [MSB:LSB]:

verilog
module alu_8bit (
    input        clk,          // 1 bit
    input        rst_n,        // 1 bit (active-low reset)
    input  [7:0] a,            // 8-bit bus, a[7] is MSB
    input  [7:0] b,
    input  [2:0] op,           // 3-bit opcode
    output [7:0] result,
    output       zero,         // 1-bit flag
    output       carry_out
);
    // body ...
endmodule

The convention [MSB:LSB] almost always means [n-1:0] for an n-bit signal. You can technically write [0:7] (little-endian bit ordering) but it is non-standard and confusing — avoid it.

Watch out: Bit-select and part-select follow the declared ordering. If you declare [7:0], then a[7] is the MSB. Mixing conventions causes hard-to-debug logic errors.

4. wire vs reg on Ports

Port DirectionAllowed TypesWhen to Use Which
inputwire (default)Always wire — inputs are driven externally, never by internal logic.
outputwire or regUse wire if driven by assign. Use reg if driven inside an always block.
inoutwire (only)Bidirectional signals are always wire — they can be driven from either side.
verilog
module example (
    input       a, b,
    output      y_wire,   // driven by assign
    output reg y_reg    // driven by always
);
    assign y_wire = a & b;   // continuous assignment

    always @(a, b)           // procedural block
        y_reg = a | b;
endmodule
SystemVerilog note: In SystemVerilog you can use logic for all ports — it replaces both wire and reg and can be driven by either assign or always. We'll cover this in the SystemVerilog tutorial.

5. ANSI vs Legacy Port Declaration Style

Verilog has two syntaxes for declaring ports. ANSI style (Verilog-2001, recommended) puts direction and type directly in the port list:

verilog — ANSI style (recommended)
// ANSI style (Verilog-2001+)
module adder (
    input  [7:0] a,
    input  [7:0] b,
    output [8:0] sum
);
    assign sum = a + b;
endmodule

The legacy Verilog-95 style separates port names from their declarations — you'll see this in older code bases:

verilog — Legacy style (Verilog-95)
// Legacy style — port names only in the list
module adder (a, b, sum);
    input  [7:0] a, b;    // declared separately
    output [8:0] sum;
    assign sum = a + b;
endmodule
Best practice: Always use ANSI style. It is cleaner, avoids declaration mismatches, and is supported by every modern tool (Vivado, Quartus, VCS, Xcelium).

6. Module Instantiation

To use a module inside another, you instantiate it — this is the Verilog equivalent of wiring a chip onto a PCB. Syntax:

verilog
// module_name instance_name ( port connections );
adder u_adder (
    .a   (operand_a),    // .port_name(signal_in_parent)
    .b   (operand_b),
    .sum (result)
);

The instance name prefix u_ (for "unit") is a common convention. Some teams use i_ for instances. Pick one style and stick to it.

7. Named vs Positional Port Connection

StyleSyntaxVerdict
Named .port_name(signal) ✅ Preferred — safe if port order changes, self-documenting
Positional Just list signals in declaration order ⚠️ Fragile — port reorder silently swaps connections
verilog — positional (avoid in real designs)
// Positional — works but fragile
adder u_add (operand_a, operand_b, result);
verilog — named (always use this)
// Named — explicit, safe
adder u_add (
    .a   (operand_a),
    .b   (operand_b),
    .sum (result)
);

You can leave a port unconnected using an empty parenthesis: .unused_port(). Unconnected outputs float to high-Z; unconnected inputs are driven 0. Simulators typically warn you about this.

8. Full Example: 4-bit Ripple-Carry Adder

Here's a complete hierarchical design: a 1-bit full adder, instantiated four times to make a 4-bit ripple-carry adder.

verilog — full_adder.v
// 1-bit full adder
module full_adder (
    input  a, b, cin,
    output sum, cout
);
    assign sum  = a ^ b ^ cin;
    assign cout = (a & b) | (b & cin) | (a & cin);
endmodule
verilog — adder4.v
// 4-bit ripple-carry adder using 4 full_adder instances
module adder4 (
    input  [3:0] a, b,
    input        cin,
    output [3:0] sum,
    output       cout
);
    wire c0, c1, c2;    // internal carry wires

    full_adder fa0 (.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(c0));
    full_adder fa1 (.a(a[1]), .b(b[1]), .cin(c0),  .sum(sum[1]), .cout(c1));
    full_adder fa2 (.a(a[2]), .b(b[2]), .cin(c1),  .sum(sum[2]), .cout(c2));
    full_adder fa3 (.a(a[3]), .b(b[3]), .cin(c2),  .sum(sum[3]), .cout(cout));
endmodule
verilog — tb_adder4.v (testbench)
module tb_adder4;   // no ports
    reg  [3:0] a, b;
    reg        cin;
    wire [3:0] sum;
    wire       cout;

    adder4 dut (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));

    initial begin
        $dumpfile("wave.vcd"); $dumpvars(0, tb_adder4);
        a=4'd5; b=4'd3;  cin=0; #10;
        $display("%0d + %0d = %0d (cout=%b)", a, b, sum, cout);
        a=4'd15; b=4'd1; cin=0; #10;
        $display("%0d + %0d = %0d (cout=%b)", a, b, sum, cout);
        $finish;
    end
endmodule

Output you'll see:

simulation output
5 + 3 = 8 (cout=0)
15 + 1 = 0 (cout=1)   ← 4-bit overflow, carry propagates out

9. Common Mistakes

MistakeWhat Goes WrongFix
Driving an input port inside the moduleCompile error or multi-driverinput ports are read-only inside the module
Using reg on an inputSyntax error in most toolsinput is always wire — just write input
Using wire on an output driven by alwaysSimulator error: "reg expected"Change to output reg
Trailing comma on last portSyntax errorRemove comma from the last port in the list
Width mismatch: 4-bit port connected to 8-bit wireTruncation (upper bits dropped) — no error, but wrong resultMatch widths or explicitly slice: .p(big_wire[3:0])
Positional port connection after port reorderSilent functional bugAlways use named connections: .port(signal)