491 lines
14 KiB
C
491 lines
14 KiB
C
/*
|
|
* hdmi_state_machine.c
|
|
*
|
|
* HDMI library support functions for Nvidia Tegra processors.
|
|
*
|
|
* Copyright (C) 2012-2013 Google - http://www.google.com/
|
|
* Copyright (C) 2013, NVIDIA CORPORATION. All rights reserved.
|
|
* Authors: John Grossman <johngro@google.com>
|
|
* Authors: Mike J. Chen <mjchen@google.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 as published by
|
|
* the Free Software Foundation.
|
|
*
|
|
* 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.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <linux/kernel.h>
|
|
#include <video/tegrafb.h>
|
|
#include "dc_priv.h"
|
|
|
|
#include "hdmi_state_machine.h"
|
|
|
|
/************************************************************
|
|
*
|
|
* state machine internal constants
|
|
*
|
|
************************************************************/
|
|
#define MAX_EDID_READ_ATTEMPTS 5
|
|
#define HDMI_EDID_MAX_LENGTH 512
|
|
|
|
/* how long of an HPD drop before we consider it gone for good.
|
|
* this is mostly a preference to work around monitors users
|
|
* reported that occasionally drop HPD.
|
|
*/
|
|
#define HPD_DROP_TIMEOUT_MS 3000
|
|
#define HOTPLUG_ALIVE_MS 1000
|
|
#define CHECK_PLUG_STATE_DELAY_MS 10
|
|
#define CHECK_EDID_DELAY_MS 60
|
|
#define HPD_DEBOUNCE_MS 500
|
|
|
|
/************************************************************
|
|
*
|
|
* state machine internal state
|
|
*
|
|
************************************************************/
|
|
static DEFINE_RT_MUTEX(work_lock);
|
|
static struct hdmi_state_machine_worker_data {
|
|
struct delayed_work dwork;
|
|
struct tegra_dc_hdmi_data *hdmi;
|
|
int shutdown;
|
|
int state;
|
|
int edid_reads;
|
|
int pending_hpd_evt;
|
|
} work_state;
|
|
|
|
/************************************************************
|
|
*
|
|
* state machine internal methods
|
|
*
|
|
************************************************************/
|
|
static void hdmi_state_machine_sched_work_l(int resched_time)
|
|
{
|
|
cancel_delayed_work(&work_state.dwork);
|
|
if ((resched_time >= 0) && !work_state.shutdown)
|
|
queue_delayed_work(system_nrt_wq,
|
|
&work_state.dwork,
|
|
msecs_to_jiffies(resched_time));
|
|
}
|
|
|
|
static const char * const state_names[] = {
|
|
"Reset",
|
|
"Check Plug",
|
|
"Check EDID",
|
|
"Disabled",
|
|
"Hotplug Alive",
|
|
"Enabled",
|
|
"Wait for HPD reassert",
|
|
"Recheck EDID"
|
|
};
|
|
|
|
static void hdmi_state_machine_set_state_l(int target_state, int resched_time)
|
|
{
|
|
rt_mutex_lock(&work_lock);
|
|
|
|
pr_info("%s: switching from state %d (%s) to state %d (%s)\n",
|
|
__func__, work_state.state, state_names[work_state.state],
|
|
target_state, state_names[target_state]);
|
|
work_state.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 (!work_state.pending_hpd_evt)
|
|
hdmi_state_machine_sched_work_l(resched_time);
|
|
|
|
rt_mutex_unlock(&work_lock);
|
|
}
|
|
|
|
static void hdmi_state_machine_handle_hpd_l(int cur_hpd)
|
|
{
|
|
int tgt_state, timeout;
|
|
|
|
if ((HDMI_STATE_HOTPLUG_ALIVE == work_state.state) && !cur_hpd) {
|
|
/* Did HPD drop while we were in HOTPLUG_ALIVE? If so, hold
|
|
* steady and wait to see if it comes back.
|
|
*/
|
|
tgt_state = HDMI_STATE_DONE_WAIT_FOR_HPD_REASSERT;
|
|
timeout = HPD_DROP_TIMEOUT_MS;
|
|
} else
|
|
if ((HDMI_STATE_DONE_ENABLED == work_state.state) && !cur_hpd) {
|
|
/* HPD drop, wait to see if it re-asserts in HPD_DEBOUNCE_MS */
|
|
tgt_state = HDMI_STATE_DONE_WAIT_FOR_HPD_REASSERT;
|
|
timeout = HPD_DEBOUNCE_MS;
|
|
} else
|
|
if (HDMI_STATE_DONE_ENABLED == work_state.state && cur_hpd) {
|
|
/* Looks like HPD dropped but came back quickly, ignore it. */
|
|
pr_info("%s: ignoring bouncing hpd\n", __func__);
|
|
return;
|
|
} else
|
|
if (HDMI_STATE_DONE_WAIT_FOR_HPD_REASSERT == work_state.state &&
|
|
cur_hpd) {
|
|
/* Looks like HPD dropped and eventually came back. Re-read the
|
|
* EDID and reset the system only if the EDID has changed.
|
|
*/
|
|
work_state.edid_reads = 0;
|
|
tgt_state = HDMI_STATE_DONE_RECHECK_EDID;
|
|
timeout = CHECK_EDID_DELAY_MS;
|
|
} 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 for at least 40 mSec, then restart the
|
|
* state machine.
|
|
*/
|
|
tgt_state = HDMI_STATE_RESET;
|
|
timeout = 40;
|
|
}
|
|
|
|
hdmi_state_machine_set_state_l(tgt_state, timeout);
|
|
}
|
|
|
|
/************************************************************
|
|
*
|
|
* internal state handlers and dispatch table
|
|
*
|
|
************************************************************/
|
|
static void hdmi_disable_l(struct tegra_dc_hdmi_data *hdmi)
|
|
{
|
|
/* First tell HDMI so it can do any fast unplug actions. */
|
|
tegra_dc_hdmi_unplugged(hdmi);
|
|
|
|
tegra_nvhdcp_set_plug(hdmi->nvhdcp, 0);
|
|
if (hdmi->dc->enabled) {
|
|
pr_info("HDMI from connected to disconnected\n");
|
|
tegra_dc_disable(hdmi->dc);
|
|
}
|
|
hdmi->dc->connected = false;
|
|
if (hdmi->dc->fb)
|
|
tegra_fb_update_monspecs(hdmi->dc->fb, NULL, NULL);
|
|
tegra_dc_ext_process_hotplug(hdmi->dc->ndev->id);
|
|
}
|
|
|
|
static void handle_reset_l(struct tegra_dc_hdmi_data *hdmi)
|
|
{
|
|
/* Were we just reset? If so, shut everything down, then schedule a
|
|
* check of the plug state in the near future.
|
|
*/
|
|
hdmi_disable_l(hdmi);
|
|
hdmi_state_machine_set_state_l(HDMI_STATE_CHECK_PLUG_STATE,
|
|
CHECK_PLUG_STATE_DELAY_MS);
|
|
}
|
|
|
|
static void handle_check_plug_state_l(struct tegra_dc_hdmi_data *hdmi)
|
|
{
|
|
if (tegra_dc_hpd(work_state.hdmi->dc)) {
|
|
/* Looks like there is something plugged in.
|
|
* Get ready to read the sink's EDID information.
|
|
*/
|
|
work_state.edid_reads = 0;
|
|
|
|
hdmi_state_machine_set_state_l(HDMI_STATE_CHECK_EDID,
|
|
CHECK_EDID_DELAY_MS);
|
|
} else {
|
|
/* nothing plugged in, so we are finished. Go to the
|
|
* DONE_DISABLED state and stay there until the next HPD event.
|
|
* */
|
|
hdmi_disable_l(hdmi);
|
|
hdmi_state_machine_set_state_l(HDMI_STATE_DONE_DISABLED, -1);
|
|
}
|
|
}
|
|
|
|
static void handle_check_edid_l(struct tegra_dc_hdmi_data *hdmi)
|
|
{
|
|
struct fb_monspecs specs;
|
|
int ret;
|
|
|
|
memset(&specs, 0, sizeof(specs));
|
|
|
|
if (!hdmi->dc->fb) {
|
|
ret = wait_for_completion_timeout(&hdmi->dc->fb_ready,
|
|
msecs_to_jiffies(3000));
|
|
if (!ret) {
|
|
pr_err("Waiting for framebuffer ready timeout\n");
|
|
goto end_disabled;
|
|
}
|
|
}
|
|
|
|
#ifdef CONFIG_FRAMEBUFFER_CONSOLE
|
|
/* Set default videomode on dc before enabling it*/
|
|
tegra_dc_set_default_videomode(hdmi->dc);
|
|
#endif
|
|
|
|
if (!tegra_dc_hpd(work_state.hdmi->dc)) {
|
|
/* hpd dropped - stop EDID read */
|
|
pr_info("hpd == 0, aborting EDID read\n");
|
|
goto end_disabled;
|
|
}
|
|
|
|
if (tegra_edid_get_monspecs(hdmi->edid, &specs)) {
|
|
/* Failed to read EDID. If we still have retry attempts left,
|
|
* schedule another attempt. Otherwise give up and just go to
|
|
* the disabled state.
|
|
*/
|
|
work_state.edid_reads++;
|
|
if (work_state.edid_reads >= MAX_EDID_READ_ATTEMPTS) {
|
|
pr_info("Failed to read EDID after %d times. Giving up.\n",
|
|
work_state.edid_reads);
|
|
goto end_disabled;
|
|
} else {
|
|
hdmi_state_machine_set_state_l(HDMI_STATE_CHECK_EDID,
|
|
CHECK_EDID_DELAY_MS);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (tegra_edid_get_eld(hdmi->edid, &hdmi->eld) < 0) {
|
|
pr_err("error populating eld\n");
|
|
goto end_disabled;
|
|
}
|
|
hdmi->eld_retrieved = true;
|
|
|
|
pr_info("panel size %d by %d\n", specs.max_x, specs.max_y);
|
|
|
|
/* monitors like to lie about these but they are still useful for
|
|
* detecting aspect ratios
|
|
*/
|
|
hdmi->dc->out->h_size = specs.max_x * 1000;
|
|
hdmi->dc->out->v_size = specs.max_y * 1000;
|
|
|
|
hdmi->dvi = !(specs.misc & FB_MISC_HDMI);
|
|
|
|
tegra_fb_update_monspecs(hdmi->dc->fb, &specs,
|
|
tegra_dc_hdmi_mode_filter);
|
|
|
|
hdmi->dc->connected = true;
|
|
tegra_dc_ext_process_hotplug(hdmi->dc->ndev->id);
|
|
hdmi_state_machine_set_state_l(HDMI_STATE_HOTPLUG_ALIVE, HOTPLUG_ALIVE_MS);
|
|
|
|
return;
|
|
|
|
end_disabled:
|
|
hdmi->eld_retrieved = false;
|
|
hdmi_disable_l(hdmi);
|
|
hdmi_state_machine_set_state_l(HDMI_STATE_DONE_DISABLED, -1);
|
|
}
|
|
|
|
|
|
static void handle_hotplug_alive_l(struct tegra_dc_hdmi_data *hdmi)
|
|
{
|
|
/* After one second, move to HDMI_STATE_DONE_ENABLED, so it will no longer
|
|
* wait for reassert if dropped.
|
|
*/
|
|
hdmi_state_machine_set_state_l(HDMI_STATE_DONE_ENABLED, -1);
|
|
}
|
|
|
|
static void handle_wait_for_hpd_reassert_l(struct tegra_dc_hdmi_data *hdmi)
|
|
{
|
|
/* Looks like HPD dropped and really did stay low. Go ahead and reset
|
|
* the system.
|
|
*/
|
|
hdmi_state_machine_set_state_l(HDMI_STATE_RESET, 0);
|
|
}
|
|
|
|
/* returns bytes read, or negative error */
|
|
static int read_edid_into_buffer(struct tegra_dc_hdmi_data *hdmi,
|
|
u8 *edid_data, size_t edid_data_len)
|
|
{
|
|
int err, i;
|
|
int extension_blocks;
|
|
int max_ext_blocks = (edid_data_len / 128) - 1;
|
|
|
|
err = tegra_edid_read_block(hdmi->edid, 0, edid_data);
|
|
if (err) {
|
|
pr_err("tegra_edid_read_block(0) returned err %d\n", err);
|
|
return err;
|
|
}
|
|
extension_blocks = edid_data[0x7e];
|
|
pr_info("%s: extension_blocks = %d, max_ext_blocks = %d\n",
|
|
__func__, 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(hdmi->edid, i, edid_data + i * 128);
|
|
if (err) {
|
|
pr_err("tegra_edid_read_block(%d) returned err %d\n",
|
|
i, err);
|
|
return err;
|
|
}
|
|
}
|
|
return i * 128;
|
|
}
|
|
|
|
/* re-read the edid and check to see if it has changed. Return 0 on a
|
|
* successful E-EDID read, or a 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 hdmi_recheck_edid(struct tegra_dc_hdmi_data *hdmi, int *match)
|
|
{
|
|
int ret;
|
|
u8 tmp[HDMI_EDID_MAX_LENGTH] = {0};
|
|
|
|
ret = read_edid_into_buffer(hdmi, tmp, sizeof(tmp));
|
|
pr_info("%s: read_edid_into_buffer() returned %d\n", __func__, ret);
|
|
if (ret > 0) {
|
|
struct tegra_dc_edid *data = tegra_edid_get_data(hdmi->edid);
|
|
pr_info("old edid len = %d\n", data->len);
|
|
*match = ((ret == data->len) &&
|
|
!memcmp(tmp, data->buf, data->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, data->buf, data->len, true);
|
|
}
|
|
tegra_edid_put_data(data);
|
|
ret = 0;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void handle_recheck_edid_l(struct tegra_dc_hdmi_data *hdmi)
|
|
{
|
|
int match, tgt_state, timeout;
|
|
|
|
tgt_state = HDMI_STATE_RESET;
|
|
timeout = 0;
|
|
|
|
if (hdmi_recheck_edid(hdmi, &match)) {
|
|
/* Failed to read EDID. If we still have retry attempts left,
|
|
* schedule another attempt. Otherwise give up and reset;
|
|
*/
|
|
work_state.edid_reads++;
|
|
if (work_state.edid_reads >= MAX_EDID_READ_ATTEMPTS) {
|
|
pr_info("Failed to read EDID after %d times. Giving up.\n",
|
|
work_state.edid_reads);
|
|
} else {
|
|
tgt_state = HDMI_STATE_DONE_RECHECK_EDID;
|
|
timeout = CHECK_EDID_DELAY_MS;
|
|
}
|
|
} 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("No EDID change after HPD bounce, taking no action.\n");
|
|
tgt_state = HDMI_STATE_DONE_ENABLED;
|
|
tegra_dc_enable(hdmi->dc);
|
|
tegra_nvhdcp_set_plug(hdmi->nvhdcp, 0);
|
|
tegra_nvhdcp_set_plug(hdmi->nvhdcp, 1);
|
|
timeout = -1;
|
|
} else {
|
|
pr_info("EDID change after HPD bounce, resetting\n");
|
|
}
|
|
}
|
|
|
|
hdmi_state_machine_set_state_l(tgt_state, timeout);
|
|
}
|
|
|
|
typedef void (*dispatch_func_t)(struct tegra_dc_hdmi_data *hdmi);
|
|
static const dispatch_func_t state_machine_dispatch[] = {
|
|
handle_reset_l, /* STATE_RESET */
|
|
handle_check_plug_state_l, /* STATE_CHECK_PLUG_STATE */
|
|
handle_check_edid_l, /* STATE_CHECK_EDID */
|
|
NULL, /* STATE_DONE_DISABLED */
|
|
handle_hotplug_alive_l, /* STATE_HOTPLUG_ALIVE */
|
|
NULL, /* STATE_DONE_ENABLED */
|
|
handle_wait_for_hpd_reassert_l, /* STATE_DONE_WAIT_FOR_HPD_REASSERT */
|
|
handle_recheck_edid_l, /* STATE_DONE_RECHECK_EDID */
|
|
};
|
|
|
|
/************************************************************
|
|
*
|
|
* main state machine worker function
|
|
*
|
|
************************************************************/
|
|
static void hdmi_state_machine_worker(struct work_struct *work)
|
|
{
|
|
int pending_hpd_evt, cur_hpd;
|
|
|
|
/* Observe and clear the pending flag and latch the current HPD state.
|
|
*/
|
|
rt_mutex_lock(&work_lock);
|
|
pending_hpd_evt = work_state.pending_hpd_evt;
|
|
work_state.pending_hpd_evt = 0;
|
|
rt_mutex_unlock(&work_lock);
|
|
cur_hpd = tegra_dc_hpd(work_state.hdmi->dc);
|
|
|
|
pr_info("%s (tid %p): state %d (%s), hpd %d, pending_hpd_evt %d\n",
|
|
__func__, current, work_state.state,
|
|
state_names[work_state.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.
|
|
*/
|
|
hdmi_state_machine_handle_hpd_l(cur_hpd);
|
|
} else if (work_state.state < ARRAY_SIZE(state_machine_dispatch)) {
|
|
dispatch_func_t func = state_machine_dispatch[work_state.state];
|
|
|
|
if (NULL == func)
|
|
pr_warn("NULL state machine handler while in state %d; how did we end up here?",
|
|
work_state.state);
|
|
else
|
|
func(work_state.hdmi);
|
|
} else {
|
|
pr_warn("hdmi state machine worker scheduled unexpected state %d",
|
|
work_state.state);
|
|
}
|
|
}
|
|
|
|
/************************************************************
|
|
*
|
|
* state machine API implementation
|
|
*
|
|
************************************************************/
|
|
void hdmi_state_machine_init(struct tegra_dc_hdmi_data *hdmi)
|
|
{
|
|
work_state.hdmi = hdmi;
|
|
work_state.state = HDMI_STATE_RESET;
|
|
work_state.pending_hpd_evt = 1;
|
|
work_state.edid_reads = 0;
|
|
work_state.shutdown = 0;
|
|
INIT_DELAYED_WORK(&work_state.dwork, hdmi_state_machine_worker);
|
|
}
|
|
|
|
void hdmi_state_machine_shutdown(void)
|
|
{
|
|
work_state.shutdown = 1;
|
|
cancel_delayed_work_sync(&work_state.dwork);
|
|
}
|
|
|
|
void hdmi_state_machine_set_pending_hpd(void)
|
|
{
|
|
rt_mutex_lock(&work_lock);
|
|
|
|
/* We always schedule work any time there is a pending HPD event */
|
|
work_state.pending_hpd_evt = 1;
|
|
hdmi_state_machine_sched_work_l(0);
|
|
|
|
rt_mutex_unlock(&work_lock);
|
|
}
|
|
|
|
int hdmi_state_machine_get_state(void)
|
|
{
|
|
int ret;
|
|
|
|
rt_mutex_lock(&work_lock);
|
|
ret = work_state.state;
|
|
rt_mutex_unlock(&work_lock);
|
|
|
|
return ret;
|
|
}
|