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:
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
assignthat hasn't been declared, Verilog implicitly creates a one-bitwirefor it. (The`default_nettype nonedirective 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 aswire [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 treatswire-typed nets as the generic case. Atri 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 flow5.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.
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.
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> [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 shorthandThe 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 shorthand —
wire ack = req & gnt;is identical towire 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.
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;
endmoduleThe standard prophylactic is a project-wide compiler directive at the top of every file:
`default_nettype noneWith 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:
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 */;
endmoduleWithout 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.
| Aspect | wire | tri |
|---|---|---|
| Resolution rule (IEEE 1364) | identical | identical |
| Default value at t = 0 | z | z |
Behaviour on multiple 0 / 1 drivers | X (contention) | X (contention) |
Behaviour on Z + 0 (or Z + 1) | Non-Z driver wins | Non-Z driver wins |
| Strength specifiers | supported | supported |
| Synthesis behaviour | wire / mux rewrite | wire / mux rewrite (same) |
signed modifier | supported | supported |
| Continuous-assign shorthand | supported | supported |
| Engineer intent | single driver expected | multi-driver intentional |
| Lint behaviour | multi-driver = warning | multi-driver = no warning |
| Reviewer expectation | "if multi-driver, ask why" | "what's the mutex contract?" |
| Typical use | every interconnect | tri-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.
`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)
);
endmodulelink 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.
`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;
endmoduleTwo things are wrong here:
- The mutex contract is not enforced. When
sel_aandsel_bare both asserted, both drivers are active with potentially different values, and the bus resolves toX. - 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:
`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;
endmoduleSame 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.
`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;
endmoduleThree 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).
`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;
endmoduleThis 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 A | Driver B | Resolved net |
|---|---|---|
0 | 0 | 0 |
0 | 1 | X |
0 | Z | 0 |
0 | X | X |
1 | 0 | X |
1 | 1 | 1 |
1 | Z | 1 |
1 | X | X |
Z | Z | Z |
Z | X | X |
X | X | X |
The pattern reduces to four rules a working engineer carries by reflex:
- Identical non-
X, non-Zdrivers → the value (0/0=0;1/1=1). - One driver
Z→ the other driver wins (whether0,1, orX). - Conflict (
0/1) →X. - Any
X→X(with one exception:XandZ→X).
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 cyclesThe 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 cyclesThe 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:
wirewith 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.triwith a single driver synthesises to the same metal as plainwire. Thetrikeyword does not add or remove any gate; it only changed the lint behaviour at RTL.triwith 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 thedata_*signals and the select lines come from theoe_*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.
// 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
trito 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(orinout) 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/IBUFon 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:
// 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
wireis declared at module scope but everyassignthat drives it is gated by a condition that's always false on this revision. - The
wireis a module port that the parent forgot to connect. - The
wireis a multi-drivertriwhose drivers all happen to be releasing (alloe_*low).
Debug method.
- In the waveform, locate the first cycle where the suspect signal becomes
Z. Inspect the drivers of that signal at exactly that cycle. - For port-level floats: search the parent instantiation; verify the port is in the connection list. Some lint suites and simulators flag unconnected
inputports as warnings — turn that warning on. - For internal
wirefloats: grepassign\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.
- 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).
- Inspect every line that drives the net: search for
assign\s+<name>\s*=and look for module-output ports that also drive it. - If the multi-driver shape is intentional (tri-state bus), redeclare as
triso 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.
- Verify the driver's reset is correct — typically the driver should produce a defined value (e.g.
8'h00) whenrst_nis asserted, not stay tri-stated. - Check that any tri-state driver's
oe_*is deasserted during reset (sotri-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:
- Multi-driver contention — two drivers active with different values (see §16.2).
Zpropagated through an operator — most Verilog operators promoteZtoX(e.g.Z & 1=X). The originalZcame from a floating net (§16.1).Xfrom an uninitialisedregthat was assigned to thewirevia a port connection.
Debug method.
- 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 carriesX). - If the upstream signal is a
wireshowingZor driven by anX-bearingreg, you've found the source. - Use the simulator's
$showvarsor waveform "drive arrow" feature to identify whichassignis producing theX.
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.
- Read the synthesis report. Search for "tri-state buffer rewritten" or "multiplexer inferred from tri-state".
- Run gate-level simulation; compare against RTL simulation at the cycle of divergence.
- If you actually need tri-state on silicon, the signal must exit the module hierarchy as a top-level
inoutport.
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:
- Every
trideclaration 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. - No
triin internal hierarchy. Atri-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. - Every
wirehas a single driver. Lint should flag multi-driverwire; review catches the cases where the warning was suppressed. - 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 byassert_one_hot_oeSVA" is the right level of detail. `default_nettype noneat the top of every file. No exceptions. This is non-negotiable in any working RTL house.- No implicit net declarations. With
default_nettype nonein place, every identifier referenced must be declared explicitly. Lint catches the residue. - Port keyword written explicitly.
input wire clkis clearer thaninput clk. Most lint suites require it. - Naming convention reflects the type. Top-level tri-state I/Os are typically named
io_<bus>or<bus>_io; internalwires use the producer's signal name (e.g.link,payload,enable). Atri ack_o;would raise an eyebrow —ackis 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.
`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;
endmoduleWhat 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_valuedrives 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-distinct — wire 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:
wireeverywhere internal. Single-driver, point-to-point. The boring choice is the right one for 95% of RTL.trionly at chip-pad boundaries. Internal multi-drivertrigets 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 noneat 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
trideclaration 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, thetrideclaration 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.
Related Tutorials
- Physical Data Types — Chapter 5.1; the parent overview of the whole net family.
- Variables & Data Types — Chapter 5; nets and variables together.
- RTL Designing — Chapter 3; how net discipline plays out across a real module.
- Operator Usage — Chapter 4.3; how
XandZpropagate through every operator. - Number Representation — Chapter 4.4; the
1'bz,8'bzliteral syntax used to release a tri-state driver.