pw_digital_io#

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.

Overview#

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) {
  PW_TRY(button.SetInterruptHandler(Trigger::kActivatingEdge,
    [](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.

Note

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. For output lines, the line is set to an initial output state that is backend-specific.

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

Note

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

DigitalInterrupt

Input Required

DigitalIn

DigitalInInterrupt

Output Required

DigitalOut

DigitalOutInterrupt

Input/Output Required

DigitalInOut

DigitalInOutInterrupt

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.

There is a helper pw::digital_io::Polarity enum provided to enable mapping from logical to physical states for backends.

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. In general, Disable() can be called in any state.

  • Calling Enable() on a line that is already enabled should be a no-op. In particular, the state of an already-enabled output line should not change.

RPC Service#

The DigitalIoService pw_rpc service is provided to support bringup/debug efforts. It allows manual control of individual DigitalIo lines for both input and output.

std::array<std::reference_wrapper<DigitalIoOptional>> lines = {
  ...DigitalIn(),
  ...DigitalOut(),
};
DigitalIoService service(lines);
rpc_server.RegisterService(service);

Set the state of the output line via RPC. This snippet demonstrates how you might do that using a Pigweed console device object.

from pw_digital_io import digital_io_pb2

device.rpcs.pw.digital_io.DigitalIo.SetState(
             line_index=0, state=digital_io_pb2.DigitalIoState.ACTIVE)

API reference#

Note

This API reference is incomplete.

class DigitalIoOptional#

A digital I/O line that may support input, output, and interrupts, but makes no guarantees about whether any operations are supported. You must check the various provides_* flags before calling optional methods. Unsupported methods invoke PW_CRASH.

All methods are potentially blocking. Unless otherwise specified, access from multiple threads to a single line must be externally synchronized - for example using pw::Borrowable. Unless otherwise specified, none of the methods are safe to call from an interrupt handler. Therefore, this abstraction may not be suitable for bitbanging and other low-level uses of GPIO.

Note that the initial state of a line is not guaranteed to be consistent with either the “enabled” or “disabled” state. Users of the API who need to ensure the line is disabled (ex. output not driving the line) should call Disable().

This class should almost never be used in APIs directly. Instead, use one of the derived classes that explicitly supports the functionality that your API needs.

This class cannot be extended directly. Instead, extend one of the derived classes that explicitly support the functionality that you want to implement.

Subclassed by pw::digital_io::DigitalIn, pw::digital_io::DigitalInInterrupt, pw::digital_io::DigitalInOut, pw::digital_io::DigitalInOutInterrupt, pw::digital_io::DigitalInterrupt, pw::digital_io::DigitalOut, pw::digital_io::DigitalOutInterrupt

Public Functions

inline constexpr bool provides_input() const#
Returns:

true if input (getting state) is supported.

inline constexpr bool provides_output() const#
Returns:

true if output (setting state) is supported.

inline constexpr bool provides_interrupt() const#
Returns:

true if interrupt handlers can be registered.

inline Result<State> GetState()#

Gets the state of the line.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Returns:

Code

Description

OK

Returns an active or inactive state.

FAILED_PRECONDITION

The line has not been enabled.

Returns Other status codes as defined by the backend.

inline Status SetState(State state)#

Sets the state of the line.

Callers are responsible to wait for the voltage level to settle after this call returns.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Returns:

Code

Description

OK

The state has been set.

FAILED_PRECONDITION

The line has not been enabled.

Returns other status codes as defined by the backend.

inline Result<bool> IsStateActive()#

Checks if the line is in the active state.

The line is in the active state when GetState() returns State::kActive.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Returns:

Code

Description

OK

true if the line is in the active state, otherwise false.

FAILED_PRECONDITION

The line has not been enabled.

Returns other status codes as defined by the backend.

inline Status SetStateActive()#

Sets the line to the active state. Equivalent to SetState(State::kActive).

Callers are responsible to wait for the voltage level to settle after this call returns.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Returns:

Code

Description

OK

The state has been set.

FAILED_PRECONDITION

The line has not been enabled.

Returns other status codes as defined by the backend.

inline Status SetStateInactive()#

Sets the line to the inactive state. Equivalent to SetState(State::kInactive).

Callers are responsible to wait for the voltage level to settle after this call returns.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Returns:

Code

Description

OK

The state has been set.

FAILED_PRECONDITION

The line has not been enabled.

Returns other status codes as defined by the backend.

inline Status SetInterruptHandler(InterruptTrigger trigger, InterruptHandler &&handler)#

Sets an interrupt handler to execute when an interrupt is triggered, and configures the condition for triggering the interrupt.

The handler is executed in a backend-specific context—this may be a system interrupt handler or a shared notification thread. Do not do any blocking or expensive work in the handler. The only universally safe operations are the IRQ-safe functions on pw_sync primitives.

In particular, it is NOT safe to get the state of a DigitalIo line—either from this line or any other DigitalIoOptional instance—inside the handler.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Pre:

No handler is currently set.

Returns:

Code

Description

OK

The interrupt handler was configured.

INVALID_ARGUMENT

The handler is empty.

Returns other status codes as defined by the backend.

inline Status ClearInterruptHandler()#

Clears the interrupt handler and disables any existing interrupts that are enabled.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Returns:

Code

Description

OK

The interrupt handler was cleared.

Returns other status codes as defined by the backend.

inline Status EnableInterruptHandler()#

Enables interrupts which will trigger the interrupt handler.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Pre:

A handler has been set using SetInterruptHandler().

Returns:

Code

Description

OK

The interrupt handler was configured.

FAILED_PRECONDITION

The line has not been enabled.

Returns other status codes as defined by the backend.

inline Status DisableInterruptHandler()#

Disables the interrupt handler. This is a no-op if interrupts are disabled.

This method can be called inside the interrupt handler for this line without any external synchronization. However, the exact behavior is backend-specific. There may be queued events that will trigger the handler again after this call returns.

Returns:

Code

Description

OK

The interrupt handler was disabled.

Returns other status codes as defined by the backend.

inline Status Enable()#

Enables the line to initialize it into the default state as determined by the backend.

This may enable pull-up/down resistors, drive the line high/low, etc. The line must be enabled before getting/setting the state or enabling interrupts. Callers are responsible for waiting for the voltage level to settle after this call returns.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Returns:

Code

Description

OK

The line is enabled and ready for use.

Returns other status codes as defined by the backend.

inline Status Disable()#

Disables the line to power down any pull-up/down resistors and disconnect from any voltage sources.

This is usually done to save power. Interrupt handlers are automatically disabled.

Warning

This method is not thread-safe and cannot be used in interrupt handlers.

Returns:

Code

Description

OK

The line is disabled.

Returns other status codes as defined by the backend.

Private Functions

virtual Status DoEnable(bool enable) = 0#

Enables the line to initialize it into the default state as determined by the backend or disables the line to power down any pull-up/down resistors and disconnect from any voltage sources.

This may enable pull-up/down resistors, drive the line high/low, etc. The line must be enabled before getting/setting the state or enabling interrupts. Callers are responsible for waiting for the voltage level to settle after this call returns.

Calling DoEnable(true) on an already-enabled line should be a no-op, it shouldn’t reset the line back to the “default state”.

Calling DoEnable(false) should force the line into the disabled state, If the line was not initialized at object construction time.

Pre:

This method cannot be used in interrupt contexts.

Pre:

When disabling, the interrupt handler must already be disabled.

Returns:

Code

Description

OK

The line is enabled and ready for use.

Returns other status codes as defined by the backend.

virtual Result<State> DoGetState() = 0#

Gets the state of the line.

Pre:

This method cannot be used in interrupt contexts.

Returns:

Code

Description

OK

An active or inactive state.

FAILED_PRECONDITION

The line has not been enabled.

Returns other status codes as defined by the backend.

virtual Status DoSetState(State level) = 0#

Sets the state of the line.

Callers are responsible to wait for the voltage level to settle after this call returns.

Pre:

This method cannot be used in interrupt contexts.

Returns:

Code

Description

OK

The state has been set.

FAILED_PRECONDITION

The line has not been enabled.

Returns other status codes as defined by the backend.

virtual Status DoSetInterruptHandler(InterruptTrigger trigger, InterruptHandler &&handler) = 0#

Sets or clears an interrupt handler to execute when an interrupt is triggered, and configures the condition for triggering the interrupt.

The handler is executed in a backend-specific context—this may be a system interrupt handler or a shared notification thread.

The implementation is expected to provide the handler the last known state of the input. The intention is to either sample the current state and provide that or if not possible provide the state which triggerred the interrupt (e.g. active for activating edge, and inactive for deactivating edge).

The handler is cleared by passing an empty handler, this can be checked by comparing the handler to a nullptr. The implementation must guarantee that the handler is not currently executing and (and will never execute again) after returning from DoSetInterruptHandler(_, nullptr).

Pre:

This method cannot be used in interrupt contexts.

Pre:

If setting a handler, no handler is permitted to be currently set.

Pre:

When cleaing a handler, the interrupt handler must be disabled.

Returns:

Code

Description

OK

The interrupt handler was configured.

Returns other status codes as defined by the backend.

virtual Status DoEnableInterruptHandler(bool enable) = 0#

Enables or disables interrupts which will trigger the interrupt handler.

Warning

This interrupt handler disabling must be both thread-safe and, interrupt-safe, however enabling is not interrupt-safe and not thread-safe.

Pre:

When enabling, a handler must have been set using DoSetInterruptHandler().

Pre:

Interrupt handler enabling cannot be used in interrupt contexts.

Returns:

Code

Description

OK

The interrupt handler was configured.

FAILED_PRECONDITION

The line has not been enabled.

Returns other status codes as defined by the backend.

Dependencies#

Zephyr#

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