Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 54 additions & 12 deletions src/hal/gpio.c
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
#include "gpio.h"

#ifndef GPIO_SYSFS
#define GPIO_SYSFS "/sys/class/gpio"
#endif

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 8, 0)
int fd_gpio = 0;

char gpio_count = 0;

void gpio_release(char pin) { (void)pin; }

void gpio_release_unused(const short *keep, int count) { (void)keep; (void)count; }

void gpio_deinit(void) {
if (!fd_gpio) return;
close(fd_gpio);
Expand Down Expand Up @@ -73,15 +81,21 @@ int gpio_init(void) {
}

/* The exported line's sysfs directory is "gpio<n>" on most kernels but
* "GPIO<n>" on Fullhan kernels; probe both and cache the format. */
* "GPIO<n>" on Fullhan kernels. The format is cached once a node proves it;
* a pin that isn't exported yet says nothing, so it is not cached then. */
static const char *gpio_node(char pin) {
static char fmt[24];
static const char *fmt;
static char dir[40];
if (!fmt[0]) {
char probe[40];
sprintf(probe, "/sys/class/gpio/GPIO%d", pin);
strcpy(fmt, access(probe, F_OK) == 0 ?
"/sys/class/gpio/GPIO%d" : "/sys/class/gpio/gpio%d");
if (!fmt) {
static const char *fmts[] = { GPIO_SYSFS "/GPIO%d", GPIO_SYSFS "/gpio%d" };
for (int i = 0; i < 2 && !fmt; i++) {
sprintf(dir, fmts[i], pin);
if (access(dir, F_OK) == 0) fmt = fmts[i];
}
if (!fmt) {
sprintf(dir, GPIO_SYSFS "/gpio%d", pin);
return dir;
}
}
sprintf(dir, fmt, pin);
return dir;
Expand All @@ -104,8 +118,8 @@ static inline int gpio_direction(char pin, char *mode) {

static inline int gpio_export(char pin, bool create) {
char path[40];
int fd = open(create ? "/sys/class/gpio/export" :
"/sys/class/gpio/unexport", O_WRONLY);
int fd = open(create ? GPIO_SYSFS "/export" :
GPIO_SYSFS "/unexport", O_WRONLY);
if (fd < 0)
HAL_ERROR("gpio", "Unable to (un)export a GPIO pin!\n");

Expand All @@ -121,6 +135,33 @@ static inline int gpio_export(char pin, bool create) {
return EXIT_SUCCESS;
}

/* The pins this process exported itself. gpio_write() leaves its pin exported (freeing the line can turn it back
* into an input and drop the level), so something has to let it go later; only pins listed here are ever
* unexported, never one that was already exported when we got to it, which belongs to somebody else. */
static unsigned char gpio_owned[32];
#define GPIO_BIT(p) (1 << ((unsigned char)(p) & 7))
#define GPIO_BYTE(p) gpio_owned[(unsigned char)(p) >> 3]
#define GPIO_OWNED(p) (GPIO_BYTE(p) & GPIO_BIT(p))
#define GPIO_OWN(p) __atomic_fetch_or(&GPIO_BYTE(p), GPIO_BIT(p), __ATOMIC_RELAXED)
#define GPIO_DISOWN(p) __atomic_fetch_and(&GPIO_BYTE(p), ~GPIO_BIT(p), __ATOMIC_RELAXED)

/* Let go of a pin gpio_write() exported. A pin this process did not export is left alone. */
void gpio_release(char pin) {
if (!GPIO_OWNED(pin)) return;
if (access(gpio_node(pin), F_OK) == 0) gpio_export(pin, false);
GPIO_DISOWN(pin);
}

/* Let go of every pin this process exported, except the ones listed as still in use */
void gpio_release_unused(const short *keep, int count) {
for (int pin = 0; pin < 128; pin++) {
if (!GPIO_OWNED(pin)) continue;
bool used = false;
for (int i = 0; i < count; i++) used |= keep[i] == pin;
if (!used) gpio_release((char)pin);
}
}

int gpio_read(char pin, bool *value) {
gpio_export(pin, true);
if (gpio_direction(pin, "in")) return EXIT_FAILURE;
Expand All @@ -143,12 +184,13 @@ int gpio_read(char pin, bool *value) {

if (gpio_direction(pin, "out")) return EXIT_FAILURE;
if (gpio_export(pin, false)) return EXIT_FAILURE;
GPIO_DISOWN(pin);

return EXIT_SUCCESS;
}

int gpio_write(char pin, bool value) {
gpio_export(pin, true);
if (access(gpio_node(pin), F_OK) && gpio_export(pin, true) == EXIT_SUCCESS) GPIO_OWN(pin);
if (gpio_direction(pin, "out")) return EXIT_FAILURE;

char path[48];
Expand All @@ -164,8 +206,8 @@ int gpio_write(char pin, bool value) {
}
close(fd);

if (gpio_export(pin, false)) return EXIT_FAILURE;

/* Stay exported: freeing the line can turn it back into an input
* (the pin controller does this on some kernels), dropping the level. */
return EXIT_SUCCESS;
}
#endif
2 changes: 2 additions & 0 deletions src/hal/gpio.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@

void gpio_deinit(void);
int gpio_init(void);
void gpio_release(char pin);
void gpio_release_unused(const short *keep, int count);
int gpio_read(char pin, bool *value);
int gpio_write(char pin, bool value);
1 change: 1 addition & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ int main(int argc, char *argv[]) {

if (app_config.night_mode_enable)
night_disable();
gpio_release_unused(NULL, 0);

sdk_stop();

Expand Down
12 changes: 12 additions & 0 deletions src/night.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ void night_irled(bool enable) {
irled = enable;
}

/* Every pin night mode drives while it runs: what must stay exported between requests */
int night_pins(short *pins) {
int count = 0;
if (app_config.ir_cut_pin1 != 999) pins[count++] = app_config.ir_cut_pin1;
if (app_config.ir_cut_pin2 != 999) pins[count++] = app_config.ir_cut_pin2;
if (app_config.ir_led_pin != 999) pins[count++] = app_config.ir_led_pin;
#if defined(__arm__) && !defined(__ARM_PCS_VFP) && __ARM_ARCH == 6
if (plat == HAL_PLATFORM_F1) pins[count++] = FH_LAMP_WHITE_GPIO;
#endif
return count;
}

static void night_lamp(bool enable) {
#if defined(__arm__) && !defined(__ARM_PCS_VFP) && __ARM_ARCH == 6
if (plat == HAL_PLATFORM_F1)
Expand Down
2 changes: 2 additions & 0 deletions src/night.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ void night_irled(bool enable);
void night_manual(bool enable);
void night_mode(bool enable);

int night_pins(short *pins);

void night_disable(void);
int night_enable(void);
5 changes: 5 additions & 0 deletions src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,11 @@ void respond_request(http_request_t *req) {
}

night_disable();
/* gpio_write() leaves its pins exported: let go of the ones this process exported that night mode
* no longer drives (a pin moved away, or night mode switched off) */
short keep[4];
int kept = app_config.night_mode_enable ? night_pins(keep) : 0;
gpio_release_unused(keep, kept);
if (app_config.night_mode_enable) night_enable();
}
respLen = sprintf(response,
Expand Down
164 changes: 164 additions & 0 deletions tst/gpio-sysfs/gpio_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/* The sysfs GPIO path (kernels older than 4.8) against a fake sysfs in a
* temp directory. A write to "export" makes the pin's node, one to
* "unexport" removes it; the node is "gpio<n>" or, as on Fullhan kernels,
* "GPIO<n>". */
#define _GNU_SOURCE
#define FAKE_ROOT "/tmp/gpio-sysfs-test"
#define GPIO_SYSFS FAKE_ROOT
#include "check.h"

#include <dirent.h>
#include <sys/stat.h>

#include "hal/gpio.h"
#include "hal/macros.h"

static const char *root = FAKE_ROOT;
static int upperNodes; /* the fake kernel names nodes GPIO<n> */
static int specialFd = -1; /* the open export/unexport file */
static int specialCreates;
static int exportCalls, unexportCalls;

static int fake_close(int fd) {
if (fd == specialFd) specialFd = -1; /* the number is reused by later opens */
return close(fd);
}

static int fake_open(const char *path, int flags, ...) {
int create = !strcmp(path, GPIO_SYSFS "/export");
int destroy = !strcmp(path, GPIO_SYSFS "/unexport");
if (create || destroy) {
int fd = open("/dev/null", O_WRONLY);
specialFd = fd;
specialCreates = create;
return fd;
}
return open(path, flags);
}

static void make_node(int pin) {
char dir[96], file[112];
snprintf(dir, sizeof(dir), "%s/%s%d", root, upperNodes ? "GPIO" : "gpio", pin);
mkdir(dir, 0755);
snprintf(file, sizeof(file), "%s/direction", dir);
close(open(file, O_WRONLY | O_CREAT, 0644));
snprintf(file, sizeof(file), "%s/value", dir);
close(open(file, O_WRONLY | O_CREAT, 0644));
}

static ssize_t fake_write(int fd, const void *buf, size_t len) {
if (fd != specialFd) return write(fd, buf, len);
int pin = atoi(buf);
char dir[96], file[112];
snprintf(dir, sizeof(dir), "%s/%s%d", root, upperNodes ? "GPIO" : "gpio", pin);
if (specialCreates) {
exportCalls++;
mkdir(dir, 0755);
snprintf(file, sizeof(file), "%s/direction", dir);
close(open(file, O_WRONLY | O_CREAT, 0644));
snprintf(file, sizeof(file), "%s/value", dir);
close(open(file, O_WRONLY | O_CREAT, 0644));
} else {
unexportCalls++;
snprintf(file, sizeof(file), "%s/direction", dir);
unlink(file);
snprintf(file, sizeof(file), "%s/value", dir);
unlink(file);
rmdir(dir);
}
return len;
}

/* The sysfs path is the one built for kernels older than 4.8; gpio.h pulls in
* <linux/version.h> itself, so the version is set after it */
#undef LINUX_VERSION_CODE
#define LINUX_VERSION_CODE KERNEL_VERSION(3, 10, 104)
#define open fake_open
#define write fake_write
#define close fake_close
#include "hal/gpio.c"
#undef open
#undef write
#undef close

static int exists(const char *name, int pin) {
char path[96];
snprintf(path, sizeof(path), "%s/%s%d", root, name, pin);
return access(path, F_OK) == 0;
}

static void run(int upper) {
char cmd[96];
upperNodes = upper;
exportCalls = unexportCalls = 0;
const char *node = upper ? "GPIO" : "gpio";

/* First write to a pin that isn't exported yet: this is where a probe
* that cached the wrong name used to break the Fullhan case */
CHECK(gpio_write(5, true) == EXIT_SUCCESS);
CHECK(exists(node, 5));
CHECK(exportCalls == 1 && unexportCalls == 0);

/* The pin stays exported, and later writes don't export again */
CHECK(gpio_write(5, false) == EXIT_SUCCESS);
CHECK(gpio_write(5, true) == EXIT_SUCCESS);
CHECK(exists(node, 5));
CHECK(exportCalls == 1 && unexportCalls == 0);

/* Letting go of it, and of one that was never exported */
gpio_release(5);
CHECK(!exists(node, 5));
CHECK(unexportCalls == 1);
gpio_release(5);
gpio_release(9);
CHECK(unexportCalls == 1);

/* A pin somebody else exported is never ours to unexport: writing to it works, but releasing it, by name or
* with everything-unused, leaves it alone */
make_node(9);
CHECK(gpio_write(9, true) == EXIT_SUCCESS);
CHECK(exportCalls == 1);
gpio_release(9);
gpio_release_unused(NULL, 0);
CHECK(exists(node, 9));
CHECK(unexportCalls == 1);

/* Everything this process exported goes, except what is still in use. This is also the case of a pin assigned
* and driven in one request and not kept: it was exported here, so it is let go of */
CHECK(gpio_write(5, true) == EXIT_SUCCESS);
CHECK(gpio_write(6, true) == EXIT_SUCCESS);
CHECK(gpio_write(7, true) == EXIT_SUCCESS);
short keep[] = { 6, 999 };
gpio_release_unused(keep, 2);
CHECK(!exists(node, 5) && exists(node, 6) && !exists(node, 7));
CHECK(exists(node, 9));
gpio_release_unused(NULL, 0);
CHECK(!exists(node, 6));
CHECK(exists(node, 9));
int after = unexportCalls;
gpio_release_unused(NULL, 0);
CHECK(unexportCalls == after);

/* A pin let go of and exported again later is ours again */
CHECK(gpio_write(5, true) == EXIT_SUCCESS);
gpio_release_unused(NULL, 0);
CHECK(!exists(node, 5));

snprintf(cmd, sizeof(cmd), "rm -rf '%s'/*", root);
CHECK(system(cmd) == 0);
}

int main(void) {
if (system("rm -rf " FAKE_ROOT)) {}
CHECK(mkdir(root, 0755) == 0);

/* The node-name format is cached for good once one has been seen, so
* each kernel flavour needs its own process */
int upper = getenv("GPIO_TEST_UPPER") != NULL;
run(upper);

char cmd[96];
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", root);
if (system(cmd)) {}
CHECK_DONE();
}
12 changes: 12 additions & 0 deletions tst/gpio-sysfs/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu

CC=${CC:-cc}
CFLAGS=${CFLAGS:--std=gnu99 -Wall -Wno-parentheses}

# gpio.c is included by the test itself, so it can see the fake sysfs
$CC $CFLAGS \
-I"$PROJECT_ROOT/src" -I"$PROJECT_ROOT/tst" \
"$TEST_DIR/gpio_test.c" -o gpio_test
./gpio_test
GPIO_TEST_UPPER=1 ./gpio_test