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:
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:
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:
wire bus;
assign (pull1, highz0) bus = 1'b1; // pull-up
assign bus = drv_en ? drv_val : 1'bz; // driverTwo problems with this pattern in real codebases:
- The pull-up
assignis noise. It carries no logic — it exists purely to inject a constant driver. In a module with twenty shared nets, twenty such pull-upassignstatements add twenty lines of clutter without adding any algorithmic behaviour. - 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 toZwhenever every peripheral releases, and the model producesXeverywhere 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 implicitpull1driver (rank 5 toward1). Default value when no other driver is active:1atpull1. Models a pull-up resistor.tri0— net carries an implicitpull0driver (rank 5 toward0). Default value when no other driver is active:0atpull0. 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
pullstrength. Notstrong, notweak. The strength is fixed by the language atpull(rank 5) — strong enough to win against a leakage / keeper /weakdriver, weak enough to lose to a real CMOS active driver. This is precisely the electrical characteristic of a real pull-up resistor. tri1andtri0work even with no active drivers. Unlikewire(which defaults toZwhen undriven) ortri(same), atri1net is a defined value (1) fromt = 0onward — the implicit pull driver is "always active." This eliminates the initialisation-Zproblem that motivates a reset / precharge phase onwire-based shared buses.- The active drivers must still output
Zwhen releasing. The implicit pull driver does its job only when every other driver isZ. If an active driver outputs1'b1to mean "released" on atri1, the resolution function sees the active driver's1atstrong1and ignores the implicitpull1— which is fine in this case (both want1) but confuses the intent. Always release withZso 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 driver | Resolved net | Strength | Reason |
|---|---|---|---|
0 at strong | 0 | strong0 | strong0 (rank 6) beats implicit pull1 (rank 5) |
1 at strong | 1 | strong1 | both push to 1; strong wins |
Z (released) | 1 | pull1 | implicit pull1 is the only contributor |
X at strong | X | strong | strong X poisons the rank-5 implicit |
0 at pull | X | pull | same-strength tie (rank 5 vs rank 5), opposite values → X |
1 at pull | 1 | pull1 | both push to 1; pull wins (same strength, same value) |
0 at weak | 1 | pull1 | weak0 (rank 3) loses to implicit pull1 (rank 5) |
1 at weak | 1 | pull1 | both push to 1; pull wins |
Three rules a working engineer carries:
- Any active
strong0driver wins. The implicitpull1is overridden; the net reads0. Exactly the open-drain "peripheral asserts" case. - Releasing the active driver returns the net to
1atpull1. The default behaviour — the pull-up resistor pulls the line back HIGH. - A same-strength opposing driver produces
X. Writingassign (pull0, highz1) bus = 1'b0;on atri1produces0 at pull0vs1 at pull1— same rank, opposing values,Xresult. 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 driver | Resolved net | Strength | Reason |
|---|---|---|---|
0 at strong | 0 | strong0 | both push to 0; strong wins |
1 at strong | 1 | strong1 | strong1 (rank 6) beats implicit pull0 (rank 5) |
Z (released) | 0 | pull0 | implicit pull0 is the only contributor |
X at strong | X | strong | strong X poisons |
0 at pull | 0 | pull0 | both push to 0; pull wins (same strength, same value) |
1 at pull | X | pull | same-strength tie, opposite values → X |
0 at weak | 0 | pull0 | both push to 0; pull wins |
1 at weak | 0 | pull0 | weak1 (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
trinet resolves toZwhen all drivers release.tri1resolves to1. - A
trinet has no implicit driver.tri1has an implicitpull1driver always active. - A
trinet's lint behaviour is "documents multi-driver intent."tri1documents 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.
`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 implicitpull1driver with an implicitstrong1driver — which is electrically wrong for a pull-up resistor model. The strength specifier on atri0/tri1declaration is almost never useful; if you need a non-pulldefault driver, usewireplus an explicitassignwith the desired strength. - Vector tri0 / tri1 has the implicit driver per-bit. A
tri1 [7:0] bus;carries eight independentpull1drivers — one per bit. Each bit defaults to1atpull1independently of the others. There is no "all-bits AND" or "all-bits OR" relationship. signedis legal but pointless. Atri1net default value is always1at every bit position; thesignedmodifier affects only the interpretation of multi-bit values in arithmetic operators, not the resolution function. Almost no production code usessigned 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 flow6.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.
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.
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.
`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;
endmoduleWalk the resolution function for the three states:
- No device asserting. All four explicit drivers output
Z. The implicitpull1driver is the only active contributor. Resolution:sda=1atpull1. - One device asserting. One driver outputs
0atstrong0. Three explicit drivers outputZ. Implicitpull1is active. Resolution:strong0(rank 6) beatspull1(rank 5).sda=0atstrong0. - Two devices asserting. Two drivers output
0atstrong0. The other two release. Implicitpull1is overridden by eitherstrong0. Resolution:0atstrong0— the wire-resolution rule for two identical0drivers is just0, 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."
`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;
endmoduleThe walk mirrors the I²C case:
- No module busy. All four explicit drivers output
Z. Implicitpull0wins.sys_busy=0atpull0. - One module busy. One driver outputs
1atstrong1. Resolution:strong1(rank 6) beatspull0(rank 5).sys_busy=1atstrong1. - Multiple modules busy. Multiple
strong1drivers;wireresolution gives1(no contention since all push to1).sys_busy=1atstrong1.
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.
`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;
endmoduleThe 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.
`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;
endmoduleThe 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:
tri1implicit driver: value1, strengthpull1(rank 5 toward1, rank 0 toward0— i.e.,highz0).tri0implicit driver: value0, strengthpull0(rank 0 toward1, rank 5 toward0).
The fact that the implicit driver is asymmetric (only on one side) means it composes correctly with active drivers on the other side:
- A
tri1with onestrong0driver: 1-side max ispull1(rank 5); 0-side max isstrong0(rank 6). The0side wins, net resolves to0atstrong0. Exactly the open-drain "peripheral asserts" semantic. - A
tri1with onestrong1driver: 1-side max isstrong1(rank 6); 0-side max ishighz0(rank 0). The1side wins, net resolves to1atstrong1. Thestrong1overrides the implicitpull1(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 cyclesThe 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 cyclesThe 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:
- 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. - Internal hierarchical
tri0/tri1. Synthesis tools typically rewrite these to plainwire+ 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 internaltri(5.1.1). - 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.
`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
endmoduleThe 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
- Use
tri1for shared nets with a board-level pull-up resistor. Open-drain buses, I²C SDA / SCL, JTAG TDO, distributed interrupts. Usetri0for shared nets with a board-level pull-down. - Drivers output
Zfor "released." The implicit pull driver only wins when every other driver isZ. Outputting1or0to mean released defeats the pull mechanism. tri0/tri1belong 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.- Don't add a redundant
assign net = 1'b1;to atri1. The keyword is the pull-up; the redundantassigncreates astrongdriver that breaks the open-drain semantic. - Pick polarity by default value, not by assertion polarity.
tri1= default1(line HIGH when released, active-LOW assertion).tri0= default0(line LOW when released, active-HIGH assertion). The keyword name follows the default. tri0/tri1are not flip-flops. They default to0or1when undriven; they do not remember the last driven value. Usereg+ clockedalwaysfor digital state.- Vector
tri0/tri1resolves per-bit. A 4-bittri1is four independent pull-up nets in parallel. There is no cross-bit interaction. - 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 1 | Explicit driver 2 | Resolved (value, strength) |
|---|---|---|---|
| 1 | Z | Z | ? |
| 2 | 0 at strong0 | Z | ? |
| 3 | 0 at strong0 | 0 at strong0 | ? |
| 4 | 1 at strong1 | Z | ? |
| 5 | 0 at weak0 | Z | ? |
| 6 | 0 at pull0 | Z | ? |
| 7 | 0 at strong0 | 1 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
0on 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.
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;
endmoduleWhat 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:
tri1net. Implicitpull1driver (rank 5 toward1). Default value:1. Models a board-level pull-up resistor.tri0net. Implicitpull0driver (rank 5 toward0). 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 ontri0. - 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/tri1only at top-level chip-pad ports. Internal RTL uses plainwirewith explicit muxes. - Drivers output
Zfor "released." The implicit pull driver wins only when every other driver isZ. - Polarity follows the default value, not the assertion polarity.
tri1defaults HIGH (active-LOW assertion);tri0defaults LOW (active-HIGH assertion). - No redundant pull-up
assigns. The keyword is the pull driver; addingassign net = 1'b1;creates a competingstrong1driver 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.
Related Tutorials
- 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.