Copyright 2023 Travis J. West, https://traviswest.ca, Input Devices and Music Interaction Laboratory (IDMIL), Centre for Interdisciplinary Research in Music Media and Technology (CIRMMT), McGill University, Montréal, Canada, and Univ. Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRIStAL, F-59000 Lille, France
SPDX-License-Identifier: MIT
This document implements type safe and lightly error-checked wrapper that attempts to expose all available behavior for a single GPIO pin through static methods of a template class, for use in ESP32 sygaldry
components. It is currently implemented via the ESP-IDF. Not all functionality has been wrapped yet, and much of it remains un-tested. GPIO reference documentation for the current version of the ESP-IDF is found here. The relevant public domain example code is found here.
The ESP-IDF provides a very uniform interface for interacting with GPIO. Almost all methods return an esp_err_t
error code, and all arguments (with the exception of a few related to interrupts) are either pin numbers or enumerations; it can reasonably be assumed that these values are known at compile time in almost all cases.
Our strategy for wrapping this API is to declare static methods without arguments. Each method calls one API function, checks the error code, and returns it. Because all of these method implementations are identical except for the names of things, we use a macro to facilitate implementation without repeating ourselves.
Endpoints
#define gpio_function(c_name, esp_idf_func, ... )\
static auto c_name() noexcept\
{\
auto ret = esp_idf_func(__VA_ARGS__);\
ESP_ERROR_CHECK_WITHOUT_ABORT(ret);\
return ret;\
}
Most (TODO: all) of the subroutines in the ESP-IDF GPIO API are then wrapped in this manner.
Component Wishes
It would be convenient to encapsulate this functionality in a true component, treating the functions as message inputs, so that a textual name and description could be provided alongside, so that the component serves as an executable manual. However, there are issues with creating reflectable message endpoints that are also convenient to use in a subcomponent. For instance, we could declare the endpoint as a functor. However, since the functor has no way to access the data of the parent component, the parent must be passed in as an argument:
struct my_message_t : name_<"my message">, description_<"a useful hint"> {
void operator()(Parent& parent) {
}
} my_message;
my_component.inputs.my_message(my_component);
Such an implementation also generally compiles such that the message endpoint take up space, despite that it has no state. The approach taken in Avendish is for message endpoints to return a function; this nicely solves the latter issue while making the interface for a user of the message endpoint in a subcomponent quite unfriendly, e.g. for a message with no arguments:
decltype(my_component)::messages::my_message::func()(my_component);
For now, we draw the line at messages. Any endpoint that can be represented with value semantics, we allow, and any component that would require messages, we avoid. Hence, our GPIO wrapper cannot yet be implemented as a true component. However, name strings and descriptions left over from an attempt to implement such a component are retained as comments, in hopes they may be useful one day beyond being documentation...
Inputs
gpio_function(remove_interrupt_handler, gpio_isr_handler_remove, pin_number);
@{weird inputs}
gpio_function(enable_interrupt, gpio_intr_enable, pin_number);
gpio_function(disable_interrupt, gpio_intr_disable, pin_number);
gpio_function(reset, gpio_reset_pin, pin_number);
gpio_function(rising_edge, gpio_set_intr_type, pin_number, GPIO_INTR_POSEDGE);
gpio_function(falling_edge, gpio_set_intr_type, pin_number, GPIO_INTR_NEGEDGE);
gpio_function(any_edge, gpio_set_intr_type, pin_number, GPIO_INTR_ANYEDGE);
gpio_function(low_level, gpio_set_intr_type, pin_number, GPIO_INTR_LOW_LEVEL);
gpio_function(high_level, gpio_set_intr_type, pin_number, GPIO_INTR_HIGH_LEVEL);
gpio_function(high, gpio_set_level, pin_number, 1);
gpio_function(low, gpio_set_level, pin_number, 0);
gpio_function(disable_pin, gpio_set_direction, pin_number, GPIO_MODE_DISABLE);
gpio_function(input_mode, gpio_set_direction, pin_number, GPIO_MODE_INPUT);
gpio_function(output_mode, gpio_set_direction, pin_number, GPIO_MODE_OUTPUT);
gpio_function(output_od_mode, gpio_set_direction, pin_number, GPIO_MODE_OUTPUT_OD);
gpio_function(input_output_mode, gpio_set_direction, pin_number, GPIO_MODE_INPUT_OUTPUT);
gpio_function(input_output_od_mode, gpio_set_direction, pin_number, GPIO_MODE_INPUT_OUTPUT_OD);
gpio_function(enable_pullup, gpio_set_pull_mode, pin_number, GPIO_PULLUP_ONLY);
gpio_function(enable_pulldown, gpio_set_pull_mode, pin_number, GPIO_PULLDOWN_ONLY);
gpio_function(enable_pullup_and_pulldown, gpio_set_pull_mode, pin_number, GPIO_PULLUP_PULLDOWN);
gpio_function(disable_pullup_and_pulldown, gpio_set_pull_mode, pin_number, GPIO_FLOATING);
gpio_function(disable_pullup, gpio_pullup_dis, pin_number);
gpio_function(disable_pulldown, gpio_pulldown_dis, pin_number);
gpio_function(rising_edge_wakeup, gpio_wakeup_enable, pin_number, GPIO_INTR_POSEDGE);
gpio_function(falling_edge_wakeup, gpio_wakeup_enable, pin_number, GPIO_INTR_NEGEDGE);
gpio_function(any_edge_wakeup, gpio_wakeup_enable, pin_number, GPIO_INTR_ANYEDGE);
gpio_function(low_level_wakeup, gpio_wakeup_enable, pin_number, GPIO_INTR_LOW_LEVEL);
gpio_function(high_level_wakeup, gpio_wakeup_enable, pin_number, GPIO_INTR_HIGH_LEVEL);
gpio_function(disable_wakeup, gpio_wakeup_disable, pin_number);
gpio_function(wakeup_high, gpio_wakeup_enable, pin_number, GPIO_INTR_HIGH_LEVEL);
gpio_function(wakeup_low, gpio_wakeup_enable, pin_number, GPIO_INTR_LOW_LEVEL);
gpio_function(set_drive_weakest, gpio_set_drive_capability, pin_number, GPIO_DRIVE_CAP_0);
gpio_function(set_drive_weak, gpio_set_drive_capability, pin_number, GPIO_DRIVE_CAP_1);
gpio_function(set_drive_medium, gpio_set_drive_capability, pin_number, GPIO_DRIVE_CAP_2);
gpio_function(set_drive_strong, gpio_set_drive_capability, pin_number, GPIO_DRIVE_CAP_DEFAULT);
gpio_function(set_drive_strongest, gpio_set_drive_capability, pin_number, GPIO_DRIVE_CAP_3);
Interrupt Handler
A few API calls require unusual arguments or have different return values. These are implemented seperately, incurring a small but hopefully tolerable amount of duplication.
As previously mentioned, the input port to install an interrupt handler is an exception to the general pattern. The component defers design of an ISR to the user, so this port accept a pointer to the ISR function and its context as arguments and passes them to the ESP-IDF method.
static auto interrupt_handler(void (*handler)(void*), void* args) noexcept
{
auto ret = gpio_isr_handler_add(pin_number, handler, args);
ESP_ERROR_CHECK_WITHOUT_ABORT(ret);
return ret;
}
Similarly, reponsibility for the interrupt allocation flags for the IDF-provided ISR service is also deferred to the user. The ISR uninstaller has no return value, so it also requires a unique implementation.
static void uninstall_isr_service() noexcept
{
gpio_uninstall_isr_service();
}
static auto install_isr_service(int intr_alloc_flags) noexcept
{
auto ret = gpio_install_isr_service(intr_alloc_flags);
ESP_ERROR_CHECK_WITHOUT_ABORT(ret);
return ret;
}
Outputs
There are significantly fewer output endpoints, since reading data from the GPIO is considerably less involved than configuring it just right.
One thing to note: since the only possible error for gpio_get_drive_capability is ESP_ERR_INVALID_ARG and the only arg that could be invalid is the pin number could be invalid, or the pointer could be null since we can statically guarantee that neither of these is the case, we can ignore the error code from this IDF function and avoid having to return the drive_capability by output argument from out port, and instead implement it as a getter. Similarly, gpio_get_level
never returns an error, so the output endpoint for this API can also be implemented as a getter.
static auto level() noexcept {
return gpio_get_level(pin_number);
}
static auto drive_capability() noexcept {
auto ret = GPIO_DRIVE_CAP_DEFAULT;
gpio_get_drive_capability(pin_number, &ret);
return ret;
}
Initialization and Pin Number Assertions
The GPIO doesn't actually require much initialization. A call to inputs.reset()
is more than adequate. We take the opportunity presented by the method, however, to assert certain requirements on the pin number. Although the ESP32 has up to 39 pins, many of these cannot conventionally be used for one reason or another as GPIO. Pins 0 to 3 (pins 0 and 1 for strapping and pins 2 and 3 for UART) are used for programming and pins 6 to 11, 16, and 17 are used for SPI flash memory–these pins cannot be used as GPIO in almost any application. Furthermore: pins 12 to 15 are used for debugging with JTAG; pin 12 strapping additionally sets the LDO voltage regulator's output voltage at boot; pins 5 and 15 strapping additionally set SDIO timing and debug logging behaviors at boot; pins 20 and 28 to 31 are not mentioned in the documentation, nor the datasheet, suggesting that these hypothetical GPIO do not exist; pins 18, 19, 21, 22, and 23 are also used for the VSPI
serial peripheral interface; pins 25 to 27 cannot be used at the same time as WiFi; and pins 32 to 39 are shared with one of the analog-to-digital converters. Indeed, there is not a single pin on the ESP32 that is not multi-purpose. It is a GPIO starved platform.
The most detailed documentation on pin functions can be found in the datasheet. The documentation also provides additional guidance. The pinout diagram for a given MCU board can offer further advice where available.
static void init()
{
static_assert(GPIO_NUM_0 <= pin_number && pin_number <= GPIO_NUM_39,
"pin number invalid");
static_assert(pin_number != GPIO_NUM_0, "GPIO0 is an important strapping pin"
"used during boot to determine SPI boot (pulled up, default) or"
"download boot (pulled down). It should not be used for GPIO");
static_assert(pin_number != GPIO_NUM_1, "GPIO1 is UART TXD, used for"
"programming, and should not be used for GPIO");
static_assert(pin_number != GPIO_NUM_2, "GPIO2 is an important strapping pin"
"that must be pulled down during boot to initiate firmware download."
"It should not be used for GPIO");
static_assert(pin_number != GPIO_NUM_3, "GPIO3 is UART_RXD, used for"
"programming, and should not be used for GPIO");
static_assert(!(GPIO_NUM_6 <= pin_number && pin_number <= GPIO_NUM_11)
&& pin_number != GPIO_NUM_16 && pin_number != GPIO_NUM_17,
"GPIO6-11, 16, and 17 are used by SPI flash memory and shoult not be"
"used for GPIO");
static_assert(pin_number != GPIO_NUM_20 && !(GPIO_NUM_28 <= pin_number && pin_number <= GPIO_NUM_32),
"GPIO20, and 28-32 likely don't exist, and can't be used for GPIO");
reset();
}
Summary
#pragma once
#include <driver/gpio.h>
#include <hal/gpio_types.h>
#include <sygah-metadata.hpp>
#include <sygah-endpoints.hpp>
namespace sygaldry { namespace sygse {
template<gpio_num_t pin_number>
struct GPIO
: name_<"GPIO Pin">
, author_<"Travis J. West">
, copyright_<"Travis J. West (C) 2023">
, description_<"An ESP-IDF GPIO API wrapper as a message-based `sygaldry` component">
{
@{gpio_function macro}
@{input wrappers};
@{output wrappers};
#undef gpio_function
@{init function with assertions}
};
} }
Tests
At the time of writing, we test only the bare minimum functionality required to read a single button in a polling loop.
#pragma once
#include <sygse-gpio.hpp>
void gpio()
{
using pin = sygaldry::components::esp32::GPIO<GPIO_NUM_23>;
pin::init();
pin::input_mode();
pin::enable_pullup();
TEST_ASSERT_EQUAL_INT_MESSAGE(1, pin::level(), "input mode pin with pullup should read high level");
pin::disable_pullup();
pin::enable_pulldown();
TEST_ASSERT_EQUAL_INT_MESSAGE(0, pin::level(), "input mode pin with pulldown should read low level");
}
# @#'CMakeLists.txt'
set(lib sygse-gpio)
add_library(${lib} INTERFACE)
target_include_directories(${lib} INTERFACE .)
# @/