From: Russell King Subject: [PATCH 025/107] cec: add generic HDMI CEC driver MIME-Version: 1.0 Content-Disposition: inline Content-Transfer-Encoding: 8bit Content-Type: text/plain; charset="utf-8" Add a generic userspace API to support HDMI Consumer Electronics Control interfaces. Signed-off-by: Russell King --- 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 622fa266b29e..fa2b20ba644f 100644 --- a/drivers/Kconfig +++ b/drivers/Kconfig @@ -176,6 +176,8 @@ source "drivers/powercap/Kconfig" source "drivers/mcb/Kconfig" +source "drivers/cec/Kconfig" + source "drivers/ras/Kconfig" source "drivers/thunderbolt/Kconfig" diff --git a/drivers/Makefile b/drivers/Makefile index ebee55537a05..b8208b267615 100644 --- a/drivers/Makefile +++ b/drivers/Makefile @@ -159,5 +159,6 @@ obj-$(CONFIG_NTB) += ntb/ obj-$(CONFIG_FMC) += fmc/ obj-$(CONFIG_POWERCAP) += powercap/ obj-$(CONFIG_MCB) += mcb/ +obj-$(CONFIG_CEC) += cec/ obj-$(CONFIG_RAS) += ras/ obj-$(CONFIG_THUNDERBOLT) += thunderbolt/ 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 +#include +#include +#include +#include +#include +#include + +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 "); +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 +#include +#include +#include +#include + +#include + +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 +#include + +#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