[PATCH 165/222] cec: add generic HDMI CEC driver
Russell King
rmk+kernel at arm.linux.org.uk
Fri Apr 25 04:48:22 PDT 2014
Add a generic userspace API to support HDMI Consumer Electronics Control
interfaces.
Signed-off-by: Russell King <rmk+kernel at arm.linux.org.uk>
---
drivers/Kconfig | 2 +
drivers/Makefile | 1 +
drivers/cec/Kconfig | 14 ++
drivers/cec/Makefile | 1 +
drivers/cec/cec-dev.c | 384 +++++++++++++++++++++++++++++++++++++++++++
include/linux/cec-dev.h | 69 ++++++++
include/uapi/linux/cec-dev.h | 34 ++++
7 files changed, 505 insertions(+)
create mode 100644 drivers/cec/Kconfig
create mode 100644 drivers/cec/Makefile
create mode 100644 drivers/cec/cec-dev.c
create mode 100644 include/linux/cec-dev.h
create mode 100644 include/uapi/linux/cec-dev.h
diff --git a/drivers/Kconfig b/drivers/Kconfig
index 0a0a90f52d26..05a21b857996 100644
--- a/drivers/Kconfig
+++ b/drivers/Kconfig
@@ -174,4 +174,6 @@ source "drivers/powercap/Kconfig"
source "drivers/mcb/Kconfig"
+source "drivers/cec/Kconfig"
+
endmenu
diff --git a/drivers/Makefile b/drivers/Makefile
index e3ced91b1784..5600518a17dd 100644
--- a/drivers/Makefile
+++ b/drivers/Makefile
@@ -157,3 +157,4 @@ obj-$(CONFIG_NTB) += ntb/
obj-$(CONFIG_FMC) += fmc/
obj-$(CONFIG_POWERCAP) += powercap/
obj-$(CONFIG_MCB) += mcb/
+obj-$(CONFIG_CEC) += cec/
diff --git a/drivers/cec/Kconfig b/drivers/cec/Kconfig
new file mode 100644
index 000000000000..d67cfb83de6a
--- /dev/null
+++ b/drivers/cec/Kconfig
@@ -0,0 +1,14 @@
+#
+# Consumer Electroncs Control support
+#
+
+menu "Consumer Electronics Control devices"
+
+config CEC
+ bool
+
+config HDMI_CEC_CORE
+ tristate
+ select CEC
+
+endmenu
diff --git a/drivers/cec/Makefile b/drivers/cec/Makefile
new file mode 100644
index 000000000000..b94278bc8321
--- /dev/null
+++ b/drivers/cec/Makefile
@@ -0,0 +1 @@
+obj-$(CONFIG_HDMI_CEC_CORE) += cec-dev.o
diff --git a/drivers/cec/cec-dev.c b/drivers/cec/cec-dev.c
new file mode 100644
index 000000000000..ba58d8217851
--- /dev/null
+++ b/drivers/cec/cec-dev.c
@@ -0,0 +1,384 @@
+/*
+ * HDMI Consumer Electronics Control
+ *
+ * This provides the user API for communication with HDMI CEC complaint
+ * devices in kernel drivers, and is based upon the protocol developed
+ * by Freescale for their i.MX SoCs.
+ *
+ * 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/cec-dev.h>
+#include <linux/device.h>
+#include <linux/fs.h>
+#include <linux/module.h>
+#include <linux/poll.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+
+struct cec_event {
+ struct cec_user_event usr;
+ struct list_head node;
+};
+
+static struct class *cec_class;
+static int cec_major;
+
+static void cec_dev_send_message(struct cec_dev *cec_dev, u8 *msg,
+ size_t count)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&cec_dev->lock, flags);
+ cec_dev->retries = 5;
+ cec_dev->write_busy = 1;
+ cec_dev->send_message(cec_dev, msg, count);
+ spin_unlock_irqrestore(&cec_dev->lock, flags);
+}
+
+void cec_dev_event(struct cec_dev *cec_dev, int type, u8 *msg, size_t len)
+{
+ struct cec_event *event;
+ unsigned long flags;
+
+ event = kzalloc(sizeof(*event), GFP_ATOMIC);
+ if (event) {
+ event->usr.event_type = type;
+ event->usr.msg_len = len;
+ if (msg)
+ memcpy(event->usr.msg, msg, len);
+
+ spin_lock_irqsave(&cec_dev->lock, flags);
+ list_add_tail(&event->node, &cec_dev->events);
+ spin_unlock_irqrestore(&cec_dev->lock, flags);
+ wake_up(&cec_dev->waitq);
+ }
+}
+EXPORT_SYMBOL_GPL(cec_dev_event);
+
+static int cec_dev_lock_write(struct cec_dev *cec_dev, struct file *file)
+ __acquires(cec_dev->mutex)
+{
+ int ret;
+
+ do {
+ if (file->f_flags & O_NONBLOCK) {
+ if (cec_dev->write_busy)
+ return -EAGAIN;
+ } else {
+ ret = wait_event_interruptible(cec_dev->waitq,
+ !cec_dev->write_busy);
+ if (ret)
+ break;
+ }
+
+ ret = mutex_lock_interruptible(&cec_dev->mutex);
+ if (ret)
+ break;
+
+ if (!cec_dev->write_busy)
+ break;
+
+ mutex_unlock(&cec_dev->mutex);
+ } while (1);
+
+ return ret;
+}
+
+static ssize_t cec_dev_read(struct file *file, char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct cec_dev *cec_dev = file->private_data;
+ ssize_t ret;
+
+ if (count > sizeof(struct cec_user_event))
+ count = sizeof(struct cec_user_event);
+
+ if (!access_ok(VERIFY_WRITE, buf, count))
+ return -EFAULT;
+
+ do {
+ struct cec_event *event = NULL;
+ unsigned long flags;
+
+ spin_lock_irqsave(&cec_dev->lock, flags);
+ if (!list_empty(&cec_dev->events)) {
+ event = list_first_entry(&cec_dev->events,
+ struct cec_event, node);
+ list_del(&event->node);
+ }
+ spin_unlock_irqrestore(&cec_dev->lock, flags);
+
+ if (event) {
+ ret = __copy_to_user(buf, &event->usr, count) ?
+ -EFAULT : count;
+ kfree(event);
+ break;
+ }
+
+ if (file->f_flags & O_NONBLOCK) {
+ ret = -EAGAIN;
+ break;
+ }
+
+ ret = wait_event_interruptible(cec_dev->waitq,
+ !list_empty(&cec_dev->events));
+ if (ret)
+ break;
+ } while (1);
+
+ return ret;
+}
+
+static ssize_t cec_dev_write(struct file *file, const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct cec_dev *cec_dev = file->private_data;
+ u8 msg[MAX_MESSAGE_LEN];
+ int ret;
+
+ if (count > sizeof(msg))
+ return -E2BIG;
+
+ if (copy_from_user(msg, buf, count))
+ return -EFAULT;
+
+ ret = cec_dev_lock_write(cec_dev, file);
+ if (ret)
+ return ret;
+
+ cec_dev_send_message(cec_dev, msg, count);
+
+ mutex_unlock(&cec_dev->mutex);
+
+ return count;
+}
+
+static long cec_dev_ioctl(struct file *file, u_int cmd, unsigned long arg)
+{
+ struct cec_dev *cec_dev = file->private_data;
+ int ret;
+
+ switch (cmd) {
+ case HDMICEC_IOC_O_SETLOGICALADDRESS:
+ case HDMICEC_IOC_SETLOGICALADDRESS:
+ if (arg > 15) {
+ ret = -EINVAL;
+ break;
+ }
+
+ ret = cec_dev_lock_write(cec_dev, file);
+ if (ret == 0) {
+ unsigned char msg[1];
+
+ cec_dev->addresses = BIT(arg);
+ cec_dev->set_address(cec_dev, cec_dev->addresses);
+
+ /*
+ * Send a ping message with the source and destination
+ * set to our address; the result indicates whether
+ * unit has chosen our address simultaneously.
+ */
+ msg[0] = arg << 4 | arg;
+ cec_dev_send_message(cec_dev, msg, sizeof(msg));
+ mutex_unlock(&cec_dev->mutex);
+ }
+ break;
+
+ case HDMICEC_IOC_STARTDEVICE:
+ ret = mutex_lock_interruptible(&cec_dev->mutex);
+ if (ret == 0) {
+ cec_dev->addresses = BIT(15);
+ cec_dev->set_address(cec_dev, cec_dev->addresses);
+ mutex_unlock(&cec_dev->mutex);
+ }
+ break;
+
+ case HDMICEC_IOC_STOPDEVICE:
+ ret = 0;
+ break;
+
+ case HDMICEC_IOC_GETPHYADDRESS:
+ ret = put_user(cec_dev->physical, (u16 __user *)arg);
+ ret = -ENOIOCTLCMD;
+ break;
+
+ default:
+ ret = -ENOIOCTLCMD;
+ break;
+ }
+
+ return ret;
+}
+
+static unsigned cec_dev_poll(struct file *file, poll_table *wait)
+{
+ struct cec_dev *cec_dev = file->private_data;
+ unsigned mask = 0;
+
+ poll_wait(file, &cec_dev->waitq, wait);
+
+ if (cec_dev->write_busy == 0)
+ mask |= POLLOUT | POLLWRNORM;
+ if (!list_empty(&cec_dev->events))
+ mask |= POLLIN | POLLRDNORM;
+
+ return mask;
+}
+
+static int cec_dev_release(struct inode *inode, struct file *file)
+{
+ struct cec_dev *cec_dev = file->private_data;
+
+ mutex_lock(&cec_dev->mutex);
+ if (cec_dev->users >= 1)
+ cec_dev->users -= 1;
+ if (cec_dev->users == 0) {
+ /*
+ * Wait for any write to complete before shutting down.
+ * A message should complete in a maximum of 2.75ms *
+ * 160 bits + 4.7ms, or 444.7ms. Let's call that 500ms.
+ * If we time out, shutdown anyway.
+ */
+ wait_event_timeout(cec_dev->waitq, !cec_dev->write_busy,
+ msecs_to_jiffies(500));
+
+ cec_dev->release(cec_dev);
+
+ while (!list_empty(&cec_dev->events)) {
+ struct cec_event *event;
+
+ event = list_first_entry(&cec_dev->events,
+ struct cec_event, node);
+ list_del(&event->node);
+ kfree(event);
+ }
+ }
+ mutex_unlock(&cec_dev->mutex);
+ return 0;
+}
+
+static int cec_dev_open(struct inode *inode, struct file *file)
+{
+ struct cec_dev *cec_dev = container_of(inode->i_cdev, struct cec_dev,
+ cdev);
+ int ret = 0;
+
+ nonseekable_open(inode, file);
+
+ file->private_data = cec_dev;
+
+ ret = mutex_lock_interruptible(&cec_dev->mutex);
+ if (ret)
+ return ret;
+
+ if (cec_dev->users++ == 0) {
+ cec_dev->addresses = BIT(15);
+
+ ret = cec_dev->open(cec_dev);
+ if (ret < 0)
+ cec_dev->users = 0;
+ }
+ mutex_unlock(&cec_dev->mutex);
+
+ return ret;
+}
+
+static const struct file_operations hdmi_cec_fops = {
+ .owner = THIS_MODULE,
+ .read = cec_dev_read,
+ .write = cec_dev_write,
+ .open = cec_dev_open,
+ .unlocked_ioctl = cec_dev_ioctl,
+ .release = cec_dev_release,
+ .poll = cec_dev_poll,
+};
+
+void cec_dev_init(struct cec_dev *cec_dev, struct module *module)
+{
+ cec_dev->devn = MKDEV(cec_major, 0);
+
+ INIT_LIST_HEAD(&cec_dev->events);
+ init_waitqueue_head(&cec_dev->waitq);
+ spin_lock_init(&cec_dev->lock);
+ mutex_init(&cec_dev->mutex);
+
+ cec_dev->addresses = BIT(15);
+
+ cdev_init(&cec_dev->cdev, &hdmi_cec_fops);
+ cec_dev->cdev.owner = module;
+}
+EXPORT_SYMBOL_GPL(cec_dev_init);
+
+int cec_dev_add(struct cec_dev *cec_dev, struct device *dev, const char *name)
+{
+ struct device *cd;
+ int ret;
+
+ ret = cdev_add(&cec_dev->cdev, cec_dev->devn, 1);
+ if (ret < 0)
+ goto err_cdev;
+
+ cd = device_create(cec_class, dev, cec_dev->devn, NULL, name);
+ if (IS_ERR(cd)) {
+ ret = PTR_ERR(cd);
+ dev_err(dev, "can't create device: %d\n", ret);
+ goto err_dev;
+ }
+
+ return 0;
+
+ err_dev:
+ cdev_del(&cec_dev->cdev);
+ err_cdev:
+ return ret;
+}
+EXPORT_SYMBOL_GPL(cec_dev_add);
+
+void cec_dev_remove(struct cec_dev *cec_dev)
+{
+ device_destroy(cec_class, cec_dev->devn);
+ cdev_del(&cec_dev->cdev);
+}
+EXPORT_SYMBOL_GPL(cec_dev_remove);
+
+static int cec_init(void)
+{
+ dev_t dev;
+ int ret;
+
+ cec_class = class_create(THIS_MODULE, "hdmi-cec");
+ if (IS_ERR(cec_class)) {
+ ret = PTR_ERR(cec_class);
+ pr_err("cec: can't create cec class: %d\n", ret);
+ goto err_class;
+ }
+
+ ret = alloc_chrdev_region(&dev, 0, 1, "hdmi-cec");
+ if (ret) {
+ pr_err("cec: can't create character devices: %d\n", ret);
+ goto err_chrdev;
+ }
+
+ cec_major = MAJOR(dev);
+
+ return 0;
+
+ err_chrdev:
+ class_destroy(cec_class);
+ err_class:
+ return ret;
+}
+subsys_initcall(cec_init);
+
+static void cec_exit(void)
+{
+ unregister_chrdev_region(MKDEV(cec_major, 0), 1);
+ class_destroy(cec_class);
+}
+module_exit(cec_exit);
+
+MODULE_AUTHOR("Russell King <rmk+kernel at arm.linux.org.uk>");
+MODULE_DESCRIPTION("Generic HDMI CEC driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/cec-dev.h b/include/linux/cec-dev.h
new file mode 100644
index 000000000000..76a7d7f6a72d
--- /dev/null
+++ b/include/linux/cec-dev.h
@@ -0,0 +1,69 @@
+#ifndef _LINUX_CEC_DEV_H
+#define _LINUX_CEC_DEV_H
+
+#include <linux/cdev.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+#include <linux/wait.h>
+
+#include <uapi/linux/cec-dev.h>
+
+struct device;
+
+struct cec_dev {
+ struct cdev cdev;
+ dev_t devn;
+
+ struct mutex mutex;
+ unsigned users;
+
+ spinlock_t lock;
+ wait_queue_head_t waitq;
+ struct list_head events;
+ u8 write_busy;
+
+ u8 retries;
+ u16 addresses;
+ u16 physical;
+
+ int (*open)(struct cec_dev *);
+ void (*release)(struct cec_dev *);
+ void (*send_message)(struct cec_dev *, u8 *, size_t);
+ void (*set_address)(struct cec_dev *, unsigned);
+};
+
+void cec_dev_event(struct cec_dev *cec_dev, int type, u8 *msg, size_t len);
+
+static inline void cec_dev_receive(struct cec_dev *cec_dev, u8 *msg,
+ unsigned len)
+{
+ cec_dev_event(cec_dev, MESSAGE_TYPE_RECEIVE_SUCCESS, msg, len);
+}
+
+static inline void cec_dev_send_complete(struct cec_dev *cec_dev, int ack)
+{
+ cec_dev->retries = 0;
+ cec_dev->write_busy = 0;
+
+ cec_dev_event(cec_dev, ack ? MESSAGE_TYPE_SEND_SUCCESS :
+ MESSAGE_TYPE_NOACK, NULL, 0);
+}
+
+static inline void cec_dev_disconnect(struct cec_dev *cec_dev)
+{
+ cec_dev->physical = 0;
+ cec_dev_event(cec_dev, MESSAGE_TYPE_DISCONNECTED, NULL, 0);
+}
+
+static inline void cec_dev_connect(struct cec_dev *cec_dev, u32 phys)
+{
+ cec_dev->physical = phys;
+ cec_dev_event(cec_dev, MESSAGE_TYPE_CONNECTED, NULL, 0);
+}
+
+void cec_dev_init(struct cec_dev *cec_dev, struct module *);
+int cec_dev_add(struct cec_dev *cec_dev, struct device *, const char *name);
+void cec_dev_remove(struct cec_dev *cec_dev);
+
+#endif
diff --git a/include/uapi/linux/cec-dev.h b/include/uapi/linux/cec-dev.h
new file mode 100644
index 000000000000..fb7a41704c77
--- /dev/null
+++ b/include/uapi/linux/cec-dev.h
@@ -0,0 +1,34 @@
+#ifndef _UAPI_LINUX_CEC_DEV_H
+#define _UAPI_LINUX_CEC_DEV_H
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+#define MAX_MESSAGE_LEN 16
+
+enum {
+ HDMICEC_IOC_MAGIC = 'H',
+ /* This is wrong: we pass the argument as a number, not a pointer */
+ HDMICEC_IOC_O_SETLOGICALADDRESS = _IOW(HDMICEC_IOC_MAGIC, 1, unsigned char),
+ HDMICEC_IOC_SETLOGICALADDRESS = _IO(HDMICEC_IOC_MAGIC, 1),
+ HDMICEC_IOC_STARTDEVICE = _IO(HDMICEC_IOC_MAGIC, 2),
+ HDMICEC_IOC_STOPDEVICE = _IO(HDMICEC_IOC_MAGIC, 3),
+ HDMICEC_IOC_GETPHYADDRESS = _IOR(HDMICEC_IOC_MAGIC, 4, unsigned char[4]),
+};
+
+enum {
+ MESSAGE_TYPE_RECEIVE_SUCCESS = 1,
+ MESSAGE_TYPE_NOACK,
+ MESSAGE_TYPE_DISCONNECTED,
+ MESSAGE_TYPE_CONNECTED,
+ MESSAGE_TYPE_SEND_SUCCESS,
+ MESSAGE_TYPE_SEND_ERROR,
+};
+
+struct cec_user_event {
+ __u32 event_type;
+ __u32 msg_len;
+ __u8 msg[MAX_MESSAGE_LEN];
+};
+
+#endif
--
1.8.3.1
More information about the linux-arm-kernel
mailing list