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.
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.
module module_name ( // port list input port_a, output port_b ); // internal signal declarations // logic: assign, always, submodule instances endmodule
- The module name must match the filename (by strong convention) — e.g.
module adder→adder.v. - The port list is comma-separated; the last port has no trailing comma.
- Everything after the
);and beforeendmoduleis the module body.
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.
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]:
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.
[7:0], then a[7] is the MSB. Mixing conventions causes hard-to-debug logic errors.4. wire vs reg on Ports
| Port Direction | Allowed Types | When to Use Which |
|---|---|---|
input | wire (default) | Always wire — inputs are driven externally, never by internal logic. |
output | wire or reg | Use wire if driven by assign. Use reg if driven inside an always block. |
inout | wire (only) | Bidirectional signals are always wire — they can be driven from either side. |
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
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:
// 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:
// 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
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:
// module_name instance_name ( port connections ); adder u_adder ( .a (operand_a), // .port_name(signal_in_parent) .b (operand_b), .sum (result) );
adder— the module being instantiated (must be defined somewhere in the project)u_adder— the instance name (unique identifier within the parent module).port(signal)— named port connection (see below)
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
| Style | Syntax | Verdict |
|---|---|---|
| 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 |
// Positional — works but fragile adder u_add (operand_a, operand_b, result);
// 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.
// 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
// 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
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:
5 + 3 = 8 (cout=0) 15 + 1 = 0 (cout=1) ← 4-bit overflow, carry propagates out
9. Common Mistakes
| Mistake | What Goes Wrong | Fix |
|---|---|---|
Driving an input port inside the module | Compile error or multi-driver | input ports are read-only inside the module |
Using reg on an input | Syntax error in most tools | input is always wire — just write input |
Using wire on an output driven by always | Simulator error: "reg expected" | Change to output reg |
| Trailing comma on last port | Syntax error | Remove comma from the last port in the list |
| Width mismatch: 4-bit port connected to 8-bit wire | Truncation (upper bits dropped) — no error, but wrong result | Match widths or explicitly slice: .p(big_wire[3:0]) |
| Positional port connection after port reorder | Silent functional bug | Always use named connections: .port(signal) |