From 8ee27aa2cd81814079bd8b66f17e40e9408aa523 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Thu, 21 Mar 2024 17:45:59 -0700 Subject: [PATCH] upgrade to linux kernel's character device API (v2 ABI) The Linux kernel has finally started phasing out the old userspace API that used sysfs calls for GPIO pins. This phased out API has impacted RPi5 since it exposes the GPIO pins in a new way - using a separate RP1 chip attached to PCI Express. Now, the sysfs approach no longer works as conveniently as it did in the past. The character device API works better and more efficiently with other benefits, namely thread safety, and it is compatible with most Linux distributions. --- src/utility/linux_kernel/gpio.cpp | 244 ++++++++++++++++++------------ src/utility/linux_kernel/gpio.h | 41 ++++- 2 files changed, 181 insertions(+), 104 deletions(-) diff --git a/src/utility/linux_kernel/gpio.cpp b/src/utility/linux_kernel/gpio.cpp index fccd20e..1d6a5ae 100644 --- a/src/utility/linux_kernel/gpio.cpp +++ b/src/utility/linux_kernel/gpio.cpp @@ -17,17 +17,67 @@ * SOFTWARE. */ #ifndef ARDUINO - #include - #include - #include - #include - #include - #include + #include + #include // close() + #include // open() + #include // ioctl() + #include // errno, strerror() + #include // std::string, strcpy() + #include #include "gpio.h" namespace cirque_pinnacle_arduino_wrappers { -std::map GPIOClass::cache; +// instantiate some global structs to setup cache +// doing this globally ensures the data struct is zero-ed out +typedef int gpio_fd; // for readability +std::map cachedPins; +struct gpio_v2_line_request request; +struct gpio_v2_line_values data; + +void GPIOChipCache::openDevice() +{ + if (fd < 0) { + fd = open(chip, O_RDONLY); + if (fd < 0) { + std::string msg = "Can't open device "; + msg += chip; + msg += "; "; + msg += strerror(errno); + throw GPIOException(msg); + return; + } + } + chipInitialized = true; +} + +void GPIOChipCache::closeDevice() +{ + if (fd >= 0) { + close(fd); + fd = -1; + } +} + +GPIOChipCache::GPIOChipCache() +{ + request.num_lines = 1; + strcpy(request.consumer, "CirquePinnacle lib"); + data.mask = 1ULL; // only change value for specified pin +} + +GPIOChipCache::~GPIOChipCache() +{ + closeDevice(); + for (std::map::iterator i = cachedPins.begin(); i != cachedPins.end(); ++i) { + if (i->second > 0) { + close(i->second); + } + } +} + +// GPIO chip cache manager +GPIOChipCache gpioCache; GPIOClass::GPIOClass() { @@ -39,121 +89,117 @@ GPIOClass::~GPIOClass() void GPIOClass::open(pinnacle_gpio_t port, bool direction) { - FILE* f; - f = fopen("/sys/class/gpio/export", "w"); - if (f == NULL) { - throw GPIOException("Can't export GPIO pin. Check access rights."); - } - fprintf(f, "%d\n", port); - fclose(f); - - int counter = 0; - char file[128]; - sprintf(file, "/sys/class/gpio/gpio%d/direction", port); - - while ((f = fopen(file, "w")) == NULL) { - //Wait 10 seconds for the file to be accessible if not open on first attempt - sleep(1); - counter++; - if (counter > 10) { - throw GPIOException("Can't access GPIO pin direction. Check access rights."); + try { + gpioCache.openDevice(); + } + catch (GPIOException& exc) { + if (gpioCache.chipInitialized) { + throw exc; + return; } + gpioCache.chip = "/dev/gpiochip0"; + gpioCache.openDevice(); } - int l = direction ? fprintf(f, "out\n") : fprintf(f, "in\n"); - if (!(l == 3 || l == 4)) { - fclose(f); - throw GPIOException("Can't set direction of GPIO pin. Check access rights."); + + // get chip info + gpiochip_info info; + memset(&info, 0, sizeof(info)); + int ret = ioctl(gpioCache.fd, GPIO_GET_CHIPINFO_IOCTL, &info); + if (ret < 0) { + std::string msg = "Could not gather info about "; + msg += gpioCache.chip; + throw GPIOException(msg); + return; } - fclose(f); - // Caches the GPIO descriptor - sprintf(file, "/sys/class/gpio/gpio%d/value", port); - int flags = direction ? O_WRONLY : O_RDONLY; - int fd = ::open(file, flags); - if (fd < 0) { - throw GPIOException("Can't initialize GPIO pin. Check access rights."); + if (port > info.lines) { + std::string msg = "pin number " + std::to_string(port) + " not available for " + gpioCache.chip; + throw GPIOException(msg); + return; + } + + // check if pin is already in use + std::map::iterator pin = cachedPins.find(port); + if (pin == cachedPins.end()) { // pin not in use; add it to cached request + request.offsets[0] = port; + request.fd = 0; } else { - cache[port] = fd; // cache the fd; - lseek(fd, 0, SEEK_SET); + request.fd = pin->second; + } + + if (request.fd <= 0) { + ret = ioctl(gpioCache.fd, GPIO_V2_GET_LINE_IOCTL, &request); + if (ret == -1 || request.fd <= 0) { + std::string msg = "[GPIO::open] Can't get line handle from IOCTL; "; + msg += strerror(errno); + throw GPIOException(msg); + return; + } } + gpioCache.closeDevice(); // in case other apps want to access it + + // set the pin and direction + request.config.flags = direction ? GPIO_V2_LINE_FLAG_OUTPUT : GPIO_V2_LINE_FLAG_INPUT; + + ret = ioctl(request.fd, GPIO_V2_LINE_SET_CONFIG_IOCTL, &request.config); + if (ret == -1) { + std::string msg = "[gpio::open] Can't set line config; "; + msg += strerror(errno); + throw GPIOException(msg); + return; + } + cachedPins.insert(std::pair(port, request.fd)); } void GPIOClass::close(pinnacle_gpio_t port) { - std::map::iterator i; - i = cache.find(port); - if (i != cache.end()) { - ::close(i->second); // close the cached fd - cache.erase(i); // Delete cache entry - } - // Do unexport - FILE* f; - f = fopen("/sys/class/gpio/unexport", "w"); - if (f != NULL) { - fprintf(f, "%d\n", port); - fclose(f); + std::map::iterator pin = cachedPins.find(port); + if (pin == cachedPins.end()) { + return; + } + if (pin->second > 0) { + ::close(pin->second); } + cachedPins.erase(pin); } bool GPIOClass::read(pinnacle_gpio_t port) { - int fd; - std::map::iterator i = cache.find(port); - if (i == cache.end()) { - throw GPIOException("GPIO pin not initialized."); - } - else { - fd = i->second; + std::map::iterator pin = cachedPins.find(port); + if (pin == cachedPins.end() || pin->second <= 0) { + throw GPIOException("[GPIO::read] pin not initialized! Use GPIO::open() first"); + return -1; } - if (lseek(fd, 0, SEEK_SET) != 0) { - if (errno == EBADF) - throw GPIOException("GPIO::read lseek() with an invalid file descriptor."); - else if (errno == EINVAL) - throw GPIOException("GPIO::read using invalid lseek(..., whence) value."); - else if (errno == ENXIO) - throw GPIOException("GPIO::read lseek(..., offset, whence) specifies position beyond end-of-file."); - else if (errno == EOVERFLOW) - throw GPIOException("GPIO::read lseek() resulting offset is out-of-bounds for a signed integer."); - else if (errno == ESPIPE) - throw GPIOException("GPIO::read lseek() file descriptor is associated with a pipe, socket, or FIFO."); - } - char c; - if (::read(fd, &c, 1) == 1) { - return (c == '0') ? 0 : 1; - } - else { - throw GPIOException("Can't read GPIO pin"); - } + data.bits = 0ULL; + + int ret = ioctl(pin->second, GPIO_V2_LINE_GET_VALUES_IOCTL, &data); + if (ret == -1) { + std::string msg = "[GPIO::read] Can't get line value from IOCTL; "; + msg += strerror(errno); + throw GPIOException(msg); + return ret; + } + return data.bits & 1ULL; } void GPIOClass::write(pinnacle_gpio_t port, const char* value) { - gpio_cache_fd_t fd; - std::map::iterator i = cache.find(port); - if (i == cache.end()) { - throw GPIOException("GPIO pin not initialized."); + std::map::iterator pin = cachedPins.find(port); + if (pin == cachedPins.end() || pin->second <= 0) { + throw GPIOException("[GPIO::write] pin not initialized! Use GPIO::open() first"); + return; } - else { - fd = i->second; - } - - if (lseek(fd, 0, SEEK_SET) != 0) { - if (errno == EBADF) - throw GPIOException("GPIO::write lseek() with an invalid file descriptor."); - else if (errno == EINVAL) - throw GPIOException("GPIO::write using invalid lseek(..., whence) value."); - else if (errno == ENXIO) - throw GPIOException("GPIO::write lseek(..., offset, whence) specifies position beyond end-of-file."); - else if (errno == EOVERFLOW) - throw GPIOException("GPIO::write lseek() resulting offset is out-of-bounds for a signed integer."); - else if (errno == ESPIPE) - throw GPIOException("GPIO::write lseek() file descriptor is associated with a pipe, socket, or FIFO."); - } - int l = ::write(fd, value, 2); - if (l != 2) { - throw GPIOException("Can't write to GPIO pin"); + + data.bits = value; + + int ret = ioctl(pin->second, GPIO_V2_LINE_SET_VALUES_IOCTL, &data); + if (ret == -1) { + std::string msg = "[GPIO::write] Can't set line value from IOCTL; "; + msg += strerror(errno); + throw GPIOException(msg); + return; } } diff --git a/src/utility/linux_kernel/gpio.h b/src/utility/linux_kernel/gpio.h index e3bedbe..7a14ad6 100644 --- a/src/utility/linux_kernel/gpio.h +++ b/src/utility/linux_kernel/gpio.h @@ -21,7 +21,8 @@ #ifndef ARDUINO #include - #include + #include + #include // gpiochip_info #ifdef __cplusplus extern "C" { @@ -30,6 +31,13 @@ extern "C" { typedef int pinnacle_gpio_t; const pinnacle_gpio_t PINNACLE_SW_DR = 0x7FFFFFFF; + #ifndef PINNACLE_LINUX_GPIO_CHIP + /** + * The default GPIO chip to use. Defaults to `/dev/gpiochip4` (for RPi5). + * Falls back to `/dev/gpiochip0` if this value is somehow incorrect. + */ + #define PINNACLE_LINUX_GPIO_CHIP "/dev/gpiochip4" + #endif namespace cirque_pinnacle_arduino_wrappers { /** Specific exception for GPIO errors */ @@ -42,7 +50,29 @@ namespace cirque_pinnacle_arduino_wrappers { } }; - typedef int gpio_cache_fd_t; + /// A struct to manage the GPIO chip file descriptor. + /// This struct's destructor should close any cached GPIO pin requests' file descriptors. + struct GPIOChipCache + { + const char* chip = PINNACLE_LINUX_GPIO_CHIP; + int fd = -1; + bool chipInitialized = false; + + /// Open the File Descriptor for the GPIO chip + void openDevice(); + + /// Close the File Descriptor for the GPIO chip + void closeDevice(); + + /// should be called automatically on program start. + /// Here, we do some one-off configuration. + GPIOChipCache(); + + /// Should be called automatically on program exit. + /// What we need here is to make sure that the File Descriptors used to + /// control GPIO pins are properly closed. + ~GPIOChipCache(); + }; class GPIOClass { @@ -50,10 +80,11 @@ namespace cirque_pinnacle_arduino_wrappers { public: GPIOClass(); - static constexpr char OUTPUT_HIGH[2] = {'1', '\n'}; - static constexpr char OUTPUT_LOW[2] = {'0', '\n'}; - static const bool DIRECTION_IN = false; static const bool DIRECTION_OUT = true; + static const bool DIRECTION_IN = false; + + static const bool OUTPUT_HIGH = true; + static const bool OUTPUT_LOW = false; /** * Similar to Arduino pinMode(pin, mode);