@@ -1483,6 +1483,15 @@ config GPIO_VIPERBOARD
endmenu
+config GPIO_AGGREGATOR
+ tristate "GPIO Aggregator"
+ select GPIOLIB_FWD
+ help
+ This enabled the GPIO Aggregator, which provides a way to aggregate
+ existing GPIOs into a new GPIO device.
+ This is useful for assigning a collection of GPIOs to a user, or
+ exported them to a virtual machine.
+
config GPIO_MOCKUP
tristate "GPIO Testing Driver"
select IRQ_SIM
@@ -26,6 +26,7 @@ obj-$(CONFIG_GPIO_74XX_MMIO) += gpio-74xx-mmio.o
obj-$(CONFIG_GPIO_ADNP) += gpio-adnp.o
obj-$(CONFIG_GPIO_ADP5520) += gpio-adp5520.o
obj-$(CONFIG_GPIO_ADP5588) += gpio-adp5588.o
+obj-$(CONFIG_GPIO_AGGREGATOR) += gpio-aggregator.o
obj-$(CONFIG_GPIO_ALTERA_A10SR) += gpio-altera-a10sr.o
obj-$(CONFIG_GPIO_ALTERA) += gpio-altera.o
obj-$(CONFIG_GPIO_AMD8111) += gpio-amd8111.o
new file mode 100644
@@ -0,0 +1,333 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// GPIO Aggregator
+//
+// Copyright (C) 2019 Glider bvba
+
+#include <linux/ctype.h>
+#include <linux/gpio/driver.h>
+#include <linux/idr.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/string.h>
+
+#include "gpiolib.h"
+#include "gpiolib-fwd.h"
+
+#define DRV_NAME "gpio-aggregator"
+
+struct gpio_aggregator {
+ struct platform_device *pdev;
+};
+
+static DEFINE_MUTEX(gpio_aggregator_lock); /* protects idr */
+static DEFINE_IDR(gpio_aggregator_idr);
+
+static int gpiochip_match_label(struct gpio_chip *chip, void *data)
+{
+ return !strcmp(chip->label, data);
+}
+
+static struct gpio_chip *gpiochip_find_by_label(const char *label)
+{
+ return gpiochip_find((void *)label, gpiochip_match_label);
+}
+
+static int gpiochip_match_id(struct gpio_chip *chip, void *data)
+{
+ unsigned int id = (uintptr_t)data;
+
+ return id == chip->base || id == chip->gpiodev->id;
+}
+
+static struct gpio_chip *gpiochip_find_by_id(const char *id)
+{
+ unsigned int x;
+
+ if (!str_has_prefix(id, "gpiochip"))
+ return NULL;
+
+ if (kstrtouint(id + strlen("gpiochip"), 10, &x))
+ return NULL;
+
+ return gpiochip_find((void *)(uintptr_t)x, gpiochip_match_id);
+}
+
+static ssize_t new_device_store(struct device_driver *driver, const char *buf,
+ size_t count)
+{
+ struct platform_device *pdev;
+ struct gpio_aggregator *aggr;
+ int res, id;
+
+ aggr = kzalloc(sizeof(*aggr), GFP_KERNEL);
+ if (!aggr)
+ return -ENOMEM;
+
+ mutex_lock(&gpio_aggregator_lock);
+ id = idr_alloc(&gpio_aggregator_idr, aggr, 0, 0, GFP_KERNEL);
+ mutex_unlock(&gpio_aggregator_lock);
+
+ if (id < 0) {
+ res = id;
+ goto free_ga;
+ }
+
+ /* kernfs guarantees string termination, so count + 1 is safe */
+ pdev = platform_device_register_data(NULL, DRV_NAME, id, buf,
+ count + 1);
+ if (IS_ERR(pdev)) {
+ res = PTR_ERR(pdev);
+ goto remove_idr;
+ }
+
+ aggr->pdev = pdev;
+ return count;
+
+remove_idr:
+ mutex_lock(&gpio_aggregator_lock);
+ idr_remove(&gpio_aggregator_idr, id);
+ mutex_unlock(&gpio_aggregator_lock);
+free_ga:
+ kfree(aggr);
+ return res;
+}
+
+static DRIVER_ATTR_WO(new_device);
+
+static ssize_t delete_device_store(struct device_driver *driver,
+ const char *buf, size_t count)
+{
+ struct gpio_aggregator *aggr;
+ unsigned int id;
+ int error;
+
+ if (!str_has_prefix(buf, DRV_NAME "."))
+ return -EINVAL;
+
+ error = kstrtouint(buf + strlen(DRV_NAME "."), 10, &id);
+ if (error)
+ return error;
+
+ mutex_lock(&gpio_aggregator_lock);
+ aggr = idr_remove(&gpio_aggregator_idr, id);
+ mutex_unlock(&gpio_aggregator_lock);
+ if (!aggr)
+ return -ENOENT;
+
+ platform_device_unregister(aggr->pdev);
+ kfree(aggr);
+ return count;
+}
+static DRIVER_ATTR_WO(delete_device);
+
+static struct attribute *gpio_aggregator_attrs[] = {
+ &driver_attr_new_device.attr,
+ &driver_attr_delete_device.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(gpio_aggregator);
+
+static char *get_arg(struct device *dev, const char **args)
+{
+ const char *start = *args, *end;
+ char *arg;
+
+ if (*start == '"') {
+ /* Quoted arg */
+ end = strchr(++start, '"');
+ if (!end)
+ return ERR_PTR(-EINVAL);
+
+ arg = devm_kasprintf(dev, GFP_KERNEL, "%.*s",
+ (int)(end++ - start), start);
+ } else {
+ /* Unquoted arg */
+ for (end = start; *end && !isspace(*end); end++) ;
+
+ if (end == start)
+ return ERR_PTR(-ENOENT);
+
+ arg = devm_kasprintf(dev, GFP_KERNEL, "%.*s",
+ (int)(end - start), start);
+ }
+ if (!arg)
+ return ERR_PTR(-ENOMEM);
+
+ while (isspace(*end))
+ end++;
+
+ *args = end;
+ return arg;
+}
+
+static int add_gpio(struct device *dev, struct gpio_desc ***descs,
+ unsigned int *n, struct gpio_desc *desc)
+{
+ struct gpio_desc **new_descs;
+
+ new_descs = devm_kmalloc_array(dev, *n + 1, sizeof(desc), GFP_KERNEL);
+ if (!new_descs)
+ return -ENOMEM;
+
+ if (*descs) {
+ memcpy(new_descs, *descs, *n * sizeof(desc));
+ devm_kfree(dev, *descs);
+ }
+
+ new_descs[(*n)++] = desc;
+ *descs = new_descs;
+ return 0;
+}
+
+static int gpio_aggregator_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ char *name, *offsets, *first, *last, *next;
+ const char *args = dev_get_platdata(dev);
+ struct gpio_desc **descs = NULL, *desc;
+ unsigned int a, b, i, n = 0;
+ struct gpiochip_fwd *fwd;
+ struct gpio_chip *chip;
+ int error;
+
+ while (isspace(*args))
+ args++;
+
+ while (*args) {
+ name = get_arg(dev, &args);
+ if (IS_ERR(name)) {
+ dev_err(dev, "Cannot get GPIO specifier: %ld\n",
+ PTR_ERR(name));
+ return PTR_ERR(name);
+ }
+
+ desc = gpio_name_to_desc(name);
+ if (desc) {
+ /* Named GPIO line */
+ error = add_gpio(dev, &descs, &n, desc);
+ if (error)
+ return error;
+
+ devm_kfree(dev, name);
+ continue;
+ }
+
+ /* GPIO chip + offsets */
+ chip = gpiochip_find_by_label(name);
+ if (!chip)
+ chip = gpiochip_find_by_id(name);
+ if (!chip) {
+ dev_err(dev, "Cannot find gpiochip %s\n", name);
+ return -EINVAL;
+ }
+
+ offsets = get_arg(dev, &args);
+ if (IS_ERR(offsets)) {
+ dev_err(dev, "Cannot get GPIO offsets: %ld\n",
+ PTR_ERR(offsets));
+ return PTR_ERR(offsets);
+ }
+
+ for (first = offsets; *first; first = next) {
+ next = strchrnul(first, ',');
+ if (*next)
+ *next++ = '\0';
+
+ last = strchr(first, '-');
+ if (last)
+ *last++ = '\0';
+
+ if (kstrtouint(first, 10, &a)) {
+ dev_err(dev, "Cannot parse gpio index %s\n",
+ first);
+ return -EINVAL;
+ }
+
+ if (!last) {
+ b = a;
+ } else if (kstrtouint(last, 10, &b)) {
+ dev_err(dev, "Cannot parse gpio index %s\n",
+ last);
+ return -EINVAL;
+ }
+
+ for (i = a; i <= b; i++) {
+ desc = gpiochip_get_desc(chip, i);
+ if (IS_ERR(desc)) {
+ dev_err(dev,
+ "Cannot get GPIO %s/%u: %ld\n",
+ name, i, PTR_ERR(desc));
+ return PTR_ERR(desc);
+ }
+
+ error = add_gpio(dev, &descs, &n, desc);
+ if (error)
+ return error;
+ }
+ }
+
+ devm_kfree(dev, offsets);
+ devm_kfree(dev, name);
+ }
+
+ if (!descs) {
+ dev_err(dev, "No GPIOs specified\n");
+ return -EINVAL;
+ }
+
+ fwd = gpiochip_fwd_create(dev_name(dev), dev, n, descs);
+ if (IS_ERR(fwd))
+ return PTR_ERR(fwd);
+
+ platform_set_drvdata(pdev, fwd);
+ return 0;
+}
+
+static int gpio_aggregator_remove(struct platform_device *pdev)
+{
+ struct gpiochip_fwd *fwd = platform_get_drvdata(pdev);
+
+ return gpiochip_fwd_destroy(fwd);
+}
+
+static struct platform_driver gpio_aggregator_driver = {
+ .probe = gpio_aggregator_probe,
+ .remove = gpio_aggregator_remove,
+ .driver = {
+ .name = DRV_NAME,
+ .groups = gpio_aggregator_groups,
+ },
+};
+
+static int __init gpio_aggregator_init(void)
+{
+ return platform_driver_register(&gpio_aggregator_driver);
+}
+module_init(gpio_aggregator_init);
+
+static int __exit gpio_aggregator_idr_remove(int id, void *p, void *data)
+{
+ struct gpio_aggregator *aggr = p;
+
+ platform_device_unregister(aggr->pdev);
+ kfree(aggr);
+ return 0;
+}
+
+static void __exit gpio_aggregator_exit(void)
+{
+ mutex_lock(&gpio_aggregator_lock);
+ idr_for_each(&gpio_aggregator_idr, gpio_aggregator_idr_remove, NULL);
+ idr_destroy(&gpio_aggregator_idr);
+ mutex_unlock(&gpio_aggregator_lock);
+
+ platform_driver_unregister(&gpio_aggregator_driver);
+}
+module_exit(gpio_aggregator_exit);
+
+MODULE_AUTHOR("Geert Uytterhoeven <geert+renesas@glider.be>");
+MODULE_DESCRIPTION("GPIO Aggregator");
+MODULE_LICENSE("GPL v2");