Skip to content
VLSI Mentor

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

An open-drain output stage. An internal value feeds driver control, which either turns on a single switch connecting the output node to ground, producing a driven LOW, or turns that switch off, leaving the output node unconnected inside the device. There is no path from the output node to the positive supply, so the device cannot drive HIGH.Internal valuepull low, or do notSwitch to groundthe one controllable pathOutput nodepulled LOW, or left aloneGroundthe only level this device canimposeNo supply pathdeleted — this is the wholeidea12
Figure 1 — what remains of the output stage. The path toward the positive supply is gone; only the controllable path to ground survives. The device can therefore establish exactly one level, and its other state is not a level at all but an absence of influence.

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 cycles
Eight intervals. The device's internal pull-low intention is shown above the resulting state of the conductor. While the device pulls, the conductor is LOW. While the device releases, the conductor is drawn as high impedance rather than HIGH, because no mechanism has yet been introduced that would restore a HIGH level. The figure deliberately leaves the released state unresolved.pulling LOWpulling LOWreleased — undefinedreleased — undefinedswitch closed — actively LOWswitch closed — activelyLOWreleased — not driving at allreleased — not driving atallreleased againreleased againpull_lowsdat0t1t2t3t4t5t6t7
Figure 2 — one device's two states over time. When the device pulls, the conductor is LOW. When it releases, the device stops influencing the conductor — shown here as the dashed high-impedance state, because nothing established so far says what the line does next. The intervals are illustrative rather than protocol timing.

Look 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_lowWhat the participant contributes to the lineLine level
1an active low-impedance path to ground0
0nothing — high impedancewhatever 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.

Azvya Education Pvt. Ltd.VLSI Mentor
od_participant.sv — one open-drain participant (SIMULATION MODEL of a bus participant)
   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
Azvya Education Pvt. Ltd.VLSI Mentor
od_bus.sv — two participants plus the pull-up (SIMULATION MODEL: models a board, not logic)
   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));
   endmodule

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

Azvya Education Pvt. Ltd.VLSI Mentor
od_bus_tb.sv — self-checking: release resolves HIGH, any pull wins, and a releasing participant observes the truth
   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
   endmodule

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

Azvya Education Pvt. Ltd.VLSI Mentor
od_participant.v — one open-drain participant in Verilog (SIMULATION MODEL)
   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
Azvya Education Pvt. Ltd.VLSI Mentor
od_bus.v — two participants and the pull-up (SIMULATION MODEL)
   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
Azvya Education Pvt. Ltd.VLSI Mentor
od_bus_tb.v — the same five checks in Verilog idiom
   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
   endmodule

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

Azvya Education Pvt. Ltd.VLSI Mentor
od_participant.vhd — one open-drain participant in VHDL (SIMULATION MODEL; std_logic resolution)
   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;
Azvya Education Pvt. Ltd.VLSI Mentor
od_bus.vhd — two participants plus a weak-H pull-up (SIMULATION MODEL)
   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;
Azvya Education Pvt. Ltd.VLSI Mentor
od_bus_tb.vhd — the same five checks with assert report severity
   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.

ConcernSystemVerilogVerilogVHDL
The shared conductorwire / tri — a net, because logic has no multi-driver resolutionwirestd_logic — a resolved type; std_ulogic would be rejected
Release1'bz1'bz'Z'
The pull-uppullup primitive — weak 1pullup primitivea driver contributing weak 'H'
Reading the linenet value directlynet value directlyto_X01(line) normalises 'H' to '1'
Why it resolvesbuilt-in net resolutionbuilt-in net resolutionstd_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:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_pads.sv — the synthesizable boundary: intent in, pin behaviour out
   // 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;
   endmodule

Four 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_in must come from the pin, not from the intent. Reading back your own sda_drive_low instead 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_in needs 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