A module declares its ports and defines the logic inside. Ports are the contract with the outside world.
Use the ANSI style, with direction, type, and width in the port list. It keeps the interface in one place.
`default_nettype none
module half_adder (
input wire a,
input wire b,
output wire sum,
output wire carry
);
assign sum = a ^ b;
assign carry = a & b;
endmodule
module full_adder (
input wire a, b, cin,
output wire sum, cout
);
wire s1, c1, c2;
// Instances connected BY NAME: order of ports does not matter
half_adder ha1 (.a(a), .b(b), .sum(s1), .carry(c1));
half_adder ha2 (.a(s1), .b(cin), .sum(sum), .carry(c2));
assign cout = c1 | c2;
endmodule
`default_nettype wireA full adder built from two half adders. Every connection uses the .port(signal) form, called connection by name.
Read it as the netlist the code describes: each box is an instance, grey names next to a box are its ports, and orange names are the wires declared inside full_adder.
s1 joins ha1.sum to ha2.a; that is the .sum(s1) and .a(s1) pair in the two instance lines.
cin goes straight to ha2.b, so ha2 adds the carry in to the first partial sum.
c1 and c2 never both equal 1, so the OR gate gives the single carry out: assign cout = c1 | c2;.
Style | Example | Risk |
|---|---|---|
By name (recommended) |
| None: order is irrelevant and ports are self-documenting. |
By position |
| Swap two arguments and the code still compiles. Adding a port silently shifts every connection. |
A parameter makes a block reusable at different sizes. Give every parameter a default and override it by name:
module invert_bus #(parameter WIDTH = 8) (
input wire [WIDTH-1:0] a,
output wire [WIDTH-1:0] y
);
assign y = ~a;
endmodule
module invert_bus_top (input wire [15:0] a, output wire [15:0] y);
invert_bus #(.WIDTH(16)) u_inv (.a(a), .y(y));
endmoduleWIDTH=8 is the default. The instance overrides it with 16, so [WIDTH-1:0] becomes [15:0] on both ports. The logic is still a per-bit NOT: parameters choose the hardware dimensions before simulation starts, not during operation. Predict the port ranges for WIDTH=4 before moving on.
The same width parameter also sizes storage. The listing below previews clock/reset events, replication and <=; those are explained in the following lessons. First understand the parameter substitution, then return to the storage behavior after the two process templates.
module reg_n #(parameter WIDTH = 8) (
input wire clk,
input wire rst_n,
input wire [WIDTH-1:0] d,
output reg [WIDTH-1:0] q
);
always @(posedge clk or negedge rst_n)
if (!rst_n) q <= {WIDTH{1'b0}};
else q <= d;
endmodule
module top (input wire clk, rst_n, input wire [15:0] din, output wire [15:0] dout);
reg_n #(.WIDTH(16)) u_reg (.clk(clk), .rst_n(rst_n), .d(din), .q(dout));
endmoduleAn undeclared name on the left of a connection or assign quietly creates a 1-bit implicit wire. The design is then wrong in a way the compiler never reports.
Start the file with the compiler directive default_nettype none, written with a leading backtick, to turn that mistake into an error. End the file with the same directive set to wire, so other files are unaffected.
// bad-example: without default_nettype none the typo 'sm' compiles
module typo_demo (input wire a, b, output wire sum);
assign sm = a ^ b; // typo: creates an implicit wire 'sm'; 'sum' stays undriven
endmoduleIt couples the instance to the port order of the module. If someone later inserts or reorders a port, positional instances still compile but connect the wrong signals. By-name connections remain correct and make a mismatch an error.