| // 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; |
| 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 <bartosz.golaszewski@linaro.org>"); |
| MODULE_DESCRIPTION("Virtual GPIO consumer module"); |
| MODULE_LICENSE("GPL"); |