Skip to content
VLSI Mentor

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.

A push-pull output stage. An internal logic value feeds driver control, which enables either a conduction path from the output node to the positive supply, producing a driven HIGH, or a conduction path from the output node to ground, producing a driven LOW. Exactly one path is enabled at a time, and both are low impedance when enabled.Positive supplythe source of a driven HIGHInternal valuethe 0 or 1 being outputDriver controlenables exactly one pathOutput nodeactively held at the chosenlevelGroundthe sink for a driven LOWpull pathpush path12
Figure 1 — a push-pull output stage. Two controllable paths share one output node: one toward the supply, one toward ground. Exactly one is enabled at a time, and whichever is enabled actively establishes the level. The strength that makes this a good point-to-point driver is precisely what makes it unusable on a shared node.

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.

Device A has its path to the positive supply enabled while Device B has its path to ground enabled. Both devices are attached to the same shared conductor, so a low-impedance current path runs from the supply through Device A, along the shared wire, through Device B to ground. The resulting node voltage is indeterminate, set by the relative strength of the two output stages.Positive supplyreached through ADevice Asupply path enabled — outputsHIGHShared conductorvoltage set by relativestrengthDevice Bground path enabled — outputsLOWGroundreached through B12
Figure 2 — the conflict. Device A's supply path and Device B's ground path are enabled at the same instant and joined by the shared conductor, forming a low-impedance route from supply to ground through the two output stages. The node voltage is set by the relative strength of the two paths, which is not a property any designer controls.

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 cycles
Nine intervals. Device A's output intention and Device B's output intention are shown separately, with the resulting node level below. While both devices intend the same level the node takes it cleanly. During the intervals where A intends HIGH and B intends LOW the node is shown as unknown, because its voltage is set by the relative strength of the two enabled output paths rather than by either device.drivers agreedrivers agreeindeterminateindeterminatedrivers agreedrivers agreeboth intend HIGH — node agreesboth intend HIGH — nodeagreesA HIGH, B LOW — conflictA HIGH, B LOW — conflictA releases to LOW — resolvedA releases to LOW —resolveda_outb_outnodeXXXt0t1t2t3t4t5t6t7t8
Figure 3 — the same conflict in time. While A and B agree, the node takes the agreed level. In the intervals where they disagree the node is indeterminate: it settles at some voltage governed by the two devices' relative drive strength, which may not be a valid logic level for either of them. The intervals are illustrative, not protocol timing.

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
first-order contention model — assumptions stated, not a device model
   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_on is 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.

Azvya Education Pvt. Ltd.VLSI Mentor
push_pull_pair.sv — two push-pull drivers on one net (SIMULATION MODEL, not synthesizable RTL)
   // 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.
   endmodule

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
push_pull_pair_tb.sv — self-checking: agreement resolves, disagreement is X, release yields the other driver
   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
   endmodule

7b. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
push_pull_pair.v — the same contention model in Verilog (SIMULATION MODEL, not synthesizable)
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
push_pull_pair_tb.v — the same seven checks in Verilog idiom
   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
   endmodule

7c. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
push_pull_pair.vhd — the same model in VHDL (SIMULATION MODEL; relies on std_logic resolution)
   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;
Azvya Education Pvt. Ltd.VLSI Mentor
push_pull_pair_tb.vhd — the same seven checks with assert report severity
   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