On Thu, 2008-06-19 at 23:52 -0700, Joel Becker wrote: > On Fri, Jun 20, 2008 at 04:19:10PM +1000, Ben Nizette wrote: > > Looks great, thx :-). I'll convert some of my stuff over to this > > interface and see how it flies in the real world. > > Excellent. What is your stuff, btw? Got code? Love to see it. I've done a few things recently. Haavard Skinnemoen has got an out-of-tree, avr32-specific gpio interface [1] which I first converted to the generic gpio framework then completely re-wrote. Both use configfs but before either version could reach completion they were obsoleted by David Brownell's gpio sysfs interface. A version of my gpio-dev interface is attached. Bear in mind it was never completed, it's full of known bugs but hey, might be useful for you anyway :-) At the moment I'm experimenting with configfs for pinmux control on AVR32 SoCs. There's really nothing to see there yet, it's all just dicking about atm. > > > ISTR when I did this I kept the old semantics and used ERR_PTR and > > friends if things went pear-shaped. Given the small number of in-tree > > users needing to be moved over, a more intrusive change for the sake of > > a cleaner API like you've done is probably a good thing anyway :-) > > Yeah, I like this better than ERR_PTR. > If you like the macros, I'll try to make the next merge window > as well. So let me know. np, will do :-) --Ben. > > joel > [1] http://git.kernel.org/?p=linux/kernel/git/hskinnemoen/avr32-2.6.git;a=blob;f=arch/avr32/mach-at32ap/gpio-dev.c;h=8cf6d1182c31db88fc069a046112a4eaac589308;hb=atmel-2.6.25
/* * GPIO /dev and configfs interface * * Copyright (C) 2008 Nias Digital P/L * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. */ #include <linux/kernel.h> #include <linux/init.h> #include <linux/module.h> #include <linux/slab.h> #include <linux/err.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/fs.h> #include <linux/spinlock.h> #include <linux/configfs.h> #include <linux/types.h> #include <linux/platform_device.h> #include <linux/poll.h> #include <linux/interrupt.h> #include <linux/irq.h> #include <linux/kfifo.h> #include <asm/uaccess.h> #include <asm/bug.h> #include <asm/gpio.h> #include "gpio-dev.h" #define MAX_NR_DEVICES 8 #define IRQ_BUFFER_SIZE 256 #define BUF_PUT_INT(k, b) \ do { \ char *p = (char *)&b; \ kfifo_put(k, p, sizeof(int)); \ } while (0) #define BUF_GET_INT(k, b) \ do { \ char *p = (char *)&b; \ kfifo_get(k, p, sizeof(int)); \ } while (0) static struct class *dev_class; static dev_t base_devt; struct gpiodev { spinlock_t lock; int id; char *bankname; struct gpiodev_pin *pins; int nr_pins; int io_enabled; int irq_enabled; struct kfifo *irq_buf; spinlock_t irq_lock; struct device *io_dev; struct device *irq_dev; /* We can unify these somewhat by doing some fop switching * magic, though I can't convince myself it's worth it*/ struct cdev io_char_dev; struct cdev irq_char_dev; wait_queue_head_t irq_wq; struct fasync_struct *async_queue; struct config_group group; }; struct gpiodev *devices[MAX_NR_DEVICES]; spinlock_t devices_lock; static irqreturn_t irq_interrupt(int irq, void *dev_id) { int i, found = 0; int gpio = irq_to_gpio(irq); struct gpiodev *bank = dev_id; for (i = 0; i < bank->nr_pins; i++) { if (bank->pins[i].pin == gpio) { found = 1; break; } } if (!found) return IRQ_NONE; BUF_PUT_INT(bank->irq_buf, i); wake_up_interruptible(&bank->irq_wq); if (bank->async_queue) kill_fasync(&bank->async_queue, SIGIO, POLL_IN); return IRQ_HANDLED; } static int io_open(struct inode *inode, struct file *file) { struct gpiodev *bank = container_of(inode->i_cdev, struct gpiodev, io_char_dev); spin_lock(&bank->lock); config_item_get(&bank->group.cg_item); file->private_data = bank; spin_unlock(&bank->lock); return 0; } static int io_release(struct inode *inode, struct file *file) { struct gpiodev *bank = file->private_data; spin_lock(&bank->lock); config_item_put(&bank->group.cg_item); spin_unlock(&bank->lock); return 0; } static ssize_t io_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { int i, ret = 0; char *val; struct gpiodev *bank = file->private_data; count = min(count, (size_t)(bank->nr_pins - *offset)); val = kmalloc(sizeof(char) * count, GFP_KERNEL); if (!val) return -ENOMEM; spin_lock(&bank->lock); for (i = *offset; i < *offset + count; i++) { int pin = bank->pins[i].pin; int value; if (file->f_flags & O_NONBLOCK) value = gpio_get_value(pin); else value = gpio_get_value_cansleep(pin); val[i] = (!!value) + '0'; } *offset += count; spin_unlock(&bank->lock); if (copy_to_user(buf, val, count)) ret = -EFAULT; kfree(val); return ret ? ret : count; } static ssize_t io_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) { int i, ret = 0; char *val, *p; struct gpiodev *bank = file->private_data; count = min(count, (size_t)(bank->nr_pins - *offset)); val = kmalloc(sizeof(char) * count, GFP_KERNEL); if (!val) return -ENOMEM; if (copy_from_user(val, buf, count)) { ret = -EFAULT; goto out_cpy; } p = val; spin_lock(&bank->lock); for (i = *offset; i < *offset + count; i++) { int pin = bank->pins[i].pin; int value = val[i] - '0'; if (value != 0 || value != 1) break; bank->pins[i].outval = value; if (file->f_flags & O_NONBLOCK) gpio_set_value(pin, value); else gpio_set_value_cansleep(pin, value); } *offset = i; spin_unlock(&bank->lock); out_cpy: kfree(val); return ret ? ret : count; } static loff_t io_llseek(struct file *file, loff_t off, int whence) { struct gpiodev *bank = file->private_data; loff_t newpos; spin_lock(&bank->lock); switch (whence) { case SEEK_SET: newpos = off; break; case SEEK_CUR: newpos = file->f_pos + off; break; case SEEK_END: newpos = bank->nr_pins + off; break; default: return -EINVAL; } spin_unlock(&bank->lock); return newpos; } static struct file_operations io_fops = { .owner = THIS_MODULE, .llseek = io_llseek, .open = io_open, .release = io_release, .read = io_read, .write = io_write, }; static int irq_open(struct inode *inode, struct file *file) { struct gpiodev *bank = container_of(inode->i_cdev, struct gpiodev, io_char_dev); nonseekable_open(inode, file); spin_lock(&bank->lock); config_item_get(&bank->group.cg_item); file->private_data = bank; spin_unlock(&bank->lock); return 0; } static int irq_release(struct inode *inode, struct file *file) { struct gpiodev *bank = file->private_data; spin_lock(&bank->lock); config_item_put(&bank->group.cg_item); spin_unlock(&bank->lock); return 0; } static ssize_t irq_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { int i, len, *kbuf; struct gpiodev *bank = file->private_data; while (!kfifo_len(bank->irq_buf)) { if (file->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(bank->irq_wq, kfifo_len(bank->irq_buf))) return -ERESTARTSYS; } len = min((size_t)kfifo_len(bank->irq_buf), count); kbuf = kmalloc(len * sizeof(int), GFP_KERNEL); if (!kbuf) return -ENOMEM; for (i = 0; i < len; i++) BUF_GET_INT(bank->irq_buf, kbuf[i]); if (copy_to_user(buf, kbuf, len)) { kfree(kbuf); return -EFAULT; } kfree(kbuf); return len; } static ssize_t irq_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) { struct gpiodev *bank = file->private_data; /* Flush buffer */ kfifo_reset(bank->irq_buf); return count; } static int irq_fasync(int fd, struct file *file, int mode) { struct gpiodev *bank = file->private_data; return fasync_helper(fd, file, mode, &bank->async_queue); } static struct file_operations irq_fops = { .owner = THIS_MODULE, .llseek = no_llseek, .open = irq_open, .release = irq_release, .read = irq_read, .write = irq_write, .fasync = irq_fasync, }; /* * {enable,disable}_bank_{io,irq}() must be called under that bank's * lock. They should only be called through their * set_bank_{io,irq}_enabled() which will (amongst other things) * take care of this locking for you. */ static int enable_bank_io(struct gpiodev *bank) { int i; /* What error should a pin request failure be?? */ int ret = -ENODEV; BUG_ON(!bank); for (i = 0; i < bank->nr_pins; i++) { if (!gpio_request(bank->pins[i].pin, bank->pins[i].label)) goto err_alloc_pins; } cdev_init(&bank->io_char_dev, &io_fops); bank->io_char_dev.owner = THIS_MODULE; ret = cdev_add(&bank->io_char_dev, MKDEV(MAJOR(base_devt), bank->id * 2), 1); if (ret < 0) goto err_cdev_add; bank->io_dev = device_create(dev_class, NULL, MKDEV(MAJOR(base_devt), bank->id * 2), "gpiod-%s", bank->bankname); if (IS_ERR(bank->io_dev)) { printk(KERN_ERR "gpio-dev: failed to create gpiod%d\n", bank->id); ret = PTR_ERR(bank->io_dev); goto err_dev; } printk(KERN_INFO "gpio-dev: created gpiod%d as (%d:%d)\n", bank->id, MAJOR(bank->io_dev->devt), MINOR(bank->io_dev->devt)); return 0; err_dev: cdev_del(&bank->io_char_dev); err_cdev_add: err_alloc_pins: while (i--) gpio_free(bank->pins[i].pin); return ret; } static int disable_bank_io(struct gpiodev *bank) { int i; BUG_ON(!bank); device_unregister(bank->io_dev); cdev_del(&bank->io_char_dev); for (i = 0; i < bank->nr_pins; i++) gpio_free(bank->pins[i].pin); return 0; } static int set_bank_io_enabled(struct gpiodev *bank, int enabled) { int ret = 0; if (!bank) { printk(KERN_ERR "gpio-dev: Attempt to change enable " \ "without valid device binding\n"); return -EINVAL; } pr_debug("gpio-dev: setting bank %s enabled from %d to %d\n", bank->bankname, bank->io_enabled, enabled); spin_lock(&bank->lock); if (bank->io_enabled == enabled) goto out; if (enabled) ret = enable_bank_io(bank); else ret = disable_bank_io(bank); if (!ret) bank->io_enabled = enabled; out: spin_unlock(&bank->lock); return ret; } static int enable_bank_irq(struct gpiodev *bank) { int ret; BUG_ON(!bank); cdev_init(&bank->irq_char_dev, &irq_fops); bank->irq_char_dev.owner = THIS_MODULE; ret = cdev_add(&bank->irq_char_dev, MKDEV(MAJOR(base_devt), bank->id * 2 + 1), 1); if (ret < 0) goto err_cdev_add; bank->irq_dev = device_create(dev_class, NULL, MKDEV(MAJOR(base_devt), bank->id), "gpioi-%s", bank->bankname); if (IS_ERR(bank->irq_dev)) { printk(KERN_ERR "gpio-dev: failed to create gpioi%d\n", bank->id); ret = PTR_ERR(bank->irq_dev); goto err_dev; } printk(KERN_INFO "gpio-dev: created gpioi%d as (%d:%d)\n", bank->id, MAJOR(bank->irq_dev->devt), MINOR(bank->irq_dev->devt)); return 0; err_dev: cdev_del(&bank->irq_char_dev); err_cdev_add: return ret; } static int disable_bank_irq(struct gpiodev *bank) { BUG_ON(!bank); device_unregister(bank->irq_dev); cdev_del(&bank->irq_char_dev); return 0; } static int set_bank_irq_enabled(struct gpiodev *bank, int enabled) { int ret = 0; if (!bank) { printk(KERN_ERR "gpio-dev: Attempt to change irq enable " \ "without valid device binding\n"); return -EINVAL; } pr_debug("gpio-dev: setting bank irq %s enabled from %d to %d\n", bank->bankname, bank->irq_enabled, enabled); spin_lock(&bank->lock); if (bank->irq_enabled == enabled) goto out; if (enabled) ret = enable_bank_irq(bank); else ret = disable_bank_irq(bank); if (!ret) bank->irq_enabled = enabled; out: spin_unlock(&bank->lock); return ret; } static inline struct gpiodev *to_gpiodev(struct config_group *group) { return group ? container_of(group, struct gpiodev, group) : NULL; } static struct configfs_attribute bank_attr_enableio = { .ca_owner = THIS_MODULE, .ca_name = "enable-io", .ca_mode = S_IRUGO | S_IWUGO, }; static struct configfs_attribute bank_attr_enableirq = { .ca_owner = THIS_MODULE, .ca_name = "enable-irq", .ca_mode = S_IRUGO | S_IWUGO, }; static struct configfs_attribute *cfg_bank_attrs[] = { &bank_attr_enableio, &bank_attr_enableirq, NULL, }; static ssize_t cfg_bank_attr_show(struct config_item *item, struct configfs_attribute *attr, char *page) { struct gpiodev *bank = to_gpiodev(to_config_group(item)); if (attr == &bank_attr_enableio) return sprintf(page, "%d\n", bank->io_enabled); else if (attr == &bank_attr_enableirq) return sprintf(page, "%d\n", bank->irq_enabled); else WARN_ON(1); return 0; } static ssize_t cfg_bank_attr_store(struct config_item *item, struct configfs_attribute *attr, const char *page, size_t count) { struct gpiodev *bank = to_gpiodev(to_config_group(item)); /* The only attributes which will come here are enable-{io,irq} */ unsigned long tmp; int ret = 0; char *p = (char *) page; tmp = simple_strtoul(p, &p, 10); if (!p || (*p && (*p != '\n'))) return -EINVAL; if (tmp > INT_MAX) return -ERANGE; /* Booleanize */ tmp = !!tmp; pr_debug("gpio-dev: changing enabled attr on bank %s, attribute %d\n", bank->bankname, (int)attr); if (attr == &bank_attr_enableio) ret = set_bank_io_enabled(bank, tmp); else if (attr == &bank_attr_enableirq) ret = set_bank_irq_enabled(bank, tmp); else { printk(KERN_ERR "unexpected attribute at %s, %d", __func__, __LINE__); WARN_ON(1); } return ret ? ret : count; } static struct configfs_item_operations cfg_bank_item_ops = { .show_attribute = cfg_bank_attr_show, .store_attribute = cfg_bank_attr_store, }; static struct config_item_type cfg_bank_type = { .ct_owner = THIS_MODULE, .ct_item_ops = &cfg_bank_item_ops, .ct_attrs = cfg_bank_attrs, }; static struct config_group *cfg_make_group(struct config_group *group, const char *name) { int i; struct gpiodev *dev = NULL; spin_lock(&devices_lock); for (i = 0; i < MAX_NR_DEVICES; i++) { if (!devices[i]) continue; if (strcmp(name, devices[i]->bankname) == 0) { dev = devices[i]; printk(KERN_INFO "gpio-dev: Bound configfs instance" \ " to device %s\n", devices[i]->bankname); } } spin_unlock(&devices_lock); if (!dev) { printk(KERN_WARNING "gpio-dev: Can't find bank \"%s\"\n", name); /* * Once I get home and can modify the configfs_mkdir code * to allow more informative return types we can modify this * to, eg, ERR_PTR(-ENODEV) */ return NULL; } /* * REVISIT: Do we need to get this here? The docco says we should drop * a reference in drop_group() so I assume we need to actually get one * at one stage */ config_item_get(&dev->group.cg_item); return &dev->group; } static void cfg_drop_group(struct config_group *group, struct config_item *item) { struct gpiodev *bank = to_gpiodev(to_config_group(item)); pr_debug("gpio-dev: bank %s getting dropped\n", bank->bankname); set_bank_io_enabled(bank, 0); set_bank_irq_enabled(bank, 0); config_item_put(item); } static struct configfs_attribute attr_banks = { .ca_owner = THIS_MODULE, .ca_name = "banks", .ca_mode = S_IRUGO, }; static struct configfs_attribute *cfg_attrs[] = { &attr_banks, NULL, }; static ssize_t cfg_attr_show(struct config_item *item, struct configfs_attribute *attr, char *page) { int i; int count = 0; /* The only attribute which comes here is 'banks' */ spin_lock(&devices_lock); for (i = 0; i < MAX_NR_DEVICES; i++) { if (devices[i]) count += sprintf(page + count, "%s\n", devices[i]->bankname); } spin_unlock(&devices_lock); return count; } static struct configfs_item_operations cfg_item_ops = { .show_attribute = cfg_attr_show, }; static struct configfs_group_operations cfg_group_ops = { .make_group = cfg_make_group, .drop_item = cfg_drop_group, }; static struct config_item_type cfg_type = { .ct_item_ops = &cfg_item_ops, .ct_group_ops = &cfg_group_ops, .ct_attrs = cfg_attrs, .ct_owner = THIS_MODULE, }; static struct configfs_subsystem cfg_subsys = { .su_group = { .cg_item = { .ci_namebuf = "gpio-dev", .ci_type = &cfg_type, }, }, }; static struct configfs_attribute pin_attr_index = { .ca_owner = THIS_MODULE, .ca_name = "index", .ca_mode = S_IRUGO, }; static struct configfs_attribute pin_attr_direction = { .ca_owner = THIS_MODULE, .ca_name = "direction", .ca_mode = S_IRUGO | S_IWUGO, }; static struct configfs_attribute pin_attr_irq = { .ca_owner = THIS_MODULE, .ca_name = "irq", .ca_mode = S_IRUGO | S_IWUGO, }; static struct configfs_attribute *pin_attrs[] = { &pin_attr_index, &pin_attr_direction, &pin_attr_irq, NULL, }; static inline struct gpiodev_pin *to_gpiodev_pin(struct config_group *group) { return group ? container_of(group, struct gpiodev_pin, group) : NULL; } static ssize_t pin_show(struct config_item *item, struct configfs_attribute *attr, char *page) { struct gpiodev_pin *pin = to_gpiodev_pin(to_config_group(item)); /* All attributes for a pin come here */ if (attr == &pin_attr_index) return sprintf(page, "%d\n", pin->index); else if (attr == &pin_attr_direction) return sprintf(page, "%d\n", pin->direction); else if (attr == &pin_attr_irq) return sprintf(page, "%d\n", pin->irq); else { printk(KERN_ERR "gpio-dev: Unexpected attribute in %s\n", __func__); return 0; } } static ssize_t pin_store(struct config_item *item, struct configfs_attribute *attr, const char *page, size_t count) { struct gpiodev_pin *pin = to_gpiodev_pin(to_config_group(item)); char *p = (char *)page; int val; int ret = 0; val = simple_strtoul(p, &p, 0); if (!p || (*p && (*p != '\n'))) return -EINVAL; if (attr == &pin_attr_direction) { pin->direction = val; if (val) gpio_direction_output(pin->pin, pin->outval); else gpio_direction_input(pin->pin); } else if (attr == &pin_attr_irq) { if (!pin->irq && val) { ret = request_irq(gpio_to_irq(pin->pin), irq_interrupt, 0, "gpio-dev", pin); if (ret) return ret; } else if (pin->irq && !val) { free_irq(gpio_to_irq(pin->pin), pin); } pin->irq = val; /* * The values written by the user directly correspond to * the values behind the IRQF_TRIGGER_* tokens. Can we * rely on this or should we explicitly check and set each * possible trigger type? */ if (val) ret = set_irq_type(gpio_to_irq(pin->pin), val); if (ret) return -EINVAL; } else { printk(KERN_ERR "gpio-dev: Unexpected attribute in %s\n", __func__); } return count; } static struct configfs_item_operations pin_ops = { .show_attribute = pin_show, .store_attribute = pin_store, }; static struct config_item_type cfg_pin_type = { .ct_owner = THIS_MODULE, .ct_attrs = pin_attrs, .ct_item_ops = &pin_ops, }; static int __init gpiodev_probe(struct platform_device *pdev) { int i, ret; struct gpiodev *bank = NULL; struct device *dev = &pdev->dev; struct gpiodev_pdata *pdata = dev->platform_data; if (!pdata) { printk(KERN_ERR "gpio-dev: No configuration data available; "\ "aborting probe."); return -EINVAL; } bank = kzalloc(sizeof(struct gpiodev), GFP_KERNEL); if (!bank) return -ENOMEM; spin_lock_init(&bank->lock); spin_lock_init(&bank->irq_lock); bank->bankname = pdata->bankname; bank->nr_pins = pdata->nr_pins; bank->pins = kmalloc(bank->nr_pins * sizeof(struct gpiodev_pin), GFP_KERNEL); if (!bank->pins) { ret = -ENOMEM; goto err_pin_alloc; } bank->irq_buf = kfifo_alloc(IRQ_BUFFER_SIZE, GFP_KERNEL, &bank->irq_lock); if (IS_ERR(bank->irq_buf)) { ret = PTR_ERR(bank->irq_buf); goto err_irq_buf_alloc; } memcpy(bank->pins, pdata->pins, bank->nr_pins * sizeof(struct gpiodev_pin)); pr_debug("gpio-dev: initing device group %s\n", bank->bankname); config_group_init_type_name(&bank->group, bank->bankname, &cfg_bank_type); bank->group.default_groups = kzalloc(sizeof(struct config_group) * bank->nr_pins, GFP_KERNEL); if (!bank->group.default_groups) { ret = -ENOMEM; goto err_group_alloc; } for (i = 0; i < bank->nr_pins; i++) { pr_debug("gpio-dev: initing pin %s on bank %s\n", bank->pins[i].label, bank->bankname); config_group_init_type_name(&bank->pins[i].group, bank->pins[i].label, &cfg_pin_type); bank->group.default_groups[i] = &bank->pins[i].group; } bank->io_enabled = 0; bank->irq_enabled = 0; platform_set_drvdata(pdev, bank); bank->id = -1; /* Find first empty slot */ spin_lock(&devices_lock); for (i = 0; i < MAX_NR_DEVICES; i++) { if (devices[i] == NULL) { devices[i] = bank; bank->id = i; break; } } spin_unlock(&devices_lock); if (bank->id == -1) { printk(KERN_ERR "gpio-dev: More than MAX_NR_DEVICES probed;"\ " no free slots found!"); ret = -ENODEV; goto err_no_slot; } return 0; err_no_slot: kfree(bank->group.default_groups); err_group_alloc: kfifo_free(bank->irq_buf); err_irq_buf_alloc: kfree(bank->pins); err_pin_alloc: kfree(bank); return ret; } static int __exit gpiodev_remove(struct platform_device *pdev) { struct gpiodev *bank = platform_get_drvdata(pdev); spin_lock(&devices_lock); devices[bank->id] = NULL; spin_unlock(&devices_lock); kfifo_free(bank->irq_buf); kfree(bank->pins); kfree(bank); return 0; } static struct platform_driver gpiodev_driver = { .driver = { .name = "gpio-dev", .owner = THIS_MODULE, }, .remove = __exit_p(gpiodev_remove), .probe = gpiodev_probe, }; static int __init gpiodev_init(void) { int ret; spin_lock_init(&devices_lock); dev_class = class_create(THIS_MODULE, "gpio-dev"); if (IS_ERR(dev_class)) { ret = PTR_ERR(dev_class); goto err_class_create; } ret = alloc_chrdev_region(&base_devt, 0, MAX_NR_DEVICES * 2, "gpio-dev"); if (ret < 0) goto err_alloc_chrdev; pr_debug("gpio-dev: Registering configfs subsystem %s\n", cfg_subsys.su_group.cg_item.ci_namebuf); config_group_init(&cfg_subsys.su_group); mutex_init(&cfg_subsys.su_mutex); ret = configfs_register_subsystem(&cfg_subsys); if (ret) { printk(KERN_ERR "Error %d while registering subsystem %s\n", ret, cfg_subsys.su_group.cg_item.ci_namebuf); goto err_subsys; } ret = platform_driver_register(&gpiodev_driver); if (ret) goto err_dev_register; return 0; err_dev_register: configfs_unregister_subsystem(&cfg_subsys); err_subsys: unregister_chrdev_region(base_devt, MAX_NR_DEVICES); err_alloc_chrdev: class_destroy(dev_class); err_class_create: printk(KERN_ALERT "gpio-dev: Failed to init gpio-dev interface\n"); return ret; } static void __exit gpiodev_exit(void) { platform_driver_unregister(&gpiodev_driver); configfs_unregister_subsystem(&cfg_subsys); unregister_chrdev_region(base_devt, MAX_NR_DEVICES * 2); class_destroy(dev_class); printk(KERN_INFO "gpio-dev: unloaded gpio-dev\n"); } module_init(gpiodev_init); module_exit(gpiodev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ben Nizette <bn@xxxxxxxxxxxxxxx>"); MODULE_DESCRIPTION("Userspace interface to the gpio framework");