SystemVerilog · Module 7
Port Types — input, output, inout, ref
Direction rules, port data types, tri-state inout, output vs output reg, ref for large structs, and synthesis vs simulation semantics.
Module 7 · Page 7.2
Every port has a direction. Get the direction wrong and your simulation drives X, your synthesis tool errors, and — in the worst case — your silicon shorts a tri-state bus. This page covers all four SystemVerilog port types, their legal data types, simulation and synthesis behaviour, five debugging labs from real chip projects, and a complete tiered interview Q&A.
Why Port Direction Matters
A port is a named connection point on a module — a pin. When you define a port, you must tell the tool which direction data flows through it. The direction determines who is allowed to drive the port and who is allowed to read it.
Get the direction right and the tool enforces it — it flags any code that tries to drive an output from the wrong side. Get it wrong and you get silent bugs: unknown values, multiple drivers, or synthesis mismatches that only show up in silicon.
| Port type | Data flow | Typical use |
|---|---|---|
input | In to the module — parent drives, module reads | Almost every port. Clocks, resets, data buses, control signals. |
output | Out of the module — module drives, parent reads | Status flags, registered results, computed outputs. |
inout | Both ways — bidirectional with tri-state | I2C SDA, JTAG TDO, memory DQ lines, GPIO pads. |
ref | Shared alias to caller's storage | Testbench tasks and functions only. Not synthesisable. |
input — Data Flows In
An input port is read-only from the module's perspective. The parent module (or testbench) drives it; the module body can only read its value. If you try to drive an input port from inside the module, the simulator throws an error.
The default data type for an input port in SystemVerilog is logic. You can also use wire, bit, integer, or any packed type — but logic covers 99% of RTL cases.
module mux_2to1 (
// ── Single-bit inputs ──────────────────────────────────────
input logic sel, // 1-bit: select line
// ── Multi-bit (bus) inputs ─────────────────────────────────
input logic [7:0] data_a, // 8-bit: first data bus
input logic [7:0] data_b, // 8-bit: second data bus
// ── Clock and reset (always inputs, always 1-bit) ──────────
input logic clk, // clock — drives always_ff
input logic rst_n, // active-low reset
output logic [7:0] y
);
// Reading inputs is fine — sel is used to choose data
assign y = sel ? data_a : data_b;
// ERROR: you cannot do this — sel is an input
// assign sel = 1'b1; ← compiler error: cannot drive input port
endmoduleoutput — Data Flows Out
An output port is driven from inside the module. In SystemVerilog, an output logic port can be driven by both assign statements (combinational) and always_ff / always_comb blocks (registered or combinational). The parent module reads the output's value through a connected signal.
output logic vs output reg (legacy)
In old Verilog, you wrote output reg to indicate a registered output, and output wire for a combinational one. SystemVerilog replaces both with output logic — logic can be driven by both procedural blocks and continuous assigns. Always use output logic in new code.
module counter_8bit (
input logic clk, rst_n, en,
// ── Combinational output: driven by assign ─────────────────
output logic is_zero, // high when count == 0
// ── Registered output: driven by always_ff ─────────────────
output logic [7:0] count // 8-bit counter value
);
// Registered output — driven by a flip-flop
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= 8'd0;
else if (en) count <= count + 8'd1;
end
// Combinational output — driven by assign (no flip-flop)
assign is_zero = (count == 8'd0);
endmoduleinout — Bidirectional (Tri-State)
An inout port carries data in both directions on the same wire. This is physically possible only with tri-state logic: a driver that can either drive the bus actively (high or low) or release it into a high-impedance state (Z), letting some other driver on the bus take control.
The classic real-world examples are: I2C (SDA and SCL lines), JTAG (TDO), SPI (MISO/MOSI in some modes), memory data buses (DQ lines), and GPIO pads on a microcontroller.
Writing an inout Port in RTL
// A simple I2C-style SDA pad model
module bidir_pad (
input logic tx_data, // data to send onto bus
input logic tx_en, // 1 = drive the bus, 0 = release (Hi-Z)
output logic rx_data, // data received from bus
inout wire sda // bidirectional I2C data line
);
// Drive sda when tx_en=1, release to Z when tx_en=0
// The ternary drives a real tri-state buffer
assign sda = tx_en ? tx_data : 1'bz;
// Always read the bus value back (regardless of who drives)
assign rx_data = sda;
endmoduleThe key line is assign sda = tx_en ? tx_data : 1'bz;. When tx_en is 1, the module actively drives the bus. When tx_en is 0, it outputs 1'bz — the high-impedance value — effectively disconnecting itself and letting another driver take control.
ref — Pass by Reference (Testbench Only)
ref is a SystemVerilog-only port type, not found in plain Verilog. Instead of copying a value into the port, ref passes a direct alias to the original variable. Both the caller and the module share the exact same memory location.
This matters in testbenches: if your driver task modifies a ref variable, the caller sees the change immediately — no need to return a value or use an output port.
// ref is used in module/task/function arguments — testbench use only
module tb_ref_demo;
int counter; // variable in the calling scope
// Task that uses 'ref' — shares the caller's variable directly
task automatic increment (ref int val, input int amount);
val = val + amount; // modifies the CALLER's variable in-place
endtask
initial begin
counter = 10;
$display("Before: counter = %0d", counter); // 10
increment(counter, 5); // ref passes 'counter' directly
$display("After: counter = %0d", counter); // 15 — changed by the task
end
endmoduleref vs input vs output — What's the Difference?
| Aspect | input | output | inout | ref |
|---|---|---|---|---|
| Direction | Caller → Module | Module → Caller | Both ways | Shared alias (both ways) |
| Data copied? | Yes — value copy | Yes — value copy out | Wire (no copy) | No — direct alias |
| Module can drive? | No — read only | Yes | Yes (tri-state) | Yes |
| Caller sees change? | N/A | Yes, after task/assign | Yes (bus) | Yes — immediately |
| Synthesisable? | Yes | Yes | Yes (with care) | No |
| Where used | RTL + Testbench | RTL + Testbench | RTL IO pads | Testbench / tasks only |
| Default data type | logic | logic | wire | Same as caller's variable |
Port Data Types — What Goes After the Direction
After the direction keyword (input / output / inout), you declare the data type and width. SystemVerilog gives you several choices.
module port_types_demo (
// ── logic: 4-state (0,1,X,Z) — USE THIS for all RTL ────────
input logic clk,
input logic [31:0] data_in,
output logic [31:0] data_out,
// ── wire: used for multi-driver nets and inout ─────────────
inout wire sda,
// ── bit: 2-state (0,1) — no X/Z — testbench stimulus ───────
input bit [7:0] stim_byte,
// ── integer: signed 32-bit — for counts/indices ────────────
input integer loop_count,
// ── Signed ports: for signed arithmetic ────────────────────
input logic signed [15:0] signed_val,
output logic signed [15:0] signed_result,
// ── No type given → defaults to logic ──────────────────────
input implicit_logic // same as: input logic
);
// ... body
endmodule| Type | States | Signed? | Best for |
|---|---|---|---|
logic | 4 (0, 1, X, Z) | Unsigned by default | All RTL signals — the go-to type |
wire | 4 (0, 1, X, Z) | Unsigned | Multi-driver nets, inout, legacy code |
bit | 2 (0, 1 only) | Unsigned by default | Testbench stimulus where X/Z never occurs |
integer | 4-state, 32 bits | Signed | Loop counters, indices in testbenches |
logic signed | 4 (0, 1, X, Z) | Yes — 2's complement | DSP operands, signed arithmetic in RTL |
All Four Port Types in One Complete Example
Here is a realistic GPIO pad model that uses all four port types. Study it to understand how they interact.
module gpio_pad (
// ── input: core-side signals (from the SoC fabric) ─────────
input logic clk, // system clock
input logic rst_n, // active-low reset
input logic oe, // output enable: 1=drive pad, 0=read pad
input logic data_out, // data to drive onto the pad
// ── output: value sampled from the pad ─────────────────────
output logic data_in, // pad value read back into the SoC
// ── inout: the physical pad — bidirectional ────────────────
inout wire pad // connects to the package pin / PCB trace
);
// ── Drive pad when oe=1, release to Hi-Z when oe=0 ─────────
assign pad = oe ? data_out : 1'bz;
// ── Always sample whatever is on the pad ───────────────────
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) data_in <= 1'b0;
else data_in <= pad; // read the live pad value
end
endmodule
// ── Testbench using ref to share test state ────────────────────
module tb_gpio;
logic clk = 0, rst_n, oe, data_out, data_in;
wire pad;
int test_id; // shared via ref in the task below
// Instantiate DUT
gpio_pad u_dut (.*); // .* = connect by name automatically
always #5 clk = ~clk;
// Task uses 'ref' — lets it update test_id in the caller's scope
task automatic run_test (ref int tid, input logic drive_en, drive_val);
tid++; // modifies caller's test_id
oe = drive_en;
data_out = drive_val;
@(posedge clk); #1;
$display("Test %0d: pad=%b data_in=%b", tid, pad, data_in);
endtask
initial begin
rst_n = 0; @(posedge clk); rst_n = 1;
run_test(test_id, 1, 1); // drive pad high
run_test(test_id, 1, 0); // drive pad low
run_test(test_id, 0, 0); // release pad (Hi-Z)
$finish;
end
endmodulePort Direction Rules — What Can Drive What
| Port type | Parent drives? | Module drives? | Module reads? | Hi-Z ok? |
|---|---|---|---|---|
input | Yes | No (error) | Yes | — |
output | No (error) | Yes | Yes | — |
inout | Yes | Yes | Yes | Yes |
ref | Yes | Yes | Yes | N/A |
Tri-State Deep Dive — The Electrical Model
To understand inout correctly you need to understand how a simulator — and the silicon itself — resolves the value on a net that has multiple potential drivers.
Wire Resolution Table
A wire in SystemVerilog follows wired-OR / wired-AND resolution. When two sources drive the same net simultaneously, the simulator combines them using this table:
| Driver A | Driver B | Result on wire | Notes |
|---|---|---|---|
0 | 0 | 0 | Both agree: low |
1 | 1 | 1 | Both agree: high |
0 | Z | 0 | One active, one released |
1 | Z | 1 | One active, one released |
Z | Z | Z | Both released — bus floats |
0 | 1 | X | CONTENTION — voltage fights |
1 | 0 | X | CONTENTION — voltage fights |
X | (any) | X | Unknown propagates |
Tri-State Inside Chip vs at the IO Pad Ring
Modern ASIC and FPGA design practice separates tri-state logic into two distinct domains:
| Location | Tri-State Allowed? | Why |
|---|---|---|
| IO Pad Ring | Yes | Physical pads have built-in tri-state buffers (TBUF cells) |
| Internal Core Logic | Avoid | Internal nets are single-driver; synthesis maps inout badly inside the core |
| FPGA Internal Routing | Not supported | FPGA switch matrix cannot implement Hi-Z inside the fabric; use mux logic instead |
Simulation Behavior — What the Simulator Does with Port Values
Understanding how a simulator initialises and resolves port values prevents hard-to-explain X bugs in your first simulation runs.
Initial Values at Time Zero
| Port Type | Default at t=0 | Why |
|---|---|---|
input | X | Undriven at start — needs testbench to drive |
output | X | Uninitialized register — needs reset |
inout wire | Z | No active driver — bus floats |
ref | X | Alias to variable — inherits its initial value |
Driving an inout Port from a Testbench
module tb_bidir;
logic clk;
logic oe_tb; // testbench output-enable
logic tx_tb; // data testbench wants to send
wire bus; // the shared bidirectional wire
// Tri-state driver from testbench side
// When oe_tb=1 testbench drives, when oe_tb=0 it releases
assign bus = oe_tb ? tx_tb : 1'bz;
// DUT instance
bidir_pad u_dut (
.tx_data (1'b0),
.tx_en (dut_oe),
.rx_data (),
.sda (bus) // connects to same wire
);
initial begin
oe_tb = 1'b0; tx_tb = 1'b0; // start released
#10; oe_tb = 1'b1; tx_tb = 1'b1; // TB drives high
#20; oe_tb = 1'b0; // TB releases
// DUT can now drive bus
end
endmoduleRTL vs Testbench Port Patterns
The same port type is used differently in RTL code and in testbench code. Mixing the patterns creates hard-to-debug simulation failures.
module counter (
input logic clk,
input logic rst_n,
output logic [7:0] count
);
always_ff @(posedge clk)
if (!rst_n) count <= 8'd0;
else count <= count + 8'd1;
endmodulemodule tb;
logic clk, rst_n;
logic [7:0] count; // receives output
counter u_dut (
.clk (clk),
.rst_n(rst_n),
.count(count)
);
// testbench only reads count
initial
$monitor("count=%0d", count);
endmoduleUsing ref for Large Structures in Testbenches
typedef struct {
logic [31:0] addr;
logic [31:0] data;
logic [3:0] strb;
} axi_t;
task check_txn(
input axi_t txn // full copy!
);
assert(txn.strb != 4'h0);
endtasktypedef struct {
logic [31:0] addr;
logic [31:0] data;
logic [3:0] strb;
} axi_t;
task check_txn(
ref axi_t txn // alias, no copy
);
assert(txn.strb != 4'h0);
endtaskDebugging Academy — 5 Real-World Port-Type Bugs
Every bug below has been encountered in actual chip projects or verification environments. Study the root cause — not just the fix — so you can recognise the same pattern when it shows up in a different context.
Bus Contention — Two Drivers Active Simultaneously
CRITICALmodule i2c_master (
inout wire sda,
input logic oe_master,
input logic tx_master
);
assign sda = oe_master ? tx_master : 1'bz;
endmodule
module i2c_slave (
inout wire sda,
input logic oe_slave,
input logic tx_slave
);
assign sda = oe_slave ? tx_slave : 1'bz;
endmodule
module tb;
wire sda;
logic oe_m, tx_m, oe_s, tx_s;
i2c_master u_m (.sda(sda), .oe_master(oe_m), .tx_master(tx_m));
i2c_slave u_s (.sda(sda), .oe_slave(oe_s), .tx_slave(tx_s));
initial begin
oe_m=1; tx_m=1; oe_s=0; tx_s=0;
#10;
// BUG: both OE high at same time!
oe_m=1; oe_s=1; tx_m=1; tx_s=0;
// sda → X (contention)
end
endmoduleThe sda bus reads X during an I2C transaction. All individual signals look correct in isolation. Simulation passes with force but fails without it.
The testbench's timing sequence asserted oe_slave at the same timestep that oe_master was still high. In RTL, the arbitration logic (I2C arbitration detect) must guarantee a minimum gap between one driver releasing and the next asserting its OE. Two drivers active with conflicting values resolves to X — in silicon this is a short circuit.
module tb_fixed;
wire sda;
logic oe_m, tx_m, oe_s, tx_s;
i2c_master u_m (.sda(sda), .oe_master(oe_m), .tx_master(tx_m));
i2c_slave u_s (.sda(sda), .oe_slave(oe_s), .tx_slave(tx_s));
// Assertion: mutual exclusion on bus ownership
property no_contention;
@(posedge clk) not (oe_m && oe_s);
endproperty
assert property(no_contention)
else $fatal(1, "Bus contention: both OE high!");
initial begin
oe_m=1; tx_m=1; oe_s=0;
#10; oe_m=0; // master releases FIRST
#5; oe_s=1; tx_s=0; // then slave drives
end
endmoduleCatch contention in simulation before it reaches silicon, where it means current spikes and potential latch-up.
Missing Hi-Z Release — Bus Permanently Stuck
HIGHalways_ff @(posedge clk) begin
if (!rst_n) begin
oe <= 1'b0;
tx_reg <= 1'b0;
end else if (start_tx) begin
oe <= 1'b1; // assert OE
tx_reg <= data_in;
end
// BUG: no branch to deassert OE when done!
// oe stays high forever after start_tx
endAfter the master finishes sending data, the slave tries to acknowledge but the bus is still being driven. The ACK bit is never seen.
Incomplete FSM transition — the state machine had a path to assert OE but no complementary path to release it. Classic "set without clear" pattern. The OE remains asserted indefinitely after start_tx, blocking any other agent from driving the bus.
always_ff @(posedge clk) begin
if (!rst_n) begin
oe <= 1'b0;
tx_reg <= 1'b0;
end else if (start_tx) begin
oe <= 1'b1;
tx_reg <= data_in;
end else if (tx_done) begin
oe <= 1'b0; // release bus!
end
end
// Complementary assertion
assert property(
@(posedge clk) tx_done |=> !oe
) else $error("OE not released after tx_done");Every OE assert must have an explicit clear. Cover this with a directed test that checks the OE signal explicitly after every transmission sequence.
inout logic Instead of inout wire — Wrong Type for Multi-Driver Net
HIGHmodule bad_bidir (
// BUG: logic is a single-driver type!
inout logic sda,
input logic oe,
input logic tx
);
assign sda = oe ? tx : 1'bz;
// Simulator: logic gets X when two
// drivers exist, even if one is Z
endmoduleSimulation gives unexpected X values even though only one driver is active at a time. The waveform shows the bus flip-flopping between the driven value and X on alternate delta cycles.
logic is a 4-state single-driver variable type. When two sources try to drive a logic net — even if one is Z — the simulator cannot apply wire resolution rules and produces X. The wire type is the correct net type for any signal with more than one potential driver.
module good_bidir (
// CORRECT: wire handles multi-driver
inout wire sda,
input logic oe,
input logic tx
);
assign sda = oe ? tx : 1'bz;
// Wire resolution: Z + driven = driven
// No spurious X between delta cycles
endmoduleCheck your lint tool — most will flag inout logic as a warning. Synopsys VCS and Xcelium both warn: "net declaration has driver type mismatch".
Reading output Port Before It Is Valid — X in Scoreboard
MEDIUMinitial begin
rst_n = 1'b0;
#5;
rst_n = 1'b1;
// BUG: checking output immediately
// register hasn't clocked yet!
if (count !== 8'd0)
$error("FAIL: count=%0d", count);
// count is still X here
endScoreboard reports FAIL on the very first transaction. The output port reads X at the moment of comparison. All subsequent transactions pass.
A registered output only becomes valid on the clock edge after the reset is released. Reading it combinationally the instant reset deasserts captures the still-X pre-reset state.
initial begin
rst_n = 1'b0;
@(posedge clk); // align to clock
#1; rst_n = 1'b1; // release reset
@(posedge clk); // let FF capture
#1; // past clock edge
// NOW output is valid (reset took effect)
if (count !== 8'd0)
$error("FAIL: count=%0d", count);
endIn UVM, this is encoded in the reset phase — run_phase starts driving stimulus only after the reset sequence completes.
Floating input Port — Z in Simulation, Tie-to-0 in Silicon
CRITICALmodule top;
logic [7:0] result;
mult_8bit u_mult (
.clk (clk),
.rst_n (rst_n),
.a (a),
.b (b),
// BUG: .bypass_mode not connected
// input floats to Z in simulation
// synthesis tool ties it to 0
.result (result)
);
endmoduleSimulation passes. Silicon fails randomly on a subset of units. The failing condition correlates with ambient temperature (threshold voltage shift). The failing signal is an enable or mode-select port left unconnected.
In simulation, an unconnected input floats to Z and the condition if (bypass_mode) evaluates to X — meaning neither branch is taken. In synthesis, the tool sees the port as undriven and ties it to logic 0 (IEEE 1364 default). The synthesised netlist always takes the bypass_mode == 0 path. If the unintended mode breaks the circuit at certain temperatures, you get a marginal, process-corner-dependent silicon failure.
module top;
logic [7:0] result;
mult_8bit u_mult (
.clk (clk),
.rst_n (rst_n),
.a (a),
.b (b),
// CORRECT: explicitly tie to 0
.bypass_mode (1'b0),
.result (result)
);
endmoduleAlways connect every input port explicitly — even unused ones — to a known value. Enable the unconnected-port lint rule in your flow.
Waveform Analysis — Reading inout Bus Behaviour
The key to debugging bidirectional buses is knowing what the waveform should look like for correct operation, so you can immediately spot the anomaly.
Correct Bus Ownership Transfer
Time(ns) oe_a tx_a oe_b tx_b bus Event
──────────┬─────┬─────┬─────┬─────┬──────┬──────────────────────────
0 │ 0 │ 0 │ 0 │ 0 │ Z │ both released, bus floats
10 │ 1 │ 1 │ 0 │ 0 │ 1 │ A drives high (B released)
20 │ 1 │ 0 │ 0 │ 0 │ 0 │ A drives low
30 │ 0 │ 0 │ 0 │ 0 │ Z │ A releases — turnaround gap
35 │ 0 │ 0 │ 1 │ 0 │ 0 │ B takes bus, drives low (ACK)
50 │ 0 │ 0 │ 1 │ 1 │ 1 │ B drives high
60 │ 0 │ 0 │ 0 │ 0 │ Z │ B releases — bus idleBus Contention — What X Looks Like
Time(ns) oe_a tx_a oe_b tx_b bus Event
──────────┬─────┬─────┬─────┬─────┬──────┬──────────────────────────
0 │ 0 │ 0 │ 0 │ 0 │ Z │ idle
10 │ 1 │ 1 │ 0 │ 0 │ 1 │ A drives high (OK)
20 │ 1 │ 1 │ 1 │ 0 │ X │ CONTENTION: A=1, B=0 simultaneously
30 │ 1 │ 1 │ 1 │ 1 │ 1 │ both agree (1) — X clears, but damage done
40 │ 0 │ 0 │ 1 │ 0 │ 0 │ A released, B drives low
──────────┴─────┴─────┴─────┴─────┴──────┴──────────────────────────
// At t=20 the bus value is X — in real silicon this is a physical short circuitSynthesis Implications — How Tools Interpret Port Directions
Synthesis tools treat port directions as hard constraints on the elaborated netlist. Understanding what the tool does with each port type prevents costly ECOs late in the flow.
| Port type | Synthesis cell inferred | Timing constraint | Notes |
|---|---|---|---|
input | Primary Input (PI) | set_input_delay | Unconstrained PI triggers warning in DC |
output | Primary Output (PO) | set_output_delay | Output load affects timing; set set_load |
inout | TBUF (tri-state buffer) at IO pad | Both input & output delay | Requires bidirectional IO cell from std-cell lib |
ref | Not synthesisable | N/A | Testbench/program blocks only — tool errors on RTL use |
Verification Patterns for Port Types
Writing a correct testbench for a module with inout ports requires special care. Here are the standard patterns used in professional verification environments.
Pattern 1 — Bidirectional Bus Agent
module tb_bidir_complete;
// ── Signals ─────────────────────────────────
logic clk = 0;
logic rst_n;
wire sda; // shared inout wire
// ── Testbench-side bus driver ──────────────
logic tb_oe = 0; // testbench output-enable
logic tb_tx = 0; // testbench drive value
assign sda = tb_oe ? tb_tx : 1'bz;
// ── DUT ────────────────────────────────────
logic dut_oe;
logic rx_captured;
bidir_pad u_dut (
.tx_data (tb_tx),
.tx_en (dut_oe),
.rx_data (rx_captured),
.sda (sda)
);
// ── Clock ──────────────────────────────────
always #5 clk = ~clk;
// ── Contention assertion ───────────────────
property no_contention;
@(posedge clk) not (tb_oe && dut_oe);
endproperty
assert property(no_contention)
else $fatal(1, "Contention: tb_oe and dut_oe both high!");
// ── Test sequence ──────────────────────────
initial begin
rst_n = 0; dut_oe = 0;
repeat(2) @(posedge clk);
rst_n = 1;
// TB drives bus high
@(posedge clk); #1;
tb_oe = 1; tb_tx = 1;
@(posedge clk); #1;
assert(sda === 1'b1) else $error("FAIL: sda should be 1");
// TB releases, DUT drives
tb_oe = 0;
@(posedge clk); #1;
dut_oe = 1;
@(posedge clk); #1;
assert(rx_captured === sda)
else $error("FAIL: rx_data mismatch");
$display("ALL CHECKS PASSED"); $finish;
end
endmodulePattern 2 — Functional Coverage for Port States
covergroup bidir_bus_cg @(posedge clk);
// Cover all four bus states
cp_bus_val: coverpoint sda {
bins bus_low = {1'b0}; // bus actively driven low
bins bus_high = {1'b1}; // bus actively driven high
bins bus_hiz = {1'bz}; // bus released (Hi-Z)
bins bus_x = {1'bx}; // contention — should NEVER hit
}
// Cover who owns the bus
cp_oe_owner: coverpoint {tb_oe, dut_oe} {
bins neither = {2'b00}; // both released
bins tb_only = {2'b10}; // testbench drives
bins dut_only = {2'b01}; // DUT drives
illegal_bins contention = {2'b11}; // both — illegal!
}
// Cross: who owns × what value
cx_owner_val: cross cp_oe_owner, cp_bus_val;
endgroupCommon Mistakes — And How to Fix Them
// ════ MISTAKE 1: Driving an input port ════════════════════════════
module bad1 (input logic sel, output logic y);
assign sel = 1'b1; // ❌ ERROR: cannot drive an input port
assign y = sel;
endmodule
// ✅ FIX: change sel to output if the module should drive it
module good1 (output logic sel, output logic y);
assign sel = 1'b1; // ✅ OK: sel is an output now
assign y = sel;
endmodule
// ════ MISTAKE 2: Using logic for a multi-driver inout ═════════════
module bad2 (inout logic sda); // ❌ logic can't model multi-driver
assign sda = 1'bz;
endmodule
// ✅ FIX: use wire for bidirectional multi-driver nets
module good2 (inout wire sda); // ✅ wire resolves multiple drivers correctly
assign sda = 1'bz;
endmodule
// ════ MISTAKE 3: Forgetting to release to Z in inout ══════════════
module bad3 (input logic tx_en, input logic tx_d, inout wire bus);
assign bus = tx_d; // ❌ Always drives — never releases — bus fights!
endmodule
// ✅ FIX: always use the tri-state ternary pattern
module good3 (input logic tx_en, input logic tx_d, inout wire bus);
assign bus = tx_en ? tx_d : 1'bz; // ✅ releases when not driving
endmoduleQuick Reference — Port Type Cheat Sheet
// ── input: driven by parent, read by module ───────────────────
input logic single_bit,
input logic [7:0] byte_bus,
input logic clk, // clock is always input
input logic rst_n, // reset is always input
// ── output: driven by module, read by parent ──────────────────
output logic flag,
output logic [15:0] result,
// ── inout: bidirectional tri-state — use wire ─────────────────
inout wire sda, // I2C data
inout wire [7:0] data_bus, // 8-bit bidirectional bus
// ── ref: pass by reference — testbench/task only ──────────────
ref int counter, // modifies caller's variable
ref logic flag_ref,
// ── Tri-state driver pattern (always use this for inout) ──────
assign sda = tx_en ? tx_data : 1'bz; // drive or release
assign rx = sda; // always read the busInterview Q&A — Port Types
Questions are organised by seniority level. Work through them in order to build a complete mental model from basics to expert-level nuance.
input— the port is driven by the parent module; the module body can only read it.output— the port is driven from inside the module; the parent can only read it.inout— bidirectional; either side can drive, but only one at a time using tri-state logic.ref— passes the actual variable storage by reference; testbench only, not synthesisable.