Modules, ports, hierarchy and parameters
- Define modules with clear ports
- Instantiate hierarchy using named connections
- Use parameters and localparams
- Identify unconnected and multiply driven nets
- Explain elaboration
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?