Skip to content

Verilog · Chapter 5.1.5 · Data & Variables

tri0 and tri1 Nets in Verilog

Many real shared buses have a board-level pull-up or pull-down resistor that holds the line at a defined value when no active driver is asserting. Open-drain families like I2C, JTAG, and SMBus rely on a pull-up, while open-emitter buses and active-low reset lines rely on a pull-down. You can model these with an explicit strength assignment, but that is verbose. Verilog offers a shorter path: declare the net as tri0, which has an implicit pull-down and defaults LOW, or tri1, which has an implicit pull-up and defaults HIGH. The keyword is the resistor, so no explicit driver is needed. Unlike most of the strength-aware net family, the synthesis story here is good, since chip-pad I/O ports with pull-net declarations are supported by every modern flow.

Foundation18 min readVerilogtri0tri1Pull-UpPull-DownI2C

Chapter 5 · Section 5.1.5 · Data & Variables

1. The Engineering Problem

You are modelling an I²C SDA line. The bus has eight peripheral controllers; each peripheral drives 0 to assert and releases (drives Z) otherwise. A board-level 4.7 kΩ resistor pulls the line HIGH when every peripheral releases. The 5.1.2 Signal Strengths approach to modelling this:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wire sda;
 
// External pull-up resistor — pull1 driver, no 0-side
assign (pull1, highz0) sda = 1'b1;
 
// Eight peripherals — each drives 0 at strong when active
assign sda = periph[0] ? 1'b0 : 1'bz;
// ... repeat for periph[1] through periph[7]
assign sda = periph[7] ? 1'b0 : 1'bz;

The model works — the pull-up's pull1 driver loses to any peripheral's strong0 driver, and when all peripherals release, pull1 wins and the bus reads HIGH. The model is also nine assign statements, the first of which exists purely to model a piece of board hardware that has no internal logic.

The tri1 net type collapses this into one keyword. The implicit pull1 driver is baked into the net's declaration:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tri1 sda;
 
// Eight peripherals — each drives 0 at strong when active
assign sda = periph[0] ? 1'b0 : 1'bz;
// ... repeat for periph[1] through periph[7]
assign sda = periph[7] ? 1'b0 : 1'bz;

The pull-up assign is gone. The behaviour is identical — when all peripherals release, the tri1 net defaults to 1 at pull1 strength; when any peripheral asserts, the peripheral's strong0 overrides the implicit pull1 and the bus falls to 0. The keyword tri1 is the resistor. The companion tri0 does the symmetric job for pull-down topologies — active-high reset lines, ECL open-emitter buses, anywhere a shared net needs to default LOW.

This page is about those two keywords: what they declare, how the implicit pull driver participates in resolution, where they survive into synthesis and where they don't, and why they're the right choice for chip-pad-level open-drain modelling.

2. Why tri0 and tri1 Exist

The 5.1.2 page established the strength lattice and the eight named strength levels. The explicit pull-resistor pattern works:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wire bus;
assign (pull1, highz0) bus = 1'b1;   // pull-up
assign bus = drv_en ? drv_val : 1'bz; // driver

Two problems with this pattern in real codebases:

  1. The pull-up assign is noise. It carries no logic — it exists purely to inject a constant driver. In a module with twenty shared nets, twenty such pull-up assign statements add twenty lines of clutter without adding any algorithmic behaviour.
  2. The pull-up is easy to forget. If a code reviewer reads only the active drivers (the lines that gate on enables), they miss the pull-up entirely. A new revision adds a peripheral; the engineer copies the assign sda = ... ? ... : 1'bz; pattern; the implicit assumption is that the pull-up is somewhere else in the module — but it might not be. The bus then floats to Z whenever every peripheral releases, and the model produces X everywhere downstream.

The tri0 / tri1 keywords solve both problems by encoding the pull driver in the net's type. Declaring tri1 sda; is a one-line statement that says: "this net carries an implicit pull1 driver as part of its type." There is no second assign to forget; there is no possibility of a code review missing the pull-up. The net declaration is the pull-up declaration.

The two keywords cover the two pull directions:

  • tri1 — net carries an implicit pull1 driver (rank 5 toward 1). Default value when no other driver is active: 1 at pull1. Models a pull-up resistor.
  • tri0 — net carries an implicit pull0 driver (rank 5 toward 0). Default value when no other driver is active: 0 at pull0. Models a pull-down resistor.

The implicit driver participates in the standard strength-lattice resolution function. A strong driver (rank 6) overrides the implicit pull (rank 5) — exactly the open-drain electrical behaviour. A weak driver (rank 3) loses to the implicit pull — meaning the pull always wins against a leakage-replacement keeper. The net presents the resolved value at the resolved strength on every cycle.

3. Mental Model

The mental-model has three practical consequences:

  • The implicit driver is always at pull strength. Not strong, not weak. The strength is fixed by the language at pull (rank 5) — strong enough to win against a leakage / keeper / weak driver, weak enough to lose to a real CMOS active driver. This is precisely the electrical characteristic of a real pull-up resistor.
  • tri1 and tri0 work even with no active drivers. Unlike wire (which defaults to Z when undriven) or tri (same), a tri1 net is a defined value (1) from t = 0 onward — the implicit pull driver is "always active." This eliminates the initialisation-Z problem that motivates a reset / precharge phase on wire-based shared buses.
  • The active drivers must still output Z when releasing. The implicit pull driver does its job only when every other driver is Z. If an active driver outputs 1'b1 to mean "released" on a tri1, the resolution function sees the active driver's 1 at strong1 and ignores the implicit pull1 — which is fine in this case (both want 1) but confuses the intent. Always release with Z so the implicit pull driver is the sole contributor.

4. The Resolution Function

tri0 and tri1 reuse the standard wire resolution function from 5.1.1 with one addition: an implicit driver is always in the driver list. The driver's value is 0 (for tri0) or 1 (for tri1); its strength is pull0 or pull1 respectively.

4.1 tri1 resolution table

The two-driver table for a tri1 net (showing only the explicit driver; the implicit pull1 is always active in addition):

Explicit driverResolved netStrengthReason
0 at strong0strong0strong0 (rank 6) beats implicit pull1 (rank 5)
1 at strong1strong1both push to 1; strong wins
Z (released)1pull1implicit pull1 is the only contributor
X at strongXstrongstrong X poisons the rank-5 implicit
0 at pullXpullsame-strength tie (rank 5 vs rank 5), opposite values → X
1 at pull1pull1both push to 1; pull wins (same strength, same value)
0 at weak1pull1weak0 (rank 3) loses to implicit pull1 (rank 5)
1 at weak1pull1both push to 1; pull wins

Three rules a working engineer carries:

  1. Any active strong0 driver wins. The implicit pull1 is overridden; the net reads 0. Exactly the open-drain "peripheral asserts" case.
  2. Releasing the active driver returns the net to 1 at pull1. The default behaviour — the pull-up resistor pulls the line back HIGH.
  3. A same-strength opposing driver produces X. Writing assign (pull0, highz1) bus = 1'b0; on a tri1 produces 0 at pull0 vs 1 at pull1 — same rank, opposing values, X result. This is a code bug (you've added a competing pull-down to a pull-up net); the simulator correctly flags it.

4.2 tri0 resolution table

The mirror image — tri0 has an implicit pull0 driver. Same rules, opposite polarity:

Explicit driverResolved netStrengthReason
0 at strong0strong0both push to 0; strong wins
1 at strong1strong1strong1 (rank 6) beats implicit pull0 (rank 5)
Z (released)0pull0implicit pull0 is the only contributor
X at strongXstrongstrong X poisons
0 at pull0pull0both push to 0; pull wins (same strength, same value)
1 at pullXpullsame-strength tie, opposite values → X
0 at weak0pull0both push to 0; pull wins
1 at weak0pull0weak1 (rank 3) loses to implicit pull0 (rank 5)

The three rules are mirrored: any strong1 driver wins; releasing returns the net to 0 at pull0; same-strength opposing drivers produce X.

4.3 What tri0 / tri1 are NOT

A tri1 is not a tri with a default value of 1. The distinction matters:

  • A tri net resolves to Z when all drivers release. tri1 resolves to 1.
  • A tri net has no implicit driver. tri1 has an implicit pull1 driver always active.
  • A tri net's lint behaviour is "documents multi-driver intent." tri1 documents both multi-driver intent and the pull-direction.

A tri1 is also not a wand or wor — the resolution function is the standard contention-aware lattice from 5.1.2, not the wired-logic AND / OR functions from 5.1.3. A tri1 with two active drivers asserting different values returns X, not the AND or OR.

5. Syntax

The declaration grammar mirrors wire / tri exactly. Every option you can write for one, you can write for the other.

tri0-tri1-syntax.v — declaration anatomy
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
// <tri0 | tri1> [signed] [strength] [range] name [= continuous-assign];
 
tri1                        sda;            // I²C SDA line (open-drain, pull-up)
tri1        [7:0]           irq_n;          // 8-bit IRQ bus (open-drain)
tri0                        reset_b;        // active-low reset (pull-down)
tri0        [3:0]           err_status;     // 4-bit error bus (default LOW)
 
tri1 signed [15:0]          s_pulled;       // signed (rare)
tri1        (strong1, highz0) sda_strong;   // explicit strength override
 
// Continuous-assign shorthand — explicit driver, the implicit pull is still there
tri1 ack_b = ~busy;

Three syntax constraints worth knowing:

  • Strength specifier overrides the implicit driver's strength. Writing tri1 (strong1, highz0) bus; replaces the implicit pull1 driver with an implicit strong1 driver — which is electrically wrong for a pull-up resistor model. The strength specifier on a tri0 / tri1 declaration is almost never useful; if you need a non-pull default driver, use wire plus an explicit assign with the desired strength.
  • Vector tri0 / tri1 has the implicit driver per-bit. A tri1 [7:0] bus; carries eight independent pull1 drivers — one per bit. Each bit defaults to 1 at pull1 independently of the others. There is no "all-bits AND" or "all-bits OR" relationship.
  • signed is legal but pointless. A tri1 net default value is always 1 at every bit position; the signed modifier affects only the interpretation of multi-bit values in arithmetic operators, not the resolution function. Almost no production code uses signed tri1.

5.1 Implicit type — tri1 / tri0 cannot be the default

`default_nettype tri1 or `default_nettype tri0 is legal — it makes every undeclared net in the file default to tri1 (or tri0). This is exceptionally rare. The right default in any modern codebase is `default_nettype none (every identifier must be explicitly declared), which catches typos and forces conscious type choice.

If you find a file with `default_nettype tri1 at the top, it's almost certainly a chip-pad-level wrapper module where every shared net genuinely is pulled HIGH at the board layer. Even then, explicit tri1 declarations on each net are clearer than relying on the directive.

6. Visual Explanation

Three figures cover the tri0 / tri1 topology.

6.1 Visual A — tri1 open-drain bus

The I²C / open-collector / SMBus pattern. Multiple peripherals share one tri1 line; each peripheral drives 0 to assert and releases otherwise. The implicit pull1 driver holds the line HIGH when every peripheral releases.

tri1 open-drain bus — implicit pull-up holds the line HIGH

data flow
tri1 open-drain bus — implicit pull-up holds the line HIGH0 or Z0 or Z0 or Z0 (asserted) or 1 (default)0 (asserted)or 1…tri1 netimplicit pull1 always activeperiph Adrives strong0 or Zperiph Bdrives strong0 or Zperiph Cdrives strong0 or Zconsumerreads bus
The implicit pull1 driver is not visible as a code line — it is part of the tri1 net's type. When every peripheral releases (drives Z), the implicit pull1 wins and the bus reads 1. When any peripheral drives 0 at strong, the strong0 overrides the implicit pull1 and the bus reads 0.

6.2 Visual B — tri0 pull-down bus

The active-high default-low pattern. Useful for distributed-status reporting (the bus reads 0 when nothing is happening; any reporting module drives 1 to signal an event), and for active-low reset lines that default to "in reset" until a driver releases.

tri0 bus topology — three modules + implicit pull-downmodule Adrives strong1 or Zmodule Bdrives strong1 or Zmodule Cdrives strong1 or Ztri0 netimplicit pull0 alwaysactiveconsumerreads 0 (default) or 1(asserted)12
Symmetric to the tri1 case: the implicit pull0 driver holds the bus LOW when all explicit drivers release. Any explicit strong1 driver overrides the implicit pull0 and pulls the bus HIGH. Mirror polarity, same mechanism.

6.3 Visual C — tri1 vs tri resolution on the same drivers

The keyword choice changes the default-state behaviour. Same drivers, same active cycles; declared as tri, the bus floats to Z between asserts and propagates X downstream. Declared as tri1, the bus reads 1 between asserts — the implicit pull-up holds it HIGH.

tri vs tri1 — same drivers, different default statedriverasserts 0 once, thenreleasestri netZ when released → Xdownstreamtri1 net1 when released → validdownstreamconsumerreads Xconsumerreads 1 (default)12
Both nets carry the same explicit drivers: one driver asserts 0 at cycle 2, then releases. On a tri net, cycles 0–1 and 3+ read Z because no driver is active — Z propagates as X through downstream operators. On a tri1, cycles 0–1 and 3+ read 1 at pull1 because the implicit driver is always active. The downstream consumer reads valid digital values throughout.

7. I²C SDA Line Example

The canonical use case for tri1. Multiple I²C devices share one SDA line; each device drives 0 via its open-drain output stage and releases (drives Z) otherwise. The board-level 4.7 kΩ resistor pulls SDA HIGH between asserts.

rtl/i2c_sda_bus.v — 4-device I²C SDA bus model
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module i2c_sda_bus (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [3:0]  dev_drive_low,    // per-device assert-low signal
    output tri1        sda                // I²C SDA — implicit pull1 from board resistor
);
    // Each device drives 0 when it wants to assert; releases (Z) otherwise.
    // The implicit pull1 driver baked into tri1 holds SDA HIGH when all
    // devices release — exactly the board-level pull-up resistor's job.
 
    assign sda = dev_drive_low[0] ? 1'b0 : 1'bz;
    assign sda = dev_drive_low[1] ? 1'b0 : 1'bz;
    assign sda = dev_drive_low[2] ? 1'b0 : 1'bz;
    assign sda = dev_drive_low[3] ? 1'b0 : 1'bz;
endmodule

Walk the resolution function for the three states:

  • No device asserting. All four explicit drivers output Z. The implicit pull1 driver is the only active contributor. Resolution: sda = 1 at pull1.
  • One device asserting. One driver outputs 0 at strong0. Three explicit drivers output Z. Implicit pull1 is active. Resolution: strong0 (rank 6) beats pull1 (rank 5). sda = 0 at strong0.
  • Two devices asserting. Two drivers output 0 at strong0. The other two release. Implicit pull1 is overridden by either strong0. Resolution: 0 at strong0 — the wire-resolution rule for two identical 0 drivers is just 0, no contention.

Compare against the 5.1.2 explicit-strength model — the only difference is that the 5.1.2 version has a fifth assign (pull1, highz0) sda = 1'b1; line for the pull-up resistor. The tri1 version drops that line because the keyword is the resistor declaration. Same behaviour, one fewer line per shared bus, no possibility of "forgot the pull-up" review-time bugs.

7.1 Why tri1 is preferred over the explicit-strength form for I²C

In every working I²C bus model, the board-level pull-up is a constant — it never changes value, it never decay, it is electrically a 4.7 kΩ resistor tied to VDD. There is no algorithmic information in the explicit assign (pull1, highz0) sda = 1'b1; line beyond "this net has a pull-up." Encoding that in the keyword (tri1) removes the algorithmic-looking syntax (an assign statement) that the line never deserved.

The 5.1.2 explicit-strength form is the general tool — it handles cases where the pull is asymmetric (pull1 only, no pull0-side), where the pull strength varies across cycles, or where the pull driver gates on a condition. tri1 is the specific tool for the common case: constant pull, never changes, never gates. Pick the specific tool when the case is specific.

8. Active-Low Reset Line Example

The active-low reset pattern. A system has an external reset_b line that pulls LOW during reset assertion and releases (back to HIGH) when the system is running. A tri0 declaration would model the inverse — a pull-down resistor holding the line LOW by default, with the explicit driver(s) pulling HIGH to signal "run."

rtl/active_high_busy_signal.v — tri0 with default-low semantics
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module active_high_busy_signal (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [3:0]  module_busy,     // per-module busy flag
    output tri0        sys_busy          // pull-down: default LOW (no module busy)
);
    // Each module drives 1 when it is busy; releases (Z) otherwise.
    // The implicit pull0 baked into tri0 holds sys_busy LOW when no
    // module is asserting — modelling a board-level pull-down resistor.
 
    assign sys_busy = module_busy[0] ? 1'b1 : 1'bz;
    assign sys_busy = module_busy[1] ? 1'b1 : 1'bz;
    assign sys_busy = module_busy[2] ? 1'b1 : 1'bz;
    assign sys_busy = module_busy[3] ? 1'b1 : 1'bz;
endmodule

The walk mirrors the I²C case:

  • No module busy. All four explicit drivers output Z. Implicit pull0 wins. sys_busy = 0 at pull0.
  • One module busy. One driver outputs 1 at strong1. Resolution: strong1 (rank 6) beats pull0 (rank 5). sys_busy = 1 at strong1.
  • Multiple modules busy. Multiple strong1 drivers; wire resolution gives 1 (no contention since all push to 1). sys_busy = 1 at strong1.

The pattern is the wired-OR aggregation from 5.1.3 (any module asserting wins) but expressed via the strength lattice instead of the AND/OR resolution function. Both produce the same behaviour; the tri0 form makes the electrical model explicit (this is a pull-down resistor + multiple drivers), and the wor form makes the logical function explicit (this is OR of all drivers). Pick the form that matches the modelled physical layer.

9. JTAG TDO Bus Example

A common chip-test pattern. Multiple devices on a board share a JTAG TDO line. When a device is selected for scan-out, it drives TDO with its scan data; when unselected, it tri-states (releases). The board-level pull-up holds TDO HIGH when no device is selected.

rtl/jtag_tdo_bus.v — multi-device JTAG TDO bus
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module jtag_tdo_bus (
    input  wire        tck,
    input  wire        trst_n,
    input  wire [3:0]  device_selected,   // one-hot device select
    input  wire [3:0]  device_tdo,        // each device's TDO bit
    output tri1        tdo                 // JTAG TDO — pull-up at board level
);
    // Each device drives device_tdo[i] when device_selected[i] is HIGH,
    // releases otherwise. The implicit pull1 in tri1 holds TDO HIGH
    // when no device is selected.
 
    assign tdo = device_selected[0] ? device_tdo[0] : 1'bz;
    assign tdo = device_selected[1] ? device_tdo[1] : 1'bz;
    assign tdo = device_selected[2] ? device_tdo[2] : 1'bz;
    assign tdo = device_selected[3] ? device_tdo[3] : 1'bz;
endmodule

The mutex contract here is one-hot selection — exactly one device is selected at a time. The tri1 resolution function does not enforce this; it produces correct values only as long as the one-hot constraint holds. If two devices are selected and one drives 0 while the other drives 1, the resolution returns X (rank-6 same-strength conflict). Lint and SVA should enforce the mutex; the tri1 keyword documents the open-drain electrical model.

JTAG is the case where the tri1 keyword actually survives synthesis at the top-level port boundary. The pad cell for a TDO output has a real pull-up resistor option in most foundries; the tri1 declaration at the top-level chip wrapper tells the synthesis tool that the pad's pull-up should be enabled. Internal tri1 nets get rewritten as plain wire + a mux at the synthesis layer — but the top-level pad-level tri1 does the right thing.

10. Multi-Bit Pull Bus

Vector tri0 / tri1 nets carry an implicit pull driver per bit. Each bit resolves independently.

rtl/parallel_irq_bus.v — 4-bit parallel IRQ bus
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module parallel_irq_bus (
    input  wire        clk,
    input  wire [3:0]  periph_a_drive_low,   // 4-bit assert from peripheral A
    input  wire [3:0]  periph_b_drive_low,   // 4-bit assert from peripheral B
    output tri1 [3:0]  irq_n                  // 4-bit open-drain bus
);
    // Each peripheral drives all 4 bits in parallel.
    assign irq_n = (|periph_a_drive_low) ? ~periph_a_drive_low : 4'bz;
    assign irq_n = (|periph_b_drive_low) ? ~periph_b_drive_low : 4'bz;
endmodule

The four bits of irq_n resolve independently. Bit 0: if either peripheral asserts bit 0 (drives it to 0), the bus bit reads 0; otherwise the implicit pull1 for that bit reads 1. Bit 1 is the same pattern, independent of bit 0. Etc.

The pattern is the same as scalar tri1 with four parallel instances. The keyword scales cleanly — there is no "vector tri1" semantic distinct from "parallel scalar tri1s."

11. Simulation Behavior

The simulator implements tri0 / tri1 by adding an implicit driver to the net's driver list. The implicit driver is always active (the simulator does not need to track when it changes — it doesn't change). The resolution function runs the same code path as for wire / tri, just with one extra driver in the list.

The implicit driver participates in every cycle's resolution:

  • tri1 implicit driver: value 1, strength pull1 (rank 5 toward 1, rank 0 toward 0 — i.e., highz0).
  • tri0 implicit driver: value 0, strength pull0 (rank 0 toward 1, rank 5 toward 0).

The fact that the implicit driver is asymmetric (only on one side) means it composes correctly with active drivers on the other side:

  • A tri1 with one strong0 driver: 1-side max is pull1 (rank 5); 0-side max is strong0 (rank 6). The 0 side wins, net resolves to 0 at strong0. Exactly the open-drain "peripheral asserts" semantic.
  • A tri1 with one strong1 driver: 1-side max is strong1 (rank 6); 0-side max is highz0 (rank 0). The 1 side wins, net resolves to 1 at strong1. The strong1 overrides the implicit pull1 (same direction, higher strength).

The simulator's waveform dump treats tri0 / tri1 exactly like a wire — the resolved value is 0, 1, Z, or X per bit, and the strength annotation is pull0 / pull1 / strong0 / strong1 / etc. depending on which driver wins. Reading a tri1 net in a waveform that shows Pu1 (or pl1) confirms the implicit pull driver is the winning contributor — no explicit driver is active.

12. Waveform Analysis

Two waveforms cover the tri1 open-drain bus and the tri0 distributed-OR bus.

12.1 Waveform #1 — tri1 I²C SDA

I²C SDA on tri1 — implicit pull-up holds HIGH between asserts

12 cycles
I²C SDA on tri1 — implicit pull-up holds HIGH between assertsno device asserting — implicit pull1 wins, sda = 1 at pull1no device asserting — …device 0 asserts → strong0 beats pull1, sda = 0 at strong0device 0 asserts → str…device 0 releases → back to pull1 at 1device 0 releases → ba…device 1 asserts → 0 at strong0device 1 asserts → 0 a…both d0 and d1 asserting → 0 at strong0 (no contention)both d0 and d1 asserti…all release → pull1 wins, back to 1all release → pull1 wi…clkd0_lowd1_lowsdastrengthpull1pull1strong0strong0pull1pull1strong0strong0strong0strong0pull1pull1t0t1t2t3t4t5t6t7t8t9t10t11
The strength row makes the resolution decision visible. The implicit pull1 driver carries the line whenever every explicit driver releases. A strong0 from any device overrides the pull1. Multiple strong0 drivers asserting simultaneously do not collide — the wire resolution treats identical values as one value.

The waveform reads exactly like the 5.1.2 strength-aware version of the same bus, with one important difference: there is no explicit assign (pull1, highz0) sda = 1'b1; line in the source code. The strength annotation pull1 on cycles 0–1, 4–5, and 10–11 is produced by the implicit driver baked into the tri1 net type. The keyword is doing the work; the source code is shorter by one line per bus.

12.2 Waveform #2 — tri0 distributed-busy bus

Distributed-busy on tri0 — pull-down default + any-module-asserting wins

12 cycles
Distributed-busy on tri0 — pull-down default + any-module-asserting winsno module busy — pull0 wins, sys_busy = 0 at pull0no module busy — pull0…m0 busy → strong1 beats pull0, sys_busy = 1 at strong1m0 busy → strong1 beat…m0 done → back to pull0 at 0m0 done → back to pull…m1 busy → 1 at strong1m1 busy → 1 at strong1m1 + m2 both busy → 1 at strong1m1 + m2 both busy → 1 …all idle → pull0 wins, back to 0all idle → pull0 wins,…clkm0_busym1_busym2_busysys_busystrengthpull0pull0strong1strong1pull0pull0strong1strong1strong1strong1pull0pull0t0t1t2t3t4t5t6t7t8t9t10t11
Mirror of the tri1 waveform. The implicit pull0 driver holds the bus LOW when no module is busy. Any module asserting strong1 overrides the pull0. Multiple modules busy do not collide. The default state is LOW; the asserted state is HIGH — the canonical 'distributed-busy' aggregation.

The two waveforms together show the full polarity range of pull-net behaviour. The keyword choice (tri1 vs tri0) selects which polarity is the default state; everything else about the model — driver semantics, resolution, downstream consumer behaviour — is symmetric.

13. Synthesis Insight

tri0 and tri1 have the best synthesis story of any net type in the strength-aware family.

The reason: chip-pad I/O cells in every modern foundry include configurable pull-up and pull-down options. A pad's pull behaviour is set during chip-level integration (the engineer specifies "this pad uses the pull-up option") and shows up in the synthesised netlist as a property of the pad cell instance. The tri0 / tri1 keyword at the top-level port boundary tells the synthesis tool: "this port's pad cell should have its pull-up (or pull-down) option enabled."

The result is a clean three-layer story:

  1. Top-level port declared tri0 / tri1. Synthesis sees the keyword, enables the corresponding pad pull option, generates a netlist that includes the pull annotation. The downstream pad-level simulation matches the RTL behaviour exactly.
  2. Internal hierarchical tri0 / tri1. Synthesis tools typically rewrite these to plain wire + a mux at the boundary. The "default value when no driver" semantic becomes a mux's default-input branch. Functional behaviour is preserved (the same logic value comes out), but the multi-driver shape disappears in the netlist — same caveat as internal tri (5.1.1).
  3. Testbench tri0 / tri1. Simulation-only; synthesis doesn't see it. Used freely for modelling external buses with board-level pulls.

The synthesis-friendly use case (case 1) is the one that makes tri0 / tri1 worth using even in modern flows. Every chip with an I²C controller, every JTAG-enabled chip, every chip with active-low reset has at least one top-level inout port that needs a tri1 or tri0 declaration to capture the board-level pull. The keyword belongs in the chip-level wrapper module's port list.

chip_top.v — top-level wrapper with tri1 / tri0 pad ports
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module chip_top (
    input  wire        clk,
    input  wire        rst_n,                // standard input — pad has no pull
    inout  tri1        sda,                  // I²C SDA — pad pull-up enabled
    inout  tri1        scl,                  // I²C SCL — pad pull-up enabled
    inout  tri0        wakeup_high,          // wake-on-event — pad pull-down enabled
    output wire        gpio_q                // standard output — pad has no pull
);
    // ... internal logic, using wire for everything internal
endmodule

The pad-level pull is configured at the synthesis layer based on the port's net type. No assign (pull1, highz0) clutter needed; the keyword is the configuration.

14. Industry Use Cases

The three places tri0 / tri1 actually show up:

14.1 Chip-pad open-drain I/O

Every I²C peripheral chip, every SMBus device, every chip with a JTAG TDO, every chip with an open-drain interrupt output — the top-level wrapper declares the relevant pads as tri1 for the pull-up configuration. This is the synthesisable case; the pad-cell library carries the actual pull-up resistor at the I/O ring layer. The tri1 keyword in the port list tells synthesis to enable that pad option.

14.2 Top-level reset and wake-up lines

A chip with an active-low external reset typically declares the reset port as tri1 (pull-up = default to "out of reset"). A chip with an active-high wake-up signal declares the wake port as tri0 (pull-down = default to "asleep"). Both are board-level conventions captured in the pad cell's pull option, declared at the top-level wrapper.

14.3 System-level testbench bus modelling

Verification environments simulating multi-chip board-level systems declare shared buses (I²C between two chips, JTAG between several devices, distributed status lines) as tri0 / tri1 to capture the board-level resistor without writing the explicit assign (pull1, highz0) line per bus. This is the simulation-only use case; the testbench doesn't go to synthesis, and the keyword is purely for clarity and brevity.

In none of these cases is tri0 / tri1 used in internal synthesisable RTL. The keyword's value is in the pad-level configuration (case 14.1, 14.2) or in the testbench-level brevity (case 14.3). Internal RTL stays on plain wire with explicit muxes.

15. Common Mistakes

Five pitfalls that catch engineers reading or writing tri0 / tri1 code.

15.1 Adding a redundant pull-up assign on a tri1

The most common over-correction. An engineer declares tri1 sda; and then writes assign sda = 1'b1; because they're not sure if the implicit driver is "really there." The redundant assign creates a strong1 driver (default strength for an unannotated assign) that overrides the implicit pull1 — the net is now permanently at 1 at strong1, and any peripheral asserting 0 would create a same-strength contention (strong0 vs strong1) producing X. The fix is to remove the redundant assign; the keyword tri1 is the pull-up.

15.2 Using tri1 for digital state

The keyword retains a default value when no driver is active — superficially similar to a flip-flop's "hold" behaviour. But tri1's default is always 1 at pull1 — not the last driven value. A signal that needs to "remember its last value" is a flop or latch (reg + clocked always); a signal that needs to "default to 1 when nothing drives" is a tri1. The two are different semantics; using tri1 for state produces a signal that resets to 1 every time the active drivers release, which is almost certainly not the intended behaviour.

15.3 Forgetting that tri1 and tri0 cannot be the default default_nettype

`default_nettype tri1 is legal but exceptionally rare. Files written with `default_nettype none (the working-engineer default) require explicit declarations of every net type — including tri1 and tri0. Writing a net's name in an inout port list without an explicit tri1 keyword does not implicitly declare it as tri1; it produces a compile error under default_nettype none. The fix is to always write the net type explicitly.

15.4 Same-strength opposing pulls

A tri1 declaration adds an implicit pull1. Adding an explicit assign (pull0, highz1) bus = 1'b0; on the same net creates a pull0 driver that competes with the implicit pull1. Same strength, opposing values → X. This is almost never intentional; the fix is to pick one polarity (either tri1 or tri0) and not add a competing explicit pull. If the design genuinely needs the bus to default to a value other than 0 or 1 (e.g., default Z for genuine tri-state), use plain tri and no pull.

15.5 Internal hierarchical tri1 vs port boundary

tri1 at a top-level pad port is synthesisable. tri1 on an internal hierarchical net gets rewritten by synthesis tools and may diverge between RTL and gates on Z-bearing cycles. The fix: use tri1 only at top-level ports. Internal RTL that needs a default value uses an explicit mux: assign internal_net = drv_en ? drv_val : default_val;. The mux's default branch carries the same information without the multi-driver shape.

16. Debugging Lab

Three tri0 / tri1 debug post-mortems

17. Coding Guidelines

  1. Use tri1 for shared nets with a board-level pull-up resistor. Open-drain buses, I²C SDA / SCL, JTAG TDO, distributed interrupts. Use tri0 for shared nets with a board-level pull-down.
  2. Drivers output Z for "released." The implicit pull driver only wins when every other driver is Z. Outputting 1 or 0 to mean released defeats the pull mechanism.
  3. tri0 / tri1 belong at top-level chip-pad ports. The pad cell's pull option is configured by the keyword. Internal hierarchical nets get rewritten by synthesis — use explicit muxes inside.
  4. Don't add a redundant assign net = 1'b1; to a tri1. The keyword is the pull-up; the redundant assign creates a strong driver that breaks the open-drain semantic.
  5. Pick polarity by default value, not by assertion polarity. tri1 = default 1 (line HIGH when released, active-LOW assertion). tri0 = default 0 (line LOW when released, active-HIGH assertion). The keyword name follows the default.
  6. tri0 / tri1 are not flip-flops. They default to 0 or 1 when undriven; they do not remember the last driven value. Use reg + clocked always for digital state.
  7. Vector tri0 / tri1 resolves per-bit. A 4-bit tri1 is four independent pull-up nets in parallel. There is no cross-bit interaction.
  8. Document the pull origin. A one-line comment naming the resistor and value ("4.7 kΩ board pull-up per I²C spec") tells the next engineer the keyword choice is intentional.

18. Interview Q&A

19. Exercises

Three exercises that turn the pull-net model into reflex.

Exercise 1 — Resolve the bus

For each scenario, predict the resolved value and strength on a tri1 net.

#Explicit driver 1Explicit driver 2Resolved (value, strength)
1ZZ?
20 at strong0Z?
30 at strong00 at strong0?
41 at strong1Z?
50 at weak0Z?
60 at pull0Z?
70 at strong01 at strong1?

Hints. Always include the implicit pull1 driver. Find the 1-side max and the 0-side max; the stronger side wins. Same-strength ties produce X.

Exercise 2 — Model an SMBus bus

Write a Verilog testbench module smbus_model that models a 2-line SMBus (SDA + SCL) with three devices. Specifications:

  • Inputs: wire [2:0] dev_sda_drive_low, wire [2:0] dev_scl_drive_low.
  • Outputs: tri1 smbus_sda, tri1 smbus_scl.
  • Each device drives 0 on the respective line when its corresponding bit is HIGH; releases otherwise.

Hints. Six assign statements — three for SDA, three for SCL. Each statement: device-bit ? 1'b0 : 1'bz. The tri1 keyword's implicit pull-up handles the released-state default value automatically.

Exercise 3 — Spot the polarity bug

The following module is intended to model a system where multiple modules can signal "I have an event" by driving a shared status line HIGH; the line defaults LOW when no module is signalling. Identify the bug and fix it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module event_bus (
    input  wire [3:0]  module_event,
    output tri1        sys_event
);
    assign sys_event = module_event[0] ? 1'b1 : 1'bz;
    assign sys_event = module_event[1] ? 1'b1 : 1'bz;
    assign sys_event = module_event[2] ? 1'b1 : 1'bz;
    assign sys_event = module_event[3] ? 1'b1 : 1'bz;
endmodule

What to produce. (a) Identify what's wrong with the keyword choice. (b) Rewrite using the correct keyword so the bus defaults LOW. (c) Explain why the original code would have sys_event stuck at 1 even when no module is signalling.

20. Summary

tri0 and tri1 are Verilog's pull-net types — nets that carry an implicit pull0 or pull1 driver as part of their type. The implicit driver participates in the standard strength-lattice resolution function: it holds the net at its default value (0 for tri0, 1 for tri1) when no other driver is active, and yields to any strong-strength explicit driver when one is active.

The mechanism is precise:

  • tri1 net. Implicit pull1 driver (rank 5 toward 1). Default value: 1. Models a board-level pull-up resistor.
  • tri0 net. Implicit pull0 driver (rank 5 toward 0). Default value: 0. Models a board-level pull-down resistor.

The resolution function is the standard wire resolution from 5.1.1, with the implicit driver always added to the driver list. A strong0 driver beats the implicit pull1; a weak0 driver does not.

The use cases are common:

  • Chip-pad open-drain I/O — I²C, JTAG TDO, SMBus, OneWire, open-drain interrupts.
  • Top-level reset and wake-up lines — active-low reset on tri1, active-high wake on tri0.
  • Multi-chip board-level testbench buses — shared nets between simulated devices with board-level resistors.

The synthesis story is uniquely good for the strength-aware family:

  • Top-level pad ports: synthesisable. The pad cell's pull option is configured from the keyword.
  • Internal hierarchical nets: rewritten by synthesis to wire + mux. Functional but loses the multi-driver shape.
  • Testbench code: simulation-only.

The day-to-day discipline:

  • Use tri0 / tri1 only at top-level chip-pad ports. Internal RTL uses plain wire with explicit muxes.
  • Drivers output Z for "released." The implicit pull driver wins only when every other driver is Z.
  • Polarity follows the default value, not the assertion polarity. tri1 defaults HIGH (active-LOW assertion); tri0 defaults LOW (active-HIGH assertion).
  • No redundant pull-up assigns. The keyword is the pull driver; adding assign net = 1'b1; creates a competing strong1 driver that breaks the open-drain semantic.

The final sibling sub-page is 5.1.6 supply0 / supply1 — net types with the supply strength (rank 7, the strongest possible) used to model the chip's power and ground rail ties. After that, the chapter moves to register data types (5.2) — variables that hold state across clock cycles, where reg, integer, and real carry the simulation's algorithmic state.

  • Signal Strengths — Chapter 5.1.2; the strength lattice the implicit pull driver rides on.
  • wire and tri Nets — Chapter 5.1.1; the baseline resolution function this page extends.
  • Wired Nets (wand and wor) — Chapter 5.1.3; the orthogonal logic-AND / OR resolution family.
  • trireg Nets — Chapter 5.1.4; the capacitive-storage net type that complements the strength lattice.
  • Physical Data Types — Chapter 5.1; the parent overview of the net family.