summaryrefslogtreecommitdiff
path: root/drivers/media/cec
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/media/cec')
-rw-r--r--drivers/media/cec/core/cec-adap.c82
-rw-r--r--drivers/media/cec/core/cec-api.c12
-rw-r--r--drivers/media/cec/core/cec-core.c38
-rw-r--r--drivers/media/cec/core/cec-pin-error-inj.c3
-rw-r--r--drivers/media/cec/core/cec-pin.c17
-rw-r--r--drivers/media/cec/core/cec-priv.h2
-rw-r--r--drivers/media/cec/i2c/Kconfig10
-rw-r--r--drivers/media/cec/i2c/Makefile1
-rw-r--r--drivers/media/cec/i2c/tda9950.c507
-rw-r--r--drivers/media/cec/platform/Kconfig2
-rw-r--r--drivers/media/cec/platform/cec-gpio/cec-gpio.c11
-rw-r--r--drivers/media/cec/platform/cros-ec/cros-ec-cec.c16
-rw-r--r--drivers/media/cec/platform/meson/ao-cec-g12a.c2
-rw-r--r--drivers/media/cec/platform/meson/ao-cec.c2
-rw-r--r--drivers/media/cec/platform/s5p/s5p_cec.c2
-rw-r--r--drivers/media/cec/platform/seco/seco-cec.c2
-rw-r--r--drivers/media/cec/platform/sti/stih-cec.c3
-rw-r--r--drivers/media/cec/platform/stm32/stm32-cec.c2
-rw-r--r--drivers/media/cec/platform/tegra/tegra_cec.c2
-rw-r--r--drivers/media/cec/usb/Kconfig1
-rw-r--r--drivers/media/cec/usb/Makefile1
-rw-r--r--drivers/media/cec/usb/extron-da-hd-4k-plus/Kconfig14
-rw-r--r--drivers/media/cec/usb/extron-da-hd-4k-plus/Makefile8
-rw-r--r--drivers/media/cec/usb/extron-da-hd-4k-plus/cec-splitter.c657
-rw-r--r--drivers/media/cec/usb/extron-da-hd-4k-plus/cec-splitter.h51
-rw-r--r--drivers/media/cec/usb/extron-da-hd-4k-plus/extron-da-hd-4k-plus.c1836
-rw-r--r--drivers/media/cec/usb/extron-da-hd-4k-plus/extron-da-hd-4k-plus.h118
-rw-r--r--drivers/media/cec/usb/pulse8/pulse8-cec.c2
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");