| // SPDX-License-Identifier: GPL-2.0-only | |
| // | |
| // GPIO Aggregator | |
| // | |
| // Copyright (C) 2019-2020 Glider bv | |
| #define DRV_NAME "gpio-aggregator" | |
| #define pr_fmt(fmt) DRV_NAME ": " fmt | |
| #include <linux/bitmap.h> | |
| #include <linux/bitops.h> | |
| #include <linux/ctype.h> | |
| #include <linux/gpio.h> | |
| #include <linux/gpio/consumer.h> | |
| #include <linux/gpio/driver.h> | |
| #include <linux/gpio/machine.h> | |
| #include <linux/idr.h> | |
| #include <linux/kernel.h> | |
| #include <linux/module.h> | |
| #include <linux/mutex.h> | |
| #include <linux/overflow.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/spinlock.h> | |
| #include <linux/string.h> | |
| /* | |
| * GPIO Aggregator sysfs interface | |
| */ | |
| struct gpio_aggregator { | |
| struct gpiod_lookup_table *lookups; | |
| struct platform_device *pdev; | |
| char args[]; | |
| }; | |
| static DEFINE_MUTEX(gpio_aggregator_lock); /* protects idr */ | |
| static DEFINE_IDR(gpio_aggregator_idr); | |
| static char *get_arg(char **args) | |
| { | |
| char *start, *end; | |
| start = skip_spaces(*args); | |
| if (!*start) | |
| return NULL; | |
| if (*start == '"') { | |
| /* Quoted arg */ | |
| end = strchr(++start, '"'); | |
| if (!end) | |
| return ERR_PTR(-EINVAL); | |
| } else { | |
| /* Unquoted arg */ | |
| for (end = start; *end && !isspace(*end); end++) ; | |
| } | |
| if (*end) | |
| *end++ = '\0'; | |
| *args = end; | |
| return start; | |
| } | |
| static bool isrange(const char *s) | |
| { | |
| size_t n; | |
| if (IS_ERR_OR_NULL(s)) | |
| return false; | |
| while (1) { | |
| n = strspn(s, "0123456789"); | |
| if (!n) | |
| return false; | |
| s += n; | |
| switch (*s++) { | |
| case '\0': | |
| return true; | |
| case '-': | |
| case ',': | |
| break; | |
| default: | |
| return false; | |
| } | |
| } | |
| } | |
| static int aggr_add_gpio(struct gpio_aggregator *aggr, const char *key, | |
| int hwnum, unsigned int *n) | |
| { | |
| struct gpiod_lookup_table *lookups; | |
| lookups = krealloc(aggr->lookups, struct_size(lookups, table, *n + 2), | |
| GFP_KERNEL); | |
| if (!lookups) | |
| return -ENOMEM; | |
| lookups->table[*n] = | |
| (struct gpiod_lookup)GPIO_LOOKUP_IDX(key, hwnum, NULL, *n, 0); | |
| (*n)++; | |
| memset(&lookups->table[*n], 0, sizeof(lookups->table[*n])); | |
| aggr->lookups = lookups; | |
| return 0; | |
| } | |
| static int aggr_parse(struct gpio_aggregator *aggr) | |
| { | |
| char *args = aggr->args; | |
| unsigned long *bitmap; | |
| unsigned int i, n = 0; | |
| char *name, *offsets; | |
| int error = 0; | |
| bitmap = bitmap_alloc(ARCH_NR_GPIOS, GFP_KERNEL); | |
| if (!bitmap) | |
| return -ENOMEM; | |
| for (name = get_arg(&args), offsets = get_arg(&args); name; | |
| offsets = get_arg(&args)) { | |
| if (IS_ERR(name)) { | |
| pr_err("Cannot get GPIO specifier: %pe\n", name); | |
| error = PTR_ERR(name); | |
| goto free_bitmap; | |
| } | |
| if (!isrange(offsets)) { | |
| /* Named GPIO line */ | |
| error = aggr_add_gpio(aggr, name, U16_MAX, &n); | |
| if (error) | |
| goto free_bitmap; | |
| name = offsets; | |
| continue; | |
| } | |
| /* GPIO chip + offset(s) */ | |
| error = bitmap_parselist(offsets, bitmap, ARCH_NR_GPIOS); | |
| if (error) { | |
| pr_err("Cannot parse %s: %d\n", offsets, error); | |
| goto free_bitmap; | |
| } | |
| for_each_set_bit(i, bitmap, ARCH_NR_GPIOS) { | |
| error = aggr_add_gpio(aggr, name, i, &n); | |
| if (error) | |
| goto free_bitmap; | |
| } | |
| name = get_arg(&args); | |
| } | |
| if (!n) { | |
| pr_err("No GPIOs specified\n"); | |
| error = -EINVAL; | |
| } | |
| free_bitmap: | |
| bitmap_free(bitmap); | |
| return error; | |
| } | |
| static ssize_t new_device_store(struct device_driver *driver, const char *buf, | |
| size_t count) | |
| { | |
| struct gpio_aggregator *aggr; | |
| struct platform_device *pdev; | |
| int res, id; | |
| /* kernfs guarantees string termination, so count + 1 is safe */ | |
| aggr = kzalloc(sizeof(*aggr) + count + 1, GFP_KERNEL); | |
| if (!aggr) | |
| return -ENOMEM; | |
| memcpy(aggr->args, buf, count + 1); | |
| aggr->lookups = kzalloc(struct_size(aggr->lookups, table, 1), | |
| GFP_KERNEL); | |
| if (!aggr->lookups) { | |
| res = -ENOMEM; | |
| goto free_ga; | |
| } | |
| 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_table; | |
| } | |
| aggr->lookups->dev_id = kasprintf(GFP_KERNEL, "%s.%d", DRV_NAME, id); | |
| if (!aggr->lookups->dev_id) { | |
| res = -ENOMEM; | |
| goto remove_idr; | |
| } | |
| res = aggr_parse(aggr); | |
| if (res) | |
| goto free_dev_id; | |
| gpiod_add_lookup_table(aggr->lookups); | |
| pdev = platform_device_register_simple(DRV_NAME, id, NULL, 0); | |
| if (IS_ERR(pdev)) { | |
| res = PTR_ERR(pdev); | |
| goto remove_table; | |
| } | |
| aggr->pdev = pdev; | |
| return count; | |
| remove_table: | |
| gpiod_remove_lookup_table(aggr->lookups); | |
| free_dev_id: | |
| kfree(aggr->lookups->dev_id); | |
| remove_idr: | |
| mutex_lock(&gpio_aggregator_lock); | |
| idr_remove(&gpio_aggregator_idr, id); | |
| mutex_unlock(&gpio_aggregator_lock); | |
| free_table: | |
| kfree(aggr->lookups); | |
| free_ga: | |
| kfree(aggr); | |
| return res; | |
| } | |
| static DRIVER_ATTR_WO(new_device); | |
| static void gpio_aggregator_free(struct gpio_aggregator *aggr) | |
| { | |
| platform_device_unregister(aggr->pdev); | |
| gpiod_remove_lookup_table(aggr->lookups); | |
| kfree(aggr->lookups->dev_id); | |
| kfree(aggr->lookups); | |
| kfree(aggr); | |
| } | |
| 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; | |
| gpio_aggregator_free(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 int __exit gpio_aggregator_idr_remove(int id, void *p, void *data) | |
| { | |
| gpio_aggregator_free(p); | |
| return 0; | |
| } | |
| static void __exit gpio_aggregator_remove_all(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); | |
| } | |
| /* | |
| * GPIO Forwarder | |
| */ | |
| struct gpiochip_fwd { | |
| struct gpio_chip chip; | |
| struct gpio_desc **descs; | |
| union { | |
| struct mutex mlock; /* protects tmp[] if can_sleep */ | |
| spinlock_t slock; /* protects tmp[] if !can_sleep */ | |
| }; | |
| unsigned long tmp[]; /* values and descs for multiple ops */ | |
| }; | |
| static int gpio_fwd_get_direction(struct gpio_chip *chip, unsigned int offset) | |
| { | |
| struct gpiochip_fwd *fwd = gpiochip_get_data(chip); | |
| return gpiod_get_direction(fwd->descs[offset]); | |
| } | |
| static int gpio_fwd_direction_input(struct gpio_chip *chip, unsigned int offset) | |
| { | |
| struct gpiochip_fwd *fwd = gpiochip_get_data(chip); | |
| return gpiod_direction_input(fwd->descs[offset]); | |
| } | |
| static int gpio_fwd_direction_output(struct gpio_chip *chip, | |
| unsigned int offset, int value) | |
| { | |
| struct gpiochip_fwd *fwd = gpiochip_get_data(chip); | |
| return gpiod_direction_output(fwd->descs[offset], value); | |
| } | |
| static int gpio_fwd_get(struct gpio_chip *chip, unsigned int offset) | |
| { | |
| struct gpiochip_fwd *fwd = gpiochip_get_data(chip); | |
| return gpiod_get_value(fwd->descs[offset]); | |
| } | |
| static int gpio_fwd_get_multiple(struct gpiochip_fwd *fwd, unsigned long *mask, | |
| unsigned long *bits) | |
| { | |
| struct gpio_desc **descs; | |
| unsigned long *values; | |
| unsigned int i, j = 0; | |
| int error; | |
| /* Both values bitmap and desc pointers are stored in tmp[] */ | |
| values = &fwd->tmp[0]; | |
| descs = (void *)&fwd->tmp[BITS_TO_LONGS(fwd->chip.ngpio)]; | |
| bitmap_clear(values, 0, fwd->chip.ngpio); | |
| for_each_set_bit(i, mask, fwd->chip.ngpio) | |
| descs[j++] = fwd->descs[i]; | |
| error = gpiod_get_array_value(j, descs, NULL, values); | |
| if (error) | |
| return error; | |
| j = 0; | |
| for_each_set_bit(i, mask, fwd->chip.ngpio) | |
| __assign_bit(i, bits, test_bit(j++, values)); | |
| return 0; | |
| } | |
| static int gpio_fwd_get_multiple_locked(struct gpio_chip *chip, | |
| unsigned long *mask, unsigned long *bits) | |
| { | |
| struct gpiochip_fwd *fwd = gpiochip_get_data(chip); | |
| unsigned long flags; | |
| int error; | |
| if (chip->can_sleep) { | |
| mutex_lock(&fwd->mlock); | |
| error = gpio_fwd_get_multiple(fwd, mask, bits); | |
| mutex_unlock(&fwd->mlock); | |
| } else { | |
| spin_lock_irqsave(&fwd->slock, flags); | |
| error = gpio_fwd_get_multiple(fwd, mask, bits); | |
| spin_unlock_irqrestore(&fwd->slock, flags); | |
| } | |
| return error; | |
| } | |
| static void gpio_fwd_set(struct gpio_chip *chip, unsigned int offset, int value) | |
| { | |
| struct gpiochip_fwd *fwd = gpiochip_get_data(chip); | |
| gpiod_set_value(fwd->descs[offset], value); | |
| } | |
| static void gpio_fwd_set_multiple(struct gpiochip_fwd *fwd, unsigned long *mask, | |
| unsigned long *bits) | |
| { | |
| struct gpio_desc **descs; | |
| unsigned long *values; | |
| unsigned int i, j = 0; | |
| /* Both values bitmap and desc pointers are stored in tmp[] */ | |
| values = &fwd->tmp[0]; | |
| descs = (void *)&fwd->tmp[BITS_TO_LONGS(fwd->chip.ngpio)]; | |
| for_each_set_bit(i, mask, fwd->chip.ngpio) { | |
| __assign_bit(j, values, test_bit(i, bits)); | |
| descs[j++] = fwd->descs[i]; | |
| } | |
| gpiod_set_array_value(j, descs, NULL, values); | |
| } | |
| static void gpio_fwd_set_multiple_locked(struct gpio_chip *chip, | |
| unsigned long *mask, unsigned long *bits) | |
| { | |
| struct gpiochip_fwd *fwd = gpiochip_get_data(chip); | |
| unsigned long flags; | |
| if (chip->can_sleep) { | |
| mutex_lock(&fwd->mlock); | |
| gpio_fwd_set_multiple(fwd, mask, bits); | |
| mutex_unlock(&fwd->mlock); | |
| } else { | |
| spin_lock_irqsave(&fwd->slock, flags); | |
| gpio_fwd_set_multiple(fwd, mask, bits); | |
| spin_unlock_irqrestore(&fwd->slock, flags); | |
| } | |
| } | |
| static int gpio_fwd_set_config(struct gpio_chip *chip, unsigned int offset, | |
| unsigned long config) | |
| { | |
| struct gpiochip_fwd *fwd = gpiochip_get_data(chip); | |
| return gpiod_set_config(fwd->descs[offset], config); | |
| } | |
| /** | |
| * gpiochip_fwd_create() - Create a new GPIO forwarder | |
| * @dev: Parent device pointer | |
| * @ngpios: Number of GPIOs in the forwarder. | |
| * @descs: Array containing the GPIO descriptors to forward to. | |
| * This array must contain @ngpios entries, and must not be deallocated | |
| * before the forwarder has been destroyed again. | |
| * | |
| * This function creates a new gpiochip, which forwards all GPIO operations to | |
| * the passed GPIO descriptors. | |
| * | |
| * Return: An opaque object pointer, or an ERR_PTR()-encoded negative error | |
| * code on failure. | |
| */ | |
| static struct gpiochip_fwd *gpiochip_fwd_create(struct device *dev, | |
| unsigned int ngpios, | |
| struct gpio_desc *descs[]) | |
| { | |
| const char *label = dev_name(dev); | |
| struct gpiochip_fwd *fwd; | |
| struct gpio_chip *chip; | |
| unsigned int i; | |
| int error; | |
| fwd = devm_kzalloc(dev, struct_size(fwd, tmp, | |
| BITS_TO_LONGS(ngpios) + ngpios), GFP_KERNEL); | |
| if (!fwd) | |
| return ERR_PTR(-ENOMEM); | |
| chip = &fwd->chip; | |
| /* | |
| * If any of the GPIO lines are sleeping, then the entire forwarder | |
| * will be sleeping. | |
| * If any of the chips support .set_config(), then the forwarder will | |
| * support setting configs. | |
| */ | |
| for (i = 0; i < ngpios; i++) { | |
| struct gpio_chip *parent = gpiod_to_chip(descs[i]); | |
| dev_dbg(dev, "%u => gpio-%d\n", i, desc_to_gpio(descs[i])); | |
| if (gpiod_cansleep(descs[i])) | |
| chip->can_sleep = true; | |
| if (parent && parent->set_config) | |
| chip->set_config = gpio_fwd_set_config; | |
| } | |
| chip->label = label; | |
| chip->parent = dev; | |
| chip->owner = THIS_MODULE; | |
| chip->get_direction = gpio_fwd_get_direction; | |
| chip->direction_input = gpio_fwd_direction_input; | |
| chip->direction_output = gpio_fwd_direction_output; | |
| chip->get = gpio_fwd_get; | |
| chip->get_multiple = gpio_fwd_get_multiple_locked; | |
| chip->set = gpio_fwd_set; | |
| chip->set_multiple = gpio_fwd_set_multiple_locked; | |
| chip->base = -1; | |
| chip->ngpio = ngpios; | |
| fwd->descs = descs; | |
| if (chip->can_sleep) | |
| mutex_init(&fwd->mlock); | |
| else | |
| spin_lock_init(&fwd->slock); | |
| error = devm_gpiochip_add_data(dev, chip, fwd); | |
| if (error) | |
| return ERR_PTR(error); | |
| return fwd; | |
| } | |
| /* | |
| * GPIO Aggregator platform device | |
| */ | |
| static int gpio_aggregator_probe(struct platform_device *pdev) | |
| { | |
| struct device *dev = &pdev->dev; | |
| struct gpio_desc **descs; | |
| struct gpiochip_fwd *fwd; | |
| int i, n; | |
| n = gpiod_count(dev, NULL); | |
| if (n < 0) | |
| return n; | |
| descs = devm_kmalloc_array(dev, n, sizeof(*descs), GFP_KERNEL); | |
| if (!descs) | |
| return -ENOMEM; | |
| for (i = 0; i < n; i++) { | |
| descs[i] = devm_gpiod_get_index(dev, NULL, i, GPIOD_ASIS); | |
| if (IS_ERR(descs[i])) | |
| return PTR_ERR(descs[i]); | |
| } | |
| fwd = gpiochip_fwd_create(dev, n, descs); | |
| if (IS_ERR(fwd)) | |
| return PTR_ERR(fwd); | |
| platform_set_drvdata(pdev, fwd); | |
| return 0; | |
| } | |
| #ifdef CONFIG_OF | |
| static const struct of_device_id gpio_aggregator_dt_ids[] = { | |
| /* | |
| * Add GPIO-operated devices controlled from userspace below, | |
| * or use "driver_override" in sysfs | |
| */ | |
| {}, | |
| }; | |
| MODULE_DEVICE_TABLE(of, gpio_aggregator_dt_ids); | |
| #endif | |
| static struct platform_driver gpio_aggregator_driver = { | |
| .probe = gpio_aggregator_probe, | |
| .driver = { | |
| .name = DRV_NAME, | |
| .groups = gpio_aggregator_groups, | |
| .of_match_table = of_match_ptr(gpio_aggregator_dt_ids), | |
| }, | |
| }; | |
| static int __init gpio_aggregator_init(void) | |
| { | |
| return platform_driver_register(&gpio_aggregator_driver); | |
| } | |
| module_init(gpio_aggregator_init); | |
| static void __exit gpio_aggregator_exit(void) | |
| { | |
| gpio_aggregator_remove_all(); | |
| 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"); |