Current section

Files

Jump to
circuits_gpio c_src hal_sysfs.c
Raw

c_src/hal_sysfs.c

// SPDX-FileCopyrightText: 2018 Frank Hunleth, Mark Sebald, Matt Ludwigs
//
// SPDX-License-Identifier: Apache-2.0
#include "gpio_nif.h"
#include <fcntl.h>
#include <limits.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include "hal_sysfs.h"
/* Some time between Linux 5.10 and Linux 5.15, GPIO numbering changed on
* AM335x devices (Beaglebone, etc.). These devices have 4 banks of 32 GPIOs.
* They used to be alphabetically sorted for file names which mirrored the
* order they showed up in the I/O address map. Now they show up. Now they show
* up with the bank at address 0x44c00000 coming after all of the 0x48000000
* banks.
*
* To get the original mapping, GPIO numbers between 0 and 127 need to be
* rotated by 32. I.e., x' = (x + 32) % 128.
*
* The real fix would be to embrace cdev and stop using GPIO numbers and the
* sysfs interface, but that requires changing a lot of code, so work around
* it.
*/
static int bbb_rotate_gpio = 0;
static void check_bbb_linux_5_15_gpio_change()
{
// Check for the gpiochip ordering that has the 0x44c00000 controller
// ordered AFTER the 0x48000000.
//
// These are ordered so that the for loop fails as soon as possible on
// non-AM335x platforms. Since few devices get up to gpiochip3, the
// readlink(2) call should fail and there shouldn't even be a string
// compare.
static const char *symlink_value[] = {
"/sys/bus/gpio/devices/gpiochip3",
"../../../devices/platform/ocp/44c00000.interconnect/44c00000.interconnect:segment@200000/44e07000.target-module/44e07000.gpio/gpiochip3",
"/sys/bus/gpio/devices/gpiochip0",
"../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@0/4804c000.target-module/4804c000.gpio/gpiochip0",
"/sys/bus/gpio/devices/gpiochip1",
"../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@100000/481ac000.target-module/481ac000.gpio/gpiochip1",
"/sys/bus/gpio/devices/gpiochip2",
"../../../devices/platform/ocp/48000000.interconnect/48000000.interconnect:segment@100000/481ae000.target-module/481ae000.gpio/gpiochip2"
};
char path[PATH_MAX];
int i;
bbb_rotate_gpio = 0;
for (i = 0; i < 8; i += 2) {
ssize_t path_len = readlink(symlink_value[i], path, sizeof(path) - 1);
if (path_len < 0)
return;
path[path_len] = '\0';
if (strcmp(symlink_value[i + 1], path) != 0)
return;
}
bbb_rotate_gpio = 1;
}
static int fix_gpio_number(int pin_number)
{
if (!bbb_rotate_gpio || pin_number >= 128)
return pin_number;
return (pin_number + 32) & 0x7f;
}
size_t hal_priv_size()
{
return sizeof(struct sysfs_priv);
}
/* This is a workaround for a first-time initialization issue where the file doesn't appear
* quickly after export.
*/
static int retry_open(const char *pathname, int flags, int retries)
{
do {
int fd = open(pathname, flags);
if (fd >= 0)
return fd;
retries--;
debug("Error opening %s. Retrying %d times", pathname, retries);
usleep(1000);
} while (retries > 0);
return -1;
}
static ssize_t sysfs_write_file(const char *pathname, const char *value, int retries)
{
int fd = retry_open(pathname, O_WRONLY, retries);
if (fd < 0) {
error("Error opening %s", pathname);
return -1;
}
size_t count = strlen(value);
ssize_t written = write(fd, value, count);
close(fd);
if (written < 0 || (size_t) written != count) {
error("Error writing '%s' to %s", value, pathname);
return -1;
}
return written;
}
static ssize_t sysfs_read_file(const char *pathname, char *value, size_t len, int retries)
{
int fd = retry_open(pathname, O_RDONLY, retries);
if (fd < 0) {
error("Error opening %s", pathname);
return -1;
}
ssize_t amount_read = read(fd, value, len);
close(fd);
if (amount_read <= 0) {
error("Error writing '%s' to %s", value, pathname);
return -1;
}
return amount_read;
}
static int export_pin(int pin_number)
{
char pinstr[16];
sprintf(pinstr, "%d", pin_number);
if (sysfs_write_file("/sys/class/gpio/export", pinstr, 0) <= 0)
return - 1;
return 0;
}
static const char *edge_mode_string(enum trigger_mode mode)
{
switch (mode) {
default:
case TRIGGER_NONE:
return "none";
case TRIGGER_FALLING:
return "falling";
case TRIGGER_RISING:
return "rising";
case TRIGGER_BOTH:
return "both";
}
}
ERL_NIF_TERM hal_info(ErlNifEnv *env, void *hal_priv, ERL_NIF_TERM info)
{
enif_make_map_put(env, info, enif_make_atom(env, "name"), enif_make_atom(env, "sysfs"), &info);
if (bbb_rotate_gpio < 0)
check_bbb_linux_5_15_gpio_change();
enif_make_map_put(env, info, enif_make_atom(env, "remap_bbb_gpios"), bbb_rotate_gpio ? enif_make_atom(env, "true") : enif_make_atom(env, "false"), &info);
#ifdef TARGET_RPI
return rpi_info(env, hal_priv, info);
#else
(void) hal_priv;
return info;
#endif
}
int hal_load(void *hal_priv)
{
struct sysfs_priv *priv = hal_priv;
memset(priv, 0, sizeof(struct sysfs_priv));
if (pipe(priv->pipe_fds) < 0) {
error("pipe failed");
return 1;
}
if (enif_thread_create("gpio_poller", &priv->poller_tid, gpio_poller_thread, &priv->pipe_fds[0], NULL) != 0) {
error("enif_thread_create failed");
return 1;
}
check_bbb_linux_5_15_gpio_change();
return 0;
}
void hal_unload(void *hal_priv)
{
struct sysfs_priv *priv = hal_priv;
// Close everything related to the listening thread so that it exits
close(priv->pipe_fds[0]);
close(priv->pipe_fds[1]);
// If the listener thread hasn't exited already, it should do so soon.
enif_thread_join(priv->poller_tid, NULL);
#ifdef TARGET_RPI
rpi_unload(priv);
#endif
// TODO free everything else!
}
int hal_open_gpio(struct gpio_pin *pin,
char *error_str,
ErlNifEnv *env)
{
*error_str = '\0';
int pin_number = fix_gpio_number(pin->pin_number);
char value_path[64];
sprintf(value_path, "/sys/class/gpio/gpio%d/value", pin_number);
pin->fd = open(value_path, O_RDWR);
if (pin->fd < 0) {
if (export_pin(pin_number) < 0) {
strcpy(error_str, "export_failed");
return -1;
}
// wait up to 1000ms for the gpio symlink to be created
pin->fd = retry_open(value_path, O_RDWR, 1000);
if (pin->fd < 0) {
strcpy(error_str, "access_denied");
return -1;
}
}
if (hal_apply_direction(pin) < 0) {
strcpy(error_str, "error_setting_direction");
goto error;
}
if (hal_apply_pull_mode(pin) < 0) {
strcpy(error_str, "error_setting_pull_mode");
goto error;
}
// Only call hal_apply_interrupts if there's a trigger. While sysfs limits
// users to one "interrupt" handler, it's still nice to be able to check a
// a GPIO's state.
if (pin->config.trigger != TRIGGER_NONE && hal_apply_interrupts(pin, env) < 0) {
strcpy(error_str, "error_setting_interrupts");
goto error;
}
return 0;
error:
close(pin->fd);
pin->fd = -1;
return -1;
}
void hal_close_gpio(struct gpio_pin *pin)
{
if (pin->fd >= 0) {
// Turn off interrupts if they're on.
if (pin->config.trigger != TRIGGER_NONE) {
pin->config.trigger = TRIGGER_NONE;
update_polling_thread(pin);
}
close(pin->fd);
pin->fd = -1;
}
}
int sysfs_read_gpio(int fd)
{
char buf;
ssize_t amount_read = pread(fd, &buf, sizeof(buf), 0);
if (amount_read == sizeof(buf))
return buf == '1';
else
return -1;
}
int hal_read_gpio(struct gpio_pin *pin)
{
return sysfs_read_gpio(pin->fd);
}
int hal_write_gpio(struct gpio_pin *pin, int value, ErlNifEnv *env)
{
(void) env;
char buff = value ? '1' : '0';
return (int) pwrite(pin->fd, &buff, sizeof(buff), 0);
}
int hal_apply_interrupts(struct gpio_pin *pin, ErlNifEnv *env)
{
(void) env;
char edge_path[64];
int pin_number = fix_gpio_number(pin->pin_number);
sprintf(edge_path, "/sys/class/gpio/gpio%d/edge", pin_number);
/* Allow 1000 * 1ms = 1 second max for retries. This is a workaround
* for a first-time initialization issue where the file doesn't appear
* quickly after export */
if (sysfs_write_file(edge_path, edge_mode_string(pin->config.trigger), 1000) < 0)
return -1;
// Tell polling thread to wait for notifications
if (update_polling_thread(pin) < 0)
return -1;
return 0;
}
int hal_apply_direction(struct gpio_pin *pin)
{
char direction_path[64];
int pin_number = fix_gpio_number(pin->pin_number);
sprintf(direction_path, "/sys/class/gpio/gpio%d/direction", pin_number);
/* Allow 1000 * 1ms = 1 second max for retries. See hal_apply_interrupts too. */
char current_dir[16];
if (sysfs_read_file(direction_path, current_dir, sizeof(current_dir), 1000) < 0)
return -1;
/* Linux only reports "in" and "out". (current_dir is NOT null terminated here) */
int current_is_output = (current_dir[0] == 'o');
if (pin->config.is_output == 0) {
// Input
return sysfs_write_file(direction_path, "in", 0);
} else {
// Output
if (pin->config.initial_value < 0) {
// Output, don't set
if (current_is_output)
return 0;
else
return sysfs_write_file(direction_path, "out", 0);
} else if (pin->config.initial_value == 0) {
// Set as output and initialize low
return sysfs_write_file(direction_path, "low", 0);
} else {
// Set as output and initialize high
return sysfs_write_file(direction_path, "high", 0);
}
}
}
int hal_apply_pull_mode(struct gpio_pin *pin)
{
if (pin->config.pull == PULL_NOT_SET)
return 0;
// Setting the pull mode is platform-specific, so delegate.
int rc = -1;
#ifdef TARGET_RPI
rc = rpi_apply_pull_mode(pin);
#endif
return rc;
}