pw_gpio: Add new module and interfaces

NOTE: this module will be renamed `pw_digital_io` immediately in a
follow-up change.

The Digital IO interface represents individual GPIO lines that support
some combination of input, output, and/or interrupt functionality.
The choice of supported capability, and most other configuration details
are left up to the backend implementation.

Change-Id: I27437464ff918592e69a0bdbcbe005c68f2e9ef5
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/94100
Reviewed-by: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Anton Markov <amarkov@google.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b41d6f3..6dcc56f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -40,6 +40,7 @@
 add_subdirectory(pw_cpu_exception_cortex_m EXCLUDE_FROM_ALL)
 add_subdirectory(pw_file EXCLUDE_FROM_ALL)
 add_subdirectory(pw_function EXCLUDE_FROM_ALL)
+add_subdirectory(pw_gpio EXCLUDE_FROM_ALL)
 add_subdirectory(pw_hdlc EXCLUDE_FROM_ALL)
 add_subdirectory(pw_interrupt EXCLUDE_FROM_ALL)
 add_subdirectory(pw_interrupt_cortex_m EXCLUDE_FROM_ALL)
diff --git a/PIGWEED_MODULES b/PIGWEED_MODULES
index 7bf9e03..7e8d572 100644
--- a/PIGWEED_MODULES
+++ b/PIGWEED_MODULES
@@ -38,6 +38,7 @@
 pw_file
 pw_function
 pw_fuzzer
+pw_gpio
 pw_hdlc
 pw_hex_dump
 pw_i2c
diff --git a/pw_build/generated_pigweed_modules_lists.gni b/pw_build/generated_pigweed_modules_lists.gni
index 8a80c64..1ec4b65 100644
--- a/pw_build/generated_pigweed_modules_lists.gni
+++ b/pw_build/generated_pigweed_modules_lists.gni
@@ -68,6 +68,7 @@
   dir_pw_file = get_path_info("../pw_file", "abspath")
   dir_pw_function = get_path_info("../pw_function", "abspath")
   dir_pw_fuzzer = get_path_info("../pw_fuzzer", "abspath")
+  dir_pw_gpio = get_path_info("../pw_gpio", "abspath")
   dir_pw_hdlc = get_path_info("../pw_hdlc", "abspath")
   dir_pw_hex_dump = get_path_info("../pw_hex_dump", "abspath")
   dir_pw_i2c = get_path_info("../pw_i2c", "abspath")
@@ -203,6 +204,7 @@
     dir_pw_file,
     dir_pw_function,
     dir_pw_fuzzer,
+    dir_pw_gpio,
     dir_pw_hdlc,
     dir_pw_hex_dump,
     dir_pw_i2c,
@@ -304,6 +306,7 @@
     "$dir_pw_file:tests",
     "$dir_pw_function:tests",
     "$dir_pw_fuzzer:tests",
+    "$dir_pw_gpio:tests",
     "$dir_pw_hdlc:tests",
     "$dir_pw_hex_dump:tests",
     "$dir_pw_i2c:tests",
@@ -395,6 +398,7 @@
     "$dir_pw_file:docs",
     "$dir_pw_function:docs",
     "$dir_pw_fuzzer:docs",
+    "$dir_pw_gpio:docs",
     "$dir_pw_hdlc:docs",
     "$dir_pw_hex_dump:docs",
     "$dir_pw_i2c:docs",
diff --git a/pw_gpio/BUILD.bazel b/pw_gpio/BUILD.bazel
new file mode 100644
index 0000000..c84638c
--- /dev/null
+++ b/pw_gpio/BUILD.bazel
@@ -0,0 +1,48 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+    "//pw_build:pigweed.bzl",
+    "pw_cc_library",
+    "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+    name = "pw_gpio",
+    srcs = ["gpio.cc"],
+    hdrs = [
+        "public/pw_gpio/gpio.h",
+        "public/pw_gpio/internal/conversions.h",
+    ],
+    includes = ["public"],
+    deps = [
+        "//pw_assert",
+        "//pw_function",
+        "//pw_result",
+        "//pw_status",
+    ],
+)
+
+pw_cc_test(
+    name = "gpio_test",
+    srcs = ["gpio_test.cc"],
+    deps = [
+        ":pw_gpio",
+        "//pw_unit_test",
+    ],
+)
diff --git a/pw_gpio/BUILD.gn b/pw_gpio/BUILD.gn
new file mode 100644
index 0000000..3d8d7ff
--- /dev/null
+++ b/pw_gpio/BUILD.gn
@@ -0,0 +1,53 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+  include_dirs = [ "public" ]
+  visibility = [ ":*" ]
+}
+
+pw_source_set("pw_gpio") {
+  public_configs = [ ":public_include_path" ]
+  public = [
+    "public/pw_gpio/gpio.h",
+    "public/pw_gpio/internal/conversions.h",
+  ]
+  sources = [ "gpio.cc" ]
+  public_deps = [
+    dir_pw_assert,
+    dir_pw_function,
+    dir_pw_result,
+    dir_pw_status,
+  ]
+}
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+  tests = [ ":gpio_test" ]
+}
+
+pw_test("gpio_test") {
+  sources = [ "gpio_test.cc" ]
+  deps = [ ":pw_gpio" ]
+}
diff --git a/pw_gpio/CMakeLists.txt b/pw_gpio/CMakeLists.txt
new file mode 100644
index 0000000..66b93dc
--- /dev/null
+++ b/pw_gpio/CMakeLists.txt
@@ -0,0 +1,43 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_module_library(pw_gpio
+  HEADERS
+    public/pw_gpio/gpio.h
+    public/pw_gpio/internal/conversions.h
+  PUBLIC_INCLUDES
+    public
+  SOURCES
+    gpio.cc
+  PUBLIC_DEPS
+    pw_assert
+    pw_function
+    pw_result
+    pw_status
+)
+if(Zephyr_FOUND AND CONFIG_PIGWEED_GPIO)
+  zephyr_link_libraries(pw_gpio)
+endif()
+
+pw_add_test(pw_gpio.stream_test
+  SOURCES
+    gpio_test.cc
+  DEPS
+    pw_gpio
+  GROUPS
+    modules
+    pw_gpio
+)
diff --git a/pw_gpio/OWNERS b/pw_gpio/OWNERS
new file mode 100644
index 0000000..dcdb6bd
--- /dev/null
+++ b/pw_gpio/OWNERS
@@ -0,0 +1 @@
+amarkov@google.com
diff --git a/pw_gpio/README.md b/pw_gpio/README.md
new file mode 100644
index 0000000..7aee495
--- /dev/null
+++ b/pw_gpio/README.md
@@ -0,0 +1,7 @@
+This directory contains the `pw` Digital IO Hardware Abstraction Layer (HAL).
+This HAL defines interfaces for working with Digital IO lines that provide
+different combinations of capabilities (input, output, and/or interrupts).
+Hardware specific backends provide implementations of these interfaces for
+different hardware platforms.
+
+Warning: This module is under construction and may not be ready for use.
diff --git a/pw_gpio/docs.rst b/pw_gpio/docs.rst
new file mode 100644
index 0000000..7e1ad1e
--- /dev/null
+++ b/pw_gpio/docs.rst
@@ -0,0 +1,283 @@
+.. _module-pw_gpio:
+
+.. cpp:namespace-push:: pw::gpio
+
+=======
+pw_gpio
+=======
+.. warning::
+   This module is under construction and may not be ready for use.
+
+``pw_gpio`` provides a set of interfaces for using General Purpose Input and
+Output (GPIO) lines for Digital IO. 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:
+
+.. code-block:: cpp
+
+   using namespace pw::gpio;
+
+   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::gpio 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.
+* 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:
+
+.. list-table::
+   :header-rows: 1
+   :stub-columns: 1
+
+   * -
+     - Interrupts Not Required
+     - Interrupts Required
+   * - Input/Output Not Required
+     -
+     - :cpp:class:`DigitalInterrupt`
+   * - Input Required
+     - :cpp:class:`DigitalIn`
+     - :cpp:class:`DigitalInInterrupt`
+   * - Output Required
+     - :cpp:class:`DigitalOut`
+     - :cpp:class:`DigitalOutInterrupt`
+   * - Input/Output Required
+     - :cpp:class:`DigitalInOut`
+     - :cpp:class:`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.
+
+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_gpio`` 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 support additional for 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:
+
+.. code-block:: cpp
+
+   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_gpio`` 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, ``DigitalIoIn`` 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.
+
+------------
+Dependencies
+------------
+* :ref:`module-pw_assert`
+* :ref:`module-pw_function`
+* :ref:`module-pw_result`
+* :ref:`module-pw_status`
+
+.. cpp:namespace-pop::
+
+Zephyr
+======
+To enable ``pw_gpio`` for Zephyr add ``CONFIG_PIGWEED_GPIO=y`` to the
+project's configuration.
diff --git a/pw_gpio/gpio.cc b/pw_gpio/gpio.cc
new file mode 100644
index 0000000..a5ce209
--- /dev/null
+++ b/pw_gpio/gpio.cc
@@ -0,0 +1,60 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_gpio/gpio.h"
+
+namespace pw::gpio {
+
+Status DigitalInterrupt::DoSetState(State) {
+  PW_CRASH("DoSetState not implemented");
+}
+Result<State> DigitalInterrupt::DoGetState() {
+  PW_CRASH("DoGetState not implemented");
+}
+
+Status DigitalIn::DoSetState(State) { PW_CRASH("DoSetState not implemented"); }
+Status DigitalIn::DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) {
+  PW_CRASH("DoSetInterruptHandler not implemented");
+}
+Status DigitalIn::DoEnableInterruptHandler(bool) {
+  PW_CRASH("DoEnableInterruptHandler not implemented");
+}
+
+Status DigitalInInterrupt::DoSetState(State) {
+  PW_CRASH("DoSetState not implemented");
+}
+
+Result<State> DigitalOut::DoGetState() {
+  PW_CRASH("DoGetState not implemented");
+}
+Status DigitalOut::DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) {
+  PW_CRASH("DoSetInterruptHandler not implemented");
+}
+Status DigitalOut::DoEnableInterruptHandler(bool) {
+  PW_CRASH("DoEnableInterruptHandler not implemented");
+}
+
+Result<State> DigitalOutInterrupt::DoGetState() {
+  PW_CRASH("DoGetState not implemented");
+}
+
+Status DigitalInOut::DoSetInterruptHandler(InterruptTrigger,
+                                           InterruptHandler&&) {
+  PW_CRASH("DoSetInterruptHandler not implemented");
+}
+Status DigitalInOut::DoEnableInterruptHandler(bool) {
+  PW_CRASH("DoEnableInterruptHandler not implemented");
+}
+
+}  // namespace pw::gpio
diff --git a/pw_gpio/gpio_test.cc b/pw_gpio/gpio_test.cc
new file mode 100644
index 0000000..9a8c115
--- /dev/null
+++ b/pw_gpio/gpio_test.cc
@@ -0,0 +1,296 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_gpio/gpio.h"
+
+#include "gtest/gtest.h"
+#include "pw_status/status.h"
+
+namespace pw::gpio {
+namespace {
+
+// The base class should be compact.
+static_assert(sizeof(DigitalIoOptional) <= 2 * sizeof(void*),
+              "DigitalIo should be no larger than two pointers (vtable pointer "
+              "& packed members)");
+
+// Skeleton implementations to test DigitalIo methods.
+class TestDigitalInterrupt : public DigitalInterrupt {
+ public:
+  TestDigitalInterrupt() = default;
+
+ private:
+  Status DoEnable(bool) override { return OkStatus(); }
+
+  Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) override {
+    return OkStatus();
+  }
+  Status DoEnableInterruptHandler(bool) override { return OkStatus(); }
+};
+
+class TestDigitalIn : public DigitalIn {
+ public:
+  TestDigitalIn() : state_(State::kInactive) {}
+
+ private:
+  Status DoEnable(bool) override { return OkStatus(); }
+  Result<State> DoGetState() override { return state_; }
+
+  const State state_;
+};
+
+class TestDigitalInInterrupt : public DigitalInInterrupt {
+ public:
+  TestDigitalInInterrupt() : state_(State::kInactive) {}
+
+ private:
+  Status DoEnable(bool) override { return OkStatus(); }
+  Result<State> DoGetState() override { return state_; }
+
+  Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) override {
+    return OkStatus();
+  }
+  Status DoEnableInterruptHandler(bool) override { return OkStatus(); }
+
+  const State state_;
+};
+
+class TestDigitalOut : public DigitalOut {
+ public:
+  TestDigitalOut() {}
+
+ private:
+  Status DoEnable(bool) override { return OkStatus(); }
+  Status DoSetState(State) override { return OkStatus(); }
+};
+
+class TestDigitalOutInterrupt : public DigitalOutInterrupt {
+ public:
+  TestDigitalOutInterrupt() {}
+
+ private:
+  Status DoEnable(bool) override { return OkStatus(); }
+  Status DoSetState(State) override { return OkStatus(); }
+
+  Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) override {
+    return OkStatus();
+  }
+  Status DoEnableInterruptHandler(bool) override { return OkStatus(); }
+};
+
+class TestDigitalInOut : public DigitalInOut {
+ public:
+  TestDigitalInOut() : state_(State::kInactive) {}
+
+ private:
+  Status DoEnable(bool) override { return OkStatus(); }
+  Result<State> DoGetState() override { return state_; }
+  Status DoSetState(State state) override {
+    state_ = state;
+    return OkStatus();
+  }
+
+  State state_;
+};
+
+class TestDigitalInOutInterrupt : public DigitalInOutInterrupt {
+ public:
+  TestDigitalInOutInterrupt() : state_(State::kInactive) {}
+
+ private:
+  Status DoEnable(bool) override { return OkStatus(); }
+  Result<State> DoGetState() override { return state_; }
+  Status DoSetState(State state) override {
+    state_ = state;
+    return OkStatus();
+  }
+
+  Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) override {
+    return OkStatus();
+  }
+  Status DoEnableInterruptHandler(bool) override { return OkStatus(); }
+
+  State state_;
+};
+
+// Test conversions between different interfaces.
+static_assert(!std::is_convertible<TestDigitalInterrupt, DigitalIn&>());
+static_assert(!std::is_convertible<TestDigitalInterrupt, DigitalOut&>());
+static_assert(
+    !std::is_convertible<TestDigitalInterrupt, DigitalInInterrupt&>());
+static_assert(
+    !std::is_convertible<TestDigitalInterrupt, DigitalOutInterrupt&>());
+static_assert(
+    !std::is_convertible<TestDigitalInterrupt, DigitalInOutInterrupt&>());
+
+static_assert(!std::is_convertible<TestDigitalIn, DigitalOut&>());
+static_assert(!std::is_convertible<TestDigitalIn, DigitalInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalIn, DigitalInInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalIn, DigitalOutInterrupt&>());
+
+static_assert(std::is_convertible<TestDigitalInInterrupt, DigitalIn&>());
+static_assert(!std::is_convertible<TestDigitalInInterrupt, DigitalOut&>());
+static_assert(std::is_convertible<TestDigitalInInterrupt, DigitalInterrupt&>());
+static_assert(
+    !std::is_convertible<TestDigitalInInterrupt, DigitalOutInterrupt&>());
+
+static_assert(!std::is_convertible<TestDigitalOut, DigitalIn&>());
+static_assert(!std::is_convertible<TestDigitalOut, DigitalInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalOut, DigitalInInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalOut, DigitalOutInterrupt&>());
+
+static_assert(!std::is_convertible<TestDigitalOutInterrupt, DigitalIn&>());
+static_assert(std::is_convertible<TestDigitalOutInterrupt, DigitalOut&>());
+static_assert(
+    std::is_convertible<TestDigitalOutInterrupt, DigitalInterrupt&>());
+static_assert(
+    !std::is_convertible<TestDigitalOutInterrupt, DigitalInInterrupt&>());
+
+static_assert(std::is_convertible<TestDigitalInOut, DigitalIn&>());
+static_assert(std::is_convertible<TestDigitalInOut, DigitalOut&>());
+static_assert(!std::is_convertible<TestDigitalInOut, DigitalInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalInOut, DigitalInInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalInOut, DigitalOutInterrupt&>());
+
+static_assert(std::is_convertible<TestDigitalInOutInterrupt, DigitalIn&>());
+static_assert(std::is_convertible<TestDigitalInOutInterrupt, DigitalOut&>());
+static_assert(
+    std::is_convertible<TestDigitalInOutInterrupt, DigitalInterrupt&>());
+static_assert(
+    std::is_convertible<TestDigitalInOutInterrupt, DigitalInInterrupt&>());
+static_assert(
+    std::is_convertible<TestDigitalInOutInterrupt, DigitalOutInterrupt&>());
+
+void FakeInterruptHandler(State) {}
+
+void TestInput(DigitalIoOptional& line) {
+  ASSERT_EQ(OkStatus(), line.Enable());
+
+  auto state_result = line.GetState();
+  ASSERT_EQ(OkStatus(), state_result.status());
+  ASSERT_EQ(State::kInactive, state_result.value());
+
+  ASSERT_EQ(OkStatus(), line.Disable());
+}
+
+void TestOutput(DigitalIoOptional& line) {
+  ASSERT_EQ(OkStatus(), line.Enable());
+
+  ASSERT_EQ(OkStatus(), line.SetState(State::kActive));
+
+  ASSERT_EQ(OkStatus(), line.Disable());
+}
+
+void TestOutputReadback(DigitalIoOptional& line) {
+  ASSERT_EQ(OkStatus(), line.Enable());
+
+  ASSERT_EQ(OkStatus(), line.SetState(State::kActive));
+  auto state_result = line.GetState();
+  ASSERT_EQ(OkStatus(), state_result.status());
+  ASSERT_EQ(State::kActive, state_result.value());
+
+  ASSERT_EQ(OkStatus(), line.Disable());
+}
+
+void TestInterrupt(DigitalIoOptional& line) {
+  ASSERT_EQ(OkStatus(), line.Enable());
+
+  ASSERT_EQ(OkStatus(),
+            line.SetInterruptHandler(InterruptTrigger::kBothEdges,
+                                     FakeInterruptHandler));
+  ASSERT_EQ(OkStatus(), line.EnableInterruptHandler());
+  ASSERT_EQ(OkStatus(), line.EnableInterruptHandler());
+  ASSERT_EQ(OkStatus(), line.DisableInterruptHandler());
+  ASSERT_EQ(OkStatus(), line.ClearInterruptHandler());
+
+  ASSERT_EQ(OkStatus(), line.Disable());
+}
+
+TEST(Digital, Interrupt) {
+  TestDigitalInterrupt line;
+
+  ASSERT_EQ(false, line.provides_input());
+  ASSERT_EQ(false, line.provides_output());
+  ASSERT_EQ(true, line.provides_interrupt());
+
+  TestInterrupt(line);
+}
+
+TEST(Digital, In) {
+  TestDigitalIn line;
+
+  ASSERT_EQ(true, line.provides_input());
+  ASSERT_EQ(false, line.provides_output());
+  ASSERT_EQ(false, line.provides_interrupt());
+
+  TestInput(line);
+}
+
+TEST(Digital, InInterrupt) {
+  TestDigitalInInterrupt line;
+
+  ASSERT_EQ(true, line.provides_input());
+  ASSERT_EQ(false, line.provides_output());
+  ASSERT_EQ(true, line.provides_interrupt());
+
+  TestInput(line);
+  TestInterrupt(line);
+}
+
+TEST(Digital, Out) {
+  TestDigitalOut line;
+
+  ASSERT_EQ(false, line.provides_input());
+  ASSERT_EQ(true, line.provides_output());
+  ASSERT_EQ(false, line.provides_interrupt());
+
+  TestOutput(line);
+}
+
+TEST(Digital, OutInterrupt) {
+  TestDigitalOutInterrupt line;
+
+  ASSERT_EQ(false, line.provides_input());
+  ASSERT_EQ(true, line.provides_output());
+  ASSERT_EQ(true, line.provides_interrupt());
+
+  TestOutput(line);
+  TestInterrupt(line);
+}
+
+TEST(Digital, InOut) {
+  TestDigitalInOut line;
+
+  ASSERT_EQ(true, line.provides_input());
+  ASSERT_EQ(true, line.provides_output());
+  ASSERT_EQ(false, line.provides_interrupt());
+
+  TestInput(line);
+  TestOutputReadback(line);
+}
+
+TEST(DigitalIo, InOutInterrupt) {
+  TestDigitalInOutInterrupt line;
+
+  ASSERT_EQ(true, line.provides_input());
+  ASSERT_EQ(true, line.provides_output());
+  ASSERT_EQ(true, line.provides_interrupt());
+
+  TestInput(line);
+  TestOutputReadback(line);
+  TestInterrupt(line);
+}
+
+}  // namespace
+}  // namespace pw::gpio
diff --git a/pw_gpio/public/pw_gpio/gpio.h b/pw_gpio/public/pw_gpio/gpio.h
new file mode 100644
index 0000000..786c496
--- /dev/null
+++ b/pw_gpio/public/pw_gpio/gpio.h
@@ -0,0 +1,447 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_assert/check.h"
+#include "pw_function/function.h"
+#include "pw_gpio/internal/conversions.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+
+namespace pw::gpio {
+
+// The logical state of a digital line.
+enum class State : bool {
+  kActive = true,
+  kInactive = false,
+};
+
+// The triggering configuration for an interrupt handler.
+enum class InterruptTrigger : int {
+  // Trigger on transition from kInactive to kActive.
+  kActivatingEdge,
+  // Trigger on transition from kActive to kInactive.
+  kDeactivatingEdge,
+  // Trigger on any state transition between kActive and kInactive.
+  kBothEdges,
+};
+
+// Interrupt handling function. The argument contains the latest known state of
+// the line. It is backend-specific if, when, and how this state is updated.
+using InterruptHandler = ::pw::Function<void(State sampled_state)>;
+
+// 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.
+//
+class DigitalIoOptional {
+ public:
+  virtual ~DigitalIoOptional() = default;
+
+  // True if input (getting state) is supported.
+  constexpr bool provides_input() const { return config_.input; }
+  // True if output (setting state) is supported.
+  constexpr bool provides_output() const { return config_.output; }
+  // True if interrupt handlers can be registered.
+  constexpr bool provides_interrupt() const { return config_.interrupt; }
+
+  // Get the state of the line.
+  //
+  // This method is not thread-safe and cannot be used in interrupt handlers.
+  //
+  // Returns:
+  //
+  //   OK - an active or inactive state.
+  //   FAILED_PRECONDITION - The line has not been enabled.
+  //   Other status codes as defined by the backend.
+  //
+  Result<State> GetState() { return DoGetState(); }
+
+  // Set the state of the line.
+  //
+  // Callers are responsible to wait for the voltage level to settle after this
+  // call returns.
+  //
+  // This method is not thread-safe and cannot be used in interrupt handlers.
+  //
+  // Returns:
+  //
+  //   OK - the state has been set.
+  //   FAILED_PRECONDITION - The line has not been enabled.
+  //   Other status codes as defined by the backend.
+  //
+  Status SetState(State state) { return DoSetState(state); }
+
+  // Set an interrupt handler to execute when an interrupt is triggered, and
+  // Configure 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 GPIO line - either from
+  // this line or any other DigitalIoOptional instance - inside the handler.
+  //
+  // This method is not thread-safe and cannot be used in interrupt handlers.
+  //
+  // Precondition: no handler is currently set.
+  //
+  // Returns:
+  //   OK - the interrupt handler was configured.
+  //   INVALID_ARGUMENT - handler is empty.
+  //   Other status codes as defined by the backend.
+  //
+  Status SetInterruptHandler(InterruptTrigger trigger,
+                             InterruptHandler&& handler) {
+    if (handler == nullptr) {
+      return Status::InvalidArgument();
+    }
+    return DoSetInterruptHandler(trigger, std::move(handler));
+  }
+
+  // Clear the interrupt handler and disable interrupts if enabled.
+  //
+  // This method is not thread-safe and cannot be used in interrupt handlers.
+  //
+  // Returns:
+  //   OK - the itnerrupt handler was cleared.
+  //   Other status codes as defined by the backend.
+  //
+  Status ClearInterruptHandler() {
+    return DoSetInterruptHandler(InterruptTrigger::kActivatingEdge, nullptr);
+  }
+
+  // Enable interrupts which will trigger the interrupt handler.
+  //
+  // This method is not thread-safe and cannot be used in interrupt handlers.
+  //
+  // Precondition: a handler has been set using SetInterruptHandler.
+  //
+  // Returns:
+  //   OK - the interrupt handler was configured.
+  //   FAILED_PRECONDITION - The line has not been enabled.
+  //   Other status codes as defined by the backend.
+  //
+  Status EnableInterruptHandler() { return DoEnableInterruptHandler(true); }
+
+  // Disable 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:
+  //   OK - the interrupt handler was configured.
+  //   Other status codes as defined by the backend.
+  //
+  Status DisableInterruptHandler() { return DoEnableInterruptHandler(false); }
+
+  // Enable 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 or
+  // low, etc. The line must be enabled before getting/setting the state
+  // or enabling interrupts.
+  //
+  // Callers are responsible to wait for the voltage level to settle after this
+  // call returns.
+  //
+  // This method is not thread-safe and cannot be used in interrupt handlers.
+  //
+  // Returns:
+  //   OK - the line is enabled and ready for use.
+  //   Other status codes as defined by the backend.
+  //
+  Status Enable() { return DoEnable(true); }
+
+  // Disable 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.
+  //
+  // This method is not thread-safe and cannot be used in interrupt handlers.
+  //
+  // Returns:
+  //   OK - the line is disabled.
+  //   Other status codes as defined by the backend.
+  //
+  Status Disable() { return DoEnable(false); }
+
+ private:
+  friend class DigitalInterrupt;
+  friend class DigitalIn;
+  friend class DigitalInInterrupt;
+  friend class DigitalOut;
+  friend class DigitalOutInterrupt;
+  friend class DigitalInOut;
+  friend class DigitalInOutInterrupt;
+
+  // Private constructor so that only friends can extend us.
+  constexpr DigitalIoOptional(internal::Provides config) : config_(config) {}
+
+  // Implemented by derived classes to provide different functionality.
+  // See the documentation of the public functions for requirements.
+  virtual Status DoEnable(bool enable) = 0;
+  virtual Result<State> DoGetState() = 0;
+  virtual Status DoSetState(State level) = 0;
+  virtual Status DoSetInterruptHandler(InterruptTrigger trigger,
+                                       InterruptHandler&& handler) = 0;
+  virtual Status DoEnableInterruptHandler(bool enable) = 0;
+
+  // The configuration of this line.
+  const internal::Provides config_;
+};
+
+// A digital I/O line that supports only interrupts.
+//
+// The input and output methods are hidden and must not be called.
+//
+// Use this class in APIs when only interrupt functionality is required.
+// Extend this class to implement a line that only supports interrupts.
+//
+template <>
+struct internal::Requires<class DigitalInterrupt> {
+  static constexpr bool input = false;
+  static constexpr bool output = false;
+  static constexpr bool interrupt = true;
+};
+class DigitalInterrupt
+    : public DigitalIoOptional,
+      public internal::Conversions<DigitalInterrupt, DigitalIoOptional> {
+ public:
+  // Available functionality
+  using DigitalIoOptional::ClearInterruptHandler;
+  using DigitalIoOptional::DisableInterruptHandler;
+  using DigitalIoOptional::EnableInterruptHandler;
+  using DigitalIoOptional::SetInterruptHandler;
+
+ protected:
+  constexpr DigitalInterrupt()
+      : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalInterrupt>()) {}
+
+ private:
+  // Unavailable functionality
+  using DigitalIoOptional::GetState;
+  using DigitalIoOptional::SetState;
+
+  // These overrides invoke PW_CRASH.
+  Status DoSetState(State) final;
+  Result<State> DoGetState() final;
+};
+
+// A digital I/O line that supports only input (getting state).
+//
+// The output and interrupt methods are hidden and must not be called.
+//
+// Use this class in APIs when only input functionality is required.
+// Extend this class to implement a line that only supports getting state.
+//
+class DigitalIn : public DigitalIoOptional,
+                  public internal::Conversions<DigitalIn, DigitalIoOptional> {
+ public:
+  // Available functionality
+  using DigitalIoOptional::GetState;
+
+ protected:
+  constexpr DigitalIn()
+      : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalIn>()) {}
+
+ private:
+  // Unavailable functionality
+  using DigitalIoOptional::ClearInterruptHandler;
+  using DigitalIoOptional::DisableInterruptHandler;
+  using DigitalIoOptional::EnableInterruptHandler;
+  using DigitalIoOptional::SetInterruptHandler;
+  using DigitalIoOptional::SetState;
+
+  // These overrides invoke PW_CRASH.
+  Status DoSetState(State) final;
+  Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) final;
+  Status DoEnableInterruptHandler(bool) final;
+};
+
+// An input line that supports interrupts.
+//
+// The output methods are hidden and must not be called.
+//
+// Use in APIs when input and interrupt functionality is required.
+//
+// Extend this class to implement a line that supports input (getting state) and
+// listening for interrupts at the same time.
+//
+class DigitalInInterrupt
+    : public DigitalIoOptional,
+      public internal::Conversions<DigitalInInterrupt, DigitalIoOptional> {
+ public:
+  // Available functionality
+  using DigitalIoOptional::ClearInterruptHandler;
+  using DigitalIoOptional::DisableInterruptHandler;
+  using DigitalIoOptional::EnableInterruptHandler;
+  using DigitalIoOptional::GetState;
+  using DigitalIoOptional::SetInterruptHandler;
+
+ protected:
+  constexpr DigitalInInterrupt()
+      : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalInInterrupt>()) {}
+
+ private:
+  // Unavailable functionality
+  using DigitalIoOptional::SetState;
+
+  // These overrides invoke PW_CRASH.
+  Status DoSetState(State) final;
+};
+
+// A digital I/O line that supports only output (setting state).
+//
+// Input and interrupt functions are hidden and must not be called.
+//
+// Use in APIs when only output functionality is required.
+// Extend this class to implement a line that supports output only.
+//
+class DigitalOut : public DigitalIoOptional,
+                   public internal::Conversions<DigitalOut, DigitalIoOptional> {
+ public:
+  // Available functionality
+  using DigitalIoOptional::SetState;
+
+ protected:
+  constexpr DigitalOut()
+      : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalOut>()) {}
+
+ private:
+  // Unavailable functionality
+  using DigitalIoOptional::ClearInterruptHandler;
+  using DigitalIoOptional::DisableInterruptHandler;
+  using DigitalIoOptional::EnableInterruptHandler;
+  using DigitalIoOptional::GetState;
+  using DigitalIoOptional::SetInterruptHandler;
+
+  // These overrides invoke PW_CRASH.
+  Result<State> DoGetState() final;
+  Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) final;
+  Status DoEnableInterruptHandler(bool) final;
+};
+
+// A digital I/O line that supports output and interrupts.
+//
+// Input methods are hidden and must not be called.
+//
+// Use in APIs when output and interrupt functionality is required. For
+// example, to represent a two-way signalling line.
+//
+// Extend this class to implement a line that supports both output and
+// listening for interrupts at the same time.
+//
+class DigitalOutInterrupt
+    : public DigitalIoOptional,
+      public internal::Conversions<DigitalOutInterrupt, DigitalIoOptional> {
+ public:
+  // Available functionality
+  using DigitalIoOptional::ClearInterruptHandler;
+  using DigitalIoOptional::DisableInterruptHandler;
+  using DigitalIoOptional::EnableInterruptHandler;
+  using DigitalIoOptional::SetInterruptHandler;
+  using DigitalIoOptional::SetState;
+
+ protected:
+  constexpr DigitalOutInterrupt()
+      : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalOutInterrupt>()) {}
+
+ private:
+  // Unavailable functionality
+  using DigitalIoOptional::GetState;
+
+  // These overrides invoke PW_CRASH.
+  Result<State> DoGetState() final;
+};
+
+// A digital I/O line that supports both input and output.
+//
+// Use in APIs when both input and output functionality is required. For
+// example, to represent a line which is shared by multiple controllers.
+//
+// Extend this class to implement a line that supports both input and output at
+// the same time.
+//
+class DigitalInOut
+    : public DigitalIoOptional,
+      public internal::Conversions<DigitalInOut, DigitalIoOptional> {
+ public:
+  // Available functionality
+  using DigitalIoOptional::GetState;
+  using DigitalIoOptional::SetState;
+
+ protected:
+  constexpr DigitalInOut()
+      : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalInOut>()) {}
+
+ private:
+  // Unavailable functionality
+  using DigitalIoOptional::ClearInterruptHandler;
+  using DigitalIoOptional::DisableInterruptHandler;
+  using DigitalIoOptional::EnableInterruptHandler;
+  using DigitalIoOptional::SetInterruptHandler;
+
+  // These overrides invoke PW_CRASH.
+  Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) final;
+  Status DoEnableInterruptHandler(bool) final;
+};
+
+// A line that supports input, output, and interrupts.
+//
+// Use in APIs when input, output, and interrupts are required. For example to
+// represent a two-way shared line with state transition notifications.
+//
+// Extend this class to implement a line that supports all the functionality at
+// the same time.
+//
+class DigitalInOutInterrupt
+    : public DigitalIoOptional,
+      public internal::Conversions<DigitalInOutInterrupt, DigitalIoOptional> {
+ public:
+  // Available functionality
+  using DigitalIoOptional::ClearInterruptHandler;
+  using DigitalIoOptional::DisableInterruptHandler;
+  using DigitalIoOptional::EnableInterruptHandler;
+  using DigitalIoOptional::GetState;
+  using DigitalIoOptional::SetInterruptHandler;
+  using DigitalIoOptional::SetState;
+
+ protected:
+  constexpr DigitalInOutInterrupt()
+      : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalInOutInterrupt>()) {
+  }
+};
+
+}  // namespace pw::gpio
diff --git a/pw_gpio/public/pw_gpio/internal/conversions.h b/pw_gpio/public/pw_gpio/internal/conversions.h
new file mode 100644
index 0000000..99595de
--- /dev/null
+++ b/pw_gpio/public/pw_gpio/internal/conversions.h
@@ -0,0 +1,141 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <type_traits>
+
+namespace pw::gpio {
+namespace internal {
+
+// A type trait that describes the functionality required by a particular type
+// of line, and also the functionality that we assume is always provided by an
+// instance of that line. Used by Converter to determine if a conversion should
+// be allowed.
+//
+// Specialize this for each type of DigitalIO line by defining a required
+// functionality as `true` and optional functionality as `false`.
+//
+// Specializations must define the following fields:
+// static constexpr bool input;
+// static constexpr bool output;
+// static constexpr bool interrupt;
+//
+template <typename T>
+struct Requires;
+
+// Concrete struct describing the available functionality of a line at runtime.
+struct Provides {
+  bool input;
+  bool output;
+  bool interrupt;
+};
+
+// Returns the functionality always provided by the given type.
+template <typename T>
+constexpr Provides AlwaysProvidedBy() {
+  return {
+      .input = Requires<T>::input,
+      .output = Requires<T>::output,
+      .interrupt = Requires<T>::interrupt,
+  };
+}
+
+// Provides conversion operators between line objects based on the available
+// functionality.
+template <typename Self, typename CommonBase>
+class Conversions {
+ private:
+  // Static check to enable conversions from `Self` to `T`.
+  template <
+      typename T,
+      typename = std::enable_if_t<std::is_base_of_v<CommonBase, T>>,
+      typename = std::enable_if_t<Requires<Self>::input || !Requires<T>::input>,
+      typename =
+          std::enable_if_t<Requires<Self>::output || !Requires<T>::output>,
+      typename = std::enable_if_t<Requires<Self>::interrupt ||
+                                  !Requires<T>::interrupt>>
+  struct Enabled {};
+
+ public:
+  template <typename T, typename = Enabled<T>>
+  constexpr operator T&() {
+    return as<T>();
+  }
+
+  template <typename T, typename = Enabled<T>>
+  constexpr operator const T&() const {
+    return as<T>();
+  }
+
+  template <typename T, typename = Enabled<T>>
+  constexpr T& as() {
+    return static_cast<T&>(static_cast<CommonBase&>(static_cast<Self&>(*this)));
+  }
+
+  template <typename T, typename = Enabled<T>>
+  constexpr const T& as() const {
+    return static_cast<const T&>(
+        static_cast<const CommonBase&>(static_cast<const Self&>(*this)));
+  }
+};
+
+}  // namespace internal
+
+// Specializations of Requires for each of the line types.
+// These live outside the `internal` namespace so that the forward class
+// declarations are in the correct namespace.
+
+template <>
+struct internal::Requires<class DigitalIn> {
+  static constexpr bool input = true;
+  static constexpr bool output = false;
+  static constexpr bool interrupt = false;
+};
+
+template <>
+struct internal::Requires<class DigitalInInterrupt> {
+  static constexpr bool input = true;
+  static constexpr bool output = false;
+  static constexpr bool interrupt = true;
+};
+
+template <>
+struct internal::Requires<class DigitalOut> {
+  static constexpr bool input = false;
+  static constexpr bool output = true;
+  static constexpr bool interrupt = false;
+};
+
+template <>
+struct internal::Requires<class DigitalOutInterrupt> {
+  static constexpr bool input = false;
+  static constexpr bool output = true;
+  static constexpr bool interrupt = true;
+};
+
+template <>
+struct internal::Requires<class DigitalInOut> {
+  static constexpr bool input = true;
+  static constexpr bool output = true;
+  static constexpr bool interrupt = false;
+};
+
+template <>
+struct internal::Requires<class DigitalInOutInterrupt> {
+  static constexpr bool input = true;
+  static constexpr bool output = true;
+  static constexpr bool interrupt = true;
+};
+
+}  // namespace pw::gpio