// SPDX-License-Identifier: GPL-2.0-only /* * Configurable virtual GPIO consumer module. * * Copyright (C) 2023-2024 Bartosz Golaszewski */ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define GPIO_VIRTUSER_NAME_BUF_LEN 32 static DEFINE_IDA(gpio_virtuser_ida); static struct dentry *gpio_virtuser_dbg_root; struct gpio_virtuser_attr_data { union { struct gpio_desc *desc; struct gpio_descs *descs; }; struct dentry *dbgfs_dir; }; struct gpio_virtuser_line_array_data { struct gpio_virtuser_attr_data ad; }; struct gpio_virtuser_line_data { struct gpio_virtuser_attr_data ad; char consumer[GPIO_VIRTUSER_NAME_BUF_LEN]; struct mutex consumer_lock; unsigned int debounce; atomic_t irq; atomic_t irq_count; }; struct gpio_virtuser_dbgfs_attr_descr { const char *name; const struct file_operations *fops; }; struct gpio_virtuser_irq_work_context { struct irq_work work; struct completion work_completion; union { struct { struct gpio_desc *desc; int dir; int val; int ret; }; struct { struct gpio_descs *descs; unsigned long *values; }; }; }; static struct gpio_virtuser_irq_work_context * to_gpio_virtuser_irq_work_context(struct irq_work *work) { return container_of(work, struct gpio_virtuser_irq_work_context, work); } static void gpio_virtuser_init_irq_work_context(struct gpio_virtuser_irq_work_context *ctx) { memset(ctx, 0, sizeof(*ctx)); init_completion(&ctx->work_completion); } static void gpio_virtuser_irq_work_queue_sync(struct gpio_virtuser_irq_work_context *ctx) { irq_work_queue(&ctx->work); wait_for_completion(&ctx->work_completion); } static void gpio_virtuser_dbgfs_emit_value_array(char *buf, unsigned long *values, size_t num_values) { size_t i; for (i = 0; i < num_values; i++) buf[i] = test_bit(i, values) ? '1' : '0'; buf[i++] = '\n'; } static void gpio_virtuser_get_value_array_atomic(struct irq_work *work) { struct gpio_virtuser_irq_work_context *ctx = to_gpio_virtuser_irq_work_context(work); struct gpio_descs *descs = ctx->descs; ctx->ret = gpiod_get_array_value(descs->ndescs, descs->desc, descs->info, ctx->values); complete(&ctx->work_completion); } static int gpio_virtuser_get_array_value(struct gpio_descs *descs, unsigned long *values, bool atomic) { struct gpio_virtuser_irq_work_context ctx; if (!atomic) return gpiod_get_array_value_cansleep(descs->ndescs, descs->desc, descs->info, values); gpio_virtuser_init_irq_work_context(&ctx); ctx.work = IRQ_WORK_INIT_HARD(gpio_virtuser_get_value_array_atomic); ctx.descs = descs; ctx.values = values; gpio_virtuser_irq_work_queue_sync(&ctx); return ctx.ret; } static ssize_t gpio_virtuser_value_array_do_read(struct file *file, char __user *user_buf, size_t size, loff_t *ppos, bool atomic) { struct gpio_virtuser_line_data *data = file->private_data; struct gpio_descs *descs = data->ad.descs; size_t bufsize; int ret; unsigned long *values __free(bitmap) = bitmap_zalloc(descs->ndescs, GFP_KERNEL); if (!values) return -ENOMEM; ret = gpio_virtuser_get_array_value(descs, values, atomic); if (ret) return ret; bufsize = descs->ndescs + 2; char *buf __free(kfree) = kzalloc(bufsize, GFP_KERNEL); if (!buf) return -ENOMEM; gpio_virtuser_dbgfs_emit_value_array(buf, values, descs->ndescs); return simple_read_from_buffer(user_buf, size, ppos, buf, descs->ndescs + 1); } static int gpio_virtuser_dbgfs_parse_value_array(const char *buf, size_t len, unsigned long *values) { size_t i; for (i = 0; i < len; i++) { if (buf[i] == '0') clear_bit(i, values); else if (buf[i] == '1') set_bit(i, values); else return -EINVAL; } return 0; } static void gpio_virtuser_set_value_array_atomic(struct irq_work *work) { struct gpio_virtuser_irq_work_context *ctx = to_gpio_virtuser_irq_work_context(work); struct gpio_descs *descs = ctx->descs; ctx->ret = gpiod_set_array_value(descs->ndescs, descs->desc, descs->info, ctx->values); complete(&ctx->work_completion); } static int gpio_virtuser_set_array_value(struct gpio_descs *descs, unsigned long *values, bool atomic) { struct gpio_virtuser_irq_work_context ctx; if (!atomic) return gpiod_set_array_value_cansleep(descs->ndescs, descs->desc, descs->info, values); gpio_virtuser_init_irq_work_context(&ctx); ctx.work = IRQ_WORK_INIT_HARD(gpio_virtuser_set_value_array_atomic); ctx.descs = descs; ctx.values = values; gpio_virtuser_irq_work_queue_sync(&ctx); return ctx.ret; } static ssize_t gpio_virtuser_value_array_do_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos, bool atomic) { struct gpio_virtuser_line_data *data = file->private_data; struct gpio_descs *descs = data->ad.descs; int ret; if (count - 1 != descs->ndescs) return -EINVAL; char *buf __free(kfree) = kzalloc(count, GFP_KERNEL); if (!buf) return -ENOMEM; ret = simple_write_to_buffer(buf, count, ppos, user_buf, count); if (ret < 0) return ret; unsigned long *values __free(bitmap) = bitmap_zalloc(descs->ndescs, GFP_KERNEL); if (!values) return -ENOMEM; ret = gpio_virtuser_dbgfs_parse_value_array(buf, count - 1, values); if (ret) return ret; ret = gpio_virtuser_set_array_value(descs, values, atomic); if (ret) return ret; return count; } static ssize_t gpio_virtuser_value_array_read(struct file *file, char __user *user_buf, size_t count, loff_t *ppos) { return gpio_virtuser_value_array_do_read(file, user_buf, count, ppos, false); } static ssize_t gpio_virtuser_value_array_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) { return gpio_virtuser_value_array_do_write(file, user_buf, count, ppos, false); } static const struct file_operations gpio_virtuser_value_array_fops = { .read = gpio_virtuser_value_array_read, .write = gpio_virtuser_value_array_write, .open = simple_open, .owner = THIS_MODULE, .llseek = default_llseek, }; static ssize_t gpio_virtuser_value_array_atomic_read(struct file *file, char __user *user_buf, size_t count, loff_t *ppos) { return gpio_virtuser_value_array_do_read(file, user_buf, count, ppos, true); } static ssize_t gpio_virtuser_value_array_atomic_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) { return gpio_virtuser_value_array_do_write(file, user_buf, count, ppos, true); } static const struct file_operations gpio_virtuser_value_array_atomic_fops = { .read = gpio_virtuser_value_array_atomic_read, .write = gpio_virtuser_value_array_atomic_write, .open = simple_open, .owner = THIS_MODULE, .llseek = default_llseek, }; static void gpio_virtuser_do_get_direction_atomic(struct irq_work *work) { struct gpio_virtuser_irq_work_context *ctx = to_gpio_virtuser_irq_work_context(work); ctx->ret = gpiod_get_direction(ctx->desc); complete(&ctx->work_completion); } static int gpio_virtuser_get_direction_atomic(struct gpio_desc *desc) { struct gpio_virtuser_irq_work_context ctx; gpio_virtuser_init_irq_work_context(&ctx); ctx.work = IRQ_WORK_INIT_HARD(gpio_virtuser_do_get_direction_atomic); ctx.desc = desc; gpio_virtuser_irq_work_queue_sync(&ctx); return ctx.ret; } static ssize_t gpio_virtuser_direction_do_read(struct file *file, char __user *user_buf, size_t size, loff_t *ppos, bool atomic) { struct gpio_virtuser_line_data *data = file->private_data; struct gpio_desc *desc = data->ad.desc; char buf[32]; int dir; if (!atomic) dir = gpiod_get_direction(desc); else dir = gpio_virtuser_get_direction_atomic(desc); if (dir < 0) return dir; snprintf(buf, sizeof(buf), "%s\n", dir ? "input" : "output"); return simple_read_from_buffer(user_buf, size, ppos, buf, strlen(buf)); } static int gpio_virtuser_set_direction(struct gpio_desc *desc, int dir, int val) { if (dir) return gpiod_direction_input(desc); return gpiod_direction_output(desc, val); } static void gpio_virtuser_do_set_direction_atomic(struct irq_work *work) { struct gpio_virtuser_irq_work_context *ctx = to_gpio_virtuser_irq_work_context(work); ctx->ret = gpio_virtuser_set_direction(ctx->desc, ctx->dir, ctx->val); complete(&ctx->work_completion); } static int gpio_virtuser_set_direction_atomic(struct gpio_desc *desc, int dir, int val) { struct gpio_virtuser_irq_work_context ctx; gpio_virtuser_init_irq_work_context(&ctx); ctx.work = IRQ_WORK_INIT_HARD(gpio_virtuser_do_set_direction_atomic); ctx.desc = desc; ctx.dir = dir; ctx.val = val; gpio_virtuser_irq_work_queue_sync(&ctx); return ctx.ret; } static ssize_t gpio_virtuser_direction_do_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos, bool atomic) { struct gpio_virtuser_line_data *data = file->private_data; struct gpio_desc *desc = data->ad.desc; char buf[32], *trimmed; int ret, dir, val = 0; ret = simple_write_to_buffer(buf, sizeof(buf), ppos, user_buf, count); if (ret < 0) return ret; trimmed = strim(buf); if (strcmp(trimmed, "input") == 0) { dir = 1; } else if (strcmp(trimmed, "output-high") == 0) { dir = 0; val = 1; } else if (strcmp(trimmed, "output-low") == 0) { dir = val = 0; } else { return -EINVAL; } if (!atomic) ret = gpio_virtuser_set_direction(desc, dir, val); else ret = gpio_virtuser_set_direction_atomic(desc, dir, val); if (ret) return ret; return count; } static ssize_t gpio_virtuser_direction_read(struct file *file, char __user *user_buf, size_t size, loff_t *ppos) { return gpio_virtuser_direction_do_read(file, user_buf, size, ppos, false); } static ssize_t gpio_virtuser_direction_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) { return gpio_virtuser_direction_do_write(file, user_buf, count, ppos, false); } static const struct file_operations gpio_virtuser_direction_fops = { .read = gpio_virtuser_direction_read, .write = gpio_virtuser_direction_write, .open = simple_open, .owner = THIS_MODULE, .llseek = default_llseek, }; static ssize_t gpio_virtuser_direction_atomic_read(struct file *file, char __user *user_buf, size_t size, loff_t *ppos) { return gpio_virtuser_direction_do_read(file, user_buf, size, ppos, true); } static ssize_t gpio_virtuser_direction_atomic_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) { return gpio_virtuser_direction_do_write(file, user_buf, count, ppos, true); } static const struct file_operations gpio_virtuser_direction_atomic_fops = { .read = gpio_virtuser_direction_atomic_read, .write = gpio_virtuser_direction_atomic_write, .open = simple_open, .owner = THIS_MODULE, .llseek = default_llseek, }; static int gpio_virtuser_value_get(void *data, u64 *val) { struct gpio_virtuser_line_data *ld = data; int ret; ret = gpiod_get_value_cansleep(ld->ad.desc); if (ret < 0) return ret; *val = ret; return 0; } static int gpio_virtuser_value_set(void *data, u64 val) { struct gpio_virtuser_line_data *ld = data; if (val > 1) return -EINVAL; gpiod_set_value_cansleep(ld->ad.desc, (int)val); return 0; } DEFINE_DEBUGFS_ATTRIBUTE(gpio_virtuser_value_fops, gpio_virtuser_value_get, gpio_virtuser_value_set, "%llu\n"); static void gpio_virtuser_get_value_atomic(struct irq_work *work) { struct gpio_virtuser_irq_work_context *ctx = to_gpio_virtuser_irq_work_context(work); ctx->val = gpiod_get_value(ctx->desc); complete(&ctx->work_completion); } static int gpio_virtuser_value_atomic_get(void *data, u64 *val) { struct gpio_virtuser_line_data *ld = data; struct gpio_virtuser_irq_work_context ctx; gpio_virtuser_init_irq_work_context(&ctx); ctx.work = IRQ_WORK_INIT_HARD(gpio_virtuser_get_value_atomic); ctx.desc = ld->ad.desc; gpio_virtuser_irq_work_queue_sync(&ctx); if (ctx.val < 0) return ctx.val; *val = ctx.val; return 0; } static void gpio_virtuser_set_value_atomic(struct irq_work *work) { struct gpio_virtuser_irq_work_context *ctx = to_gpio_virtuser_irq_work_context(work); gpiod_set_value(ctx->desc, ctx->val); complete(&ctx->work_completion); } static int gpio_virtuser_value_atomic_set(void *data, u64 val) { struct gpio_virtuser_line_data *ld = data; struct gpio_virtuser_irq_work_context ctx; if (val > 1) return -EINVAL; gpio_virtuser_init_irq_work_context(&ctx); ctx.work = IRQ_WORK_INIT_HARD(gpio_virtuser_set_value_atomic); ctx.desc = ld->ad.desc; ctx.val = (int)val; gpio_virtuser_irq_work_queue_sync(&ctx); return 0; } DEFINE_DEBUGFS_ATTRIBUTE(gpio_virtuser_value_atomic_fops, gpio_virtuser_value_atomic_get, gpio_virtuser_value_atomic_set, "%llu\n"); static int gpio_virtuser_debounce_get(void *data, u64 *val) { struct gpio_virtuser_line_data *ld = data; *val = READ_ONCE(ld->debounce); return 0; } static int gpio_virtuser_debounce_set(void *data, u64 val) { struct gpio_virtuser_line_data *ld = data; int ret; if (val > UINT_MAX) return -E2BIG; ret = gpiod_set_debounce(ld->ad.desc, (unsigned int)val); if (ret) /* Don't propagate errno unknown to user-space. */ return ret == -ENOTSUPP ? -EOPNOTSUPP : ret; WRITE_ONCE(ld->debounce, (unsigned int)val); return 0; } DEFINE_DEBUGFS_ATTRIBUTE(gpio_virtuser_debounce_fops, gpio_virtuser_debounce_get, gpio_virtuser_debounce_set, "%llu\n"); static ssize_t gpio_virtuser_consumer_read(struct file *file, char __user *user_buf, size_t size, loff_t *ppos) { struct gpio_virtuser_line_data *data = file->private_data; char buf[GPIO_VIRTUSER_NAME_BUF_LEN + 1]; ssize_t ret; memset(buf, 0x0, sizeof(buf)); scoped_guard(mutex, &data->consumer_lock) ret = snprintf(buf, sizeof(buf), "%s\n", data->consumer); return simple_read_from_buffer(user_buf, size, ppos, buf, ret); } static ssize_t gpio_virtuser_consumer_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) { struct gpio_virtuser_line_data *data = file->private_data; char buf[GPIO_VIRTUSER_NAME_BUF_LEN + 2]; int ret; ret = simple_write_to_buffer(buf, GPIO_VIRTUSER_NAME_BUF_LEN, ppos, user_buf, count); if (ret < 0) return ret; buf[strlen(buf) - 1] = '\0'; ret = gpiod_set_consumer_name(data->ad.desc, buf); if (ret) return ret; scoped_guard(mutex, &data->consumer_lock) strscpy(data->consumer, buf, sizeof(data->consumer)); return count; } static const struct file_operations gpio_virtuser_consumer_fops = { .read = gpio_virtuser_consumer_read, .write = gpio_virtuser_consumer_write, .open = simple_open, .owner = THIS_MODULE, .llseek = default_llseek, }; static int gpio_virtuser_interrupts_get(void *data, u64 *val) { struct gpio_virtuser_line_data *ld = data; *val = atomic_read(&ld->irq_count); return 0; } static irqreturn_t gpio_virtuser_irq_handler(int irq, void *data) { struct gpio_virtuser_line_data *ld = data; atomic_inc(&ld->irq_count); return IRQ_HANDLED; } static int gpio_virtuser_interrupts_set(void *data, u64 val) { struct gpio_virtuser_line_data *ld = data; int irq, ret; if (val > 1) return -EINVAL; if (val) { irq = gpiod_to_irq(ld->ad.desc); if (irq < 0) return irq; ret = request_threaded_irq(irq, NULL, gpio_virtuser_irq_handler, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, ld->consumer, data); if (ret) return ret; atomic_set(&ld->irq, irq); } else { irq = atomic_xchg(&ld->irq, 0); free_irq(irq, ld); } return 0; } DEFINE_DEBUGFS_ATTRIBUTE(gpio_virtuser_interrupts_fops, gpio_virtuser_interrupts_get, gpio_virtuser_interrupts_set, "%llu\n"); static const struct gpio_virtuser_dbgfs_attr_descr gpio_virtuser_line_array_dbgfs_attrs[] = { { .name = "values", .fops = &gpio_virtuser_value_array_fops, }, { .name = "values_atomic", .fops = &gpio_virtuser_value_array_atomic_fops, }, }; static const struct gpio_virtuser_dbgfs_attr_descr gpio_virtuser_line_dbgfs_attrs[] = { { .name = "direction", .fops = &gpio_virtuser_direction_fops, }, { .name = "direction_atomic", .fops = &gpio_virtuser_direction_atomic_fops, }, { .name = "value", .fops = &gpio_virtuser_value_fops, }, { .name = "value_atomic", .fops = &gpio_virtuser_value_atomic_fops, }, { .name = "debounce", .fops = &gpio_virtuser_debounce_fops, }, { .name = "consumer", .fops = &gpio_virtuser_consumer_fops, }, { .name = "interrupts", .fops = &gpio_virtuser_interrupts_fops, }, }; static int gpio_virtuser_create_debugfs_attrs( const struct gpio_virtuser_dbgfs_attr_descr *attr, size_t num_attrs, struct dentry *parent, void *data) { struct dentry *ret; size_t i; for (i = 0; i < num_attrs; i++, attr++) { ret = debugfs_create_file(attr->name, 0644, parent, data, attr->fops); if (IS_ERR(ret)) return PTR_ERR(ret); } return 0; } static int gpio_virtuser_dbgfs_init_line_array_attrs(struct device *dev, struct gpio_descs *descs, const char *id, struct dentry *dbgfs_entry) { struct gpio_virtuser_line_array_data *data; char *name; data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; data->ad.descs = descs; name = devm_kasprintf(dev, GFP_KERNEL, "gpiod:%s", id); if (!name) return -ENOMEM; data->ad.dbgfs_dir = debugfs_create_dir(name, dbgfs_entry); if (IS_ERR(data->ad.dbgfs_dir)) return PTR_ERR(data->ad.dbgfs_dir); return gpio_virtuser_create_debugfs_attrs( gpio_virtuser_line_array_dbgfs_attrs, ARRAY_SIZE(gpio_virtuser_line_array_dbgfs_attrs), data->ad.dbgfs_dir, data); } static int gpio_virtuser_dbgfs_init_line_attrs(struct device *dev, struct gpio_desc *desc, const char *id, unsigned int index, struct dentry *dbgfs_entry) { struct gpio_virtuser_line_data *data; char *name; int ret; data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; data->ad.desc = desc; strscpy(data->consumer, id); atomic_set(&data->irq, 0); atomic_set(&data->irq_count, 0); name = devm_kasprintf(dev, GFP_KERNEL, "gpiod:%s:%u", id, index); if (!name) return -ENOMEM; ret = devm_mutex_init(dev, &data->consumer_lock); if (ret) return ret; data->ad.dbgfs_dir = debugfs_create_dir(name, dbgfs_entry); if (IS_ERR(data->ad.dbgfs_dir)) return PTR_ERR(data->ad.dbgfs_dir); return gpio_virtuser_create_debugfs_attrs( gpio_virtuser_line_dbgfs_attrs, ARRAY_SIZE(gpio_virtuser_line_dbgfs_attrs), data->ad.dbgfs_dir, data); } static void gpio_virtuser_debugfs_remove(void *data) { struct dentry *dbgfs_entry = data; debugfs_remove_recursive(dbgfs_entry); } static int gpio_virtuser_prop_is_gpio(struct property *prop) { char *dash = strrchr(prop->name, '-'); return dash && strcmp(dash, "-gpios") == 0; } /* * If this is an OF-based system, then we iterate over properties and consider * all whose names end in "-gpios". For configfs we expect an additional string * array property - "gpio-virtuser,ids" - containing the list of all GPIO IDs * to request. */ static int gpio_virtuser_count_ids(struct device *dev) { struct device_node *of_node = dev_of_node(dev); struct property *prop; int ret = 0; if (!of_node) return device_property_string_array_count(dev, "gpio-virtuser,ids"); for_each_property_of_node(of_node, prop) { if (gpio_virtuser_prop_is_gpio(prop)) ++ret; } return ret; } static int gpio_virtuser_get_ids(struct device *dev, const char **ids, int num_ids) { struct device_node *of_node = dev_of_node(dev); struct property *prop; size_t pos = 0, diff; char *dash, *tmp; if (!of_node) return device_property_read_string_array(dev, "gpio-virtuser,ids", ids, num_ids); for_each_property_of_node(of_node, prop) { if (!gpio_virtuser_prop_is_gpio(prop)) continue; dash = strrchr(prop->name, '-'); diff = dash - prop->name; tmp = devm_kmemdup(dev, prop->name, diff + 1, GFP_KERNEL); if (!tmp) return -ENOMEM; tmp[diff] = '\0'; ids[pos++] = tmp; } return 0; } static int gpio_virtuser_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct dentry *dbgfs_entry; struct gpio_descs *descs; int ret, num_ids = 0, i; const char **ids; unsigned int j; num_ids = gpio_virtuser_count_ids(dev); if (num_ids < 0) return dev_err_probe(dev, num_ids, "Failed to get the number of GPIOs to request\n"); if (num_ids == 0) return dev_err_probe(dev, -EINVAL, "No GPIO IDs specified\n"); ids = devm_kcalloc(dev, num_ids, sizeof(*ids), GFP_KERNEL); if (!ids) return -ENOMEM; ret = gpio_virtuser_get_ids(dev, ids, num_ids); if (ret < 0) return dev_err_probe(dev, ret, "Failed to get the IDs of GPIOs to request\n"); dbgfs_entry = debugfs_create_dir(dev_name(dev), gpio_virtuser_dbg_root); ret = devm_add_action_or_reset(dev, gpio_virtuser_debugfs_remove, dbgfs_entry); if (ret) return ret; for (i = 0; i < num_ids; i++) { descs = devm_gpiod_get_array(dev, ids[i], GPIOD_ASIS); if (IS_ERR(descs)) return dev_err_probe(dev, PTR_ERR(descs), "Failed to request the '%s' GPIOs\n", ids[i]); ret = gpio_virtuser_dbgfs_init_line_array_attrs(dev, descs, ids[i], dbgfs_entry); if (ret) return dev_err_probe(dev, ret, "Failed to setup the debugfs array interface for the '%s' GPIOs\n", ids[i]); for (j = 0; j < descs->ndescs; j++) { ret = gpio_virtuser_dbgfs_init_line_attrs(dev, descs->desc[j], ids[i], j, dbgfs_entry); if (ret) return dev_err_probe(dev, ret, "Failed to setup the debugfs line interface for the '%s' GPIOs\n", ids[i]); } } return 0; } static const struct of_device_id gpio_virtuser_of_match[] = { { .compatible = "gpio-virtuser" }, { } }; MODULE_DEVICE_TABLE(of, gpio_virtuser_of_match); static struct platform_driver gpio_virtuser_driver = { .driver = { .name = "gpio-virtuser", .of_match_table = gpio_virtuser_of_match, }, .probe = gpio_virtuser_probe, }; struct gpio_virtuser_device { struct config_group group; struct platform_device *pdev; int id; struct mutex lock; struct notifier_block bus_notifier; struct completion probe_completion; bool driver_bound; struct gpiod_lookup_table *lookup_table; struct list_head lookup_list; }; static int gpio_virtuser_bus_notifier_call(struct notifier_block *nb, unsigned long action, void *data) { struct gpio_virtuser_device *vdev; struct device *dev = data; char devname[32]; vdev = container_of(nb, struct gpio_virtuser_device, bus_notifier); snprintf(devname, sizeof(devname), "gpio-virtuser.%d", vdev->id); if (!device_match_name(dev, devname)) return NOTIFY_DONE; switch (action) { case BUS_NOTIFY_BOUND_DRIVER: vdev->driver_bound = true; break; case BUS_NOTIFY_DRIVER_NOT_BOUND: vdev->driver_bound = false; break; default: return NOTIFY_DONE; } complete(&vdev->probe_completion); return NOTIFY_OK; } static struct gpio_virtuser_device * to_gpio_virtuser_device(struct config_item *item) { struct config_group *group = to_config_group(item); return container_of(group, struct gpio_virtuser_device, group); } static bool gpio_virtuser_device_is_live(struct gpio_virtuser_device *dev) { lockdep_assert_held(&dev->lock); return !!dev->pdev; } struct gpio_virtuser_lookup { struct config_group group; struct gpio_virtuser_device *parent; struct list_head siblings; char *con_id; struct list_head entry_list; }; static struct gpio_virtuser_lookup * to_gpio_virtuser_lookup(struct config_item *item) { struct config_group *group = to_config_group(item); return container_of(group, struct gpio_virtuser_lookup, group); } struct gpio_virtuser_lookup_entry { struct config_group group; struct gpio_virtuser_lookup *parent; struct list_head siblings; char *key; /* Can be negative to indicate lookup by name. */ int offset; enum gpio_lookup_flags flags; }; static struct gpio_virtuser_lookup_entry * to_gpio_virtuser_lookup_entry(struct config_item *item) { struct config_group *group = to_config_group(item); return container_of(group, struct gpio_virtuser_lookup_entry, group); } static ssize_t gpio_virtuser_lookup_entry_config_key_show(struct config_item *item, char *page) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; guard(mutex)(&dev->lock); return sprintf(page, "%s\n", entry->key ?: ""); } static ssize_t gpio_virtuser_lookup_entry_config_key_store(struct config_item *item, const char *page, size_t count) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; char *key __free(kfree) = kstrndup(skip_spaces(page), count, GFP_KERNEL); if (!key) return -ENOMEM; strim(key); guard(mutex)(&dev->lock); if (gpio_virtuser_device_is_live(dev)) return -EBUSY; kfree(entry->key); entry->key = no_free_ptr(key); return count; } CONFIGFS_ATTR(gpio_virtuser_lookup_entry_config_, key); static ssize_t gpio_virtuser_lookup_entry_config_offset_show(struct config_item *item, char *page) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; unsigned int offset; scoped_guard(mutex, &dev->lock) offset = entry->offset; return sprintf(page, "%d\n", offset); } static ssize_t gpio_virtuser_lookup_entry_config_offset_store(struct config_item *item, const char *page, size_t count) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; int offset, ret; ret = kstrtoint(page, 0, &offset); if (ret) return ret; /* * Negative number here means: 'key' represents a line name to lookup. * Non-negative means: 'key' represents the label of the chip with * the 'offset' value representing the line within that chip. * * GPIOLIB uses the U16_MAX value to indicate lookup by line name so * the greatest offset we can accept is (U16_MAX - 1). */ if (offset > (U16_MAX - 1)) return -EINVAL; guard(mutex)(&dev->lock); if (gpio_virtuser_device_is_live(dev)) return -EBUSY; entry->offset = offset; return count; } CONFIGFS_ATTR(gpio_virtuser_lookup_entry_config_, offset); static enum gpio_lookup_flags gpio_virtuser_lookup_get_flags(struct config_item *item) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; guard(mutex)(&dev->lock); return entry->flags; } static ssize_t gpio_virtuser_lookup_entry_config_drive_show(struct config_item *item, char *page) { enum gpio_lookup_flags flags = gpio_virtuser_lookup_get_flags(item); const char *repr; if (flags & GPIO_OPEN_DRAIN) repr = "open-drain"; else if (flags & GPIO_OPEN_SOURCE) repr = "open-source"; else repr = "push-pull"; return sprintf(page, "%s\n", repr); } static ssize_t gpio_virtuser_lookup_entry_config_drive_store(struct config_item *item, const char *page, size_t count) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; guard(mutex)(&dev->lock); if (gpio_virtuser_device_is_live(dev)) return -EBUSY; if (sysfs_streq(page, "push-pull")) { entry->flags &= ~(GPIO_OPEN_DRAIN | GPIO_OPEN_SOURCE); } else if (sysfs_streq(page, "open-drain")) { entry->flags &= ~GPIO_OPEN_SOURCE; entry->flags |= GPIO_OPEN_DRAIN; } else if (sysfs_streq(page, "open-source")) { entry->flags &= ~GPIO_OPEN_DRAIN; entry->flags |= GPIO_OPEN_SOURCE; } else { count = -EINVAL; } return count; } CONFIGFS_ATTR(gpio_virtuser_lookup_entry_config_, drive); static ssize_t gpio_virtuser_lookup_entry_config_pull_show(struct config_item *item, char *page) { enum gpio_lookup_flags flags = gpio_virtuser_lookup_get_flags(item); const char *repr; if (flags & GPIO_PULL_UP) repr = "pull-up"; else if (flags & GPIO_PULL_DOWN) repr = "pull-down"; else if (flags & GPIO_PULL_DISABLE) repr = "pull-disabled"; else repr = "as-is"; return sprintf(page, "%s\n", repr); } static ssize_t gpio_virtuser_lookup_entry_config_pull_store(struct config_item *item, const char *page, size_t count) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; guard(mutex)(&dev->lock); if (gpio_virtuser_device_is_live(dev)) return -EBUSY; if (sysfs_streq(page, "pull-up")) { entry->flags &= ~(GPIO_PULL_DOWN | GPIO_PULL_DISABLE); entry->flags |= GPIO_PULL_UP; } else if (sysfs_streq(page, "pull-down")) { entry->flags &= ~(GPIO_PULL_UP | GPIO_PULL_DISABLE); entry->flags |= GPIO_PULL_DOWN; } else if (sysfs_streq(page, "pull-disabled")) { entry->flags &= ~(GPIO_PULL_UP | GPIO_PULL_DOWN); entry->flags |= GPIO_PULL_DISABLE; } else if (sysfs_streq(page, "as-is")) { entry->flags &= ~(GPIO_PULL_UP | GPIO_PULL_DOWN | GPIO_PULL_DISABLE); } else { count = -EINVAL; } return count; } CONFIGFS_ATTR(gpio_virtuser_lookup_entry_config_, pull); static ssize_t gpio_virtuser_lookup_entry_config_active_low_show(struct config_item *item, char *page) { enum gpio_lookup_flags flags = gpio_virtuser_lookup_get_flags(item); return sprintf(page, "%c\n", flags & GPIO_ACTIVE_LOW ? '1' : '0'); } static ssize_t gpio_virtuser_lookup_entry_config_active_low_store(struct config_item *item, const char *page, size_t count) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; bool active_low; int ret; ret = kstrtobool(page, &active_low); if (ret) return ret; guard(mutex)(&dev->lock); if (gpio_virtuser_device_is_live(dev)) return -EBUSY; if (active_low) entry->flags |= GPIO_ACTIVE_LOW; else entry->flags &= ~GPIO_ACTIVE_LOW; return count; } CONFIGFS_ATTR(gpio_virtuser_lookup_entry_config_, active_low); static ssize_t gpio_virtuser_lookup_entry_config_transitory_show(struct config_item *item, char *page) { enum gpio_lookup_flags flags = gpio_virtuser_lookup_get_flags(item); return sprintf(page, "%c\n", flags & GPIO_TRANSITORY ? '1' : '0'); } static ssize_t gpio_virtuser_lookup_entry_config_transitory_store(struct config_item *item, const char *page, size_t count) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; bool transitory; int ret; ret = kstrtobool(page, &transitory); if (ret) return ret; guard(mutex)(&dev->lock); if (gpio_virtuser_device_is_live(dev)) return -EBUSY; if (transitory) entry->flags |= GPIO_TRANSITORY; else entry->flags &= ~GPIO_TRANSITORY; return count; } CONFIGFS_ATTR(gpio_virtuser_lookup_entry_config_, transitory); static struct configfs_attribute *gpio_virtuser_lookup_entry_config_attrs[] = { &gpio_virtuser_lookup_entry_config_attr_key, &gpio_virtuser_lookup_entry_config_attr_offset, &gpio_virtuser_lookup_entry_config_attr_drive, &gpio_virtuser_lookup_entry_config_attr_pull, &gpio_virtuser_lookup_entry_config_attr_active_low, &gpio_virtuser_lookup_entry_config_attr_transitory, NULL }; static ssize_t gpio_virtuser_device_config_dev_name_show(struct config_item *item, char *page) { struct gpio_virtuser_device *dev = to_gpio_virtuser_device(item); struct platform_device *pdev; guard(mutex)(&dev->lock); pdev = dev->pdev; if (pdev) return sprintf(page, "%s\n", dev_name(&pdev->dev)); return sprintf(page, "gpio-sim.%d\n", dev->id); } CONFIGFS_ATTR_RO(gpio_virtuser_device_config_, dev_name); static ssize_t gpio_virtuser_device_config_live_show(struct config_item *item, char *page) { struct gpio_virtuser_device *dev = to_gpio_virtuser_device(item); bool live; scoped_guard(mutex, &dev->lock) live = gpio_virtuser_device_is_live(dev); return sprintf(page, "%c\n", live ? '1' : '0'); } static size_t gpio_virtuser_get_lookup_count(struct gpio_virtuser_device *dev) { struct gpio_virtuser_lookup *lookup; size_t count = 0; lockdep_assert_held(&dev->lock); list_for_each_entry(lookup, &dev->lookup_list, siblings) count += list_count_nodes(&lookup->entry_list); return count; } static int gpio_virtuser_make_lookup_table(struct gpio_virtuser_device *dev) { size_t num_entries = gpio_virtuser_get_lookup_count(dev); struct gpio_virtuser_lookup_entry *entry; struct gpio_virtuser_lookup *lookup; unsigned int i = 0; lockdep_assert_held(&dev->lock); struct gpiod_lookup_table *table __free(kfree) = kzalloc(struct_size(table, table, num_entries + 1), GFP_KERNEL); if (!table) return -ENOMEM; table->dev_id = kasprintf(GFP_KERNEL, "gpio-virtuser.%d", dev->id); if (!table->dev_id) return -ENOMEM; list_for_each_entry(lookup, &dev->lookup_list, siblings) { list_for_each_entry(entry, &lookup->entry_list, siblings) { table->table[i] = GPIO_LOOKUP_IDX(entry->key, entry->offset < 0 ? U16_MAX : entry->offset, lookup->con_id, i, entry->flags); i++; } } gpiod_add_lookup_table(table); dev->lookup_table = no_free_ptr(table); return 0; } static struct fwnode_handle * gpio_virtuser_make_device_swnode(struct gpio_virtuser_device *dev) { struct property_entry properties[2]; struct gpio_virtuser_lookup *lookup; unsigned int i = 0; size_t num_ids; memset(properties, 0, sizeof(properties)); num_ids = list_count_nodes(&dev->lookup_list); char **ids __free(kfree) = kcalloc(num_ids + 1, sizeof(*ids), GFP_KERNEL); if (!ids) return ERR_PTR(-ENOMEM); list_for_each_entry(lookup, &dev->lookup_list, siblings) ids[i++] = lookup->con_id; properties[0] = PROPERTY_ENTRY_STRING_ARRAY_LEN("gpio-virtuser,ids", ids, num_ids); return fwnode_create_software_node(properties, NULL); } static int gpio_virtuser_device_activate(struct gpio_virtuser_device *dev) { struct platform_device_info pdevinfo; struct fwnode_handle *swnode; struct platform_device *pdev; int ret; lockdep_assert_held(&dev->lock); if (list_empty(&dev->lookup_list)) return -ENODATA; swnode = gpio_virtuser_make_device_swnode(dev); if (IS_ERR(swnode)) return PTR_ERR(swnode); memset(&pdevinfo, 0, sizeof(pdevinfo)); pdevinfo.name = "gpio-virtuser"; pdevinfo.id = dev->id; pdevinfo.fwnode = swnode; ret = gpio_virtuser_make_lookup_table(dev); if (ret) { fwnode_remove_software_node(swnode); return ret; } reinit_completion(&dev->probe_completion); dev->driver_bound = false; bus_register_notifier(&platform_bus_type, &dev->bus_notifier); pdev = platform_device_register_full(&pdevinfo); if (IS_ERR(pdev)) { bus_unregister_notifier(&platform_bus_type, &dev->bus_notifier); fwnode_remove_software_node(swnode); return PTR_ERR(pdev); } wait_for_completion(&dev->probe_completion); bus_unregister_notifier(&platform_bus_type, &dev->bus_notifier); if (!dev->driver_bound) { platform_device_unregister(pdev); fwnode_remove_software_node(swnode); return -ENXIO; } dev->pdev = pdev; return 0; } static void gpio_virtuser_device_deactivate(struct gpio_virtuser_device *dev) { struct fwnode_handle *swnode; lockdep_assert_held(&dev->lock); swnode = dev_fwnode(&dev->pdev->dev); platform_device_unregister(dev->pdev); fwnode_remove_software_node(swnode); dev->pdev = NULL; gpiod_remove_lookup_table(dev->lookup_table); kfree(dev->lookup_table); } static ssize_t gpio_virtuser_device_config_live_store(struct config_item *item, const char *page, size_t count) { struct gpio_virtuser_device *dev = to_gpio_virtuser_device(item); int ret = 0; bool live; ret = kstrtobool(page, &live); if (ret) return ret; guard(mutex)(&dev->lock); if (live == gpio_virtuser_device_is_live(dev)) return -EPERM; if (live) ret = gpio_virtuser_device_activate(dev); else gpio_virtuser_device_deactivate(dev); return ret ?: count; } CONFIGFS_ATTR(gpio_virtuser_device_config_, live); static struct configfs_attribute *gpio_virtuser_device_config_attrs[] = { &gpio_virtuser_device_config_attr_dev_name, &gpio_virtuser_device_config_attr_live, NULL }; static void gpio_virtuser_lookup_entry_config_group_release(struct config_item *item) { struct gpio_virtuser_lookup_entry *entry = to_gpio_virtuser_lookup_entry(item); struct gpio_virtuser_device *dev = entry->parent->parent; guard(mutex)(&dev->lock); list_del(&entry->siblings); kfree(entry->key); kfree(entry); } static struct configfs_item_operations gpio_virtuser_lookup_entry_config_item_ops = { .release = gpio_virtuser_lookup_entry_config_group_release, }; static const struct config_item_type gpio_virtuser_lookup_entry_config_group_type = { .ct_item_ops = &gpio_virtuser_lookup_entry_config_item_ops, .ct_attrs = gpio_virtuser_lookup_entry_config_attrs, .ct_owner = THIS_MODULE, }; static struct config_group * gpio_virtuser_make_lookup_entry_group(struct config_group *group, const char *name) { struct gpio_virtuser_lookup *lookup = to_gpio_virtuser_lookup(&group->cg_item); struct gpio_virtuser_device *dev = lookup->parent; guard(mutex)(&dev->lock); if (gpio_virtuser_device_is_live(dev)) return ERR_PTR(-EBUSY); struct gpio_virtuser_lookup_entry *entry __free(kfree) = kzalloc(sizeof(*entry), GFP_KERNEL); if (!entry) return ERR_PTR(-ENOMEM); config_group_init_type_name(&entry->group, name, &gpio_virtuser_lookup_entry_config_group_type); entry->flags = GPIO_LOOKUP_FLAGS_DEFAULT; entry->parent = lookup; list_add_tail(&entry->siblings, &lookup->entry_list); return &no_free_ptr(entry)->group; } static void gpio_virtuser_lookup_config_group_release(struct config_item *item) { struct gpio_virtuser_lookup *lookup = to_gpio_virtuser_lookup(item); struct gpio_virtuser_device *dev = lookup->parent; guard(mutex)(&dev->lock); list_del(&lookup->siblings); kfree(lookup->con_id); kfree(lookup); } static struct configfs_item_operations gpio_virtuser_lookup_config_item_ops = { .release = gpio_virtuser_lookup_config_group_release, }; static struct configfs_group_operations gpio_virtuser_lookup_config_group_ops = { .make_group = gpio_virtuser_make_lookup_entry_group, }; static const struct config_item_type gpio_virtuser_lookup_config_group_type = { .ct_group_ops = &gpio_virtuser_lookup_config_group_ops, .ct_item_ops = &gpio_virtuser_lookup_config_item_ops, .ct_owner = THIS_MODULE, }; static struct config_group * gpio_virtuser_make_lookup_group(struct config_group *group, const char *name) { struct gpio_virtuser_device *dev = to_gpio_virtuser_device(&group->cg_item); if (strlen(name) > (GPIO_VIRTUSER_NAME_BUF_LEN - 1)) return ERR_PTR(-E2BIG); guard(mutex)(&dev->lock); if (gpio_virtuser_device_is_live(dev)) return ERR_PTR(-EBUSY); struct gpio_virtuser_lookup *lookup __free(kfree) = kzalloc(sizeof(*lookup), GFP_KERNEL); if (!lookup) return ERR_PTR(-ENOMEM); lookup->con_id = kstrdup(name, GFP_KERNEL); if (!lookup->con_id) return ERR_PTR(-ENOMEM); config_group_init_type_name(&lookup->group, name, &gpio_virtuser_lookup_config_group_type); INIT_LIST_HEAD(&lookup->entry_list); lookup->parent = dev; list_add_tail(&lookup->siblings, &dev->lookup_list); return &no_free_ptr(lookup)->group; } static void gpio_virtuser_device_config_group_release(struct config_item *item) { struct gpio_virtuser_device *dev = to_gpio_virtuser_device(item); guard(mutex)(&dev->lock); if (gpio_virtuser_device_is_live(dev)) gpio_virtuser_device_deactivate(dev); mutex_destroy(&dev->lock); ida_free(&gpio_virtuser_ida, dev->id); kfree(dev); } static struct configfs_item_operations gpio_virtuser_device_config_item_ops = { .release = gpio_virtuser_device_config_group_release, }; static struct configfs_group_operations gpio_virtuser_device_config_group_ops = { .make_group = gpio_virtuser_make_lookup_group, }; static const struct config_item_type gpio_virtuser_device_config_group_type = { .ct_group_ops = &gpio_virtuser_device_config_group_ops, .ct_item_ops = &gpio_virtuser_device_config_item_ops, .ct_attrs = gpio_virtuser_device_config_attrs, .ct_owner = THIS_MODULE, }; static struct config_group * gpio_virtuser_config_make_device_group(struct config_group *group, const char *name) { struct gpio_virtuser_device *dev __free(kfree) = kzalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return ERR_PTR(-ENOMEM); dev->id = ida_alloc(&gpio_virtuser_ida, GFP_KERNEL); if (dev->id < 0) return ERR_PTR(dev->id); config_group_init_type_name(&dev->group, name, &gpio_virtuser_device_config_group_type); mutex_init(&dev->lock); INIT_LIST_HEAD(&dev->lookup_list); dev->bus_notifier.notifier_call = gpio_virtuser_bus_notifier_call; init_completion(&dev->probe_completion); return &no_free_ptr(dev)->group; } static struct configfs_group_operations gpio_virtuser_config_group_ops = { .make_group = gpio_virtuser_config_make_device_group, }; static const struct config_item_type gpio_virtuser_config_type = { .ct_group_ops = &gpio_virtuser_config_group_ops, .ct_owner = THIS_MODULE, }; static struct configfs_subsystem gpio_virtuser_config_subsys = { .su_group = { .cg_item = { .ci_namebuf = "gpio-virtuser", .ci_type = &gpio_virtuser_config_type, }, }, }; static int __init gpio_virtuser_init(void) { int ret; ret = platform_driver_register(&gpio_virtuser_driver); if (ret) { pr_err("Failed to register the platform driver: %d\n", ret); return ret; } config_group_init(&gpio_virtuser_config_subsys.su_group); mutex_init(&gpio_virtuser_config_subsys.su_mutex); ret = configfs_register_subsystem(&gpio_virtuser_config_subsys); if (ret) { pr_err("Failed to register the '%s' configfs subsystem: %d\n", gpio_virtuser_config_subsys.su_group.cg_item.ci_namebuf, ret); goto err_plat_drv_unreg; } gpio_virtuser_dbg_root = debugfs_create_dir("gpio-virtuser", NULL); if (IS_ERR(gpio_virtuser_dbg_root)) { ret = PTR_ERR(gpio_virtuser_dbg_root); pr_err("Failed to create the debugfs tree: %d\n", ret); goto err_configfs_unreg; } return 0; err_configfs_unreg: configfs_unregister_subsystem(&gpio_virtuser_config_subsys); err_plat_drv_unreg: mutex_destroy(&gpio_virtuser_config_subsys.su_mutex); platform_driver_unregister(&gpio_virtuser_driver); return ret; } module_init(gpio_virtuser_init); static void __exit gpio_virtuser_exit(void) { configfs_unregister_subsystem(&gpio_virtuser_config_subsys); mutex_destroy(&gpio_virtuser_config_subsys.su_mutex); platform_driver_unregister(&gpio_virtuser_driver); debugfs_remove_recursive(gpio_virtuser_dbg_root); } module_exit(gpio_virtuser_exit); MODULE_AUTHOR("Bartosz Golaszewski "); MODULE_DESCRIPTION("Virtual GPIO consumer module"); MODULE_LICENSE("GPL");