Skip to content

Commit

Permalink
upgrade to linux kernel's character device API (v2 ABI)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
2bndy5 committed Mar 22, 2024
1 parent d33a90b commit 8ee27aa
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 104 deletions.
244 changes: 145 additions & 99 deletions src/utility/linux_kernel/gpio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,67 @@
* SOFTWARE.
*/
#ifndef ARDUINO
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/gpio.h>
#include <unistd.h> // close()
#include <fcntl.h> // open()
#include <sys/ioctl.h> // ioctl()
#include <errno.h> // errno, strerror()
#include <string.h> // std::string, strcpy()
#include <map>
#include "gpio.h"

namespace cirque_pinnacle_arduino_wrappers {

std::map<pinnacle_gpio_t, gpio_cache_fd_t> 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<rf24_gpio_pin_t, gpio_fd> 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<rf24_gpio_pin_t, gpio_fd>::iterator i = cachedPins.begin(); i != cachedPins.end(); ++i) {
if (i->second > 0) {
close(i->second);
}
}
}

// GPIO chip cache manager
GPIOChipCache gpioCache;

GPIOClass::GPIOClass()
{
Expand All @@ -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<rf24_gpio_pin_t, gpio_fd>::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<rf24_gpio_pin_t, gpio_fd>(port, request.fd));
}

void GPIOClass::close(pinnacle_gpio_t port)
{
std::map<pinnacle_gpio_t, gpio_cache_fd_t>::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<rf24_gpio_pin_t, gpio_fd>::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<pinnacle_gpio_t, gpio_cache_fd_t>::iterator i = cache.find(port);
if (i == cache.end()) {
throw GPIOException("GPIO pin not initialized.");
}
else {
fd = i->second;
std::map<rf24_gpio_pin_t, gpio_fd>::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<pinnacle_gpio_t, gpio_cache_fd_t>::iterator i = cache.find(port);
if (i == cache.end()) {
throw GPIOException("GPIO pin not initialized.");
std::map<rf24_gpio_pin_t, gpio_fd>::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;
}
}

Expand Down
41 changes: 36 additions & 5 deletions src/utility/linux_kernel/gpio.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
#ifndef ARDUINO

#include <stdexcept>
#include <map>
#include <cstdint>
#include <linux/gpio.h> // gpiochip_info

#ifdef __cplusplus
extern "C" {
Expand All @@ -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 */
Expand All @@ -42,18 +50,41 @@ 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
{

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);
Expand Down

0 comments on commit 8ee27aa

Please sign in to comment.