[RFC 8/8] media: dwc: dw-hdmi-rx: Add support for CEC

From: Nelson Costa
Date: Wed Apr 28 2021 - 11:25:26 EST


This adds support for the optional HDMI CEC feature
for the Synopsys DesignWare HDMI RX Controller.

It uses the generic CEC framework interface.

Signed-off-by: Jose Abreu <jose.abreu@xxxxxxxxxxxx>
Signed-off-by: Nelson Costa <nelson.costa@xxxxxxxxxxxx>
---
drivers/media/platform/dwc/Kconfig | 10 ++
drivers/media/platform/dwc/dw-hdmi-rx.c | 259 +++++++++++++++++++++++++++++++-
drivers/media/platform/dwc/dw-hdmi-rx.h | 57 +++++++
3 files changed, 324 insertions(+), 2 deletions(-)

diff --git a/drivers/media/platform/dwc/Kconfig b/drivers/media/platform/dwc/Kconfig
index ef2a6435..4d39be7 100644
--- a/drivers/media/platform/dwc/Kconfig
+++ b/drivers/media/platform/dwc/Kconfig
@@ -8,3 +8,13 @@ config VIDEO_DWC_HDMI_RX

To compile this driver as a module, choose M here. The module
will be called dw-hdmi-rx.
+
+config VIDEO_DWC_HDMI_RX_CEC
+ bool "Synopsys DesignWare HDMI Receiver CEC support"
+ depends on VIDEO_DWC_HDMI_RX
+ select CEC_CORE
+ help
+ When selected the Synopsys DesignWare HDMI RX controller
+ will support the optional HDMI CEC feature.
+
+ It uses the generic CEC framework interface.
diff --git a/drivers/media/platform/dwc/dw-hdmi-rx.c b/drivers/media/platform/dwc/dw-hdmi-rx.c
index 78d9ef3..a8b7fe19 100644
--- a/drivers/media/platform/dwc/dw-hdmi-rx.c
+++ b/drivers/media/platform/dwc/dw-hdmi-rx.c
@@ -21,6 +21,7 @@
#include <linux/workqueue.h>
#include <linux/rational.h>
#include <linux/hdmi.h>
+#include <media/cec.h>
#include <media/v4l2-async.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-device.h>
@@ -36,6 +37,7 @@
#define DW_HDMI_JTAG_TAP_WRITE_CMD 1
#define DW_HDMI_JTAG_TAP_READ_CMD 3
#define DW_HDMI_AUDIO_FREQ_RANGE 1000
+#define DW_HDMI_CEC_MAX_LOG_ADDRS CEC_MAX_LOG_ADDRS

/* EDID for HDMI RX */
static u32 dw_hdmi_edid[] = {
@@ -164,6 +166,9 @@ struct dw_hdmi_dev {
union hdmi_infoframe audioif;
union hdmi_infoframe vsif;

+ /* CEC */
+ struct cec_adapter *cec_adap;
+
/* v4l2 device */
struct v4l2_subdev sd;
struct v4l2_ctrl_handler hdl;
@@ -365,6 +370,20 @@ static void dw_hdmi_reset(struct dw_hdmi_dev *dw_dev)
dw_hdmi_main_reset(dw_dev);

dw_hdmi_disable_hpd(dw_dev);
+
+ /* After a main reset try to re-enable the cec adapter in order to
+ * reconfigure the required cec registers. For this the physical address
+ * is invalidated and reconfigured, and with CEC_CAP_NEEDS_HPD allowing
+ * to re-enable the adapter.
+ */
+ if (dw_dev->cec_adap) {
+ u16 phys_addr = dw_dev->cec_adap->phys_addr;
+
+ cec_phys_addr_invalidate(dw_dev->cec_adap);
+ cec_s_phys_addr(dw_dev->cec_adap, phys_addr, false);
+ dev_dbg(dw_dev->dev, "%s: re-enable cec adapter\n",
+ __func__);
+ }
}

static inline bool is_off(struct dw_hdmi_dev *dw_dev)
@@ -1463,6 +1482,184 @@ static u32 dw_hdmi_get_int_val(struct dw_hdmi_dev *dw_dev, u32 ists, u32 ien)
return hdmi_readl(dw_dev, ists) & hdmi_readl(dw_dev, ien);
}

+#if IS_ENABLED(CONFIG_VIDEO_DWC_HDMI_RX_CEC)
+static void dw_hdmi_cec_enable_ints(struct dw_hdmi_dev *dw_dev)
+{
+ u32 mask = DW_HDMI_DONE_ISTS | DW_HDMI_EOM_ISTS |
+ DW_HDMI_NACK_ISTS | DW_HDMI_ARBLST_ISTS |
+ DW_HDMI_ERROR_INIT_ISTS | DW_HDMI_ERROR_FOLL_ISTS;
+
+ hdmi_writel(dw_dev, mask, DW_HDMI_AUD_CEC_IEN_SET);
+ hdmi_writel(dw_dev, 0x0, DW_HDMI_CEC_MASK);
+}
+
+static void dw_hdmi_cec_disable_ints(struct dw_hdmi_dev *dw_dev)
+{
+ hdmi_writel(dw_dev, ~0x0, DW_HDMI_AUD_CEC_IEN_CLR);
+ hdmi_writel(dw_dev, ~0x0, DW_HDMI_CEC_MASK);
+}
+
+static void dw_hdmi_cec_clear_ints(struct dw_hdmi_dev *dw_dev)
+{
+ hdmi_writel(dw_dev, ~0x0, DW_HDMI_AUD_CEC_ICLR);
+}
+
+static void dw_hdmi_cec_tx_raw_status(struct dw_hdmi_dev *dw_dev, u32 stat)
+{
+ if (hdmi_readl(dw_dev, DW_HDMI_CEC_CTRL) & DW_HDMI_SEND_MASK) {
+ dev_dbg(dw_dev->dev, "%s: tx is busy\n", __func__);
+ return;
+ }
+
+ if (stat & DW_HDMI_ARBLST_ISTS) {
+ cec_transmit_attempt_done(dw_dev->cec_adap,
+ CEC_TX_STATUS_ARB_LOST);
+ return;
+ }
+
+ if (stat & DW_HDMI_NACK_ISTS) {
+ cec_transmit_attempt_done(dw_dev->cec_adap, CEC_TX_STATUS_NACK);
+ return;
+ }
+
+ if (stat & DW_HDMI_ERROR_INIT_ISTS) {
+ dev_dbg(dw_dev->dev, "%s: got low drive error\n", __func__);
+ cec_transmit_attempt_done(dw_dev->cec_adap,
+ CEC_TX_STATUS_LOW_DRIVE);
+ return;
+ }
+
+ if (stat & DW_HDMI_DONE_ISTS) {
+ cec_transmit_attempt_done(dw_dev->cec_adap, CEC_TX_STATUS_OK);
+ return;
+ }
+}
+
+static void dw_hdmi_cec_received_msg(struct dw_hdmi_dev *dw_dev)
+{
+ struct cec_msg msg;
+ u8 i;
+
+ msg.len = hdmi_readl(dw_dev, DW_HDMI_CEC_RX_CNT);
+ if (!msg.len || msg.len > DW_HDMI_CEC_RX_DATA_MAX)
+ return; /* it's an invalid/non-existent message */
+
+ for (i = 0; i < msg.len; i++)
+ msg.msg[i] = hdmi_readl(dw_dev, DW_HDMI_CEC_RX_DATA(i));
+
+ hdmi_writel(dw_dev, 0x0, DW_HDMI_CEC_LOCK);
+ cec_received_msg(dw_dev->cec_adap, &msg);
+}
+
+static int dw_hdmi_cec_adap_enable(struct cec_adapter *adap, bool enable)
+{
+ struct dw_hdmi_dev *dw_dev = cec_get_drvdata(adap);
+
+ dev_dbg(dw_dev->dev, "%s: enable=%d\n", __func__, enable);
+
+ hdmi_writel(dw_dev, 0x0, DW_HDMI_CEC_ADDR_L);
+ hdmi_writel(dw_dev, 0x0, DW_HDMI_CEC_ADDR_H);
+
+ if (enable) {
+ hdmi_writel(dw_dev, 0x0, DW_HDMI_CEC_LOCK);
+ dw_hdmi_cec_clear_ints(dw_dev);
+ dw_hdmi_cec_enable_ints(dw_dev);
+ } else {
+ dw_hdmi_cec_disable_ints(dw_dev);
+ dw_hdmi_cec_clear_ints(dw_dev);
+ }
+
+ return 0;
+}
+
+static int dw_hdmi_cec_adap_log_addr(struct cec_adapter *adap, u8 addr)
+{
+ struct dw_hdmi_dev *dw_dev = cec_get_drvdata(adap);
+ u32 tmp;
+
+ dev_dbg(dw_dev->dev, "%s: addr=%d\n", __func__, addr);
+
+ if (addr == CEC_LOG_ADDR_INVALID) {
+ hdmi_writel(dw_dev, 0x0, DW_HDMI_CEC_ADDR_L);
+ hdmi_writel(dw_dev, 0x0, DW_HDMI_CEC_ADDR_H);
+ return 0;
+ }
+
+ if (addr >= 8) {
+ tmp = hdmi_readl(dw_dev, DW_HDMI_CEC_ADDR_H);
+ tmp |= BIT(addr - 8);
+ hdmi_writel(dw_dev, tmp, DW_HDMI_CEC_ADDR_H);
+ } else {
+ tmp = hdmi_readl(dw_dev, DW_HDMI_CEC_ADDR_L);
+ tmp |= BIT(addr);
+ hdmi_writel(dw_dev, tmp, DW_HDMI_CEC_ADDR_L);
+ }
+
+ return 0;
+}
+
+static int dw_hdmi_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
+ u32 signal_free_time, struct cec_msg *msg)
+{
+ struct dw_hdmi_dev *dw_dev = cec_get_drvdata(adap);
+ u8 len = msg->len;
+ u32 reg;
+ u8 i;
+
+ dev_dbg(dw_dev->dev, "%s: len=%d\n", __func__, len);
+
+ if (hdmi_readl(dw_dev, DW_HDMI_CEC_CTRL) & DW_HDMI_SEND_MASK) {
+ dev_err(dw_dev->dev, "%s: tx is busy\n", __func__);
+ return -EBUSY;
+ }
+
+ for (i = 0; i < len; i++)
+ hdmi_writel(dw_dev, msg->msg[i], DW_HDMI_CEC_TX_DATA(i));
+
+ switch (signal_free_time) {
+ case CEC_SIGNAL_FREE_TIME_RETRY:
+ reg = 0x0;
+ break;
+ case CEC_SIGNAL_FREE_TIME_NEXT_XFER:
+ reg = 0x2;
+ break;
+ case CEC_SIGNAL_FREE_TIME_NEW_INITIATOR:
+ default:
+ reg = 0x1;
+ break;
+ }
+
+ hdmi_writel(dw_dev, len, DW_HDMI_CEC_TX_CNT);
+ hdmi_mask_writel(dw_dev, reg, DW_HDMI_CEC_CTRL,
+ DW_HDMI_FRAME_TYP_OFFSET,
+ DW_HDMI_FRAME_TYP_MASK);
+ hdmi_mask_writel(dw_dev, 0x1, DW_HDMI_CEC_CTRL,
+ DW_HDMI_SEND_OFFSET,
+ DW_HDMI_SEND_MASK);
+ return 0;
+}
+
+static const struct cec_adap_ops dw_hdmi_cec_adap_ops = {
+ .adap_enable = dw_hdmi_cec_adap_enable,
+ .adap_log_addr = dw_hdmi_cec_adap_log_addr,
+ .adap_transmit = dw_hdmi_cec_adap_transmit,
+};
+
+static void dw_hdmi_cec_irq_handler(struct dw_hdmi_dev *dw_dev)
+{
+ u32 cec_ists = dw_hdmi_get_int_val(dw_dev, DW_HDMI_AUD_CEC_ISTS,
+ DW_HDMI_AUD_CEC_IEN);
+
+ dw_hdmi_cec_clear_ints(dw_dev);
+
+ if (cec_ists) {
+ dw_hdmi_cec_tx_raw_status(dw_dev, cec_ists);
+ if (cec_ists & DW_HDMI_EOM_ISTS)
+ dw_hdmi_cec_received_msg(dw_dev);
+ }
+}
+#endif /* CONFIG_VIDEO_DWC_HDMI_RX_CEC */
+
static u8 dw_hdmi_get_curr_vic(struct dw_hdmi_dev *dw_dev, bool *is_hdmi_vic)
{
u8 vic = hdmi_mask_readl(dw_dev, DW_HDMI_PDEC_AVI_PB,
@@ -2061,6 +2258,10 @@ static irqreturn_t dw_hdmi_irq_handler(int irq, void *dev_data)
}
}

+#if IS_ENABLED(CONFIG_VIDEO_DWC_HDMI_RX_CEC)
+ dw_hdmi_cec_irq_handler(dw_dev);
+#endif /* CONFIG_VIDEO_DWC_HDMI_RX_CEC */
+
return IRQ_HANDLED;
}

@@ -2558,14 +2759,27 @@ static int dw_hdmi_set_edid(struct v4l2_subdev *sd, struct v4l2_edid *edid)
struct dw_hdmi_dev *dw_dev = to_dw_dev(sd);
int input_count = dw_dev->config->phy->input_count;
int size, ret;
+ u16 phys_addr;
u32 *tmp;

memset(edid->reserved, 0, sizeof(edid->reserved));

- if (edid->pad >= input_count || !edid->edid || !edid->blocks)
+ if (edid->pad >= input_count || !edid->edid)
return -EINVAL;
if (edid->start_block != 0)
return -EINVAL;
+ if (!edid->blocks) {
+ phys_addr = CEC_PHYS_ADDR_INVALID;
+ goto set_phys_addr;
+ }
+
+ /* get the source physical address (PA) from edid */
+ phys_addr = cec_get_edid_phys_addr(edid->edid, edid->blocks * 128,
+ NULL);
+ /* get the own physical address getting the parent of Source PA */
+ ret = v4l2_phys_addr_validate(phys_addr, &phys_addr, NULL);
+ if (ret)
+ return ret;

/* Clear old EDID */
size = dw_dev->curr_edid_blocks[edid->pad] * 128;
@@ -2594,7 +2808,9 @@ static int dw_hdmi_set_edid(struct v4l2_subdev *sd, struct v4l2_edid *edid)
if (ret)
return ret;

+set_phys_addr:
dw_dev->curr_edid_blocks[edid->pad] = edid->blocks;
+ cec_s_phys_addr(dw_dev->cec_adap, phys_addr, false);
return 0;
}

@@ -2827,15 +3043,33 @@ static int dw_hdmi_subscribe_event(struct v4l2_subdev *sd, struct v4l2_fh *fh,
static int dw_hdmi_registered(struct v4l2_subdev *sd)
{
struct dw_hdmi_dev *dw_dev = to_dw_dev(sd);
+ int ret;
+
+ ret = cec_register_adapter(dw_dev->cec_adap, dw_dev->dev);
+ if (ret) {
+ dev_err(dw_dev->dev, "failed to register CEC adapter\n");
+ goto err_adapter;
+ }
+ cec_s_phys_addr(dw_dev->cec_adap, 0, false);
+ if (dw_dev->cec_adap)
+ dev_info(dw_dev->dev,
+ "CEC adapter %s registered for HDMI input\n",
+ dev_name(&dw_dev->cec_adap->devnode.dev));

dw_dev->registered = true;
return 0;
+
+err_adapter:
+ cec_delete_adapter(dw_dev->cec_adap);
+ return ret;
}

static void dw_hdmi_unregistered(struct v4l2_subdev *sd)
{
struct dw_hdmi_dev *dw_dev = to_dw_dev(sd);

+ cec_unregister_adapter(dw_dev->cec_adap);
+
dw_dev->registered = false;
}

@@ -3196,10 +3430,29 @@ static int dw_hdmi_rx_probe(struct platform_device *pdev)
if (ret)
goto err_phy_exit;

+ /* CEC */
+#if IS_ENABLED(CONFIG_VIDEO_DWC_HDMI_RX_CEC)
+ dw_dev->cec_adap = cec_allocate_adapter(&dw_hdmi_cec_adap_ops,
+ dw_dev, dev_name(dev),
+ (CEC_CAP_DEFAULTS |
+ CEC_CAP_NEEDS_HPD |
+ CEC_CAP_CONNECTOR_INFO),
+ DW_HDMI_CEC_MAX_LOG_ADDRS);
+ ret = PTR_ERR_OR_ZERO(dw_dev->cec_adap);
+ if (ret) {
+ dev_err(dev, "failed to allocate CEC adapter\n");
+ goto err_cec;
+ }
+
+ dev_info(dev, "CEC is enabled\n");
+#else
+ dev_info(dev, "CEC is disabled\n");
+#endif /* CONFIG_VIDEO_DWC_HDMI_RX_CEC */
+
ret = v4l2_async_register_subdev(sd);
if (ret) {
dev_err(dev, "failed to register subdev\n");
- goto err_phy_exit;
+ goto err_cec;
}

/* Fill initial format settings */
@@ -3232,6 +3485,8 @@ static int dw_hdmi_rx_probe(struct platform_device *pdev)

err_subdev:
v4l2_async_unregister_subdev(sd);
+err_cec:
+ cec_delete_adapter(dw_dev->cec_adap);
err_phy_exit:
dw_hdmi_phy_exit(dw_dev);
err_hdl:
diff --git a/drivers/media/platform/dwc/dw-hdmi-rx.h b/drivers/media/platform/dwc/dw-hdmi-rx.h
index f0ea1d4..775b7a9 100644
--- a/drivers/media/platform/dwc/dw-hdmi-rx.h
+++ b/drivers/media/platform/dwc/dw-hdmi-rx.h
@@ -325,6 +325,25 @@

#define DW_HDMI_HDCP22_STATUS 0x08fc

+/* id_audio_and_cec_interrupt Registers */
+#define DW_HDMI_AUD_CEC_IEN_CLR 0x0f90
+#define DW_HDMI_AUD_CEC_IEN_SET 0x0f94
+
+#define DW_HDMI_AUD_CEC_ISTS 0x0f98
+#define DW_HDMI_WAKEUPCTRL_ISTS BIT(22)
+#define DW_HDMI_ERROR_FOLL_ISTS BIT(21)
+#define DW_HDMI_ERROR_INIT_ISTS BIT(20)
+#define DW_HDMI_ARBLST_ISTS BIT(19)
+#define DW_HDMI_NACK_ISTS BIT(18)
+#define DW_HDMI_EOM_ISTS BIT(17)
+#define DW_HDMI_DONE_ISTS BIT(16)
+#define DW_HDMI_SCK_STABLE_ISTS BIT(1)
+#define DW_HDMI_CTSN_CNT_ISTS BIT(0)
+
+#define DW_HDMI_AUD_CEC_IEN 0x0f9c
+#define DW_HDMI_AUD_CEC_ICLR 0x0fa0
+#define DW_HDMI_AUD_CEC_ISET 0x0fa4
+
/* id_mode_detection_interrupt Registers */
#define DW_HDMI_MD_IEN_CLR 0x0fc0
#define DW_HDMI_MD_IEN_SET 0x0fc4
@@ -426,6 +445,44 @@
#define DW_HDMI_HDMI_ENABLE_MASK BIT(2)
#define DW_HDMI_HDMI_ENABLE_OFFSET 2

+/* id_cec Registers */
+#define DW_HDMI_CEC_CTRL 0x1f00
+#define DW_HDMI_STANDBY_MASK BIT(4)
+#define DW_HDMI_STANDBY_OFFSET 4
+#define DW_HDMI_BC_NACK_MASK BIT(3)
+#define DW_HDMI_BC_NACK_OFFSET 3
+#define DW_HDMI_FRAME_TYP_MASK GENMASK(2, 1)
+#define DW_HDMI_FRAME_TYP_OFFSET 1
+#define DW_HDMI_SEND_MASK BIT(0)
+#define DW_HDMI_SEND_OFFSET 0
+
+#define DW_HDMI_CEC_MASK 0x1f08
+#define DW_HDMI_WAKEUP_MASK BIT(6)
+#define DW_HDMI_WAKEUP_OFFSET 6
+#define DW_HDMI_ERROR_FLOW_MASK BIT(5)
+#define DW_HDMI_ERROR_FLOW_OFFSET 5
+#define DW_HDMI_ERROR_INITITATOR_MASK BIT(4)
+#define DW_HDMI_ERROR_INITITATOR_OFFSET 4
+#define DW_HDMI_ARB_LOST_MASK BIT(3)
+#define DW_HDMI_ARB_LOST_OFFSET 3
+#define DW_HDMI_NACK_MASK BIT(2)
+#define DW_HDMI_NACK_OFFSET 2
+#define DW_HDMI_EOM_MASK BIT(1)
+#define DW_HDMI_EOM_OFFSET 1
+#define DW_HDMI_DONE_MASK BIT(0)
+#define DW_HDMI_DONE_OFFSET 0
+
+#define DW_HDMI_CEC_ADDR_L 0x1f14
+#define DW_HDMI_CEC_ADDR_H 0x1f18
+#define DW_HDMI_CEC_TX_CNT 0x1f1c
+#define DW_HDMI_CEC_RX_CNT 0x1f20
+#define DW_HDMI_CEC_TX_DATA(i) (0x1f40 + ((i) * 4))
+#define DW_HDMI_CEC_TX_DATA_MAX 16
+#define DW_HDMI_CEC_RX_DATA(i) (0x1f80 + ((i) * 4))
+#define DW_HDMI_CEC_RX_DATA_MAX 16
+#define DW_HDMI_CEC_LOCK 0x1fc0
+#define DW_HDMI_CEC_WAKEUPCTRL 0x1fc4
+
/* id_cbus Registers */
#define DW_HDMI_CBUSIOCTRL 0x3020
#define DW_HDMI_DATAPATH_CBUSZ_MASK BIT(24)
--
2.7.4