This module is under construction and may not be ready for use.

pw_digital_io provides a set of interfaces for using General Purpose Input and Output (GPIO) lines for simple Digital I/O. This module can either be used directly by the application code or wrapped in a device driver for more complex peripherals.


The interfaces provide an abstract concept of a Digital IO line. The interfaces abstract away details about the hardware and platform-specific drivers. A platform-specific backend is responsible for configuring lines and providing an implementation of the interface that matches the capabilities and intended usage of the line.

Example API usage:

using namespace pw::digital_io;

Status UpdateLedFromSwitch(const DigitalIn& switch, DigitalOut& led) {
  PW_TRY_ASSIGN(const DigitalIo::State state, switch.GetState());
  return led.SetState(state);

Status ListenForButtonPress(DigitalInterrupt& button) {
    [](State sampled_state) {
      // Handle the button press.
      // NOTE: this may run in an interrupt context!
  return button.EnableInterruptHandler();

pw::digital_io Interfaces#

There are 3 basic capabilities of a Digital IO line:

  • Input - Get the state of the line.

  • Output - Set the state of the line.

  • Interrupt - Register a handler that is called when a trigger happens.


Capabilities refer to how the line is intended to be used in a particular device given its actual physical wiring, rather than the theoretical capabilities of the hardware.

Additionally, all lines can be enabled and disabled:

  • Enable - tell the hardware to apply power to an output line, connect any pull-up/down resistors, etc.

  • Disable - tell the hardware to stop applying power and return the line to its default state. This may save power or allow some other component to drive a shared line.


The initial state of a line is implementation-defined and may not match either the “enabled” or “disabled” state. Users of the API who need to ensure the line is disabled (ex. output is not driving the line) should explicitly call Disable().

Functionality overview#

The following table summarizes the interfaces and their required functionality:

Interrupts Not Required

Interrupts Required

Input/Output Not Required


Input Required



Output Required



Input/Output Required



Synchronization requirements#

  • An instance of a line has exclusive ownership of that line and may be used independently of other line objects without additional synchronization.

  • Access to a single line instance must be synchronized at the application level. For example, by wrapping the line instance in pw::Borrowable.

  • Unless otherwise stated, the line interface must not be used from within an interrupt context.

Design Notes#

The interfaces are intended to support many but not all use cases, and they do not cover every possible type of functionality supported by the hardware. There will be edge cases that require the backend to expose some additional (custom) interfaces, or require the use of a lower-level API.

Examples of intended use cases:

  • Do input and output on lines that have two logical states - active and inactive - regardless of the underlying hardware configuration.

    • Example: Read the state of a switch.

    • Example: Control a simple LED with on/off.

    • Example: Activate/deactivate power for a peripheral.

    • Example: Trigger reset of an I2C bus.

  • Run code based on an external interrupt.

    • Example: Trigger when a hardware switch is flipped.

    • Example: Trigger when device is connected to external power.

    • Example: Handle data ready signals from peripherals connected to I2C/SPI/etc.

  • Enable and disable lines as part of a high-level policy:

    • Example: For power management - disable lines to use less power.

    • Example: To support shared lines used for multiple purposes (ex. GPIO or I2C).

Examples of use cases we want to allow but don’t explicitly support in the API:

  • Software-controlled pull up/down resistors, high drive, polarity controls, etc.

    • It’s up to the backend implementation to expose configuration for these settings.

    • Enabling a line should set it into the state that is configured in the backend.

  • Level-triggered interrupts on RTOS platforms.

    • We explicitly support disabling the interrupt handler while in the context of the handler.

    • Otherwise, it’s up to the backend to provide any additional level-trigger support.

Examples of uses cases we explicitly don’t plan to support:

  • Using Digital IO to simulate serial interfaces like I2C (bit banging), or any use cases requiring exact timing and access to line voltage, clock controls, etc.

  • Mode selection - controlling hardware multiplexing or logically switching from GPIO to I2C mode.

API decisions that have been deferred:

  • Supporting operations on multiple lines in parallel - for example to simulate a memory register or other parallel interface.

  • Helpers to support different patterns for interrupt handlers - running in the interrupt context, dispatching to a dedicated thread, using a pw_sync primitive, etc.

The following sub-sections discuss specific design decisions in detail.

States vs. voltage levels#

Digital IO line values are represented as active and inactive states. These states abstract away the actual electrical level and other physical properties of the line. This allows applications to interact with Digital IO lines across targets that may have different physical configurations. It is up to the backend to provide a consistent definition of state.

Interrupt handling#

Interrupt handling is part of this API. The alternative was to have a separate API for interrupts. We wanted to have a single object that refers to each line and represents all the functionality that is available on the line.

Interrupt triggers are configured through the SetInterruptHandler method. The type of trigger is tightly coupled to what the handler wants to do with that trigger.

The handler is passed the latest known sampled state of the line. Otherwise handlers running in an interrupt context cannot query the state of the line.

Class Hierarchy#

pw_digital_io contains a 2-level hierarchy of classes.

  • DigitalIoOptional acts as the base class and represents a line that does not guarantee any particular functionality is available.

    • This should be rarely used in APIs. Prefer to use one of the derived classes.

    • This class is never extended outside this module. Extend one of the derived classes.

  • Derived classes represent a line with a particular combination of functionality.

    • Use a specific class in APIs to represent the requirements.

    • Extend the specific class that has the actual capabilities of the line.

In the future, we may add new classes that describe lines with optional functionality. For example, DigitalInOptionalInterrupt could describe a line that supports input and optionally supports interrupts.

When using any classes with optional functionality, including DigitalIoOptional, you must check that a functionality is available using the provides_* runtime flags. Calling a method that is not supported will trigger PW_CRASH.

We define the public API through non-virtual methods declared in DigitalIoOptional. These methods delegate to private pure virtual methods.

Type Conversions#

Conversions are provided between classes with compatible requirements. For example:

DigitalInInterrupt& in_interrupt_line;
DigitalIn& in_line = in_interrupt_line;

DigitalInInterrupt* in_interrupt_line_ptr;
DigitalIn* in_line_ptr = &in_interrupt_line_ptr->as<DigitalIn>();

Asynchronous APIs#

At present, pw_digital_io is synchronous. All the API calls are expected to block until the operation is complete. This is desirable for simple GPIO chips that are controlled through direct register access. However, this may be undesirable for GPIO extenders controlled through I2C or another shared bus.

The API may be extended in the future to add asynchronous capabilities, or a separate asynchronous API may be created.

Backend Implemention Notes#

  • Derived classes explicitly list the non-virtual methods as public or private depending on the supported set of functionality. For example, DigitalIn declare GetState public and SetState private.

  • Derived classes that exclude a particular functionality provide a private, final implementation of the unsupported virtual method that crashes if it is called. For example, DigitalIn implements DoSetState to trigger PW_CRASH.

  • Backend implementations provide real implementation for the remaining pure virtual functions of the class they extend.

  • Classes that support optional functionality make the non-virtual optional methods public, but they do not provide an implementation for the pure virtual functions. These classes are never extended.

  • Backend implementations must check preconditions for each operations. For example, check that the line is actually enabled before trying to get/set the state of the line. Otherwise return pw::Status::FailedPrecondition().

  • Backends may leave the line in an uninitialized state after construction, but implementors are strongly encouraged to initialize the line to a known state.

    • If backends initialize the line, it must be initialized to the disabled state. i.e. the same state it would be in after calling Enable() followed by Disable().

    • Calling Disable() on an uninitialized line must put it into the disabled state.



To enable pw_digital_io for Zephyr add CONFIG_PIGWEED_DIGITAL_IO=y to the project’s configuration.