SystemVerilog · Module 7
Named & Positional Port Connections
Three connection styles — positional, named, and .* — with width/type mismatch handling, inout patterns, 6 debugging labs, and refactoring guidance.
Module 7 · Page 7.3
When you instantiate a module you must connect its ports to signals. SystemVerilog gives you three ways to do this — positional, named .port(signal), and implicit .* — and each has a very different safety profile. This page covers every connection style, what the elaborator actually does, width/sign/type mismatches at the port boundary, inout net rules, six real bugs from tape-out post-mortems, the refactor playbook, and ten interview-ready questions.
Three Ways to Connect Ports
Every time you write an instantiation, you must tell the simulator which signal in the parent connects to which port of the child module. SystemVerilog provides three connection styles:
- Positional — signals listed in the same order as the module's port list. Fast to type. Dangerous — order is invisible at a glance.
- Named
.port(signal)— each port named explicitly. Order does not matter. Recommended for all production code — self-documenting and refactoring-safe. - Implicit
.*— connects ports whose name exactly matches a signal in the parent. Fastest to write. Requires strict naming discipline.
Positional Connections
In positional style, you list the signals inside the parentheses in the exact same order as the ports appear in the module definition. No port names are written. The first signal connects to the first port, the second to the second, and so on.
// The module being instantiated:
// module adder_4bit (input logic a, b, cin, output logic sum, cout);
// ① ② ③ ④ ⑤
// Positional instantiation — signal order must match port order
adder_4bit u_add (
operand_a, // ① connects to: a
operand_b, // ② connects to: b
carry_in, // ③ connects to: cin
result, // ④ connects to: sum
carry_out // ⑤ connects to: cout
);The Hidden Danger of Positional Connections
Positional connections look clean, but they carry a serious risk: if anyone reorders or adds a port to the module definition, every instantiation silently rewires itself — often without any compiler error. The design simulates, but produces wrong results.
Named Connections — .port(signal)
In named style, each connection is written as .port_name(signal_name). The dot and port name come from the module definition; the signal name inside the parentheses comes from the parent module. Order is irrelevant — you can list ports in any sequence you like.
// ── Standard named connections ─────────────────────────────────
adder_4bit u_add (
.a (operand_a), // port 'a' ← signal 'operand_a'
.b (operand_b), // port 'b' ← signal 'operand_b'
.cin (carry_in), // port 'cin' ← signal 'carry_in'
.sum (result), // port 'sum' ← signal 'result'
.cout(carry_out) // port 'cout'← signal 'carry_out'
);
// ── Order doesn't matter — this is identical ───────────────────
adder_4bit u_add2 (
.cout(carry_out), // outputs listed first — totally fine
.sum (result),
.cin (carry_in),
.b (operand_b),
.a (operand_a)
);
// ── Port name = signal name? You can write .port(port) ─────────
// Or even shorter — named connection where names match:
adder_4bit u_add3 (
.a (a), // port 'a' connected to signal 'a' in parent
.b (b),
.cin (cin),
.sum (sum),
.cout(cout)
);
// ── Connecting a constant or expression directly ────────────────
adder_4bit u_add4 (
.a (operand_a),
.b (operand_b),
.cin (1'b0), // ← constant tied to a port
.sum (result),
.cout() // ← intentionally unconnected (empty parens)
);Why Named Connections Are Safe
Named connections are unaffected by new ports being added. The compiler warns about the unconnected new port — you fix it immediately instead of spending days debugging wrong results.
Implicit .* Connections
SystemVerilog's .* (dot-star) connection is the most compact style. When you write (.*), the compiler automatically connects every port to a signal in the parent module that has exactly the same name. No port names are written at all.
module top;
// Signals named IDENTICALLY to the module's ports
logic [3:0] a, b, sum;
logic cin, cout;
// .* connects every port to a same-named signal automatically
// adder_4bit has ports: a, b, cin, sum, cout
adder_4bit u_add (.*); // one token — zero manual wiring!
// ── Mixing .* with explicit overrides ──────────────────────
// You can override specific ports while .* handles the rest
logic [3:0] alt_b;
adder_4bit u_add2 (
.*, // connect a, cin, sum, cout by name
.b(alt_b) // override: connect b to alt_b instead
);
endmoduleWhen .* Is a Good Choice
module tb_counter;
// Declare signals with the EXACT same names as the DUT's ports
logic clk = 0;
logic rst_n;
logic en;
logic [7:0] count; // output port 'count'
logic is_zero; // output port 'is_zero'
// DUT: counter_8bit has ports: clk, rst_n, en, count, is_zero
counter_8bit u_dut (.*); // all five connected in one token
always #5 clk = ~clk;
initial begin
rst_n = 0; en = 0;
@(posedge clk); rst_n = 1; en = 1;
repeat(20) @(posedge clk);
$finish;
end
endmoduleHow the Elaborator Resolves .*
Understanding .* deeply requires understanding what the elaborator does — not the parser. Parsing only tokenises; elaboration is the stage where the simulator builds the hierarchical netlist, resolves names, and wires ports. Here is what really happens, in order, every time it sees .*:
- Collect ports. The elaborator looks at the instantiated module's port list — every input, output, and inout.
- Search the enclosing scope. For each port name, it searches the immediately enclosing scope (the parent module, generate block, or begin–end block where the instance lives) for an identifier with an exact match — same name, same case. It does not walk up the hierarchy.
- Type compatibility check. The found signal must be width- and type-compatible. A 4-bit port connecting to an 8-bit signal will silently truncate (covered below) — no error.
- Skip overridden ports. Any port explicitly listed with
.port(sig)in the same instance is skipped — explicit overrides win over.*. - Warn on unmatched ports. A port with no matching parent signal is left unconnected — most tools emit a warning, not an error. Read your warnings.
Leaving Ports Unconnected — The Right Way
Sometimes you genuinely do not need to connect a port. For example, you might not care about the carry-out of an adder, or an enable input should always be tied high. SystemVerilog gives you clean ways to handle both cases.
// ── Empty parentheses = intentionally unconnected output ───────
adder_4bit u_add (
.a (operand_a),
.b (operand_b),
.cin (1'b0), // tie carry-in LOW with a constant
.sum (result),
.cout() // empty () = "I know it's unconnected, intentional"
);
// ── Tying inputs to constants ───────────────────────────────────
uart_tx u_uart (
.clk (sys_clk),
.rst_n (rst_n),
.en (1'b1), // always enabled — tied HIGH
.data (tx_data),
.baud (2'b00), // fixed baud rate — tied to constant
.tx_out (serial_out),
.busy () // don't care about busy flag — unconnected
);
// ── What NOT to do ─────────────────────────────────────────────
// Simply omitting a port in named style also leaves it unconnected,
// but gives no documentation of intent — harder to review:
adder_4bit u_add_bad (
.a (operand_a),
.b (operand_b),
.cin (1'b0),
.sum (result)
// cout omitted — compiler warns, reader wonders if it was a mistake
);| Situation | How to Handle It | What Happens |
|---|---|---|
| Output port you don't need | .cout() — empty parens | Port floats — value computed but discarded. Simulator may warn if the port is used in the child. |
| Input port always HIGH | .en(1'b1) | Constant tied to port. Synthesis optimises the logic accordingly. |
| Input port always LOW | .cin(1'b0) or .cin('0) | Zero tied to port. Clean and explicit. |
| Input port — wide zero bus | .data('0) | '0 auto-sizes to match port width. |
| Input port left disconnected | Avoid — input floats to X | Undefined behaviour. The module reads X on that input every cycle. |
Comparison — Choosing the Right Style
| Criterion | Positional | Named .port(sig) | Implicit .* |
|---|---|---|---|
| Syntax length | Shortest | Longest | One token |
| Order matters? | Yes — strictly | No | No (by name) |
| Port visible in code? | No — hidden | Yes — explicit | No — implicit |
| Safe if ports change? | No — silent rewire | Yes — compiler warns | Mostly — warns on new port with no match |
| Refactor-safe? | No | Yes | Only if signal names match |
| Self-documenting? | No | Yes — very clear | No — reader must look up ports |
| Can mix with others? | No — must be all positional | Can mix with .* | Can mix with .port(sig) |
| Best for | Quick throwaway testbenches only | All production RTL and verification | Testbenches where port=signal names align |
Complete Working Example — All Three Styles
Here is a full file showing all three connection styles on the same module, so you can compare them side by side in a real simulator.
// ── The DUT: simple half-adder ──────────────────────────────────
module half_adder (
input logic a, b,
output logic sum, cout
);
assign sum = a ^ b;
assign cout = a & b;
endmodule
// ── Testbench comparing all three styles ────────────────────────
module tb_connection_styles;
// Signals for named & positional instances
logic p, q, s_pos, c_pos; // for positional
logic x, y, s_nam, c_nam; // for named
// Signals named to MATCH half_adder ports (for .* instance)
logic a, b, sum, cout; // for implicit .*
// ── ❶ Positional ──────────────────────────────────────────────
half_adder u_pos (p, q, s_pos, c_pos);
// ── ❷ Named .port(signal) ─────────────────────────────────────
half_adder u_named (.a(x), .b(y), .sum(s_nam), .cout(c_nam));
// ── ❸ Implicit .* ─────────────────────────────────────────────
half_adder u_impl (.*); // a,b,sum,cout connected by name
// ── Apply the same stimulus to all three ──────────────────────
initial begin
$monitor("p=%b q=%b | pos: s=%b c=%b | named: s=%b c=%b | impl: s=%b c=%b",
p, q, s_pos, c_pos, s_nam, c_nam, sum, cout);
{p,x,a} = 3'b000; {q,y,b} = 3'b000; #10;
{p,x,a} = 3'b111; {q,y,b} = 3'b000; #10;
{p,x,a} = 3'b000; {q,y,b} = 3'b111; #10;
{p,x,a} = 3'b111; {q,y,b} = 3'b111; #10;
$finish;
end
endmoduleCommon Mistakes — Wrong vs Correct
half_adder u_mix (
a, // positional
.b(y) // ← NAMED — illegal!
);
// Error: cannot mix positional
// and named port connections.half_adder u_fix (
.a(x),
.b(y),
.sum (s),
.cout(c)
);logic operand_a, operand_b;
logic result, carry;
// half_adder ports: a, b, sum, cout
half_adder u_bad (.*);
// Error: no match for a, b, sum, cout
// — operand_a ≠ alogic a, b; // matches port 'a', 'b'
logic sum, cout; // matches port 'sum', 'cout'
half_adder u_good (.*);
// All four ports connected — clean ✅adder_4bit u_bad (
.a (data_a),
.b (data_b),
// .cin not connected!
// cin floats to X
.sum (result),
.cout(co)
);adder_4bit u_good (
.a (data_a),
.b (data_b),
.cin (1'b0), // tied to 0 — explicit
.sum (result),
.cout() // output: intentionally unconnected
);Quick Reference — Connection Syntax Cheat Sheet
// ── ❶ Positional — signal order must match port order ──────────
mod_name inst_name (sig1, sig2, sig3);
// ── ❷ Named — .port(signal) — RECOMMENDED for all RTL ──────────
mod_name inst_name (
.port1(signal_a),
.port2(signal_b),
.port3(1'b0), // tie input to constant
.port4() // intentionally unconnected output
);
// ── ❸ Implicit .* — connect every port by name match ───────────
mod_name inst_name (.*);
// ── ❸b Mix .* with explicit overrides ──────────────────────────
mod_name inst_name (.*, .port2(different_signal));
// ── Cannot mix: positional + named ─────────────────────────────
// mod_name inst_name (sig1, .port2(sig2)); ← ILLEGAL
// ── Tie to constants ────────────────────────────────────────────
// .en (1'b1) tie HIGH
// .sel (1'b0) tie LOW
// .data ('0) tie whole bus to zero
// .data ('1) tie whole bus to ones
// .out () output — intentionally unconnectedWidth, Sign & Type Mismatches at the Port Boundary
One of the most overlooked port-connection bugs has nothing to do with named vs positional style — it has to do with width. SystemVerilog's LRM is permissive at module boundaries: a wider signal connected to a narrower port is silently truncated, and a narrower signal connected to a wider port is silently zero-extended (or sign-extended, depending on the type). No error. No warning, on many tool versions. Just wrong silicon.
// ── DUT: an 8-bit register file write port ─────────────────────
module reg_file (
input logic clk,
input logic we,
input logic [2:0] waddr,
input logic [7:0] wdata // ← 8 bits
);
logic [7:0] mem [8];
always_ff @(posedge clk) if (we) mem[waddr] <= wdata;
endmodule
// ── Parent: somebody widens the data bus to 16 bits ────────────
module cpu_core;
logic clk, we;
logic [2:0] waddr;
logic [15:0] alu_result; // ← now 16 bits!
// LRM-legal but DANGEROUS — upper 8 bits silently truncated
reg_file u_rf (
.clk(clk),
.we (we),
.waddr(waddr),
.wdata(alu_result) // ❌ alu_result[15:8] thrown away silently
);
endmodule
// ── The fix: be explicit. The truncation is now a CHOICE ──────
reg_file u_rf_safe (
.clk(clk), .we(we), .waddr(waddr),
.wdata(alu_result[7:0]) // ✅ truncation is documented & intentional
);What the Simulator Does — Truncation & Extension Table
| Port width | Signal width | Signal type | What happens | Compiler diagnostic |
|---|---|---|---|---|
| 8 bits (input) | 16 bits | any | Upper 8 bits silently truncated. Module sees signal[7:0] only. | Often no warning. Strict lint catches it. |
| 16 bits (input) | 8 bits | logic (unsigned) | Zero-extended to 16 bits. Upper byte = 0. | Usually no warning. |
| 16 bits (input) | 8 bits | logic signed | Sign-extended — upper 8 bits replicate bit[7]. Wreaks havoc if you assumed unsigned. | No warning unless signed/unsigned mismatch checks are on. |
| 1 bit (input) | 4 bits | any | Only bit[0] connected. Bits [3:1] discarded. | Most tools warn here — it's a common typo. |
| 4 bits (output) | 4 bits but declared wire on input port | — | Multi-driver if anything else also drives the wire. Hard X bug. | Elaboration-time error if both drivers are continuous. |
Sign-Extension Bug — A Real Trap
// Port is signed 16-bit:
module mac(input logic signed [15:0] op, ...);
// Parent signal is unsigned 8-bit:
logic [7:0] data; // 0xFF intended as 255
mac u_m (.op(data));
// At port boundary:
// data=8'hFF → sign-extended →
// op = 16'hFFFF (= -1, NOT 255!) ❌// Cast clearly says "treat as unsigned":
mac u_m (
.op(16'(unsigned'(data)))
);
// op = 16'h00FF = 255 ✅
// Or even more explicit:
mac u_m2 (
.op({8'b0, data})
);
// Zero-extension is now in the source ✅Bidirectional (inout) Connection Patterns
inout ports are the most error-prone connection in SystemVerilog. They must be driven by a net (typically wire or tri) on both sides of the boundary — never a logic variable in the parent. Getting this wrong gives elaboration errors at best, and silent X-propagation at worst.
// ── A tri-state I/O pad model ──────────────────────────────────
module tristate_pad (
inout wire pad, // ← MUST be a net, not logic
input logic drive_en,
input logic drive_val,
output logic rx_val
);
assign pad = drive_en ? drive_val : 1'bz; // tri-state
assign rx_val = pad;
endmodule
// ── ❌ WRONG: parent uses 'logic' for the inout net ────────────
module top_bad;
logic bus_net; // ← variable! cannot drive an inout
logic en, dval, rval;
tristate_pad u_pad (
.pad(bus_net), // ❌ elaboration error in most tools
.drive_en(en), .drive_val(dval), .rx_val(rval)
);
endmodule
// ── ✅ CORRECT: declare the shared bus as a wire/tri ───────────
module top_good;
wire bus_net; // ✅ net — can be driven by multiple sources
logic en, dval, rval;
// Optional weak pull-up so bus is not X when nobody drives
pullup (bus_net); // strength 'pull' on bus_net
tristate_pad u_pad (
.pad(bus_net),
.drive_en(en), .drive_val(dval), .rx_val(rval)
);
endmoduleVerification Engineer Patterns — DUT ↔ TB Connections
In a real UVM/SV verification environment, port connections are made in three layers, and each layer has a preferred connection style. Knowing the pattern saves hours of hierarchical-path debugging later.
The Three Layers
- Layer 1 — Pin-Level (DUT instance). The DUT is instantiated once at the top of the testbench. Always named. Almost always uses a SystemVerilog
interfaceto bundle ports cleanly. - Layer 2 — Interface Binding. The interface is bound to the agent through a virtual interface handle. Connection happens via
uvm_config_db::set/get— not at instantiation. - Layer 3 — Driver / Monitor. Drivers/monitors talk to DUT signals through the virtual interface — no port connections at all. This is the power of interface-based verification.
// ── ❶ The interface bundles the DUT-facing signals ────────────
interface axi_if (input logic aclk, aresetn);
logic [31:0] awaddr;
logic awvalid, awready;
logic [31:0] wdata;
logic wvalid, wready;
// ... plus modports for master/slave/monitor
endinterface
// ── ❷ Top-level TB instantiates DUT with NAMED connections ────
module tb_top;
logic aclk = 0, aresetn;
always #5 aclk = ~aclk;
// One interface instance per AXI port
axi_if m_if (aclk, aresetn);
// DUT instantiation — NAMED, with interface signals on the right
dma_engine u_dut (
.aclk (aclk),
.aresetn (aresetn),
.m_awaddr (m_if.awaddr),
.m_awvalid(m_if.awvalid),
.m_awready(m_if.awready),
.m_wdata (m_if.wdata),
.m_wvalid (m_if.wvalid),
.m_wready (m_if.wready)
);
// ❸ Publish the virtual interface so UVM driver/monitor can grab it
initial begin
uvm_config_db#(virtual axi_if)::set(null, "uvm_test_top.env.agent*",
"vif", m_if);
run_test("axi_smoke_test");
end
endmodule.* in a Clean Verification Wrapper
.* shines when you write thin pass-through wrappers — a very common verification pattern where a DUT is wrapped in a debug shell that just forwards every signal. With .*, adding a new DUT port requires touching exactly two places: the DUT and the wrapper's signal list.
module dut_with_probes (
// SAME port list as the DUT — wrapper is transparent
input logic clk, rst_n, en,
input logic [31:0] data_in,
output logic [31:0] data_out, status
);
// ← .* connects every signal whose name matches — fast & safe
dut u_dut (.*);
// Add SVA / coverage / debug probes here — independent of port list
assert property (@(posedge clk) disable iff (!rst_n)
en |-> ##[1:5] status[0])
else $error("DUT did not ack within 5 cycles");
endmoduleDebugging Academy — 6 Real Port Connection Bugs
These are the bugs you will actually meet in a real RTL or DV project — pulled from tape-out post-mortems, FPGA bring-up logs, and verification regression triages. For each, study the symptom and the root cause until you can spot the pattern in your own code review.
Positional Reorder Silent Break
SILENT REWIRE// DUT modified — clk_en added at pos 3
module fifo(
input clk, rst_n,
input clk_en, // NEW
input [7:0] threshold);
// Parent left unchanged
fifo u_f(clk, rst, th_val);
// th_val drives clk_en! ❌
// threshold floats unconnectedAfter a clean RTL release, an unrelated block (FIFO depth programmability) starts misbehaving in regression. Waveform shows the FIFO's threshold input driven by the wrong value.
Positional connections silently shifted by one position when the new port was inserted. The simulator accepts the new wiring as legal — no compile error, no warning.
fifo u_f(
.clk (clk),
.rst_n (rst_n),
.clk_en (1'b1), // tied HIGH
.threshold(th_val) // correct port
);
// Now order-independent ✅Ban positional connections in CI via a lint check (Spyglass ConnectByName).
Silent Width Truncation
WIDTH TRUNCATIONlogic [31:0] alu_out;
// reg_file.wdata is only 16 bits
reg_file u_rf (
.wdata(alu_out) // upper 16 lost!
);
// No error. No warning by default.ALU computes 0x1_2345 but the register file shows 0x2345. Random-looking. Only fails on values greater than 16 bits.
SystemVerilog allows a wider signal to be connected to a narrower port; the upper bits are silently discarded at the port boundary with no diagnostic.
// Choice #1 — explicit truncate
reg_file u_rf (.wdata(alu_out[15:0]));
// Choice #2 — widen the port
// (preferred — keeps data integrity)
module reg_file(
input [31:0] wdata);Enable Verilator -Wall -Werror or VCS +lint=PCWM-L to make width mismatches blocking.
Inout Driven by `logic` Variable
INOUT NETlogic sda; // variable
logic scl;
i2c_master u_m (
.sda(sda), // ❌ X on bus
.scl(scl)
);
i2c_slave u_s (.sda(sda),
.scl(scl));Elaboration error in VCS: "Net type required for inout connection 'sda', got variable". Some tools accept this silently and the bus stays at X for the entire simulation.
inout requires a net (typically wire) because nets support multiple drivers and resolve by strength. A logic variable has exactly one driver and no resolution function, so it cannot represent a shared bus.
wire sda, scl; // nets ✅
pullup(sda); pullup(scl);
i2c_master u_m (.sda(sda),
.scl(scl));
i2c_slave u_s (.sda(sda),
.scl(scl));Always declare bidirectional shared buses as wire (or tri) with appropriate pullup/pulldown strength.
`.*` Picking the Wrong Scope
SCOPE SHADOWINGlogic [7:0] a; // parent
logic [7:0] b [4];
generate for(genvar i=0; i<4; i++) begin : g
logic [7:0] a; // shadows parent!
assign a = b[i];
cmp u(.*); // uses g.a not top.a!
end endgenerateA generate-array of comparators behaves correctly for instance [0] but garbage for [1..N-1]. Waveforms show all instances reading the same a input from instance [0].
Inside a generate block, .* searches the genvar-iterated scope as well as the parent. The generate-local a shadows the parent a, and the elaborator binds the inner-scope name silently — no warning on most simulators.
generate for(genvar i=0; i<4; i++)
begin : g
cmp u (
.a(b[i]), // explicit ✅
.b(b[i+1])
);
end endgenerate
// No name shadowing risk ✅Defensive rule: never use .* inside generate blocks unless you have audited every name in both scopes.
Unconnected Input Floating to X
FLOATING INPUTctrl u_c (
.clk (clk),
.rst_n(rst_n),
// .mode_sel omitted!
.out (dout)
);
// Warning ignored at compile.
// mode_sel = X → every
// case-statement → X branch.The entire downstream pipeline is X from cycle 0. Reset doesn't help. The block's internal state goes X within one clock.
An unconnected input port floats to X. Every case statement reading that input falls through to the default (X) branch, poisoning all dependent logic.
ctrl u_c (
.clk (clk),
.rst_n (rst_n),
.mode_sel(2'b00), // chosen mode
.out (dout)
);
// Promote "port unconnected" to ERROR
// in CI — never let it ship.Promote "port unconnected" lint warnings (Spyglass W287, Synopsys VC UndrivenInTerm) to errors in CI.
Mixing `wire` and `logic` at the Connection
IMPLICIT WIRE// status not declared in parent
block_a u_a(.out(status));
block_b u_b(.out(status));
// Verilog auto-declares 'status'
// as a 1-bit wire (`default_nettype
// wire is on). Both blocks now drive
// the same net → X on conflict
// or merged in synthesis. 🐛Elaboration succeeds. Simulation runs. RTL-level results match the testbench. But synthesis silently drops a continuous-assign because two parallel drivers tied to the same implicit net are merged.
With `default_nettype wire (the legacy default), any undeclared identifier used at a port connection becomes an implicit 1-bit wire. Two instances driving the same name produce a multi-driver net — undefined in RTL, silently merged in synthesis.
`default_nettype none
// Force every signal to be declared.
logic status_a, status_b;
block_a u_a(.out(status_a));
block_b u_b(.out(status_b));
// One net per driver ✅
// Catches typos at compile.Put `default_nettype none as the first line of every .sv file via a template or pre-commit hook.
Senior Engineer Insights & Best Practices
- 🚀 RTL Insight. Treat your port list as a versioned API. Adding, reordering, or renaming a port is a breaking change to every parent. Bump a version comment at the top of the file and announce it on the team channel.
- ⚠️ Common Mistake. Engineers use positional in "just a small instantiation" and tell themselves they will fix it later. They never do. By the time the design has 50 instantiations, refactor cost is prohibitive — and silent bugs are inevitable.
- 🔍 Verification Tip. In UVM testbenches, prefer interfaces + virtual interfaces over long named port lists. It collapses hundreds of connections to one
.vif(vif)and refactors cleanly. - 🏗 Synthesis Concern. Unconnected outputs synthesise to optimised-away logic — fine. Unconnected inputs can drive synthesis to insert pull-low cells or X-propagation gates, ballooning area and creating CDC false-positives.
- ⏱ Timing Analysis. Wide buses tied to constants (
.data('0)) are optimised at synthesis and produce zero-timing-paths. This is the cleanest way to disable a feature block without ifdef-storming the RTL. - 📜 LRM Reality. IEEE 1800-2017 §23.3.2 — port connections are evaluated at elaboration time, not runtime. That is why you cannot use a runtime variable to pick which signal drives a port — only
generate+ parameter expressions, evaluated before time 0.
Team-Wide Rules That Save Tape-Outs
| Rule | Why it matters | How to enforce |
|---|---|---|
| No positional connections in RTL | Eliminates silent reorder bugs forever. | Lint rule (Spyglass ConnectByName); reject at CI. |
`default_nettype none at every file top | Forces every signal to be declared — catches typos. | Add as the first line of every .sv via template / git pre-commit hook. |
| Width-mismatch warnings → errors | Stops silent truncation/extension at the source. | Verilator -Wall -Werror; VCS +lint=PCWM-L. |
| Unconnected input port → error | Stops floating X bugs from reaching simulation. | Spyglass W287; Synopsys VC UndrivenInTerm. |
Ban .* inside generate blocks | Avoids the scope-shadowing trap (Bug #4). | Team coding-style document; PR review checklist. |
| Interfaces for any bus with more than 8 signals | Reduces refactor cost, eliminates connection mistakes. | Code review: long port lists must be justified. |
Refactoring Legacy Positional → Named (Industrial Approach)
You inherit a codebase with thousands of positional instantiations. You cannot rewrite them all by hand. Here is the pragmatic, low-risk approach used by real teams in IP houses and SoC groups.
- Lock the design. Tag the current commit. Run full regression to get a known-good baseline (lint + sim + synth + equivalence-check report).
- Identify hot modules. Use
grep/ripgrepto find the most-instantiated modules (highest blast radius if their port list ever changes). Refactor those first. - Script the conversion. Tools like
verible-verilog-format, Pyverilog, or in-house Perl scripts can re-emit positional instantiations as named by parsing the module definition and the instantiation site. - Run formal equivalence (LEC). After the refactor, run Synopsys Formality or Cadence Conformal against the baseline RTL. If LEC passes, the refactor is guaranteed-equivalent — no functional risk.
- Land in small chunks. One module per PR. Easy to revert, easy to review, easy to bisect if a downstream tool chokes.
- Lock it in. Add the lint rule banning positional connections. Past that day, no new positional instantiation can be merged.
# Find positional instantiations: open-paren followed by
# an identifier (NOT a dot) — heuristic but catches 95% of cases.
rg -t verilog -nP '\b\w+\s+\w+\s*\(\s*[a-zA-Z_]' rtl/ \
| grep -v '\.\w' \
| awk -F: '{print $1}' \
| sort | uniq -c | sort -rn \
| head -20
# Output (most-frequent files first — refactor these first):
# 34 rtl/cpu/decode_stage.sv
# 27 rtl/mem/cache_ctrl.sv
# 19 rtl/io/uart_top.sv
# ...Interview Q&A — Port Connections
These are the questions asked at the senior-RTL and senior-DV interview loops at Intel, NVIDIA, Qualcomm, AMD, Apple Silicon, and most SoC product companies. Each has a one-liner answer and the longer answer your interviewer wants to hear.
Short: because adding or reordering a port silently re-wires every instantiation with no compiler error.
Long: positional connections rely on the lexical order of the module's port list. Any maintenance change to that order changes hardware behaviour everywhere the module is used. Bugs surface late — usually in regression on an unrelated test, or worse, in silicon — and they are hard to triage because the simulator accepts the new code as legal.