summaryrefslogtreecommitdiffstats
path: root/drivers/media/cec/i2c
diff options
context:
space:
mode:
authorKees Cook <kees+treewide@kernel.org>2026-09-02 15:31:14 -0700
committerKees Cook <kees@kernel.org>2026-09-04 21:37:00 -0700
commit3a2c4d55e32ad65efebdb6de44eef3bfa08bb49d (patch)
treec65086f9bdcd48c6360fb7cb4598bca084da1f32 /drivers/media/cec/i2c
downloadlinux-stable-3a2c4d55e32ad65efebdb6de44eef3bfa08bb49d.tar.gz
linux-stable-3a2c4d55e32ad65efebdb6de44eef3bfa08bb49d.zip
treewide: refresh kmalloc_obj() conversionsgrafted
This is another run of the Coccinelle script for converting kmalloc() family of allocations to kmalloc_obj() via the existing rules in scripts/coccinelle/api/kmalloc_objs.cocci This catches both the set of kmalloc() uses added since the first kmalloc_obj() conversions in v7.0 and adds a large group missed in the first pass due to Coccinelle not interacting well with the cleanup.h scoped_...() family of macros[1]. I worked around this with spatch's "--macro-file" argument to a file with all the scoped_...() macros mapped to Coccinelle's YACFE_ITERATOR[2] as that was the closest viable control flow indicator I could find. Build tested allmodconfig on x86, arm64, arm, loongarch, mips, powerpc, riscv, and s390 with no new warnings. Link: https://lore.kernel.org/lkml/202609021314.8A9C0B8@keescook/ [1] Link: https://github.com/coccinelle/coccinelle/blob/master/standard.h [2] Signed-off-by: Kees Cook <kees+treewide@kernel.org>
Diffstat (limited to 'drivers/media/cec/i2c')
-rw-r--r--drivers/media/cec/i2c/Kconfig25
-rw-r--r--drivers/media/cec/i2c/Makefile6
-rw-r--r--drivers/media/cec/i2c/ch7322.c602
-rw-r--r--drivers/media/cec/i2c/tda9950.c507
4 files changed, 1140 insertions, 0 deletions
diff --git a/drivers/media/cec/i2c/Kconfig b/drivers/media/cec/i2c/Kconfig
new file mode 100644
index 000000000..c31abc26f
--- /dev/null
+++ b/drivers/media/cec/i2c/Kconfig
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# I2C drivers
+
+config CEC_CH7322
+ tristate "Chrontel CH7322 CEC controller"
+ depends on I2C
+ select REGMAP
+ select REGMAP_I2C
+ select CEC_CORE
+ help
+ This is a driver for the Chrontel CH7322 CEC controller. It uses the
+ 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
new file mode 100644
index 000000000..95c9eda52
--- /dev/null
+++ b/drivers/media/cec/i2c/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# 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/ch7322.c b/drivers/media/cec/i2c/ch7322.c
new file mode 100644
index 000000000..b8755337b
--- /dev/null
+++ b/drivers/media/cec/i2c/ch7322.c
@@ -0,0 +1,602 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Driver for the Chrontel CH7322 CEC Controller
+ *
+ * Copyright 2020 Google LLC.
+ */
+
+/*
+ * Notes
+ *
+ * - This device powers on in Auto Mode which has limited functionality. This
+ * driver disables Auto Mode when it attaches.
+ *
+ */
+
+#include <linux/cec.h>
+#include <linux/dmi.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <linux/regmap.h>
+#include <media/cec.h>
+#include <media/cec-notifier.h>
+
+#define CH7322_WRITE 0x00
+#define CH7322_WRITE_MSENT 0x80
+#define CH7322_WRITE_BOK 0x40
+#define CH7322_WRITE_NMASK 0x0f
+
+/* Write buffer is 0x01-0x10 */
+#define CH7322_WRBUF 0x01
+#define CH7322_WRBUF_LEN 0x10
+
+#define CH7322_READ 0x40
+#define CH7322_READ_NRDT 0x80
+#define CH7322_READ_MSENT 0x20
+#define CH7322_READ_NMASK 0x0f
+
+/* Read buffer is 0x41-0x50 */
+#define CH7322_RDBUF 0x41
+#define CH7322_RDBUF_LEN 0x10
+
+#define CH7322_MODE 0x11
+#define CH7322_MODE_AUTO 0x78
+#define CH7322_MODE_SW 0xb5
+
+#define CH7322_RESET 0x12
+#define CH7322_RESET_RST 0x00
+
+#define CH7322_POWER 0x13
+#define CH7322_POWER_FPD 0x04
+
+#define CH7322_CFG0 0x17
+#define CH7322_CFG0_EOBEN 0x40
+#define CH7322_CFG0_PEOB 0x20
+#define CH7322_CFG0_CLRSPP 0x10
+#define CH7322_CFG0_FLOW 0x08
+
+#define CH7322_CFG1 0x1a
+#define CH7322_CFG1_STDBYO 0x04
+#define CH7322_CFG1_HPBP 0x02
+#define CH7322_CFG1_PIO 0x01
+
+#define CH7322_INTCTL 0x1b
+#define CH7322_INTCTL_INTPB 0x80
+#define CH7322_INTCTL_STDBY 0x40
+#define CH7322_INTCTL_HPDFALL 0x20
+#define CH7322_INTCTL_HPDRISE 0x10
+#define CH7322_INTCTL_RXMSG 0x08
+#define CH7322_INTCTL_TXMSG 0x04
+#define CH7322_INTCTL_NEWPHA 0x02
+#define CH7322_INTCTL_ERROR 0x01
+
+#define CH7322_DVCLKFNH 0x1d
+#define CH7322_DVCLKFNL 0x1e
+
+#define CH7322_CTL 0x31
+#define CH7322_CTL_FSTDBY 0x80
+#define CH7322_CTL_PLSEN 0x40
+#define CH7322_CTL_PLSPB 0x20
+#define CH7322_CTL_SPADL 0x10
+#define CH7322_CTL_HINIT 0x08
+#define CH7322_CTL_WPHYA 0x04
+#define CH7322_CTL_H1T 0x02
+#define CH7322_CTL_S1T 0x01
+
+#define CH7322_PAWH 0x32
+#define CH7322_PAWL 0x33
+
+#define CH7322_ADDLW 0x34
+#define CH7322_ADDLW_MASK 0xf0
+
+#define CH7322_ADDLR 0x3d
+#define CH7322_ADDLR_HPD 0x80
+#define CH7322_ADDLR_MASK 0x0f
+
+#define CH7322_INTDATA 0x3e
+#define CH7322_INTDATA_MODE 0x80
+#define CH7322_INTDATA_STDBY 0x40
+#define CH7322_INTDATA_HPDFALL 0x20
+#define CH7322_INTDATA_HPDRISE 0x10
+#define CH7322_INTDATA_RXMSG 0x08
+#define CH7322_INTDATA_TXMSG 0x04
+#define CH7322_INTDATA_NEWPHA 0x02
+#define CH7322_INTDATA_ERROR 0x01
+
+#define CH7322_EVENT 0x3f
+#define CH7322_EVENT_TXERR 0x80
+#define CH7322_EVENT_HRST 0x40
+#define CH7322_EVENT_HFST 0x20
+#define CH7322_EVENT_PHACHG 0x10
+#define CH7322_EVENT_ACTST 0x08
+#define CH7322_EVENT_PHARDY 0x04
+#define CH7322_EVENT_BSOK 0x02
+#define CH7322_EVENT_ERRADCF 0x01
+
+#define CH7322_DID 0x51
+#define CH7322_DID_CH7322 0x5b
+#define CH7322_DID_CH7323 0x5f
+
+#define CH7322_REVISIONID 0x52
+
+#define CH7322_PARH 0x53
+#define CH7322_PARL 0x54
+
+#define CH7322_IOCFG2 0x75
+#define CH7322_IOCFG_CIO 0x80
+#define CH7322_IOCFG_IOCFGMASK 0x78
+#define CH7322_IOCFG_AUDIO 0x04
+#define CH7322_IOCFG_SPAMST 0x02
+#define CH7322_IOCFG_SPAMSP 0x01
+
+#define CH7322_CTL3 0x7b
+#define CH7322_CTL3_SWENA 0x80
+#define CH7322_CTL3_FC_INIT 0x40
+#define CH7322_CTL3_SML_FL 0x20
+#define CH7322_CTL3_SM_RDST 0x10
+#define CH7322_CTL3_SPP_CIAH 0x08
+#define CH7322_CTL3_SPP_CIAL 0x04
+#define CH7322_CTL3_SPP_ACTH 0x02
+#define CH7322_CTL3_SPP_ACTL 0x01
+
+/* BOK status means NACK */
+#define CH7322_TX_FLAG_NACK BIT(0)
+/* Device will retry automatically */
+#define CH7322_TX_FLAG_RETRY BIT(1)
+
+struct ch7322 {
+ struct i2c_client *i2c;
+ struct regmap *regmap;
+ struct cec_adapter *cec;
+ struct mutex mutex; /* device access mutex */
+ u8 tx_flags;
+};
+
+static const struct regmap_config ch7322_regmap = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0x7f,
+ .disable_locking = true,
+};
+
+static int ch7322_send_message(struct ch7322 *ch7322, const struct cec_msg *msg)
+{
+ unsigned int val;
+ unsigned int len = msg->len;
+ int ret;
+ int i;
+
+ WARN_ON(!mutex_is_locked(&ch7322->mutex));
+
+ if (len > CH7322_WRBUF_LEN || len < 1)
+ return -EINVAL;
+
+ ret = regmap_read(ch7322->regmap, CH7322_WRITE, &val);
+ if (ret)
+ return ret;
+
+ /* Buffer not ready */
+ if (!(val & CH7322_WRITE_MSENT))
+ return -EBUSY;
+
+ if (cec_msg_opcode(msg) == -1 &&
+ cec_msg_initiator(msg) == cec_msg_destination(msg)) {
+ ch7322->tx_flags = CH7322_TX_FLAG_NACK | CH7322_TX_FLAG_RETRY;
+ } else if (cec_msg_is_broadcast(msg)) {
+ ch7322->tx_flags = CH7322_TX_FLAG_NACK;
+ } else {
+ ch7322->tx_flags = CH7322_TX_FLAG_RETRY;
+ }
+
+ ret = regmap_write(ch7322->regmap, CH7322_WRITE, len - 1);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < len; i++) {
+ ret = regmap_write(ch7322->regmap,
+ CH7322_WRBUF + i, msg->msg[i]);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ch7322_receive_message(struct ch7322 *ch7322, struct cec_msg *msg)
+{
+ unsigned int val;
+ int ret = 0;
+ int i;
+
+ WARN_ON(!mutex_is_locked(&ch7322->mutex));
+
+ ret = regmap_read(ch7322->regmap, CH7322_READ, &val);
+ if (ret)
+ return ret;
+
+ /* Message not ready */
+ if (!(val & CH7322_READ_NRDT))
+ return -EIO;
+
+ msg->len = (val & CH7322_READ_NMASK) + 1;
+
+ /* Read entire RDBUF to clear state */
+ for (i = 0; i < CH7322_RDBUF_LEN; i++) {
+ ret = regmap_read(ch7322->regmap, CH7322_RDBUF + i, &val);
+ if (ret)
+ return ret;
+ msg->msg[i] = (u8)val;
+ }
+
+ return 0;
+}
+
+static void ch7322_tx_done(struct ch7322 *ch7322)
+{
+ int ret;
+ unsigned int val;
+ u8 status, flags;
+
+ mutex_lock(&ch7322->mutex);
+ ret = regmap_read(ch7322->regmap, CH7322_WRITE, &val);
+ flags = ch7322->tx_flags;
+ mutex_unlock(&ch7322->mutex);
+
+ /*
+ * The device returns a one-bit OK status which usually means ACK but
+ * actually means NACK when sending a logical address query or a
+ * broadcast.
+ */
+ if (ret)
+ status = CEC_TX_STATUS_ERROR;
+ else if ((val & CH7322_WRITE_BOK) && (flags & CH7322_TX_FLAG_NACK))
+ status = CEC_TX_STATUS_NACK;
+ else if (val & CH7322_WRITE_BOK)
+ status = CEC_TX_STATUS_OK;
+ else if (flags & CH7322_TX_FLAG_NACK)
+ status = CEC_TX_STATUS_OK;
+ else
+ status = CEC_TX_STATUS_NACK;
+
+ if (status == CEC_TX_STATUS_NACK && (flags & CH7322_TX_FLAG_RETRY))
+ status |= CEC_TX_STATUS_MAX_RETRIES;
+
+ cec_transmit_attempt_done(ch7322->cec, status);
+}
+
+static void ch7322_rx_done(struct ch7322 *ch7322)
+{
+ struct cec_msg msg;
+ int ret;
+
+ mutex_lock(&ch7322->mutex);
+ ret = ch7322_receive_message(ch7322, &msg);
+ mutex_unlock(&ch7322->mutex);
+
+ if (ret)
+ dev_err(&ch7322->i2c->dev, "cec receive error: %d\n", ret);
+ else
+ cec_received_msg(ch7322->cec, &msg);
+}
+
+/*
+ * This device can either monitor the DDC lines to obtain the physical address
+ * or it can allow the host to program it. This driver lets the device obtain
+ * it.
+ */
+static void ch7322_phys_addr(struct ch7322 *ch7322)
+{
+ unsigned int pah, pal;
+ int ret = 0;
+
+ mutex_lock(&ch7322->mutex);
+ ret |= regmap_read(ch7322->regmap, CH7322_PARH, &pah);
+ ret |= regmap_read(ch7322->regmap, CH7322_PARL, &pal);
+ mutex_unlock(&ch7322->mutex);
+
+ if (ret)
+ dev_err(&ch7322->i2c->dev, "phys addr error\n");
+ else
+ cec_s_phys_addr(ch7322->cec, pal | (pah << 8), false);
+}
+
+static irqreturn_t ch7322_irq(int irq, void *dev)
+{
+ struct ch7322 *ch7322 = dev;
+ unsigned int data = 0;
+
+ mutex_lock(&ch7322->mutex);
+ regmap_read(ch7322->regmap, CH7322_INTDATA, &data);
+ regmap_write(ch7322->regmap, CH7322_INTDATA, data);
+ mutex_unlock(&ch7322->mutex);
+
+ if (data & CH7322_INTDATA_HPDFALL)
+ cec_phys_addr_invalidate(ch7322->cec);
+
+ if (data & CH7322_INTDATA_TXMSG)
+ ch7322_tx_done(ch7322);
+
+ if (data & CH7322_INTDATA_RXMSG)
+ ch7322_rx_done(ch7322);
+
+ if (data & CH7322_INTDATA_NEWPHA)
+ ch7322_phys_addr(ch7322);
+
+ if (data & CH7322_INTDATA_ERROR)
+ dev_dbg(&ch7322->i2c->dev, "unknown error\n");
+
+ return IRQ_HANDLED;
+}
+
+/* This device is always enabled */
+static int ch7322_cec_adap_enable(struct cec_adapter *adap, bool enable)
+{
+ return 0;
+}
+
+static int ch7322_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr)
+{
+ struct ch7322 *ch7322 = cec_get_drvdata(adap);
+ int ret;
+
+ mutex_lock(&ch7322->mutex);
+ ret = regmap_update_bits(ch7322->regmap, CH7322_ADDLW,
+ CH7322_ADDLW_MASK, log_addr << 4);
+ mutex_unlock(&ch7322->mutex);
+
+ return ret;
+}
+
+static int ch7322_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
+ u32 signal_free_time, struct cec_msg *msg)
+{
+ struct ch7322 *ch7322 = cec_get_drvdata(adap);
+ int ret;
+
+ mutex_lock(&ch7322->mutex);
+ ret = ch7322_send_message(ch7322, msg);
+ mutex_unlock(&ch7322->mutex);
+
+ return ret;
+}
+
+static const struct cec_adap_ops ch7322_cec_adap_ops = {
+ .adap_enable = ch7322_cec_adap_enable,
+ .adap_log_addr = ch7322_cec_adap_log_addr,
+ .adap_transmit = ch7322_cec_adap_transmit,
+};
+
+#if IS_ENABLED(CONFIG_PCI) && IS_ENABLED(CONFIG_DMI)
+
+struct ch7322_conn_match {
+ const char *dev_name;
+ const char *pci_name;
+ const char *port_name;
+};
+
+static struct ch7322_conn_match google_endeavour[] = {
+ { "i2c-PRP0001:00", "0000:00:02.0", "Port B" },
+ { "i2c-PRP0001:01", "0000:00:02.0", "Port C" },
+ { },
+};
+
+static const struct dmi_system_id ch7322_dmi_table[] = {
+ {
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "Google"),
+ DMI_MATCH(DMI_BOARD_NAME, "Endeavour"),
+ },
+ .driver_data = google_endeavour,
+ },
+ { },
+};
+
+/* Make a best-effort attempt to locate a matching HDMI port */
+static int ch7322_get_port(struct i2c_client *client,
+ struct device **dev,
+ const char **port)
+{
+ const struct dmi_system_id *system;
+ const struct ch7322_conn_match *conn;
+
+ *dev = NULL;
+ *port = NULL;
+
+ system = dmi_first_match(ch7322_dmi_table);
+ if (!system)
+ return 0;
+
+ for (conn = system->driver_data; conn->dev_name; conn++) {
+ if (!strcmp(dev_name(&client->dev), conn->dev_name)) {
+ struct device *d;
+
+ d = bus_find_device_by_name(&pci_bus_type, NULL,
+ conn->pci_name);
+ if (!d)
+ return -EPROBE_DEFER;
+
+ put_device(d);
+
+ *dev = d;
+ *port = conn->port_name;
+
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+#else
+
+static int ch7322_get_port(struct i2c_client *client,
+ struct device **dev,
+ const char **port)
+{
+ *dev = NULL;
+ *port = NULL;
+
+ return 0;
+}
+
+#endif
+
+static int ch7322_probe(struct i2c_client *client)
+{
+ struct device *hdmi_dev;
+ const char *port_name;
+ struct ch7322 *ch7322;
+ struct cec_notifier *notifier = NULL;
+ u32 caps = CEC_CAP_DEFAULTS;
+ int ret;
+ unsigned int val;
+
+ ret = ch7322_get_port(client, &hdmi_dev, &port_name);
+ if (ret)
+ return ret;
+
+ if (hdmi_dev)
+ caps |= CEC_CAP_CONNECTOR_INFO;
+
+ ch7322 = devm_kzalloc(&client->dev, sizeof(*ch7322), GFP_KERNEL);
+ if (!ch7322)
+ return -ENOMEM;
+
+ ch7322->regmap = devm_regmap_init_i2c(client, &ch7322_regmap);
+ if (IS_ERR(ch7322->regmap))
+ return PTR_ERR(ch7322->regmap);
+
+ ret = regmap_read(ch7322->regmap, CH7322_DID, &val);
+ if (ret)
+ return ret;
+
+ if (val != CH7322_DID_CH7322)
+ return -EOPNOTSUPP;
+
+ mutex_init(&ch7322->mutex);
+ ch7322->i2c = client;
+ ch7322->tx_flags = 0;
+
+ i2c_set_clientdata(client, ch7322);
+
+ /* Disable auto mode */
+ ret = regmap_write(ch7322->regmap, CH7322_MODE, CH7322_MODE_SW);
+ if (ret)
+ goto err_mutex;
+
+ /* Enable logical address register */
+ ret = regmap_update_bits(ch7322->regmap, CH7322_CTL,
+ CH7322_CTL_SPADL, CH7322_CTL_SPADL);
+ if (ret)
+ goto err_mutex;
+
+ ch7322->cec = cec_allocate_adapter(&ch7322_cec_adap_ops, ch7322,
+ dev_name(&client->dev),
+ caps, 1);
+
+ if (IS_ERR(ch7322->cec)) {
+ ret = PTR_ERR(ch7322->cec);
+ goto err_mutex;
+ }
+
+ ch7322->cec->adap_controls_phys_addr = true;
+
+ if (hdmi_dev) {
+ notifier = cec_notifier_cec_adap_register(hdmi_dev,
+ port_name,
+ ch7322->cec);
+ if (!notifier) {
+ ret = -ENOMEM;
+ goto err_cec;
+ }
+ }
+
+ /* Configure, mask, and clear interrupt */
+ ret = regmap_write(ch7322->regmap, CH7322_CFG1, 0);
+ if (ret)
+ goto err_notifier;
+ ret = regmap_write(ch7322->regmap, CH7322_INTCTL, CH7322_INTCTL_INTPB);
+ if (ret)
+ goto err_notifier;
+ ret = regmap_write(ch7322->regmap, CH7322_INTDATA, 0xff);
+ if (ret)
+ goto err_notifier;
+
+ /* If HPD is up read physical address */
+ ret = regmap_read(ch7322->regmap, CH7322_ADDLR, &val);
+ if (ret)
+ goto err_notifier;
+ if (val & CH7322_ADDLR_HPD)
+ ch7322_phys_addr(ch7322);
+
+ ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
+ ch7322_irq,
+ IRQF_ONESHOT | IRQF_TRIGGER_RISING,
+ client->name, ch7322);
+ if (ret)
+ goto err_notifier;
+
+ /* Unmask interrupt */
+ mutex_lock(&ch7322->mutex);
+ ret = regmap_write(ch7322->regmap, CH7322_INTCTL, 0xff);
+ mutex_unlock(&ch7322->mutex);
+
+ if (ret)
+ goto err_notifier;
+
+ ret = cec_register_adapter(ch7322->cec, &client->dev);
+ if (ret)
+ goto err_notifier;
+
+ dev_info(&client->dev, "device registered\n");
+
+ return 0;
+
+err_notifier:
+ if (notifier)
+ cec_notifier_cec_adap_unregister(notifier, ch7322->cec);
+err_cec:
+ cec_delete_adapter(ch7322->cec);
+err_mutex:
+ mutex_destroy(&ch7322->mutex);
+ return ret;
+}
+
+static void ch7322_remove(struct i2c_client *client)
+{
+ struct ch7322 *ch7322 = i2c_get_clientdata(client);
+
+ /* Mask interrupt */
+ mutex_lock(&ch7322->mutex);
+ regmap_write(ch7322->regmap, CH7322_INTCTL, CH7322_INTCTL_INTPB);
+ mutex_unlock(&ch7322->mutex);
+
+ cec_unregister_adapter(ch7322->cec);
+ mutex_destroy(&ch7322->mutex);
+
+ dev_info(&client->dev, "device unregistered\n");
+}
+
+static const struct of_device_id ch7322_of_match[] = {
+ { .compatible = "chrontel,ch7322", },
+ {},
+};
+MODULE_DEVICE_TABLE(of, ch7322_of_match);
+
+static struct i2c_driver ch7322_i2c_driver = {
+ .driver = {
+ .name = "ch7322",
+ .of_match_table = ch7322_of_match,
+ },
+ .probe = ch7322_probe,
+ .remove = ch7322_remove,
+};
+
+module_i2c_driver(ch7322_i2c_driver);
+
+MODULE_DESCRIPTION("Chrontel CH7322 CEC Controller Driver");
+MODULE_AUTHOR("Jeff Chase <jnchase@google.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/media/cec/i2c/tda9950.c b/drivers/media/cec/i2c/tda9950.c
new file mode 100644
index 000000000..2bece4e63
--- /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[] = {
+ { .name = "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");