| /* |
| * Simple driver for userspace access to inter-VM interrupts under the OKL4 |
| * Microvisor. |
| * |
| * Copyright (c) 2016 Cog Systems Pty Ltd. |
| * |
| * 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/init.h> |
| #include <linux/module.h> |
| #include <linux/device.h> |
| #include <linux/platform_device.h> |
| #include <linux/cdev.h> |
| #include <linux/of.h> |
| #include <linux/fs.h> |
| #include <linux/poll.h> |
| #include <linux/sched.h> |
| #include <linux/wait.h> |
| #include <linux/sysfs.h> |
| #include <linux/interrupt.h> |
| #include <linux/bitmap.h> |
| #include <linux/version.h> |
| #include <microvisor/microvisor.h> |
| #include <asm-generic/okl4_virq.h> |
| |
| /* Linux version compatibility */ |
| #if LINUX_VERSION_CODE < KERNEL_VERSION(3,11,0) |
| #define DEVICE_ATTR_RO(_name) \ |
| struct device_attribute dev_attr_##_name = __ATTR_RO(_name) |
| #define OLD_ATTRS |
| #endif |
| |
| #define VIRQ_MINORBITS 9 |
| |
| #define VIRQ_MINORS (1 << VIRQ_MINORBITS) |
| #define VIRQ_MINOR_MAX (VIRQ_MINORS - 1) |
| |
| struct virq_data { |
| wait_queue_head_t wq; |
| |
| int irqno; |
| |
| bool raised; |
| unsigned long payload; |
| }; |
| |
| struct source_data { |
| okl4_kcap_t kcap; |
| }; |
| |
| struct virq_device { |
| struct device *dev; |
| struct cdev cdev; |
| |
| int type; |
| struct virq_data virq; |
| struct source_data source; |
| |
| int minor; |
| const char *label; |
| }; |
| |
| static irqreturn_t okl4_virq_handler(int irq, void *dev_id) |
| { |
| struct virq_device *dev = dev_id; |
| unsigned long payload = okl4_get_virq_payload(irq); |
| |
| dev->virq.payload |= payload; |
| smp_wmb(); |
| dev->virq.raised = true; |
| |
| wake_up_interruptible(&dev->virq.wq); |
| return IRQ_HANDLED; |
| } |
| |
| static int virq_dev_open(struct inode *inode, struct file *file) |
| { |
| struct virq_device *dev; |
| |
| dev = container_of(inode->i_cdev, struct virq_device, cdev); |
| if (!dev) |
| return -ENODEV; |
| |
| file->private_data = dev; |
| |
| return nonseekable_open(inode, file); |
| } |
| |
| static int virq_dev_release(struct inode *inode, struct file *file) |
| { |
| return 0; |
| } |
| |
| static ssize_t virq_read(struct file *file, char __user *buf, size_t length, |
| loff_t *off) |
| { |
| struct virq_device *dev = file->private_data; |
| unsigned long payload; |
| int ret; |
| |
| if (!dev->virq.raised && (file->f_flags & O_NONBLOCK)) { |
| ret = -EAGAIN; |
| goto err_wait; |
| } |
| |
| do { |
| ret = wait_event_interruptible(dev->virq.wq, dev->virq.raised); |
| if (ret < 0) |
| goto err_wait; |
| } while(!xchg(&dev->virq.raised, 0)); |
| |
| smp_rmb(); |
| payload = xchg(&dev->virq.payload, 0); |
| |
| if (length >= sizeof(payload)) { |
| if (copy_to_user(buf, &payload, sizeof(payload))) { |
| dev_dbg(dev->dev, "failed to copy data to userland\n"); |
| return -EFAULT; |
| } |
| length = sizeof(payload); |
| } else if (length != 0) { |
| return -EIO; |
| } |
| |
| return length; |
| |
| err_wait: |
| return ret; |
| } |
| |
| static unsigned int virq_poll(struct file *file, poll_table *table) |
| { |
| int mask = 0; |
| struct virq_device *dev = file->private_data; |
| |
| poll_wait(file, &dev->virq.wq, table); |
| |
| if (dev->virq.raised) |
| mask |= POLLIN | POLLRDNORM; |
| |
| return mask; |
| } |
| |
| static ssize_t source_write(struct file *file, const char __user *ubuf, |
| size_t length, loff_t *offset) |
| { |
| struct virq_device *dev = file->private_data; |
| okl4_error_t err; |
| unsigned long payload; |
| |
| if (length == 0) |
| payload = 0; |
| else if (length == sizeof(payload)) { |
| if (copy_from_user(&payload, ubuf, length)) { |
| dev_dbg(dev->dev, "failed to copy data from userland\n"); |
| return -EFAULT; |
| } |
| } else { |
| return -EIO; |
| } |
| |
| err =_okl4_sys_vinterrupt_raise(dev->source.kcap, payload); |
| if (err != OKL4_OK) { |
| dev_dbg(dev->dev, "failed to raise virtual interrupt\n"); |
| return -EIO; |
| } |
| |
| return length; |
| } |
| |
| static ssize_t is_virq_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct virq_device *priv = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", priv->virq.irqno >= 0); |
| } |
| |
| static ssize_t is_source_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct virq_device *priv = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", |
| priv->source.kcap != OKL4_KCAP_INVALID); |
| } |
| |
| static ssize_t label_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct virq_device *priv = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%s\n", priv->label); |
| } |
| |
| /* Device registration */ |
| |
| /* |
| * file operations structure will be used for virq device. |
| */ |
| static const struct file_operations virq_r_fops = { |
| .owner = THIS_MODULE, |
| .open = virq_dev_open, |
| .release = virq_dev_release, |
| .read = virq_read, |
| .poll = virq_poll, |
| }; |
| |
| /* |
| * file operations structure will be used for virtual_interrupt_line device. |
| */ |
| static const struct file_operations virq_w_fops = { |
| .owner = THIS_MODULE, |
| .open = virq_dev_open, |
| .release = virq_dev_release, |
| .write = source_write, |
| }; |
| |
| /* |
| * file operations structure will be used for combined virq+source device. |
| */ |
| static const struct file_operations virq_rw_fops = { |
| .owner = THIS_MODULE, |
| .open = virq_dev_open, |
| .release = virq_dev_release, |
| .read = virq_read, |
| .poll = virq_poll, |
| .write = source_write, |
| }; |
| |
| #ifndef OLD_ATTRS |
| static DEVICE_ATTR_RO(is_virq); |
| static DEVICE_ATTR_RO(is_source); |
| static DEVICE_ATTR_RO(label); |
| |
| static struct attribute *virq_attrs[] = { |
| &dev_attr_is_virq.attr, |
| &dev_attr_is_source.attr, |
| &dev_attr_label.attr, |
| NULL |
| }; |
| ATTRIBUTE_GROUPS(virq); |
| #else |
| static struct device_attribute virq_attrs[] = { |
| __ATTR(is_virq, S_IRUGO, is_virq_show, NULL), |
| __ATTR(is_source, S_IRUGO, is_source_show, NULL), |
| __ATTR(label, S_IRUGO, label_show, NULL), |
| __ATTR_NULL, |
| }; |
| #endif |
| |
| static struct class *virq_class; |
| static dev_t virq_devt; |
| static DEFINE_MUTEX(virq_minor_lock); |
| unsigned long virq_ids[BITS_TO_LONGS(VIRQ_MINORBITS)]; |
| |
| /** |
| * virq_minor_get - obtain next free device minor number |
| * |
| * @dev: device pointer |
| * |
| * Return: allocated minor, or -ENOSPC if no free minor left |
| */ |
| static int virq_minor_get(struct virq_device *dev) |
| { |
| int ret; |
| |
| mutex_lock(&virq_minor_lock); |
| |
| ret = find_first_zero_bit(virq_ids, VIRQ_MINORS); |
| |
| if (ret >= VIRQ_MINORS) { |
| dev_err(dev->dev, "too many virq devices\n"); |
| ret = -ENODEV; |
| } else { |
| dev->minor = ret; |
| set_bit(ret, virq_ids); |
| ret = 0; |
| } |
| mutex_unlock(&virq_minor_lock); |
| return ret; |
| } |
| |
| /** |
| * virq_minor_free - mark device minor number as free |
| * |
| * @dev: device pointer |
| */ |
| static void virq_minor_free(struct virq_device *dev) |
| { |
| mutex_lock(&virq_minor_lock); |
| clear_bit(dev->minor, virq_ids); |
| mutex_unlock(&virq_minor_lock); |
| } |
| |
| static void virq_dev_deregister(struct virq_device *dev) |
| { |
| int devno; |
| |
| devno = dev->cdev.dev; |
| cdev_del(&dev->cdev); |
| |
| device_destroy(virq_class, devno); |
| |
| virq_minor_free(dev); |
| } |
| |
| static int okl4_virq_probe(struct platform_device *pdev) |
| { |
| int ret, devno, irq; |
| okl4_kcap_t kcap; |
| struct virq_device *dev; |
| struct device *parent; |
| struct device *clsdev; /* class device */ |
| const struct file_operations *fops; |
| struct device_node *node; |
| char devname[32]; |
| |
| parent = &pdev->dev; |
| node = parent->of_node; |
| |
| dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); |
| if (!dev) |
| return -ENOMEM; |
| |
| dev->virq.raised = false; |
| dev->virq.payload = 0; |
| |
| irq = platform_get_irq(pdev, 0); |
| dev->virq.irqno = irq; |
| |
| init_waitqueue_head(&dev->virq.wq); |
| |
| ret = virq_minor_get(dev); |
| if (ret < 0) |
| goto err; |
| |
| if (irq >= 0) { |
| ret = devm_request_irq(parent, irq, okl4_virq_handler, |
| IRQF_TRIGGER_RISING, dev_name(parent), dev); |
| if (ret < 0) |
| goto err_free_minor; |
| } |
| |
| kcap = OKL4_KCAP_INVALID; |
| if (of_device_is_compatible(node, "okl,microvisor-interrupt-line")) { |
| ret = of_property_read_u32(node, "reg", &kcap); |
| if (ret < 0) |
| goto err_free_minor; |
| } |
| dev->source.kcap = kcap; |
| |
| ret = of_property_read_string(node, "label", &dev->label); |
| if (ret) |
| dev->label = NULL; |
| |
| /* Fill in the data structures */ |
| devno = MKDEV(MAJOR(virq_devt), dev->minor); |
| |
| ret = -ENODEV; |
| if (irq >= 0 && kcap != OKL4_KCAP_INVALID) |
| fops = &virq_rw_fops; |
| else if (irq >= 0) |
| fops = &virq_r_fops; |
| else if (kcap != OKL4_KCAP_INVALID) |
| fops = &virq_w_fops; |
| else |
| goto err_free_minor; |
| |
| cdev_init(&dev->cdev, fops); |
| dev->cdev.owner = fops->owner; |
| |
| /* Add the device */ |
| ret = cdev_add(&dev->cdev, devno, 1); |
| if (ret) { |
| dev_err(parent, "unable to add device %d:%d\n", |
| MAJOR(virq_devt), dev->minor); |
| goto err_free_minor; |
| } |
| |
| scnprintf(devname, sizeof(devname), "virq%d", dev->minor); |
| #ifndef OLD_ATTRS |
| clsdev = device_create_with_groups(virq_class, parent, devno, dev, |
| virq_groups, devname, dev->minor); |
| #else |
| clsdev = device_create(virq_class, parent, devno, dev, devname, |
| dev->minor); |
| #endif |
| if (IS_ERR(clsdev)) { |
| dev_err(parent, "unable to create device %d:%d\n", |
| MAJOR(virq_devt), dev->minor); |
| ret = PTR_ERR(clsdev); |
| goto err_dev_create; |
| } |
| |
| dev_set_drvdata(parent, dev); |
| |
| return 0; |
| |
| err_dev_create: |
| cdev_del(&dev->cdev); |
| err_free_minor: |
| virq_minor_free(dev); |
| err: |
| devm_kfree(&pdev->dev, dev); |
| return ret; |
| } |
| |
| static int okl4_virq_remove(struct platform_device *pdev) |
| { |
| struct virq_device *priv = dev_get_drvdata(&pdev->dev); |
| |
| virq_dev_deregister(priv); |
| |
| return 0; |
| } |
| |
| /* Driver registration */ |
| |
| static const struct of_device_id of_plat_virq_table[] = { |
| { .compatible = "okl,user-virq" }, |
| { /* end */ }, |
| }; |
| |
| static struct platform_driver of_plat_virq_driver = { |
| .driver = { |
| .name = "okl4_virq", |
| .of_match_table = of_plat_virq_table, |
| }, |
| .probe = okl4_virq_probe, |
| .remove = okl4_virq_remove, |
| }; |
| |
| static int __init okl4_virq_init(void) |
| { |
| int ret; |
| |
| memset(virq_ids, 0, sizeof(virq_ids)); |
| |
| virq_class = class_create(THIS_MODULE, "okl4_virq"); |
| if (IS_ERR(virq_class)) { |
| pr_err("failed to create class\n"); |
| ret = PTR_ERR(virq_class); |
| goto err; |
| } |
| #ifdef OLD_ATTRS |
| virq_class->dev_attrs = virq_attrs; |
| #endif |
| |
| ret = alloc_chrdev_region(&virq_devt, 0, MINORMASK, "okl4_virq"); |
| if (ret < 0) { |
| pr_err("failed to allocate char dev region\n"); |
| goto err_class; |
| } |
| |
| ret = platform_driver_register(&of_plat_virq_driver); |
| if (ret) |
| goto err_virq_driver; |
| |
| return 0; |
| |
| err_virq_driver: |
| unregister_chrdev_region(virq_devt, MINORMASK); |
| err_class: |
| class_destroy(virq_class); |
| err: |
| return ret; |
| } |
| |
| static void __exit okl4_virq_exit(void) |
| { |
| platform_driver_unregister(&of_plat_virq_driver); |
| |
| unregister_chrdev_region(virq_devt, MINORMASK); |
| class_destroy(virq_class); |
| } |
| |
| module_init(okl4_virq_init); |
| module_exit(okl4_virq_exit); |
| |
| MODULE_AUTHOR("Cog Systems Pty Ltd"); |
| MODULE_DESCRIPTION("OKL4 userspace VIRQ interface"); |