Re: [PATCH v2 2/4] [media] platform: Add Synopsys Designware HDMI RX Controller Driver

From: Hans Verkuil
Date: Fri Jun 16 2017 - 08:39:08 EST


Hi Jose,

Just a quick review of the new CEC code:

On 06/16/17 12:07, Jose Abreu wrote:
> This is an initial submission for the Synopsys Designware HDMI RX
> Controller Driver. This driver interacts with a phy driver so that
> a communication between them is created and a video pipeline is
> configured.
>
> The controller + phy pipeline can then be integrated into a fully
> featured system that can be able to receive video up to 4k@60Hz
> with deep color 48bit RGB, depending on the platform. Although,
> this initial version does not yet handle deep color modes.
>
> This driver was implemented as a standard V4L2 subdevice and its
> main features are:
> - Internal state machine that reconfigures phy until the
> video is not stable
> - JTAG communication with phy
> - Inter-module communication with phy driver
> - Debug write/read ioctls
>
> Some notes:
> - RX sense controller (cable connection/disconnection) must
> be handled by the platform wrapper as this is not integrated
> into the controller RTL
> - The same goes for EDID ROM's
> - ZCAL calibration is needed only in FPGA platforms, in ASIC
> this is not needed
> - The state machine is not an ideal solution as it creates a
> kthread but it is needed because some sources might not be
> very stable at sending the video (i.e. we must react
> accordingly).
>
> Signed-off-by: Jose Abreu <joabreu@xxxxxxxxxxxx>
> Cc: Carlos Palminha <palminha@xxxxxxxxxxxx>
> Cc: Mauro Carvalho Chehab <mchehab@xxxxxxxxxx>
> Cc: Hans Verkuil <hans.verkuil@xxxxxxxxx>
>
> Changes from v1:
> - Added support for CEC
> - Correct typo errors
> - Correctly detect interlaced video modes
> - Correct VIC parsing
> Changes from RFC:
> - Added support for HDCP 1.4
> - Fixup HDMI_VIC not being parsed (Hans)
> - Send source change signal when powering off (Hans)
> - Added a "wait stable delay"
> - Detect interlaced video modes (Hans)
> - Restrain g/s_register from reading/writing to HDCP regs (Hans)
>
> Signed-off-by: Jose Abreu <joabreu@xxxxxxxxxxxx>
> ---
> drivers/media/platform/dwc/Kconfig | 10 +
> drivers/media/platform/dwc/Makefile | 1 +
> drivers/media/platform/dwc/dw-hdmi-rx.c | 1764 +++++++++++++++++++++++++++++++
> drivers/media/platform/dwc/dw-hdmi-rx.h | 435 ++++++++
> include/media/dwc/dw-hdmi-rx-pdata.h | 63 ++
> 5 files changed, 2273 insertions(+)
> create mode 100644 drivers/media/platform/dwc/dw-hdmi-rx.c
> create mode 100644 drivers/media/platform/dwc/dw-hdmi-rx.h
> create mode 100644 include/media/dwc/dw-hdmi-rx-pdata.h
> diff --git a/drivers/media/platform/dwc/Kconfig b/drivers/media/platform/dwc/Kconfig
> index 361d38d..aba18af 100644
> --- a/drivers/media/platform/dwc/Kconfig
> +++ b/drivers/media/platform/dwc/Kconfig
> @@ -6,3 +6,13 @@ config VIDEO_DWC_HDMI_PHY_E405
>
> To compile this driver as a module, choose M here. The module
> will be called dw-hdmi-phy-e405.
> +
> +config VIDEO_DWC_HDMI_RX
> + tristate "Synopsys Designware HDMI Receiver driver"
> + depends on VIDEO_V4L2 && VIDEO_V4L2_SUBDEV_API
> + select CEC_CORE
> + help
> + Support for Synopsys Designware HDMI RX controller.
> +
> + To compile this driver as a module, choose M here. The module
> + will be called dw-hdmi-rx.

Add a separate config VIDEO_DWC_HDMI_RX_CEC to explicitly enable
CEC. CEC is an optional component of HDMI and supporting CEC should
be something you select in the kernel config.

<snip>

General question: is this CEC IP the same IP as is used in the HDMI
transmitter? If so, then it is not a good idea to duplicate the
code.

If they are different, then that should be noted in the source.

>
> +static void dw_hdmi_cec_enable_ints(struct dw_hdmi_dev *dw_dev)
> +{
> + u32 mask = HDMI_AUD_CEC_ISTS_DONE | HDMI_AUD_CEC_ISTS_EOM |
> + HDMI_AUD_CEC_ISTS_NACK | HDMI_AUD_CEC_ISTS_ARBLST |
> + HDMI_AUD_CEC_ISTS_ERROR_INIT | HDMI_AUD_CEC_ISTS_ERROR_FOLL;
> +
> + hdmi_writel(dw_dev, mask, HDMI_AUD_CEC_IEN_SET);
> + hdmi_writel(dw_dev, 0x0, HDMI_CEC_MASK);
> +}
> +
> +static void dw_hdmi_cec_disable_ints(struct dw_hdmi_dev *dw_dev)
> +{
> + hdmi_writel(dw_dev, ~0x0, HDMI_AUD_CEC_IEN_CLR);
> + hdmi_writel(dw_dev, ~0x0, HDMI_CEC_MASK);
> +}
> +
> +static void dw_hdmi_cec_clear_ints(struct dw_hdmi_dev *dw_dev)
> +{
> + hdmi_writel(dw_dev, ~0x0, HDMI_AUD_CEC_ICLR);
> +}
> +
> +static void dw_hdmi_cec_tx_raw_status(struct dw_hdmi_dev *dw_dev, u32 stat)
> +{
> + u32 error_mask = HDMI_AUD_CEC_ISTS_NACK |
> + HDMI_AUD_CEC_ISTS_ERROR_INIT;
> +
> + if (hdmi_readl(dw_dev, HDMI_CEC_CTRL) & HDMI_CEC_CTRL_SEND_MASK) {
> + dev_dbg(dw_dev->dev, "%s: tx is busy\n", __func__);
> + return;
> + }
> +
> + if (stat & HDMI_AUD_CEC_ISTS_ARBLST) {
> + dev_dbg(dw_dev->dev, "%s: arbitration lost\n", __func__);
> + cec_transmit_done(dw_dev->cec_adap, CEC_TX_STATUS_ARB_LOST,
> + 1, 0, 0, 0);
> + return;
> + }
> +
> + if (stat & HDMI_AUD_CEC_ISTS_DONE) {
> + dev_dbg(dw_dev->dev, "%s: transmission done\n", __func__);
> + cec_transmit_done(dw_dev->cec_adap, CEC_TX_STATUS_OK, 0, 0, 0, 0);
> + return;
> + }
> +
> + if (stat & error_mask) {
> + /* If we reached here we have an error */
> + u8 status = 0, nack_cnt = 0, error_cnt = 0;
> +
> + dev_dbg(dw_dev->dev, "%s: error found (stat=0x%x)\n", __func__,
> + stat);
> +
> + if (stat & HDMI_AUD_CEC_ISTS_NACK) {
> + status |= CEC_TX_STATUS_NACK;
> + nack_cnt++;
> + }
> +
> + if (stat & HDMI_AUD_CEC_ISTS_ERROR_INIT) {
> + status |= CEC_TX_STATUS_ERROR;
> + error_cnt++;
> + }

What does it mean if you have both NACK and ERROR_INIT flags set? Is that
an error or a NACK? You have to chose here which one you pick when calling
cec_transmit_done.

> +
> + cec_transmit_done(dw_dev->cec_adap, status, 0, nack_cnt, 0,
> + error_cnt);
> + 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, HDMI_CEC_RX_CNT);
> + if (!msg.len || msg.len > HDMI_CEC_RX_DATA_MAX)
> + return; /* its an invalid/non-existent message */

it's

> +
> + for (i = 0; i < msg.len; i++)
> + msg.msg[i] = hdmi_readl(dw_dev, HDMI_CEC_RX_DATA(i));
> +
> + hdmi_writel(dw_dev, 0x0, 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);
> +
> + if (!dw_dev->cec_enabled_adap && enable) {
> + hdmi_writel(dw_dev, 0x0, HDMI_CEC_LOCK);
> + dw_hdmi_cec_enable_ints(dw_dev);
> + } else if (dw_dev->cec_enabled_adap && !enable) {
> + dw_hdmi_cec_disable_ints(dw_dev);
> + dw_dev->cec_valid_addrs = 0;
> + }
> +
> + dw_dev->cec_enabled_adap = enable;
> + 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);
> + int i, free_idx = HDMI_CEC_MAX_LOG_ADDRS;
> + u32 tmp;
> +
> + if (!dw_dev->cec_enabled_adap)
> + return addr == CEC_LOG_ADDR_INVALID ? 0 : -EIO;

This can't be called unless the adapter is enabled. So this check
can be removed.

> +
> + if (addr == CEC_LOG_ADDR_INVALID) {
> + hdmi_writel(dw_dev, 0x0, HDMI_CEC_ADDR_L);
> + hdmi_writel(dw_dev, 0x0, HDMI_CEC_ADDR_H);
> + dw_dev->cec_valid_addrs = 0;
> + return 0;
> + }
> +
> + for (i = 0; i < HDMI_CEC_MAX_LOG_ADDRS; i++) {
> + bool is_valid = dw_dev->cec_valid_addrs & (1 << i);
> +
> + if (free_idx == HDMI_CEC_MAX_LOG_ADDRS && !is_valid)
> + free_idx = i;
> + if (is_valid && dw_dev->cec_addr[i] == addr)
> + return 0;
> + }
> +
> + if (i == HDMI_CEC_MAX_LOG_ADDRS) {
> + i = free_idx;
> + if (i == HDMI_CEC_MAX_LOG_ADDRS)
> + return -ENXIO;
> + }
> +
> + dw_dev->cec_addr[i] = addr;
> + dw_dev->cec_valid_addrs |= 1 << i;
> +
> + if (i >= 8) {
> + tmp = hdmi_readl(dw_dev, HDMI_CEC_ADDR_H);
> + tmp |= BIT(i - 8);
> + hdmi_writel(dw_dev, tmp, HDMI_CEC_ADDR_H);
> + } else {
> + tmp = hdmi_readl(dw_dev, HDMI_CEC_ADDR_L);
> + tmp |= BIT(i);
> + hdmi_writel(dw_dev, tmp, HDMI_CEC_ADDR_L);
> + }

This seems overengineered. HDMI_CEC_ADDR_H/L is just a bitmask, so just set the
bit corresponding to addr. I'm not sure what the point of cec_addr[] and cec_valid_addrs
is.

> +
> + 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;
> + int i;
> +
> + if (len > HDMI_CEC_TX_DATA_MAX) {
> + dev_err(dw_dev->dev, "%s: invalid len (%d)\n", __func__, len);
> + return -EINVAL;
> + }

Can't happen.

> +
> + if (hdmi_readl(dw_dev, HDMI_CEC_CTRL) & HDMI_CEC_CTRL_SEND_MASK) {
> + dev_err(dw_dev->dev, "%s: tx is busy\n", __func__);
> + return -EBUSY;

This should never happen, but it doesn't hurt to check.

> + }
> +
> + for (i = 0; i < HDMI_CEC_TX_DATA_MAX; i++)
> + hdmi_writel(dw_dev, 0x0, HDMI_CEC_TX_DATA(i));

Why clear this? Is it necessary?

> + for (i = 0; i < len; i++)
> + hdmi_writel(dw_dev, msg->msg[i], HDMI_CEC_TX_DATA(i));
> +
> + switch (signal_free_time) {
> + case 3:
> + reg = 0x0;
> + break;
> + case 7:
> + reg = 0x2;
> + break;
> + case 5:

Use the defines in media/cec.h instead of hardcoding these values.

> + default:
> + reg = 0x1;
> + break;
> + }
> +
> + hdmi_writel(dw_dev, len, HDMI_CEC_TX_CNT);
> + hdmi_mask_writel(dw_dev, reg, HDMI_CEC_CTRL,
> + HDMI_CEC_CTRL_FRAME_TYP_OFFSET,
> + HDMI_CEC_CTRL_FRAME_TYP_MASK);
> + hdmi_mask_writel(dw_dev, 0x1, HDMI_CEC_CTRL,
> + HDMI_CEC_CTRL_SEND_OFFSET,
> + HDMI_CEC_CTRL_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,
> +};

<snip>

> +static int dw_hdmi_log_status(struct v4l2_subdev *sd)
> +{
> + struct dw_hdmi_dev *dw_dev = to_dw_dev(sd);
> + struct v4l2_dv_timings timings;
> +
> + v4l2_info(sd, "--- Chip configuration ---\n");
> + v4l2_info(sd, "cfg_clk=%dMHz\n", dw_dev->cfg_clk);
> + v4l2_info(sd, "phy_drv=%s, phy_jtag_addr=0x%x\n", dw_dev->phy_drv,
> + dw_dev->phy_jtag_addr);
> +
> + v4l2_info(sd, "--- Chip status ---\n");
> + v4l2_info(sd, "selected_input=%d: signal=%d\n", dw_dev->selected_input,
> + has_signal(dw_dev, dw_dev->selected_input));
> + v4l2_info(sd, "configured_input=%d: signal=%d\n",
> + dw_dev->configured_input,
> + has_signal(dw_dev, dw_dev->configured_input));
> +
> + v4l2_info(sd, "--- CEC status ---\n");
> + if (dw_dev->cec_enabled_adap) {
> + int i;
> +
> + for (i = 0; i < HDMI_CEC_MAX_LOG_ADDRS; i++) {
> + bool is_valid = dw_dev->cec_valid_addrs & (1 << i);
> +
> + if (is_valid)
> + v4l2_info(sd, "cec_logical_address=0x%x\n",
> + dw_dev->cec_addr[i]);
> + }
> + }

I'm not sure how useful this is. The same information is available through
/sys/kernel/debug/cec/cecX/status.

And just running cec-ctl gives it as well.

Up to you, though.

> +
> + v4l2_info(sd, "--- Video status ---\n");
> + v4l2_info(sd, "type=%s, color_depth=%dbits",
> + hdmi_readl(dw_dev, HDMI_PDEC_STS) &
> + HDMI_PDEC_STS_DVIDET ? "dvi" : "hdmi",
> + dw_hdmi_get_colordepth(dw_dev));
> +
> + v4l2_info(sd, "--- Video timings ---\n");
> + if (dw_hdmi_query_dv_timings(sd, &timings))
> + v4l2_info(sd, "No video detected\n");
> + else
> + v4l2_print_dv_timings(sd->name, "Detected format: ",
> + &timings, true);
> + v4l2_print_dv_timings(sd->name, "Configured format: ",
> + &dw_dev->timings, true);
> + return 0;
> +}
> +

<snip>

> + /* CEC */
> + dw_dev->cec_adap = cec_allocate_adapter(&dw_hdmi_cec_adap_ops,
> + dw_dev, dev_name(dev), CEC_CAP_TRANSMIT |
> + CEC_CAP_PHYS_ADDR | CEC_CAP_LOG_ADDRS,

Add CEC_CAP_RC and CEC_CAP_PASSTHROUGH.

I'm not sure about CEC_CAP_PHYS_ADDR. The problem here is that this driver
doesn't handle the EDID, but without that it doesn't know what physical
address to use.

I wonder if the cec-notifier can be used for this, possibly with adaptations.
Relying on users to set the physical address is a last resort since it is very
painful to do so. cec-notifier was specifically designed to solve this.

> + 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_phy;
> + }

Regards,

Hans