/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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", .owner = THIS_MODULE, .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");