SystemVerilog · Module 7
Module Definition & Instantiation
Module anatomy, port lists, hierarchical design, multi-instance wiring, named/positional/wildcard connections, and instance arrays.
Module 7 · Page 7.1
Modules are the fundamental building blocks of every digital design. This page teaches the full anatomy of a SystemVerilog module, how to instantiate one, how to assemble many into a hierarchical chip, every port-connection style that exists, instance arrays and generate loops, testbench patterns, five debugging labs from real silicon projects, and twelve interview-ready questions.
What is a Module?
Every piece of hardware you design in SystemVerilog lives inside a module. Think of a module as a black box: it has inputs, outputs, and internal logic. From the outside, you only see the pins — the wires that connect to it. The internals are hidden and self-contained.
A simple AND gate is a module. A 32-bit ALU is a module. A full SoC with hundreds of sub-blocks is also a module — one that contains dozens of other modules inside it. This is the concept of hierarchical design, and it is the most powerful idea in RTL engineering.
Anatomy of a Module
Every module in SystemVerilog has the same skeleton. Once you know the anatomy, you can read any RTL file in any company's codebase.
// ① module keyword + module name
module half_adder (
// ② ANSI port list (direction + type + name)
input logic a, b,
output logic sum, cout
);
// ③ Internal signals (not ports)
logic carry_internal;
// ④ RTL logic (assign / always / etc.)
assign sum = a ^ b;
assign cout = a & b;
endmodule // ⑤ endmodule closes the moduleLet's break down each part:
| Part | Role |
|---|---|
① module … endmodule | Keywords that open and close the module boundary. Everything between belongs here. |
| ② Port list | Declares every pin — direction (input/output/inout) and data type. |
| ③ Internal signals | Wires and registers used inside the module. Invisible from outside. |
| ④ RTL logic | The actual behaviour: assign statements, always blocks, sub-module instances. |
⑤ endmodule | Closes the module. Optionally labelled (e.g. endmodule : half_adder). |
Your First Module — Every Part Explained
Let's write a complete, realistic module from scratch: a 4-bit adder. Read through the full code first, then check each numbered explanation below.
// ── ① Module declaration: name comes right after 'module' ──────
module adder_4bit (
// ── ② Port list (ANSI style — direction + type + name) ─────
input logic [3:0] a, // 4-bit operand A
input logic [3:0] b, // 4-bit operand B
input logic cin, // carry-in
output logic [3:0] sum, // 4-bit result
output logic cout // carry-out
);
// ── ③ Internal signal (not visible to the outside world) ────
logic [4:0] full_result;
// ── ④ RTL logic: combinational addition ─────────────────────
assign full_result = {1'b0, a} + {1'b0, b} + cin;
// ── ⑤ Drive outputs from internal result ────────────────────
assign sum = full_result[3:0];
assign cout = full_result[4];
endmodule // ← ⑥ Always end with endmodule① Module Name
The module name follows immediately after the module keyword. By convention, use lowercase with underscores for RTL modules (e.g. adder_4bit, not Adder4Bit). The name must be unique across your project — two modules with the same name cause elaboration errors.
② ANSI Port List
This is the ANSI C-style port declaration — the modern way to write ports in SystemVerilog. Each port declares its direction (input / output / inout), its type (logic, wire, etc.), its width ([3:0]), and its name — all in one line, inside the parentheses right after the module name. Ports are separated by commas; the last port has no comma.
③ Internal Signals
logic [4:0] full_result; is a local wire used only inside adder_4bit. It is invisible when you look at the module from the outside. You declare it with logic (or wire) after the port list closes with );.
④⑤ RTL Logic
assign statements describe combinational logic. The concatenation {1'b0, a} zero-extends a to 5 bits so the addition result can hold the carry without overflow. Then the lower 4 bits go to sum and bit 4 becomes cout.
⑥ endmodule
Every module must be closed with endmodule. You can optionally add the module name as a label for clarity: endmodule : adder_4bit. This is especially useful in long files.
Module Instantiation
Defining a module gives you a blueprint. Instantiation is the act of placing a physical copy of that module inside another module and connecting its ports to signals. Every copy you place is called an instance.
The syntax is:
// module_name instance_name ( .port_name(signal), ... );
// ─────────── ───────────── ──────────────────────────
// what to use unique label how to wire the ports
adder_4bit u_adder (
.a (operand_a),
.b (operand_b),
.cin (carry_in),
.sum (result),
.cout(carry_out)
);Three parts every instantiation needs:
| Part | Description |
|---|---|
| Module Name | The name of the module you want to use. Exactly as it appears in its definition — case-sensitive. |
| Instance Name | A unique label for this particular copy. Convention: prefix with u_ (unit) or i_. Unique within parent. |
| Port Connections | Named connections: .port_name(signal_name). Port name from definition; signal name from parent module. |
A Complete Parent Module with an Instance Inside
module alu_top (
input logic [3:0] x, y,
input logic c_in,
output logic [3:0] alu_result,
output logic alu_cout
);
// Internal wires to connect the sub-module
logic [3:0] add_sum;
logic add_cout;
// ── Instantiate adder_4bit ─────────────────────────────────────
// module_name instance_name (named port connections)
adder_4bit u_adder (
.a (x), // connect port 'a' to signal 'x'
.b (y), // connect port 'b' to signal 'y'
.cin (c_in), // connect port 'cin' to 'c_in'
.sum (add_sum), // connect port 'sum' to 'add_sum'
.cout(add_cout) // connect port 'cout' to 'add_cout'
);
// Drive top-level outputs from internal wires
assign alu_result = add_sum;
assign alu_cout = add_cout;
endmoduleBuilding a Design Hierarchy
Real chips are not flat. They are hierarchical — big modules contain smaller modules, which in turn contain even smaller modules. This layered structure is what makes it possible to design billion-transistor chips in manageable pieces.
In this hierarchy, chip_top is the top-level module. It instantiates cpu_core, mem_ctrl, and uart_if. Inside cpu_core, there is an instance of alu_top — which itself contains our adder_4bit. Each level only needs to know about the modules directly below it.
Multiple Instances of the Same Module
One of the biggest advantages of modules is reuse. You write a module once — say, a 4-bit adder — and then use it as many times as you need, each time with a different instance name and port connections. The simulator and synthesis tool create a separate physical copy for each instance.
module multi_add (
input logic [3:0] p, q, r, s,
output logic [3:0] result
);
logic [3:0] sum_pq, sum_rs;
logic cout_pq, cout_rs, cout_final;
// ── Instance 1: add p + q ──────────────────────────────────
adder_4bit u_add_pq (
.a (p), .b (q),
.cin (1'b0), .sum (sum_pq), .cout(cout_pq)
);
// ── Instance 2: add r + s ──────────────────────────────────
adder_4bit u_add_rs (
.a (r), .b (s),
.cin (1'b0), .sum (sum_rs), .cout(cout_rs)
);
// ── Instance 3: add (p+q) + (r+s) ─────────────────────────
adder_4bit u_add_final (
.a (sum_pq), .b (sum_rs),
.cin (cout_pq), .sum (result), .cout(cout_final)
);
endmoduleThe Testbench Module
A testbench is also a module — but one with no ports. It sits at the top of the simulation hierarchy, generates stimulus, instantiates the design under test (DUT), and checks the outputs.
// Testbench: no port list — it drives itself
module tb_adder_4bit;
// ── Declare signals to drive / observe ──────────────────────
logic [3:0] a, b;
logic cin;
logic [3:0] sum;
logic cout;
// ── Instantiate the DUT (Device Under Test) ──────────────────
adder_4bit u_dut (
.a (a),
.b (b),
.cin (cin),
.sum (sum),
.cout(cout)
);
// ── Apply stimulus ────────────────────────────────────────────
initial begin
$dumpfile("adder.vcd");
$dumpvars(0, tb_adder_4bit);
// Test 1: 3 + 5 + 0 = 8, no carry
a = 4'd3; b = 4'd5; cin = 0; #10;
$display("a=%0d b=%0d cin=%0b => sum=%0d cout=%0b", a, b, cin, sum, cout);
// Test 2: 9 + 8 + 0 = 17, carry-out
a = 4'd9; b = 4'd8; cin = 0; #10;
$display("a=%0d b=%0d cin=%0b => sum=%0d cout=%0b", a, b, cin, sum, cout);
// Test 3: 15 + 15 + 1 = 31, carry-out
a = 4'd15; b = 4'd15; cin = 1; #10;
$display("a=%0d b=%0d cin=%0b => sum=%0d cout=%0b", a, b, cin, sum, cout);
$finish;
end
endmoduleOld-Style vs ANSI Port Declaration
You will see both styles in the real world. Know how to read the old style — but always write the new ANSI style in your own code.
| Aspect | Old Verilog Style | ANSI Style (SystemVerilog) |
|---|---|---|
| Port names | Listed in parentheses only (no types) | Direction + type + name all together in port list |
| Type declarations | Declared again separately inside the module body | Declared once — in the port list |
| Lines of code | More — each port appears twice | Less — each port declared exactly once |
| Error risk | Higher — mismatch between list and body | Lower — single source of truth |
| Use in new code? | No — legacy only | Yes — always prefer ANSI |
// ════ OLD VERILOG STYLE — you may see this in legacy code ════════
module half_adder_old (a, b, sum, cout); // names only in list
input a, b; // direction declared below
output sum, cout; // type defaults to wire
assign sum = a ^ b;
assign cout = a & b;
endmodule
// ════ ANSI STYLE — write this in all new code ════════════════════
module half_adder_ansi (
input logic a, // direction + type + name
input logic b, // all in one place
output logic sum,
output logic cout
);
assign sum = a ^ b;
assign cout = a & b;
endmodulePort Connection Styles — Named, Positional & Wildcard
When you instantiate a module, SystemVerilog gives you three ways to wire up its ports. Each has different implications for safety, readability, and maintainability. Understanding all three is critical — not just for writing new code, but for reading the legacy code you will encounter on every real project.
| Connection Style | Syntax | Order Sensitive? | Safety | When to Use |
|---|---|---|---|---|
| Named | .port_name(signal) | No | ✅ Safest | All production RTL and testbench code |
| Positional | (sig1, sig2, sig3) | Yes — must match port order exactly | ⚠️ Fragile | Legacy Verilog only — never write new |
| Wildcard | (.*) | No — name match | ⚠️ Careful | Testbench where signal names mirror port names |
Named Connections — The Professional Standard
Named connections pair each port with its driving signal using .port_name(signal_name) syntax. Port order is completely irrelevant — you can list them alphabetically, by importance, or in any sequence that improves readability. When the designer adds a new optional port months later, your instantiation site is completely unaffected.
// ════ NAMED CONNECTIONS — order does not matter ══════════════════
adder_4bit u_add (
.a (operand_a), // port name . signal name
.cin (carry_in), // listed out of "natural" order — still correct
.b (operand_b),
.cout (carry_out),
.sum (result_sum)
);
// If the designer later adds an optional 'mode' port:
// module adder_4bit(input logic mode, input logic[3:0] a, b, ...)
// Your connection above still compiles. 'mode' just floats (0 or X).
// No silent wrong behaviour. You can address it explicitly when ready.Positional Connections — Why They Break
In positional connections, there are no port names — just signals listed in the exact order the ports appear in the module definition. This looks compact, but it is extremely brittle. The moment any port is added, removed, or reordered in the module definition, every positional instantiation site silently rewires — with no compiler error.
// Original module port order:
// module adder_4bit(a, b, cin, sum, cout)
// Positional instantiation (old style):
adder_4bit u_add (opa, opb, ci, res, co);
// ────────────────────────────────────────────
// Designer adds carry_kill between cin & sum:
// module adder_4bit(a,b,cin,carry_kill,sum,cout)
// NOW: res → carry_kill port ❌
// co → sum port ❌
// No compiler error. Wrong results in sim.// Named instantiation:
adder_4bit u_add (
.a (opa),
.b (opb),
.cin (ci),
.sum (res),
.cout(co)
);
// After port reorder:
// carry_kill is simply unconnected (0/X).
// All existing connections remain correct.
// Easy to add: .carry_kill(1'b0) when ready.Wildcard .* Connections — Productivity Shortcut with Caveats
The .* syntax tells the simulator: "for every port, look for a signal in the current scope with the same name, and connect them." It is a SystemVerilog-only feature that can dramatically reduce boilerplate in testbenches where you deliberately name your local signals to match the DUT's port names.
module tb_adder_4bit;
// Signal names deliberately match port names exactly
logic [3:0] a, b, sum;
logic cin, cout;
// .* auto-connects all ports by name match
adder_4bit u_dut (.*); // port a→a, b→b, cin→cin, sum→sum, cout→cout
// ─────────────────────────────────────────────────────────────────
// Mix: auto-connect most, explicitly override one port
adder_4bit u_dut2 (
.cin (1'b0), // explicit override — always zero carry-in
.* // all other ports matched by name automatically
);
// ⚠ DANGER: if you rename signal 'a' to 'operand_a' in the TB,
// port 'a' silently becomes unconnected → X propagates into DUT.
// No compile error. This is why .* is testbench-only.
endmoduleInstance Arrays — Replicate Hardware with One Declaration
When your design needs multiple identical blocks with different data slices — think N-lane processors, N-channel DMAs, N-bit carry chains — SystemVerilog lets you declare an array of instances in a single statement. This is far more compact than copy-pasting N instantiations and eliminates the off-by-one mistakes that plague manual replication.
// ════ Four separate instances (tedious, error-prone) ══════════════
adder_4bit u_add0 (.a(a[3:0]), .b(b[3:0]), .cin(1'b0), .sum(s[3:0]), .cout(co[0]));
adder_4bit u_add1 (.a(a[7:4]), .b(b[7:4]), .cin(1'b0), .sum(s[7:4]), .cout(co[1]));
adder_4bit u_add2 (.a(a[11:8]), .b(b[11:8]), .cin(1'b0), .sum(s[11:8]), .cout(co[2]));
adder_4bit u_add3 (.a(a[15:12]),.b(b[15:12]),.cin(1'b0), .sum(s[15:12]),.cout(co[3]));
// ════ Equivalent instance array (elegant) ════════════════════════
// Creates u_adds[0], u_adds[1], u_adds[2], u_adds[3]
adder_4bit u_adds [3:0] (
.a (a[15:0]), // 16-bit wide → 4 bits per instance
.b (b[15:0]), // slicing: [0]→[3:0], [1]→[7:4], etc.
.cin (4'b0), // scalar: same 1-bit value replicated to all 4
.sum (s[15:0]),
.cout(co[3:0])
);Generate Loop — Full Control for Complex Patterns
When the connection pattern is more complex — particularly when each instance's output feeds the next instance's input (carry chains, pipeline stages, shift registers) — a generate loop gives you element-by-element control that instance arrays cannot express.
module adder_16bit (
input logic [15:0] a, b,
input logic cin,
output logic [15:0] sum,
output logic cout
);
// Carry chain: carry[0] = primary cin, carry[4] = final cout
logic [4:0] carry;
assign carry[0] = cin;
assign cout = carry[4];
genvar i;
generate
for (i = 0; i < 4; i++) begin : gen_stage
adder_4bit u_adder (
.a (a [i*4+3 : i*4]),
.b (b [i*4+3 : i*4]),
.cin (carry[i]),
.sum (sum [i*4+3 : i*4]),
.cout(carry[i+1])
);
end
endgenerate
endmodule
// Resulting hierarchy in simulation/synthesis:
// adder_16bit
// ├── gen_stage[0].u_adder → bits [3:0]
// ├── gen_stage[1].u_adder → bits [7:4]
// ├── gen_stage[2].u_adder → bits [11:8]
// └── gen_stage[3].u_adder → bits [15:12]
//
// Hierarchical reference in TB assertions:
// tb.u_dut.gen_stage[2].u_adder.coutCommon Mistakes to Avoid
| Mistake | Why it bites |
|---|---|
Missing endmodule | The compiler throws a confusing error at the end of the file. Always close every module. |
| Duplicate instance names | Two instances named u_add in the same module cause an elaboration error. Every instance needs a unique name. |
| Leaving ports unconnected | Unconnected output ports float to X. Unconnected input ports drive 0 in synthesis but Z in simulation. |
| Width mismatch | Connecting a 4-bit signal to an 8-bit port causes a width-mismatch warning — or worse, silent truncation. |
Debugging Academy — Real Bugs in Module Instantiation
Module instantiation bugs are some of the most insidious in hardware design. They often compile cleanly, simulate without errors, and produce plausible-looking results — until they don't. Each lab below is based on a category of bug seen in real silicon projects. Learn the symptoms, the waveform signature, and the fix before you encounter them under deadline pressure.
Width Mismatch — Silent Truncation or Zero-Extension
RTL + VERIFICATIONDUT produces correct results for small input values but fails for values above a threshold. Simulation shows width-mismatch warnings that the team has been suppressing.
An 8-bit bus is connected to a 4-bit input port. The simulator truncates the upper bits silently. For inputs 0–15 everything works. For inputs 16–255, the upper nibble is lost.
logic [7:0] data_bus; // 8-bit signal
// adder_4bit.a is 4-bit wide
adder_4bit u_add (
.a (data_bus), // ❌ 8→4: truncated!
.b (b),
.cin (cin),
.sum (sum),
.cout(cout)
);
// data_bus[7:4] silently dropped
// Simulator warns; lint tools flag itlogic [7:0] data_bus;
// Explicitly take the lower nibble
adder_4bit u_add (
.a (data_bus[3:0]), // ✅ explicit
.b (b),
.cin (cin),
.sum (sum),
.cout(cout)
);
// Intent is clear. Reviewers see the choice.Waveform signature: data_bus = 8'hA5 (165 decimal) → port a sees 4'h5 (5 decimal). Upper nibble A is gone. Sum shows 5+b, not 165+b.
Prevention: enable lint checks in your simulator. Synopsys VCS: +lint=all. Cadence Xcelium: -messages. Never suppress width-mismatch warnings without investigating.
Unconnected Output — X Propagation Corrupts Downstream Logic
CRITICAL SILICON BUGSimulation shows X values spreading through the design — first on one signal, then contaminating others. The bug appears intermittently and the X seems to "appear from nowhere."
An important output port of a sub-module was left unconnected (no signal, no empty parentheses). The downstream logic that was supposed to receive this output is instead floating.
// ════ BUGGY — carry_out is critical but left unconnected ═════════
logic [3:0] sum;
// carry_out signal never declared — just omitted in the port list
adder_4bit u_add (
.a (opa),
.b (opb),
.cin (cin),
.sum (sum)
// .cout — MISSING! cout port is not connected at all
);
// The next stage that needs carry-out is using an uninitialised signal
adder_4bit u_add_hi (
.cin (floating_carry), // ❌ floating — X or Z value in simulation
// ...
);// ════ FIXED — explicit empty () for intentionally unused, or connect it ═
logic carry_out; // declare it
adder_4bit u_add (
.a (opa),
.b (opb),
.cin (cin),
.sum (sum),
.cout(carry_out) // ✅ connected and declared
);
// If you truly don't need an output port:
adder_4bit u_add_nc (
.cout(), // ✅ intentionally unconnected — documents intent
// ...
);Waveform signature: the signal fed by the missing output shows as X (red in most waveform viewers) for the entire simulation. Downstream registers clocking X propagate it further. X-contamination typically fans out in waves with each clock edge.
Debug process: open waveform viewer. Find the first X-valued signal at time 0 (before any stimulus). Trace its driver. If it has no driver, you found the floating port. Simulator $cast or lint checks will report "undriven output connection."
Multi-Driver Conflict — Two Instances Driving the Same Net
ELABORATION / SIMULATIONSignal constantly shows X in simulation regardless of what either driver produces. Simulator may print a "multi-driven net" warning that was overlooked during integration.
Two module output ports are connected to the same logic signal. In SystemVerilog, a logic net with multiple drivers resolves to X when drivers disagree.
logic [3:0] shared_result;
adder_4bit u_add_a (
// ...
.sum(shared_result) // driver 1
);
adder_4bit u_add_b (
// ...
.sum(shared_result) // driver 2 ❌
);
// shared_result = X when add_a ≠ add_blogic [3:0] result_a, result_b;
adder_4bit u_add_a (
// ...
.sum(result_a) // ✅ unique net
);
adder_4bit u_add_b (
// ...
.sum(result_b) // ✅ unique net
);
// Mux downstream:
assign out = sel ? result_b : result_a;Each module output must drive exactly one unique internal net. Use a MUX (assign out = sel ? a : b) or arbitration logic to combine multiple results — never connect two output ports to the same wire.
Duplicate Instance Name — Elaboration Fails with Cryptic Error
ELABORATION ERRORSimulator aborts during elaboration with an error like "identifier u_adder already declared" or "hierarchical name conflict". The design never reaches simulation.
Two instances inside the same parent module share the same instance name. Copy-paste of an instantiation block without changing the instance name is the most frequent cause.
// Copy-pasted second instance, forgot to rename
adder_4bit u_adder (
.a(opa), .b(opb), /* ... */
);
adder_4bit u_adder ( // ❌ same name!
.a(opc), .b(opd), /* ... */
);
// ERROR: elaboration failsadder_4bit u_adder_lo (
.a(opa), .b(opb), /* ... */
);
adder_4bit u_adder_hi ( // ✅ unique
.a(opc), .b(opd), /* ... */
);
// Elaboration succeeds.
// Names are descriptive too: lo/hiPrevention: use a search-for-duplicate-names lint check. In large integration files with 50+ instances, visually spotting duplicates is unreliable. Most lint tools (Spyglass, Ascent) catch this immediately.
Floating Input Port — Z Drives Register, Clock, or Control Logic
SILENT FUNCTIONAL BUGControl signals behave non-deterministically. A feature is sometimes active, sometimes not. The bug disappears when you add debug probes (classic Heisenbug). Sometimes the circuit works at slow clock speeds but fails at speed.
An input port of a module was not connected during instantiation. In simulation, unconnected inputs default to Z (high-impedance). If a register, clock enable, or reset logic feeds on this port, behaviour is undefined. In synthesis, the undriven input gets tied to 0 — which may differ from what the simulation showed.
// Imagine a parameterized multiplier with an enable port
module mult_8bit (
input logic clk, rst_n, enable,
input logic [7:0] a, b,
output logic [15:0] product
);
// ════ BUGGY instantiation — 'enable' port not connected ══════════
mult_8bit u_mult (
.clk (sys_clk),
.rst_n (sys_rst_n),
// .enable — OMITTED! Port receives Z in simulation
.a (operand_a),
.b (operand_b),
.product(result)
);
// In simulation: enable = Z → non-deterministic multiplier
// In synthesis : enable tied to 0 → multiplier never fires
// Simulation PASSES, silicon FAILS — classic sim-vs-silicon gap// ════ FIXED — all ports explicitly connected ══════════════════════
mult_8bit u_mult (
.clk (sys_clk),
.rst_n (sys_rst_n),
.enable (mult_enable), // ✅ every input wired
.a (operand_a),
.b (operand_b),
.product(result)
);Key insight: simulation and synthesis treat unconnected inputs differently. Simulator: Z (high-impedance, unpredictable). Synthesis tool: ties to 0 (logic constant). This divergence is one of the most dangerous sim-vs-silicon gap sources. Your simulation passes, your silicon fails.
Tool tip: run Spyglass or CDC checks with the rule NoUnconnectedInPort on every RTL integration. This should be a mandatory signoff gate — not optional lint.
Waveform & Simulation Thinking — How Modules Behave in Time
Understanding what a module's waveform looks like — and why — is the difference between a verification engineer who debugs fast and one who guesses. This section teaches you to read a module's simulation output with the same fluency as reading its code.
Combinational Module — Zero-Delay Evaluation
A purely combinational module like adder_4bit has no registered state. Its outputs respond to input changes within the same simulation time step (delta cycle). The waveform below shows what you should expect when running the testbench from earlier.
Time a[3:0] b[3:0] cin sum[3:0] cout Notes
────────────────────────────────────────────────────────────────────────
0 ns XXXX XXXX X XXXX X Before stimulus — all logic uninitialized
0 ns 4'd3 4'd5 0 4'd8 0 Inputs set → outputs update at Δ (same ns)
10 ns 4'd9 4'd8 0 4'd1 1 9+8=17 → sum=1 (LSB 4 bits), cout=1
20 ns 4'd15 4'd15 1 4'd15 1 15+15+1=31 → sum=15 (0b1111), cout=1
────────────────────────────────────────────────────────────────────────
Key: X=Unknown 0=Logic-0 1=Logic-1 Δ=delta cycle (within same ns)What X Propagation Looks Like in the Waveform
When a module's input is undriven (floating) or a driver produces X, the X value propagates through all dependent logic. This is one of the most important waveform patterns to recognize immediately.
Time enable a[7:0] b[7:0] product[15:0] Notes
──────────────────────────────────────────────────────────────────────────────
0 ns Z 8'hAB 8'hCD XXXX_XXXX_XXXX enable=Z (unconnected) → product=X
10 ns Z 8'h01 8'h01 XXXX_XXXX_XXXX Despite simple inputs, X persists
20 ns Z 8'h00 8'h00 XXXX_XXXX_XXXX Even 0*0 gives X — X is contagious
──────────────────────────────────────────────────────────────────────────────
Z = High-impedance (undriven) X = Unknown (propagated from Z enable)
In synthesis: enable tied to 0 → product always 0. Sim says X, silicon says 0.Hierarchy in Waveform Viewers
In any waveform viewer (DVE, Verdi, SimVision, GTKWave), the module hierarchy maps directly to the signal browser tree on the left. Understanding the hierarchy in the waveform viewer is essential for targeted debug.
Verification Patterns — How Modules Are Used in Testbenches
A testbench is itself a module — the top of the simulation hierarchy. Understanding common testbench patterns will prepare you for UVM and professional verification environments.
Pattern 1 — Self-Checking Testbench with Reference Model
A self-checking testbench computes the expected result independently and compares it against the DUT output automatically. This is essential for regression — you cannot manually check results when running thousands of random test vectors.
module tb_adder_self_check;
logic [3:0] a, b, sum;
logic cin, cout;
int pass_count = 0;
int fail_count = 0;
// ── DUT instance ──────────────────────────────────────────────
adder_4bit u_dut (
.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout)
);
// ── Reference model (pure SW, always correct) ─────────────────
function automatic void check_result (
input logic [3:0] exp_a, exp_b,
input logic exp_cin
);
logic [4:0] expected = exp_a + exp_b + exp_cin;
logic [3:0] exp_sum = expected[3:0];
logic exp_cout = expected[4];
if (sum === exp_sum && cout === exp_cout) begin
pass_count++;
end else begin
fail_count++;
$display("FAIL: a=%0d b=%0d cin=%0b → sum=%0d(exp %0d) cout=%0b(exp %0b)",
exp_a, exp_b, exp_cin, sum, exp_sum, cout, exp_cout);
end
endfunction
// ── Stimulus + automatic checking ─────────────────────────────
initial begin
// Directed tests
a = 4'd3; b = 4'd5; cin = 0; #1; check_result(a, b, cin);
a = 4'd9; b = 4'd8; cin = 0; #1; check_result(a, b, cin);
a = 4'd15; b = 4'd15; cin = 1; #1; check_result(a, b, cin);
// Random tests — 200 random vectors
repeat(200) begin
a = $urandom_range(0, 15);
b = $urandom_range(0, 15);
cin = $urandom_range(0, 1);
#1;
check_result(a, b, cin);
end
$display("─── Results: PASS=%0d FAIL=%0d ───", pass_count, fail_count);
$finish;
end
endmodulePattern 2 — Clock Generator Module
In a real project testbench, the clock generator is often factored into its own module (or task). This demonstrates how a testbench module's own internal logic — including always blocks — co-exists with the DUT instance.
// Imaginary registered accumulator DUT
module tb_accum;
parameter int CLK_PERIOD = 10; // 10 ns → 100 MHz
// ── Testbench signals ──────────────────────────────────────────
logic clk = 0; // initialise to avoid X at t=0
logic rst_n = 0;
logic [7:0] data_in;
logic valid_in;
logic [15:0] accum_out;
logic valid_out;
// ── Clock generator — always block inside TB module ────────────
always #(CLK_PERIOD/2) clk = ~clk;
// ── DUT instantiation ──────────────────────────────────────────
accumulator u_dut (
.clk (clk),
.rst_n (rst_n),
.data_in (data_in),
.valid_in (valid_in),
.accum_out(accum_out),
.valid_out(valid_out)
);
// ── Reset sequence ─────────────────────────────────────────────
initial begin
rst_n = 0;
data_in = 8'd0;
valid_in = 0;
repeat(4) @(posedge clk); // hold reset for 4 cycles
@(negedge clk); rst_n = 1; // deassert at negedge to avoid race
// ── Apply stimulus after reset ────────────────────────────
repeat(2) @(posedge clk);
@(negedge clk);
data_in = 8'd10; valid_in = 1;
repeat(5) @(posedge clk);
@(negedge clk);
data_in = 8'd20;
repeat(3) @(posedge clk);
valid_in = 0;
repeat(10) @(posedge clk);
$display("Accum result: %0d", accum_out);
$finish;
end
endmoduleQuick Reference — Module Syntax at a Glance
Bookmark this. Every syntax you need for this page in one place.
// ── Define a module (ANSI style) ──────────────────────────────
module module_name (
input logic [W-1:0] port_in,
output logic [W-1:0] port_out
);
// internal signals and logic here
endmodule
// ── Instantiate a module (named connections) ──────────────────
module_name instance_name (
.port_in (signal_a),
.port_out (signal_b)
);
// ── Leave an output intentionally unconnected ─────────────────
module_name u_inst (
.port_in (signal_a),
.port_out () // empty () = intentionally unconnected
);
// ── Testbench module (no ports) ───────────────────────────────
module tb_module_name;
// declare signals, instantiate DUT, write stimulus
endmodule
// ── endmodule with optional label (good practice) ─────────────
endmodule : module_nameInterview Q&A — Module Definition & Instantiation
These questions span the full spectrum from entry-level to principal engineer. A strong candidate answers the beginner questions fluently without thinking, and can articulate the advanced ones from real experience.
A module definition is the blueprint — it describes the ports, internal signals, and logic but does not create any hardware by itself. A module instance is a physical copy placed inside another module with a unique name and specific port connections. You can have one definition and hundreds of instances. Analogy: a blueprint for a house vs. the actual houses built from it.