Re: [PATCH v2 2/4] [media] platform: Add Synopsys Designware HDMI RX Controller Driver
From: Hans Verkuil <hidden>
Date: 2017-06-16 12:38:54
Also in:
linux-media, lkml
Hi Jose, Just a quick review of the new CEC code: On 06/16/17 12:07, Jose Abreu wrote:
quoted hunk ↗ jump to hunk
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 <redacted> Cc: Carlos Palminha <redacted> Cc: Mauro Carvalho Chehab <mchehab@kernel.org> Cc: Hans Verkuil <redacted> 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 <redacted> --- 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.hdiff --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