diff options
Diffstat (limited to 'drivers/media/cec')
28 files changed, 3317 insertions, 87 deletions
diff --git a/drivers/media/cec/core/cec-adap.c b/drivers/media/cec/core/cec-adap.c index 559a172ebc6c..ba6828ef540e 100644 --- a/drivers/media/cec/core/cec-adap.c +++ b/drivers/media/cec/core/cec-adap.c @@ -7,12 +7,13 @@ #include <linux/errno.h> #include <linux/init.h> -#include <linux/module.h> #include <linux/kernel.h> #include <linux/kmod.h> #include <linux/ktime.h> -#include <linux/slab.h> #include <linux/mm.h> +#include <linux/module.h> +#include <linux/seq_file.h> +#include <linux/slab.h> #include <linux/string.h> #include <linux/types.h> @@ -490,6 +491,15 @@ int cec_thread_func(void *_adap) goto unlock; } + if (adap->transmit_in_progress && + adap->transmit_in_progress_aborted) { + if (adap->transmitting) + cec_data_cancel(adap->transmitting, + CEC_TX_STATUS_ABORTED, 0); + adap->transmit_in_progress = false; + adap->transmit_in_progress_aborted = false; + goto unlock; + } if (adap->transmit_in_progress && timeout) { /* * If we timeout, then log that. Normally this does @@ -664,8 +674,9 @@ void cec_transmit_done_ts(struct cec_adapter *adap, u8 status, /* Retry this message */ data->attempts -= attempts_made; if (msg->timeout) - dprintk(2, "retransmit: %*ph (attempts: %d, wait for 0x%02x)\n", - msg->len, msg->msg, data->attempts, msg->reply); + dprintk(2, "retransmit: %*ph (attempts: %d, wait for %*ph)\n", + msg->len, msg->msg, data->attempts, + data->match_len, data->match_reply); else dprintk(2, "retransmit: %*ph (attempts: %d)\n", msg->len, msg->msg, data->attempts); @@ -771,6 +782,9 @@ int cec_transmit_msg_fh(struct cec_adapter *adap, struct cec_msg *msg, { struct cec_data *data; bool is_raw = msg_is_raw(msg); + bool reply_vendor_id = (msg->flags & CEC_MSG_FL_REPLY_VENDOR_ID) && + msg->len > 1 && msg->msg[1] == CEC_MSG_VENDOR_COMMAND_WITH_ID; + int err; if (adap->devnode.unregistered) return -ENODEV; @@ -784,12 +798,13 @@ int cec_transmit_msg_fh(struct cec_adapter *adap, struct cec_msg *msg, msg->tx_low_drive_cnt = 0; msg->tx_error_cnt = 0; msg->sequence = 0; + msg->flags &= CEC_MSG_FL_REPLY_TO_FOLLOWERS | CEC_MSG_FL_RAW | + (reply_vendor_id ? CEC_MSG_FL_REPLY_VENDOR_ID : 0); - if (msg->reply && msg->timeout == 0) { + if ((reply_vendor_id || msg->reply) && msg->timeout == 0) { /* Make sure the timeout isn't 0. */ msg->timeout = 1000; } - msg->flags &= CEC_MSG_FL_REPLY_TO_FOLLOWERS | CEC_MSG_FL_RAW; if (!msg->timeout) msg->flags &= ~CEC_MSG_FL_REPLY_TO_FOLLOWERS; @@ -799,6 +814,11 @@ int cec_transmit_msg_fh(struct cec_adapter *adap, struct cec_msg *msg, dprintk(1, "%s: invalid length %d\n", __func__, msg->len); return -EINVAL; } + if (reply_vendor_id && msg->len < 6) { + dprintk(1, "%s: <Vendor Command With ID> message too short\n", + __func__); + return -EINVAL; + } memset(msg->msg + msg->len, 0, sizeof(msg->msg) - msg->len); @@ -890,8 +910,9 @@ int cec_transmit_msg_fh(struct cec_adapter *adap, struct cec_msg *msg, __func__); return -ENONET; } - if (msg->reply) { - dprintk(1, "%s: invalid msg->reply\n", __func__); + if (reply_vendor_id || msg->reply) { + dprintk(1, "%s: adapter is unconfigured so reply is not supported\n", + __func__); return -EINVAL; } } @@ -913,6 +934,14 @@ int cec_transmit_msg_fh(struct cec_adapter *adap, struct cec_msg *msg, data->fh = fh; data->adap = adap; data->blocking = block; + if (reply_vendor_id) { + memcpy(data->match_reply, msg->msg + 1, 4); + data->match_reply[4] = msg->reply; + data->match_len = 5; + } else if (msg->timeout) { + data->match_reply[0] = msg->reply; + data->match_len = 1; + } init_completion(&data->c); INIT_DELAYED_WORK(&data->work, cec_wait_timeout); @@ -935,11 +964,13 @@ int cec_transmit_msg_fh(struct cec_adapter *adap, struct cec_msg *msg, * Release the lock and wait, retake the lock afterwards. */ mutex_unlock(&adap->lock); - wait_for_completion_killable(&data->c); - if (!data->completed) - cancel_delayed_work_sync(&data->work); + err = wait_for_completion_killable(&data->c); + cancel_delayed_work_sync(&data->work); mutex_lock(&adap->lock); + if (err) + adap->transmit_in_progress_aborted = true; + /* Cancel the transmit if it was interrupted */ if (!data->completed) { if (data->msg.tx_status & CEC_TX_STATUS_OK) @@ -1199,13 +1230,15 @@ void cec_received_msg_ts(struct cec_adapter *adap, if (!abort && dst->msg[1] == CEC_MSG_INITIATE_ARC && (cmd == CEC_MSG_REPORT_ARC_INITIATED || cmd == CEC_MSG_REPORT_ARC_TERMINATED) && - (dst->reply == CEC_MSG_REPORT_ARC_INITIATED || - dst->reply == CEC_MSG_REPORT_ARC_TERMINATED)) + (data->match_reply[0] == CEC_MSG_REPORT_ARC_INITIATED || + data->match_reply[0] == CEC_MSG_REPORT_ARC_TERMINATED)) { dst->reply = cmd; + data->match_reply[0] = cmd; + } /* Does the command match? */ if ((abort && cmd != dst->msg[1]) || - (!abort && cmd != dst->reply)) + (!abort && memcmp(data->match_reply, msg->msg + 1, data->match_len))) continue; /* Does the addressing match? */ @@ -1575,9 +1608,12 @@ unconfigure: */ static void cec_claim_log_addrs(struct cec_adapter *adap, bool block) { - if (WARN_ON(adap->is_configuring || adap->is_configured)) + if (WARN_ON(adap->is_claiming_log_addrs || + adap->is_configuring || adap->is_configured)) return; + adap->is_claiming_log_addrs = true; + init_completion(&adap->config_completion); /* Ready to kick off the thread */ @@ -1592,6 +1628,7 @@ static void cec_claim_log_addrs(struct cec_adapter *adap, bool block) wait_for_completion(&adap->config_completion); mutex_lock(&adap->lock); } + adap->is_claiming_log_addrs = false; } /* @@ -2302,18 +2339,21 @@ int cec_adap_status(struct seq_file *file, void *priv) } data = adap->transmitting; if (data) - seq_printf(file, "transmitting message: %*ph (reply: %02x, timeout: %ums)\n", - data->msg.len, data->msg.msg, data->msg.reply, + seq_printf(file, "transmitting message: %*ph (reply: %*ph, timeout: %ums)\n", + data->msg.len, data->msg.msg, + data->match_len, data->match_reply, data->msg.timeout); seq_printf(file, "pending transmits: %u\n", adap->transmit_queue_sz); list_for_each_entry(data, &adap->transmit_queue, list) { - seq_printf(file, "queued tx message: %*ph (reply: %02x, timeout: %ums)\n", - data->msg.len, data->msg.msg, data->msg.reply, + seq_printf(file, "queued tx message: %*ph (reply: %*ph, timeout: %ums)\n", + data->msg.len, data->msg.msg, + data->match_len, data->match_reply, data->msg.timeout); } list_for_each_entry(data, &adap->wait_queue, list) { - seq_printf(file, "message waiting for reply: %*ph (reply: %02x, timeout: %ums)\n", - data->msg.len, data->msg.msg, data->msg.reply, + seq_printf(file, "message waiting for reply: %*ph (reply: %*ph, timeout: %ums)\n", + data->msg.len, data->msg.msg, + data->match_len, data->match_reply, data->msg.timeout); } diff --git a/drivers/media/cec/core/cec-api.c b/drivers/media/cec/core/cec-api.c index 67dc79ef1705..2b50578d107e 100644 --- a/drivers/media/cec/core/cec-api.c +++ b/drivers/media/cec/core/cec-api.c @@ -178,7 +178,7 @@ static long cec_adap_s_log_addrs(struct cec_adapter *adap, struct cec_fh *fh, CEC_LOG_ADDRS_FL_ALLOW_RC_PASSTHRU | CEC_LOG_ADDRS_FL_CDC_ONLY; mutex_lock(&adap->lock); - if (!adap->is_configuring && + if (!adap->is_claiming_log_addrs && !adap->is_configuring && (!log_addrs.num_log_addrs || !adap->is_configured) && !cec_is_busy(adap, fh)) { err = __cec_s_log_addrs(adap, &log_addrs, block); @@ -222,7 +222,7 @@ static long cec_transmit(struct cec_adapter *adap, struct cec_fh *fh, mutex_lock(&adap->lock); if (adap->log_addrs.num_log_addrs == 0) err = -EPERM; - else if (adap->is_configuring) + else if (adap->is_configuring && !msg_is_raw(&msg)) err = -ENONET; else if (cec_is_busy(adap, fh)) err = -EBUSY; @@ -580,7 +580,7 @@ static int cec_open(struct inode *inode, struct file *filp) fh->mode_initiator = CEC_MODE_INITIATOR; fh->adap = adap; - err = cec_get_device(devnode); + err = cec_get_device(adap); if (err) { kfree(fh); return err; @@ -664,6 +664,8 @@ static int cec_release(struct inode *inode, struct file *filp) list_del_init(&data->xfer_list); } mutex_unlock(&adap->lock); + + mutex_lock(&fh->lock); while (!list_empty(&fh->msgs)) { struct cec_msg_entry *entry = list_first_entry(&fh->msgs, struct cec_msg_entry, list); @@ -681,9 +683,10 @@ static int cec_release(struct inode *inode, struct file *filp) kfree(entry); } } + mutex_unlock(&fh->lock); kfree(fh); - cec_put_device(devnode); + cec_put_device(adap); filp->private_data = NULL; return 0; } @@ -695,5 +698,4 @@ const struct file_operations cec_devnode_fops = { .compat_ioctl = cec_ioctl, .release = cec_release, .poll = cec_poll, - .llseek = no_llseek, }; diff --git a/drivers/media/cec/core/cec-core.c b/drivers/media/cec/core/cec-core.c index 5a54db839e5d..e10bd588a586 100644 --- a/drivers/media/cec/core/cec-core.c +++ b/drivers/media/cec/core/cec-core.c @@ -5,13 +5,14 @@ * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved. */ +#include <linux/debugfs.h> #include <linux/errno.h> #include <linux/init.h> -#include <linux/module.h> #include <linux/kernel.h> #include <linux/kmod.h> -#include <linux/slab.h> #include <linux/mm.h> +#include <linux/module.h> +#include <linux/slab.h> #include <linux/string.h> #include <linux/types.h> @@ -51,35 +52,6 @@ static struct dentry *top_cec_dir; /* dev to cec_devnode */ #define to_cec_devnode(cd) container_of(cd, struct cec_devnode, dev) -int cec_get_device(struct cec_devnode *devnode) -{ - /* - * Check if the cec device is available. This needs to be done with - * the devnode->lock held to prevent an open/unregister race: - * without the lock, the device could be unregistered and freed between - * the devnode->registered check and get_device() calls, leading to - * a crash. - */ - mutex_lock(&devnode->lock); - /* - * return ENXIO if the cec device has been removed - * already or if it is not registered anymore. - */ - if (!devnode->registered) { - mutex_unlock(&devnode->lock); - return -ENXIO; - } - /* and increase the device refcount */ - get_device(&devnode->dev); - mutex_unlock(&devnode->lock); - return 0; -} - -void cec_put_device(struct cec_devnode *devnode) -{ - put_device(&devnode->dev); -} - /* Called when the last user of the cec device exits. */ static void cec_devnode_release(struct device *cd) { @@ -273,7 +245,7 @@ struct cec_adapter *cec_allocate_adapter(const struct cec_adap_ops *ops, adap->cec_pin_is_high = true; adap->log_addrs.cec_version = CEC_OP_CEC_VERSION_2_0; adap->log_addrs.vendor_id = CEC_VENDOR_ID_NONE; - adap->capabilities = caps; + adap->capabilities = caps | CEC_CAP_REPLY_VENDOR_ID; if (debug_phys_addr) adap->capabilities |= CEC_CAP_PHYS_ADDR; adap->needs_hpd = caps & CEC_CAP_NEEDS_HPD; @@ -467,6 +439,6 @@ static void __exit cec_devnode_exit(void) subsys_initcall(cec_devnode_init); module_exit(cec_devnode_exit) -MODULE_AUTHOR("Hans Verkuil <hans.verkuil@cisco.com>"); +MODULE_AUTHOR("Hans Verkuil <hansverk@cisco.com>"); MODULE_DESCRIPTION("Device node registration for cec drivers"); MODULE_LICENSE("GPL"); diff --git a/drivers/media/cec/core/cec-pin-error-inj.c b/drivers/media/cec/core/cec-pin-error-inj.c index fc0968b9d40e..6e61a04b8168 100644 --- a/drivers/media/cec/core/cec-pin-error-inj.c +++ b/drivers/media/cec/core/cec-pin-error-inj.c @@ -4,8 +4,9 @@ */ #include <linux/delay.h> -#include <linux/slab.h> #include <linux/sched/types.h> +#include <linux/seq_file.h> +#include <linux/slab.h> #include <media/cec-pin.h> #include "cec-pin-priv.h" diff --git a/drivers/media/cec/core/cec-pin.c b/drivers/media/cec/core/cec-pin.c index 330d5d5d86ab..59ac12113f3a 100644 --- a/drivers/media/cec/core/cec-pin.c +++ b/drivers/media/cec/core/cec-pin.c @@ -4,8 +4,9 @@ */ #include <linux/delay.h> -#include <linux/slab.h> #include <linux/sched/types.h> +#include <linux/seq_file.h> +#include <linux/slab.h> #include <media/cec-pin.h> #include "cec-pin-priv.h" @@ -872,19 +873,19 @@ static enum hrtimer_restart cec_pin_timer(struct hrtimer *timer) if (pin->wait_usecs > 150) { pin->wait_usecs -= 100; pin->timer_ts = ktime_add_us(ts, 100); - hrtimer_forward_now(timer, ns_to_ktime(100000)); + hrtimer_forward_now(timer, us_to_ktime(100)); return HRTIMER_RESTART; } if (pin->wait_usecs > 100) { pin->wait_usecs /= 2; pin->timer_ts = ktime_add_us(ts, pin->wait_usecs); hrtimer_forward_now(timer, - ns_to_ktime(pin->wait_usecs * 1000)); + us_to_ktime(pin->wait_usecs)); return HRTIMER_RESTART; } pin->timer_ts = ktime_add_us(ts, pin->wait_usecs); hrtimer_forward_now(timer, - ns_to_ktime(pin->wait_usecs * 1000)); + us_to_ktime(pin->wait_usecs)); pin->wait_usecs = 0; return HRTIMER_RESTART; } @@ -1019,13 +1020,12 @@ static enum hrtimer_restart cec_pin_timer(struct hrtimer *timer) if (!adap->monitor_pin_cnt || usecs <= 150) { pin->wait_usecs = 0; pin->timer_ts = ktime_add_us(ts, usecs); - hrtimer_forward_now(timer, - ns_to_ktime(usecs * 1000)); + hrtimer_forward_now(timer, us_to_ktime(usecs)); return HRTIMER_RESTART; } pin->wait_usecs = usecs - 100; pin->timer_ts = ktime_add_us(ts, 100); - hrtimer_forward_now(timer, ns_to_ktime(100000)); + hrtimer_forward_now(timer, us_to_ktime(100)); return HRTIMER_RESTART; } @@ -1345,9 +1345,8 @@ struct cec_adapter *cec_pin_allocate_adapter(const struct cec_pin_ops *pin_ops, if (pin == NULL) return ERR_PTR(-ENOMEM); pin->ops = pin_ops; - hrtimer_init(&pin->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); atomic_set(&pin->work_pin_num_events, 0); - pin->timer.function = cec_pin_timer; + hrtimer_setup(&pin->timer, cec_pin_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); init_waitqueue_head(&pin->kthread_waitq); pin->tx_custom_low_usecs = CEC_TIM_CUSTOM_DEFAULT; pin->tx_custom_high_usecs = CEC_TIM_CUSTOM_DEFAULT; diff --git a/drivers/media/cec/core/cec-priv.h b/drivers/media/cec/core/cec-priv.h index ed1f8c67626b..ce42a37c4ac0 100644 --- a/drivers/media/cec/core/cec-priv.h +++ b/drivers/media/cec/core/cec-priv.h @@ -37,8 +37,6 @@ static inline bool msg_is_raw(const struct cec_msg *msg) /* cec-core.c */ extern int cec_debug; -int cec_get_device(struct cec_devnode *devnode); -void cec_put_device(struct cec_devnode *devnode); /* cec-adap.c */ int cec_monitor_all_cnt_inc(struct cec_adapter *adap); diff --git a/drivers/media/cec/i2c/Kconfig b/drivers/media/cec/i2c/Kconfig index d912d143fb31..c31abc26f602 100644 --- a/drivers/media/cec/i2c/Kconfig +++ b/drivers/media/cec/i2c/Kconfig @@ -13,3 +13,13 @@ config CEC_CH7322 generic CEC framework interface. CEC bus is present in the HDMI connector and enables communication between compatible devices. + +config CEC_NXP_TDA9950 + tristate "NXP Semiconductors TDA9950/TDA998X HDMI CEC" + depends on I2C + select CEC_NOTIFIER + select CEC_CORE + default DRM_I2C_NXP_TDA998X + help + This is a driver for the NXP TDA9950 CEC controller and for the CEC + controller block integrated into several NXP TDA998x HDMI encoders. diff --git a/drivers/media/cec/i2c/Makefile b/drivers/media/cec/i2c/Makefile index d7496dfd0fa4..95c9eda52583 100644 --- a/drivers/media/cec/i2c/Makefile +++ b/drivers/media/cec/i2c/Makefile @@ -3,3 +3,4 @@ # Makefile for the CEC I2C device drivers. # obj-$(CONFIG_CEC_CH7322) += ch7322.o +obj-$(CONFIG_CEC_NXP_TDA9950) += tda9950.o diff --git a/drivers/media/cec/i2c/tda9950.c b/drivers/media/cec/i2c/tda9950.c new file mode 100644 index 000000000000..cbff851e0c85 --- /dev/null +++ b/drivers/media/cec/i2c/tda9950.c @@ -0,0 +1,507 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * TDA9950 Consumer Electronics Control driver + * + * The NXP TDA9950 implements the HDMI Consumer Electronics Control + * interface. The host interface is similar to a mailbox: the data + * registers starting at REG_CDR0 are written to send a command to the + * internal CPU, and replies are read from these registers. + * + * As the data registers represent a mailbox, they must be accessed + * as a single I2C transaction. See the TDA9950 data sheet for details. + */ +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/platform_data/tda9950.h> +#include <linux/slab.h> +#include <drm/drm_edid.h> +#include <media/cec.h> +#include <media/cec-notifier.h> + +enum { + REG_CSR = 0x00, + CSR_BUSY = BIT(7), + CSR_INT = BIT(6), + CSR_ERR = BIT(5), + + REG_CER = 0x01, + + REG_CVR = 0x02, + + REG_CCR = 0x03, + CCR_RESET = BIT(7), + CCR_ON = BIT(6), + + REG_ACKH = 0x04, + REG_ACKL = 0x05, + + REG_CCONR = 0x06, + CCONR_ENABLE_ERROR = BIT(4), + CCONR_RETRY_MASK = 7, + + REG_CDR0 = 0x07, + + CDR1_REQ = 0x00, + CDR1_CNF = 0x01, + CDR1_IND = 0x81, + CDR1_ERR = 0x82, + CDR1_IER = 0x83, + + CDR2_CNF_SUCCESS = 0x00, + CDR2_CNF_OFF_STATE = 0x80, + CDR2_CNF_BAD_REQ = 0x81, + CDR2_CNF_CEC_ACCESS = 0x82, + CDR2_CNF_ARB_ERROR = 0x83, + CDR2_CNF_BAD_TIMING = 0x84, + CDR2_CNF_NACK_ADDR = 0x85, + CDR2_CNF_NACK_DATA = 0x86, +}; + +struct tda9950_priv { + struct i2c_client *client; + struct device *hdmi; + struct cec_adapter *adap; + struct tda9950_glue *glue; + u16 addresses; + struct cec_msg rx_msg; + struct cec_notifier *notify; + bool open; +}; + +static int tda9950_write_range(struct i2c_client *client, u8 addr, u8 *p, int cnt) +{ + struct i2c_msg msg; + u8 buf[CEC_MAX_MSG_SIZE + 3]; + int ret; + + if (WARN_ON(cnt > sizeof(buf) - 1)) + return -EINVAL; + + buf[0] = addr; + memcpy(buf + 1, p, cnt); + + msg.addr = client->addr; + msg.flags = 0; + msg.len = cnt + 1; + msg.buf = buf; + + dev_dbg(&client->dev, "wr 0x%02x: %*ph\n", addr, cnt, p); + + ret = i2c_transfer(client->adapter, &msg, 1); + if (ret < 0) + dev_err(&client->dev, "Error %d writing to cec:0x%x\n", ret, addr); + return ret < 0 ? ret : 0; +} + +static void tda9950_write(struct i2c_client *client, u8 addr, u8 val) +{ + tda9950_write_range(client, addr, &val, 1); +} + +static int tda9950_read_range(struct i2c_client *client, u8 addr, u8 *p, int cnt) +{ + struct i2c_msg msg[2]; + int ret; + + msg[0].addr = client->addr; + msg[0].flags = 0; + msg[0].len = 1; + msg[0].buf = &addr; + msg[1].addr = client->addr; + msg[1].flags = I2C_M_RD; + msg[1].len = cnt; + msg[1].buf = p; + + ret = i2c_transfer(client->adapter, msg, 2); + if (ret < 0) + dev_err(&client->dev, "Error %d reading from cec:0x%x\n", ret, addr); + + dev_dbg(&client->dev, "rd 0x%02x: %*ph\n", addr, cnt, p); + + return ret; +} + +static u8 tda9950_read(struct i2c_client *client, u8 addr) +{ + int ret; + u8 val; + + ret = tda9950_read_range(client, addr, &val, 1); + if (ret < 0) + val = 0; + + return val; +} + +static irqreturn_t tda9950_irq(int irq, void *data) +{ + struct tda9950_priv *priv = data; + unsigned int tx_status; + u8 csr, cconr, buf[19]; + u8 arb_lost_cnt, nack_cnt, err_cnt; + + if (!priv->open) + return IRQ_NONE; + + csr = tda9950_read(priv->client, REG_CSR); + if (!(csr & CSR_INT)) + return IRQ_NONE; + + cconr = tda9950_read(priv->client, REG_CCONR) & CCONR_RETRY_MASK; + + tda9950_read_range(priv->client, REG_CDR0, buf, sizeof(buf)); + + /* + * This should never happen: the data sheet says that there will + * always be a valid message if the interrupt line is asserted. + */ + if (buf[0] == 0) { + dev_warn(&priv->client->dev, "interrupt pending, but no message?\n"); + return IRQ_NONE; + } + + switch (buf[1]) { + case CDR1_CNF: /* transmit result */ + arb_lost_cnt = nack_cnt = err_cnt = 0; + switch (buf[2]) { + case CDR2_CNF_SUCCESS: + tx_status = CEC_TX_STATUS_OK; + break; + + case CDR2_CNF_ARB_ERROR: + tx_status = CEC_TX_STATUS_ARB_LOST; + arb_lost_cnt = cconr; + break; + + case CDR2_CNF_NACK_ADDR: + tx_status = CEC_TX_STATUS_NACK; + nack_cnt = cconr; + break; + + default: /* some other error, refer to TDA9950 docs */ + dev_err(&priv->client->dev, "CNF reply error 0x%02x\n", + buf[2]); + tx_status = CEC_TX_STATUS_ERROR; + err_cnt = cconr; + break; + } + /* TDA9950 executes all retries for us */ + if (tx_status != CEC_TX_STATUS_OK) + tx_status |= CEC_TX_STATUS_MAX_RETRIES; + cec_transmit_done(priv->adap, tx_status, arb_lost_cnt, + nack_cnt, 0, err_cnt); + break; + + case CDR1_IND: + priv->rx_msg.len = buf[0] - 2; + if (priv->rx_msg.len > CEC_MAX_MSG_SIZE) + priv->rx_msg.len = CEC_MAX_MSG_SIZE; + + memcpy(priv->rx_msg.msg, buf + 2, priv->rx_msg.len); + cec_received_msg(priv->adap, &priv->rx_msg); + break; + + default: /* unknown */ + dev_err(&priv->client->dev, "unknown service id 0x%02x\n", + buf[1]); + break; + } + + return IRQ_HANDLED; +} + +static int tda9950_cec_transmit(struct cec_adapter *adap, u8 attempts, + u32 signal_free_time, struct cec_msg *msg) +{ + struct tda9950_priv *priv = adap->priv; + u8 buf[CEC_MAX_MSG_SIZE + 2]; + + buf[0] = 2 + msg->len; + buf[1] = CDR1_REQ; + memcpy(buf + 2, msg->msg, msg->len); + + if (attempts > 5) + attempts = 5; + + tda9950_write(priv->client, REG_CCONR, attempts); + + return tda9950_write_range(priv->client, REG_CDR0, buf, 2 + msg->len); +} + +static int tda9950_cec_adap_log_addr(struct cec_adapter *adap, u8 addr) +{ + struct tda9950_priv *priv = adap->priv; + u16 addresses; + u8 buf[2]; + + if (addr == CEC_LOG_ADDR_INVALID) + addresses = priv->addresses = 0; + else + addresses = priv->addresses |= BIT(addr); + + /* TDA9950 doesn't want address 15 set */ + addresses &= 0x7fff; + buf[0] = addresses >> 8; + buf[1] = addresses; + + return tda9950_write_range(priv->client, REG_ACKH, buf, 2); +} + +/* + * When operating as part of the TDA998x, we need additional handling + * to initialise and shut down the TDA9950 part of the device. These + * two hooks are provided to allow the TDA998x code to perform those + * activities. + */ +static int tda9950_glue_open(struct tda9950_priv *priv) +{ + int ret = 0; + + if (priv->glue && priv->glue->open) + ret = priv->glue->open(priv->glue->data); + + priv->open = true; + + return ret; +} + +static void tda9950_glue_release(struct tda9950_priv *priv) +{ + priv->open = false; + + if (priv->glue && priv->glue->release) + priv->glue->release(priv->glue->data); +} + +static int tda9950_open(struct tda9950_priv *priv) +{ + struct i2c_client *client = priv->client; + int ret; + + ret = tda9950_glue_open(priv); + if (ret) + return ret; + + /* Reset the TDA9950, and wait 250ms for it to recover */ + tda9950_write(client, REG_CCR, CCR_RESET); + msleep(250); + + tda9950_cec_adap_log_addr(priv->adap, CEC_LOG_ADDR_INVALID); + + /* Start the command processor */ + tda9950_write(client, REG_CCR, CCR_ON); + + return 0; +} + +static void tda9950_release(struct tda9950_priv *priv) +{ + struct i2c_client *client = priv->client; + int timeout = 50; + u8 csr; + + /* Stop the command processor */ + tda9950_write(client, REG_CCR, 0); + + /* Wait up to .5s for it to signal non-busy */ + do { + csr = tda9950_read(client, REG_CSR); + if (!(csr & CSR_BUSY) || !--timeout) + break; + msleep(10); + } while (1); + + /* Warn the user that their IRQ may die if it's shared. */ + if (csr & CSR_BUSY) + dev_warn(&client->dev, "command processor failed to stop, irq%d may die (csr=0x%02x)\n", + client->irq, csr); + + tda9950_glue_release(priv); +} + +static int tda9950_cec_adap_enable(struct cec_adapter *adap, bool enable) +{ + struct tda9950_priv *priv = adap->priv; + + if (!enable) { + tda9950_release(priv); + return 0; + } else { + return tda9950_open(priv); + } +} + +static const struct cec_adap_ops tda9950_cec_ops = { + .adap_enable = tda9950_cec_adap_enable, + .adap_log_addr = tda9950_cec_adap_log_addr, + .adap_transmit = tda9950_cec_transmit, +}; + +/* + * When operating as part of the TDA998x, we need to claim additional + * resources. These two hooks permit the management of those resources. + */ +static void tda9950_devm_glue_exit(void *data) +{ + struct tda9950_glue *glue = data; + + if (glue && glue->exit) + glue->exit(glue->data); +} + +static int tda9950_devm_glue_init(struct device *dev, struct tda9950_glue *glue) +{ + int ret; + + if (glue && glue->init) { + ret = glue->init(glue->data); + if (ret) + return ret; + } + + ret = devm_add_action(dev, tda9950_devm_glue_exit, glue); + if (ret) + tda9950_devm_glue_exit(glue); + + return ret; +} + +static void tda9950_cec_del(void *data) +{ + struct tda9950_priv *priv = data; + + cec_delete_adapter(priv->adap); +} + +static int tda9950_probe(struct i2c_client *client) +{ + struct tda9950_glue *glue = client->dev.platform_data; + struct device *dev = &client->dev; + struct tda9950_priv *priv; + unsigned long irqflags; + int ret; + u8 cvr; + + /* + * We must have I2C functionality: our multi-byte accesses + * must be performed as a single contiguous transaction. + */ + if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { + dev_err(&client->dev, + "adapter does not support I2C functionality\n"); + return -ENXIO; + } + + /* We must have an interrupt to be functional. */ + if (client->irq <= 0) { + dev_err(&client->dev, "driver requires an interrupt\n"); + return -ENXIO; + } + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->client = client; + priv->glue = glue; + + i2c_set_clientdata(client, priv); + + /* + * If we're part of a TDA998x, we want the class devices to be + * associated with the HDMI Tx so we have a tight relationship + * between the HDMI interface and the CEC interface. + */ + priv->hdmi = dev; + if (glue && glue->parent) + priv->hdmi = glue->parent; + + priv->adap = cec_allocate_adapter(&tda9950_cec_ops, priv, "tda9950", + CEC_CAP_DEFAULTS | + CEC_CAP_CONNECTOR_INFO, + CEC_MAX_LOG_ADDRS); + if (IS_ERR(priv->adap)) + return PTR_ERR(priv->adap); + + ret = devm_add_action(dev, tda9950_cec_del, priv); + if (ret) { + cec_delete_adapter(priv->adap); + return ret; + } + + ret = tda9950_devm_glue_init(dev, glue); + if (ret) + return ret; + + ret = tda9950_glue_open(priv); + if (ret) + return ret; + + cvr = tda9950_read(client, REG_CVR); + + dev_info(&client->dev, + "TDA9950 CEC interface, hardware version %u.%u\n", + cvr >> 4, cvr & 15); + + tda9950_glue_release(priv); + + irqflags = IRQF_TRIGGER_FALLING; + if (glue) + irqflags = glue->irq_flags; + + ret = devm_request_threaded_irq(dev, client->irq, NULL, tda9950_irq, + irqflags | IRQF_SHARED | IRQF_ONESHOT, + dev_name(&client->dev), priv); + if (ret < 0) + return ret; + + priv->notify = cec_notifier_cec_adap_register(priv->hdmi, NULL, + priv->adap); + if (!priv->notify) + return -ENOMEM; + + ret = cec_register_adapter(priv->adap, priv->hdmi); + if (ret < 0) { + cec_notifier_cec_adap_unregister(priv->notify, priv->adap); + return ret; + } + + /* + * CEC documentation says we must not call cec_delete_adapter + * after a successful call to cec_register_adapter(). + */ + devm_remove_action(dev, tda9950_cec_del, priv); + + return 0; +} + +static void tda9950_remove(struct i2c_client *client) +{ + struct tda9950_priv *priv = i2c_get_clientdata(client); + + cec_notifier_cec_adap_unregister(priv->notify, priv->adap); + cec_unregister_adapter(priv->adap); +} + +static struct i2c_device_id tda9950_ids[] = { + { "tda9950" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, tda9950_ids); + +static struct i2c_driver tda9950_driver = { + .probe = tda9950_probe, + .remove = tda9950_remove, + .driver = { + .name = "tda9950", + }, + .id_table = tda9950_ids, +}; + +module_i2c_driver(tda9950_driver); + +MODULE_AUTHOR("Russell King <rmk+kernel@armlinux.org.uk>"); +MODULE_DESCRIPTION("TDA9950/TDA998x Consumer Electronics Control Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/media/cec/platform/Kconfig b/drivers/media/cec/platform/Kconfig index ede81fe331b0..e40413609f53 100644 --- a/drivers/media/cec/platform/Kconfig +++ b/drivers/media/cec/platform/Kconfig @@ -99,7 +99,7 @@ config CEC_TEGRA config CEC_SECO tristate "SECO Boards HDMI CEC driver" - depends on X86 || COMPILE_TEST + depends on X86 || (COMPILE_TEST && HAS_IOPORT) depends on PCI && DMI select CEC_CORE select CEC_NOTIFIER diff --git a/drivers/media/cec/platform/cec-gpio/cec-gpio.c b/drivers/media/cec/platform/cec-gpio/cec-gpio.c index 98dacb0919b6..50cdc557c943 100644 --- a/drivers/media/cec/platform/cec-gpio/cec-gpio.c +++ b/drivers/media/cec/platform/cec-gpio/cec-gpio.c @@ -3,11 +3,12 @@ * Copyright 2017 Cisco Systems, Inc. and/or its affiliates. All rights reserved. */ -#include <linux/module.h> -#include <linux/interrupt.h> #include <linux/delay.h> -#include <linux/platform_device.h> #include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/seq_file.h> #include <media/cec-notifier.h> #include <media/cec-pin.h> @@ -279,7 +280,7 @@ MODULE_DEVICE_TABLE(of, cec_gpio_match); static struct platform_driver cec_gpio_pdrv = { .probe = cec_gpio_probe, - .remove_new = cec_gpio_remove, + .remove = cec_gpio_remove, .driver = { .name = "cec-gpio", .of_match_table = cec_gpio_match, @@ -288,6 +289,6 @@ static struct platform_driver cec_gpio_pdrv = { module_platform_driver(cec_gpio_pdrv); -MODULE_AUTHOR("Hans Verkuil <hans.verkuil@cisco.com>"); +MODULE_AUTHOR("Hans Verkuil <hansverk@cisco.com>"); MODULE_LICENSE("GPL v2"); MODULE_DESCRIPTION("CEC GPIO driver"); diff --git a/drivers/media/cec/platform/cros-ec/cros-ec-cec.c b/drivers/media/cec/platform/cros-ec/cros-ec-cec.c index 48ed2993d2f0..419b9a7abcce 100644 --- a/drivers/media/cec/platform/cros-ec/cros-ec-cec.c +++ b/drivers/media/cec/platform/cros-ec/cros-ec-cec.c @@ -8,6 +8,7 @@ #include <linux/kernel.h> #include <linux/module.h> +#include <linux/mod_devicetable.h> #include <linux/platform_device.h> #include <linux/dmi.h> #include <linux/pci.h> @@ -297,6 +298,7 @@ struct cec_dmi_match { static const char *const port_b_conns[] = { "Port B", NULL }; static const char *const port_db_conns[] = { "Port D", "Port B", NULL }; static const char *const port_ba_conns[] = { "Port B", "Port A", NULL }; +static const char *const port_ab_conns[] = { "Port A", "Port B", NULL }; static const char *const port_d_conns[] = { "Port D", NULL }; static const struct cec_dmi_match cec_dmi_match_table[] = { @@ -328,6 +330,10 @@ static const struct cec_dmi_match cec_dmi_match_table[] = { { "Google", "Dexi", "0000:00:02.0", port_db_conns }, /* Google Dita */ { "Google", "Dita", "0000:00:02.0", port_db_conns }, + /* Google Dirks */ + { "Google", "Dirks", "0000:00:02.0", port_ab_conns }, + /* Google Moxie */ + { "Google", "Moxie", "0000:00:02.0", port_b_conns }, }; static struct device *cros_ec_cec_find_hdmi_dev(struct device *dev, @@ -573,13 +579,20 @@ static void cros_ec_cec_remove(struct platform_device *pdev) } } +static const struct platform_device_id cros_ec_cec_id[] = { + { DRV_NAME, 0 }, + {} +}; +MODULE_DEVICE_TABLE(platform, cros_ec_cec_id); + static struct platform_driver cros_ec_cec_driver = { .probe = cros_ec_cec_probe, - .remove_new = cros_ec_cec_remove, + .remove = cros_ec_cec_remove, .driver = { .name = DRV_NAME, .pm = &cros_ec_cec_pm_ops, }, + .id_table = cros_ec_cec_id, }; module_platform_driver(cros_ec_cec_driver); @@ -587,4 +600,3 @@ module_platform_driver(cros_ec_cec_driver); MODULE_DESCRIPTION("CEC driver for ChromeOS ECs"); MODULE_AUTHOR("Neil Armstrong <narmstrong@baylibre.com>"); MODULE_LICENSE("GPL"); -MODULE_ALIAS("platform:" DRV_NAME); diff --git a/drivers/media/cec/platform/meson/ao-cec-g12a.c b/drivers/media/cec/platform/meson/ao-cec-g12a.c index 51294b9b6cd5..41f5b8669cb0 100644 --- a/drivers/media/cec/platform/meson/ao-cec-g12a.c +++ b/drivers/media/cec/platform/meson/ao-cec-g12a.c @@ -778,7 +778,7 @@ MODULE_DEVICE_TABLE(of, meson_ao_cec_g12a_of_match); static struct platform_driver meson_ao_cec_g12a_driver = { .probe = meson_ao_cec_g12a_probe, - .remove_new = meson_ao_cec_g12a_remove, + .remove = meson_ao_cec_g12a_remove, .driver = { .name = "meson-ao-cec-g12a", .of_match_table = of_match_ptr(meson_ao_cec_g12a_of_match), diff --git a/drivers/media/cec/platform/meson/ao-cec.c b/drivers/media/cec/platform/meson/ao-cec.c index 494738daf09a..145efd9af6ac 100644 --- a/drivers/media/cec/platform/meson/ao-cec.c +++ b/drivers/media/cec/platform/meson/ao-cec.c @@ -714,7 +714,7 @@ MODULE_DEVICE_TABLE(of, meson_ao_cec_of_match); static struct platform_driver meson_ao_cec_driver = { .probe = meson_ao_cec_probe, - .remove_new = meson_ao_cec_remove, + .remove = meson_ao_cec_remove, .driver = { .name = "meson-ao-cec", .of_match_table = meson_ao_cec_of_match, diff --git a/drivers/media/cec/platform/s5p/s5p_cec.c b/drivers/media/cec/platform/s5p/s5p_cec.c index 51ab4a80aafe..4a92d3230f66 100644 --- a/drivers/media/cec/platform/s5p/s5p_cec.c +++ b/drivers/media/cec/platform/s5p/s5p_cec.c @@ -294,7 +294,7 @@ MODULE_DEVICE_TABLE(of, s5p_cec_match); static struct platform_driver s5p_cec_pdrv = { .probe = s5p_cec_probe, - .remove_new = s5p_cec_remove, + .remove = s5p_cec_remove, .driver = { .name = CEC_NAME, .of_match_table = s5p_cec_match, diff --git a/drivers/media/cec/platform/seco/seco-cec.c b/drivers/media/cec/platform/seco/seco-cec.c index 5d4c5a2cae09..b7bb49f02395 100644 --- a/drivers/media/cec/platform/seco/seco-cec.c +++ b/drivers/media/cec/platform/seco/seco-cec.c @@ -778,7 +778,7 @@ static struct platform_driver secocec_driver = { .pm = SECOCEC_PM_OPS, }, .probe = secocec_probe, - .remove_new = secocec_remove, + .remove = secocec_remove, }; module_platform_driver(secocec_driver); diff --git a/drivers/media/cec/platform/sti/stih-cec.c b/drivers/media/cec/platform/sti/stih-cec.c index a20fc5c0c88d..49843d576c7c 100644 --- a/drivers/media/cec/platform/sti/stih-cec.c +++ b/drivers/media/cec/platform/sti/stih-cec.c @@ -6,6 +6,7 @@ */ #include <linux/clk.h> #include <linux/interrupt.h> +#include <linux/io.h> #include <linux/kernel.h> #include <linux/mfd/syscon.h> #include <linux/module.h> @@ -382,7 +383,7 @@ MODULE_DEVICE_TABLE(of, stih_cec_match); static struct platform_driver stih_cec_pdrv = { .probe = stih_cec_probe, - .remove_new = stih_cec_remove, + .remove = stih_cec_remove, .driver = { .name = CEC_NAME, .of_match_table = stih_cec_match, diff --git a/drivers/media/cec/platform/stm32/stm32-cec.c b/drivers/media/cec/platform/stm32/stm32-cec.c index bda9d254041a..fea2d65acffc 100644 --- a/drivers/media/cec/platform/stm32/stm32-cec.c +++ b/drivers/media/cec/platform/stm32/stm32-cec.c @@ -361,7 +361,7 @@ MODULE_DEVICE_TABLE(of, stm32_cec_of_match); static struct platform_driver stm32_cec_driver = { .probe = stm32_cec_probe, - .remove_new = stm32_cec_remove, + .remove = stm32_cec_remove, .driver = { .name = CEC_NAME, .of_match_table = stm32_cec_of_match, diff --git a/drivers/media/cec/platform/tegra/tegra_cec.c b/drivers/media/cec/platform/tegra/tegra_cec.c index 7c1022cee1e8..3ed50097262f 100644 --- a/drivers/media/cec/platform/tegra/tegra_cec.c +++ b/drivers/media/cec/platform/tegra/tegra_cec.c @@ -465,7 +465,7 @@ static struct platform_driver tegra_cec_driver = { .of_match_table = tegra_cec_of_match, }, .probe = tegra_cec_probe, - .remove_new = tegra_cec_remove, + .remove = tegra_cec_remove, #ifdef CONFIG_PM .suspend = tegra_cec_suspend, diff --git a/drivers/media/cec/usb/Kconfig b/drivers/media/cec/usb/Kconfig index 3f3a5c75287a..6faf4742981d 100644 --- a/drivers/media/cec/usb/Kconfig +++ b/drivers/media/cec/usb/Kconfig @@ -3,6 +3,7 @@ # USB drivers if USB_SUPPORT && TTY +source "drivers/media/cec/usb/extron-da-hd-4k-plus/Kconfig" source "drivers/media/cec/usb/pulse8/Kconfig" source "drivers/media/cec/usb/rainshadow/Kconfig" endif diff --git a/drivers/media/cec/usb/Makefile b/drivers/media/cec/usb/Makefile index e4183d1bfa9a..c082679f5318 100644 --- a/drivers/media/cec/usb/Makefile +++ b/drivers/media/cec/usb/Makefile @@ -2,5 +2,6 @@ # # Makefile for the CEC USB device drivers. # +obj-$(CONFIG_USB_EXTRON_DA_HD_4K_PLUS_CEC) += extron-da-hd-4k-plus/ obj-$(CONFIG_USB_PULSE8_CEC) += pulse8/ obj-$(CONFIG_USB_RAINSHADOW_CEC) += rainshadow/ diff --git a/drivers/media/cec/usb/extron-da-hd-4k-plus/Kconfig b/drivers/media/cec/usb/extron-da-hd-4k-plus/Kconfig new file mode 100644 index 000000000000..5354f0eebe5c --- /dev/null +++ b/drivers/media/cec/usb/extron-da-hd-4k-plus/Kconfig @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0-only +config USB_EXTRON_DA_HD_4K_PLUS_CEC + tristate "Extron DA HD 4K Plus CEC driver" + depends on VIDEO_DEV + depends on USB + depends on USB_ACM + select CEC_CORE + select SERIO + select SERIO_SERPORT + help + This is a CEC driver for the Extron DA HD 4K Plus HDMI Splitter. + + To compile this driver as a module, choose M here: the + module will be called extron-da-hd-4k-plus-cec. diff --git a/drivers/media/cec/usb/extron-da-hd-4k-plus/Makefile b/drivers/media/cec/usb/extron-da-hd-4k-plus/Makefile new file mode 100644 index 000000000000..2e8f7f60263f --- /dev/null +++ b/drivers/media/cec/usb/extron-da-hd-4k-plus/Makefile @@ -0,0 +1,8 @@ +extron-da-hd-4k-plus-cec-objs := extron-da-hd-4k-plus.o cec-splitter.o +obj-$(CONFIG_USB_EXTRON_DA_HD_4K_PLUS_CEC) := extron-da-hd-4k-plus-cec.o + +all: + $(MAKE) -C $(KDIR) M=$(shell pwd) modules + +install: + $(MAKE) -C $(KDIR) M=$(shell pwd) modules_install diff --git a/drivers/media/cec/usb/extron-da-hd-4k-plus/cec-splitter.c b/drivers/media/cec/usb/extron-da-hd-4k-plus/cec-splitter.c new file mode 100644 index 000000000000..73fdec4b791d --- /dev/null +++ b/drivers/media/cec/usb/extron-da-hd-4k-plus/cec-splitter.c @@ -0,0 +1,657 @@ +// SPDX-License-Identifier: GPL-2.0-only + +/* + * Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + */ + +#include <media/cec.h> + +#include "cec-splitter.h" + +/* + * Helper function to reply to a received message with a Feature Abort + * message. + */ +static int cec_feature_abort_reason(struct cec_adapter *adap, + struct cec_msg *msg, u8 reason) +{ + struct cec_msg tx_msg = { }; + + /* + * Don't reply with CEC_MSG_FEATURE_ABORT to a CEC_MSG_FEATURE_ABORT + * message! + */ + if (msg->msg[1] == CEC_MSG_FEATURE_ABORT) + return 0; + /* Don't Feature Abort messages from 'Unregistered' */ + if (cec_msg_initiator(msg) == CEC_LOG_ADDR_UNREGISTERED) + return 0; + cec_msg_set_reply_to(&tx_msg, msg); + cec_msg_feature_abort(&tx_msg, msg->msg[1], reason); + return cec_transmit_msg(adap, &tx_msg, false); +} + +/* Transmit an Active Source message from this output port to a sink */ +static void cec_port_out_active_source(struct cec_splitter_port *p) +{ + struct cec_adapter *adap = p->adap; + struct cec_msg msg; + + if (!adap->is_configured) + return; + p->is_active_source = true; + cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); + cec_msg_active_source(&msg, adap->phys_addr); + cec_transmit_msg(adap, &msg, false); +} + +/* Transmit Active Source messages from all output ports to the sinks */ +static void cec_out_active_source(struct cec_splitter *splitter) +{ + unsigned int i; + + for (i = 0; i < splitter->num_out_ports; i++) + cec_port_out_active_source(splitter->ports[i]); +} + +/* Transmit a Standby message from this output port to a sink */ +static void cec_port_out_standby(struct cec_splitter_port *p) +{ + struct cec_adapter *adap = p->adap; + struct cec_msg msg; + + if (!adap->is_configured) + return; + cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); + cec_msg_standby(&msg); + cec_transmit_msg(adap, &msg, false); +} + +/* Transmit Standby messages from all output ports to the sinks */ +static void cec_out_standby(struct cec_splitter *splitter) +{ + unsigned int i; + + for (i = 0; i < splitter->num_out_ports; i++) + cec_port_out_standby(splitter->ports[i]); +} + +/* Transmit an Image/Text View On message from this output port to a sink */ +static void cec_port_out_wakeup(struct cec_splitter_port *p, u8 opcode) +{ + struct cec_adapter *adap = p->adap; + u8 la = adap->log_addrs.log_addr[0]; + struct cec_msg msg; + + if (la == CEC_LOG_ADDR_INVALID) + la = CEC_LOG_ADDR_UNREGISTERED; + cec_msg_init(&msg, la, 0); + msg.len = 2; + msg.msg[1] = opcode; + cec_transmit_msg(adap, &msg, false); +} + +/* Transmit Image/Text View On messages from all output ports to the sinks */ +static void cec_out_wakeup(struct cec_splitter *splitter, u8 opcode) +{ + unsigned int i; + + for (i = 0; i < splitter->num_out_ports; i++) + cec_port_out_wakeup(splitter->ports[i], opcode); +} + +/* + * Update the power state of the unconfigured CEC device to either + * Off or On depending on the current state of the splitter. + * This keeps the outputs in a consistent state. + */ +void cec_splitter_unconfigured_output(struct cec_splitter_port *p) +{ + p->video_latency = 1; + p->power_status = p->splitter->is_standby ? + CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON; + + /* The adapter was unconfigured, so clear the sequence and ts values */ + p->out_give_device_power_status_seq = 0; + p->out_give_device_power_status_ts = ktime_set(0, 0); + p->out_request_current_latency_seq = 0; + p->out_request_current_latency_ts = ktime_set(0, 0); +} + +/* + * Update the power state of the newly configured CEC device to either + * Off or On depending on the current state of the splitter. + * This keeps the outputs in a consistent state. + */ +void cec_splitter_configured_output(struct cec_splitter_port *p) +{ + p->video_latency = 1; + p->power_status = p->splitter->is_standby ? + CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON; + + if (p->splitter->is_standby) { + /* + * Some sinks only obey Standby if it comes from the + * active source. + */ + cec_port_out_active_source(p); + cec_port_out_standby(p); + } else { + cec_port_out_wakeup(p, CEC_MSG_IMAGE_VIEW_ON); + } +} + +/* Pass the in_msg on to all output ports */ +static void cec_out_passthrough(struct cec_splitter *splitter, + const struct cec_msg *in_msg) +{ + unsigned int i; + + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + struct cec_adapter *adap = p->adap; + struct cec_msg msg; + + if (!adap->is_configured) + continue; + cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); + msg.len = in_msg->len; + memcpy(msg.msg + 1, in_msg->msg + 1, msg.len - 1); + cec_transmit_msg(adap, &msg, false); + } +} + +/* + * See if all output ports received the Report Current Latency message, + * and if so, transmit the result from the input port to the video source. + */ +static void cec_out_report_current_latency(struct cec_splitter *splitter, + struct cec_adapter *input_adap) +{ + struct cec_msg reply = {}; + unsigned int reply_lat = 0; + unsigned int cnt = 0; + unsigned int i; + + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + struct cec_adapter *adap = p->adap; + + /* Skip unconfigured ports */ + if (!adap->is_configured) + continue; + /* Return if a port is still waiting for a reply */ + if (p->out_request_current_latency_seq) + return; + reply_lat += p->video_latency - 1; + cnt++; + } + + /* + * All ports that can reply, replied, so clear the sequence + * and timestamp values. + */ + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + + p->out_request_current_latency_seq = 0; + p->out_request_current_latency_ts = ktime_set(0, 0); + } + + /* + * Return if there were no replies or the input port is no longer + * configured. + */ + if (!cnt || !input_adap->is_configured) + return; + + /* Reply with the average latency */ + reply_lat = 1 + reply_lat / cnt; + cec_msg_init(&reply, input_adap->log_addrs.log_addr[0], + splitter->request_current_latency_dest); + cec_msg_report_current_latency(&reply, input_adap->phys_addr, + reply_lat, 1, 1, 1); + cec_transmit_msg(input_adap, &reply, false); +} + +/* Transmit Request Current Latency to all output ports */ +static int cec_out_request_current_latency(struct cec_splitter *splitter) +{ + ktime_t now = ktime_get(); + bool error = true; + unsigned int i; + + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + struct cec_adapter *adap = p->adap; + + if (!adap->is_configured) { + /* Clear if not configured */ + p->out_request_current_latency_seq = 0; + p->out_request_current_latency_ts = ktime_set(0, 0); + } else if (!p->out_request_current_latency_seq) { + /* + * Keep the old ts if an earlier request is still + * pending. This ensures that the request will + * eventually time out based on the timestamp of + * the first request if the sink is unresponsive. + */ + p->out_request_current_latency_ts = now; + } + } + + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + struct cec_adapter *adap = p->adap; + struct cec_msg msg; + + if (!adap->is_configured) + continue; + cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); + cec_msg_request_current_latency(&msg, true, adap->phys_addr); + if (cec_transmit_msg(adap, &msg, false)) + continue; + p->out_request_current_latency_seq = msg.sequence | (1U << 31); + error = false; + } + return error ? -ENODEV : 0; +} + +/* + * See if all output ports received the Report Power Status message, + * and if so, transmit the result from the input port to the video source. + */ +static void cec_out_report_power_status(struct cec_splitter *splitter, + struct cec_adapter *input_adap) +{ + struct cec_msg reply = {}; + /* The target power status of the splitter itself */ + u8 splitter_pwr = splitter->is_standby ? + CEC_OP_POWER_STATUS_STANDBY : CEC_OP_POWER_STATUS_ON; + /* + * The transient power status of the splitter, used if not all + * output report the target power status. + */ + u8 splitter_transient_pwr = splitter->is_standby ? + CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON; + u8 reply_pwr = splitter_pwr; + unsigned int i; + + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + + /* Skip if no sink was found (HPD was low for more than 5s) */ + if (!p->found_sink) + continue; + + /* Return if a port is still waiting for a reply */ + if (p->out_give_device_power_status_seq) + return; + if (p->power_status != splitter_pwr) + reply_pwr = splitter_transient_pwr; + } + + /* + * All ports that can reply, replied, so clear the sequence + * and timestamp values. + */ + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + + p->out_give_device_power_status_seq = 0; + p->out_give_device_power_status_ts = ktime_set(0, 0); + } + + /* Return if the input port is no longer configured. */ + if (!input_adap->is_configured) + return; + + /* Reply with the new power status */ + cec_msg_init(&reply, input_adap->log_addrs.log_addr[0], + splitter->give_device_power_status_dest); + cec_msg_report_power_status(&reply, reply_pwr); + cec_transmit_msg(input_adap, &reply, false); +} + +/* Transmit Give Device Power Status to all output ports */ +static int cec_out_give_device_power_status(struct cec_splitter *splitter) +{ + ktime_t now = ktime_get(); + bool error = true; + unsigned int i; + + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + struct cec_adapter *adap = p->adap; + + /* + * Keep the old ts if an earlier request is still + * pending. This ensures that the request will + * eventually time out based on the timestamp of + * the first request if the sink is unresponsive. + */ + if (adap->is_configured && !p->out_give_device_power_status_seq) + p->out_give_device_power_status_ts = now; + } + + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + struct cec_adapter *adap = p->adap; + struct cec_msg msg; + + if (!adap->is_configured) + continue; + + cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); + cec_msg_give_device_power_status(&msg, true); + if (cec_transmit_msg(adap, &msg, false)) + continue; + p->out_give_device_power_status_seq = msg.sequence | (1U << 31); + error = false; + } + return error ? -ENODEV : 0; +} + +/* + * CEC messages received on the HDMI input of the splitter are + * forwarded (if relevant) to the HDMI outputs of the splitter. + */ +int cec_splitter_received_input(struct cec_splitter_port *p, struct cec_msg *msg) +{ + if (!cec_msg_status_is_ok(msg)) + return 0; + + if (msg->len < 2) + return -ENOMSG; + + switch (msg->msg[1]) { + case CEC_MSG_DEVICE_VENDOR_ID: + case CEC_MSG_REPORT_POWER_STATUS: + case CEC_MSG_SET_STREAM_PATH: + case CEC_MSG_ROUTING_CHANGE: + case CEC_MSG_REQUEST_ACTIVE_SOURCE: + case CEC_MSG_SYSTEM_AUDIO_MODE_STATUS: + return 0; + + case CEC_MSG_STANDBY: + p->splitter->is_standby = true; + cec_out_standby(p->splitter); + return 0; + + case CEC_MSG_IMAGE_VIEW_ON: + case CEC_MSG_TEXT_VIEW_ON: + p->splitter->is_standby = false; + cec_out_wakeup(p->splitter, msg->msg[1]); + return 0; + + case CEC_MSG_ACTIVE_SOURCE: + cec_out_active_source(p->splitter); + return 0; + + case CEC_MSG_SET_SYSTEM_AUDIO_MODE: + cec_out_passthrough(p->splitter, msg); + return 0; + + case CEC_MSG_GIVE_DEVICE_POWER_STATUS: + p->splitter->give_device_power_status_dest = + cec_msg_initiator(msg); + if (cec_out_give_device_power_status(p->splitter)) + cec_feature_abort_reason(p->adap, msg, + CEC_OP_ABORT_INCORRECT_MODE); + return 0; + + case CEC_MSG_REQUEST_CURRENT_LATENCY: { + u16 pa; + + p->splitter->request_current_latency_dest = + cec_msg_initiator(msg); + cec_ops_request_current_latency(msg, &pa); + if (pa == p->adap->phys_addr && + cec_out_request_current_latency(p->splitter)) + cec_feature_abort_reason(p->adap, msg, + CEC_OP_ABORT_INCORRECT_MODE); + return 0; + } + + default: + return -ENOMSG; + } + return -ENOMSG; +} + +void cec_splitter_nb_transmit_canceled_output(struct cec_splitter_port *p, + const struct cec_msg *msg, + struct cec_adapter *input_adap) +{ + struct cec_splitter *splitter = p->splitter; + u32 seq = msg->sequence | (1U << 31); + + /* + * If this is the result of a failed non-blocking transmit, or it is + * the result of the failed reply to a non-blocking transmit, then + * check if the original transmit was to get the current power status + * or latency and, if so, assume that the remove device is for one + * reason or another unavailable and assume that it is in the same + * power status as the splitter, or has no video latency. + */ + if ((cec_msg_recv_is_tx_result(msg) && !(msg->tx_status & CEC_TX_STATUS_OK)) || + (cec_msg_recv_is_rx_result(msg) && !(msg->rx_status & CEC_RX_STATUS_OK))) { + u8 tx_op = msg->msg[1]; + + if (msg->len < 2) + return; + if (cec_msg_recv_is_rx_result(msg) && + (msg->rx_status & CEC_RX_STATUS_FEATURE_ABORT)) + tx_op = msg->msg[2]; + switch (tx_op) { + case CEC_MSG_GIVE_DEVICE_POWER_STATUS: + if (p->out_give_device_power_status_seq != seq) + break; + p->out_give_device_power_status_seq = 0; + p->out_give_device_power_status_ts = ktime_set(0, 0); + p->power_status = splitter->is_standby ? + CEC_OP_POWER_STATUS_STANDBY : + CEC_OP_POWER_STATUS_ON; + cec_out_report_power_status(splitter, input_adap); + break; + case CEC_MSG_REQUEST_CURRENT_LATENCY: + if (p->out_request_current_latency_seq != seq) + break; + p->video_latency = 1; + p->out_request_current_latency_seq = 0; + p->out_request_current_latency_ts = ktime_set(0, 0); + cec_out_report_current_latency(splitter, input_adap); + break; + } + return; + } + + if (cec_msg_recv_is_tx_result(msg)) { + if (p->out_request_current_latency_seq != seq) + return; + p->out_request_current_latency_ts = ns_to_ktime(msg->tx_ts); + return; + } +} + +/* + * CEC messages received on an HDMI output of the splitter + * are processed here. + */ +int cec_splitter_received_output(struct cec_splitter_port *p, struct cec_msg *msg, + struct cec_adapter *input_adap) +{ + struct cec_adapter *adap = p->adap; + struct cec_splitter *splitter = p->splitter; + u32 seq = msg->sequence | (1U << 31); + struct cec_msg reply = {}; + u16 pa; + + if (!adap->is_configured || msg->len < 2) + return -ENOMSG; + + switch (msg->msg[1]) { + case CEC_MSG_REPORT_POWER_STATUS: { + u8 pwr; + + cec_ops_report_power_status(msg, &pwr); + if (pwr > CEC_OP_POWER_STATUS_TO_STANDBY) + pwr = splitter->is_standby ? + CEC_OP_POWER_STATUS_TO_STANDBY : + CEC_OP_POWER_STATUS_TO_ON; + p->power_status = pwr; + if (p->out_give_device_power_status_seq == seq) { + p->out_give_device_power_status_seq = 0; + p->out_give_device_power_status_ts = ktime_set(0, 0); + } + cec_out_report_power_status(splitter, input_adap); + return 0; + } + + case CEC_MSG_REPORT_CURRENT_LATENCY: { + u8 video_lat; + u8 low_lat_mode; + u8 audio_out_comp; + u8 audio_out_delay; + + cec_ops_report_current_latency(msg, &pa, + &video_lat, &low_lat_mode, + &audio_out_comp, &audio_out_delay); + if (!video_lat || video_lat >= 252) + video_lat = 1; + p->video_latency = video_lat; + if (p->out_request_current_latency_seq == seq) { + p->out_request_current_latency_seq = 0; + p->out_request_current_latency_ts = ktime_set(0, 0); + } + cec_out_report_current_latency(splitter, input_adap); + return 0; + } + + case CEC_MSG_STANDBY: + case CEC_MSG_ROUTING_CHANGE: + case CEC_MSG_GIVE_SYSTEM_AUDIO_MODE_STATUS: + return 0; + + case CEC_MSG_ACTIVE_SOURCE: + cec_ops_active_source(msg, &pa); + if (pa == 0) + p->is_active_source = false; + return 0; + + case CEC_MSG_REQUEST_ACTIVE_SOURCE: + if (!p->is_active_source) + return 0; + cec_msg_set_reply_to(&reply, msg); + cec_msg_active_source(&reply, adap->phys_addr); + cec_transmit_msg(adap, &reply, false); + return 0; + + case CEC_MSG_GIVE_DEVICE_POWER_STATUS: + cec_msg_set_reply_to(&reply, msg); + cec_msg_report_power_status(&reply, splitter->is_standby ? + CEC_OP_POWER_STATUS_STANDBY : + CEC_OP_POWER_STATUS_ON); + cec_transmit_msg(adap, &reply, false); + return 0; + + case CEC_MSG_SET_STREAM_PATH: + cec_ops_set_stream_path(msg, &pa); + if (pa == adap->phys_addr) { + cec_msg_set_reply_to(&reply, msg); + cec_msg_active_source(&reply, pa); + cec_transmit_msg(adap, &reply, false); + } + return 0; + + default: + return -ENOMSG; + } + return -ENOMSG; +} + +/* + * Called every second to check for timed out messages and whether there + * still is a video sink connected or not. + * + * Returns true if sinks were lost. + */ +bool cec_splitter_poll(struct cec_splitter *splitter, + struct cec_adapter *input_adap, bool debug) +{ + ktime_t now = ktime_get(); + u8 pwr = splitter->is_standby ? + CEC_OP_POWER_STATUS_STANDBY : CEC_OP_POWER_STATUS_ON; + unsigned int max_delay_ms = input_adap->xfer_timeout_ms + 2000; + unsigned int i; + bool res = false; + + for (i = 0; i < splitter->num_out_ports; i++) { + struct cec_splitter_port *p = splitter->ports[i]; + s64 pwr_delta, lat_delta; + bool pwr_timeout, lat_timeout; + + if (!p) + continue; + + pwr_delta = ktime_ms_delta(now, p->out_give_device_power_status_ts); + pwr_timeout = p->out_give_device_power_status_seq && + pwr_delta >= max_delay_ms; + lat_delta = ktime_ms_delta(now, p->out_request_current_latency_ts); + lat_timeout = p->out_request_current_latency_seq && + lat_delta >= max_delay_ms; + + /* + * If the HPD is low for more than 5 seconds, then assume no display + * is connected. + */ + if (p->found_sink && ktime_to_ns(p->lost_sink_ts) && + ktime_ms_delta(now, p->lost_sink_ts) > 5000) { + if (debug) + dev_info(splitter->dev, + "port %u: HPD low for more than 5s, assume no sink is connected.\n", + p->port); + p->found_sink = false; + p->lost_sink_ts = ktime_set(0, 0); + res = true; + } + + /* + * If the power status request timed out, then set the port's + * power status to that of the splitter, ensuring a consistent + * power state. + */ + if (pwr_timeout) { + mutex_lock(&p->adap->lock); + if (debug) + dev_info(splitter->dev, + "port %u: give up on power status for seq %u\n", + p->port, + p->out_give_device_power_status_seq & ~(1 << 31)); + p->power_status = pwr; + p->out_give_device_power_status_seq = 0; + p->out_give_device_power_status_ts = ktime_set(0, 0); + mutex_unlock(&p->adap->lock); + cec_out_report_power_status(splitter, input_adap); + } + + /* + * If the current latency request timed out, then set the port's + * latency to 1. + */ + if (lat_timeout) { + mutex_lock(&p->adap->lock); + if (debug) + dev_info(splitter->dev, + "port %u: give up on latency for seq %u\n", + p->port, + p->out_request_current_latency_seq & ~(1 << 31)); + p->video_latency = 1; + p->out_request_current_latency_seq = 0; + p->out_request_current_latency_ts = ktime_set(0, 0); + mutex_unlock(&p->adap->lock); + cec_out_report_current_latency(splitter, input_adap); + } + } + return res; +} diff --git a/drivers/media/cec/usb/extron-da-hd-4k-plus/cec-splitter.h b/drivers/media/cec/usb/extron-da-hd-4k-plus/cec-splitter.h new file mode 100644 index 000000000000..7422f7c5719e --- /dev/null +++ b/drivers/media/cec/usb/extron-da-hd-4k-plus/cec-splitter.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +/* + * Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + */ + +#ifndef _CEC_SPLITTER_H_ +#define _CEC_SPLITTER_H_ + +struct cec_splitter; + +#define STATE_CHANGE_MAX_REPEATS 2 + +struct cec_splitter_port { + struct cec_splitter *splitter; + struct cec_adapter *adap; + unsigned int port; + bool is_active_source; + bool found_sink; + ktime_t lost_sink_ts; + u32 out_request_current_latency_seq; + ktime_t out_request_current_latency_ts; + u8 video_latency; + u32 out_give_device_power_status_seq; + ktime_t out_give_device_power_status_ts; + u8 power_status; +}; + +struct cec_splitter { + struct device *dev; + unsigned int num_out_ports; + struct cec_splitter_port **ports; + + /* High-level splitter state */ + u8 request_current_latency_dest; + u8 give_device_power_status_dest; + bool is_standby; +}; + +void cec_splitter_unconfigured_output(struct cec_splitter_port *port); +void cec_splitter_configured_output(struct cec_splitter_port *port); +int cec_splitter_received_input(struct cec_splitter_port *port, struct cec_msg *msg); +int cec_splitter_received_output(struct cec_splitter_port *port, struct cec_msg *msg, + struct cec_adapter *input_adap); +void cec_splitter_nb_transmit_canceled_output(struct cec_splitter_port *port, + const struct cec_msg *msg, + struct cec_adapter *input_adap); +bool cec_splitter_poll(struct cec_splitter *splitter, + struct cec_adapter *input_adap, bool debug); + +#endif diff --git a/drivers/media/cec/usb/extron-da-hd-4k-plus/extron-da-hd-4k-plus.c b/drivers/media/cec/usb/extron-da-hd-4k-plus/extron-da-hd-4k-plus.c new file mode 100644 index 000000000000..41d019b01ec0 --- /dev/null +++ b/drivers/media/cec/usb/extron-da-hd-4k-plus/extron-da-hd-4k-plus.c @@ -0,0 +1,1836 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + */ + +/* + * Currently this driver does not fully support the serial port of the + * Extron, only the USB port is fully supported. + * + * Issues specific to using the serial port instead of the USB since the + * serial port doesn't detect if the device is powered off: + * + * - Some periodic ping mechanism is needed to detect when the Extron is + * powered off and when it is powered on again. + * - What to do when it is powered off and the driver is modprobed? Keep + * trying to contact the Extron indefinitely? + */ + +#include <linux/completion.h> +#include <linux/ctype.h> +#include <linux/delay.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/time.h> + +#include "extron-da-hd-4k-plus.h" + +MODULE_AUTHOR("Hans Verkuil <hansverk@cisco.com>"); +MODULE_DESCRIPTION("Extron DA HD 4K PLUS HDMI CEC driver"); +MODULE_LICENSE("GPL"); + +static int debug; +module_param(debug, int, 0644); +MODULE_PARM_DESC(debug, "debug level (0-1)"); + +static unsigned int vendor_id; +module_param(vendor_id, uint, 0444); +MODULE_PARM_DESC(vendor_id, "CEC Vendor ID"); + +static char manufacturer_name[4]; +module_param_string(manufacturer_name, manufacturer_name, + sizeof(manufacturer_name), 0644); +MODULE_PARM_DESC(manufacturer_name, + "EDID Vendor String (3 uppercase characters)"); + +static bool hpd_never_low; +module_param(hpd_never_low, bool, 0644); +MODULE_PARM_DESC(hpd_never_low, "Input HPD will never go low (1), or go low if all output HPDs are low (0, default)"); + +#define EXTRON_TIMEOUT_SECS 6 + +static const u8 hdmi_edid[256] = { + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x31, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x20, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78, + 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, + 0x0f, 0x50, 0x54, 0x20, 0x00, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3a, + 0x80, 0x18, 0x71, 0x38, 0x2d, 0x40, 0x58, 0x2c, + 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, + 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x55, 0x18, + 0x87, 0x11, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x68, + 0x64, 0x6d, 0x69, 0x2d, 0x31, 0x30, 0x38, 0x30, + 0x70, 0x36, 0x30, 0x0a, 0x00, 0x00, 0x00, 0xfe, + 0x00, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x95, + + 0x02, 0x03, 0x1b, 0xf1, 0x42, 0x10, 0x01, 0x23, + 0x09, 0x07, 0x07, 0x83, 0x01, 0x00, 0x00, 0x68, + 0x03, 0x0c, 0x00, 0x10, 0x00, 0x00, 0x21, 0x01, + 0xe2, 0x00, 0xca, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, +}; + +static const u8 hdmi_edid_4k_300[256] = { + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x31, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x20, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78, + 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, + 0x0f, 0x50, 0x54, 0x20, 0x00, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3a, + 0x80, 0x18, 0x71, 0x38, 0x2d, 0x40, 0x58, 0x2c, + 0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, + 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x55, 0x18, + 0x87, 0x3c, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x68, + 0x64, 0x6d, 0x69, 0x2d, 0x34, 0x6b, 0x2d, 0x36, + 0x30, 0x30, 0x0a, 0x20, 0x00, 0x00, 0x00, 0xfe, + 0x00, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x87, + + 0x02, 0x03, 0x1f, 0xf1, 0x43, 0x10, 0x5f, 0x01, + 0x23, 0x09, 0x07, 0x07, 0x83, 0x01, 0x00, 0x00, + 0x6b, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x00, 0x3c, + 0x21, 0x00, 0x20, 0x01, 0xe2, 0x00, 0xca, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, +}; + +static const u8 hdmi_edid_4k_600[256] = { + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x31, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x20, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78, + 0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, + 0x0f, 0x50, 0x54, 0x20, 0x00, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x08, 0xe8, + 0x00, 0x30, 0xf2, 0x70, 0x5a, 0x80, 0xb0, 0x58, + 0x8a, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e, + 0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x55, 0x18, + 0x87, 0x3c, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x68, + 0x64, 0x6d, 0x69, 0x2d, 0x34, 0x6b, 0x2d, 0x36, + 0x30, 0x30, 0x0a, 0x20, 0x00, 0x00, 0x00, 0xfe, + 0x00, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x4c, + + 0x02, 0x03, 0x28, 0xf1, 0x44, 0x61, 0x5f, 0x10, + 0x01, 0x23, 0x09, 0x07, 0x07, 0x83, 0x01, 0x00, + 0x00, 0x6b, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x00, + 0x3c, 0x21, 0x00, 0x20, 0x01, 0x67, 0xd8, 0x5d, + 0xc4, 0x01, 0x78, 0x00, 0x00, 0xe2, 0x00, 0xca, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, +}; + +static int extron_send_byte(struct serio *serio, char byte) +{ + int err, i; + + for (i = 0; i < 100; i++) { + err = serio_write(serio, byte); + if (!err) + break; + usleep_range(80, 120); + } + if (err) + dev_warn(&serio->dev, "unable to write byte after 100 attempts\n"); + return err ? -EIO : 0; +} + +static int extron_send_len(struct serio *serio, const char *command, + const unsigned char *bin, unsigned int len) +{ + int err = 0; + + for (; !err && *command; command++) + err = extron_send_byte(serio, *command); + if (!err) + err = extron_send_byte(serio, '\r'); + if (bin) + for (; !err && len; len--) + err = extron_send_byte(serio, *bin++); + return err; +} + +static int extron_send_and_wait_len(struct extron *extron, struct extron_port *port, + const char *cmd, const unsigned char *bin, + unsigned int len, const char *response) +{ + int timeout = EXTRON_TIMEOUT_SECS * HZ; + int err; + + if (debug) { + if (response) + dev_info(extron->dev, "transmit %s (response: %s)\n", + cmd, response); + else + dev_info(extron->dev, "transmit %s\n", cmd); + } + + mutex_lock(&extron->serio_lock); + if (port) { + init_completion(&port->cmd_done); + port->cmd_error = 0; + port->response = response; + } else { + init_completion(&extron->cmd_done); + extron->cmd_error = 0; + extron->response = response; + } + err = extron_send_len(extron->serio, cmd, bin, len); + + if (!err && response && + !wait_for_completion_timeout(port ? &port->cmd_done : &extron->cmd_done, timeout)) { + dev_info(extron->dev, "transmit %s failed with %s (expected: %s)\n", + cmd, extron->reply, response); + err = -ETIMEDOUT; + } + + if (!err && response && (port ? port->cmd_error : extron->cmd_error)) { + dev_info(extron->dev, "transmit %s failed with E%02u (expected: %s)\n", + cmd, port ? port->cmd_error : extron->cmd_error, response); + if (port) + port->cmd_error = 0; + else + extron->cmd_error = 0; + err = -EPROTO; + } + if (port) + port->response = NULL; + else + extron->response = NULL; + mutex_unlock(&extron->serio_lock); + return err; +} + +static int extron_send_and_wait(struct extron *extron, struct extron_port *port, + const char *cmd, const char *response) +{ + return extron_send_and_wait_len(extron, port, cmd, NULL, 0, response); +} + +static void extron_parse_edid(struct extron_port *port) +{ + const u8 *edid = port->edid; + unsigned int i, end; + u8 d; + + port->has_4kp30 = false; + port->has_4kp60 = false; + port->has_qy = false; + port->has_qs = false; + /* Store Established Timings 1 and 2 */ + port->est_i = edid[0x23]; + port->est_ii = edid[0x24]; + + // Check DTDs in base block + for (i = 0; i < 4; i++) { + const u8 *dtd = edid + 0x36 + i * 18; + unsigned int w, h; + unsigned int mhz; + u64 pclk; + + if (!dtd[0] && !dtd[1]) + continue; + w = dtd[2] + ((dtd[4] & 0xf0) << 4); + h = dtd[5] + ((dtd[7] & 0xf0) << 4); + if (w != 3840 || h != 2160) + continue; + + w += dtd[3] + ((dtd[4] & 0x0f) << 8); + h += dtd[6] + ((dtd[7] & 0x0f) << 8); + pclk = dtd[0] + (dtd[1] << 8); + pclk *= 100000; + mhz = div_u64(pclk, w * h); + if (mhz >= 297) + port->has_4kp30 = true; + if (mhz >= 594) + port->has_4kp60 = true; + } + + if (port->edid_blocks == 1) + return; + + edid += 128; + + /* Return if not a CTA-861 extension block */ + if (edid[0] != 0x02 || edid[1] != 0x03) + return; + + /* search Video Data Block (tag 2) */ + d = edid[2] & 0x7f; + /* Check if there are Data Blocks */ + if (d <= 4) + return; + + i = 4; + end = d; + + do { + u8 tag = edid[i] >> 5; + u8 len = edid[i] & 0x1f; + + /* Avoid buffer overrun in case the EDID is malformed */ + if (i + len + 1 > 0x7f) + return; + + switch (tag) { + case 2: /* Video Data Block */ + /* Search for VIC 97 */ + if (memchr(edid + i + 1, 97, len)) + port->has_4kp60 = true; + /* Search for VIC 95 */ + if (memchr(edid + i + 1, 95, len)) + port->has_4kp30 = true; + break; + + case 7: /* Use Extended Tag */ + switch (edid[i + 1]) { + case 0: /* Video Capability Data Block */ + if (edid[i + 2] & 0x80) + port->has_qy = true; + if (edid[i + 2] & 0x40) + port->has_qs = true; + break; + } + break; + } + i += len + 1; + } while (i < end); +} + +static int get_edid_tag_location(const u8 *edid, unsigned int size, + u8 want_tag, u8 ext_tag) +{ + unsigned int offset = 128; + int i, end; + u8 d; + + edid += offset; + + /* Return if not a CTA-861 extension block */ + if (size < 256 || edid[0] != 0x02 || edid[1] != 0x03) + return -ENOENT; + + /* search tag */ + d = edid[0x02] & 0x7f; + if (d <= 4) + return -ENOENT; + + i = 0x04; + end = 0x00 + d; + + do { + unsigned char tag = edid[i] >> 5; + unsigned char len = edid[i] & 0x1f; + + if (tag != want_tag || i + len > end) { + i += len + 1; + continue; + } + + if (tag < 7 || (len >= 1 && edid[i + 1] == ext_tag)) + return offset + i; + i += len + 1; + } while (i < end); + return -ENOENT; +} + +static void extron_edid_crc(u8 *edid) +{ + u8 sum = 0; + int offset; + + /* Update CRC */ + for (offset = 0; offset < 127; offset++) + sum += edid[offset]; + edid[127] = 256 - sum; +} + +/* + * Fill in EDID string. As per VESA EDID-1.3, strings are at most 13 chars + * long. If shorter then add a 0x0a character after the string and pad the + * remainder with spaces. + */ +static void extron_set_edid_string(u8 *start, const char *s) +{ + const unsigned int max_len = 13; + int len = strlen(s); + + memset(start, ' ', max_len); + if (len > max_len) + len = max_len; + memcpy(start, s, len); + if (len < max_len) + start[len] = 0x0a; +} + +static void extron_update_edid(struct extron_port *port, unsigned int blocks) +{ + int offset; + u8 c1, c2; + + c1 = ((manufacturer_name[0] - '@') << 2) | + (((manufacturer_name[1] - '@') >> 3) & 0x03); + c2 = (((manufacturer_name[1] - '@') & 0x07) << 5) | + ((manufacturer_name[2] - '@') & 0x1f); + + port->edid_tmp[8] = c1; + port->edid_tmp[9] = c2; + + /* Set Established Timings, but always enable VGA */ + port->edid_tmp[0x23] = port->est_i | 0x20; + port->edid_tmp[0x24] = port->est_ii; + + /* Set the Monitor Name to the unit name */ + extron_set_edid_string(port->edid_tmp + 0x5f, port->extron->unit_name); + /* Set the ASCII String to the CEC adapter name */ + extron_set_edid_string(port->edid_tmp + 0x71, port->adap->name); + + extron_edid_crc(port->edid_tmp); + + /* Find Video Capability Data Block */ + offset = get_edid_tag_location(port->edid_tmp, blocks * 128, 7, 0); + if (offset > 0) { + port->edid_tmp[offset + 2] &= ~0xc0; + if (port->has_qy) + port->edid_tmp[offset + 2] |= 0x80; + if (port->has_qs) + port->edid_tmp[offset + 2] |= 0x40; + } + + extron_edid_crc(port->edid_tmp + 128); +} + +static int extron_write_edid(struct extron_port *port, + const u8 *edid, unsigned int blocks) +{ + struct extron *extron = port->extron; + u16 phys_addr = CEC_PHYS_ADDR_INVALID; + int ret; + + if (cec_get_edid_spa_location(edid, blocks * 128)) + phys_addr = 0; + + if (mutex_lock_interruptible(&extron->edid_lock)) + return -EINTR; + + memcpy(port->edid_tmp, edid, blocks * 128); + + if (manufacturer_name[0]) + extron_update_edid(port, blocks); + + ret = extron_send_and_wait_len(port->extron, port, "W+UF256,in.bin", + port->edid_tmp, sizeof(port->edid_tmp), + "Upl"); + if (ret) + goto unlock; + ret = extron_send_and_wait(port->extron, port, "WI1,in.binEDID", + "EdidI01"); + if (ret) + goto unlock; + + port->edid_blocks = blocks; + memcpy(port->edid, port->edid_tmp, blocks * 128); + port->read_edid = true; + mutex_unlock(&extron->edid_lock); + + cec_s_phys_addr(port->adap, phys_addr, false); + return 0; + +unlock: + mutex_unlock(&extron->edid_lock); + return ret; +} + +static void update_edid_work(struct work_struct *w) +{ + struct extron *extron = container_of(w, struct extron, + work_update_edid.work); + struct extron_port *in = extron->ports[extron->num_out_ports]; + struct extron_port *p; + bool has_edid = false; + bool has_4kp30 = true; + bool has_4kp60 = true; + bool has_qy = true; + bool has_qs = true; + u8 est_i = 0xff; + u8 est_ii = 0xff; + unsigned int out; + + for (out = 0; has_4kp60 && out < extron->num_out_ports; out++) { + p = extron->ports[out]; + if (p->read_edid) { + has_4kp60 = p->has_4kp60; + est_i &= p->est_i; + est_ii &= p->est_ii; + has_edid = true; + } + } + for (out = 0; has_4kp30 && out < extron->num_out_ports; out++) + if (extron->ports[out]->read_edid) + has_4kp30 = extron->ports[out]->has_4kp30; + + for (out = 0; has_qy && out < extron->num_out_ports; out++) + if (extron->ports[out]->read_edid) + has_qy = extron->ports[out]->has_qy; + + for (out = 0; has_qs && out < extron->num_out_ports; out++) + if (extron->ports[out]->read_edid) + has_qs = extron->ports[out]->has_qs; + + /* exit if no output port had an EDID */ + if (!has_edid) + return; + + /* exit if the input EDID properties remained unchanged */ + if (has_4kp60 == in->has_4kp60 && has_4kp30 == in->has_4kp30 && + has_qy == in->has_qy && has_qs == in->has_qs && + est_i == in->est_i && est_ii == in->est_ii) + return; + + in->has_4kp60 = has_4kp60; + in->has_4kp30 = has_4kp30; + in->has_qy = has_qy; + in->has_qs = has_qs; + in->est_i = est_i; + in->est_ii = est_ii; + extron_write_edid(extron->ports[extron->num_out_ports], + has_4kp60 ? hdmi_edid_4k_600 : + (has_4kp30 ? hdmi_edid_4k_300 : hdmi_edid), 2); +} + +static void extron_read_edid(struct extron_port *port) +{ + struct extron *extron = port->extron; + char cmd[10], reply[10]; + unsigned int idx; + + idx = port->port.port + (port->is_input ? 0 : extron->num_in_ports); + snprintf(cmd, sizeof(cmd), "WR%uEDID", idx); + snprintf(reply, sizeof(reply), "EdidR%u", idx); + if (mutex_lock_interruptible(&extron->edid_lock)) + return; + if (port->read_edid) + goto unlock; + extron->edid_bytes_read = 0; + extron->edid_port = port; + port->edid_blocks = 0; + if (!port->has_edid) + goto no_edid; + + extron->edid_reading = true; + + if (!extron_send_and_wait(extron, port, cmd, reply)) + wait_for_completion_killable_timeout(&extron->edid_completion, + msecs_to_jiffies(1000)); + if (port->edid_blocks) { + extron_parse_edid(port); + port->read_edid = true; + if (!port->is_input) + v4l2_ctrl_s_ctrl(port->ctrl_tx_edid_present, 1); + } +no_edid: + extron->edid_reading = false; +unlock: + mutex_unlock(&extron->edid_lock); + cancel_delayed_work_sync(&extron->work_update_edid); + if (manufacturer_name[0]) + schedule_delayed_work(&extron->work_update_edid, + msecs_to_jiffies(1000)); +} + +static void extron_irq_work_handler(struct work_struct *work) +{ + struct extron_port *port = + container_of(work, struct extron_port, irq_work); + struct extron *extron = port->extron; + unsigned long flags; + bool update_pa; + u16 pa; + bool update_has_signal; + bool has_signal; + bool update_has_edid; + bool has_edid; + u32 status; + + spin_lock_irqsave(&port->msg_lock, flags); + while (port->rx_msg_num) { + spin_unlock_irqrestore(&port->msg_lock, flags); + cec_received_msg(port->adap, + &port->rx_msg[port->rx_msg_cur_idx]); + spin_lock_irqsave(&port->msg_lock, flags); + if (port->rx_msg_num) + port->rx_msg_num--; + port->rx_msg_cur_idx = + (port->rx_msg_cur_idx + 1) % NUM_MSGS; + } + update_pa = port->update_phys_addr; + pa = port->phys_addr; + port->update_phys_addr = false; + update_has_signal = port->update_has_signal; + has_signal = port->has_signal; + port->update_has_signal = false; + update_has_edid = port->update_has_edid; + has_edid = port->has_edid; + port->update_has_edid = false; + status = port->tx_done_status; + port->tx_done_status = 0; + spin_unlock_irqrestore(&port->msg_lock, flags); + + if (status) + cec_transmit_done(port->adap, status, 0, 0, 0, 0); + + if (update_has_signal && port->is_input) + v4l2_ctrl_s_ctrl(port->ctrl_rx_power_present, has_signal); + + if (update_has_edid && !port->is_input) { + v4l2_ctrl_s_ctrl(port->ctrl_tx_hotplug, + port->has_edid); + if (port->has_edid) { + port->port.found_sink = true; + port->port.lost_sink_ts = ktime_set(0, 0); + } else { + port->port.lost_sink_ts = ktime_get(); + } + if (!has_edid) { + port->edid_blocks = 0; + port->read_edid = false; + if (extron->edid_reading && !has_edid && + extron->edid_port == port) + extron->edid_reading = false; + v4l2_ctrl_s_ctrl(port->ctrl_tx_edid_present, 0); + } else if (!extron->edid_reading || extron->edid_port != port) { + extron_read_edid(port); + } + } + if (update_pa) + cec_s_phys_addr(port->adap, pa, false); +} + +static void extron_process_received(struct extron_port *port, const char *data) +{ + struct cec_msg msg = {}; + unsigned int len = strlen(data); + unsigned long irq_flags; + unsigned int idx; + + if (!port || port->disconnected) + return; + + if (len < 5 || (len - 2) % 3 || data[len - 2] != '*') + goto malformed; + + while (*data != '*') { + int v = hex2bin(&msg.msg[msg.len], data + 1, 1); + + if (*data != '%' || v) + goto malformed; + msg.len++; + data += 3; + } + + spin_lock_irqsave(&port->msg_lock, irq_flags); + idx = (port->rx_msg_cur_idx + port->rx_msg_num) % + NUM_MSGS; + if (port->rx_msg_num == NUM_MSGS) { + dev_warn(port->dev, + "message queue is full, dropping %*ph\n", + msg.len, msg.msg); + spin_unlock_irqrestore(&port->msg_lock, + irq_flags); + return; + } + port->rx_msg_num++; + port->rx_msg[idx] = msg; + spin_unlock_irqrestore(&port->msg_lock, irq_flags); + if (!port->disconnected) + schedule_work(&port->irq_work); + return; + +malformed: + dev_info(port->extron->dev, "malformed msg received: '%s'\n", data); +} + +static void extron_port_signal_change(struct extron_port *port, bool has_sig) +{ + unsigned long irq_flags; + bool update = false; + + if (!port) + return; + + spin_lock_irqsave(&port->msg_lock, irq_flags); + if (!port->update_has_signal && port->has_signal != has_sig) { + port->update_has_signal = true; + update = true; + } + port->has_signal = has_sig; + spin_unlock_irqrestore(&port->msg_lock, irq_flags); + if (update && !port->disconnected) + schedule_work(&port->irq_work); +} + +static void extron_process_signal_change(struct extron *extron, const char *data) +{ + unsigned int i; + + extron_port_signal_change(extron->ports[extron->num_out_ports], + data[0] == '1'); + for (i = 0; i < extron->num_out_ports; i++) + extron_port_signal_change(extron->ports[i], + data[2 + 2 * i] != '0'); +} + +static void extron_port_edid_change(struct extron_port *port, bool has_edid) +{ + unsigned long irq_flags; + bool update = false; + + if (!port) + return; + + spin_lock_irqsave(&port->msg_lock, irq_flags); + if (!port->update_has_edid && port->has_edid != has_edid) { + port->update_has_edid = true; + update = true; + } + port->has_edid = has_edid; + spin_unlock_irqrestore(&port->msg_lock, irq_flags); + if (update && !port->disconnected) + schedule_work(&port->irq_work); +} + +static void extron_process_edid_change(struct extron *extron, const char *data) +{ + unsigned int i; + + /* + * Do nothing if the Extron isn't ready yet. Trying to do this + * while the Extron firmware is still settling will fail. + */ + if (!extron->is_ready) + return; + + for (i = 0; i < extron->num_out_ports; i++) + extron_port_edid_change(extron->ports[i], + data[2 + 2 * i] != '0'); +} + +static void extron_phys_addr_change(struct extron_port *port, u16 pa) +{ + unsigned long irq_flags; + bool update = false; + + if (!port) + return; + + spin_lock_irqsave(&port->msg_lock, irq_flags); + if (!port->update_phys_addr && port->phys_addr != pa) { + update = true; + port->update_phys_addr = true; + } + port->phys_addr = pa; + spin_unlock_irqrestore(&port->msg_lock, irq_flags); + if (update && !port->disconnected) + schedule_work(&port->irq_work); +} + +static void extron_process_tx_done(struct extron_port *port, char status) +{ + unsigned long irq_flags; + unsigned int tx_status; + + if (!port) + return; + + switch (status) { + case '0': + tx_status = CEC_TX_STATUS_NACK | CEC_TX_STATUS_MAX_RETRIES; + break; + case '1': + tx_status = CEC_TX_STATUS_OK; + break; + default: + tx_status = CEC_TX_STATUS_ERROR; + break; + } + spin_lock_irqsave(&port->msg_lock, irq_flags); + port->tx_done_status = tx_status; + spin_unlock_irqrestore(&port->msg_lock, irq_flags); + if (!port->disconnected) + schedule_work(&port->irq_work); +} + +static void extron_add_edid(struct extron_port *port, const char *hex) +{ + struct extron *extron = port ? port->extron : NULL; + + if (!port || port != extron->edid_port) + return; + while (extron->edid_bytes_read < sizeof(port->edid) && *hex) { + int err = hex2bin(&port->edid[extron->edid_bytes_read], hex, 1); + + if (err) { + extron->edid_reading = false; + complete(&extron->edid_completion); + break; + } + extron->edid_bytes_read++; + hex += 2; + } + if (extron->edid_bytes_read == 128 && + port->edid[126] == 0) { + /* There are no extension blocks, we're done */ + port->edid_blocks = 1; + extron->edid_reading = false; + complete(&extron->edid_completion); + } + if (extron->edid_bytes_read < sizeof(port->edid)) + return; + if (!*hex) + port->edid_blocks = 2; + extron->edid_reading = false; + complete(&extron->edid_completion); +} + +static irqreturn_t extron_interrupt(struct serio *serio, unsigned char data, + unsigned int flags) +{ + struct extron *extron = serio_get_drvdata(serio); + struct extron_port *port = NULL; + bool found_response; + unsigned int p; + + if (data == '\r' || data == '\n') { + if (extron->idx == 0) + return IRQ_HANDLED; + memcpy(extron->data, extron->buf, extron->idx); + extron->len = extron->idx; + extron->data[extron->len] = 0; + if (debug) + dev_info(extron->dev, "received %s\n", extron->data); + extron->idx = 0; + if (!memcmp(extron->data, "Sig", 3) && + extron->data[4] == '*') { + extron_process_signal_change(extron, extron->data + 3); + } else if (!memcmp(extron->data, "Hdcp", 4) && + extron->data[5] == '*') { + extron_process_edid_change(extron, extron->data + 4); + } else if (!memcmp(extron->data, "DcecI", 5) && + extron->data[5] >= '1' && + extron->data[5] < '1' + extron->num_in_ports) { + unsigned int p = extron->data[5] - '1'; + + p += extron->num_out_ports; + extron_process_tx_done(extron->ports[p], + extron->data[extron->len - 1]); + } else if (!memcmp(extron->data, "Ceci", 4) && + extron->data[4] >= '1' && + extron->data[4] < '1' + extron->num_in_ports && + extron->data[5] == '*') { + unsigned int p = extron->data[4] - '1'; + + p += extron->num_out_ports; + extron_process_received(extron->ports[p], + extron->data + 6); + } else if (!memcmp(extron->data, "DcecO", 5) && + extron->data[5] >= '1' && + extron->data[5] < '1' + extron->num_out_ports) { + unsigned int p = extron->data[5] - '1'; + + extron_process_tx_done(extron->ports[p], + extron->data[extron->len - 1]); + } else if (!memcmp(extron->data, "Ceco", 4) && + extron->data[4] >= '1' && + extron->data[4] < '1' + extron->num_out_ports && + extron->data[5] == '*') { + unsigned int p = extron->data[4] - '1'; + + extron_process_received(extron->ports[p], + extron->data + 6); + } else if (!memcmp(extron->data, "Pceco", 5) && + extron->data[5] >= '1' && + extron->data[5] < '1' + extron->num_out_ports) { + unsigned int p = extron->data[5] - '1'; + unsigned int tmp_pa[2] = { 0xff, 0xff }; + + if (sscanf(extron->data + 7, "%%%02x%%%02x", + &tmp_pa[0], &tmp_pa[1]) == 2) + extron_phys_addr_change(extron->ports[p], + tmp_pa[0] << 8 | tmp_pa[1]); + } else if (!memcmp(extron->data, "Pceci", 5) && + extron->data[5] >= '1' && + extron->data[5] < '1' + extron->num_in_ports) { + unsigned int p = extron->data[5] - '1'; + unsigned int tmp_pa[2] = { 0xff, 0xff }; + + p += extron->num_out_ports; + if (sscanf(extron->data + 7, "%%%02x%%%02x", + &tmp_pa[0], &tmp_pa[1]) == 2) + extron_phys_addr_change(extron->ports[p], + tmp_pa[0] << 8 | tmp_pa[1]); + } else if (!memcmp(extron->data, "EdidR", 5) && + extron->data[5] >= '1' && + extron->data[5] < '1' + extron->num_ports && + extron->data[6] == '*') { + unsigned int p = extron->data[5] - '1'; + + if (p) + p--; + else + p = extron->num_out_ports; + extron_add_edid(extron->ports[p], extron->data + 7); + } else if (extron->edid_reading && extron->len == 32 && + extron->edid_port) { + extron_add_edid(extron->edid_port, extron->data); + } + + found_response = false; + if (extron->response && + !strncmp(extron->response, extron->data, + strlen(extron->response))) + found_response = true; + + for (p = 0; !found_response && p < extron->num_ports; p++) { + port = extron->ports[p]; + if (port && port->response && + !strncmp(port->response, extron->data, + strlen(port->response))) + found_response = true; + } + + if (!found_response && extron->response && + extron->data[0] == 'E' && + isdigit(extron->data[1]) && + isdigit(extron->data[2]) && + !extron->data[3]) { + extron->cmd_error = (extron->data[1] - '0') * 10 + + extron->data[2] - '0'; + extron->response = NULL; + complete(&extron->cmd_done); + } + + if (!found_response) + return IRQ_HANDLED; + + memcpy(extron->reply, extron->data, extron->len); + extron->reply[extron->len] = 0; + if (!port) { + extron->response = NULL; + complete(&extron->cmd_done); + } else { + port->response = NULL; + complete(&port->cmd_done); + } + return IRQ_HANDLED; + } + + if (extron->idx >= DATA_SIZE - 1) { + dev_info(extron->dev, + "throwing away %d bytes of garbage\n", extron->idx); + extron->idx = 0; + } + extron->buf[extron->idx++] = (char)data; + return IRQ_HANDLED; +} + +static int extron_cec_adap_enable(struct cec_adapter *adap, bool enable) +{ + struct extron_port *port = cec_get_drvdata(adap); + + return (port->disconnected && enable) ? -ENODEV : 0; +} + +static int extron_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr) +{ + struct extron_port *port = cec_get_drvdata(adap); + char cmd[26]; + char resp[25]; + u8 la = log_addr == CEC_LOG_ADDR_INVALID ? 15 : log_addr; + int err; + + if (port->disconnected) + return -ENODEV; + snprintf(cmd, sizeof(cmd), "W%c%u*%uLCEC", + port->direction, port->port.port, la); + snprintf(resp, sizeof(resp), "Lcec%c%u*%u", + port->direction, port->port.port, la); + err = extron_send_and_wait(port->extron, port, cmd, resp); + return log_addr != CEC_LOG_ADDR_INVALID && err ? err : 0; +} + +static int extron_cec_adap_transmit(struct cec_adapter *adap, u8 attempts, + u32 signal_free_time, struct cec_msg *msg) +{ + struct extron_port *port = cec_get_drvdata(adap); + char buf[(CEC_MAX_MSG_SIZE - 1) * 3 + 1]; + char cmd[sizeof(buf) + 14]; + unsigned int i; + + if (port->disconnected) + return -ENODEV; + buf[0] = 0; + for (i = 0; i < msg->len - 1; i++) + sprintf(buf + i * 3, "%%%02X", msg->msg[i + 1]); + snprintf(cmd, sizeof(cmd), "W%c%u*%u*%u*%sDCEC", + port->direction, port->port.port, + cec_msg_initiator(msg), cec_msg_destination(msg), buf); + return extron_send_and_wait(port->extron, port, cmd, NULL); +} + +static void extron_cec_adap_unconfigured(struct cec_adapter *adap) +{ + struct extron_port *port = cec_get_drvdata(adap); + + if (port->disconnected) + return; + if (debug) + dev_info(port->extron->dev, "unconfigured port %d (%s)\n", + port->port.port, + port->extron->splitter.is_standby ? "Off" : "On"); + if (!port->is_input) + cec_splitter_unconfigured_output(&port->port); +} + +static void extron_cec_configured(struct cec_adapter *adap) +{ + struct extron_port *port = cec_get_drvdata(adap); + + if (port->disconnected) + return; + if (debug) + dev_info(port->extron->dev, "configured port %d (%s)\n", + port->port.port, + port->extron->splitter.is_standby ? "Off" : "On"); + if (!port->is_input) + cec_splitter_configured_output(&port->port); +} + +static void extron_cec_adap_nb_transmit_canceled(struct cec_adapter *adap, + const struct cec_msg *msg) +{ + struct extron_port *port = cec_get_drvdata(adap); + struct cec_adapter *input_adap; + + if (!vendor_id) + return; + if (port->disconnected || port->is_input) + return; + input_adap = port->extron->ports[port->extron->num_out_ports]->adap; + cec_splitter_nb_transmit_canceled_output(&port->port, msg, input_adap); +} + +static int extron_received(struct cec_adapter *adap, struct cec_msg *msg) +{ + struct extron_port *port = cec_get_drvdata(adap); + + if (!vendor_id) + return -ENOMSG; + if (port->disconnected) + return -ENOMSG; + if (port->is_input) + return cec_splitter_received_input(&port->port, msg); + return cec_splitter_received_output(&port->port, msg, + port->extron->ports[port->extron->num_out_ports]->adap); +} + +#define log_printf(adap, file, fmt, arg...) \ + do { \ + if (file) \ + seq_printf((file), fmt, ## arg); \ + else \ + pr_info("cec-%s: " fmt, (adap)->name, ## arg); \ + } while (0) + +static const char * const pwr_state[] = { + "on", + "standby", + "to on", + "to standby", +}; + +static void extron_adap_status_port(struct extron_port *port, struct seq_file *file) +{ + struct cec_adapter *adap = port->adap; + + if (port->disconnected) { + log_printf(adap, file, + "\tport %u: disconnected\n", port->port.port); + return; + } + if (port->is_input) + log_printf(adap, file, + "\tport %u: %s signal, %s edid, %s 4kp30, %s 4kp60, %sQS/%sQY, is %s\n", + port->port.port, + port->has_signal ? "has" : "no", + port->has_edid ? "has" : "no", + port->has_4kp30 ? "has" : "no", + port->has_4kp60 ? "has" : "no", + port->has_qs ? "" : "no ", + port->has_qy ? "" : "no ", + !port->port.adap->is_configured ? "not configured" : + pwr_state[port->extron->splitter.is_standby]); + else + log_printf(adap, file, + "\tport %u: %s sink, %s signal, %s edid, %s 4kp30, %s 4kp60, %sQS/%sQY, is %sactive source, is %s\n", + port->port.port, + port->port.found_sink ? "found" : "no", + port->has_signal ? "has" : "no", + port->has_edid ? "has" : "no", + port->has_4kp30 ? "has" : "no", + port->has_4kp60 ? "has" : "no", + port->has_qs ? "" : "no ", + port->has_qy ? "" : "no ", + port->port.is_active_source ? "" : "not ", + !port->port.adap->is_configured ? "not configured" : + pwr_state[port->port.power_status & 3]); + if (port->port.out_give_device_power_status_seq) + log_printf(adap, file, + "\tport %u: querying power status (%u, %lldms)\n", + port->port.port, + port->port.out_give_device_power_status_seq & ~(1 << 31), + ktime_ms_delta(ktime_get(), + port->port.out_give_device_power_status_ts)); + if (port->port.out_request_current_latency_seq) + log_printf(adap, file, + "\tport %u: querying latency (%u, %lldms)\n", + port->port.port, + port->port.out_request_current_latency_seq & ~(1 << 31), + ktime_ms_delta(ktime_get(), + port->port.out_request_current_latency_ts)); +} + +static void extron_adap_status(struct cec_adapter *adap, struct seq_file *file) +{ + struct extron_port *port = cec_get_drvdata(adap); + struct extron *extron = port->extron; + unsigned int i; + + log_printf(adap, file, "name: %s type: %s\n", + extron->unit_name, extron->unit_type); + log_printf(adap, file, "model: 60-160%c-01 (1 input, %u outputs)\n", + '6' + extron->num_out_ports / 2, extron->num_out_ports); + log_printf(adap, file, "firmware version: %s CEC engine version: %s\n", + extron->unit_fw_version, extron->unit_cec_engine_version); + if (extron->hpd_never_low) + log_printf(adap, file, "always keep input HPD high\n"); + else + log_printf(adap, file, + "pull input HPD low if all output HPDs are low\n"); + if (vendor_id) + log_printf(adap, file, + "splitter vendor ID: 0x%06x\n", vendor_id); + if (manufacturer_name[0]) + log_printf(adap, file, "splitter manufacturer name: %s\n", + manufacturer_name); + log_printf(adap, file, "splitter power status: %s\n", + pwr_state[extron->splitter.is_standby]); + log_printf(adap, file, "%s port: %d (%s)\n", + port->is_input ? "input" : "output", + port->port.port, port->name); + log_printf(adap, file, "splitter input port:\n"); + extron_adap_status_port(extron->ports[extron->num_out_ports], file); + + log_printf(adap, file, "splitter output ports:\n"); + for (i = 0; i < extron->num_out_ports; i++) + extron_adap_status_port(extron->ports[i], file); + + if (!port->has_edid || !port->read_edid) + return; + + for (i = 0; i < port->edid_blocks * 128; i += 16) { + if (i % 128 == 0) + log_printf(adap, file, "\n"); + log_printf(adap, file, "EDID: %*ph\n", 16, port->edid + i); + } +} + +static const struct cec_adap_ops extron_cec_adap_ops = { + .adap_enable = extron_cec_adap_enable, + .adap_log_addr = extron_cec_adap_log_addr, + .adap_transmit = extron_cec_adap_transmit, + .adap_nb_transmit_canceled = extron_cec_adap_nb_transmit_canceled, + .adap_unconfigured = extron_cec_adap_unconfigured, + .adap_status = extron_adap_status, + .configured = extron_cec_configured, + .received = extron_received, +}; + +static int extron_querycap(struct file *file, void *priv, + struct v4l2_capability *cap) +{ + struct extron_port *port = video_drvdata(file); + + strscpy(cap->driver, "extron-da-hd-4k-plus-cec", sizeof(cap->driver)); + strscpy(cap->card, cap->driver, sizeof(cap->card)); + snprintf(cap->bus_info, sizeof(cap->bus_info), "serio:%s", port->name); + return 0; +} + +static int extron_enum_input(struct file *file, void *priv, struct v4l2_input *inp) +{ + struct extron_port *port = video_drvdata(file); + + if (inp->index) + return -EINVAL; + inp->type = V4L2_INPUT_TYPE_CAMERA; + snprintf(inp->name, sizeof(inp->name), "HDMI IN %u", port->port.port); + inp->status = v4l2_ctrl_g_ctrl(port->ctrl_rx_power_present) ? + 0 : V4L2_IN_ST_NO_SIGNAL; + return 0; +} + +static int extron_g_input(struct file *file, void *priv, unsigned int *i) +{ + *i = 0; + return 0; +} + +static int extron_s_input(struct file *file, void *priv, unsigned int i) +{ + return i ? -EINVAL : 0; +} + +static int extron_enum_output(struct file *file, void *priv, struct v4l2_output *out) +{ + struct extron_port *port = video_drvdata(file); + + if (out->index) + return -EINVAL; + out->type = V4L2_OUTPUT_TYPE_ANALOG; + snprintf(out->name, sizeof(out->name), "HDMI OUT %u", port->port.port); + return 0; +} + +static int extron_g_output(struct file *file, void *priv, unsigned int *o) +{ + *o = 0; + return 0; +} + +static int extron_s_output(struct file *file, void *priv, unsigned int o) +{ + return o ? -EINVAL : 0; +} + +static int extron_g_edid(struct file *file, void *_fh, + struct v4l2_edid *edid) +{ + struct extron_port *port = video_drvdata(file); + + memset(edid->reserved, 0, sizeof(edid->reserved)); + if (port->disconnected) + return -ENODEV; + if (edid->pad) + return -EINVAL; + if (!port->has_edid) + return -ENODATA; + if (!port->read_edid) + extron_read_edid(port); + if (!port->read_edid) + return -ENODATA; + if (edid->start_block == 0 && edid->blocks == 0) { + edid->blocks = port->edid_blocks; + return 0; + } + if (edid->start_block >= port->edid_blocks) + return -EINVAL; + if (edid->blocks > port->edid_blocks - edid->start_block) + edid->blocks = port->edid_blocks - edid->start_block; + memcpy(edid->edid, port->edid + edid->start_block * 128, edid->blocks * 128); + return 0; +} + +static int extron_s_edid(struct file *file, void *_fh, struct v4l2_edid *edid) +{ + struct extron_port *port = video_drvdata(file); + + memset(edid->reserved, 0, sizeof(edid->reserved)); + if (port->disconnected) + return -ENODEV; + if (edid->pad) + return -EINVAL; + + /* Unfortunately it is not possible to clear the EDID */ + if (edid->blocks == 0) + return -EINVAL; + + if (edid->blocks > MAX_EDID_BLOCKS) { + edid->blocks = MAX_EDID_BLOCKS; + return -E2BIG; + } + + if (cec_get_edid_spa_location(edid->edid, edid->blocks * 128)) + v4l2_set_edid_phys_addr(edid->edid, edid->blocks * 128, 0); + extron_parse_edid(port); + return extron_write_edid(port, edid->edid, edid->blocks); +} + +static int extron_log_status(struct file *file, void *priv) +{ + struct extron_port *port = video_drvdata(file); + + extron_adap_status(port->adap, NULL); + return v4l2_ctrl_log_status(file, priv); +} + +static const struct v4l2_ioctl_ops extron_ioctl_ops = { + .vidioc_querycap = extron_querycap, + .vidioc_enum_input = extron_enum_input, + .vidioc_g_input = extron_g_input, + .vidioc_s_input = extron_s_input, + .vidioc_enum_output = extron_enum_output, + .vidioc_g_output = extron_g_output, + .vidioc_s_output = extron_s_output, + .vidioc_g_edid = extron_g_edid, + .vidioc_s_edid = extron_s_edid, + .vidioc_log_status = extron_log_status, + .vidioc_subscribe_event = v4l2_ctrl_subscribe_event, + .vidioc_unsubscribe_event = v4l2_event_unsubscribe, +}; + +static const struct v4l2_file_operations extron_fops = { + .owner = THIS_MODULE, + .open = v4l2_fh_open, + .release = v4l2_fh_release, + .poll = v4l2_ctrl_poll, + .unlocked_ioctl = video_ioctl2, +}; + +static const struct video_device extron_videodev = { + .name = "extron-da-hd-4k-plus-cec", + .vfl_dir = VFL_DIR_RX, + .fops = &extron_fops, + .ioctl_ops = &extron_ioctl_ops, + .minor = -1, + .release = video_device_release_empty, +}; + +static void extron_disconnect(struct serio *serio) +{ + struct extron *extron = serio_get_drvdata(serio); + unsigned int p; + + kthread_stop(extron->kthread_setup); + + for (p = 0; p < extron->num_ports; p++) { + struct extron_port *port = extron->ports[p]; + + if (!port) + continue; + port->disconnected = true; + cancel_work_sync(&port->irq_work); + } + cancel_delayed_work_sync(&extron->work_update_edid); + for (p = 0; p < extron->num_ports; p++) { + struct extron_port *port = extron->ports[p]; + + if (!port) + continue; + + if (port->cec_was_registered) { + if (cec_is_registered(port->adap)) + cec_unregister_adapter(port->adap); + /* + * After registering the adapter, the + * extron_setup_thread() function took an extra + * reference to the device. We call the corresponding + * put here. + */ + cec_put_device(port->adap); + } else { + cec_delete_adapter(port->adap); + } + video_unregister_device(&port->vdev); + } + + complete(&extron->edid_completion); + + for (p = 0; p < extron->num_ports; p++) { + struct extron_port *port = extron->ports[p]; + + if (!port) + continue; + v4l2_ctrl_handler_free(&port->hdl); + mutex_destroy(&port->video_lock); + kfree(port); + } + mutex_destroy(&extron->edid_lock); + mutex_destroy(&extron->serio_lock); + extron->serio = NULL; + serio_set_drvdata(serio, NULL); + serio_close(serio); +} + +static int extron_setup(struct extron *extron) +{ + struct serio *serio = extron->serio; + struct extron_port *port; + u8 *reply = extron->reply; + unsigned int p; + unsigned int major, minor; + int err; + + /* + * Attempt to disable CEC: avoid received CEC messages + * from interfering with the other serial port traffic. + */ + extron_send_and_wait(extron, NULL, "WI1*0CCEC", NULL); + extron_send_and_wait(extron, NULL, "WO0*CCEC", NULL); + + /* Obtain unit part number */ + err = extron_send_and_wait(extron, NULL, "N", "Pno"); + if (err) + return err; + dev_info(extron->dev, "Unit part number: %s\n", reply + 3); + if (strcmp(reply + 3, "60-1607-01") && + strcmp(reply + 3, "60-1608-01") && + strcmp(reply + 3, "60-1609-01")) { + dev_err(extron->dev, "Unsupported model\n"); + return -ENODEV; + } + /* Up to 6 output ports and one input port */ + extron->num_out_ports = 2 * (reply[9] - '6'); + extron->splitter.num_out_ports = extron->num_out_ports; + extron->splitter.ports = extron->splitter_ports; + extron->splitter.dev = extron->dev; + extron->num_in_ports = 1; + extron->num_ports = extron->num_out_ports + extron->num_in_ports; + dev_info(extron->dev, "Unit output ports: %d\n", extron->num_out_ports); + dev_info(extron->dev, "Unit input ports: %d\n", extron->num_in_ports); + + err = extron_send_and_wait(extron, NULL, "W CN", "Ipn "); + if (err) + return err; + dev_info(extron->dev, "Unit name: %s\n", reply + 4); + strscpy(extron->unit_name, reply + 4, sizeof(extron->unit_name)); + + err = extron_send_and_wait(extron, NULL, "*Q", "Bld"); + if (err) + return err; + dev_info(extron->dev, "Unit FW Version: %s\n", reply + 3); + strscpy(extron->unit_fw_version, reply + 3, + sizeof(extron->unit_fw_version)); + if (sscanf(reply + 3, "%u.%u.", &major, &minor) < 2 || + major < 1 || minor < 2) { + dev_err(extron->dev, + "Unsupported FW version (only 1.02 or up is supported)\n"); + return -ENODEV; + } + + err = extron_send_and_wait(extron, NULL, "2i", "Inf02*"); + if (err) + return err; + dev_info(extron->dev, "Unit Type: %s\n", reply + 6); + strscpy(extron->unit_type, reply + 6, sizeof(extron->unit_type)); + + err = extron_send_and_wait(extron, NULL, "39Q", "Ver39*"); + if (err) + return err; + dev_info(extron->dev, "CEC Engine Version: %s\n", reply + 6); + strscpy(extron->unit_cec_engine_version, reply + 6, + sizeof(extron->unit_cec_engine_version)); + + /* Disable CEC */ + err = extron_send_and_wait(extron, NULL, "WI1*0CCEC", "CcecI1*"); + if (err) + return err; + err = extron_send_and_wait(extron, NULL, "WO0*CCEC", "CcecO0"); + if (err) + return err; + + extron->hpd_never_low = hpd_never_low; + + /* Pull input port HPD low if all output ports also have a low HPD */ + if (hpd_never_low) { + dev_info(extron->dev, "Always keep input HPD high\n"); + } else { + dev_info(extron->dev, "Pull input HPD low if all output HPDs are low\n"); + extron_send_and_wait(extron, NULL, "W1ihpd", "Ihpd1"); + } + + for (p = 0; p < extron->num_ports; p++) { + u32 caps = CEC_CAP_DEFAULTS | CEC_CAP_MONITOR_ALL; + + if (vendor_id) + caps &= ~CEC_CAP_LOG_ADDRS; + port = kzalloc(sizeof(*port), GFP_KERNEL); + if (!port) + return -ENOMEM; + + INIT_WORK(&port->irq_work, extron_irq_work_handler); + spin_lock_init(&port->msg_lock); + mutex_init(&port->video_lock); + port->extron = extron; + port->is_input = p >= extron->num_out_ports; + port->direction = port->is_input ? 'I' : 'O'; + port->port.port = 1 + (port->is_input ? p - extron->num_out_ports : p); + port->port.splitter = &extron->splitter; + port->phys_addr = CEC_PHYS_ADDR_INVALID; + snprintf(port->name, sizeof(port->name), "%s-%s-%u", + dev_name(&serio->dev), port->is_input ? "in" : "out", + port->port.port); + + port->dev = extron->dev; + port->adap = cec_allocate_adapter(&extron_cec_adap_ops, port, + port->name, caps, 1); + err = PTR_ERR_OR_ZERO(port->adap); + if (err < 0) { + kfree(port); + return err; + } + + port->adap->xfer_timeout_ms = EXTRON_TIMEOUT_SECS * 1000; + port->port.adap = port->adap; + port->vdev = extron_videodev; + port->vdev.lock = &port->video_lock; + port->vdev.v4l2_dev = &extron->v4l2_dev; + port->vdev.ctrl_handler = &port->hdl; + port->vdev.device_caps = V4L2_CAP_EDID; + video_set_drvdata(&port->vdev, port); + + v4l2_ctrl_handler_init(&port->hdl, 2); + + if (port->is_input) { + port->vdev.vfl_dir = VFL_DIR_RX; + port->ctrl_rx_power_present = + v4l2_ctrl_new_std(&port->hdl, NULL, + V4L2_CID_DV_RX_POWER_PRESENT, + 0, 1, 0, 0); + port->has_edid = true; + } else { + port->vdev.vfl_dir = VFL_DIR_TX; + port->ctrl_tx_hotplug = + v4l2_ctrl_new_std(&port->hdl, NULL, + V4L2_CID_DV_TX_HOTPLUG, + 0, 1, 0, 0); + port->ctrl_tx_edid_present = + v4l2_ctrl_new_std(&port->hdl, NULL, + V4L2_CID_DV_TX_EDID_PRESENT, + 0, 1, 0, 0); + } + + err = port->hdl.error; + if (err < 0) { + cec_delete_adapter(port->adap); + kfree(port); + return err; + } + extron->ports[p] = port; + extron->splitter_ports[p] = &port->port; + if (port->is_input && manufacturer_name[0]) + extron_write_edid(port, hdmi_edid, 2); + } + + /* Enable CEC (manual mode, i.e. controlled by the driver) */ + err = extron_send_and_wait(extron, NULL, "WI1*20CCEC", "CcecI1*"); + if (err) + return err; + + err = extron_send_and_wait(extron, NULL, "WO20*CCEC", "CcecO20"); + if (err) + return err; + + /* Set logical addresses to 15 */ + err = extron_send_and_wait(extron, NULL, "WI1*15LCEC", "LcecI1*15"); + if (err) + return err; + + for (p = 0; p < extron->num_out_ports; p++) { + char cmd[20]; + char resp[20]; + + snprintf(cmd, sizeof(cmd), "WO%u*15LCEC", p + 1); + snprintf(resp, sizeof(resp), "LcecO%u*15", p + 1); + err = extron_send_and_wait(extron, extron->ports[p], cmd, resp); + if (err) + return err; + } + + /* + * The Extron is now ready for operation. Specifically it is now + * possible to retrieve EDIDs. + */ + extron->is_ready = true; + + /* Query HDCP and Signal states, used to update the initial state */ + err = extron_send_and_wait(extron, NULL, "WHDCP", "Hdcp"); + if (err) + return err; + + return extron_send_and_wait(extron, NULL, "WLS", "Sig"); +} + +static int extron_setup_thread(void *_extron) +{ + struct extron *extron = _extron; + struct extron_port *port; + unsigned int p; + bool poll_splitter = false; + bool was_connected = true; + int err; + + while (1) { + if (kthread_should_stop()) + return 0; + err = extron_send_and_wait(extron, NULL, "W3CV", "Vrb3"); + // that should make it possible to detect a serio disconnect + // here by stopping the workqueue + if (err >= 0) + break; + was_connected = false; + ssleep(1); + } + + /* + * If the Extron was not connected at probe() time, i.e. it just got + * powered up and while the serial port is working, the firmware is + * still booting up, then wait 10 seconds for the firmware to settle. + * + * Trying to continue too soon means that some commands will not + * work yet. + */ + if (!was_connected) + ssleep(10); + + err = extron_setup(extron); + if (err) + goto disable_ports; + + for (p = 0; p < extron->num_ports; p++) { + struct cec_log_addrs log_addrs = {}; + + port = extron->ports[p]; + if (port->is_input && manufacturer_name[0]) + v4l2_disable_ioctl(&port->vdev, VIDIOC_S_EDID); + err = video_register_device(&port->vdev, VFL_TYPE_VIDEO, -1); + if (err) { + v4l2_err(&extron->v4l2_dev, "Failed to register video device\n"); + goto disable_ports; + } + + err = cec_register_adapter(port->adap, extron->dev); + if (err < 0) + goto disable_ports; + port->dev = &port->adap->devnode.dev; + port->cec_was_registered = true; + /* + * This driver is unusual in that the whole setup takes place + * in a thread since it can take such a long time before the + * Extron Splitter boots up, and you do not want to block the + * probe function on this driver. In addition, as soon as + * CEC adapters come online, they can be used, and you cannot + * just unregister them again if an error occurs, since that + * can delete the underlying CEC adapter, which might already + * be in use. + * + * So we take an additional reference to the adapter. This + * allows us to unregister the device node if needed, without + * deleting the actual adapter. + * + * In the disconnect function we will do the corresponding + * put call to ensure the adapter is deleted. + */ + cec_get_device(port->adap); + + /* + * If vendor_id wasn't set, then userspace configures the + * CEC devices. Otherwise the driver configures the CEC + * devices as TV (input) and Playback (outputs) devices + * and the driver processes all CEC messages. + */ + if (!vendor_id) + continue; + + log_addrs.cec_version = CEC_OP_CEC_VERSION_2_0; + log_addrs.num_log_addrs = 1; + log_addrs.vendor_id = vendor_id; + if (port->is_input) { + strscpy(log_addrs.osd_name, "Splitter In", + sizeof(log_addrs.osd_name)); + log_addrs.log_addr_type[0] = CEC_LOG_ADDR_TYPE_TV; + log_addrs.primary_device_type[0] = CEC_OP_PRIM_DEVTYPE_TV; + log_addrs.all_device_types[0] = CEC_OP_ALL_DEVTYPE_TV; + } else { + snprintf(log_addrs.osd_name, sizeof(log_addrs.osd_name), + "Splitter Out%u", port->port.port); + log_addrs.log_addr_type[0] = CEC_LOG_ADDR_TYPE_PLAYBACK; + log_addrs.primary_device_type[0] = CEC_OP_PRIM_DEVTYPE_PLAYBACK; + log_addrs.all_device_types[0] = CEC_OP_ALL_DEVTYPE_PLAYBACK; + } + err = cec_s_log_addrs(port->adap, &log_addrs, false); + if (err < 0) + goto disable_ports; + } + poll_splitter = true; + + port = extron->ports[extron->num_out_ports]; + while (!kthread_should_stop()) { + ssleep(1); + if (hpd_never_low != extron->hpd_never_low) { + /* + * Keep input port HPD high at all times, or pull it low + * if all output ports also have a low HPD + */ + if (hpd_never_low) { + dev_info(extron->dev, "Always keep input HPD high\n"); + extron_send_and_wait(extron, NULL, "W0ihpd", "Ihpd0"); + } else { + dev_info(extron->dev, "Pull input HPD low if all output HPDs are low\n"); + extron_send_and_wait(extron, NULL, "W1ihpd", "Ihpd1"); + } + extron->hpd_never_low = hpd_never_low; + } + if (poll_splitter && + cec_splitter_poll(&extron->splitter, port->adap, debug) && + manufacturer_name[0]) { + /* + * Sinks were lost, so see if the input edid needs to + * be updated. + */ + cancel_delayed_work_sync(&extron->work_update_edid); + schedule_delayed_work(&extron->work_update_edid, + msecs_to_jiffies(1000)); + } + } + return 0; + +disable_ports: + extron->is_ready = false; + for (p = 0; p < extron->num_ports; p++) { + struct extron_port *port = extron->ports[p]; + + if (!port) + continue; + port->disconnected = true; + cancel_work_sync(&port->irq_work); + video_unregister_device(&port->vdev); + if (port->cec_was_registered) + cec_unregister_adapter(port->adap); + } + cancel_delayed_work_sync(&extron->work_update_edid); + complete(&extron->edid_completion); + dev_err(extron->dev, "Setup failed with error %d\n", err); + while (!kthread_should_stop()) + ssleep(1); + return err; +} + +static int extron_connect(struct serio *serio, struct serio_driver *drv) +{ + struct extron *extron; + int err = -ENOMEM; + + if (manufacturer_name[0] && + (!isupper(manufacturer_name[0]) || + !isupper(manufacturer_name[1]) || + !isupper(manufacturer_name[2]))) { + dev_warn(&serio->dev, "ignoring invalid manufacturer name\n"); + manufacturer_name[0] = 0; + } + + extron = kzalloc(sizeof(*extron), GFP_KERNEL); + + if (!extron) + return -ENOMEM; + + extron->serio = serio; + extron->dev = &serio->dev; + mutex_init(&extron->serio_lock); + mutex_init(&extron->edid_lock); + INIT_DELAYED_WORK(&extron->work_update_edid, update_edid_work); + + err = v4l2_device_register(extron->dev, &extron->v4l2_dev); + if (err) + goto free_device; + + err = serio_open(serio, drv); + if (err) + goto unreg_v4l2_dev; + + serio_set_drvdata(serio, extron); + init_completion(&extron->edid_completion); + + extron->kthread_setup = kthread_run(extron_setup_thread, extron, + "extron-da-hd-4k-plus-cec-%s", dev_name(&serio->dev)); + if (!IS_ERR(extron->kthread_setup)) + return 0; + + dev_err(extron->dev, "kthread_run() failed\n"); + err = PTR_ERR(extron->kthread_setup); + + extron->serio = NULL; + serio_set_drvdata(serio, NULL); + serio_close(serio); +unreg_v4l2_dev: + v4l2_device_unregister(&extron->v4l2_dev); +free_device: + mutex_destroy(&extron->edid_lock); + mutex_destroy(&extron->serio_lock); + kfree(extron); + return err; +} + +static const struct serio_device_id extron_serio_ids[] = { + { + .type = SERIO_RS232, + .proto = SERIO_EXTRON_DA_HD_4K_PLUS, + .id = SERIO_ANY, + .extra = SERIO_ANY, + }, + { 0 } +}; + +MODULE_DEVICE_TABLE(serio, extron_serio_ids); + +static struct serio_driver extron_drv = { + .driver = { + .name = "extron-da-hd-4k-plus-cec", + }, + .description = "Extron DA HD 4K PLUS HDMI CEC driver", + .id_table = extron_serio_ids, + .interrupt = extron_interrupt, + .connect = extron_connect, + .disconnect = extron_disconnect, +}; + +module_serio_driver(extron_drv); diff --git a/drivers/media/cec/usb/extron-da-hd-4k-plus/extron-da-hd-4k-plus.h b/drivers/media/cec/usb/extron-da-hd-4k-plus/extron-da-hd-4k-plus.h new file mode 100644 index 000000000000..b79f1253ab5d --- /dev/null +++ b/drivers/media/cec/usb/extron-da-hd-4k-plus/extron-da-hd-4k-plus.h @@ -0,0 +1,118 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +/* + * Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + */ + +#ifndef _EXTRON_DA_HD_4K_PLUS_H_ +#define _EXTRON_DA_HD_4K_PLUS_H_ + +#include <linux/kthread.h> +#include <linux/serio.h> +#include <linux/workqueue.h> +#include <media/cec.h> +#include <media/v4l2-ctrls.h> +#include <media/v4l2-dev.h> +#include <media/v4l2-device.h> +#include <media/v4l2-dv-timings.h> +#include <media/v4l2-event.h> +#include <media/v4l2-fh.h> +#include <media/v4l2-ioctl.h> + +#include "cec-splitter.h" + +#define DATA_SIZE 256 + +#define PING_PERIOD (15 * HZ) + +#define NUM_MSGS CEC_MAX_MSG_RX_QUEUE_SZ + +#define MAX_PORTS (1 + 6) + +#define MAX_EDID_BLOCKS 2 + +struct extron; + +struct extron_port { + struct cec_splitter_port port; + struct device *dev; + struct cec_adapter *adap; + struct video_device vdev; + struct v4l2_ctrl_handler hdl; + struct v4l2_ctrl *ctrl_rx_power_present; + struct v4l2_ctrl *ctrl_tx_hotplug; + struct v4l2_ctrl *ctrl_tx_edid_present; + bool is_input; + char direction; + char name[26]; + unsigned char edid[MAX_EDID_BLOCKS * 128]; + unsigned char edid_tmp[MAX_EDID_BLOCKS * 128]; + unsigned int edid_blocks; + bool read_edid; + struct extron *extron; + struct work_struct irq_work; + struct completion cmd_done; + const char *response; + unsigned int cmd_error; + struct cec_msg rx_msg[NUM_MSGS]; + unsigned int rx_msg_cur_idx, rx_msg_num; + /* protect rx_msg_cur_idx and rx_msg_num */ + spinlock_t msg_lock; + u32 tx_done_status; + bool update_phys_addr; + u16 phys_addr; + bool cec_was_registered; + bool disconnected; + bool update_has_signal; + bool has_signal; + bool update_has_edid; + bool has_edid; + bool has_4kp30; + bool has_4kp60; + bool has_qy; + bool has_qs; + u8 est_i, est_ii; + + /* locks access to the video_device */ + struct mutex video_lock; +}; + +struct extron { + struct cec_splitter splitter; + struct device *dev; + struct serio *serio; + /* locks access to serio */ + struct mutex serio_lock; + unsigned int num_ports; + unsigned int num_in_ports; + unsigned int num_out_ports; + char unit_name[32]; + char unit_type[64]; + char unit_fw_version[32]; + char unit_cec_engine_version[32]; + struct extron_port *ports[MAX_PORTS]; + struct cec_splitter_port *splitter_ports[MAX_PORTS]; + struct v4l2_device v4l2_dev; + bool hpd_never_low; + struct task_struct *kthread_setup; + struct delayed_work work_update_edid; + + /* serializes EDID reading */ + struct mutex edid_lock; + unsigned int edid_bytes_read; + struct extron_port *edid_port; + struct completion edid_completion; + bool edid_reading; + bool is_ready; + + struct completion cmd_done; + const char *response; + unsigned int cmd_error; + char data[DATA_SIZE]; + unsigned int len; + char reply[DATA_SIZE]; + char buf[DATA_SIZE]; + unsigned int idx; +}; + +#endif diff --git a/drivers/media/cec/usb/pulse8/pulse8-cec.c b/drivers/media/cec/usb/pulse8/pulse8-cec.c index ba67587bd43e..171366fe3544 100644 --- a/drivers/media/cec/usb/pulse8/pulse8-cec.c +++ b/drivers/media/cec/usb/pulse8/pulse8-cec.c @@ -685,7 +685,7 @@ static int pulse8_setup(struct pulse8 *pulse8, struct serio *serio, err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 4); if (err) return err; - date = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; + date = ((unsigned)data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; dev_info(pulse8->dev, "Firmware build date %ptT\n", &date); dev_dbg(pulse8->dev, "Persistent config:\n"); |