Skip to content

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 typeData flowTypical use
inputIn to the module — parent drives, module readsAlmost every port. Clocks, resets, data buses, control signals.
outputOut of the module — module drives, parent readsStatus flags, registered results, computed outputs.
inoutBoth ways — bidirectional with tri-stateI2C SDA, JTAG TDO, memory DQ lines, GPIO pads.
refShared alias to caller's storageTestbench 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.

SystemVerilog — input port examples
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

output — 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 logiclogic can be driven by both procedural blocks and continuous assigns. Always use output logic in new code.

SystemVerilog — output port: combinational vs registered
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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);
 
endmodule

inout — 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

SystemVerilog — inout port with tri-state driver
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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;
 
endmodule

The 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.

SystemVerilog — ref port in a task (testbench only)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

ref vs input vs output — What's the Difference?

Aspectinputoutputinoutref
DirectionCaller → ModuleModule → CallerBoth waysShared alias (both ways)
Data copied?Yes — value copyYes — value copy outWire (no copy)No — direct alias
Module can drive?No — read onlyYesYes (tri-state)Yes
Caller sees change?N/AYes, after task/assignYes (bus)Yes — immediately
Synthesisable?YesYesYes (with care)No
Where usedRTL + TestbenchRTL + TestbenchRTL IO padsTestbench / tasks only
Default data typelogiclogicwireSame 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.

SystemVerilog — port data type examples
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
TypeStatesSigned?Best for
logic4 (0, 1, X, Z)Unsigned by defaultAll RTL signals — the go-to type
wire4 (0, 1, X, Z)UnsignedMulti-driver nets, inout, legacy code
bit2 (0, 1 only)Unsigned by defaultTestbench stimulus where X/Z never occurs
integer4-state, 32 bitsSignedLoop counters, indices in testbenches
logic signed4 (0, 1, X, Z)Yes — 2's complementDSP 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.

SystemVerilog — GPIO pad with all port types
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

Port Direction Rules — What Can Drive What

Port typeParent drives?Module drives?Module reads?Hi-Z ok?
inputYesNo (error)Yes
outputNo (error)YesYes
inoutYesYesYesYes
refYesYesYesN/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 ADriver BResult on wireNotes
000Both agree: low
111Both agree: high
0Z0One active, one released
1Z1One active, one released
ZZZBoth released — bus floats
01XCONTENTION — voltage fights
10XCONTENTION — voltage fights
X(any)XUnknown 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:

LocationTri-State Allowed?Why
IO Pad RingYesPhysical pads have built-in tri-state buffers (TBUF cells)
Internal Core LogicAvoidInternal nets are single-driver; synthesis maps inout badly inside the core
FPGA Internal RoutingNot supportedFPGA 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 TypeDefault at t=0Why
inputXUndriven at start — needs testbench to drive
outputXUninitialized register — needs reset
inout wireZNo active driver — bus floats
refXAlias to variable — inherits its initial value

Driving an inout Port from a Testbench

SystemVerilog — driving inout from a testbench
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

RTL 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.

RTL module — driving output
RTL — module produces output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;
endmodule
Testbench — reading output
Testbench — receives output via signal
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module 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);
endmodule

Using ref for Large Structures in Testbenches

Slow — copies the entire struct every call
input — full struct copy on every call
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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);
endtask
Fast — ref passes address only
ref — alias, no copy
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
typedef 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);
endtask

Debugging 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.

1

Bus Contention — Two Drivers Active Simultaneously

CRITICAL
Buggy Code
Both OE signals go high at the same timestep
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module 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
endmodule
Symptom

The sda bus reads X during an I2C transaction. All individual signals look correct in isolation. Simulation passes with force but fails without it.

Root Cause

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.

Fix
Add a mutual-exclusion property assertion + timing gap
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

Catch contention in simulation before it reaches silicon, where it means current spikes and potential latch-up.

2

Missing Hi-Z Release — Bus Permanently Stuck

HIGH
Buggy Code
OE asserted but never cleared — set without clear
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;   // assert OE
        tx_reg <= data_in;
    end
    // BUG: no branch to deassert OE when done!
    // oe stays high forever after start_tx
end
Symptom

After the master finishes sending data, the slave tries to acknowledge but the bus is still being driven. The ACK bit is never seen.

Root Cause

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.

Fix
Add the explicit clear path + a liveness assertion
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

3

inout logic Instead of inout wire — Wrong Type for Multi-Driver Net

HIGH
Buggy Code
logic cannot handle multi-driver resolution
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module 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
endmodule
Symptom

Simulation 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.

Root Cause

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.

Fix
Use inout wire so the multi-driver resolution rules apply
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

Check your lint tool — most will flag inout logic as a warning. Synopsys VCS and Xcelium both warn: "net declaration has driver type mismatch".

4

Reading output Port Before It Is Valid — X in Scoreboard

MEDIUM
Buggy Code
Scoreboard samples before reset completes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial 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
end
Symptom

Scoreboard reports FAIL on the very first transaction. The output port reads X at the moment of comparison. All subsequent transactions pass.

Root Cause

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.

Fix
Wait a full clock edge after releasing reset before sampling
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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);
end

In UVM, this is encoded in the reset phase — run_phase starts driving stimulus only after the reset sequence completes.

5

Floating input Port — Z in Simulation, Tie-to-0 in Silicon

CRITICAL
Buggy Code
Unconnected input — sim vs synth diverge
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module 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)
    );
endmodule
Symptom

Simulation 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.

Root Cause

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.

Fix
Explicitly tie unused inputs — never leave them floating
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
    );
endmodule

Always 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

Waveform — I2C-style bus handoff (correct)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Time(ns)  oe_a  tx_a  oe_b  tx_b  bus    Event
──────────┬─────┬─────┬─────┬─────┬──────┬──────────────────────────
00000Z    │ both released, bus floats
1011001    │ A drives high (B released)
2010000    │ A drives low
300000Z    │ A releases — turnaround gap
3500100    │ B takes bus, drives low (ACK)
5000111    │ B drives high
600000Z    │ B releases — bus idle

Bus Contention — What X Looks Like

Waveform — contention (WRONG)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Time(ns)  oe_a  tx_a  oe_b  tx_b  bus    Event
──────────┬─────┬─────┬─────┬─────┬──────┬──────────────────────────
00000Z    │ idle
1011001    │ A drives high (OK)
201110X    │ CONTENTION: A=1, B=0 simultaneously
3011111    │ both agree (1) — X clears, but damage done
4000100    │ A released, B drives low
──────────┴─────┴─────┴─────┴─────┴──────┴──────────────────────────
// At t=20 the bus value is X — in real silicon this is a physical short circuit

Synthesis 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 typeSynthesis cell inferredTiming constraintNotes
inputPrimary Input (PI)set_input_delayUnconstrained PI triggers warning in DC
outputPrimary Output (PO)set_output_delayOutput load affects timing; set set_load
inoutTBUF (tri-state buffer) at IO padBoth input & output delayRequires bidirectional IO cell from std-cell lib
refNot synthesisableN/ATestbench/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

SystemVerilog — self-checking testbench with bidirectional bus model
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

Pattern 2 — Functional Coverage for Port States

SystemVerilog — functional coverage for inout port states
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;
 
endgroup

Common Mistakes — And How to Fix Them

SystemVerilog — port mistakes (wrong) vs correct
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ 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
endmodule

Quick Reference — Port Type Cheat Sheet

SystemVerilog — port types quick reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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 bus

Interview 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.