root/sound/soc/codecs/wcd-mbhc-v2.c
// SPDX-License-Identifier: GPL-2.0-only
// Copyright (c) 2015-2021, The Linux Foundation. All rights reserved.

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/pm_runtime.h>
#include <linux/printk.h>
#include <linux/delay.h>
#include <linux/kernel.h>
#include <sound/soc.h>
#include <sound/jack.h>
#include "wcd-mbhc-v2.h"

#define HS_DETECT_PLUG_TIME_MS          (3 * 1000)
#define MBHC_BUTTON_PRESS_THRESHOLD_MIN 250
#define GND_MIC_SWAP_THRESHOLD          4
#define GND_MIC_USBC_SWAP_THRESHOLD     2
#define WCD_FAKE_REMOVAL_MIN_PERIOD_MS  100
#define HPHL_CROSS_CONN_THRESHOLD       100
#define HS_VREF_MIN_VAL                 1400
#define FAKE_REM_RETRY_ATTEMPTS         3
#define WCD_MBHC_ADC_HS_THRESHOLD_MV    1700
#define WCD_MBHC_ADC_HPH_THRESHOLD_MV   75
#define WCD_MBHC_ADC_MICBIAS_MV         1800
#define WCD_MBHC_FAKE_INS_RETRY         4

#define WCD_MBHC_JACK_MASK (SND_JACK_HEADSET | SND_JACK_LINEOUT | \
                           SND_JACK_MECHANICAL)

#define WCD_MBHC_JACK_BUTTON_MASK (SND_JACK_BTN_0 | SND_JACK_BTN_1 | \
                                  SND_JACK_BTN_2 | SND_JACK_BTN_3 | \
                                  SND_JACK_BTN_4 | SND_JACK_BTN_5)

enum wcd_mbhc_adc_mux_ctl {
        MUX_CTL_AUTO = 0,
        MUX_CTL_IN2P,
        MUX_CTL_IN3P,
        MUX_CTL_IN4P,
        MUX_CTL_HPH_L,
        MUX_CTL_HPH_R,
        MUX_CTL_NONE,
};

struct wcd_mbhc {
        struct device *dev;
        struct snd_soc_component *component;
        struct snd_soc_jack *jack;
        struct wcd_mbhc_config *cfg;
        const struct wcd_mbhc_cb *mbhc_cb;
        const struct wcd_mbhc_intr *intr_ids;
        const struct wcd_mbhc_field *fields;
        /* Delayed work to report long button press */
        struct delayed_work mbhc_btn_dwork;
        /* Work to handle plug report */
        struct work_struct mbhc_plug_detect_work;
        /* Work to correct accessory type */
        struct work_struct correct_plug_swch;
        struct mutex lock;
        int buttons_pressed;
        u32 hph_status; /* track headhpone status */
        u8 current_plug;
        unsigned int swap_thr;
        bool is_btn_press;
        bool in_swch_irq_handler;
        bool hs_detect_work_stop;
        bool is_hs_recording;
        bool extn_cable_hph_rem;
        bool force_linein;
        bool impedance_detect;
        unsigned long event_state;
        unsigned long jiffies_atreport;
        /* impedance of hphl and hphr */
        uint32_t zl, zr;
        /* Holds type of Headset - Mono/Stereo */
        enum wcd_mbhc_hph_type hph_type;
        /* Holds mbhc detection method - ADC/Legacy */
        int mbhc_detection_logic;
};

static inline int wcd_mbhc_write_field(const struct wcd_mbhc *mbhc,
                                       int field, int val)
{
        if (!mbhc->fields[field].reg)
                return 0;

        return snd_soc_component_write_field(mbhc->component,
                                             mbhc->fields[field].reg,
                                             mbhc->fields[field].mask, val);
}

static inline int wcd_mbhc_read_field(const struct wcd_mbhc *mbhc, int field)
{
        if (!mbhc->fields[field].reg)
                return 0;

        return snd_soc_component_read_field(mbhc->component,
                                            mbhc->fields[field].reg,
                                            mbhc->fields[field].mask);
}

static void wcd_program_hs_vref(struct wcd_mbhc *mbhc)
{
        u32 reg_val = ((mbhc->cfg->v_hs_max - HS_VREF_MIN_VAL) / 100);

        wcd_mbhc_write_field(mbhc, WCD_MBHC_HS_VREF, reg_val);
}

static void wcd_program_btn_threshold(const struct wcd_mbhc *mbhc, bool micbias)
{
        struct snd_soc_component *component = mbhc->component;

        mbhc->mbhc_cb->set_btn_thr(component, mbhc->cfg->btn_low,
                                   mbhc->cfg->btn_high,
                                   mbhc->cfg->num_btn, micbias);
}

static void wcd_mbhc_curr_micbias_control(const struct wcd_mbhc *mbhc,
                                          const enum wcd_mbhc_cs_mb_en_flag cs_mb_en)
{

        /*
         * Some codecs handle micbias/pullup enablement in codec
         * drivers itself and micbias is not needed for regular
         * plug type detection. So if micbias_control callback function
         * is defined, just return.
         */
        if (mbhc->mbhc_cb->mbhc_micbias_control)
                return;

        switch (cs_mb_en) {
        case WCD_MBHC_EN_CS:
                wcd_mbhc_write_field(mbhc, WCD_MBHC_MICB_CTRL, 0);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 3);
                /* Program Button threshold registers as per CS */
                wcd_program_btn_threshold(mbhc, false);
                break;
        case WCD_MBHC_EN_MB:
                wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 0);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 1);
                /* Disable PULL_UP_EN & enable MICBIAS */
                wcd_mbhc_write_field(mbhc, WCD_MBHC_MICB_CTRL, 2);
                /* Program Button threshold registers as per MICBIAS */
                wcd_program_btn_threshold(mbhc, true);
                break;
        case WCD_MBHC_EN_PULLUP:
                wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 3);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 1);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_MICB_CTRL, 1);
                /* Program Button threshold registers as per MICBIAS */
                wcd_program_btn_threshold(mbhc, true);
                break;
        case WCD_MBHC_EN_NONE:
                wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 0);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 1);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_MICB_CTRL, 0);
                break;
        default:
                dev_err(mbhc->dev, "%s: Invalid parameter", __func__);
                break;
        }
}

int wcd_mbhc_event_notify(struct wcd_mbhc *mbhc, unsigned long event)
{

        struct snd_soc_component *component;
        bool micbias2 = false;

        if (!mbhc)
                return 0;

        component = mbhc->component;

        if (mbhc->mbhc_cb->micbias_enable_status)
                micbias2 = mbhc->mbhc_cb->micbias_enable_status(component, MIC_BIAS_2);

        switch (event) {
        /* MICBIAS usage change */
        case WCD_EVENT_POST_DAPM_MICBIAS_2_ON:
                mbhc->is_hs_recording = true;
                break;
        case WCD_EVENT_POST_MICBIAS_2_ON:
                /* Disable current source if micbias2 enabled */
                if (mbhc->mbhc_cb->mbhc_micbias_control) {
                        if (wcd_mbhc_read_field(mbhc, WCD_MBHC_FSM_EN))
                                wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 0);
                } else {
                        mbhc->is_hs_recording = true;
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_MB);
                }
                break;
        case WCD_EVENT_PRE_MICBIAS_2_OFF:
                /*
                 * Before MICBIAS_2 is turned off, if FSM is enabled,
                 * make sure current source is enabled so as to detect
                 * button press/release events
                 */
                if (mbhc->mbhc_cb->mbhc_micbias_control/* && !mbhc->micbias_enable*/) {
                        if (wcd_mbhc_read_field(mbhc, WCD_MBHC_FSM_EN))
                                wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 3);
                }
                break;
        /* MICBIAS usage change */
        case WCD_EVENT_POST_DAPM_MICBIAS_2_OFF:
                mbhc->is_hs_recording = false;
                break;
        case WCD_EVENT_POST_MICBIAS_2_OFF:
                if (!mbhc->mbhc_cb->mbhc_micbias_control)
                        mbhc->is_hs_recording = false;

                /* Enable PULL UP if PA's are enabled */
                if ((test_bit(WCD_MBHC_EVENT_PA_HPHL, &mbhc->event_state)) ||
                    (test_bit(WCD_MBHC_EVENT_PA_HPHR, &mbhc->event_state)))
                        /* enable pullup and cs, disable mb */
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_PULLUP);
                else
                        /* enable current source and disable mb, pullup*/
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_CS);

                break;
        case WCD_EVENT_POST_HPHL_PA_OFF:
                clear_bit(WCD_MBHC_EVENT_PA_HPHL, &mbhc->event_state);

                /* check if micbias is enabled */
                if (micbias2)
                        /* Disable cs, pullup & enable micbias */
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_MB);
                else
                        /* Disable micbias, pullup & enable cs */
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_CS);
                break;
        case WCD_EVENT_POST_HPHR_PA_OFF:
                clear_bit(WCD_MBHC_EVENT_PA_HPHR, &mbhc->event_state);
                /* check if micbias is enabled */
                if (micbias2)
                        /* Disable cs, pullup & enable micbias */
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_MB);
                else
                        /* Disable micbias, pullup & enable cs */
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_CS);
                break;
        case WCD_EVENT_PRE_HPHL_PA_ON:
                set_bit(WCD_MBHC_EVENT_PA_HPHL, &mbhc->event_state);
                /* check if micbias is enabled */
                if (micbias2)
                        /* Disable cs, pullup & enable micbias */
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_MB);
                else
                        /* Disable micbias, enable pullup & cs */
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_PULLUP);
                break;
        case WCD_EVENT_PRE_HPHR_PA_ON:
                set_bit(WCD_MBHC_EVENT_PA_HPHR, &mbhc->event_state);
                /* check if micbias is enabled */
                if (micbias2)
                        /* Disable cs, pullup & enable micbias */
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_MB);
                else
                        /* Disable micbias, enable pullup & cs */
                        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_PULLUP);
                break;
        default:
                break;
        }
        return 0;
}
EXPORT_SYMBOL_GPL(wcd_mbhc_event_notify);

static int wcd_cancel_btn_work(struct wcd_mbhc *mbhc)
{
        return cancel_delayed_work_sync(&mbhc->mbhc_btn_dwork);
}

static void wcd_micbias_disable(struct wcd_mbhc *mbhc)
{
        struct snd_soc_component *component = mbhc->component;

        if (mbhc->mbhc_cb->mbhc_micbias_control)
                mbhc->mbhc_cb->mbhc_micbias_control(component, MIC_BIAS_2, MICB_DISABLE);

        if (mbhc->mbhc_cb->mbhc_micb_ctrl_thr_mic)
                mbhc->mbhc_cb->mbhc_micb_ctrl_thr_mic(component, MIC_BIAS_2, false);

        if (mbhc->mbhc_cb->set_micbias_value) {
                mbhc->mbhc_cb->set_micbias_value(component);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_MICB_CTRL, 0);
        }
}

static void wcd_mbhc_report_plug_removal(struct wcd_mbhc *mbhc,
                                         enum snd_jack_types jack_type)
{
        mbhc->hph_status &= ~jack_type;
        /*
         * cancel possibly scheduled btn work and
         * report release if we reported button press
         */
        if (!wcd_cancel_btn_work(mbhc) && mbhc->buttons_pressed) {
                snd_soc_jack_report(mbhc->jack, 0, mbhc->buttons_pressed);
                mbhc->buttons_pressed &= ~WCD_MBHC_JACK_BUTTON_MASK;
        }

        wcd_micbias_disable(mbhc);
        mbhc->hph_type = WCD_MBHC_HPH_NONE;
        mbhc->zl = mbhc->zr = 0;
        snd_soc_jack_report(mbhc->jack, mbhc->hph_status, WCD_MBHC_JACK_MASK);
        mbhc->current_plug = MBHC_PLUG_TYPE_NONE;
        mbhc->force_linein = false;
}

static void wcd_mbhc_compute_impedance(struct wcd_mbhc *mbhc)
{

        if (!mbhc->impedance_detect)
                return;

        if (mbhc->cfg->linein_th != 0) {
                u8 fsm_en = wcd_mbhc_read_field(mbhc, WCD_MBHC_FSM_EN);
                /* Set MUX_CTL to AUTO for Z-det */

                wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 0);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_MUX_CTL, MUX_CTL_AUTO);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 1);
                mbhc->mbhc_cb->compute_impedance(mbhc->component, &mbhc->zl, &mbhc->zr);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, fsm_en);
        }
}

static void wcd_mbhc_report_plug_insertion(struct wcd_mbhc *mbhc,
                                           enum snd_jack_types jack_type)
{
        bool is_pa_on;
        /*
         * Report removal of current jack type.
         * Headphone to headset shouldn't report headphone
         * removal.
         */
        if (mbhc->current_plug == MBHC_PLUG_TYPE_HEADSET &&
            jack_type == SND_JACK_HEADPHONE)
                mbhc->hph_status &= ~SND_JACK_HEADSET;

        /* Report insertion */
        switch (jack_type) {
        case SND_JACK_HEADPHONE:
                mbhc->current_plug = MBHC_PLUG_TYPE_HEADPHONE;
                break;
        case SND_JACK_HEADSET:
                mbhc->current_plug = MBHC_PLUG_TYPE_HEADSET;
                mbhc->jiffies_atreport = jiffies;
                break;
        case SND_JACK_LINEOUT:
                mbhc->current_plug = MBHC_PLUG_TYPE_HIGH_HPH;
                break;
        default:
                break;
        }


        is_pa_on = wcd_mbhc_read_field(mbhc, WCD_MBHC_HPH_PA_EN);

        if (!is_pa_on) {
                wcd_mbhc_compute_impedance(mbhc);
                if ((mbhc->zl > mbhc->cfg->linein_th) &&
                    (mbhc->zr > mbhc->cfg->linein_th) &&
                    (jack_type == SND_JACK_HEADPHONE)) {
                        jack_type = SND_JACK_LINEOUT;
                        mbhc->force_linein = true;
                        mbhc->current_plug = MBHC_PLUG_TYPE_HIGH_HPH;
                        if (mbhc->hph_status) {
                                mbhc->hph_status &= ~(SND_JACK_HEADSET |
                                                      SND_JACK_LINEOUT);
                                snd_soc_jack_report(mbhc->jack, mbhc->hph_status,
                                                    WCD_MBHC_JACK_MASK);
                        }
                }
        }

        /* Do not calculate impedance again for lineout
         * as during playback pa is on and impedance values
         * will not be correct resulting in lineout detected
         * as headphone.
         */
        if (is_pa_on && mbhc->force_linein) {
                jack_type = SND_JACK_LINEOUT;
                mbhc->current_plug = MBHC_PLUG_TYPE_HIGH_HPH;
                if (mbhc->hph_status) {
                        mbhc->hph_status &= ~(SND_JACK_HEADSET |
                                              SND_JACK_LINEOUT);
                        snd_soc_jack_report(mbhc->jack, mbhc->hph_status,
                                            WCD_MBHC_JACK_MASK);
                }
        }

        mbhc->hph_status |= jack_type;

        if (jack_type == SND_JACK_HEADPHONE && mbhc->mbhc_cb->mbhc_micb_ramp_control)
                mbhc->mbhc_cb->mbhc_micb_ramp_control(mbhc->component, false);

        snd_soc_jack_report(mbhc->jack, (mbhc->hph_status | SND_JACK_MECHANICAL),
                            WCD_MBHC_JACK_MASK);
}

static void wcd_mbhc_report_plug(struct wcd_mbhc *mbhc, int insertion,
                                 enum snd_jack_types jack_type)
{

        WARN_ON(!mutex_is_locked(&mbhc->lock));

        if (!insertion) /* Report removal */
                wcd_mbhc_report_plug_removal(mbhc, jack_type);
        else
                wcd_mbhc_report_plug_insertion(mbhc, jack_type);

}

static void wcd_cancel_hs_detect_plug(struct wcd_mbhc *mbhc,
                                      struct work_struct *work)
{
        mbhc->hs_detect_work_stop = true;
        mutex_unlock(&mbhc->lock);
        cancel_work_sync(work);
        mutex_lock(&mbhc->lock);
}

static void wcd_mbhc_cancel_pending_work(struct wcd_mbhc *mbhc)
{
        /* cancel pending button press */
        wcd_cancel_btn_work(mbhc);
        /* cancel correct work function */
        wcd_cancel_hs_detect_plug(mbhc, &mbhc->correct_plug_swch);
}

static void wcd_mbhc_elec_hs_report_unplug(struct wcd_mbhc *mbhc)
{
        wcd_mbhc_cancel_pending_work(mbhc);
        /* Report extension cable */
        wcd_mbhc_report_plug(mbhc, 1, SND_JACK_LINEOUT);
        /*
         * Disable HPHL trigger and MIC Schmitt triggers.
         * Setup for insertion detection.
         */
        disable_irq_nosync(mbhc->intr_ids->mbhc_hs_rem_intr);
        wcd_mbhc_curr_micbias_control(mbhc, WCD_MBHC_EN_NONE);
        /* Disable HW FSM */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 0);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ELECT_SCHMT_ISRC, 3);

        /* Set the detection type appropriately */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ELECT_DETECTION_TYPE, 1);
        enable_irq(mbhc->intr_ids->mbhc_hs_ins_intr);
}

static void wcd_mbhc_find_plug_and_report(struct wcd_mbhc *mbhc,
                                   enum wcd_mbhc_plug_type plug_type)
{
        if (mbhc->current_plug == plug_type)
                return;

        mutex_lock(&mbhc->lock);

        switch (plug_type) {
        case MBHC_PLUG_TYPE_HEADPHONE:
                wcd_mbhc_report_plug(mbhc, 1, SND_JACK_HEADPHONE);
                break;
        case MBHC_PLUG_TYPE_HEADSET:
                wcd_mbhc_report_plug(mbhc, 1, SND_JACK_HEADSET);
                break;
        case MBHC_PLUG_TYPE_HIGH_HPH:
                wcd_mbhc_report_plug(mbhc, 1, SND_JACK_LINEOUT);
                break;
        case MBHC_PLUG_TYPE_GND_MIC_SWAP:
                if (mbhc->current_plug == MBHC_PLUG_TYPE_HEADPHONE)
                        wcd_mbhc_report_plug(mbhc, 0, SND_JACK_HEADPHONE);
                if (mbhc->current_plug == MBHC_PLUG_TYPE_HEADSET)
                        wcd_mbhc_report_plug(mbhc, 0, SND_JACK_HEADSET);
                break;
        default:
                WARN(1, "Unexpected current plug_type %d, plug_type %d\n",
                     mbhc->current_plug, plug_type);
                break;
        }
        mutex_unlock(&mbhc->lock);
}

static void wcd_schedule_hs_detect_plug(struct wcd_mbhc *mbhc,
                                            struct work_struct *work)
{
        WARN_ON(!mutex_is_locked(&mbhc->lock));
        mbhc->hs_detect_work_stop = false;
        schedule_work(work);
}

static void wcd_mbhc_adc_detect_plug_type(struct wcd_mbhc *mbhc)
{
        struct snd_soc_component *component = mbhc->component;

        WARN_ON(!mutex_is_locked(&mbhc->lock));

        if (mbhc->mbhc_cb->hph_pull_down_ctrl)
                mbhc->mbhc_cb->hph_pull_down_ctrl(component, false);

        wcd_mbhc_write_field(mbhc, WCD_MBHC_DETECTION_DONE, 0);

        if (mbhc->mbhc_cb->mbhc_micbias_control) {
                mbhc->mbhc_cb->mbhc_micbias_control(component, MIC_BIAS_2,
                                                    MICB_ENABLE);
                wcd_schedule_hs_detect_plug(mbhc, &mbhc->correct_plug_swch);
        }
}

static void mbhc_plug_detect_fn(struct work_struct *work)
{
        struct wcd_mbhc *mbhc = container_of(work, struct wcd_mbhc, mbhc_plug_detect_work);
        struct snd_soc_component *component = mbhc->component;
        enum snd_jack_types jack_type;
        bool detection_type;

        mutex_lock(&mbhc->lock);

        mbhc->in_swch_irq_handler = true;

        wcd_mbhc_cancel_pending_work(mbhc);

        detection_type = wcd_mbhc_read_field(mbhc, WCD_MBHC_MECH_DETECTION_TYPE);

        /* Set the detection type appropriately */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_MECH_DETECTION_TYPE, !detection_type);

        /* Enable micbias ramp */
        if (mbhc->mbhc_cb->mbhc_micb_ramp_control)
                mbhc->mbhc_cb->mbhc_micb_ramp_control(component, true);

        if (detection_type) {
                if (mbhc->current_plug != MBHC_PLUG_TYPE_NONE)
                        goto exit;
                /* Make sure MASTER_BIAS_CTL is enabled */
                mbhc->mbhc_cb->mbhc_bias(component, true);
                mbhc->is_btn_press = false;
                wcd_mbhc_adc_detect_plug_type(mbhc);
        } else {
                /* Disable HW FSM */
                wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 0);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 0);
                mbhc->extn_cable_hph_rem = false;

                if (mbhc->current_plug == MBHC_PLUG_TYPE_NONE)
                        goto exit;

                mbhc->is_btn_press = false;
                switch (mbhc->current_plug) {
                case MBHC_PLUG_TYPE_HEADPHONE:
                        jack_type = SND_JACK_HEADPHONE;
                        break;
                case MBHC_PLUG_TYPE_HEADSET:
                        jack_type = SND_JACK_HEADSET;
                        break;
                case MBHC_PLUG_TYPE_HIGH_HPH:
                        if (mbhc->mbhc_detection_logic == WCD_DETECTION_ADC)
                                wcd_mbhc_write_field(mbhc, WCD_MBHC_ELECT_ISRC_EN, 0);
                        jack_type = SND_JACK_LINEOUT;
                        break;
                case MBHC_PLUG_TYPE_GND_MIC_SWAP:
                        dev_err(mbhc->dev, "Ground and Mic Swapped on plug\n");
                        goto exit;
                default:
                        dev_err(mbhc->dev, "Invalid current plug: %d\n",
                                mbhc->current_plug);
                        goto exit;
                }
                disable_irq_nosync(mbhc->intr_ids->mbhc_hs_rem_intr);
                disable_irq_nosync(mbhc->intr_ids->mbhc_hs_ins_intr);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_ELECT_DETECTION_TYPE, 1);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_ELECT_SCHMT_ISRC, 0);
                wcd_mbhc_report_plug(mbhc, 0, jack_type);
        }

exit:
        mbhc->in_swch_irq_handler = false;
        mutex_unlock(&mbhc->lock);
}

static irqreturn_t wcd_mbhc_mech_plug_detect_irq(int irq, void *data)
{
        struct wcd_mbhc *mbhc = data;

        if (!mbhc->cfg->typec_analog_mux)
                schedule_work(&mbhc->mbhc_plug_detect_work);

        return IRQ_HANDLED;
}

int wcd_mbhc_typec_report_unplug(struct wcd_mbhc *mbhc)
{

        if (!mbhc || !mbhc->cfg->typec_analog_mux)
                return -EINVAL;

        if (mbhc->mbhc_cb->clk_setup)
                mbhc->mbhc_cb->clk_setup(mbhc->component, false);

        wcd_mbhc_write_field(mbhc, WCD_MBHC_L_DET_EN, 0);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_MECH_DETECTION_TYPE, 0);

        schedule_work(&mbhc->mbhc_plug_detect_work);

        return 0;
}
EXPORT_SYMBOL_GPL(wcd_mbhc_typec_report_unplug);

int wcd_mbhc_typec_report_plug(struct wcd_mbhc *mbhc)
{
        if (!mbhc || !mbhc->cfg->typec_analog_mux)
                return -EINVAL;

        if (mbhc->mbhc_cb->clk_setup)
                mbhc->mbhc_cb->clk_setup(mbhc->component, true);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_L_DET_EN, 1);

        schedule_work(&mbhc->mbhc_plug_detect_work);

        return 0;
}
EXPORT_SYMBOL_GPL(wcd_mbhc_typec_report_plug);

static int wcd_mbhc_get_button_mask(struct wcd_mbhc *mbhc)
{
        int mask = 0;
        int btn;

        btn = wcd_mbhc_read_field(mbhc, WCD_MBHC_BTN_RESULT);

        switch (btn) {
        case 0:
                mask = SND_JACK_BTN_0;
                break;
        case 1:
                mask = SND_JACK_BTN_1;
                break;
        case 2:
                mask = SND_JACK_BTN_2;
                break;
        case 3:
                mask = SND_JACK_BTN_3;
                break;
        case 4:
                mask = SND_JACK_BTN_4;
                break;
        case 5:
                mask = SND_JACK_BTN_5;
                break;
        default:
                break;
        }

        return mask;
}

static void wcd_btn_long_press_fn(struct work_struct *work)
{
        struct delayed_work *dwork = to_delayed_work(work);
        struct wcd_mbhc *mbhc = container_of(dwork, struct wcd_mbhc, mbhc_btn_dwork);

        if (mbhc->current_plug == MBHC_PLUG_TYPE_HEADSET)
                snd_soc_jack_report(mbhc->jack, mbhc->buttons_pressed,
                                    mbhc->buttons_pressed);
}

static irqreturn_t wcd_mbhc_btn_press_handler(int irq, void *data)
{
        struct wcd_mbhc *mbhc = data;
        int mask;
        unsigned long msec_val;

        mutex_lock(&mbhc->lock);
        wcd_cancel_btn_work(mbhc);
        mbhc->is_btn_press = true;
        msec_val = jiffies_to_msecs(jiffies - mbhc->jiffies_atreport);

        /* Too short, ignore button press */
        if (msec_val < MBHC_BUTTON_PRESS_THRESHOLD_MIN)
                goto done;

        /* If switch interrupt already kicked in, ignore button press */
        if (mbhc->in_swch_irq_handler)
                goto done;

        /* Plug isn't headset, ignore button press */
        if (mbhc->current_plug != MBHC_PLUG_TYPE_HEADSET)
                goto done;

        mask = wcd_mbhc_get_button_mask(mbhc);
        mbhc->buttons_pressed |= mask;
        if (schedule_delayed_work(&mbhc->mbhc_btn_dwork, msecs_to_jiffies(400)) == 0)
                WARN(1, "Button pressed twice without release event\n");
done:
        mutex_unlock(&mbhc->lock);
        return IRQ_HANDLED;
}

static irqreturn_t wcd_mbhc_btn_release_handler(int irq, void *data)
{
        struct wcd_mbhc *mbhc = data;
        int ret;

        mutex_lock(&mbhc->lock);
        if (mbhc->is_btn_press)
                mbhc->is_btn_press = false;
        else /* fake btn press */
                goto exit;

        if (!(mbhc->buttons_pressed & WCD_MBHC_JACK_BUTTON_MASK))
                goto exit;

        ret = wcd_cancel_btn_work(mbhc);
        if (ret == 0) { /* Reporting long button release event */
                snd_soc_jack_report(mbhc->jack, 0, mbhc->buttons_pressed);
        } else {
                if (!mbhc->in_swch_irq_handler) {
                        /* Reporting btn press n Release */
                        snd_soc_jack_report(mbhc->jack, mbhc->buttons_pressed,
                                            mbhc->buttons_pressed);
                        snd_soc_jack_report(mbhc->jack, 0, mbhc->buttons_pressed);
                }
        }
        mbhc->buttons_pressed &= ~WCD_MBHC_JACK_BUTTON_MASK;
exit:
        mutex_unlock(&mbhc->lock);

        return IRQ_HANDLED;
}

static irqreturn_t wcd_mbhc_hph_ocp_irq(struct wcd_mbhc *mbhc, bool hphr)
{

        /* TODO Find a better way to report this to Userspace */
        dev_err(mbhc->dev, "MBHC Over Current on %s detected\n",
                hphr ? "HPHR" : "HPHL");

        wcd_mbhc_write_field(mbhc, WCD_MBHC_OCP_FSM_EN, 0);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_OCP_FSM_EN, 1);

        return IRQ_HANDLED;
}

static irqreturn_t wcd_mbhc_hphl_ocp_irq(int irq, void *data)
{
        return wcd_mbhc_hph_ocp_irq(data, false);
}

static irqreturn_t wcd_mbhc_hphr_ocp_irq(int irq, void *data)
{
        return wcd_mbhc_hph_ocp_irq(data, true);
}

static int wcd_mbhc_initialise(struct wcd_mbhc *mbhc)
{
        struct snd_soc_component *component = mbhc->component;
        int ret;

        ret = pm_runtime_get_sync(component->dev);
        if (ret < 0 && ret != -EACCES) {
                dev_err_ratelimited(component->dev,
                                    "pm_runtime_get_sync failed in %s, ret %d\n",
                                    __func__, ret);
                pm_runtime_put_noidle(component->dev);
                return ret;
        }

        mutex_lock(&mbhc->lock);

        if (mbhc->cfg->typec_analog_mux)
                mbhc->swap_thr = GND_MIC_USBC_SWAP_THRESHOLD;
        else
                mbhc->swap_thr = GND_MIC_SWAP_THRESHOLD;

        /* setup HS detection */
        if (mbhc->mbhc_cb->hph_pull_up_control_v2)
                mbhc->mbhc_cb->hph_pull_up_control_v2(component,
                                mbhc->cfg->typec_analog_mux ?
                                        HS_PULLUP_I_OFF : HS_PULLUP_I_DEFAULT);
        else if (mbhc->mbhc_cb->hph_pull_up_control)
                mbhc->mbhc_cb->hph_pull_up_control(component,
                                mbhc->cfg->typec_analog_mux ?
                                        I_OFF : I_DEFAULT);
        else
                wcd_mbhc_write_field(mbhc, WCD_MBHC_HS_L_DET_PULL_UP_CTRL,
                                mbhc->cfg->typec_analog_mux ? 0 : 3);

        wcd_mbhc_write_field(mbhc, WCD_MBHC_HPHL_PLUG_TYPE, mbhc->cfg->hphl_swh);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_GND_PLUG_TYPE, mbhc->cfg->gnd_swh);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_SW_HPH_LP_100K_TO_GND, 1);
        if (mbhc->cfg->gnd_det_en && mbhc->mbhc_cb->mbhc_gnd_det_ctrl)
                mbhc->mbhc_cb->mbhc_gnd_det_ctrl(component, true);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_HS_L_DET_PULL_UP_COMP_CTRL, 1);

        /* Plug detect is triggered manually if analog goes through USBCC */
        if (mbhc->cfg->typec_analog_mux)
                wcd_mbhc_write_field(mbhc, WCD_MBHC_L_DET_EN, 0);
        else
                wcd_mbhc_write_field(mbhc, WCD_MBHC_L_DET_EN, 1);

        if (mbhc->cfg->typec_analog_mux)
                /* Insertion debounce set to 48ms */
                wcd_mbhc_write_field(mbhc, WCD_MBHC_INSREM_DBNC, 4);
        else
                /* Insertion debounce set to 96ms */
                wcd_mbhc_write_field(mbhc, WCD_MBHC_INSREM_DBNC, 6);

        /* Button Debounce set to 16ms */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_DBNC, 2);

        /* enable bias */
        mbhc->mbhc_cb->mbhc_bias(component, true);
        /* enable MBHC clock */
        if (mbhc->mbhc_cb->clk_setup)
                mbhc->mbhc_cb->clk_setup(component,
                                mbhc->cfg->typec_analog_mux ? false : true);

        /* program HS_VREF value */
        wcd_program_hs_vref(mbhc);

        wcd_program_btn_threshold(mbhc, false);

        mutex_unlock(&mbhc->lock);

        pm_runtime_put_autosuspend(component->dev);

        return 0;
}

static int wcd_mbhc_get_micbias(struct wcd_mbhc *mbhc)
{
        int micbias = 0;

        if (mbhc->mbhc_cb->get_micbias_val) {
                mbhc->mbhc_cb->get_micbias_val(mbhc->component, &micbias);
        } else {
                u8 vout_ctl = 0;
                /* Read MBHC Micbias (Mic Bias2) voltage */
                vout_ctl = wcd_mbhc_read_field(mbhc, WCD_MBHC_MICB2_VOUT);
                /* Formula for getting micbias from vout
                 * micbias = 1.0V + VOUT_CTL * 50mV
                 */
                micbias = 1000 + (vout_ctl * 50);
        }
        return micbias;
}

static int wcd_get_voltage_from_adc(u8 val, int micbias)
{
        /* Formula for calculating voltage from ADC
         * Voltage = ADC_RESULT*12.5mV*V_MICBIAS/1.8
         */
        return ((val * 125 * micbias)/(WCD_MBHC_ADC_MICBIAS_MV * 10));
}

static int wcd_measure_adc_continuous(struct wcd_mbhc *mbhc)
{
        u8 adc_result;
        int output_mv;
        int retry = 3;
        u8 adc_en;

        /* Pre-requisites for ADC continuous measurement */
        /* Read legacy electircal detection and disable */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ELECT_SCHMT_ISRC, 0x00);
        /* Set ADC to continuous measurement */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_MODE, 1);
        /* Read ADC Enable bit to restore after adc measurement */
        adc_en = wcd_mbhc_read_field(mbhc, WCD_MBHC_ADC_EN);
        /* Disable ADC_ENABLE bit */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, 0);
        /* Disable MBHC FSM */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 0);
        /* Set the MUX selection to IN2P */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_MUX_CTL, MUX_CTL_IN2P);
        /* Enable MBHC FSM */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 1);
        /* Enable ADC_ENABLE bit */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, 1);

        while (retry--) {
                /* wait for 3 msec before reading ADC result */
                usleep_range(3000, 3100);
                adc_result = wcd_mbhc_read_field(mbhc, WCD_MBHC_ADC_RESULT);
        }

        /* Restore ADC Enable */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, adc_en);
        /* Get voltage from ADC result */
        output_mv = wcd_get_voltage_from_adc(adc_result, wcd_mbhc_get_micbias(mbhc));

        return output_mv;
}

static int wcd_measure_adc_once(struct wcd_mbhc *mbhc, int mux_ctl)
{
        struct device *dev = mbhc->dev;
        u8 adc_timeout = 0;
        u8 adc_complete = 0;
        u8 adc_result;
        int retry = 6;
        int ret;
        int output_mv = 0;
        u8 adc_en;

        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_MODE, 0);
        /* Read ADC Enable bit to restore after adc measurement */
        adc_en = wcd_mbhc_read_field(mbhc, WCD_MBHC_ADC_EN);
        /* Trigger ADC one time measurement */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, 0);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 0);
        /* Set the appropriate MUX selection */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_MUX_CTL, mux_ctl);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 1);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, 1);

        while (retry--) {
                /* wait for 600usec to get adc results */
                usleep_range(600, 610);

                /* check for ADC Timeout */
                adc_timeout = wcd_mbhc_read_field(mbhc, WCD_MBHC_ADC_TIMEOUT);
                if (adc_timeout)
                        continue;

                /* Read ADC complete bit */
                adc_complete = wcd_mbhc_read_field(mbhc, WCD_MBHC_ADC_COMPLETE);
                if (!adc_complete)
                        continue;

                /* Read ADC result */
                adc_result = wcd_mbhc_read_field(mbhc, WCD_MBHC_ADC_RESULT);

                /* Get voltage from ADC result */
                output_mv = wcd_get_voltage_from_adc(adc_result,
                                                wcd_mbhc_get_micbias(mbhc));
                break;
        }

        /* Restore ADC Enable */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, adc_en);

        if (retry <= 0) {
                dev_err(dev, "%s: adc complete: %d, adc timeout: %d\n",
                        __func__, adc_complete, adc_timeout);
                ret = -EINVAL;
        } else {
                ret = output_mv;
        }

        return ret;
}

/* To determine if cross connection occurred */
static int wcd_check_cross_conn(struct wcd_mbhc *mbhc)
{
        u8 adc_mode, elect_ctl, adc_en, fsm_en;
        int hphl_adc_res, hphr_adc_res;
        bool is_cross_conn = false;

        /* If PA is enabled, dont check for cross-connection */
        if (wcd_mbhc_read_field(mbhc, WCD_MBHC_HPH_PA_EN))
                return -EINVAL;

        /* Read legacy electircal detection and disable */
        elect_ctl = wcd_mbhc_read_field(mbhc, WCD_MBHC_ELECT_SCHMT_ISRC);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ELECT_SCHMT_ISRC, 0);

        /* Read and set ADC to single measurement */
        adc_mode = wcd_mbhc_read_field(mbhc, WCD_MBHC_ADC_MODE);
        /* Read ADC Enable bit to restore after adc measurement */
        adc_en = wcd_mbhc_read_field(mbhc, WCD_MBHC_ADC_EN);
        /* Read FSM status */
        fsm_en = wcd_mbhc_read_field(mbhc, WCD_MBHC_FSM_EN);

        /* Get adc result for HPH L */
        hphl_adc_res = wcd_measure_adc_once(mbhc, MUX_CTL_HPH_L);
        if (hphl_adc_res < 0)
                return hphl_adc_res;

        /* Get adc result for HPH R in mV */
        hphr_adc_res = wcd_measure_adc_once(mbhc, MUX_CTL_HPH_R);
        if (hphr_adc_res < 0)
                return hphr_adc_res;

        if (hphl_adc_res > HPHL_CROSS_CONN_THRESHOLD ||
            hphr_adc_res > HPHL_CROSS_CONN_THRESHOLD)
                is_cross_conn = true;

        wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 0);
        /* Set the MUX selection to Auto */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_MUX_CTL, MUX_CTL_AUTO);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, 1);
        /* Restore ADC Enable */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, adc_en);
        /* Restore ADC mode */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_MODE, adc_mode);
        /* Restore FSM state */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_FSM_EN, fsm_en);
        /* Restore electrical detection */
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ELECT_SCHMT_ISRC, elect_ctl);

        return is_cross_conn;
}

static int wcd_mbhc_adc_get_hs_thres(struct wcd_mbhc *mbhc)
{
        int hs_threshold, micbias_mv;

        micbias_mv = wcd_mbhc_get_micbias(mbhc);
        if (mbhc->cfg->hs_thr) {
                if (mbhc->cfg->micb_mv == micbias_mv)
                        hs_threshold = mbhc->cfg->hs_thr;
                else
                        hs_threshold = (mbhc->cfg->hs_thr *
                                micbias_mv) / mbhc->cfg->micb_mv;
        } else {
                hs_threshold = ((WCD_MBHC_ADC_HS_THRESHOLD_MV *
                        micbias_mv) / WCD_MBHC_ADC_MICBIAS_MV);
        }
        return hs_threshold;
}

static int wcd_mbhc_adc_get_hph_thres(struct wcd_mbhc *mbhc)
{
        int hph_threshold, micbias_mv;

        micbias_mv = wcd_mbhc_get_micbias(mbhc);
        if (mbhc->cfg->hph_thr) {
                if (mbhc->cfg->micb_mv == micbias_mv)
                        hph_threshold = mbhc->cfg->hph_thr;
                else
                        hph_threshold = (mbhc->cfg->hph_thr *
                                micbias_mv) / mbhc->cfg->micb_mv;
        } else {
                hph_threshold = ((WCD_MBHC_ADC_HPH_THRESHOLD_MV *
                        micbias_mv) / WCD_MBHC_ADC_MICBIAS_MV);
        }
        return hph_threshold;
}

static void wcd_mbhc_adc_update_fsm_source(struct wcd_mbhc *mbhc,
                                           enum wcd_mbhc_plug_type plug_type)
{
        bool micbias2 = false;

        switch (plug_type) {
        case MBHC_PLUG_TYPE_HEADPHONE:
                wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 3);
                break;
        case MBHC_PLUG_TYPE_HEADSET:
                if (mbhc->mbhc_cb->micbias_enable_status)
                        micbias2 = mbhc->mbhc_cb->micbias_enable_status(mbhc->component,
                                                                        MIC_BIAS_2);

                if (!mbhc->is_hs_recording && !micbias2)
                        wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 3);
                break;
        default:
                wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 0);
                break;

        }
}

static void wcd_mbhc_bcs_enable(struct wcd_mbhc *mbhc, int plug_type, bool enable)
{
        switch (plug_type) {
        case MBHC_PLUG_TYPE_HEADSET:
        case MBHC_PLUG_TYPE_HEADPHONE:
                if (mbhc->mbhc_cb->bcs_enable)
                        mbhc->mbhc_cb->bcs_enable(mbhc->component, enable);
                break;
        default:
                break;
        }
}

static int wcd_mbhc_get_plug_from_adc(struct wcd_mbhc *mbhc, int adc_result)

{
        enum wcd_mbhc_plug_type plug_type;
        u32 hph_thr, hs_thr;

        hs_thr = wcd_mbhc_adc_get_hs_thres(mbhc);
        hph_thr = wcd_mbhc_adc_get_hph_thres(mbhc);

        if (adc_result < hph_thr)
                plug_type = MBHC_PLUG_TYPE_HEADPHONE;
        else if (adc_result > hs_thr)
                plug_type = MBHC_PLUG_TYPE_HIGH_HPH;
        else
                plug_type = MBHC_PLUG_TYPE_HEADSET;

        return plug_type;
}

static int wcd_mbhc_get_spl_hs_thres(struct wcd_mbhc *mbhc)
{
        int hs_threshold, micbias_mv;

        micbias_mv = wcd_mbhc_get_micbias(mbhc);
        if (mbhc->cfg->hs_thr && mbhc->cfg->micb_mv != WCD_MBHC_ADC_MICBIAS_MV) {
                if (mbhc->cfg->micb_mv == micbias_mv)
                        hs_threshold = mbhc->cfg->hs_thr;
                else
                        hs_threshold = (mbhc->cfg->hs_thr * micbias_mv) / mbhc->cfg->micb_mv;
        } else {
                hs_threshold = ((WCD_MBHC_ADC_HS_THRESHOLD_MV * micbias_mv) /
                                                        WCD_MBHC_ADC_MICBIAS_MV);
        }
        return hs_threshold;
}

static bool wcd_mbhc_check_for_spl_headset(struct wcd_mbhc *mbhc)
{
        bool is_spl_hs = false;
        int output_mv, hs_threshold, hph_threshold;

        if (!mbhc->mbhc_cb->mbhc_micb_ctrl_thr_mic)
                return false;

        /* Bump up MIC_BIAS2 to 2.7V */
        mbhc->mbhc_cb->mbhc_micb_ctrl_thr_mic(mbhc->component, MIC_BIAS_2, true);
        usleep_range(10000, 10100);

        output_mv = wcd_measure_adc_once(mbhc, MUX_CTL_IN2P);
        hs_threshold = wcd_mbhc_get_spl_hs_thres(mbhc);
        hph_threshold = wcd_mbhc_adc_get_hph_thres(mbhc);

        if (!(output_mv > hs_threshold || output_mv < hph_threshold))
                is_spl_hs = true;

        /* Back MIC_BIAS2 to 1.8v if the type is not special headset */
        if (!is_spl_hs) {
                mbhc->mbhc_cb->mbhc_micb_ctrl_thr_mic(mbhc->component, MIC_BIAS_2, false);
                /* Add 10ms delay for micbias to settle */
                usleep_range(10000, 10100);
        }

        return is_spl_hs;
}

static void wcd_correct_swch_plug(struct work_struct *work)
{
        struct wcd_mbhc *mbhc;
        struct snd_soc_component *component;
        enum wcd_mbhc_plug_type plug_type = MBHC_PLUG_TYPE_INVALID;
        unsigned long timeout;
        int pt_gnd_mic_swap_cnt = 0;
        int output_mv, cross_conn, hs_threshold, try = 0, micbias_mv;
        bool is_spl_hs = false;
        bool is_pa_on;
        int ret;

        mbhc = container_of(work, struct wcd_mbhc, correct_plug_swch);
        component = mbhc->component;

        ret = pm_runtime_get_sync(component->dev);
        if (ret < 0 && ret != -EACCES) {
                dev_err_ratelimited(component->dev,
                                    "pm_runtime_get_sync failed in %s, ret %d\n",
                                    __func__, ret);
                pm_runtime_put_noidle(component->dev);
                return;
        }
        micbias_mv = wcd_mbhc_get_micbias(mbhc);
        hs_threshold = wcd_mbhc_adc_get_hs_thres(mbhc);

        /* Mask ADC COMPLETE interrupt */
        disable_irq_nosync(mbhc->intr_ids->mbhc_hs_ins_intr);

        /* Check for cross connection */
        do {
                cross_conn = wcd_check_cross_conn(mbhc);
                try++;
        } while (try < mbhc->swap_thr);

        if (cross_conn > 0) {
                plug_type = MBHC_PLUG_TYPE_GND_MIC_SWAP;
                dev_err(mbhc->dev, "cross connection found, Plug type %d\n",
                        plug_type);
                goto correct_plug_type;
        }

        /* Find plug type */
        output_mv = wcd_measure_adc_continuous(mbhc);
        plug_type = wcd_mbhc_get_plug_from_adc(mbhc, output_mv);

        /*
         * Report plug type if it is either headset or headphone
         * else start the 3 sec loop
         */
        switch (plug_type) {
        case MBHC_PLUG_TYPE_HEADPHONE:
                wcd_mbhc_find_plug_and_report(mbhc, plug_type);
                break;
        case MBHC_PLUG_TYPE_HEADSET:
                wcd_mbhc_find_plug_and_report(mbhc, plug_type);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_MODE, 0);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, 0);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_DETECTION_DONE, 1);
                break;
        default:
                break;
        }

correct_plug_type:

        /* Disable BCS slow insertion detection */
        wcd_mbhc_bcs_enable(mbhc, plug_type, false);

        timeout = jiffies + msecs_to_jiffies(HS_DETECT_PLUG_TIME_MS);

        while (!time_after(jiffies, timeout)) {
                if (mbhc->hs_detect_work_stop) {
                        wcd_micbias_disable(mbhc);
                        goto exit;
                }

                msleep(180);
                /*
                 * Use ADC single mode to minimize the chance of missing out
                 * btn press/release for HEADSET type during correct work.
                 */
                output_mv = wcd_measure_adc_once(mbhc, MUX_CTL_IN2P);
                plug_type = wcd_mbhc_get_plug_from_adc(mbhc, output_mv);
                is_pa_on = wcd_mbhc_read_field(mbhc, WCD_MBHC_HPH_PA_EN);

                if (output_mv > hs_threshold && !is_spl_hs) {
                        is_spl_hs = wcd_mbhc_check_for_spl_headset(mbhc);
                        output_mv = wcd_measure_adc_once(mbhc, MUX_CTL_IN2P);

                        if (is_spl_hs) {
                                hs_threshold *= wcd_mbhc_get_micbias(mbhc);
                                hs_threshold /= micbias_mv;
                        }
                }

                if ((output_mv <= hs_threshold) && !is_pa_on) {
                        /* Check for cross connection*/
                        cross_conn = wcd_check_cross_conn(mbhc);
                        if (cross_conn > 0) { /* cross-connection */
                                pt_gnd_mic_swap_cnt++;
                                if (pt_gnd_mic_swap_cnt < mbhc->swap_thr)
                                        continue;
                                else
                                        plug_type = MBHC_PLUG_TYPE_GND_MIC_SWAP;
                        } else if (!cross_conn) { /* no cross connection */
                                pt_gnd_mic_swap_cnt = 0;
                                plug_type = wcd_mbhc_get_plug_from_adc(mbhc, output_mv);
                                continue;
                        } else /* Error if (cross_conn < 0) */
                                continue;

                        if (pt_gnd_mic_swap_cnt == mbhc->swap_thr) {
                                /* US_EU gpio present, flip switch */
                                if (mbhc->cfg->swap_gnd_mic) {
                                        if (mbhc->cfg->swap_gnd_mic(component))
                                                continue;
                                }
                        }
                }

                /* cable is extension cable */
                if (output_mv > hs_threshold || mbhc->force_linein)
                        plug_type = MBHC_PLUG_TYPE_HIGH_HPH;
        }

        wcd_mbhc_bcs_enable(mbhc, plug_type, true);

        if (plug_type == MBHC_PLUG_TYPE_HIGH_HPH) {
                if (is_spl_hs)
                        plug_type = MBHC_PLUG_TYPE_HEADSET;
                else
                        wcd_mbhc_write_field(mbhc, WCD_MBHC_ELECT_ISRC_EN, 1);
        }

        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_MODE, 0);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, 0);
        wcd_mbhc_find_plug_and_report(mbhc, plug_type);

        /*
         * Set DETECTION_DONE bit for HEADSET
         * so that btn press/release interrupt can be generated.
         * For other plug type, clear the bit.
         */
        if (plug_type == MBHC_PLUG_TYPE_HEADSET)
                wcd_mbhc_write_field(mbhc, WCD_MBHC_DETECTION_DONE, 1);
        else
                wcd_mbhc_write_field(mbhc, WCD_MBHC_DETECTION_DONE, 0);

        if (mbhc->mbhc_cb->mbhc_micbias_control)
                wcd_mbhc_adc_update_fsm_source(mbhc, plug_type);

exit:
        if (mbhc->mbhc_cb->mbhc_micbias_control/* &&  !mbhc->micbias_enable*/)
                mbhc->mbhc_cb->mbhc_micbias_control(component, MIC_BIAS_2, MICB_DISABLE);

        /*
         * If plug type is corrected from special headset to headphone,
         * clear the micbias enable flag, set micbias back to 1.8V and
         * disable micbias.
         */
        if (plug_type == MBHC_PLUG_TYPE_HEADPHONE) {
                wcd_micbias_disable(mbhc);
                /*
                 * Enable ADC COMPLETE interrupt for HEADPHONE.
                 * Btn release may happen after the correct work, ADC COMPLETE
                 * interrupt needs to be captured to correct plug type.
                 */
                enable_irq(mbhc->intr_ids->mbhc_hs_ins_intr);
        }

        if (mbhc->mbhc_cb->hph_pull_down_ctrl)
                mbhc->mbhc_cb->hph_pull_down_ctrl(component, true);

        pm_runtime_put_autosuspend(component->dev);
}

static irqreturn_t wcd_mbhc_adc_hs_rem_irq(int irq, void *data)
{
        struct wcd_mbhc *mbhc = data;
        unsigned long timeout;
        int adc_threshold, output_mv, retry = 0;

        mutex_lock(&mbhc->lock);
        timeout = jiffies + msecs_to_jiffies(WCD_FAKE_REMOVAL_MIN_PERIOD_MS);
        adc_threshold = wcd_mbhc_adc_get_hs_thres(mbhc);

        do {
                retry++;
                /*
                 * read output_mv every 10ms to look for
                 * any change in IN2_P
                 */
                usleep_range(10000, 10100);
                output_mv = wcd_measure_adc_once(mbhc, MUX_CTL_IN2P);

                /* Check for fake removal */
                if ((output_mv <= adc_threshold) && retry > FAKE_REM_RETRY_ATTEMPTS)
                        goto exit;
        } while (!time_after(jiffies, timeout));

        /*
         * ADC COMPLETE and ELEC_REM interrupts are both enabled for
         * HEADPHONE, need to reject the ADC COMPLETE interrupt which
         * follows ELEC_REM one when HEADPHONE is removed.
         */
        if (mbhc->current_plug == MBHC_PLUG_TYPE_HEADPHONE)
                mbhc->extn_cable_hph_rem = true;

        wcd_mbhc_write_field(mbhc, WCD_MBHC_DETECTION_DONE, 0);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_MODE, 0);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_ADC_EN, 0);
        wcd_mbhc_elec_hs_report_unplug(mbhc);
        wcd_mbhc_write_field(mbhc, WCD_MBHC_BTN_ISRC_CTL, 0);

exit:
        mutex_unlock(&mbhc->lock);
        return IRQ_HANDLED;
}

static irqreturn_t wcd_mbhc_adc_hs_ins_irq(int irq, void *data)
{
        struct wcd_mbhc *mbhc = data;
        u8 clamp_state;
        u8 clamp_retry = WCD_MBHC_FAKE_INS_RETRY;

        /*
         * ADC COMPLETE and ELEC_REM interrupts are both enabled for HEADPHONE,
         * need to reject the ADC COMPLETE interrupt which follows ELEC_REM one
         * when HEADPHONE is removed.
         */
        if (mbhc->extn_cable_hph_rem == true) {
                mbhc->extn_cable_hph_rem = false;
                return IRQ_HANDLED;
        }

        do {
                clamp_state = wcd_mbhc_read_field(mbhc, WCD_MBHC_IN2P_CLAMP_STATE);
                if (clamp_state)
                        return IRQ_HANDLED;
                /*
                 * check clamp for 120ms but at 30ms chunks to leave
                 * room for other interrupts to be processed
                 */
                usleep_range(30000, 30100);
        } while (--clamp_retry);

        /*
         * If current plug is headphone then there is no chance to
         * get ADC complete interrupt, so connected cable should be
         * headset not headphone.
         */
        if (mbhc->current_plug == MBHC_PLUG_TYPE_HEADPHONE) {
                disable_irq_nosync(mbhc->intr_ids->mbhc_hs_ins_intr);
                wcd_mbhc_write_field(mbhc, WCD_MBHC_DETECTION_DONE, 1);
                wcd_mbhc_find_plug_and_report(mbhc, MBHC_PLUG_TYPE_HEADSET);
                return IRQ_HANDLED;
        }

        return IRQ_HANDLED;
}

int wcd_mbhc_get_impedance(struct wcd_mbhc *mbhc, uint32_t *zl, uint32_t *zr)
{
        *zl = mbhc->zl;
        *zr = mbhc->zr;

        if (*zl && *zr)
                return 0;
        else
                return -EINVAL;
}
EXPORT_SYMBOL(wcd_mbhc_get_impedance);

void wcd_mbhc_set_hph_type(struct wcd_mbhc *mbhc, int hph_type)
{
        mbhc->hph_type = hph_type;
}
EXPORT_SYMBOL(wcd_mbhc_set_hph_type);

int wcd_mbhc_get_hph_type(struct wcd_mbhc *mbhc)
{
        return mbhc->hph_type;
}
EXPORT_SYMBOL(wcd_mbhc_get_hph_type);

int wcd_mbhc_start(struct wcd_mbhc *mbhc, struct wcd_mbhc_config *cfg,
                   struct snd_soc_jack *jack)
{
        if (!mbhc || !cfg || !jack)
                return -EINVAL;

        mbhc->cfg = cfg;
        mbhc->jack = jack;

        return wcd_mbhc_initialise(mbhc);
}
EXPORT_SYMBOL(wcd_mbhc_start);

void wcd_mbhc_stop(struct wcd_mbhc *mbhc)
{
        mbhc->current_plug = MBHC_PLUG_TYPE_NONE;
        mbhc->hph_status = 0;
        disable_irq_nosync(mbhc->intr_ids->hph_left_ocp);
        disable_irq_nosync(mbhc->intr_ids->hph_right_ocp);
}
EXPORT_SYMBOL(wcd_mbhc_stop);

int wcd_dt_parse_mbhc_data(struct device *dev, struct wcd_mbhc_config *cfg)
{
        struct device_node *np = dev->of_node;
        int ret, i, microvolt;

        if (of_property_read_bool(np, "qcom,hphl-jack-type-normally-closed"))
                cfg->hphl_swh = false;
        else
                cfg->hphl_swh = true;

        if (of_property_read_bool(np, "qcom,ground-jack-type-normally-closed"))
                cfg->gnd_swh = false;
        else
                cfg->gnd_swh = true;

        ret = of_property_read_u32(np, "qcom,mbhc-headset-vthreshold-microvolt",
                                   &microvolt);
        if (ret)
                dev_dbg(dev, "missing qcom,mbhc-hs-mic-max-vthreshold--microvolt in dt node\n");
        else
                cfg->hs_thr = microvolt/1000;

        ret = of_property_read_u32(np, "qcom,mbhc-headphone-vthreshold-microvolt",
                                   &microvolt);
        if (ret)
                dev_dbg(dev, "missing qcom,mbhc-hs-mic-min-vthreshold-microvolt entry\n");
        else
                cfg->hph_thr = microvolt/1000;

        ret = of_property_read_u32_array(np,
                                         "qcom,mbhc-buttons-vthreshold-microvolt",
                                         &cfg->btn_high[0],
                                         WCD_MBHC_DEF_BUTTONS);
        if (ret)
                dev_err(dev, "missing qcom,mbhc-buttons-vthreshold-microvolt entry\n");

        for (i = 0; i < WCD_MBHC_DEF_BUTTONS; i++) {
                if (ret) /* default voltage */
                        cfg->btn_high[i] = 500000;
                else
                        /* Micro to Milli Volts */
                        cfg->btn_high[i] = cfg->btn_high[i]/1000;
        }

        return 0;
}
EXPORT_SYMBOL(wcd_dt_parse_mbhc_data);

struct wcd_mbhc *wcd_mbhc_init(struct snd_soc_component *component,
                               const struct wcd_mbhc_cb *mbhc_cb,
                               const struct wcd_mbhc_intr *intr_ids,
                               const struct wcd_mbhc_field *fields,
                               bool impedance_det_en)
{
        struct device *dev = component->dev;
        struct wcd_mbhc *mbhc;
        int ret;

        if (!intr_ids || !fields || !mbhc_cb || !mbhc_cb->mbhc_bias || !mbhc_cb->set_btn_thr) {
                dev_err(dev, "%s: Insufficient mbhc configuration\n", __func__);
                return ERR_PTR(-EINVAL);
        }

        mbhc = kzalloc_obj(*mbhc);
        if (!mbhc)
                return ERR_PTR(-ENOMEM);

        mbhc->component = component;
        mbhc->dev = dev;
        mbhc->intr_ids = intr_ids;
        mbhc->mbhc_cb = mbhc_cb;
        mbhc->fields = fields;
        mbhc->mbhc_detection_logic = WCD_DETECTION_ADC;

        if (mbhc_cb->compute_impedance)
                mbhc->impedance_detect = impedance_det_en;

        INIT_DELAYED_WORK(&mbhc->mbhc_btn_dwork, wcd_btn_long_press_fn);

        mutex_init(&mbhc->lock);

        INIT_WORK(&mbhc->correct_plug_swch, wcd_correct_swch_plug);
        INIT_WORK(&mbhc->mbhc_plug_detect_work, mbhc_plug_detect_fn);

        ret = request_threaded_irq(mbhc->intr_ids->mbhc_sw_intr, NULL,
                                        wcd_mbhc_mech_plug_detect_irq,
                                        IRQF_ONESHOT | IRQF_TRIGGER_RISING,
                                        "mbhc sw intr", mbhc);
        if (ret)
                goto err_free_mbhc;

        ret = request_threaded_irq(mbhc->intr_ids->mbhc_btn_press_intr, NULL,
                                        wcd_mbhc_btn_press_handler,
                                        IRQF_ONESHOT | IRQF_TRIGGER_RISING,
                                        "Button Press detect", mbhc);
        if (ret)
                goto err_free_sw_intr;

        ret = request_threaded_irq(mbhc->intr_ids->mbhc_btn_release_intr, NULL,
                                        wcd_mbhc_btn_release_handler,
                                        IRQF_ONESHOT | IRQF_TRIGGER_RISING,
                                        "Button Release detect", mbhc);
        if (ret)
                goto err_free_btn_press_intr;

        ret = request_threaded_irq(mbhc->intr_ids->mbhc_hs_ins_intr, NULL,
                                        wcd_mbhc_adc_hs_ins_irq,
                                        IRQF_ONESHOT | IRQF_TRIGGER_RISING,
                                        "Elect Insert", mbhc);
        if (ret)
                goto err_free_btn_release_intr;

        disable_irq_nosync(mbhc->intr_ids->mbhc_hs_ins_intr);

        ret = request_threaded_irq(mbhc->intr_ids->mbhc_hs_rem_intr, NULL,
                                        wcd_mbhc_adc_hs_rem_irq,
                                        IRQF_ONESHOT | IRQF_TRIGGER_RISING,
                                        "Elect Remove", mbhc);
        if (ret)
                goto err_free_hs_ins_intr;

        disable_irq_nosync(mbhc->intr_ids->mbhc_hs_rem_intr);

        ret = request_threaded_irq(mbhc->intr_ids->hph_left_ocp, NULL,
                                        wcd_mbhc_hphl_ocp_irq,
                                        IRQF_ONESHOT | IRQF_TRIGGER_RISING,
                                        "HPH_L OCP detect", mbhc);
        if (ret)
                goto err_free_hs_rem_intr;

        ret = request_threaded_irq(mbhc->intr_ids->hph_right_ocp, NULL,
                                        wcd_mbhc_hphr_ocp_irq,
                                        IRQF_ONESHOT | IRQF_TRIGGER_RISING,
                                        "HPH_R OCP detect", mbhc);
        if (ret)
                goto err_free_hph_left_ocp;

        return mbhc;

err_free_hph_left_ocp:
        free_irq(mbhc->intr_ids->hph_left_ocp, mbhc);
err_free_hs_rem_intr:
        free_irq(mbhc->intr_ids->mbhc_hs_rem_intr, mbhc);
err_free_hs_ins_intr:
        free_irq(mbhc->intr_ids->mbhc_hs_ins_intr, mbhc);
err_free_btn_release_intr:
        free_irq(mbhc->intr_ids->mbhc_btn_release_intr, mbhc);
err_free_btn_press_intr:
        free_irq(mbhc->intr_ids->mbhc_btn_press_intr, mbhc);
err_free_sw_intr:
        free_irq(mbhc->intr_ids->mbhc_sw_intr, mbhc);
err_free_mbhc:
        kfree(mbhc);

        dev_err(dev, "Failed to request mbhc interrupts %d\n", ret);

        return ERR_PTR(ret);
}
EXPORT_SYMBOL(wcd_mbhc_init);

void wcd_mbhc_deinit(struct wcd_mbhc *mbhc)
{
        free_irq(mbhc->intr_ids->hph_right_ocp, mbhc);
        free_irq(mbhc->intr_ids->hph_left_ocp, mbhc);
        free_irq(mbhc->intr_ids->mbhc_hs_rem_intr, mbhc);
        free_irq(mbhc->intr_ids->mbhc_hs_ins_intr, mbhc);
        free_irq(mbhc->intr_ids->mbhc_btn_release_intr, mbhc);
        free_irq(mbhc->intr_ids->mbhc_btn_press_intr, mbhc);
        free_irq(mbhc->intr_ids->mbhc_sw_intr, mbhc);

        mutex_lock(&mbhc->lock);
        wcd_cancel_hs_detect_plug(mbhc, &mbhc->correct_plug_swch);
        cancel_work_sync(&mbhc->mbhc_plug_detect_work);
        mutex_unlock(&mbhc->lock);

        kfree(mbhc);
}
EXPORT_SYMBOL(wcd_mbhc_deinit);

MODULE_DESCRIPTION("wcd MBHC v2 module");
MODULE_LICENSE("GPL");