|  | // 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_mark_last_busy(component->dev); | 
|  | 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, true)) | 
|  | 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_mark_last_busy(component->dev); | 
|  | 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", | 
|  | µvolt); | 
|  | 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", | 
|  | µvolt); | 
|  | 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(sizeof(*mbhc), GFP_KERNEL); | 
|  | 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); | 
|  |  | 
|  | static int __init mbhc_init(void) | 
|  | { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void __exit mbhc_exit(void) | 
|  | { | 
|  | } | 
|  |  | 
|  | module_init(mbhc_init); | 
|  | module_exit(mbhc_exit); | 
|  |  | 
|  | MODULE_DESCRIPTION("wcd MBHC v2 module"); | 
|  | MODULE_LICENSE("GPL"); |