When a cable is replugged while the upstream display pipeline is still active (e.g. a compositor holds the CRTC), the bridge can retrain the DP link and re-enable the video stream directly from the HPD interrupt work handler without requiring a full DRM atomic commit. This allows applications to recover display output after a cable replug.
Signed-off-by: Yashas D <[email protected]> --- drivers/gpu/drm/bridge/ti-sn65dsi86.c | 215 +++++++++++++++++++++----- 1 file changed, 179 insertions(+), 36 deletions(-) diff --git a/drivers/gpu/drm/bridge/ti-sn65dsi86.c b/drivers/gpu/drm/bridge/ti-sn65dsi86.c index d9bd4ef8f0e2..f6f930ca1519 100644 --- a/drivers/gpu/drm/bridge/ti-sn65dsi86.c +++ b/drivers/gpu/drm/bridge/ti-sn65dsi86.c @@ -212,6 +212,24 @@ struct ti_sn65dsi86 { struct mutex comms_mutex; struct mutex hpd_mutex; + /* + * bridge_enabled, cached_bpp and cached_mode are written by + * atomic_enable()/atomic_disable() and read by hpd_work(); all + * three are only ever accessed while holding hpd_mutex. + * + * Set true by atomic_enable(), false by atomic_disable(). When the + * cable is replugged while true, hpd_work can retrain the link + * directly without a DRM atomic commit. + */ + bool bridge_enabled; + unsigned int cached_bpp; + /* + * Copy of the last adjusted mode programmed by atomic_enable(). + */ + struct drm_display_mode cached_mode; + struct drm_display_mode hpd_mode; + struct work_struct hpd_work; + #if defined(CONFIG_OF_GPIO) struct gpio_chip gchip; DECLARE_BITMAP(gchip_output, SN_NUM_GPIOS); @@ -285,13 +303,32 @@ static struct drm_display_mode * get_new_adjusted_display_mode(struct drm_bridge *bridge, struct drm_atomic_commit *state) { - struct drm_connector *connector = + struct ti_sn65dsi86 *pdata = container_of(bridge, struct ti_sn65dsi86, + bridge); + struct drm_connector *connector; + struct drm_connector_state *conn_state; + struct drm_crtc_state *crtc_state; + + /* + * hpd_work calls this with state == NULL since it runs outside any + * DRM commit and holds no modeset lock. It has already taken its + * own private snapshot (hpd_mode) under hpd_mutex at the start of + * its run, so just return that instead of touching live CRTC state + */ + if (!state) + return &pdata->hpd_mode; + + connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder); - struct drm_connector_state *conn_state = + conn_state = drm_atomic_get_new_connector_state(state, connector); - struct drm_crtc_state *crtc_state = + crtc_state = drm_atomic_get_new_crtc_state(state, conn_state->crtc); + mutex_lock(&pdata->hpd_mutex); + drm_mode_copy(&pdata->cached_mode, &crtc_state->adjusted_mode); + mutex_unlock(&pdata->hpd_mutex); + return &crtc_state->adjusted_mode; } @@ -833,8 +870,16 @@ static void ti_sn_bridge_atomic_disable(struct drm_bridge *bridge, { struct ti_sn65dsi86 *pdata = bridge_to_ti_sn65dsi86(bridge); - /* disable video stream */ + /* + * Clear bridge_enabled and disable the video stream under hpd_mutex. + * hpd_work takes the same lock before its own VSTREAM_ENABLE write + * and rechecks bridge_enabled at that point, so whichever of the two + * runs last under the lock decides the final hardware state. + */ + mutex_lock(&pdata->hpd_mutex); + pdata->bridge_enabled = false; regmap_update_bits(pdata->regmap, SN_ENH_FRAME_REG, VSTREAM_ENABLE, 0); + mutex_unlock(&pdata->hpd_mutex); } static void ti_sn_bridge_set_dsi_rate(struct ti_sn65dsi86 *pdata, @@ -1092,34 +1137,27 @@ static int ti_sn_link_training(struct ti_sn65dsi86 *pdata, int dp_rate_idx, return ret; } -static void ti_sn_bridge_atomic_enable(struct drm_bridge *bridge, - struct drm_atomic_commit *state) +/* + * ti_sn_bridge_link_train - configure lanes, scrambler, data format and + * run DP link training. + * + * Shared by atomic_enable() (state from DRM commit) and hpd_work() + * (state == NULL, falls back to current CRTC state). + */ +static int ti_sn_bridge_link_train(struct ti_sn65dsi86 *pdata, + unsigned int bpp, + struct drm_atomic_commit *state) { - struct ti_sn65dsi86 *pdata = bridge_to_ti_sn65dsi86(bridge); - struct drm_connector *connector; const char *last_err_str = "No supported DP rate"; unsigned int valid_rates; int dp_rate_idx; unsigned int val; int ret = -EINVAL; - int max_dp_lanes; - unsigned int bpp; - - connector = drm_atomic_get_new_connector_for_encoder(state, - bridge->encoder); - if (!connector) { - dev_err_ratelimited(pdata->dev, "Could not get the connector\n"); - return; - } - - max_dp_lanes = ti_sn_get_max_lanes(pdata); - pdata->dp_lanes = min(pdata->dp_lanes, max_dp_lanes); /* DSI_A lane config */ val = CHA_DSI_LANES(SN_MAX_DP_LANES - pdata->dsi->lanes); regmap_update_bits(pdata->regmap, SN_DSI_LANES_REG, CHA_DSI_LANES_MASK, val); - regmap_write(pdata->regmap, SN_LN_ASSIGN_REG, pdata->ln_assign); regmap_update_bits(pdata->regmap, SN_ENH_FRAME_REG, LN_POLRS_MASK, pdata->ln_polrs << LN_POLRS_OFFSET); @@ -1139,7 +1177,6 @@ static void ti_sn_bridge_atomic_enable(struct drm_bridge *bridge, if (pdata->bridge.type == DRM_MODE_CONNECTOR_eDP) { drm_dp_dpcd_writeb(&pdata->aux, DP_EDP_CONFIGURATION_SET, DP_ALTERNATE_SCRAMBLER_RESET_ENABLE); - regmap_update_bits(pdata->regmap, SN_TRAINING_SETTING_REG, SCRAMBLE_DISABLE, 0); } else { @@ -1147,7 +1184,6 @@ static void ti_sn_bridge_atomic_enable(struct drm_bridge *bridge, SCRAMBLE_DISABLE, SCRAMBLE_DISABLE); } - bpp = ti_sn_bridge_get_bpp(connector); /* Set the DP output format (18 bpp or 24 bpp) */ val = bpp == 18 ? BPP_18_RGB : 0; regmap_update_bits(pdata->regmap, SN_DATA_FORMAT_REG, BPP_18_RGB, val); @@ -1159,28 +1195,130 @@ static void ti_sn_bridge_atomic_enable(struct drm_bridge *bridge, valid_rates = ti_sn_bridge_read_valid_rates(pdata); - /* Train until we run out of rates */ for (dp_rate_idx = ti_sn_bridge_calc_min_dp_rate_idx(pdata, state, bpp); dp_rate_idx < ARRAY_SIZE(ti_sn_bridge_dp_rate_lut); dp_rate_idx++) { if (!(valid_rates & BIT(dp_rate_idx))) continue; - ret = ti_sn_link_training(pdata, dp_rate_idx, &last_err_str); if (!ret) break; } - if (ret) { - DRM_DEV_ERROR(pdata->dev, "%s (%d)\n", last_err_str, ret); + + if (ret) + DRM_DEV_ERROR(pdata->dev, "link training failed: %s\n", + last_err_str); + + return ret; +} + +/* + * ti_sn_bridge_hpd_work - retrain the DP link on cable replug + * + * If bridge_enabled is true the upstream pipeline is still active so + * the link can be retrained directly without a DRM atomic commit, + * allowing applications to recover after a cable replug. + */ +static void ti_sn_bridge_hpd_work(struct work_struct *work) +{ + struct ti_sn65dsi86 *pdata = + container_of(work, struct ti_sn65dsi86, hpd_work); + struct drm_connector *connector; + unsigned int hpd_status; + int max_dp_lanes; + unsigned int bpp; + bool enabled; + int ret; + + pm_runtime_get_sync(pdata->dev); + + ret = regmap_read(pdata->regmap, SN_HPD_DISABLE_REG, &hpd_status); + if (ret || !(hpd_status & HPD_DEBOUNCED_STATE)) + goto notify; + + /* + * Snapshot what atomic_enable() published under hpd_mutex. + * hpd_mode is only ever written/read by hpd_work, which never runs + * concurrently with itself, so it is safe to use lock-free for the + * rest of this function. + */ + mutex_lock(&pdata->hpd_mutex); + enabled = pdata->bridge_enabled; + bpp = pdata->cached_bpp; + drm_mode_copy(&pdata->hpd_mode, &pdata->cached_mode); + mutex_unlock(&pdata->hpd_mutex); + + if (!enabled) + goto notify; + + max_dp_lanes = ti_sn_get_max_lanes(pdata); + mutex_lock(&pdata->hpd_mutex); + pdata->dp_lanes = min(pdata->dp_lanes, max_dp_lanes); + mutex_unlock(&pdata->hpd_mutex); + + ret = ti_sn_bridge_link_train(pdata, bpp, NULL); + if (ret) + goto notify; + + ti_sn_bridge_set_video_timings(pdata, NULL); + mutex_lock(&pdata->hpd_mutex); + if (pdata->bridge_enabled) + regmap_update_bits(pdata->regmap, SN_ENH_FRAME_REG, + VSTREAM_ENABLE, VSTREAM_ENABLE); + mutex_unlock(&pdata->hpd_mutex); + +notify: + pm_runtime_put_autosuspend(pdata->dev); + + if (pdata->bridge.hpd_data) { + connector = (struct drm_connector *)pdata->bridge.hpd_data; + drm_connector_helper_hpd_irq_event(connector); + } +} + +static void ti_sn_bridge_atomic_enable(struct drm_bridge *bridge, + struct drm_atomic_commit *state) +{ + struct ti_sn65dsi86 *pdata = bridge_to_ti_sn65dsi86(bridge); + struct drm_connector *connector; + int max_dp_lanes; + unsigned int bpp; + int ret; + + connector = drm_atomic_get_new_connector_for_encoder(state, + bridge->encoder); + if (!connector) { + dev_err_ratelimited(pdata->dev, "Could not get the connector\n"); return; } + max_dp_lanes = ti_sn_get_max_lanes(pdata); + mutex_lock(&pdata->hpd_mutex); + pdata->dp_lanes = min(pdata->dp_lanes, max_dp_lanes); + mutex_unlock(&pdata->hpd_mutex); + bpp = ti_sn_bridge_get_bpp(connector); + + ret = ti_sn_bridge_link_train(pdata, bpp, state); + if (ret) + return; + /* config video parameters */ ti_sn_bridge_set_video_timings(pdata, state); /* enable video stream */ regmap_update_bits(pdata->regmap, SN_ENH_FRAME_REG, VSTREAM_ENABLE, VSTREAM_ENABLE); + + /* + * Publish cached_bpp and bridge_enabled under hpd_mutex. hpd_work + * reads both under the same lock, which also makes every write this + * function made above (dp_lanes, cached_mode via + * get_new_adjusted_display_mode()) visible to it. + */ + mutex_lock(&pdata->hpd_mutex); + pdata->cached_bpp = bpp; + pdata->bridge_enabled = true; + mutex_unlock(&pdata->hpd_mutex); } static void ti_sn_bridge_atomic_pre_enable(struct drm_bridge *bridge, @@ -1270,6 +1408,14 @@ static void ti_sn_bridge_hpd_enable(struct drm_bridge *bridge) mutex_unlock(&pdata->hpd_mutex); if (client->irq) { + /* + * Clear stale status on all three IRQ registers before + * enabling, to avoid a spurious event. + */ + regmap_write(pdata->regmap, SN_IRQ_STATUS_REG, 0xFF); + regmap_write(pdata->regmap, SN_IRQ_STATUS2_REG, 0xFF); + regmap_write(pdata->regmap, SN_IRQ_STATUS3_REG, 0xFF); + ret = regmap_set_bits(pdata->regmap, SN_IRQ_EVENTS_EN_REG, HPD_REMOVAL_EN | HPD_INSERTION_EN | HPD_REPLUG_EN); if (ret) @@ -1294,6 +1440,8 @@ static void ti_sn_bridge_hpd_disable(struct drm_bridge *bridge) pdata->hpd_enabled = false; mutex_unlock(&pdata->hpd_mutex); + cancel_work_sync(&pdata->hpd_work); + pm_runtime_put_autosuspend(pdata->dev); } @@ -1410,16 +1558,10 @@ static irqreturn_t ti_sn_bridge_interrupt(int irq, void *private) return IRQ_NONE; } - /* Notify only the DP connector, not all connectors on the device. */ mutex_lock(&pdata->hpd_mutex); - if (pdata->hpd_enabled && hpd_event && pdata->bridge.hpd_data) { - struct drm_connector *connector = - (struct drm_connector *)pdata->bridge.hpd_data; - mutex_unlock(&pdata->hpd_mutex); - drm_connector_helper_hpd_irq_event(connector); - } else { - mutex_unlock(&pdata->hpd_mutex); - } + if (pdata->hpd_enabled && hpd_event) + schedule_work(&pdata->hpd_work); + mutex_unlock(&pdata->hpd_mutex); return IRQ_HANDLED; } @@ -2050,6 +2192,7 @@ static int ti_sn65dsi86_probe(struct i2c_client *client) mutex_init(&pdata->hpd_mutex); mutex_init(&pdata->comms_mutex); + INIT_WORK(&pdata->hpd_work, ti_sn_bridge_hpd_work); pdata->regmap = devm_regmap_init_i2c(client, &ti_sn65dsi86_regmap_config); -- 2.34.1
