I²C · Module 2
Open-Drain Outputs — Drive Low, Release High
The architectural move the whole bus rests on, and it is a subtraction: delete every device's ability to drive HIGH. What remains is one switch to ground, so the two states are pull LOW and release — and two devices can never impose opposite levels because only one level can be imposed at all.
Chapter 2.2 ended with a requirement rather than a solution: the conflicting condition has to become inexpressible, not merely discouraged. Two devices imposing opposite levels on one conductor is an electrical fault with no defined outcome, and no rule enforced by the participants themselves can be trusted to prevent it.
The answer is the most important single idea in this curriculum, and its form is worth noticing before its content. It is a subtraction. Nothing is added — no arbiter, no strength negotiation, no enable protocol. A capability is removed, and the problem disappears with it.
1. Remove the Capability That Caused the Conflict
The conflict required two things to be simultaneously true: one device actively establishing HIGH, and another actively establishing LOW. Remove either and there is nothing to conflict.
Removing the ability to drive LOW would be useless — LOW is how a device makes itself felt, and Chapter 2.1 established that asserting a line is the entire behavioural vocabulary the bus has.
So remove the other one. Take away every device's ability to actively drive the conductor HIGH. What remains is an output stage with a single controllable path: from the node to ground.
The consequences are immediate. A device can now pull the conductor toward ground, or it can do nothing at all. It has no way to oppose another device, because the only thing it can impose is the same thing every other device would impose. Two devices acting at once are both pulling toward ground — which is not a conflict, because they agree.
This output structure is called open-drain. The name describes the circuit: the controllable element's output terminal — its drain — is left open, connected to the pin and to nothing else inside the device. (In older bipolar parts the equivalent arrangement is open-collector; the behaviour is the same and the terms are used interchangeably in practice.)
2. The Switch Model
For everything in this curriculum, the useful abstraction is a switch. Forget the semiconductor detail; what matters is the behaviour at the pin.
Switch closed. The output node is connected to ground through a low-impedance path. The device is actively pulling the conductor LOW, exactly as a push-pull output's ground path would.
Switch open. The output node is connected to nothing inside the device. The device is not influencing the conductor at all — it has neither pulled it low nor pushed it high. Electrically the pin has become high impedance, and whatever the conductor does next is somebody else's business.
That second state is the one that takes getting used to, because it has no equivalent in ordinary logic. An ordinary output always has an opinion. An open-drain output has exactly one opinion it can express, and its alternative is silence.
3. Two States, and Why Naming Them Correctly Matters
It is tempting to describe the two states as "drive 0" and "drive 1". That description is wrong, and the wrongness is not pedantic — it is the single most productive correction in this module.
The states are:
Pull LOW — an active, low-impedance connection to ground. The device is doing something.
Release — the switch is open. The device is doing nothing.
Releasing is not driving HIGH. It is declining to drive at all. A released line is not HIGH because this device made it so; it is HIGH only if nothing else is pulling it down and something else restores it — which is precisely the question this chapter ends on.
Once that distinction is internalised, several later behaviours stop being surprising. A device can release a line and find it still LOW, because another participant is pulling it. That is not a contradiction or a failure; it is the expected consequence of release meaning absence of influence rather than assertion of a level. Several of the bus's most important mechanisms are built on exactly that observation, and Chapter 2.5 develops it.
An open-drain device pulling and releasing, with nothing else attached yet
8 cyclesLook carefully at what the released intervals show. They are not drawn HIGH. Nothing introduced so far can make this conductor HIGH — every device on it has had that capability deliberately removed, and a conductor connected to nothing has no defined level at all. The figure is not being coy; it is recording the actual state of the argument.
4. What This Buys, Immediately
Before answering the open question, notice how completely the original problem has gone.
Put two open-drain devices on one conductor. Device A pulls; Device B releases. There is one active path to ground, and the node is LOW. Now swap them: B pulls, A releases. One path to ground, node LOW. Now both pull. Two parallel paths to ground, node still LOW — and crucially, no current flows between the two devices, because neither is a source. Both are sinks, and they are sinking to the same place.
There is no combination of two open-drain devices that produces a supply-to-ground path through the pair. The fault from Chapter 2.2 is not merely unlikely; it is unrepresentable. No device has the capability required to be one half of it.
That is what makes this a structural fix rather than a safeguard. There is no rule to violate, no timing window to miss, and no need for participants to be well-behaved. A confused device, a half-initialised device, a device coming out of reset with its outputs in an unexpected state — none of them can create the fault, because none of them can drive HIGH.
5. Modelling Open Drain Correctly in RTL
This is the chapter's most practically important section, because the majority of broken I²C hardware fails here — at the boundary, in the one line that decides what the pin does.
Chapter 2.1 argued for a four-port interface: sda_drive_low and scl_drive_low for intent, sda_in and scl_in for observation. That port list now becomes executable, and the block that implements it is tiny — which is the point. The whole open-drain idea is one conditional assignment, and getting that one line right or wrong decides whether the design can share a bus at all.
The behaviour every implementation below must produce:
drive_low | What the participant contributes to the line | Line level |
|---|---|---|
1 | an active low-impedance path to ground | 0 |
0 | nothing — high impedance | whatever other participants and the pull-up determine |
Note the second row carefully. The contribution is Z, not 1. A participant that is not pulling has no opinion, and the line's level in that case is somebody else's business.
5a. SystemVerilog
Two type decisions are doing real work, and both are easy to get wrong.
The shared line must be a net (wire or tri), not a logic variable. A net has multi-driver resolution semantics; a variable does not, and two drivers on one logic variable is a compile error rather than a bus. logic is the right default for almost everything in modern SystemVerilog and the wrong choice here — and "use logic everywhere" is precisely how this bug gets written.
The pull-up is modelled with the pullup primitive, which contributes a weak 1. Weak is the operative word: it loses to any strong driver, so a participant pulling LOW wins, and it prevails only when every participant has released. That is the electrical arrangement rather than an approximation of it.
module od_participant (
input logic drive_low, // 1 = pull the line LOW; 0 = release it
inout wire line, // the shared conductor -- a NET, so it resolves
output logic line_in // what the line actually resolved to
);
// THE open-drain line. Contribute 0 when pulling, contribute NOTHING (high
// impedance) otherwise. There is deliberately no branch that contributes 1:
// this participant has no way to drive the line HIGH, which is the entire
// architectural point of Chapter 2.3.
assign line = drive_low ? 1'b0 : 1'bz;
// Observation is a separate port from intent. A participant that released
// may still read 0, because another participant can be holding the line.
assign line_in = line;
endmodule module od_bus (
input logic a_low,
input logic b_low,
output logic a_sees,
output logic b_sees,
output wire line
);
// The pull-up: a WEAK 1 on the net. It loses to any strong 0, so it
// establishes HIGH only when every participant has released. Without it
// a fully released line would sit at Z with no defined level at all.
pullup (line);
od_participant pa (.drive_low(a_low), .line(line), .line_in(a_sees));
od_participant pb (.drive_low(b_low), .line(line), .line_in(b_sees));
endmoduleThe testbench proves the four properties a participant depends on, and its third and fourth checks are the ones that matter for everything later: a participant that releases while another pulls observes LOW, and that is correct rather than a fault.
module od_bus_tb;
logic a_low, b_low;
logic a_sees, b_sees;
wire line;
int errors = 0;
od_bus dut (.a_low(a_low), .b_low(b_low), .a_sees(a_sees), .b_sees(b_sees), .line(line));
// `===` throughout: X and Z must be compared literally.
task automatic chk(input logic exp_line, input string what);
#1;
if (line !== exp_line) begin
$error("%s: line = %b, expected %b", what, line, exp_line); errors++;
end
// Every participant must observe the SAME resolved level -- there is one
// conductor, so disagreement here would mean the model is wrong.
if (a_sees !== line || b_sees !== line) begin
$error("%s: observation disagrees with the line (a=%b b=%b line=%b)",
what, a_sees, b_sees, line); errors++;
end
endtask
initial begin
// 1 -- NOBODY PULLS: the pull-up establishes HIGH. Note that no
// participant drove this 1; it is the absence of any pull.
a_low = 0; b_low = 0; chk(1'b1, "both release -- pull-up establishes HIGH");
// 2 -- ONE PULLS: a strong 0 beats the weak pull-up.
a_low = 1; b_low = 0; chk(1'b0, "A pulls, B releases");
a_low = 0; b_low = 1; chk(1'b0, "B pulls, A releases");
// 3 -- BOTH PULL: two paths to the same ground. NOT contention -- both
// participants impose the same level and no current flows between
// them. This is the case that was destructive in Chapter 2.2.
a_low = 1; b_low = 1; chk(1'b0, "both pull -- agreement, not conflict");
// 4 -- THE KEY OBSERVATION: A releases while B still pulls. A reads 0.
// This is not a failure of A's release; it is A discovering that
// somebody else is asserting the line. Chapter 2.5 builds on it.
a_low = 0; b_low = 1; #1;
if (a_sees !== 1'b0) begin
$error("a participant that released must still observe the real level"); errors++;
end
// 5 -- NO ACTIVE HIGH ANYWHERE: with the pull-up removed from the
// picture, a released line has no driver at all. Proven here by
// construction: neither participant can contribute 1, so the only
// source of HIGH in the whole design is the pull-up primitive.
a_low = 0; b_low = 0; chk(1'b1, "release resolves HIGH only via the pull-up");
if (errors == 0) $display("PASS: open-drain -- any pull wins, release defers, HIGH comes from the pull-up alone");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule5b. Verilog
The same three modules. pullup is a Verilog primitive, not a SystemVerilog addition, so the electrical model is unchanged; only the typing and reporting idiom differ.
module od_participant (
input wire drive_low,
inout wire line,
output wire line_in
);
// Pull LOW, or contribute nothing. No branch contributes 1.
assign line = drive_low ? 1'b0 : 1'bz;
assign line_in = line;
endmodule module od_bus (
input wire a_low,
input wire b_low,
output wire a_sees,
output wire b_sees,
output wire line
);
pullup (line); // weak 1: loses to any strong 0
od_participant pa (.drive_low(a_low), .line(line), .line_in(a_sees));
od_participant pb (.drive_low(b_low), .line(line), .line_in(b_sees));
endmodule module od_bus_tb;
reg a_low, b_low;
wire a_sees, b_sees, line;
integer errors;
od_bus dut (.a_low(a_low), .b_low(b_low), .a_sees(a_sees), .b_sees(b_sees), .line(line));
task chk;
input exp_line;
input [8*48:1] what;
begin
#1;
if (line !== exp_line) begin
$display("FAIL: %0s: line = %b, expected %b", what, line, exp_line);
errors = errors + 1;
end
if (a_sees !== line || b_sees !== line) begin
$display("FAIL: %0s: observation disagrees with the line", what);
errors = errors + 1;
end
end
endtask
initial begin
errors = 0;
a_low = 0; b_low = 0; chk(1'b1, "both release -- pull-up establishes HIGH");
a_low = 1; b_low = 0; chk(1'b0, "A pulls, B releases");
a_low = 0; b_low = 1; chk(1'b0, "B pulls, A releases");
a_low = 1; b_low = 1; chk(1'b0, "both pull -- agreement not conflict");
a_low = 0; b_low = 1; #1;
if (a_sees !== 1'b0) begin
$display("FAIL: a released participant must observe the real level");
errors = errors + 1;
end
a_low = 0; b_low = 0; chk(1'b1, "release resolves HIGH via the pull-up alone");
if (errors == 0) $display("PASS: open-drain -- any pull wins, release defers, HIGH from pull-up only");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule5c. VHDL
VHDL reaches the same behaviour through std_logic's resolution function, and it is worth understanding rather than pattern-matching because it explains why the arrangement works.
std_logic includes the strength-bearing values '0' and '1' (strong), 'H' and 'L' (weak), and 'Z' (high impedance). The resolution function applied to multiple drivers says: a strong value beats a weak one, a weak value beats 'Z', and two opposing strong values give 'X'. That is precisely the electrical arrangement of an I²C line — so the pull-up is modelled as a driver contributing weak 'H', and it is beaten by any participant's strong '0' while prevailing over 'Z'.
The library also supplies to_X01, which maps 'H' to '1' — useful because a resolved line sitting at 'H' is a logic HIGH, and code reading the line should not have to special-case the strength.
std_ulogic would be wrong here for the same reason as in Chapter 2.2: it is unresolved, so a signal of that type with several drivers is rejected at elaboration rather than resolved.
library ieee;
use ieee.std_logic_1164.all;
entity od_participant is
port (
drive_low : in std_logic;
line : inout std_logic; -- resolved: several participants drive it
line_in : out std_logic -- what the line actually resolved to
);
end entity;
architecture rtl of od_participant is
begin
-- Pull LOW with a STRONG '0', or contribute 'Z'. No branch contributes '1'.
line <= '0' when drive_low = '1' else 'Z';
-- Observation. to_X01 maps the pull-up's weak 'H' onto '1' so that reading
-- code sees a plain logic level rather than a strength-qualified one.
line_in <= to_X01(line);
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity od_bus is
port (
a_low : in std_logic;
b_low : in std_logic;
a_sees : out std_logic;
b_sees : out std_logic;
line : inout std_logic
);
end entity;
architecture rtl of od_bus is
begin
-- THE PULL-UP, as a third driver contributing weak 'H'. std_logic resolution
-- gives a strong '0' priority over 'H', and 'H' priority over 'Z' -- which is
-- exactly what a resistor to the supply does against a transistor to ground.
line <= 'H';
pa : entity work.od_participant
port map (drive_low => a_low, line => line, line_in => a_sees);
pb : entity work.od_participant
port map (drive_low => b_low, line => line, line_in => b_sees);
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity od_bus_tb is
end entity;
architecture sim of od_bus_tb is
signal a_low, b_low : std_logic := '0';
signal a_sees, b_sees : std_logic;
signal line : std_logic;
-- The line resolves to 'H' when only the pull-up drives it, so checks compare
-- the NORMALISED level rather than the raw strength.
procedure chk (actual : in std_logic; expected : in std_logic; what : in string) is
begin
assert to_X01(actual) = expected
report "od_bus: " & what severity error;
end procedure;
begin
dut : entity work.od_bus
port map (a_low => a_low, b_low => b_low,
a_sees => a_sees, b_sees => b_sees, line => line);
stim : process
begin
a_low <= '0'; b_low <= '0'; wait for 1 ns;
chk(line, '1', "both release -- pull-up establishes HIGH");
a_low <= '1'; b_low <= '0'; wait for 1 ns;
chk(line, '0', "A pulls, B releases");
a_low <= '0'; b_low <= '1'; wait for 1 ns;
chk(line, '0', "B pulls, A releases");
a_low <= '1'; b_low <= '1'; wait for 1 ns;
chk(line, '0', "both pull -- agreement, not conflict");
-- THE KEY OBSERVATION: A has released and must still read the real level.
a_low <= '0'; b_low <= '1'; wait for 1 ns;
assert to_X01(a_sees) = '0'
report "od_bus: a released participant must observe the real level" severity error;
a_low <= '0'; b_low <= '0'; wait for 1 ns;
chk(line, '1', "release resolves HIGH via the pull-up alone");
report "od_bus self-check complete" severity note;
wait;
end process;
end architecture;5d. Cross-Language Comparison
The circuit is identical: two participants each contributing 0 or high-impedance, one weak driver establishing HIGH, one resolved conductor, and an observation path separate from intent. Ports, stimulus and expected results match across all three.
| Concern | SystemVerilog | Verilog | VHDL |
|---|---|---|---|
| The shared conductor | wire / tri — a net, because logic has no multi-driver resolution | wire | std_logic — a resolved type; std_ulogic would be rejected |
| Release | 1'bz | 1'bz | 'Z' |
| The pull-up | pullup primitive — weak 1 | pullup primitive | a driver contributing weak 'H' |
| Reading the line | net value directly | net value directly | to_X01(line) normalises 'H' to '1' |
| Why it resolves | built-in net resolution | built-in net resolution | std_logic's declared resolution function |
The VHDL row is the most instructive. Verilog and SystemVerilog give you the right answer through built-in net semantics you cannot inspect; VHDL makes the strength ordering explicit in a function you can read. Both describe the same resistor-versus-transistor arrangement, and a designer who understands the VHDL version understands why the Verilog version works.
All six blocks above are simulation models of a bus, not synthesizable RTL for a chip. They model the conductor and the pull-up, which are board-level objects that no synthesis tool creates. What is synthesizable is the participant's intent — the drive_low signal — plus the I/O structure that turns it into a pin behaviour, which is §6 and §7.
6. What This Becomes on an FPGA
The models above put a pullup on a net inside a module. Nothing on an FPGA works that way, and understanding why keeps a design out of a common trap.
Tri-state belongs at the I/O boundary. Modern FPGA fabric does not implement general-purpose internal tri-state buses — the interconnect is multiplexer-based, so a Z on an internal signal has nowhere to go physically. Where tri-state genuinely exists is at the I/O buffer, which has a drive path, an input path, and an output-enable that decides whether the pin is driven at all. That is the structure open-drain needs, and it is the reason the behaviour must be expressed at the top level of the design rather than on some internal net.
So a synthesizable I²C block does not contain a pullup and does not resolve a bus. It produces the intent, and the top level maps intent onto the pin:
// This is the SYNTHESIZABLE part. `sda` is a top-level port that goes to a
// package pin; the tool infers an I/O buffer whose output-enable is driven by
// the block's intent. There is no pull-up here -- it is a resistor on the board.
module i2c_pads (
input logic sda_drive_low, // from the I2C block: pull, or release
output logic sda_in, // to the I2C block: what the pin actually is
inout wire sda // TOP-LEVEL port -> package pin
);
// Drive 0 when pulling; otherwise turn the output driver OFF entirely.
// The conditional operator with 1'bz is the portable way to ask for this;
// synthesis turns it into an output-enable on the I/O buffer.
assign sda = sda_drive_low ? 1'b0 : 1'bz;
assign sda_in = sda;
endmoduleFour practical consequences worth knowing before Module 19 covers implementation properly:
- The external pull-up is not optional and not internal. An FPGA's weak internal pull-up exists for holding unused or configuration pins at a defined level; it is far too weak to serve as an I²C bus pull-up at any useful speed or loading. The resistor is a board component.
- The I/O voltage standard has to match the bus. The pin's bank voltage and its input thresholds must be compatible with the rail the pull-up goes to — which is the problem Chapter 2.6 develops as level shifting.
sda_inmust come from the pin, not from the intent. Reading back your ownsda_drive_lowinstead of the pin's actual level produces a design that cannot observe other participants. It simulates beautifully on its own and fails the moment a second device exists.- The pin is an asynchronous input. Nothing on the bus is related to the FPGA's clock, so
sda_inneeds synchronising before logic uses it. That is genuinely Module 19's subject and is flagged here only so you do not sample it naively.
Details beyond this — vendor I/O primitives, constraints, drive and slew settings, synchroniser placement — belong to Module 19, and vendor-specific code is deliberately absent because the portable form above is what the concept requires.
7. What This Becomes on an ASIC
The same separation, with a different implementation of the boundary.
An ASIC does not synthesize Z out of ordinary standard cells. Standard-cell logic drives both levels; there is no cell in a normal digital library that produces a high-impedance output on demand. What provides the behaviour is an I/O cell in the pad ring — a structure explicitly designed with an output driver, an output-enable, an input receiver, and ESD protection.
For I²C the pad must be chosen for three properties rather than assumed:
- Open-drain capable, or a bidirectional pad used that way. Some libraries offer an explicitly open-drain pad; more often a general bidirectional pad is driven with its enable tied to the pull-low intent, so the pin is either driven low or released.
- Enough sink capability at a valid low level. The pad must hold the line below the required low voltage while sinking the current the board's pull-up delivers. This is the same lower bound on pull-up resistance that Chapter 2.4 derives, seen from the silicon side — and it is a pad-selection decision, not something RTL can fix.
- Input thresholds compatible with the bus supply. The receiver's switching thresholds and the pad's voltage tolerance have to suit the rail the bus is pulled up to, which becomes a real constraint when the core and the bus run at different voltages.
Two more integration realities, kept brief because the pad ring is its own discipline: the pad's ESD and clamp structures mean the pin is never truly disconnected even when released, which contributes leakage and capacitance to the bus; and a bus that must survive with the chip unpowered while other devices remain active is a system-level question about the pad's behaviour in that state, not an RTL question.
The design rule that follows is the same one the FPGA section reached: the synthesizable RTL produces intent, and the boundary cell produces the electrical behaviour. A design that tries to express Z inside its logic is describing something its target cannot build.
8. The Question This Chapter Ends On
Every device can pull the conductor LOW or release it. No device can drive it HIGH.
So what makes the line HIGH?
Chapter 2.1's topology figure already showed two resistors to VDD, deliberately unexplained. This is where they stop being decoration: the subtraction just made them necessary.
The question is not rhetorical, and it is not a gap in the design — it is the direct consequence of the subtraction, and it has a specific answer. Notice, too, that the answer is now forced: something outside the devices has to restore the released line, because the devices have been deliberately deprived of the ability. Open-drain outputs and whatever restores HIGH are therefore not two independent facts about I²C that happen to co-occur. One requires the other.
9. Common Misconceptions
10. Reason It Through
Work this through before reading the answers.
Two devices, A and B, share a conductor and both use open-drain outputs. A releases the line. B pulls it LOW. A reads the conductor back through its input.
What level does A observe? LOW. A is not influencing the conductor at all, and B is holding it to ground, so the node is LOW and A's input sees LOW.
Is A's observation a contradiction of its own action? No, and this is the central point of the chapter. A did not drive HIGH; it released. Release is the absence of influence, so there is nothing for the observed LOW to contradict. Expecting HIGH after releasing is the push-pull mental model leaking back in.
Is anything wrong or dangerous here? Nothing at all. There is one active path to ground and no current flowing between A and B, because neither device is a source. This is the normal state of a shared open-drain node.
What can A conclude from the observation? Something genuinely useful: that another participant is asserting the conductor. A released, so the LOW is not its own doing — it must be somebody else's. A device that can release and look is a device that can detect the presence of another participant, using nothing but its ordinary input. Hold that thought; Chapter 2.5 shows what the bus builds out of it, and later modules turn it into three distinct protocol mechanisms.
11. Understanding Check
12. Summary
The fix for Chapter 2.2's fault is a subtraction: delete every device's ability to actively drive the conductor HIGH. What remains is an open-drain output — a single controllable switch between the output node and ground, with the path to the supply simply absent.
That leaves two states, and naming them precisely is the whole lesson. Pull LOW is an active, low-impedance connection to ground. Release is the switch opening, after which the device contributes nothing and the conductor's level is determined by everything else. Release is not driving HIGH.
The original fault becomes unrepresentable rather than merely unlikely. No combination of open-drain devices creates a supply-to-ground path through the pair; the worst case is several devices pulling toward ground together, which is agreement, with no current between them. Correctness therefore does not depend on participants behaving well, which matters because the participants are the things most likely to misbehave.
It also creates a capability worth carrying forward: a device that releases and then observes can discover that another participant is holding the line, using nothing but its ordinary input.
And it creates a debt. With no device able to drive HIGH, something outside the devices must restore a released conductor — which makes open-drain outputs and that restoring mechanism two halves of one design rather than two facts that happen to co-occur.
13. What Comes Next
Chapter 2.4 answers the question this chapter ends on. The restoring element is a resistor to the supply, and it does more than set an idle level: because it restores HIGH passively, through a real conductor with real capacitance, it determines how long a rising edge takes — which turns an electrical choice into a timing constraint the protocol has to live with.
Browse the full path on the I²C tutorials index.
Continue learning
Related tutorials
- Related topic
Tri-State Buffers and FPGA I/O Primitives
What sits between an RTL signal and a package pin: the data path, the enable, the always-live input buffer and the pad. Builds the vendor-neutral wrapper an I²C core talks to, shows why an internal tri-state net is a different proposition from a top-level one, and why the one signal every vendor spells differently is the one most likely to be inverted.
- Related topic
Modeling Open Drain — 0/Z, Output Enable and What Synthesis Infers
An I²C output has two states, LOW and RELEASED, and neither is 'drive a 1'. Compares the resolved-0/Z pin, the output-enable pair and the two-valued drive intent, builds the boundary stage that joins them, and separates what synthesis is expected to infer from what this chapter can actually prove.
- Related topic
RTL Design Review — Master and Slave
How to review an I²C block you did not write: sorting review questions into the ones code answers, the ones a simulation answers, and the ones nothing available answers. Four specimens, each with a purposeful test that passes both the good and the defective version, and the single observation that separates them — including the filter depth window, measured at 6 to 26 samples.
- Related topic
Why Push-Pull Fails on a Shared Bus
An ordinary digital output drives both levels actively, which assumes it owns the node. Attach two of them to one conductor, let them disagree, and the result is not a confused message but a low-impedance path from supply to ground through two output stages — an electrical fault no protocol discipline can prevent.
