root/drivers/gpu/drm/msm/hdmi/hdmi_hpd.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2013 Red Hat
 * Author: Rob Clark <robdclark@gmail.com>
 */

#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/pinctrl/consumer.h>

#include "msm_kms.h"
#include "hdmi.h"

static void msm_hdmi_phy_reset(struct hdmi *hdmi)
{
        unsigned int val;

        val = hdmi_read(hdmi, REG_HDMI_PHY_CTRL);

        if (val & HDMI_PHY_CTRL_SW_RESET_LOW) {
                /* pull low */
                hdmi_write(hdmi, REG_HDMI_PHY_CTRL,
                                val & ~HDMI_PHY_CTRL_SW_RESET);
        } else {
                /* pull high */
                hdmi_write(hdmi, REG_HDMI_PHY_CTRL,
                                val | HDMI_PHY_CTRL_SW_RESET);
        }

        if (val & HDMI_PHY_CTRL_SW_RESET_PLL_LOW) {
                /* pull low */
                hdmi_write(hdmi, REG_HDMI_PHY_CTRL,
                                val & ~HDMI_PHY_CTRL_SW_RESET_PLL);
        } else {
                /* pull high */
                hdmi_write(hdmi, REG_HDMI_PHY_CTRL,
                                val | HDMI_PHY_CTRL_SW_RESET_PLL);
        }

        msleep(100);

        if (val & HDMI_PHY_CTRL_SW_RESET_LOW) {
                /* pull high */
                hdmi_write(hdmi, REG_HDMI_PHY_CTRL,
                                val | HDMI_PHY_CTRL_SW_RESET);
        } else {
                /* pull low */
                hdmi_write(hdmi, REG_HDMI_PHY_CTRL,
                                val & ~HDMI_PHY_CTRL_SW_RESET);
        }

        if (val & HDMI_PHY_CTRL_SW_RESET_PLL_LOW) {
                /* pull high */
                hdmi_write(hdmi, REG_HDMI_PHY_CTRL,
                                val | HDMI_PHY_CTRL_SW_RESET_PLL);
        } else {
                /* pull low */
                hdmi_write(hdmi, REG_HDMI_PHY_CTRL,
                                val & ~HDMI_PHY_CTRL_SW_RESET_PLL);
        }
}

void msm_hdmi_hpd_enable(struct drm_bridge *bridge)
{
        struct hdmi_bridge *hdmi_bridge = to_hdmi_bridge(bridge);
        struct hdmi *hdmi = hdmi_bridge->hdmi;
        struct device *dev = &hdmi->pdev->dev;
        uint32_t hpd_ctrl;
        int ret;
        unsigned long flags;

        if (hdmi->hpd_gpiod)
                gpiod_set_value_cansleep(hdmi->hpd_gpiod, 1);

        ret = pm_runtime_resume_and_get(dev);
        if (WARN_ON(ret))
                return;

        mutex_lock(&hdmi->state_mutex);
        msm_hdmi_set_mode(hdmi, false);
        msm_hdmi_phy_reset(hdmi);
        msm_hdmi_set_mode(hdmi, true);

        hdmi->hpd_enabled = true;
        mutex_unlock(&hdmi->state_mutex);

        hdmi_write(hdmi, REG_HDMI_USEC_REFTIMER, 0x0001001b);

        /* enable HPD events: */
        hdmi_write(hdmi, REG_HDMI_HPD_INT_CTRL,
                        HDMI_HPD_INT_CTRL_INT_CONNECT |
                        HDMI_HPD_INT_CTRL_INT_EN);

        /* set timeout to 4.1ms (max) for hardware debounce */
        spin_lock_irqsave(&hdmi->reg_lock, flags);
        hpd_ctrl = hdmi_read(hdmi, REG_HDMI_HPD_CTRL);
        hpd_ctrl |= HDMI_HPD_CTRL_TIMEOUT(0x1fff);

        /* Toggle HPD circuit to trigger HPD sense */
        hdmi_write(hdmi, REG_HDMI_HPD_CTRL,
                        ~HDMI_HPD_CTRL_ENABLE & hpd_ctrl);
        hdmi_write(hdmi, REG_HDMI_HPD_CTRL,
                        HDMI_HPD_CTRL_ENABLE | hpd_ctrl);
        spin_unlock_irqrestore(&hdmi->reg_lock, flags);
}

void msm_hdmi_hpd_disable(struct drm_bridge *bridge)
{
        struct hdmi_bridge *hdmi_bridge = to_hdmi_bridge(bridge);
        struct hdmi *hdmi = hdmi_bridge->hdmi;
        struct device *dev = &hdmi->pdev->dev;

        /* Disable HPD interrupt */
        hdmi_write(hdmi, REG_HDMI_HPD_INT_CTRL, 0);

        mutex_lock(&hdmi->state_mutex);
        hdmi->hpd_enabled = false;
        msm_hdmi_set_mode(hdmi, hdmi->power_on);
        mutex_unlock(&hdmi->state_mutex);

        pm_runtime_put(dev);
}

void msm_hdmi_hpd_irq(struct drm_bridge *bridge)
{
        struct hdmi_bridge *hdmi_bridge = to_hdmi_bridge(bridge);
        struct hdmi *hdmi = hdmi_bridge->hdmi;
        uint32_t hpd_int_status, hpd_int_ctrl;

        /* Process HPD: */
        hpd_int_status = hdmi_read(hdmi, REG_HDMI_HPD_INT_STATUS);
        hpd_int_ctrl   = hdmi_read(hdmi, REG_HDMI_HPD_INT_CTRL);

        if ((hpd_int_ctrl & HDMI_HPD_INT_CTRL_INT_EN) &&
                        (hpd_int_status & HDMI_HPD_INT_STATUS_INT)) {
                bool detected = !!(hpd_int_status & HDMI_HPD_INT_STATUS_CABLE_DETECTED);

                /* ack & disable (temporarily) HPD events: */
                hdmi_write(hdmi, REG_HDMI_HPD_INT_CTRL,
                        HDMI_HPD_INT_CTRL_INT_ACK);

                DBG("status=%04x, ctrl=%04x", hpd_int_status, hpd_int_ctrl);

                /* detect disconnect if we are connected or visa versa: */
                hpd_int_ctrl = HDMI_HPD_INT_CTRL_INT_EN;
                if (!detected)
                        hpd_int_ctrl |= HDMI_HPD_INT_CTRL_INT_CONNECT;
                hdmi_write(hdmi, REG_HDMI_HPD_INT_CTRL, hpd_int_ctrl);

                queue_work(hdmi->workq, &hdmi_bridge->hpd_work);
        }
}

static enum drm_connector_status detect_reg(struct hdmi *hdmi)
{
        u32 hpd_int_status = 0;
        int ret;

        ret = pm_runtime_resume_and_get(&hdmi->pdev->dev);
        if (ret)
                goto out;

        hpd_int_status = hdmi_read(hdmi, REG_HDMI_HPD_INT_STATUS);

out:
        pm_runtime_put(&hdmi->pdev->dev);

        return (hpd_int_status & HDMI_HPD_INT_STATUS_CABLE_DETECTED) ?
                        connector_status_connected : connector_status_disconnected;
}

#define HPD_GPIO_INDEX  2
static enum drm_connector_status detect_gpio(struct hdmi *hdmi)
{
        return gpiod_get_value(hdmi->hpd_gpiod) ?
                        connector_status_connected :
                        connector_status_disconnected;
}

enum drm_connector_status
msm_hdmi_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector)
{
        struct hdmi_bridge *hdmi_bridge = to_hdmi_bridge(bridge);
        struct hdmi *hdmi = hdmi_bridge->hdmi;
        enum drm_connector_status stat_gpio, stat_reg;
        int retry = 20;

        /*
         * some platforms may not have hpd gpio. Rely only on the status
         * provided by REG_HDMI_HPD_INT_STATUS in this case.
         */
        if (!hdmi->hpd_gpiod)
                return detect_reg(hdmi);

        do {
                stat_gpio = detect_gpio(hdmi);
                stat_reg  = detect_reg(hdmi);

                if (stat_gpio == stat_reg)
                        break;

                mdelay(10);
        } while (--retry);

        /* the status we get from reading gpio seems to be more reliable,
         * so trust that one the most if we didn't manage to get hdmi and
         * gpio status to agree:
         */
        if (stat_gpio != stat_reg) {
                DBG("HDMI_HPD_INT_STATUS tells us: %d", stat_reg);
                DBG("hpd gpio tells us: %d", stat_gpio);
        }

        return stat_gpio;
}