You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
667 lines
17 KiB
667 lines
17 KiB
/*
|
|
* Driver for inter-cell links using the shared-buffer transport.
|
|
*
|
|
* 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/atomic.h>
|
|
#include <linux/cdev.h>
|
|
#include <linux/device.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/init.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/io.h>
|
|
#include <linux/ioctl.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/module.h>
|
|
#include <linux/of.h>
|
|
#include <linux/of_address.h>
|
|
#include <linux/of_irq.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/rwsem.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/types.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/wait.h>
|
|
#include <linux/version.h>
|
|
#include <microvisor/microvisor.h>
|
|
#include <uapi/linux/okl4-link-shbuf.h>
|
|
|
|
static const char DEVICE_NAME[] = "okl4_link_shbuf";
|
|
|
|
/* Created devices will appear as /dev/<DEV_PREFIX><name> */
|
|
static const char DEV_PREFIX[] = "okl4-";
|
|
|
|
static const struct of_device_id okl4_link_shbuf_match[] = {
|
|
{
|
|
.compatible = "okl,microvisor-link-shbuf",
|
|
},
|
|
{},
|
|
};
|
|
MODULE_DEVICE_TABLE(of, okl4_link_shbuf_match);
|
|
|
|
static struct class *link_shbuf_class;
|
|
static dev_t link_shbuf_dev;
|
|
|
|
/* A lock used to protect access to link_shbuf_dev */
|
|
static spinlock_t device_number_allocate;
|
|
|
|
/* Sentinel values for indicating missing communication channels */
|
|
static const u32 NO_OUTGOING_IRQ = 0;
|
|
static const int NO_INCOMING_IRQ = -1;
|
|
|
|
/* Private data for this driver */
|
|
struct link_shbuf_data {
|
|
|
|
/* Outgoing vIRQ */
|
|
u32 virqline;
|
|
|
|
/* Incoming vIRQ */
|
|
int virq;
|
|
atomic64_t virq_payload;
|
|
bool virq_pending;
|
|
wait_queue_head_t virq_wq;
|
|
|
|
/* Shared memory region */
|
|
void *base;
|
|
fmode_t permissions;
|
|
struct resource buffer;
|
|
|
|
/* Device data */
|
|
dev_t devt;
|
|
struct device *dev;
|
|
struct cdev cdev;
|
|
|
|
};
|
|
|
|
static bool link_shbuf_data_invariant(const struct link_shbuf_data *priv)
|
|
{
|
|
if (!priv)
|
|
return false;
|
|
|
|
if (!priv->base || (uintptr_t)priv->base % PAGE_SIZE != 0)
|
|
return false;
|
|
|
|
if (resource_size(&priv->buffer) == 0)
|
|
return false;
|
|
|
|
if (!priv->dev)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool link_shbuf_valid_access(size_t size, loff_t pos, size_t count)
|
|
{
|
|
return pos < size && count <= size && size - count >= pos;
|
|
}
|
|
|
|
static ssize_t link_shbuf_read(struct file *file, char __user *buffer,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
long remaining;
|
|
const struct link_shbuf_data *priv;
|
|
|
|
/* The file should have been opened with read access to reach here */
|
|
if (WARN_ON(!(file->f_mode & FMODE_READ)))
|
|
return -EINVAL;
|
|
|
|
priv = file->private_data;
|
|
if (WARN_ON(!link_shbuf_data_invariant(priv)))
|
|
return -EINVAL;
|
|
|
|
if (!link_shbuf_valid_access(resource_size(&priv->buffer), *ppos, count))
|
|
return -EINVAL;
|
|
|
|
remaining = copy_to_user(buffer, priv->base + *ppos, count);
|
|
*ppos += count - remaining;
|
|
return count - remaining;
|
|
}
|
|
|
|
static ssize_t link_shbuf_write(struct file *file, const char __user *buffer,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
long remaining;
|
|
const struct link_shbuf_data *priv;
|
|
|
|
/* The file should have been opened with write access to reach here */
|
|
if (WARN_ON(!(file->f_mode & FMODE_WRITE)))
|
|
return -EINVAL;
|
|
|
|
priv = file->private_data;
|
|
if (WARN_ON(!link_shbuf_data_invariant(priv)))
|
|
return -EINVAL;
|
|
|
|
if (!link_shbuf_valid_access(resource_size(&priv->buffer), *ppos, count))
|
|
return -EINVAL;
|
|
|
|
remaining = copy_from_user(priv->base + *ppos, buffer, count);
|
|
*ppos += count - remaining;
|
|
return count - remaining;
|
|
}
|
|
|
|
static unsigned int link_shbuf_poll(struct file *file, poll_table *table)
|
|
{
|
|
struct link_shbuf_data *priv;
|
|
unsigned int mask;
|
|
|
|
priv = file->private_data;
|
|
if (WARN_ON(!link_shbuf_data_invariant(priv)))
|
|
return POLLERR;
|
|
|
|
poll_wait(file, &priv->virq_wq, table);
|
|
|
|
/* The shared memory is always considered ready for reading and writing. */
|
|
mask = POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM;
|
|
|
|
if (priv->virq_pending)
|
|
mask |= POLLPRI;
|
|
|
|
return mask;
|
|
}
|
|
|
|
static long link_shbuf_ioctl_irq_tx(const struct link_shbuf_data *priv,
|
|
unsigned long arg)
|
|
{
|
|
okl4_error_t err;
|
|
u64 payload;
|
|
const u64 __user *user_arg = (const u64 __user*)arg;
|
|
|
|
if (priv->virqline == NO_OUTGOING_IRQ)
|
|
return -EINVAL;
|
|
|
|
#if defined(CONFIG_ARM) && (LINUX_VERSION_CODE < KERNEL_VERSION(3, 17, 0))
|
|
if (copy_from_user(&payload, user_arg, sizeof(payload)))
|
|
return -EFAULT;
|
|
#else
|
|
if (get_user(payload, user_arg))
|
|
return -EFAULT;
|
|
#endif
|
|
|
|
err = _okl4_sys_vinterrupt_raise(priv->virqline, payload);
|
|
if (WARN_ON(err != OKL4_OK))
|
|
return -EINVAL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static long link_shbuf_ioctl_irq_clr(struct link_shbuf_data *priv,
|
|
unsigned long arg)
|
|
{
|
|
u64 payload;
|
|
u64 __user *user_arg = (u64 __user*)arg;
|
|
|
|
/*
|
|
* Check validity of the user pointer before clearing the interrupt to avoid
|
|
* races involved with having to undo the latter.
|
|
*/
|
|
if (!access_ok(VERIFY_WRITE, user_arg, sizeof(*user_arg)))
|
|
return -EFAULT;
|
|
|
|
/*
|
|
* Note that the clearing of the pending flag can race with the setting of
|
|
* this flag in the IRQ handler. It is up to the user to coordinate these
|
|
* actions.
|
|
*/
|
|
priv->virq_pending = false;
|
|
smp_rmb();
|
|
payload = atomic64_xchg(&priv->virq_payload, 0);
|
|
|
|
/* We've already checked that this access is OK, so no need for put_user. */
|
|
if (__put_user(payload, user_arg))
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static long link_shbuf_ioctl(struct file *file, unsigned int request,
|
|
unsigned long arg)
|
|
{
|
|
struct link_shbuf_data *priv;
|
|
|
|
priv = file->private_data;
|
|
if (WARN_ON(!link_shbuf_data_invariant(priv)))
|
|
return -EINVAL;
|
|
|
|
/* We only support two ioctls */
|
|
switch (request) {
|
|
|
|
case OKL4_LINK_SHBUF_IOCTL_IRQ_TX:
|
|
return link_shbuf_ioctl_irq_tx(priv, arg);
|
|
|
|
case OKL4_LINK_SHBUF_IOCTL_IRQ_CLR:
|
|
return link_shbuf_ioctl_irq_clr(priv, arg);
|
|
|
|
}
|
|
|
|
/*
|
|
* Handy for debugging when userspace is linking against ioctl headers from
|
|
* a different kernel revision.
|
|
*/
|
|
dev_dbg(priv->dev, "ioctl request 0x%x received which did not match either "
|
|
"OKL4_LINK_SHBUF_IOCTL_IRQ_TX (0x%x) or OKL4_LINK_SHBUF_IOCTL_IRQ_CLR "
|
|
"(0x%x)\n", request, (unsigned)OKL4_LINK_SHBUF_IOCTL_IRQ_TX,
|
|
(unsigned)OKL4_LINK_SHBUF_IOCTL_IRQ_CLR);
|
|
|
|
return -EINVAL;
|
|
}
|
|
|
|
static int link_shbuf_mmap(struct file *file, struct vm_area_struct *vma)
|
|
{
|
|
const struct link_shbuf_data *priv;
|
|
unsigned long offset, pfn, flags;
|
|
size_t size;
|
|
pgprot_t prot;
|
|
|
|
/* Our caller should have taken the MM semaphore. */
|
|
if (WARN_ON(!rwsem_is_locked(&vma->vm_mm->mmap_sem)))
|
|
return -EINVAL;
|
|
|
|
/*
|
|
* The file should have been opened with a superset of the mmap requested
|
|
* permissions.
|
|
*/
|
|
flags = vma->vm_flags;
|
|
if (WARN_ON((flags & VM_READ) && !(file->f_mode & FMODE_READ)))
|
|
return -EINVAL;
|
|
if (WARN_ON((flags & VM_WRITE) && !(file->f_mode & FMODE_WRITE)))
|
|
return -EINVAL;
|
|
if (WARN_ON((flags & VM_EXEC) && !(file->f_mode & FMODE_EXEC)))
|
|
return -EINVAL;
|
|
|
|
/* Retrieve our private data. */
|
|
priv = file->private_data;
|
|
if (WARN_ON(!link_shbuf_data_invariant(priv)))
|
|
return -EINVAL;
|
|
|
|
/* Check the mmap request is within bounds. */
|
|
size = vma->vm_end - vma->vm_start;
|
|
offset = vma->vm_pgoff << PAGE_SHIFT;
|
|
if (!link_shbuf_valid_access(resource_size(&priv->buffer), offset, size))
|
|
return -EINVAL;
|
|
|
|
pfn = (priv->buffer.start + offset) >> PAGE_SHIFT;
|
|
prot = vm_get_page_prot(flags);
|
|
|
|
return remap_pfn_range(vma, vma->vm_start, pfn, size, prot);
|
|
}
|
|
|
|
static bool link_shbuf_access_ok(fmode_t allowed, fmode_t request)
|
|
{
|
|
static const fmode_t ACCESS_MASK = FMODE_READ|FMODE_WRITE|FMODE_EXEC;
|
|
fmode_t relevant = request & ACCESS_MASK;
|
|
return (relevant & allowed) == relevant;
|
|
}
|
|
|
|
static int link_shbuf_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct cdev *cdev;
|
|
struct link_shbuf_data *priv;
|
|
|
|
/* Retrieve a pointer to our private data */
|
|
cdev = inode->i_cdev;
|
|
priv = container_of(cdev, struct link_shbuf_data, cdev);
|
|
if (WARN_ON(!link_shbuf_data_invariant(priv)))
|
|
return -EINVAL;
|
|
|
|
if (!link_shbuf_access_ok(priv->permissions, file->f_mode))
|
|
return -EACCES;
|
|
|
|
file->private_data = priv;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct file_operations link_shbuf_ops = {
|
|
.owner = THIS_MODULE,
|
|
.read = link_shbuf_read,
|
|
.write = link_shbuf_write,
|
|
.poll = link_shbuf_poll,
|
|
.unlocked_ioctl = link_shbuf_ioctl,
|
|
#ifdef CONFIG_COMPAT
|
|
.compat_ioctl = link_shbuf_ioctl,
|
|
#endif
|
|
#ifdef CONFIG_MMU
|
|
.mmap = link_shbuf_mmap,
|
|
#endif
|
|
.open = link_shbuf_open,
|
|
};
|
|
|
|
/*
|
|
* Interrupt handler.
|
|
*
|
|
* This function will be called when our link partner uses the ioctl on their
|
|
* shared memory device to send an outgoing interrupt.
|
|
*/
|
|
static irqreturn_t link_shbuf_irq_handler(int irq, void *data)
|
|
{
|
|
u64 payload, old, new;
|
|
struct _okl4_sys_interrupt_get_payload_return _payload;
|
|
|
|
/* Retrieve a pointer to our private data. */
|
|
struct link_shbuf_data *priv = data;
|
|
if (WARN_ON(!link_shbuf_data_invariant(priv)))
|
|
return IRQ_NONE;
|
|
|
|
/*
|
|
* We should only ever be handling a single interrupt, and only if there
|
|
* was an incoming interrupt in the configuration.
|
|
*/
|
|
if (WARN_ON(priv->virq < 0 || priv->virq != irq))
|
|
return IRQ_NONE;
|
|
|
|
_payload = _okl4_sys_interrupt_get_payload(irq);
|
|
payload = (u64)_payload.payload;
|
|
|
|
/*
|
|
* At this point, it is possible the pending flag is already set. It is up to
|
|
* the user to synchronise their transmission and acknowledgement of
|
|
* interrupts.
|
|
*/
|
|
|
|
/* We open code atomic64_or which is not universally available. */
|
|
do {
|
|
old = atomic64_read(&priv->virq_payload);
|
|
new = old | payload;
|
|
} while (atomic64_cmpxchg(&priv->virq_payload, old, new) != old);
|
|
smp_wmb();
|
|
priv->virq_pending = true;
|
|
|
|
wake_up_interruptible(&priv->virq_wq);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
/*
|
|
* Allocate a unique device number for this device.
|
|
*
|
|
* Note that this function needs to lock its access to link_shbuf_dev as there
|
|
* may be multiple threads attempting to acquire a new device number.
|
|
*/
|
|
static int link_shbuf_allocate_device(dev_t *devt)
|
|
{
|
|
int ret = 0;
|
|
dev_t next;
|
|
|
|
spin_lock(&device_number_allocate);
|
|
|
|
*devt = link_shbuf_dev;
|
|
next = MKDEV(MAJOR(link_shbuf_dev), MINOR(link_shbuf_dev) + 1);
|
|
/* Check for overflow */
|
|
if (MINOR(next) != MINOR(link_shbuf_dev) + 1)
|
|
ret = -ENOSPC;
|
|
else
|
|
link_shbuf_dev = next;
|
|
|
|
spin_unlock(&device_number_allocate);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Discover and add a new shared-buffer link.
|
|
*
|
|
* In the following function, we are expecting to parse device tree entries
|
|
* looking like the following:
|
|
*
|
|
* hypervisor {
|
|
* ...
|
|
* interrupt-line@1d {
|
|
* compatible = "okl,microvisor-interrupt-line",
|
|
* "okl,microvisor-capability";
|
|
* phandle = <0x7>;
|
|
* reg = <0x1d>;
|
|
* label = "foo_virqline";
|
|
* };
|
|
* ;
|
|
*
|
|
* foo@41003000 {
|
|
* compatible = "okl,microvisor-link-shbuf",
|
|
* "okl,microvisor-shared-memory";
|
|
* phandle = <0xd>;
|
|
* reg = <0x0 0x41003000 0x2000>;
|
|
* label = "foo";
|
|
* okl,rwx = <0x6>;
|
|
* okl,interrupt-line = <0x7>;
|
|
* interrupts = <0x0 0x4 0x1>;
|
|
* interrupt-parent = <0x1>;
|
|
* };
|
|
*/
|
|
static int link_shbuf_probe(struct platform_device *pdev)
|
|
{
|
|
int ret;
|
|
struct device_node *node, *virqline;
|
|
struct link_shbuf_data *priv;
|
|
const char *name;
|
|
u32 permissions;
|
|
|
|
node = pdev->dev.of_node;
|
|
|
|
if (!node)
|
|
return -ENODEV;
|
|
|
|
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
|
|
if (!priv)
|
|
return -ENOMEM;
|
|
|
|
/*
|
|
* Retrieve the outgoing vIRQ cap. Note, this is configurable and we
|
|
* anticipate that it may not exist.
|
|
*/
|
|
virqline = of_parse_phandle(node, "okl,interrupt-line", 0);
|
|
if (!virqline) {
|
|
priv->virqline = NO_OUTGOING_IRQ;
|
|
} else {
|
|
ret = of_property_read_u32(virqline, "reg", &priv->virqline);
|
|
if (ret < 0 || priv->virqline == OKL4_KCAP_INVALID) {
|
|
of_node_put(virqline);
|
|
ret = -ENODEV;
|
|
goto err_free_dev;
|
|
}
|
|
}
|
|
of_node_put(virqline);
|
|
|
|
/* Retrieve the incoming vIRQ number. Again, this is configurable and we
|
|
* anticipate that it may not exist.
|
|
*/
|
|
priv->virq = platform_get_irq(pdev, 0);
|
|
if (priv->virq < 0)
|
|
priv->virq = NO_INCOMING_IRQ;
|
|
|
|
/* If we have a valid incoming vIRQ, register to handle it. */
|
|
if (priv->virq >= 0) {
|
|
ret = devm_request_irq(&pdev->dev, priv->virq, link_shbuf_irq_handler,
|
|
0, dev_name(&pdev->dev), priv);
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "failed request for IRQ\n");
|
|
goto err_free_dev;
|
|
}
|
|
}
|
|
|
|
init_waitqueue_head(&priv->virq_wq);
|
|
priv->virq_pending = false;
|
|
|
|
/* Retrieve information about the shared memory region. */
|
|
ret = of_address_to_resource(node, 0, &priv->buffer);
|
|
if (ret < 0)
|
|
goto err_free_irq;
|
|
/*
|
|
* We expect the Elfweaver to have validated that we have a non-NULL,
|
|
* page-aligned region.
|
|
*/
|
|
if (WARN_ON(priv->buffer.start == 0) ||
|
|
WARN_ON(resource_size(&priv->buffer) % PAGE_SIZE != 0))
|
|
goto err_free_irq;
|
|
if (!devm_request_mem_region(&pdev->dev, priv->buffer.start,
|
|
resource_size(&priv->buffer), dev_name(&pdev->dev))) {
|
|
ret = -ENODEV;
|
|
goto err_free_irq;
|
|
}
|
|
priv->base = devm_ioremap(&pdev->dev, priv->buffer.start,
|
|
resource_size(&priv->buffer));
|
|
if (!priv->base)
|
|
goto err_release_region;
|
|
|
|
/* Read the permissions of the shared memory region. */
|
|
ret = of_property_read_u32(node, "okl,rwx", &permissions);
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "failed to read shared memory permissions\n");
|
|
goto err_unmap_dev;
|
|
}
|
|
if (permissions & ~S_IRWXO) {
|
|
ret = -EINVAL;
|
|
goto err_unmap_dev;
|
|
}
|
|
priv->permissions = ((permissions & S_IROTH) ? FMODE_READ : 0) |
|
|
((permissions & S_IWOTH) ? FMODE_WRITE : 0) |
|
|
((permissions & S_IXOTH) ? FMODE_EXEC : 0);
|
|
if (WARN_ON(priv->permissions == 0)) {
|
|
ret = -EINVAL;
|
|
goto err_unmap_dev;
|
|
}
|
|
|
|
/* Retrieve the label of this device. This will be the "name" attribute of
|
|
* the corresponding "link" tag in the system's XML specification.
|
|
*/
|
|
ret = of_property_read_string(node, "label", &name);
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "failed to read label\n");
|
|
goto err_unmap_dev;
|
|
}
|
|
|
|
cdev_init(&priv->cdev, &link_shbuf_ops);
|
|
ret = cdev_add(&priv->cdev, link_shbuf_dev, 1);
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "failed to add char dev region\n");
|
|
goto err_unmap_dev;
|
|
}
|
|
|
|
ret = link_shbuf_allocate_device(&priv->devt);
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "failed to allocate new device number\n");
|
|
goto err_unmap_dev;
|
|
}
|
|
|
|
/* We're now ready to create the device itself. */
|
|
BUG_ON(name == NULL);
|
|
priv->dev = device_create(link_shbuf_class, &pdev->dev, priv->devt,
|
|
priv, "%s%s", DEV_PREFIX, name);
|
|
if (IS_ERR(priv->dev)) {
|
|
dev_err(&pdev->dev, "failed to create device\n");
|
|
ret = PTR_ERR(priv->dev);
|
|
goto err_del_dev;
|
|
}
|
|
|
|
dev_set_drvdata(&pdev->dev, priv);
|
|
|
|
return 0;
|
|
|
|
err_del_dev:
|
|
cdev_del(&priv->cdev);
|
|
err_unmap_dev:
|
|
devm_iounmap(&pdev->dev, priv->base);
|
|
err_release_region:
|
|
devm_release_mem_region(&pdev->dev, priv->buffer.start,
|
|
resource_size(&priv->buffer));
|
|
err_free_irq:
|
|
if (priv->virq != NO_INCOMING_IRQ)
|
|
devm_free_irq(&pdev->dev, priv->virq, priv);
|
|
err_free_dev:
|
|
devm_kfree(&pdev->dev, priv);
|
|
return ret;
|
|
}
|
|
|
|
static int link_shbuf_remove(struct platform_device *pdev)
|
|
{
|
|
struct link_shbuf_data *priv;
|
|
|
|
priv = dev_get_drvdata(&pdev->dev);
|
|
WARN_ON(!link_shbuf_data_invariant(priv));
|
|
|
|
device_destroy(link_shbuf_class, priv->devt);
|
|
|
|
cdev_del(&priv->cdev);
|
|
|
|
/*
|
|
* None of the following is strictly required, as these are all managed
|
|
* resources, but we clean it up anyway for clarity.
|
|
*/
|
|
|
|
devm_iounmap(&pdev->dev, priv->base);
|
|
|
|
devm_release_mem_region(&pdev->dev, priv->buffer.start,
|
|
resource_size(&priv->buffer));
|
|
|
|
if (priv->virq != NO_INCOMING_IRQ)
|
|
devm_free_irq(&pdev->dev, priv->virq, priv);
|
|
|
|
devm_kfree(&pdev->dev, priv);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct platform_driver of_plat_link_shbuf_driver = {
|
|
.driver = {
|
|
.name = "okl4-shbuf",
|
|
.owner = THIS_MODULE,
|
|
.of_match_table = okl4_link_shbuf_match,
|
|
},
|
|
.probe = link_shbuf_probe,
|
|
.remove = link_shbuf_remove,
|
|
};
|
|
|
|
/* Maximum number of minor device numbers */
|
|
enum {
|
|
MAX_MINOR = 1 << MINORBITS,
|
|
};
|
|
|
|
static int __init okl4_link_shbuf_init(void)
|
|
{
|
|
int ret;
|
|
|
|
link_shbuf_class = class_create(THIS_MODULE, DEVICE_NAME);
|
|
if (IS_ERR(link_shbuf_class)) {
|
|
pr_err("failed to create class\n");
|
|
ret = PTR_ERR(link_shbuf_class);
|
|
return ret;
|
|
}
|
|
|
|
ret = alloc_chrdev_region(&link_shbuf_dev, 0, MAX_MINOR, DEVICE_NAME);
|
|
if (ret < 0) {
|
|
pr_err("failed to allocate char dev region\n");
|
|
goto err_destroy_class;
|
|
}
|
|
|
|
ret = platform_driver_register(&of_plat_link_shbuf_driver);
|
|
if (ret < 0) {
|
|
pr_err("failed to register driver\n");
|
|
goto err_unregister_dev_region;
|
|
}
|
|
|
|
spin_lock_init(&device_number_allocate);
|
|
|
|
return 0;
|
|
|
|
err_unregister_dev_region:
|
|
unregister_chrdev_region(link_shbuf_dev, MAX_MINOR);
|
|
err_destroy_class:
|
|
class_destroy(link_shbuf_class);
|
|
return ret;
|
|
}
|
|
module_init(okl4_link_shbuf_init);
|
|
|
|
static void __exit okl4_link_shbuf_exit(void)
|
|
{
|
|
platform_driver_unregister(&of_plat_link_shbuf_driver);
|
|
unregister_chrdev_region(link_shbuf_dev, MAX_MINOR);
|
|
class_destroy(link_shbuf_class);
|
|
}
|
|
module_exit(okl4_link_shbuf_exit);
|
|
|
|
MODULE_DESCRIPTION("OKL4 shared buffer link driver");
|
|
MODULE_AUTHOR("Cog Systems Pty Ltd");
|
|
|