Simulating Open-Drain GPIO in Software

In today's day and age, it's rare to find a modern microcontroller that does not support configurable GPIO. We can easily take these configuration options for granted, especially when interacting with circuits and communication busses (e.g. I2C) that require GPIO to be configured in open-drain mode.

As embedded developers we do not always get to work with the latest-and-greatest parts. Sometimes we end up working with tiny or cheap processors (e.g. AVR), and other times we need to support a legacy part.

Fortunately, it's quite simple to recreate open-drain GPIO behavior in software.

Table of Contents:

  1. Open-Drain Support in Software
    1. Defining Types
    2. Setting Pin States
    3. Reading Pin Values
    4. Initial Configuration
  2. Further Reading

Open-Drain Support in Software

To enable open-drain support, we'll need to support three primary operations:

  1. Actively drive output low
  2. Put the port in input mode with a pull-up (logical 1)
  3. Put the port in high-impedance mode (floating input with no pull-up or pull-down enabled)

If the open-drain circuit has an external pull-up resistor, operation #2 is not necessary. However, in situations where there is not an external pull-up resistor, you will need to rely on the microcontroller's internal pull-ups.

We will create simple wrapper functions to enable these modes. I'll be using pseudo-code for these examples, since GPIO interfaces vary widely.

Defining Types

First, rather than using a plain 0 or 1 value, we'll define a custom type to describe the three possible states:

typedef enum
    OD_LOW = 0,
    OD_HIGH = 1,
    OD_HIGH_Z = 2
} opendrain_state_t;

As an alternative to the less-descriptive bool, we'll also create a helper type that can be used for initial configuration:

typedef enum
} opendrain_config_t;

Setting Pin States

Now that we have our states defined, we can create a function to manage an open-drain pin:

void setOpenDrainPin(void* port, unsigned pin, opendrain_state_t state)
        case OD_LOW:
            setPinMode(port, pin, OUTPUT);
            setOutput(port, pin, 0);
        case OD_HIGH:
            setPinMode(port, pin, INPUT_PULLUP);
        case OD_HIGH_Z:
            // No pull-up in hi-z mode
            setPinMode(port, pin, INPUT);

We can also create helper functions:

void setOpenDrainHigh(void * port, unsigned pin)
    setOpenDrainPin(port, pin, OD_HIGH);
void setOpenDrainLow(void * port, unsigned pin)
    setOpenDrainPin(port, pin, OD_LOW);
void setOpenDrainHiZ(void * port, unsigned pin)
    setOpenDrainPin(port, pin, OD_HIGH_Z);

Reading Pin Values

To read the state of an open-drain pin, no special behavior is necessary. Simply follow your normal procedures for reading the value of the pin.

Initial Configuration

We can then create a function which we will use to configure our open-drain pins:

void configureOpenDrainPin(void* port, unsigned pin, 
    opendrain_config_t config)
        case OD_CONFIG_NO_PULLUP:
            setOpenDrainHiZ(port, pin);
        case OD_CONFIG_PULLUP:
            setOpenDrainHigh(port, pin);

This allows you to configure your pin in a straightforward way that is easier for other users to understand.

You could also expand support to limit the operations of the setOpenDrainPin function based on the initial configuration of that pin.

Further Reading

Demystifying Microcontroller GPIO Settings

Updated: 20190913

Anyone who writes software for microcontrollers will have to configure and manage general purpose input/output (GPIO) pins. On their surface, GPIO configuration seems simple: pins are input or output, and they can be high or low.

However, inevitably you will come across a fancy processor with a plethora of configuration options, or an electrical engineer will request pin settings which you don't understand ("make this line Hi-Z").

This guide aims to help you understand different pin configuration options that are provided on modern microcontrollers.

Table of Contents:

  1. Background Information
    1. Tri-state Logic
    2. High-impedance
    3. Floating
    4. Pull-up
    5. Pull-down
    6. Current Sink
    7. Current Source
  2. Input and Output Modes
    1. GPIO Input Modes
    2. GPIO Output Modes
      1. Push-Pull Output
      2. Open-Drain Output
      3. Open-Collector
  3. Speed
  4. High Drive
  5. Further Reading

Background Information

Before we dive into configuration options, it's useful to understand some general descriptions and terms that are associated with GPIO and IO signals.

Tri-state Logic

Most modern GPIO lines are implemented as a tri-state buffer. This means that the GPIO line can effectively assume three values:

  1. Logical 0 (connection to ground)
  2. Logical 1 (connection to VCC)
  3. High-impedance (also called "floating", "Hi-Z", "tri-stated")


When a line is put into a high-impedance state, the output is effectively removed from the circuit. This allows multiple circuits or devices to share the same output lines and is commonly utilized to implement communication busses. Failure to utilize a high-impedance state when it's required leads to IO contention and short-circuits.


A signal is said to be "floating" when its state is indeterminate, meaning that it is neither connected to VCC or to ground. The signal's voltage will "float" to match the residual voltage.

The term "floating" is often used interchangeably to describe a pin which is in the high-impedance state.


Pull-ups are resistors that connect a signal to VCC. Pull-ups are used to set a default state when the signal is floating.

Recall that when an input pin is in high-impedance mode and not driven by external sources, it is floating at a residual voltage level. Pull-up resistors prevent the pin from floating by forcing the signal to VCC when it is not being actively driven. When another source drives the signal low (connects to ground), the pull-up is overridden and the input pin will read a '0'.

Many microcontrollers supply internal pull-up configuration options. Sometimes, a specific pull-up resistor value is required which necessitates using an external pull-up instead of a chip's internal pull-up.


Pull-downs are resistors that connect an signal to ground. Pull-downs are used to set a default state when the signal is floating. When another source drives the signal high (connects to VCC), the pull-down is overridden and the input pin will read a '1'.

Many microcontrollers supply internal pull-down configuration options. Sometimes, a specific pull-down resistor value is required which necessitates using an external pull-down instead of a chip's internal pull-down.

Current Sink

A "current sink" means that current is flowing into a pin, node, or signal. For digital IO, a current sink provides the ground connection to the load.

Current Source

A "current source" is the opposite of a current sink: the current is flowing out of a pin, node or signal. For digital IO, a current source provides the voltage source to the load.

Both a current source and a current sink have current flowing, but in different directions.

Input and Output Modes

The primary configuration option for a GPIO pin is input or output.

GPIO Input Modes

When a GPIO is configured as an input, it can be used to read the state of the electrical signal. Configuring a GPIO as an input puts the pin into a high-impedance state.

In general, there GPIO inputs are primarily configured in one of three ways:

  1. High-impedance (default - floats if not driven)
  2. Pull-up (internal resistor connected to VCC)
  3. Pull-down (internal resistor connected to Ground)

Most GPIO input pins also feature internal hysteresis, which prevents spurious state changes on the pins. Usually hysteresis is a built-in feature, rather than a configurable setting.

GPIO Output Modes

When a GPIO is configured as an output, it can be used to drive a signal high or low. There are primarily two configuration options for GPIO outputs: push-pull and open-drain.

Push-pull output

Push-pull is the default GPIO output setting in most cases. A push-pull GPIO has the ability to both source and sink current.

With a push-pull GPIO, a transistor connects to VCC or GND to drive a signal high or low. When the output goes low, the signal is actively "pulled" to ground, and when the output goes high it is actively "pushed" to VCC.

Open-Drain Output

Unlike push-pull, an open-drain output can only sink current. The output has two states: low and high-impedance. In order to achieve a logical high output on the line, a pull-up resistor is used to connect the open-drain output to the desired output voltage level.

You can think of an open-drain GPIO as behaving like a switch which is either connected to ground or disconnected.

Open-drain GPIO can typically be configured in two different modes:

  • Open-drain
  • Open-drain with internal pull-up

Most applications which utilize open-drain circuitry utilize external pull-ups on open-drain outputs. Often, internal pull-up values are not sufficient for the target circuitry.

Open-drain outputs are useful when multiple gates or pins are connected together, such as with the I2C bus. When a device is not using the bus, the open-drain output is in high-impedance mode and the voltage level is pulled high by the pull-up resistor. When a device drives the output low, all connected lines will go low, as they are tied together.

Another common use for open-drain outputs is having multiple external devices drive a single, active-low interrupt pin on a microcontroller.


An "open collector" is functionally the same as an "open drain". An "open collector" refers to a current sink on a BJT transistor output, while an "open drain" refers to a current sink on a FET output.

I encounter "open collector" more on component datasheets than I do on microcontroller datasheets.

GPIO Speed

GPIO speed controls the slew rate, or the rate at which a signal can change between low/high values (the "rise time" and "fall time"). Speed configuration options are described as "speed", "slew rate", "frequency", and "high-frequency mode".

By increasing the GPIO speed, you increase the rate of change of the output voltage (reducing rise time). However, power consumption and noise radiated by the circuit increases along with the GPIO speed. By default, you should keep GPIO speed low unless there is a specific reason for increasing it.

High Drive

High-drive GPIO are push-pull pins that are capable of providing more current than typical pins. While you must check each chip's datasheet to understand the current capacity of your pins, typical push-pull GPIO can source/sink around ±8mA, while a high-drive pin can source/sink up to ±40mA.

High-drive pins enable your microcontroller to directly drive IO that requires higher-than-normal current, such as an LED. Using high-drive pins can help simplify the electrical design and reduce cost by eliminating the need for external current amplifying circuitry.

Further Reading

Change Log

  • 20190913:
    • Demoted headings for consistency across the website
  • 20190424:
    • Fixed typo "change change" -> "can change"
    • Added Further Reading section to Table of Concents
    • Clarified that an open collector is a current sink for a BJT transistor


Thanks to Gabriel Staples for identifying corrections needed by the article.