I²C · Module 2
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.
Chapter 2.1 established the topology and stopped exactly where the mechanism should begin: several devices attached to the same conductor, each able to influence it, with nothing arbitrating between them. The natural next move is to wire it up with the output stage every digital design already uses and see what happens.
It does not work, and the way it fails is worth understanding in detail. The failure is not a subtle timing issue or a corner case — it is a structural mismatch between what an ordinary output assumes and what a shared node is. Everything the bus does afterwards is a response to it.
1. What an Ordinary Digital Output Does
A conventional logic output — the kind driving a signal between two blocks inside a chip, or between two chips that are wired point to point — is a push-pull stage. The name describes its structure: it contains two controllable conduction paths, and it uses them alternately.
To output a HIGH it turns on a path from the node toward the positive supply, and turns the other one off. To output a LOW it turns on a path from the node toward ground, and turns the first one off. In both cases the output is actively establishing the level: current flows through the enabled path to hold the node where the driver wants it.
Two properties follow, and both are normally virtues.
It is strong in both directions. Because HIGH is driven rather than merely permitted, the output can move the node quickly to either level against whatever capacitance the wire presents. Sharp edges in both directions come free.
Its two paths are never on together. In correct operation exactly one is enabled at a time, because enabling both would connect supply to ground through the output stage itself.
2. The Assumption Hidden in Every Ordinary Output
Notice what the structure takes for granted. A push-pull output establishes a level unconditionally: it does not ask what else is attached to the node, and it has no mechanism for finding out. When it decides to output HIGH, it turns on the path to the supply and expects the node to become HIGH.
That expectation is reasonable because of an assumption so ordinary it is rarely stated: the output owns the node. Nothing else is driving it, so establishing a level is the same as determining it.
On a point-to-point connection that assumption is true by construction. The wire runs from one output to one or more inputs, and inputs are high impedance — they observe the node without influencing it. The output is the only thing with any authority over the level, so "I am driving HIGH" and "the node is HIGH" are the same statement.
Chapter 2.1 destroyed that assumption. On a shared bus the node has several attached devices, each with the same unconditional capability and no knowledge of the others. The statement "I am driving HIGH" no longer implies anything at all about what the node is doing.
3. Two Drivers, One Node, Opposite Intentions
Make it concrete. Device A and Device B are both attached to SDA, both using push-pull outputs. At some instant A decides to output HIGH and B decides to output LOW.
A turns on its path from the node toward the supply. B turns on its path from the node toward ground. Both are enabled, both are low impedance, and they are connected to each other through the shared conductor.
Ask the question the node forces: what voltage is SDA?
There is no satisfying answer, and that is the finding. The node is not "HIGH because A is stronger" or "LOW because B is stronger" in any way a designer can rely on. It settles somewhere determined by the relative conduction of the two enabled paths — which depends on the two devices' process, their supply voltages, temperature, and how each vendor happened to size that output stage. Two parts from different manufacturers give a different answer from two parts from the same one, and the same pair can give a different answer at a different temperature.
Follow the path the figure draws. Supply, through A's enabled path, along the shared conductor, through B's enabled path, to ground. Nothing in that route is a load the system wanted — it is a connection between the supply and ground made out of two output stages that were each doing exactly what they were told.
4. What the Node Actually Does
Two push-pull drivers on one conductor, agreeing and then disagreeing
9 cyclesThe hatched intervals are the honest representation. It is not that the node takes some other valid level during a conflict — it is that its voltage is not a designed quantity at all. It may land near a valid HIGH, near a valid LOW, or in the region between them where a receiving input has no defined interpretation, and different devices listening to the same conductor may not even agree with each other about what they heard.
5. Why This Is an Electrical Fault, Not a Protocol Bug
It is tempting to file this under "the two devices disagreed", as though it were a coordination problem with an electrical symptom. That framing is backwards and leads to the wrong fix.
The consequences are physical, and worth being precise about:
A current path exists that the design never intended. Supply to ground through two output stages, limited only by their combined conduction. That current does no useful work; it is dissipated as heat in the two devices.
The node voltage is undefined in the engineering sense. Not random, but determined by parameters — relative drive strength, process, supply, temperature — that no board designer specifies or controls, and that vary between parts that are nominally interchangeable.
Receivers may disagree. A voltage in the undefined region between valid levels is not guaranteed to be interpreted consistently, either between two listening devices or by the same device at a different moment.
The devices are stressed. Each output stage is conducting current it was not sized to conduct continuously. Whether this matters depends on how much current, for how long, how often, and what the parts were designed to tolerate — and that is exactly the problem.
That last point deserves care, because this topic attracts overstatement. A single brief conflict does not reliably destroy a device, and many parts survive it. Sustained or repeated conflict is a genuine reliability concern, and the current and heat are real. But the reason to rule push-pull out is not a prediction about damage. It is that the architecture has no defined behaviour: it produces a node voltage nobody specified, through a current path nobody designed, with an outcome that varies between interchangeable parts. An arrangement whose result cannot be stated is not usable, independently of whether it happens to survive.
6. Putting a Number on the Current
The prose above says "low-impedance path". It is worth making that quantitative enough to feel, while being honest about how crude the model is.
Treat each conducting transistor as a resistance — call it R_on — and the two in series form the whole path from supply to ground:
I ≈ VDD / (R_on,A + R_on,B)Every symbol: VDD is the supply the winning pull-up path reaches, and R_on,A and R_on,B are the effective on-resistances of Device A's supply-side path and Device B's ground-side path.
What this model is good for, and it is only one thing: it shows that the current is set by the drivers, not by any load the designer chose. Two outputs built to switch a board trace quickly are built to be low-impedance, so a few ohms to a few tens of ohms each is the right order of magnitude — and a supply divided by tens of ohms is a current far larger than any signal on the board was meant to carry.
What this model is wrong about, and this matters more than the number:
- A MOSFET is not a resistor. Its conduction depends on its gate drive and on the drain-source voltage across it, and in contention that voltage is precisely what is unknown.
R_onis not a published constant. It varies with process, supply, and temperature, and vendors specify output drive capability rather than an on-resistance you can put in a formula.- The node voltage is not a division of two fixed resistors, it is the operating point where the two devices' current-voltage characteristics cross — which is why two parts that are nominally interchangeable can settle at different voltages.
So use the model to answer "is this current large?" — yes, alarmingly — and not to answer "what is the current?" Anyone quoting a specific milliamp figure for contention between two unspecified parts is inventing precision. That is also why the objection to push-pull sharing is architectural rather than a damage calculation: the honest statement is that the outcome is not a designed quantity, and no amount of arithmetic changes that.
7. Modelling Contention — What a Simulator Actually Does
There is a second layer to get right, and conflating it with the first is extremely common.
A digital simulator does not solve for a voltage. It resolves a net with more than one driver by combining the driven values, and when two drivers supply opposing strong values there is no value that represents the result — so it produces X.
That X is not a voltage. It is not "half-way". It is the simulator saying the digital abstraction cannot represent what is happening here. Which is exactly right, and exactly useful: a design that produces X on a shared net in simulation has told you, cheaply and before any board exists, that two participants are driving against each other.
The model below builds that situation deliberately so you can see both halves of the lesson — the resolution rule, and its limits.
7a. SystemVerilog
Two points of SystemVerilog discipline are doing real work here. The shared conductor must be a net (wire/tri), not a logic variable, because only a net has multi-driver resolution semantics — a variable with two procedural drivers is an error, not a bus. And each driver is a continuous assignment that contributes either a value or 1'bz, which is how "this driver is not participating" is expressed.
// EDUCATIONAL CONTENTION MODEL. This is deliberately the WRONG way to share a
// conductor, built so its failure can be observed. It is not I2C, and nothing
// in later modules drives a shared line HIGH.
module push_pull_pair (
input wire a_en, // A is driving at all
input wire a_val, // the level A drives
input wire b_en,
input wire b_val,
output wire node // the shared conductor: a NET, so it resolves
);
// A push-pull driver establishes BOTH levels actively. When enabled it
// contributes a_val; when not, it contributes nothing (high impedance).
assign node = a_en ? a_val : 1'bz;
assign node = b_en ? b_val : 1'bz;
// Two continuous assignments to one net is legal and is the point: the
// net resolves them. Two procedural drivers on a `logic` variable would
// be a compile error instead of a bus.
endmoduleThe testbench walks the four combinations of two drivers plus the released cases, and checks the resolved value against what the resolution rule requires. Its most important check is that disagreement produces X — a testbench that skipped it would pass against a model that silently picked a winner.
module push_pull_pair_tb;
logic a_en, a_val, b_en, b_val;
wire node;
int errors = 0;
push_pull_pair dut (.a_en(a_en), .a_val(a_val), .b_en(b_en), .b_val(b_val), .node(node));
// `===` is required throughout: it compares X and Z literally, where `==`
// would return X and quietly never fail.
task automatic chk(input logic exp, input string what);
#1;
if (node !== exp) begin
$error("%s: node = %b, expected %b", what, node, exp);
errors++;
end
endtask
initial begin
// --- both drivers enabled, agreeing: ordinary digital behaviour ----
a_en = 1; b_en = 1;
a_val = 0; b_val = 0; chk(1'b0, "both drive 0");
a_val = 1; b_val = 1; chk(1'b1, "both drive 1");
// --- both enabled, disagreeing: THE FAULT ---------------------------
a_val = 0; b_val = 1; chk(1'bx, "A drives 0 while B drives 1");
a_val = 1; b_val = 0; chk(1'bx, "A drives 1 while B drives 0");
// --- one driver released: the other one determines the net ----------
a_en = 0; b_en = 1; b_val = 0; chk(1'b0, "A released, B drives 0");
a_en = 0; b_en = 1; b_val = 1; chk(1'b1, "A released, B drives 1");
// --- nobody driving: a push-pull bus has NO defined level ------------
// Note what this proves: with no participant driving, the net
// floats at Z. Nothing restores it. Chapter 2.4 is about the
// component that has to exist for a released line to mean anything.
a_en = 0; b_en = 0; chk(1'bz, "both released -- nothing restores the net");
if (errors == 0) $display("PASS: agreement resolves, disagreement is X, release defers to the other driver");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule7b. Verilog
Identical hardware and identical resolution — the language difference is only typing. reg for the stimulus driven procedurally, wire for the resolved net, and explicit comparison with $display in place of $error.
// EDUCATIONAL CONTENTION MODEL -- deliberately the wrong way to share a wire.
module push_pull_pair (
input wire a_en,
input wire a_val,
input wire b_en,
input wire b_val,
output wire node
);
assign node = a_en ? a_val : 1'bz;
assign node = b_en ? b_val : 1'bz;
endmodule module push_pull_pair_tb;
reg a_en, a_val, b_en, b_val;
wire node;
integer errors;
push_pull_pair dut (.a_en(a_en), .a_val(a_val), .b_en(b_en), .b_val(b_val), .node(node));
task chk;
input exp;
input [8*40:1] what;
begin
#1;
if (node !== exp) begin
$display("FAIL: %0s: node = %b, expected %b", what, node, exp);
errors = errors + 1;
end
end
endtask
initial begin
errors = 0;
a_en = 1; b_en = 1;
a_val = 0; b_val = 0; chk(1'b0, "both drive 0");
a_val = 1; b_val = 1; chk(1'b1, "both drive 1");
a_val = 0; b_val = 1; chk(1'bx, "A=0 B=1 contention");
a_val = 1; b_val = 0; chk(1'bx, "A=1 B=0 contention");
a_en = 0; b_en = 1; b_val = 0; chk(1'b0, "A released, B drives 0");
a_en = 0; b_en = 1; b_val = 1; chk(1'b1, "A released, B drives 1");
a_en = 0; b_en = 0; chk(1'bz, "both released");
if (errors == 0) $display("PASS: agreement resolves, disagreement is X, release defers");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule7c. VHDL
VHDL reaches the same result by a mechanism worth understanding rather than memorising, because it is the clearest of the three about what is actually happening.
std_logic is a resolved type. Its declaration carries a resolution function that VHDL applies automatically whenever a signal has more than one driver: the function takes all the driving values and returns the single value the signal takes. For std_logic that function returns 'X' when two drivers supply opposing strong values — the same conclusion the Verilog net reaches, but arrived at by an explicitly declared function rather than by built-in net semantics.
This is also why std_ulogic — the unresolved variant — would be the wrong choice here: a signal of an unresolved type with two drivers is an error at elaboration. The deliberately multiply-driven conductor needs the resolved type, and saying so is the lesson.
library ieee;
use ieee.std_logic_1164.all;
-- EDUCATIONAL CONTENTION MODEL -- deliberately the wrong way to share a wire.
entity push_pull_pair is
port (
a_en : in std_logic;
a_val : in std_logic;
b_en : in std_logic;
b_val : in std_logic;
node : out std_logic -- std_logic, NOT std_ulogic: this signal is
); -- deliberately driven by two sources and must
end entity; -- be resolved rather than rejected.
architecture sim of push_pull_pair is
begin
-- Two concurrent drivers on one signal. VHDL applies std_logic's resolution
-- function to them: agreement passes through, opposing strong values give 'X',
-- and a driver contributing 'Z' defers to the other.
node <= a_val when a_en = '1' else 'Z';
node <= b_val when b_en = '1' else 'Z';
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity push_pull_pair_tb is
end entity;
architecture sim of push_pull_pair_tb is
signal a_en, a_val, b_en, b_val : std_logic := '0';
signal node : std_logic;
procedure chk (actual : in std_logic; expected : in std_logic; what : in string) is
begin
assert actual = expected
report "push_pull_pair: " & what severity error;
end procedure;
begin
dut : entity work.push_pull_pair
port map (a_en => a_en, a_val => a_val, b_en => b_en, b_val => b_val, node => node);
stim : process
begin
a_en <= '1'; b_en <= '1';
a_val <= '0'; b_val <= '0'; wait for 1 ns; chk(node, '0', "both drive 0");
a_val <= '1'; b_val <= '1'; wait for 1 ns; chk(node, '1', "both drive 1");
a_val <= '0'; b_val <= '1'; wait for 1 ns; chk(node, 'X', "A=0 B=1 must resolve to X");
a_val <= '1'; b_val <= '0'; wait for 1 ns; chk(node, 'X', "A=1 B=0 must resolve to X");
a_en <= '0'; b_val <= '0'; wait for 1 ns; chk(node, '0', "A released, B drives 0");
b_val <= '1'; wait for 1 ns; chk(node, '1', "A released, B drives 1");
b_en <= '0'; wait for 1 ns; chk(node, 'Z', "both released -- nothing restores it");
report "push_pull_pair self-check complete" severity note;
wait;
end process;
end architecture;7d. What the Three Models Share, and the One Thing They All Mislead About
Ports, stimulus, expected values and conclusions are identical across the three. The resolution mechanism differs in how it is specified — Verilog and SystemVerilog nets have it built in, VHDL declares it as std_logic's resolution function — and reaches the same answer.
Now the caution, which is the most important paragraph in this chapter:
X is a property of the simulation, not of the silicon. Real hardware does not produce X on a wire. It produces a current from supply to ground, heat in two packages, and a voltage somewhere between the rails that depends on the two devices' characteristics. X is the digital abstraction refusing to guess at that analog result, which is the most useful thing it could possibly do — but a reader who takes X as "the voltage is undefined-ish" has learned the model instead of the hardware.
The pairing is worth holding as one sentence: in simulation, contention is X; in silicon, contention is current. Everything in §4 and §5 is about the second half, and everything in §7 is about the first. A verification engineer needs both, because the first is the only one that is cheap to detect.
Explicit scope note. This is the only place in the entire I²C curriculum where a participant drives a shared line HIGH, and it exists to be rejected. Every model from Chapter 2.3 onward can pull LOW or release, and nothing else — if you ever find yourself writing assign sda = something; where something can be 1, you have reintroduced this chapter's fault.
8. "Just Make Sure They Take Turns"
The obvious objection is that conflict only happens if two devices act at once, and the protocol exists precisely to stop that. Why not keep push-pull outputs and rely on the rules to prevent overlap?
The objection is worth taking seriously, and it fails for three separate reasons.
The rules are implemented by the devices the rules govern. A participant that is confused, mis-addressed, half-initialised, or recovering from a reset is exactly the participant most likely to act out of turn — and it is the same participant responsible for knowing whose turn it is. Protocol discipline is not an independent safeguard; it is a property of the things being safeguarded.
Turn-taking has to start somewhere. At power-up, devices come alive at different times as their supplies rise, before any coordination has happened. An arrangement that is only safe once everyone agrees has no safe beginning.
Some behaviours require deliberate overlap. This is the decisive one, and it becomes visible in Chapter 2.5. Several of the bus's most useful mechanisms work by having one participant assert a line while another is also acting on it — that is not a failure to take turns, it is the mechanism. An architecture in which simultaneous action is destructive could not support them at all.
So the fix cannot be "be careful". It has to be structural: make the conflicting condition impossible to express. That is what Chapter 2.3 does, and the way it does so is almost startlingly simple — it removes a capability rather than adding a safeguard.
9. Common Misconceptions
10. Reason It Through
Work this through before reading the answers.
During bring-up an engineer measures an I²C-like bus built with ordinary push-pull outputs. Most of the time it appears to work. Occasionally a byte is wrong, and a thermal camera shows one device running warmer than expected.
What is the most likely explanation? Two participants are acting on the conductor at the same time in some situation the engineer has not yet identified. The occasional wrong byte is a receiver interpreting an indeterminate node voltage; the heat is the supply-to-ground current path through two output stages during those overlaps.
Why does it mostly work? Because conflict only occurs in the intervals where two devices actually disagree, and the rest of the traffic is fine. That intermittency is the trap: the board passes casual testing, and the failure rate depends on timing, temperature and which parts happen to be fitted.
Why is "add a retry on bad bytes" the wrong fix? Because it treats a symptom in the data domain while leaving the electrical fault running. The current path, the heat and the undefined node voltage all remain, and the underlying overlap is unaffected. A retry may even increase total bus activity and therefore the number of conflicts.
What would make this reliably safe? Not better timing discipline. The devices enforcing that discipline are the same ones failing it, there is no coordinated state at power-up, and the architecture is meant to support deliberately simultaneous assertion. The only durable answer is an output stage from which the conflicting condition cannot be expressed — which is the next chapter.
11. Understanding Check
12. Summary
An ordinary digital output is push-pull: two conduction paths sharing one node, one toward the supply and one toward ground, with exactly one enabled at a time. Whichever is enabled actively establishes the level, which gives sharp edges in both directions and makes it an excellent point-to-point driver.
It carries a hidden assumption — that the output owns the node. True by construction when the only other things attached are high-impedance inputs. False on a shared bus.
Attach two of them to one conductor and let them disagree, and the result is not a confused message. It is a low-impedance path from supply to ground through two output stages, dissipating power in both, with a node voltage determined by relative drive strength — a parameter that depends on process, supply and temperature and varies between interchangeable parts. Listening devices may not agree about what they heard.
A single brief conflict often does no lasting harm, and saying otherwise overstates the case. The reason to rule the architecture out is stronger than a damage prediction: it has no defined behaviour. And protocol discipline cannot rescue it, because the rules are implemented by the devices they would protect, there is no coordinated state at power-up, and the bus's own mechanisms depend on deliberate simultaneous assertion.
The fix therefore has to be structural — not a safeguard, but a change that makes the conflicting condition inexpressible.
13. What Comes Next
Chapter 2.3 makes that change, and it is a subtraction rather than an addition: remove the ability to actively drive HIGH, leaving each device able to pull the conductor LOW or to release it. Two devices can then never impose opposite levels, because only one level can be imposed at all.
That immediately raises a question the chapter deliberately ends on — if nobody can drive HIGH, what makes the line HIGH? Browse the full path on the I²C tutorials index.
Continue learning
Related tutorials
- Related topic
Why Chips on a Board Need a Bus
A connection between two chips is not a wire. It is a pin on each package, a routed trace, the board area and layers that trace consumes, and an I/O cell driving it — and all of that is paid for again for every device added. This is the cost structure that makes dedicating an interface per peripheral stop scaling, and that forces a board to share one set of wires instead.
- Related topic
From Parallel Buses to Two Wires
Derive the bus rather than meet it. Trading wires for time gives serialisation; trading exclusivity for coordination gives a shared medium; losing the wire as an implicit address forces a logical one. Each step is a deliberate exchange, and what falls out is a two-wire addressed bus — which is what Philips specified as I²C.
- Related topic
I²C vs SPI vs UART — Choosing a Board-Level Bus
Three interfaces that solve three different problems. Work the dimensions that actually decide — where identity lives, whether timing is sent or agreed, how wire count grows with device count, and whether the system is waiting — then apply them to four concrete board decisions where the right answer is not always I²C.
- Related topic
SDA and SCL — The Two-Wire Contract
Name the two conductors, establish that both are bidirectional shared nodes rather than any device's output, and define what a free bus looks like. Then state precisely the question the rest of the module exists to answer: what happens when two devices attached to the same wire disagree about its level?
