Sygaldry
|
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.
Most (TODO: all) of the subroutines in the ESP-IDF GPIO API are then wrapped in this manner.
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:
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:
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...
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.
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.
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.
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.
At the time of writing, we test only the bare minimum functionality required to read a single button in a polling loop.