Hello,

On 4/26/26 08:20, Cristian Ciocaltea wrote:
Enable HDMI 2.0 display modes (e.g. 4K@60Hz) by adding SCDC management
for the high TMDS clock ratio and scrambling, required when the TMDS
character rate exceeds the 340 MHz HDMI 1.4b limit.

A periodic work item monitors the sink's scrambling status to recover
from sink-side resets.  On hotplug detect, if SCDC scrambling state is
out of sync with the driver, trigger a CRTC reset to re-establish the
link.

Reject modes requiring TMDS rates above 600 MHz, as those fall in the
HDMI 2.1 FRL domain which is not supported. In no_hpd configurations,
further restrict to 340 MHz since SCDC requires a connected sink.

Tested-by: Diederik de Haas <[email protected]>
Tested-by: Maud Spierings <[email protected]>
Signed-off-by: Cristian Ciocaltea <[email protected]>
---
  drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 188 ++++++++++++++++++++++++---
  1 file changed, 172 insertions(+), 16 deletions(-)

diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c 
b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c
index d649a1cf07f5..c482a8e7da25 100644
--- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c
+++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c
@@ -2,6 +2,7 @@
  /*
   * Copyright (c) 2021-2022 Rockchip Electronics Co., Ltd.
   * Copyright (c) 2024 Collabora Ltd.
+ * Copyright (c) 2025 Amazon.com, Inc. or its affiliates.
   *
   * Author: Algea Cao <[email protected]>
   * Author: Cristian Ciocaltea <[email protected]>
@@ -21,9 +22,11 @@
  #include <drm/display/drm_hdmi_helper.h>
  #include <drm/display/drm_hdmi_cec_helper.h>
  #include <drm/display/drm_hdmi_state_helper.h>
+#include <drm/display/drm_scdc_helper.h>
  #include <drm/drm_atomic.h>
  #include <drm/drm_atomic_helper.h>
  #include <drm/drm_bridge.h>
+#include <drm/drm_bridge_helper.h>
  #include <drm/drm_connector.h>
  #include <drm/drm_edid.h>
  #include <drm/drm_modes.h>
@@ -39,7 +42,9 @@
  #define DDC_SEGMENT_ADDR      0x30
#define HDMI14_MAX_TMDSCLK 340000000
+#define HDMI20_MAX_TMDSRATE    600000000
+#define SCDC_MAX_SOURCE_VERSION 0x1
  #define SCRAMB_POLL_DELAY_MS  3000
/*
@@ -164,6 +169,11 @@ struct dw_hdmi_qp {
        } phy;
unsigned long ref_clk_rate;
+
+       struct drm_connector *curr_conn;
+       struct delayed_work scramb_work;
+       bool scramb_enabled;
+
        struct regmap *regm;
        int main_irq;
@@ -749,28 +759,124 @@ static struct i2c_adapter *dw_hdmi_qp_i2c_adapter(struct dw_hdmi_qp *hdmi)
        return adap;
  }
+static bool dw_hdmi_qp_supports_scrambling(struct drm_display_info *display)
+{
+       if (!display->is_hdmi)
+               return false;
+
+       return display->hdmi.scdc.supported &&
+               display->hdmi.scdc.scrambling.supported;
+}
+
+static int dw_hdmi_qp_set_scramb(struct dw_hdmi_qp *hdmi)
+{
+       bool done;
+
+       dev_dbg(hdmi->dev, "set scrambling\n");
+
+       done = drm_scdc_set_high_tmds_clock_ratio(hdmi->curr_conn, true);
+       if (!done)
+               return -EIO;
+
+       done = drm_scdc_set_scrambling(hdmi->curr_conn, true);
+       if (!done) {
+               drm_scdc_set_high_tmds_clock_ratio(hdmi->curr_conn, false);
+               return -EIO;
+       }
+
+       schedule_delayed_work(&hdmi->scramb_work,
+                             msecs_to_jiffies(SCRAMB_POLL_DELAY_MS));
+       return 0;
+}
+
+static void dw_hdmi_qp_scramb_work(struct work_struct *work)
+{
+       struct dw_hdmi_qp *hdmi = container_of(to_delayed_work(work),
+                                              struct dw_hdmi_qp,
+                                              scramb_work);
+       if (READ_ONCE(hdmi->scramb_enabled) &&
+           !drm_scdc_get_scrambling_status(hdmi->curr_conn))
+               dw_hdmi_qp_set_scramb(hdmi);
+}
+
+static void dw_hdmi_qp_enable_scramb(struct dw_hdmi_qp *hdmi)
+{
+       int ret;
+       u8 ver;
+
+       if (!dw_hdmi_qp_supports_scrambling(&hdmi->curr_conn->display_info))
+               return;
+
+       ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_SINK_VERSION, &ver);
+       if (ret) {
+               dev_err(hdmi->dev, "Failed to read SCDC_SINK_VERSION: %d\n", 
ret);
+               return;
+       }
+
+       ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_SOURCE_VERSION,
+                             min_t(u8, ver, SCDC_MAX_SOURCE_VERSION));
+       if (ret) {
+               dev_err(hdmi->dev, "Failed to write SCDC_SOURCE_VERSION: %d\n", 
ret);
+               return;
+       }
+
+       WRITE_ONCE(hdmi->scramb_enabled, true);
+
+       ret = dw_hdmi_qp_set_scramb(hdmi);
+       if (ret) {
+               hdmi->scramb_enabled = false;
+               return;
+       }
+
+       dw_hdmi_qp_write(hdmi, 1, SCRAMB_CONFIG0);
+
+       /* Wait at least 1 ms before resuming TMDS transmission */
+       usleep_range(1000, 5000);
+}
+
+static void dw_hdmi_qp_disable_scramb(struct dw_hdmi_qp *hdmi)
+{
+       if (!hdmi->scramb_enabled)
+               return;
+
+       dev_dbg(hdmi->dev, "disable scrambling\n");
+
+       WRITE_ONCE(hdmi->scramb_enabled, false);
+       cancel_delayed_work_sync(&hdmi->scramb_work);
+
+       dw_hdmi_qp_write(hdmi, 0, SCRAMB_CONFIG0);
+
+       if (hdmi->curr_conn->status == connector_status_connected) {
+               drm_scdc_set_scrambling(hdmi->curr_conn, false);
+               drm_scdc_set_high_tmds_clock_ratio(hdmi->curr_conn, false);
+       }
+}
+
  static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge,
                                            struct drm_atomic_state *state)
  {
        struct dw_hdmi_qp *hdmi = bridge->driver_private;
        struct drm_connector_state *conn_state;
-       struct drm_connector *connector;
        unsigned int op_mode;
- connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder);
-       if (WARN_ON(!connector))
+       hdmi->curr_conn = drm_atomic_get_new_connector_for_encoder(state,
+                                                                  
bridge->encoder);
+       if (WARN_ON(!hdmi->curr_conn))
                return;
- conn_state = drm_atomic_get_new_connector_state(state, connector);
+       conn_state = drm_atomic_get_new_connector_state(state, hdmi->curr_conn);
        if (WARN_ON(!conn_state))
                return;
- if (connector->display_info.is_hdmi) {
+       if (hdmi->curr_conn->display_info.is_hdmi) {
                dev_dbg(hdmi->dev, "%s mode=HDMI %s rate=%llu bpc=%u\n", 
__func__,
                        
drm_hdmi_connector_get_output_format_name(conn_state->hdmi.output_format),
                        conn_state->hdmi.tmds_char_rate, 
conn_state->hdmi.output_bpc);
                op_mode = 0;
                hdmi->tmds_char_rate = conn_state->hdmi.tmds_char_rate;
+
+               if (conn_state->hdmi.tmds_char_rate > HDMI14_MAX_TMDSCLK)
+                       dw_hdmi_qp_enable_scramb(hdmi);
        } else {
                dev_dbg(hdmi->dev, "%s mode=DVI\n", __func__);
                op_mode = OPMODE_DVI;
@@ -781,7 +887,7 @@ static void dw_hdmi_qp_bridge_atomic_enable(struct 
drm_bridge *bridge,
        dw_hdmi_qp_mod(hdmi, HDCP2_BYPASS, HDCP2_BYPASS, HDCP2LOGIC_CONFIG0);
        dw_hdmi_qp_mod(hdmi, op_mode, OPMODE_DVI, LINK_CONFIG0);
- drm_atomic_helper_connector_hdmi_update_infoframes(connector, state);
+       drm_atomic_helper_connector_hdmi_update_infoframes(hdmi->curr_conn, 
state);
  }
static void dw_hdmi_qp_bridge_atomic_disable(struct drm_bridge *bridge,
@@ -791,14 +897,49 @@ static void dw_hdmi_qp_bridge_atomic_disable(struct 
drm_bridge *bridge,
hdmi->tmds_char_rate = 0; + dw_hdmi_qp_disable_scramb(hdmi);
+
+       hdmi->curr_conn = NULL;
        hdmi->phy.ops->disable(hdmi, hdmi->phy.data);
  }
-static enum drm_connector_status
-dw_hdmi_qp_bridge_detect(struct drm_bridge *bridge, struct drm_connector 
*connector)
+static int dw_hdmi_qp_reset_crtc(struct dw_hdmi_qp *hdmi,
+                                struct drm_connector *connector,
+                                struct drm_modeset_acquire_ctx *ctx)
+{
+       u8 config;
+       int ret;
+
+       ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_TMDS_CONFIG, &config);
+       if (ret < 0) {
+               dev_err(hdmi->dev, "Failed to read TMDS config: %d\n", ret);
+               return ret;
+       }
+
+       if (!!(config & SCDC_SCRAMBLING_ENABLE) == hdmi->scramb_enabled)
+               return 0;
+
+       drm_atomic_helper_connector_hdmi_hotplug(connector,
+                                                connector_status_connected);
+       /*
+        * Conform to HDMI 2.0 spec by ensuring scrambled data is not sent
+        * before configuring the sink scrambling, as well as suspending any
+        * TMDS transmission while changing the TMDS clock rate in the sink.
+        */
+
+       dev_dbg(hdmi->dev, "resetting crtc\n");
+
+       return drm_bridge_helper_reset_crtc(&hdmi->bridge, ctx);
+}
+
+static int dw_hdmi_qp_bridge_detect_ctx(struct drm_bridge *bridge,
+                                       struct drm_connector *connector,
+                                       struct drm_modeset_acquire_ctx *ctx)
  {
        struct dw_hdmi_qp *hdmi = bridge->driver_private;
+       enum drm_connector_status status;
        const struct drm_edid *drm_edid;
+       int ret;
if (hdmi->no_hpd) {
                drm_edid = drm_edid_read_ddc(connector, bridge->ddc);
@@ -808,7 +949,20 @@ dw_hdmi_qp_bridge_detect(struct drm_bridge *bridge, struct 
drm_connector *connec
                        return connector_status_disconnected;
        }
- return hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data);
+       status = hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data);
+
+       dev_dbg(hdmi->dev, "%s status=%d scramb=%d\n", __func__,
+               status, hdmi->scramb_enabled);
+
+       if (status == connector_status_connected && hdmi->scramb_enabled) {
+               ret = dw_hdmi_qp_reset_crtc(hdmi, connector, ctx);
+               if (ret == -EDEADLK)
+                       return ret;
+               if (ret < 0)
+                       status = connector_status_unknown;
+       }
+
+       return status;
  }
static const struct drm_edid *
@@ -832,12 +986,12 @@ dw_hdmi_qp_bridge_tmds_char_rate_valid(const struct 
drm_bridge *bridge,
  {
        struct dw_hdmi_qp *hdmi = bridge->driver_private;
- /*
-        * TODO: when hdmi->no_hpd is 1 we must not support modes that
-        * require scrambling, including every mode with a clock above
-        * HDMI14_MAX_TMDSCLK.
-        */
-       if (rate > HDMI14_MAX_TMDSCLK) {

The locally defined HDMI14_MAX_TMDSCLK has been removed, we should now use 
HDMI_1_3_CHAR_RATE_MAX,
and I think we should apply the same change to HDMI20_MAX_TMDSRATE


+       if (hdmi->no_hpd && rate > HDMI14_MAX_TMDSCLK) {
+               dev_dbg(hdmi->dev, "Unsupported TMDS char rate in no_hpd mode: 
%lld\n", rate);
+               return MODE_CLOCK_HIGH;
+       }
+
+       if (rate > HDMI20_MAX_TMDSRATE) {
                dev_dbg(hdmi->dev, "Unsupported TMDS char rate: %lld\n", rate);
                return MODE_CLOCK_HIGH;
        }
@@ -1197,7 +1351,7 @@ static const struct drm_bridge_funcs 
dw_hdmi_qp_bridge_funcs = {
        .atomic_reset = drm_atomic_helper_bridge_reset,
        .atomic_enable = dw_hdmi_qp_bridge_atomic_enable,
        .atomic_disable = dw_hdmi_qp_bridge_atomic_disable,
-       .detect = dw_hdmi_qp_bridge_detect,
+       .detect_ctx = dw_hdmi_qp_bridge_detect_ctx,
        .edid_read = dw_hdmi_qp_bridge_edid_read,
        .hdmi_tmds_char_rate_valid = dw_hdmi_qp_bridge_tmds_char_rate_valid,
        .hdmi_clear_avi_infoframe = dw_hdmi_qp_bridge_clear_avi_infoframe,
@@ -1287,6 +1441,8 @@ struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device 
*pdev,
        if (IS_ERR(hdmi))
                return ERR_CAST(hdmi);
+ INIT_DELAYED_WORK(&hdmi->scramb_work, dw_hdmi_qp_scramb_work);
+
        hdmi->dev = dev;
regs = devm_platform_ioremap_resource(pdev, 0);


Reply via email to