/* * hpd.c: hotplug detection functions. * * Copyright (c) 2015-2019, NVIDIA CORPORATION, All rights reserved. * Author: Animesh Kishore * * This software is licensed under the terms of the GNU General Public * License version 2, as published by the Free Software Foundation, and * may be copied, distributed, and modified under those terms. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #include #ifdef CONFIG_SWITCH #include #endif #include #include "dc_priv.h" #include "dc.h" #include "hpd.h" #define MAX_EDID_READ_ATTEMPTS 5 #define HPD_EDID_MAX_LENGTH 512 static const char * const state_names[] = { "Reset", "Check Plug", "Check EDID", "Disabled", "Enabled", "Wait for HPD reassert", "Recheck EDID", "Takeover from bootloader", }; static void set_hpd_state(struct tegra_hpd_data *data, int target_state, int resched_time); static void hpd_disable(struct tegra_hpd_data *data) { if (data->dc->connected) { pr_info("hpd: DC from connected to disconnected\n"); if (!data->dc->suspended) { data->dc->connected = false; tegra_dc_disable(data->dc); } } if ((!data->dc->suspended) && (data->dc->fb)) tegra_fb_update_monspecs(data->dc->fb, NULL, NULL); if (data->ops->disable) data->ops->disable(data->drv_data); tegra_dc_ext_process_hotplug(data->dc->ndev->id); tegra_dc_extcon_hpd_notify(data->dc); #ifdef CONFIG_SWITCH if (data->hpd_switch.name) { switch_set_state(&data->hpd_switch, 0); pr_info("hpd: hpd_switch 0\n"); } #endif } /* returns bytes read, or negative error */ static int read_edid_into_buffer(struct tegra_hpd_data *data, u8 *edid_data, size_t edid_data_len) { #define EXT_BLOCK_COUNT_OFFSET 0x7e int err, i; int extension_blocks; int max_ext_blocks = (edid_data_len / 128) - 1; err = tegra_edid_read_block(data->edid, 0, edid_data); if (err) { pr_err("hpd: tegra_edid_read_block(0) returned err %d\n", err); return err; } extension_blocks = edid_data[EXT_BLOCK_COUNT_OFFSET]; pr_info("hpd: extension_blocks = %d, max_ext_blocks = %d\n", extension_blocks, max_ext_blocks); if (extension_blocks > max_ext_blocks) extension_blocks = max_ext_blocks; for (i = 1; i <= extension_blocks; i++) { err = tegra_edid_read_block(data->edid, i, edid_data + i * 128); if (err) { pr_err( "hpd: tegra_edid_read_block(%d) returned err %d\n", i, err); return err; } } return i * 128; #undef EXT_BLOCK_COUNT_OFFSET } /* * re-read the edid and check to see if it has changed. Return 0 on a * successful E-EDID read, or non-zero error code on failure. If we succeed, * set match to 1 if the old E-EDID matches the new E-EDID. Otherwise, set * match to 0. */ static int recheck_edid(struct tegra_hpd_data *data, int *match) { int ret; u8 tmp[HPD_EDID_MAX_LENGTH] = {0}; ret = read_edid_into_buffer(data, tmp, sizeof(tmp)); pr_info("hpd: read_edid_into_buffer() returned %d\n", ret); if (ret > 0) { struct tegra_dc_edid *dc_edid = tegra_edid_get_data(data->edid); if (!dc_edid) { *match = 0; return 0; } pr_info("hpd: old edid len = %ld\n", (long int)dc_edid->len); *match = !!((ret == dc_edid->len) && !memcmp(tmp, dc_edid->buf, dc_edid->len)); if (*match == 0) { print_hex_dump(KERN_INFO, "tmp :", DUMP_PREFIX_ADDRESS, 16, 4, tmp, ret, true); print_hex_dump(KERN_INFO, "data:", DUMP_PREFIX_ADDRESS, 16, 4, dc_edid->buf, dc_edid->len, true); } tegra_edid_put_data(dc_edid); ret = 0; } return ret; } static void edid_read_notify(struct tegra_hpd_data *data) { tegra_fb_update_monspecs(data->dc->fb, &data->mon_spec, (data->ops->get_mode_filter) ? (data->ops->get_mode_filter(data->drv_data)) : NULL); tegra_fb_update_fix(data->dc->fb, &data->mon_spec); data->dc->connected = true; tegra_dc_ext_process_hotplug(data->dc->ndev->id); tegra_dc_extcon_hpd_notify(data->dc); #ifdef CONFIG_SWITCH if (data->hpd_switch.name) { switch_set_state(&data->hpd_switch, 1); pr_info("hpd: Display connected, hpd_switch 1\n"); } #endif if (data->ops->edid_notify) data->ops->edid_notify(data->drv_data); } static void hpd_reset_state(struct tegra_hpd_data *data) { /* * Shut everything down, and then schedule a check of the plug state. */ hpd_disable(data); set_hpd_state(data, STATE_PLUG, 0); } static void hpd_plug_state(struct tegra_hpd_data *data) { if (data->ops->get_hpd_state(data->drv_data)) { int tgt_state; /* * Looks like there is something plugged in. * Get ready to read the sink's EDID information. */ data->edid_reads = 0; if (data->hpd_resuming && data->dc->connected) tgt_state = STATE_RECHECK_EDID; else tgt_state = STATE_CHECK_EDID; set_hpd_state(data, tgt_state, data->timer_data.check_edid_delay_us); } else { /* * Nothing plugged in, so we are finished. Go to the * DONE_DISABLED state and stay there until the next HPD event. */ set_hpd_state(data, STATE_DONE_DISABLED, -1); } } static void edid_check_state(struct tegra_hpd_data *data) { memset(&data->mon_spec, 0, sizeof(data->mon_spec)); if (tegra_fb_is_console_enabled(data->dc->pdata)) { /* Set default videomode on dc before enabling it */ tegra_dc_set_default_videomode(data->dc); } if (!data->ops->get_hpd_state(data->drv_data)) { /* hpd dropped - stop EDID read */ pr_info("hpd: dropped, abort EDID read\n"); goto end_disabled; } if (data->ops->edid_read_prepare) if (!data->ops->edid_read_prepare(data->drv_data)) { pr_err("hpd: edid read prepare failed"); goto end_disabled; } if (tegra_edid_get_monspecs(data->edid, &data->mon_spec)) { /* * Failed to read EDID. If we still have retry attempts left, * schedule another attempt. Otherwise give up and just go to * the disabled state. */ data->edid_reads++; if (data->edid_reads >= MAX_EDID_READ_ATTEMPTS) { pr_info("hpd: EDID read failed %d times. Giving up.\n", data->edid_reads); goto end_disabled; } else { set_hpd_state(data, STATE_CHECK_EDID, data->timer_data.check_edid_delay_us); } return; } if (tegra_edid_get_eld(data->edid, &data->eld) < 0) { pr_err("hpd: error populating eld\n"); goto end_disabled; } data->eld_retrieved = true; if (data->ops->edid_ready) data->ops->edid_ready(data->drv_data); edid_read_notify(data); set_hpd_state(data, STATE_DONE_ENABLED, -1); return; end_disabled: data->eld_retrieved = false; hpd_disable(data); set_hpd_state(data, STATE_DONE_DISABLED, -1); } static void wait_for_hpd_reassert_state(struct tegra_hpd_data *data) { /* * Looks like HPD dropped and really did stay low. * Go ahead and disable the system. */ hpd_disable(data); set_hpd_state(data, STATE_DONE_DISABLED, -1); } static void edid_recheck_state(struct tegra_hpd_data *data) { int match = 0, tgt_state, timeout; tgt_state = STATE_HPD_RESET; timeout = 0; if (recheck_edid(data, &match)) { /* * Failed to read EDID. If we still have retry attempts left, * schedule another attempt. Otherwise give up and reset; */ data->edid_reads++; if (data->edid_reads >= MAX_EDID_READ_ATTEMPTS) { pr_info("hpd: EDID retry %d times. Giving up.\n", data->edid_reads); } else { tgt_state = STATE_RECHECK_EDID; timeout = data->timer_data.check_edid_delay_us; } } else { /* * Successful read! If the EDID is unchanged, just go back to * the DONE_ENABLED state and do nothing. If something changed, * just reset the whole system. */ if (match) { pr_info("hpd: No EDID change, taking no action.\n"); tgt_state = STATE_DONE_ENABLED; timeout = -1; } else { pr_info("hpd: EDID change, reset hpd state machine\n"); } } /* * During dc suspend/resume sequence put "hpd_resuming = false" * when new mointor connected/edid read failed. So state machine * does not go in loop between STATE_RECHECK_EDID and * STATE_HPD_RESET. * * If EDID changed in suspend, we do not want kernel to enable DC * during resume. Hence, set dc->reenable_on_resume to false. */ if (data->dc->suspended && !match) { data->dc->reenable_on_resume = false; data->hpd_resuming = false; tgt_state = STATE_HPD_RESET; timeout = 0; } set_hpd_state(data, tgt_state, timeout); /* * This callback should always proceed next hpd * state set. Thereby, callers can query regarding * next state. */ if (data->ops->edid_recheck) data->ops->edid_recheck(data->drv_data); } typedef void (*dispatch_func_t)(struct tegra_hpd_data *data); static const dispatch_func_t state_machine_dispatch[] = { hpd_reset_state, /* STATE_HPD_RESET */ hpd_plug_state, /* STATE_PLUG */ edid_check_state, /* STATE_CHECK_EDID */ NULL, /* STATE_DONE_DISABLED */ NULL, /* STATE_DONE_ENABLED */ wait_for_hpd_reassert_state, /* STATE_WAIT_FOR_HPD_REASSERT */ edid_recheck_state, /* STATE_RECHECK_EDID */ NULL, /* STATE_INIT_FROM_BOOTLOADER */ }; static void handle_hpd_evt(struct tegra_hpd_data *data, int cur_hpd) { struct tegra_hpd_timer_data *timer_data = &data->timer_data; int tgt_state; int timeout = 0; if (data->req_suspend) { pr_info("hpd: request suspend\n"); tgt_state = STATE_DONE_DISABLED; timeout = -1; data->req_suspend = false; } else if ((STATE_DONE_ENABLED == data->state) && !cur_hpd) { /* If HPD drops, wait for it to be re-asserted. */ tgt_state = STATE_WAIT_FOR_HPD_REASSERT; timeout = timer_data->reassert_delay_us; } else if (STATE_WAIT_FOR_HPD_REASSERT == data->state && cur_hpd) { /* * HPD dropped, but came back up. * * If reset_on_reassert is true, the state machine should reset * itself. Otherwise, re-check the EDID, and only reset if the * EDID has changed. */ if (timer_data->reset_on_reassert) { tgt_state = STATE_HPD_RESET; timeout = 0; } else { data->edid_reads = 0; tgt_state = STATE_RECHECK_EDID; timeout = timer_data->check_edid_delay_us; } } else if (STATE_DONE_ENABLED == data->state && cur_hpd) { if (!tegra_dc_ext_is_userspace_active()) { /* No userspace running. Enable DC with cached mode. */ pr_info("hpd: No EDID change. No userspace active. " "Using cached mode to initialize dc!\n"); data->dc->use_cached_mode = true; tgt_state = STATE_CHECK_EDID; } else { /* * Looks like HPD dropped but came back quickly. * * If reset_on_plug_bounce is true, reset the state * machine. Otherwise, ignore this event. */ if (timer_data->reset_on_plug_bounce) { tgt_state = STATE_HPD_RESET; timeout = 0; } else { pr_info("hpd: ignoring bouncing hpd\n"); return; } } } else if (STATE_INIT_FROM_BOOTLOADER == data->state && cur_hpd) { /* * We follow the same protocol as STATE_HPD_RESET in the * last branch here, but avoid actually entering that state so * we do not actively disable HPD. */ tgt_state = STATE_PLUG; timeout = timer_data->plug_stabilize_delay_us; } else { /* * Looks like there was HPD activity while we were neither * waiting for it to go away during steady state output, nor * looking for it to come back after such an event. Wait until * HPD has been steady, and then restart the state machine. */ tgt_state = STATE_HPD_RESET; timeout = cur_hpd ? timer_data->plug_stabilize_delay_us : timer_data->unplug_stabilize_delay_us; } set_hpd_state(data, tgt_state, timeout); } static void hpd_worker(struct work_struct *work) { int pending_hpd_evt, cur_hpd; struct tegra_hpd_data *data = container_of( to_delayed_work(work), struct tegra_hpd_data, dwork); /* * Observe and clear pending flag * and latch the current HPD state. */ rt_mutex_lock(&data->lock); pending_hpd_evt = data->pending_hpd_evt; data->pending_hpd_evt = 0; rt_mutex_unlock(&data->lock); cur_hpd = data->ops->get_hpd_state(data->drv_data); pr_info("hpd: state %d (%s), hpd %d, pending_hpd_evt %d\n", data->state, state_names[data->state], cur_hpd, pending_hpd_evt); if (pending_hpd_evt) { /* * If we were woken up because of HPD activity, just schedule * the next appropriate task and get out. */ handle_hpd_evt(data, cur_hpd); } else if (data->state < ARRAY_SIZE(state_machine_dispatch)) { dispatch_func_t func = state_machine_dispatch[data->state]; if (NULL == func) pr_warn("hpd: NULL state handler in state %d\n", data->state); else func(data); } else { pr_warn("hpd: unexpected state scheduled %d", data->state); } } static void sched_hpd_work(struct tegra_hpd_data *data, int resched_time) { cancel_delayed_work(&data->dwork); /* * system_nrt_wq is non-reentrant * and guarantees that any given work is * never executed parallelly by multiple CPUs */ if ((resched_time >= 0) && !data->shutdown) { queue_delayed_work(system_wq, &data->dwork, usecs_to_jiffies(resched_time)); } else { /* * We reach here when hpd state machine completes i.e * hpd state is ENABLE or DISABLE and during DC suspend * resume sequence. The hotplug detection also requires * this completion to be triggered. */ complete(&data->dc->hpd_complete); data->hpd_resuming = false; } } static void set_hpd_state(struct tegra_hpd_data *data, int target_state, int resched_time) { rt_mutex_lock(&data->lock); pr_info("hpd: switching from state %d (%s) to state %d (%s)\n", data->state, state_names[data->state], target_state, state_names[target_state]); data->state = target_state; /* * If the pending_hpd_evt flag is already set, don't bother to * reschedule the state machine worker. We should be able to assert * that there is a worker callback already scheduled, and that it is * scheduled to run immediately. This is particularly important when * making the transition to the steady state ENABLED or DISABLED states. * If an HPD event occurs while the worker is in flight, after the * worker checks the state of the pending HPD flag, and then the state * machine transitions to ENABLE or DISABLED, the system would end up * canceling the callback to handle the HPD event were it not for this * check. */ if (!data->pending_hpd_evt) sched_hpd_work(data, resched_time); rt_mutex_unlock(&data->lock); } void tegra_hpd_shutdown(struct tegra_hpd_data *data) { data->shutdown = 1; cancel_delayed_work_sync(&data->dwork); tegra_edid_destroy(data->edid); #ifdef CONFIG_SWITCH if (data->hpd_switch.name) { switch_dev_unregister(&data->hpd_switch); } #endif } int tegra_hpd_get_state(struct tegra_hpd_data *data) { int ret; rt_mutex_lock(&data->lock); ret = data->state; rt_mutex_unlock(&data->lock); return ret; } void tegra_hpd_set_pending_evt(struct tegra_hpd_data *data) { rt_mutex_lock(&data->lock); /* We always schedule work any time there is a pending HPD event */ data->pending_hpd_evt = 1; sched_hpd_work(data, 0); rt_mutex_unlock(&data->lock); } /* * Pushes the state machine to disable state. Typical usecase is * when device is being suspended. We do not send any notification. * Lest device might wake due to notifications. This function needs to work * with system wide power management. Hence, we do not explicitly disable * display subsystem as well. Power management is expected to do that. */ void tegra_hpd_suspend(struct tegra_hpd_data *data) { rt_mutex_lock(&data->lock); data->req_suspend = true; rt_mutex_unlock(&data->lock); tegra_hpd_set_pending_evt(data); } void tegra_hpd_init(struct tegra_hpd_data *data, struct tegra_dc *dc, void *drv_data, struct tegra_hpd_ops *ops) { BUG_ON(!dc || !data || !ops || !ops->get_hpd_state || !ops->edid_read); memset(&data->timer_data, 0, sizeof(data->timer_data)); if (ops->init) ops->init(drv_data); data->drv_data = drv_data; data->state = STATE_INIT_FROM_BOOTLOADER; data->pending_hpd_evt = 0; data->shutdown = 0; data->ops = ops; data->dc = dc; data->edid = tegra_edid_create(dc, data->ops->edid_read(data->drv_data)); if (IS_ERR_OR_NULL(data->edid)) { pr_err("hpd: edid create failed\n"); return; } tegra_dc_set_edid(dc, data->edid); data->eld_retrieved = false; data->edid_reads = 0; data->hpd_resuming = false; memset(&data->mon_spec, 0, sizeof(data->mon_spec)); rt_mutex_init(&data->lock); INIT_DELAYED_WORK(&data->dwork, hpd_worker); }