Packages
circuits_gpio
1.2.1
2.3.0
2.2.0
2.1.3
2.1.2
retired
2.1.1
retired
2.1.0
retired
2.0.2
retired
2.0.1
retired
2.0.0
retired
2.0.0-pre.6
retired
2.0.0-pre.5
retired
2.0.0-pre.4
retired
2.0.0-pre.3
retired
2.0.0-pre.2
retired
2.0.0-pre.1
retired
2.0.0-pre.0
retired
1.2.2
1.2.1
1.2.0
1.1.0
1.0.1
1.0.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.1
0.3.0
0.2.0
0.1.0
Use GPIOs in Elixir
Current section
Files
Jump to
Current section
Files
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;
}