summaryrefslogtreecommitdiff
path: root/drivers/gpio/gpio-virtuser.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/gpio/gpio-virtuser.c')
-rw-r--r--drivers/gpio/gpio-virtuser.c1807
1 files changed, 1807 insertions, 0 deletions
diff --git a/drivers/gpio/gpio-virtuser.c b/drivers/gpio/gpio-virtuser.c
new file mode 100644
index 000000000000..0e0d55da4f01
--- /dev/null
+++ b/drivers/gpio/gpio-virtuser.c
@@ -0,0 +1,1807 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Configurable virtual GPIO consumer module.
+ *
+ * Copyright (C) 2023-2024 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/array_size.h>
+#include <linux/atomic.h>
+#include <linux/bitmap.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/configfs.h>
+#include <linux/debugfs.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/gpio/machine.h>
+#include <linux/idr.h>
+#include <linux/interrupt.h>
+#include <linux/irq_work.h>
+#include <linux/limits.h>
+#include <linux/list.h>
+#include <linux/lockdep.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/notifier.h>
+#include <linux/of.h>
+#include <linux/overflow.h>
+#include <linux/platform_device.h>
+#include <linux/printk.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+#include <linux/string_helpers.h>
+#include <linux/types.h>
+
+#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;
+ sprintf(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