diff --git a/src/hal/gpio.c b/src/hal/gpio.c index 1f93c704..0e75ad15 100644 --- a/src/hal/gpio.c +++ b/src/hal/gpio.c @@ -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); @@ -73,15 +81,21 @@ int gpio_init(void) { } /* The exported line's sysfs directory is "gpio" on most kernels but - * "GPIO" on Fullhan kernels; probe both and cache the format. */ + * "GPIO" 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; @@ -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"); @@ -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; @@ -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]; @@ -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 \ No newline at end of file diff --git a/src/hal/gpio.h b/src/hal/gpio.h index dad1929c..5159163d 100644 --- a/src/hal/gpio.h +++ b/src/hal/gpio.h @@ -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); diff --git a/src/main.c b/src/main.c index 3332630a..5132f516 100644 --- a/src/main.c +++ b/src/main.c @@ -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(); diff --git a/src/night.c b/src/night.c index 86e59626..7bacedaf 100644 --- a/src/night.c +++ b/src/night.c @@ -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) diff --git a/src/night.h b/src/night.h index ace2fd6d..567ad0f9 100644 --- a/src/night.h +++ b/src/night.h @@ -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); diff --git a/src/server.c b/src/server.c index c50af083..f966d3bc 100644 --- a/src/server.c +++ b/src/server.c @@ -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, diff --git a/tst/gpio-sysfs/gpio_test.c b/tst/gpio-sysfs/gpio_test.c new file mode 100644 index 00000000..81d01465 --- /dev/null +++ b/tst/gpio-sysfs/gpio_test.c @@ -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" or, as on Fullhan kernels, + * "GPIO". */ +#define _GNU_SOURCE +#define FAKE_ROOT "/tmp/gpio-sysfs-test" +#define GPIO_SYSFS FAKE_ROOT +#include "check.h" + +#include +#include + +#include "hal/gpio.h" +#include "hal/macros.h" + +static const char *root = FAKE_ROOT; +static int upperNodes; /* the fake kernel names nodes GPIO */ +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 + * 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(); +} diff --git a/tst/gpio-sysfs/run.sh b/tst/gpio-sysfs/run.sh new file mode 100755 index 00000000..468d7556 --- /dev/null +++ b/tst/gpio-sysfs/run.sh @@ -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