Skip to content

Verilog · Chapter 5.1.1 · Data & Variables

wire and tri Nets in Verilog

The wire is the default Verilog net, the keyword that names every piece of metal in your design, from a one-bit enable to a wide data bus. The tri net is its sibling, with the same simulator behaviour, the same resolution rules, and the same default of Z, but a different message to anyone reading the code: this net is intentionally driven by more than one source. Together this pair carries the large majority of all net declarations in working RTL. Knowing when to reach for which one, and understanding what each becomes after synthesis on an ASIC versus an FPGA, is the bedrock of physical-data-type discipline. This page is the deep drill into that wire and tri pair, covering single-driver nets, tri-state buses, and driver contention.

Foundation22 min readVerilogwiretriNetsTri-State

Chapter 5 · Section 5.1.1 · Data & Variables

1. The Engineering Problem

You are reading the RTL of an IP block one of your colleagues wrote two years ago. Two declarations sit next to each other in the port list of the top module:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
output wire [31:0]  dout;
inout  tri  [31:0]  dq;

Why two different keywords? Both are nets. Both are 32-bit vectors. Both will end up as pieces of metal on the chip. So what does the engineer reading the next line — or the lint tool running on the next CI build, or the synthesis tool elaborating the netlist tomorrow morning — get from the keyword difference?

The short answer: not much from the simulator (the two behave identically), and a great deal from everyone else. wire says "ordinary point-to-point net, one driver expected." tri says "this net is intentionally driven by more than one source — please do not flag it as a multi-driver bug." The choice is a documentation choice, encoded as a keyword.

Get that distinction wrong and one of two things happens. You declare a real tri-state bus as wire — lint warns on every build, code reviewers ask the same question every time, the next engineer assumes it's a bug. Or you declare a single-driver net as tri — lint stays silent on a real multi-driver bug, and the contention shows up six months later as an X propagating through your data path on the day a customer's chip comes back from the lab.

This page is about getting that distinction right.

2. Why wire Exists

Every signal in a Verilog design has a type. The language could in principle pick the type from context (whether you wrote assign or always), but it doesn't — every declaration is explicit. wire is the keyword Verilog gives you for the most common case: a single-driver net carrying a single value at a time, fed by a continuous-assignment expression or by a module's output port.

In a typical block of 2,000 lines of RTL you will find 200 to 400 wire declarations and roughly zero of every other net type. wire is the default in two senses:

  • Implicit default for unknown identifiers. By default, if you reference a name on the right-hand side of assign that hasn't been declared, Verilog implicitly creates a one-bit wire for it. (The `default_nettype none directive disables this — strongly recommended, but historically off by default.)
  • Default keyword for module port declarations. A port declared output [7:0] dout; without an explicit net type is treated as wire [7:0] dout;.

The keyword exists because Verilog's whole job in the net half of the type system is to name pieces of physical interconnect. wire is the boring, predictable, single-driver case — and most of the time that's exactly what you want.

3. Why tri Exists

If wire already handles single-driver nets, what is tri for?

tri exists for a single reason: documentation. The Verilog language definition (IEEE 1364, §6.1.1) is unambiguous — tri and wire "share the same syntax and functions identically." Their resolution tables are the same. Their default values are the same. The simulator treats them the same. If you replace every tri in your design with wire, the gates after synthesis are identical and the simulation results are bit-for-bit equal.

What tri adds is intent. When you declare a net as tri, you are telling the next human (and the next lint tool, and the next code reviewer) that this net is intentionally multi-driven — that the system contract guarantees mutual exclusion among its drivers, and that no, this is not a bug. Without tri, every multi-driver wire looks like a contention bug; with tri, the keyword is the assertion of intent.

Lint suites typically encode this: a multi-driver wire raises a warning; a multi-driver tri does not. The language gives you the same gates either way; the engineering process gets cleaner with the keyword that documents the design.

4. Mental Model

The mental-model has two practical consequences.

  • Naming hint. Real-world RTL almost always names tri-typed nets after the bus they represent (io_dq, shared_addr_bus, irq_n) and treats wire-typed nets as the generic case. A tri ack_o; in working RTL would be unusual and probably wrong.
  • Review hint. When reviewing code that declares tri, the first review question is "what's the mutual-exclusion contract here?" That contract usually appears as a one-line comment above the declaration or as an SVA assertion checking $countones({oe_a, oe_b, oe_c}) <= 1. If you can't find the contract, the declaration is probably a copy-paste accident from another module.

5. Visual Explanation

Three figures cover every wire-and-tri topology you will meet in RTL.

5.1 Visual A — single-driver wire

The default case: one producer drives, one (or several) consumers read. The wire is the connection — no storage, no resolution, value follows the driver.

Single-driver wire — the workhorse pattern

data flow
Single-driver wire — the workhorse patterndata_qdata_inproducerdrives data_qwireinterconnect — no storageconsumersamples data
One driver, one or more loads. The wire carries whatever the producer puts on it; the consumer sees the same value at the same instant (modulo wire delay, modelled separately).

5.2 Visual B — tri-state bus with three masters

The multi-driver case the tri keyword exists to document. Three masters share one net. At any instant the system contract says at most one master asserts its enable; the other two release to Z and contribute nothing to resolution.

Tri-state bus topology — three masters converging on one tri netmaster Adrives when oe_a=1master Bdrives when oe_b=1master Cdrives when oe_c=1tri busshared netconsumerreads bus12
Three potential drivers, one shared tri net, one consumer. The system contract — typically a one-hot enable signal — guarantees mutual exclusion; the keyword `tri` documents the multi-driver intent so lint and reviewers know it is by design.

5.3 Visual C — contention

When the system contract is violated and two drivers fight for the bus on the same cycle, the resolution rule for wire / tri returns X. The simulator flags the ambiguity; the silicon would show a destructive short.

Bus contention — two drivers, opposing values, X resultdriver Adrives 1driver Bdrives 0busX — contentionconsumersamples X12
Driver A wants `1`; driver B wants `0`. The wire-resolution rule has no rule for picking a winner, so the simulator returns `X` — the formal symbol for 'undefined'. On silicon, the same condition produces excessive current through both drivers and an indeterminate voltage on the wire.

6. Syntax Deep Dive

The declaration grammar for wire and tri is identical. Every option you can write for one, you can write for the other.

wire-tri-syntax.v — declaration anatomy
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// <wire | tri> [signed] [strength] [range] name [= continuous-assign];
 
wire                       enable;            // scalar, default unsigned
wire        [7:0]          data_bus;          // 8-bit packed vector
wire signed [15:0]         s_acc_w;           // signed 16-bit net
wire        [DW-1:0]       payload;           // parametrised width
wire                       ack = req & gnt;   // continuous-assign shorthand
 
tri                        io_irq_n;          // scalar tri-state line
tri         [7:0]          shared_bus;        // 8-bit tri-state bus
tri         (strong1, weak0) [31:0]  dq;      // strength specifier (rare)
tri                        sda = drv_sda ? 1'b0 : 1'bz;  // tri shorthand

The optional pieces — same for both keywords:

  • signed — changes operator semantics for <, >, >>> and width-extension rules. Default is unsigned.
  • Strength specifier(strong1, strong0) is the default; explicit specifiers ((pull1, weak0), etc.) override the resolution-table inputs. Almost never used in RTL — the sibling 5.1.2 Signal Strengths page (planned) covers this in depth.
  • Range[MSB:LSB] makes the net a vector. Convention is [N-1:0]; mixing [0:N-1] across modules causes byte-order bugs.
  • Continuous-assign shorthandwire ack = req & gnt; is identical to wire ack; assign ack = req & gnt; on two lines.

6.1 Implicit declaration and `default_nettype ``

Verilog has a backward-compatibility footgun: if you reference an undeclared identifier on the right-hand side of assign (or in a port list), the compiler implicitly creates a one-bit wire for it. This is the source of countless "why is my 32-bit bus stuck at one bit?" debugging sessions.

implicit-net.v — the silent footgun
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module bad;
    wire [7:0] data_bus;
    wire [7:0] derived;
 
    // Typo: `data_bsu` instead of `data_bus`.
    // Verilog implicitly declares `data_bsu` as a 1-bit wire.
    // No compile error — but `derived` will be garbage.
    assign derived = data_bsu;
 
endmodule

The standard prophylactic is a project-wide compiler directive at the top of every file:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none

With default_nettype none, the implicit-declaration rule is disabled — any undeclared identifier raises a compile error. Every working RTL house uses this; if your codebase doesn't, fix that before doing anything else.

6.2 Port-list shorthand

Verilog-2001 lets you declare a port and its net type on the same line:

port-list-style.v — modern port declaration
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo_frontend (
    input  wire        clk,         // explicit `wire` keyword
    input  wire        rst_n,
    input  wire [7:0]  data_in,
    output wire        ack          // `wire` on the output port
);
    // Inside the module, `ack` is already a declared wire.
    assign ack = /* whatever */;
endmodule

Without the explicit wire, the same declaration input clk; still works because wire is the implicit default for ports — but writing the keyword is a clarity habit. Some lint suites require it.

7. wire vs tri — the comparison

The single most important table on this page. The columns describe behaviour; the rows describe aspects.

Aspectwiretri
Resolution rule (IEEE 1364)identicalidentical
Default value at t = 0zz
Behaviour on multiple 0 / 1 driversX (contention)X (contention)
Behaviour on Z + 0 (or Z + 1)Non-Z driver winsNon-Z driver wins
Strength specifierssupportedsupported
Synthesis behaviourwire / mux rewritewire / mux rewrite (same)
signed modifiersupportedsupported
Continuous-assign shorthandsupportedsupported
Engineer intentsingle driver expectedmulti-driver intentional
Lint behaviourmulti-driver = warningmulti-driver = no warning
Reviewer expectation"if multi-driver, ask why""what's the mutex contract?"
Typical useevery interconnecttri-state buses, pad I/Os

The first eight rows are the IEEE 1364 spec — there is no observable runtime difference between the two keywords. The bottom four rows are the engineering reality — code reviews, lint, and downstream maintenance all read the keyword as documentation.

8. Single Driver Example

The 95% case. One producer module drives a wire; one consumer module reads it.

rtl/wire_basic_pp.v — point-to-point wire between modules
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module producer (
    input  wire        clk,
    input  wire        rst_n,
    output wire [7:0]  data_q
);
    reg [7:0] count_q;
 
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) count_q <= 8'h00;
        else        count_q <= count_q + 8'h01;
    end
 
    assign data_q = count_q;   // single continuous driver of `data_q`
endmodule
 
 
module consumer (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  data_in,
    output reg  [7:0]  data_q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) data_q <= 8'h00;
        else        data_q <= data_in;
    end
endmodule
 
 
module wire_basic_pp (
    input  wire        clk,
    input  wire        rst_n,
    output wire [7:0]  sample_q
);
    // The wire that connects producer to consumer — single driver, single load.
    wire [7:0] link;
 
    producer u_prod (
        .clk     (clk),
        .rst_n   (rst_n),
        .data_q  (link)
    );
 
    consumer u_cons (
        .clk     (clk),
        .rst_n   (rst_n),
        .data_in (link),
        .data_q  (sample_q)
    );
endmodule

link is a wire. Its driver is producer.data_q (via the port connection). Its load is consumer.data_in. Every cycle, the producer drives a value onto the wire; the consumer samples it on the next clock edge. Lint stays silent (single driver, well-defined); synthesis produces a piece of metal between the two flop banks.

9. Multi Driver Example

The pathological case the tri keyword exists to prevent. Two assign statements drive the same plain wire, with overlapping conditions. Lint warns; the simulator returns X.

rtl/wire_multi_driver_bug.v — the bug pattern
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module wire_multi_driver_bug (
    input  wire        sel_a,
    input  wire        sel_b,
    input  wire [7:0]  data_a,
    input  wire [7:0]  data_b,
    output wire [7:0]  mux_out_w     // declared as plain `wire`
);
    // Two continuous drivers — both active when sel_a == sel_b == 1.
    // Author thought "the system will never assert both"; the system
    // does, and `mux_out_w` resolves to X.
    assign mux_out_w = sel_a ? data_a : 8'bz;
    assign mux_out_w = sel_b ? data_b : 8'bz;
endmodule

Two things are wrong here:

  1. The mutex contract is not enforced. When sel_a and sel_b are both asserted, both drivers are active with potentially different values, and the bus resolves to X.
  2. The keyword does not document the intent. Declared as wire, the multi-driver shape reads as a bug to anyone (including lint) inspecting the module — even though the author intended a "tri-state mux" pattern.

The correct rewrite — if a tri-state pattern is actually wanted — declares the bus as tri and adds an SVA assertion (or a comment) enforcing the mutex:

rtl/tri_mux_documented.v — same pattern, documented intent
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module tri_mux_documented (
    input  wire        sel_a,
    input  wire        sel_b,
    input  wire [7:0]  data_a,
    input  wire [7:0]  data_b,
    output tri  [7:0]  mux_out_t     // declared as `tri` — intent documented
);
    // System contract: at most one of {sel_a, sel_b} asserted per cycle.
    // assert property (@(posedge clk) $countones({sel_a, sel_b}) <= 1);
 
    assign mux_out_t = sel_a ? data_a : 8'bz;
    assign mux_out_t = sel_b ? data_b : 8'bz;
endmodule

Same gates after synthesis (likely a mux — see §13), but the keyword tells the next engineer this is a tri-state mux pattern and not a multi-driver bug. The lint suite reads the keyword and stays silent.

The deeper lesson: if you find yourself writing multiple assign statements to the same net, you have a fork in the road. Either (a) the multi-driver pattern is genuinely a tri-state bus — declare as tri, document the mutex — or (b) the multi-driver pattern is a mistake and you wanted a mux all along. In case (b), rewrite as a single assign with a ternary or a case. There is no third option.

10. Tri-State Bus Example

The canonical multi-driver tri pattern. Three masters share one bus; the slave reads. Mutual exclusion is enforced by the one-hot encoding of oe_* from the upstream arbiter.

rtl/tristate_bus.v — three masters, one tri bus
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module tristate_bus (
    input  wire        oe_a,
    input  wire        oe_b,
    input  wire        oe_c,
    input  wire [7:0]  data_a,
    input  wire [7:0]  data_b,
    input  wire [7:0]  data_c,
    output tri  [7:0]  bus           // `tri` — intentionally multi-driven
);
    // System contract: at most one of {oe_a, oe_b, oe_c} asserted at any time.
    // The upstream arbiter is one-hot; lint and a top-level SVA enforce it.
 
    assign bus = oe_a ? data_a : 8'bz;
    assign bus = oe_b ? data_b : 8'bz;
    assign bus = oe_c ? data_c : 8'bz;
endmodule

Three continuous drivers; whichever has its enable asserted wins (the other two contribute Z, which loses resolution to any non-Z value). When all three release, the bus floats to Z — fine for a simulation-only model, problematic on real silicon (an undriven gate input is a floating node), which is why a real top-level tri-state bus declares the line as tri1 (pulled to 1 when undriven) or tri0, and the chip-pad has the matching pull resistor.

11. Industry Bus Example

The pattern you actually see in shipping chips: an open-drain interrupt line with multiple peripherals sharing the line and an implicit pull-up. The line is tri1 (a sibling net type covered on the planned 5.1.5 page), but the bus structure — multiple drivers, one shared net, one consumer — is the same shape as the tri-state-bus example, with the resolution rule changed so that 1 (release) loses to 0 (assertion).

rtl/irq_open_drain.v — open-drain IRQ bus (industry pattern)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module irq_open_drain (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [3:0]  peripheral_req,    // one-hot per peripheral
    output tri1        irq_n              // pulled HIGH when no driver active
);
    // Each peripheral pulls irq_n LOW when it wants service; releases otherwise.
    // tri1 means "if nothing drives, resolve to 1" — the implicit pull-up.
 
    assign irq_n = peripheral_req[0] ? 1'b0 : 1'bz;
    assign irq_n = peripheral_req[1] ? 1'b0 : 1'bz;
    assign irq_n = peripheral_req[2] ? 1'b0 : 1'bz;
    assign irq_n = peripheral_req[3] ? 1'b0 : 1'bz;
endmodule

This is the working-RTL pattern: tri or its pull-net cousins (tri0 / tri1) at the chip boundary, plain wire everywhere else. The pull-net resolution makes the "no driver active" case deterministic.

12. Simulation Behavior

The simulator implements the IEEE 1364 resolution table on every cycle that any driver of a wire or tri net changes value. The table for a two-driver net:

Driver ADriver BResolved net
000
01X
0Z0
0XX
10X
111
1Z1
1XX
ZZZ
ZXX
XXX

The pattern reduces to four rules a working engineer carries by reflex:

  1. Identical non-X, non-Z drivers → the value (0 / 0 = 0; 1 / 1 = 1).
  2. One driver Z → the other driver wins (whether 0, 1, or X).
  3. Conflict (0 / 1)X.
  4. Any XX (with one exception: X and ZX).

Default value at t = 0 is Z for both wire and tri. The first procedural assignment to a downstream reg that samples an undriven wire typically captures X (because Z propagates as X through most operators).

For nets with more than two drivers, the table extends pairwise — the resolution function is associative, so the final value is what you'd get by reducing the driver list with the two-driver table.

13. Waveform Analysis

Two waveforms — one showing normal tri-state operation (mutex respected), one showing contention (mutex violated). Together they cover every interesting wire / tri behaviour.

13.1 Waveform #1 — Normal tri-state operation

Tri-state bus — drivers honouring the mutex contract

12 cycles
Tri-state bus — drivers honouring the mutex contractA drives — bus follows data_a (=1)A drives — bus follows…no driver — bus floats to Zno driver — bus floats…B drives — bus follows data_b (=0)B drives — bus follows…B releases — bus back to ZB releases — bus back …clkoe_adata_aXXA1A1XXXXXXXXoe_bdata_bXXXXXXB1B1XXXXbust0t1t2t3t4t5t6t7t8t9t10t11
Read the bus row top to bottom: when oe_a=1, bus carries data_a's value. When both oe_a and oe_b are 0, the bus has no driver and resolves to Z. When oe_b=1, bus follows data_b. The mutex contract is honoured — at no cycle are both oe_* signals asserted simultaneously.

The interesting observation on this waveform is the floating cycles (4–5 and 8–9). On a wire or tri net with no driver active, the simulator returns Z. A downstream flop sampling Z will typically capture X — silicon does not see Z because every node has parasitic capacitance, so the silicon value is whatever the last driver charged it to. The simulator's Z is a warning that the design assumes a driver where none is active.

For a real chip, the floating cycles are exactly the cycles a pull resistor solves — declare the bus as tri0 or tri1 (instead of plain tri) and the floating cycles resolve to 0 or 1 deterministically.

13.2 Waveform #2 — Contention

Bus contention — mutex contract violated

10 cycles
Bus contention — mutex contract violatedA drives — bus = 1A drives — bus = 1BOTH drive (data_a=1, data_b=0) → XBOTH drive (data_a=1, …A releases — bus recovers to data_b (=0)A releases — bus recov…B releases — bus back to ZB releases — bus back …clkoe_adata_aXX1111XXXXoe_bdata_bXXXX0000XXbusXXt0t1t2t3t4t5t6t7t8t9
At cycle 4, oe_a is still asserted (driving data_a=1) when oe_b also asserts (driving data_b=0). Two drivers, conflicting values — the resolution rule returns X. The simulator flags the contract violation; on silicon, the same condition produces a short between supply and ground through the two driver output stages. When A releases at cycle 6, B is left alone and the bus recovers to data_b's value.

The exact mechanism of the X on cycle 4: the resolution function for wire / tri has no rule that picks a winner between two non-Z, non-X drivers with different values. The two driver values arrive at the resolution function — 1 from driver A, 0 from driver B — and the function's only legal answer is X. The simulator returns X on the resolved net; downstream logic that consumes the net propagates X through its operators (X & 1 = X; X | 0 = X; etc.).

On silicon, the same condition causes both driver output stages to fight: driver A's pull-up transistor sources current into driver B's pull-down transistor. The voltage on the wire settles somewhere between supply and ground, typically below the input-low threshold of any receiver, but the current draw on the power rail can be tens of milliamps per fighting pair — enough to cause electromigration on the rail and detectable as a thermal hot spot. This is why contention is a system-level failure on real chips, not just a simulator warning.

14. Synthesis Insights

What wire and tri become after the synthesis tool reads your RTL:

  • wire with a single driver synthesises to a piece of metal — a wire in the netlist, connecting the driver's output to the consumer's input. No logic added.
  • tri with a single driver synthesises to the same metal as plain wire. The tri keyword does not add or remove any gate; it only changed the lint behaviour at RTL.
  • tri with multiple drivers is the interesting case. On a modern ASIC standard-cell library, internal tri-state buffers usually do not exist — the library has no cell with three states (0, 1, Z) on its output. The synthesis tool must rewrite the tri-state structure into a multiplexer: it builds a mux tree where each input is one of the data_* signals and the select lines come from the oe_* enables.

The rewrite has a subtle consequence: the Z value is gone from the netlist. After synthesis, what the simulator saw as "no driver active → bus = Z" becomes "all oe_* deasserted → mux selects default → bus = 0 (or 1, depending on the tool's default)." Gate-level simulation no longer matches RTL simulation on the cycles where the bus was supposed to float.

synthesis-rewrite.v — what the synthesis tool produces
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Original RTL (3-master tri-state bus):
assign bus = oe_a ? data_a : 8'bz;
assign bus = oe_b ? data_b : 8'bz;
assign bus = oe_c ? data_c : 8'bz;
 
// Synthesis tool rewrites internally to:
assign bus_after_synth = oe_a ? data_a :
                         oe_b ? data_b :
                         oe_c ? data_c :
                                8'h00;   // tool-defined default
// (a priority encoder collapses into a one-hot mux when oe_* are mutex)

The synthesis tool's report file lists this rewrite as a warning ("tri-state buffer inferred at internal net; rewritten to multiplexer"). A working RTL engineer reads that report.

15. FPGA vs ASIC Reality

The synthesis story differs between the two major target technologies in subtle but important ways.

15.1 ASIC

  • Internal tri-state: not supported by standard-cell libraries on any modern process node (7nm, 5nm, 3nm). The synthesis tool rewrites internal tri to mux without asking.
  • Top-level pad tri-state: supported via dedicated bidirectional I/O pad cells (IOBIDIR, IOBI, etc., names vary by foundry). The tri-state direction is controlled by an enable signal routed to the pad.
  • Working RTL convention: declare tri (or inout) only at the top-level module's port list; everywhere internal, write muxes.

15.2 FPGA

  • Xilinx 7-series and later (Artix-7, Kintex-7, Virtex-7, UltraScale, Versal): internal tri-state has been removed since the 7-series. The Vivado synthesis tool automatically rewrites internal tri-state to multiplexer; the rewrite is visible in the synthesis log.
  • Intel/Altera Stratix V and later: same story — internal tri-state is rewritten to mux.
  • Older FPGAs (Spartan-3/6, Stratix II/III/IV, etc.): had limited dedicated internal tri-state interconnect lines, but using them required vendor-specific attributes and the lines were slow and power-hungry. New designs do not use them.
  • All modern FPGAs support tri-state at the I/O pad boundary via dedicated buffer primitives (IOBUF / OBUFT / IBUF on Xilinx; equivalent on Intel/Altera). Bidirectional I/O is the canonical use case.

The day-to-day FPGA convention is identical to the ASIC convention: internal RTL uses plain muxes; tri-state appears only at top-level I/O ports.

15.3 Practical implication

If you write multi-driver tri inside an internal module, both ASIC and FPGA synthesis tools will rewrite it to a mux. The behaviour at the gate-level differs from RTL simulation in the Z cycles. If your verification environment runs gate-level simulation (and it should, at least for sign-off), you will see the mismatch.

The correct internal-RTL idiom is therefore an explicit mux:

explicit-mux.v — the FPGA / ASIC-portable idiom
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Internal mux — no tri-state, no synthesis surprises, no gate-level
// mismatch. Same gates as the synthesis tool would produce from the
// tri-state form, but the intent is now explicit at RTL.
assign bus_internal = oe_a ? data_a :
                      oe_b ? data_b :
                      oe_c ? data_c :
                             8'h00;

Reach for tri (or inout) only at the module ports that connect to chip pads.

16. Debugging Guide

Five debug recipes that cover most wire / tri failures.

16.1 Floating nets

Symptom. A signal downstream of a wire shows Z (in the waveform) or X (after one or more procedural assignments downstream).

Root cause. The wire has no active driver at the cycle in question. Common scenarios:

  • The wire is declared at module scope but every assign that drives it is gated by a condition that's always false on this revision.
  • The wire is a module port that the parent forgot to connect.
  • The wire is a multi-driver tri whose drivers all happen to be releasing (all oe_* low).

Debug method.

  1. In the waveform, locate the first cycle where the suspect signal becomes Z. Inspect the drivers of that signal at exactly that cycle.
  2. For port-level floats: search the parent instantiation; verify the port is in the connection list. Some lint suites and simulators flag unconnected input ports as warnings — turn that warning on.
  3. For internal wire floats: grep assign\s+<name> and verify there's exactly one driver path that's active on the suspect cycle.

Prevention. Use `default_nettype none to eliminate the implicit-net footgun. For nets that may genuinely have no driver in some cycles (rare), declare them as tri0 or tri1 so the floating value is deterministic.

16.2 Multiple drivers (silent bug)

Symptom. A wire resolves to X on certain cycles; lint warns about multiple drivers; sometimes the warning is suppressed and the bug ships.

Root cause. Two or more continuous drivers on the same net with conflicting values on a cycle the author thought would be mutually exclusive.

Debug method.

  1. Run lint with multi-driver warnings enabled (most suites have this on by default; some teams turn it off after years of false positives — turn it back on).
  2. Inspect every line that drives the net: search for assign\s+<name>\s*= and look for module-output ports that also drive it.
  3. If the multi-driver shape is intentional (tri-state bus), redeclare as tri so the lint warning becomes a documented exception.

Prevention. Declare every truly-tri-state net as tri. Add an SVA assertion enforcing the mutex contract: assert property (@(posedge clk) $countones({oe_a, oe_b, oe_c}) <= 1);. The assertion catches the contract violation the moment it happens, instead of having you trace X propagation through the data path.

16.3 Unexpected Z

Symptom. A wire you expected to carry a logic value shows Z at simulation start (t = 0) and stays there until the first driver activates.

Root cause. Default value of a wire is Z at t = 0. If the reset path doesn't activate the driver before downstream logic samples, the consumer captures Z.

Debug method.

  1. Verify the driver's reset is correct — typically the driver should produce a defined value (e.g. 8'h00) when rst_n is asserted, not stay tri-stated.
  2. Check that any tri-state driver's oe_* is deasserted during reset (so tri-bus pull resistors or default mux values take over).

Prevention. Every wire should have a reset path that drives a defined value from t = 0 onward. For shared buses, the system contract should specify a default driver (often a low-priority master, or a pull resistor at the chip boundary).

16.4 Unexpected X

Symptom. A wire carries X somewhere in the data path; downstream logic propagates X through operators.

Root cause. Three common ones:

  1. Multi-driver contention — two drivers active with different values (see §16.2).
  2. Z propagated through an operator — most Verilog operators promote Z to X (e.g. Z & 1 = X). The original Z came from a floating net (§16.1).
  3. X from an uninitialised reg that was assigned to the wire via a port connection.

Debug method.

  1. Trace backward through the data path one operator at a time, looking for the first cycle the value becomes X (or the first signal that carries X).
  2. If the upstream signal is a wire showing Z or driven by an X-bearing reg, you've found the source.
  3. Use the simulator's $showvars or waveform "drive arrow" feature to identify which assign is producing the X.

Prevention. Always reset every reg in synthesisable RTL. Prefer wire with explicit drivers over multi-driver patterns where possible. Run code coverage with X-propagation checks enabled.

16.5 Synthesis mismatch

Symptom. Pre-synthesis RTL simulation passes. Post-synthesis gate-level simulation fails on cycles involving a tri-state bus.

Root cause. The synthesis tool rewrote the internal tri into a mux. The mux's default value (when all oe_* are deasserted) is 0 in the gates but Z in the RTL — downstream logic sees different values on the floating cycles.

Debug method.

  1. Read the synthesis report. Search for "tri-state buffer rewritten" or "multiplexer inferred from tri-state".
  2. Run gate-level simulation; compare against RTL simulation at the cycle of divergence.
  3. If you actually need tri-state on silicon, the signal must exit the module hierarchy as a top-level inout port.

Prevention. Inside any synthesisable block, write muxes explicitly and use wire. Reserve tri for top-level port declarations only.

17. Design Review Notes

What a senior RTL reviewer flags on a wire or tri declaration:

  1. Every tri declaration has a mutex contract. The reviewer asks: "What guarantees that at most one driver is active at a time?" The answer should be one of: an SVA assertion, a one-hot arbiter upstream, an external bus master that owns the line. If the answer is "nothing," the declaration is a bug.
  2. No tri in internal hierarchy. A tri-typed net that doesn't reach a top-level port is a code smell — the synthesis tool will rewrite it to a mux, and gate-level simulation will diverge from RTL. Reviewer asks for the rewrite to an explicit mux.
  3. Every wire has a single driver. Lint should flag multi-driver wire; review catches the cases where the warning was suppressed.
  4. Bus ownership is documented. For any shared bus, a one-line comment near the declaration names the arbiter and points to the mutex assertion. "Arbiter at u_arb_top; mutex enforced by assert_one_hot_oe SVA" is the right level of detail.
  5. `default_nettype none at the top of every file. No exceptions. This is non-negotiable in any working RTL house.
  6. No implicit net declarations. With default_nettype none in place, every identifier referenced must be declared explicitly. Lint catches the residue.
  7. Port keyword written explicitly. input wire clk is clearer than input clk. Most lint suites require it.
  8. Naming convention reflects the type. Top-level tri-state I/Os are typically named io_<bus> or <bus>_io; internal wires use the producer's signal name (e.g. link, payload, enable). A tri ack_o; would raise an eyebrow — ack is rarely tri-state in practice.

18. Interview Insights

19. Exercises

Three exercises that turn the page's mental model into reflex.

Exercise 1 — Spot the bug

The following module compiles cleanly, simulates without lint warnings, but produces X on mux_out whenever both sel_a and sel_b are asserted. The author intended sel_a to take priority. Identify the two distinct bugs and rewrite the module to honour the intent.

exercise-1.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module mux_unsafe (
    input  wire        sel_a,
    input  wire        sel_b,
    input  wire [7:0]  data_a,
    input  wire [7:0]  data_b,
    output wire [7:0]  mux_out
);
    assign mux_out = sel_a ? data_a : 8'bz;
    assign mux_out = sel_b ? data_b : 8'bz;
endmodule

What to produce. (a) Name both bugs (one is structural; one is the type declaration). (b) Provide the corrected RTL — an explicit mux with sel_a priority. (c) Explain why the corrected version is portable across ASIC and FPGA targets.

Exercise 2 — Implement it

Write a synthesisable Verilog module tri_bus_3way that implements a 16-bit tri-state bus shared by three masters with a one-hot enable interface and a fourth "default driver" that asserts when no master is active. Specifications:

  • Inputs: wire [2:0] oe_master (one-hot), wire [15:0] data_master_a, wire [15:0] data_master_b, wire [15:0] data_master_c, wire [15:0] default_value.
  • Output: tri [15:0] bus.
  • Behaviour: when oe_master[N] is asserted, master N drives the bus. When none is asserted, default_value drives the bus.

Hints. You may use four assign statements driving the same tri net. Each assign is gated by a single bit of oe_master (or by !|oe_master for the default driver). Add a one-line comment above the tri declaration documenting the one-hot contract.

Exercise 3 — Predict the synthesis output

Given the multi-driver tri example from §10 (the 3-master tristate_bus module), sketch (in pseudocode or a small Verilog snippet) the equivalent multiplexer the synthesis tool will produce. Identify two ways the gate-level behaviour will differ from the RTL simulation when all three oe_* signals are deasserted.

Hint. Refer to §14 for the rewrite pattern; the mux's default value is the source of the divergence.

20. Summary

wire and tri are the two workhorse keywords of Verilog's net family. They are simulator-identical — same resolution rule, same default Z, same accepted modifiers, same gates after synthesis. They are engineering-distinctwire says "single-driver net; flag any multi-driver pattern as a bug"; tri says "intentionally multi-driven; the system contract guarantees mutual exclusion."

The day-to-day discipline:

  • wire everywhere internal. Single-driver, point-to-point. The boring choice is the right one for 95% of RTL.
  • tri only at chip-pad boundaries. Internal multi-driver tri gets rewritten to a mux by the synthesis tool on every modern target (ASIC standard cells, Xilinx 7-series and later, Intel Stratix V and later).
  • `default_nettype none at the top of every file. No exceptions — the implicit-net rule is a footgun that has cost the industry collectively a million hours of debug.
  • Every tri declaration carries a mutex contract. Document the contract as an SVA assertion or as a comment naming the arbiter; if you can't name the contract, the tri declaration is wrong.
  • For internal "tri-state-mux" patterns, write the mux. It's the same gates, it's gate-level / RTL portable, it's understood by every reviewer.

Reach for tri only when a real piece of metal on real silicon has multiple drivers — which, in modern designs, means only at the chip-pad layer. Everywhere else, plain wire and an explicit mux are the working-RTL convention.

The sibling sub-pages drill into the rest of the net family: 5.1.2 Signal Strengths covers the eight-level strength lattice underlying the resolution function; 5.1.3 Wired Nets covers wand / wor; 5.1.4 trireg Nets covers capacitive charge storage; 5.1.5 tri0 / tri1 Nets covers pull-resistor models; 5.1.6 supply0 / supply1 covers power-rail ties. Chapter 5.2 picks up with the variable family on the other side of the net-vs-variable line.