ASoC: wcd9xxx: integrate 4-pole extension cable support

Commit 07b6bd6a9a1e431a86efa76b14030d882ee7771b is checked into wcd9310
driver to support 4-pole extension cable detection.
Integrate the commit to wcd9xxx-mbhc driver.

Change-Id: I44973b5d9562b71b041498e3c9ad9709f4266181
Signed-off-by: Joonwoo Park <joonwoop@codeaurora.org>
diff --git a/sound/soc/codecs/wcd9xxx-mbhc.c b/sound/soc/codecs/wcd9xxx-mbhc.c
index 16d415b..3d7c0d4 100644
--- a/sound/soc/codecs/wcd9xxx-mbhc.c
+++ b/sound/soc/codecs/wcd9xxx-mbhc.c
@@ -37,7 +37,8 @@
 #include "wcd9xxx-resmgr.h"
 
 #define WCD9XXX_JACK_MASK (SND_JACK_HEADSET | SND_JACK_OC_HPHL | \
-			   SND_JACK_OC_HPHR | SND_JACK_UNSUPPORTED)
+			   SND_JACK_OC_HPHR | SND_JACK_LINEOUT | \
+			   SND_JACK_UNSUPPORTED)
 #define WCD9XXX_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 | \
@@ -574,6 +575,8 @@
 {
 	WCD9XXX_BCL_ASSERT_LOCKED(mbhc->resmgr);
 
+	pr_debug("%s: enter insertion %d hph_status %x\n",
+		 __func__, insertion, mbhc->hph_status);
 	if (!insertion) {
 		/* Report removal */
 		mbhc->hph_status &= ~jack_type;
@@ -601,6 +604,16 @@
 		mbhc->current_plug = PLUG_TYPE_NONE;
 		mbhc->polling_active = false;
 	} else {
+		if (mbhc->mbhc_cfg->detect_extn_cable) {
+			/* Report removal of current jack type */
+			if (mbhc->hph_status != jack_type) {
+				pr_debug("%s: Reporting removal (%x)\n",
+					 __func__, mbhc->hph_status);
+				wcd9xxx_jack_report(&mbhc->headset_jack,
+						    0, WCD9XXX_JACK_MASK);
+				mbhc->hph_status = 0;
+			}
+		}
 		/* Report insertion */
 		mbhc->hph_status |= jack_type;
 
@@ -611,6 +624,8 @@
 		} else if (jack_type == SND_JACK_HEADSET) {
 			mbhc->polling_active = BUTTON_POLLING_SUPPORTED;
 			mbhc->current_plug = PLUG_TYPE_HEADSET;
+		} else if (jack_type == SND_JACK_LINEOUT) {
+			mbhc->current_plug = PLUG_TYPE_HIGH_HPH;
 		}
 		pr_debug("%s: Reporting insertion %d(%x)\n", __func__,
 			 jack_type, mbhc->hph_status);
@@ -620,6 +635,8 @@
 	}
 	/* Setup insert detect */
 	wcd9xxx_insert_detect_setup(mbhc, !insertion);
+
+	pr_debug("%s: leave hph_status %x\n", __func__, mbhc->hph_status);
 }
 
 /* should be called under interrupt context that hold suspend */
@@ -929,6 +946,7 @@
 	bool highdelta;
 	bool ahighv = false, highv;
 
+	pr_debug("%s: enter\n", __func__);
 	WCD9XXX_BCL_ASSERT_LOCKED(mbhc->resmgr);
 
 	/* make sure override is on */
@@ -1035,6 +1053,7 @@
 	}
 
 	pr_debug("%s: Detected plug type %d\n", __func__, plug_type[0]);
+	pr_debug("%s: leave\n", __func__);
 	return plug_type[0];
 }
 
@@ -1068,6 +1087,9 @@
 	const struct wcd9xxx_mbhc_plug_detect_cfg *plug_det =
 	    WCD9XXX_MBHC_CAL_PLUG_DET_PTR(mbhc->mbhc_cfg->calibration);
 
+	pr_debug("%s: enter insertion(%d) trigger(0x%x)\n",
+		 __func__, insertion, trigger);
+
 	if (!mbhc->mbhc_cfg->calibration) {
 		pr_err("Error, no wcd9xxx calibration\n");
 		return -EINVAL;
@@ -1179,6 +1201,7 @@
 
 	wcd9xxx_enable_irq(mbhc->resmgr->core, WCD9XXX_IRQ_MBHC_INSERTION);
 	snd_soc_update_bits(codec, WCD9XXX_A_CDC_MBHC_INT_CTL, 0x1, 0x1);
+	pr_debug("%s: leave\n", __func__);
 
 	return 0;
 }
@@ -1187,6 +1210,9 @@
 static void wcd9xxx_find_plug_and_report(struct wcd9xxx_mbhc *mbhc,
 					 enum wcd9xxx_mbhc_plug_type plug_type)
 {
+	pr_debug("%s: enter current_plug(%d) new_plug(%d)\n",
+		 __func__, mbhc->current_plug, plug_type);
+
 	WCD9XXX_BCL_ASSERT_LOCKED(mbhc->resmgr);
 
 	if (plug_type == PLUG_TYPE_HEADPHONE &&
@@ -1198,11 +1224,14 @@
 		wcd9xxx_report_plug(mbhc, 1, SND_JACK_HEADPHONE);
 		wcd9xxx_cleanup_hs_polling(mbhc);
 	} else if (plug_type == PLUG_TYPE_GND_MIC_SWAP) {
-		if (mbhc->current_plug == PLUG_TYPE_HEADSET)
-			wcd9xxx_report_plug(mbhc, 0, SND_JACK_HEADSET);
-		else if (mbhc->current_plug == PLUG_TYPE_HEADPHONE)
-			wcd9xxx_report_plug(mbhc, 0, SND_JACK_HEADPHONE);
-
+		if (!mbhc->mbhc_cfg->detect_extn_cable) {
+			if (mbhc->current_plug == PLUG_TYPE_HEADSET)
+				wcd9xxx_report_plug(mbhc, 0,
+							 SND_JACK_HEADSET);
+			else if (mbhc->current_plug == PLUG_TYPE_HEADPHONE)
+				wcd9xxx_report_plug(mbhc, 0,
+							 SND_JACK_HEADPHONE);
+		}
 		wcd9xxx_report_plug(mbhc, 1, SND_JACK_UNSUPPORTED);
 		wcd9xxx_cleanup_hs_polling(mbhc);
 	} else if (plug_type == PLUG_TYPE_HEADSET) {
@@ -1214,17 +1243,35 @@
 		msleep(100);
 		wcd9xxx_start_hs_polling(mbhc);
 	} else if (plug_type == PLUG_TYPE_HIGH_HPH) {
-		if (mbhc->current_plug == PLUG_TYPE_NONE)
-			wcd9xxx_report_plug(mbhc, 1, SND_JACK_HEADPHONE);
-		wcd9xxx_cleanup_hs_polling(mbhc);
-		pr_debug("setup mic trigger for further detection\n");
-		mbhc->lpi_enabled = true;
-		wcd9xxx_enable_hs_detect(mbhc, 1, MBHC_USE_MB_TRIGGER |
-						  MBHC_USE_HPHL_TRIGGER, false);
+		if (mbhc->mbhc_cfg->detect_extn_cable) {
+			/* High impedance device found. Report as LINEOUT*/
+			wcd9xxx_report_plug(mbhc, 1, SND_JACK_LINEOUT);
+			wcd9xxx_cleanup_hs_polling(mbhc);
+			pr_debug("%s: setup mic trigger for further detection\n",
+				 __func__);
+			mbhc->lpi_enabled = true;
+			/*
+			 * Do not enable HPHL trigger. If playback is active,
+			 * it might lead to continuous false HPHL triggers
+			 */
+			wcd9xxx_enable_hs_detect(mbhc, 1, MBHC_USE_MB_TRIGGER,
+						 false);
+		} else {
+			if (mbhc->current_plug == PLUG_TYPE_NONE)
+				wcd9xxx_report_plug(mbhc, 1,
+							 SND_JACK_HEADPHONE);
+			wcd9xxx_cleanup_hs_polling(mbhc);
+			pr_debug("setup mic trigger for further detection\n");
+			mbhc->lpi_enabled = true;
+			wcd9xxx_enable_hs_detect(mbhc, 1, MBHC_USE_MB_TRIGGER |
+							  MBHC_USE_HPHL_TRIGGER,
+						 false);
+		}
 	} else {
 		WARN(1, "Unexpected current plug_type %d, plug_type %d\n",
 		     mbhc->current_plug, plug_type);
 	}
+	pr_debug("%s: leave\n", __func__);
 }
 
 /* called under codec_resource_lock acquisition */
@@ -1260,6 +1307,7 @@
 			 __func__, plug_type);
 		wcd9xxx_find_plug_and_report(mbhc, plug_type);
 	}
+	pr_debug("%s: leave\n", __func__);
 }
 
 /* called under codec_resource_lock acquisition */
@@ -1269,6 +1317,7 @@
 	const struct wcd9xxx_mbhc_plug_detect_cfg *plug_det =
 		WCD9XXX_MBHC_CAL_PLUG_DET_PTR(mbhc->mbhc_cfg->calibration);
 
+	pr_debug("%s: enter\n", __func__);
 	WCD9XXX_BCL_ASSERT_LOCKED(mbhc->resmgr);
 
 	/*
@@ -1289,6 +1338,7 @@
 			 __func__);
 	else
 		wcd9xxx_mbhc_decide_swch_plug(mbhc);
+	pr_debug("%s: leave\n", __func__);
 }
 
 /* called only from interrupt which is under codec_resource_lock acquisition */
@@ -1313,6 +1363,19 @@
 			pr_debug("%s: Invalid insertion stop plug detection\n",
 				 __func__);
 		}
+	} else if (mbhc->mbhc_cfg->detect_extn_cable) {
+		pr_debug("%s: Removal\n", __func__);
+		if (!wcd9xxx_swch_level_remove(mbhc)) {
+			/*
+			 * Switch indicates, something is still inserted.
+			 * This could be extension cable i.e. headset is
+			 * removed from extension cable.
+			 */
+			/* cancel detect plug */
+			wcd9xxx_cancel_hs_detect_plug(mbhc,
+						      &mbhc->correct_plug_swch);
+			wcd9xxx_mbhc_decide_swch_plug(mbhc);
+		}
 	} else {
 		pr_err("%s: Switch IRQ used, invalid MBHC Removal\n", __func__);
 	}
@@ -1398,9 +1461,100 @@
 /* called only from interrupt which is under codec_resource_lock acquisition */
 static void wcd9xxx_hs_remove_irq_swch(struct wcd9xxx_mbhc *mbhc)
 {
+	pr_debug("%s: enter\n", __func__);
 	if (wcd9xxx_hs_remove_settle(mbhc))
 		wcd9xxx_start_hs_polling(mbhc);
-	pr_debug("%s: remove settle done\n", __func__);
+	pr_debug("%s: leave\n", __func__);
+}
+
+/* called only from interrupt which is under codec_resource_lock acquisition */
+static void wcd9xxx_hs_remove_irq_noswch(struct wcd9xxx_mbhc *mbhc)
+{
+	short bias_value;
+	bool removed = true;
+	struct snd_soc_codec *codec = mbhc->codec;
+	const struct wcd9xxx_mbhc_general_cfg *generic =
+		WCD9XXX_MBHC_CAL_GENERAL_PTR(mbhc->mbhc_cfg->calibration);
+	int min_us = FAKE_REMOVAL_MIN_PERIOD_MS * 1000;
+
+	pr_debug("%s: enter\n", __func__);
+	if (mbhc->current_plug != PLUG_TYPE_HEADSET) {
+		pr_debug("%s(): Headset is not inserted, ignore removal\n",
+			 __func__);
+		snd_soc_update_bits(codec, WCD9XXX_A_CDC_MBHC_CLK_CTL,
+				0x08, 0x08);
+		return;
+	}
+
+	usleep_range(generic->t_shutdown_plug_rem,
+			generic->t_shutdown_plug_rem);
+
+	do {
+		bias_value = wcd9xxx_codec_sta_dce(mbhc, 1,  true);
+		pr_debug("%s: DCE %d,%d, %d us left\n", __func__, bias_value,
+			 wcd9xxx_codec_sta_dce_v(mbhc, 1, bias_value), min_us);
+		if (bias_value < wcd9xxx_get_current_v_ins(mbhc, false)) {
+			pr_debug("%s: checking false removal\n", __func__);
+			msleep(500);
+			removed = !wcd9xxx_hs_remove_settle(mbhc);
+			pr_debug("%s: headset %sactually removed\n", __func__,
+				 removed ? "" : "not ");
+			break;
+		}
+		min_us -= mbhc->mbhc_data.t_dce;
+	} while (min_us > 0);
+
+	if (removed) {
+		if (mbhc->mbhc_cfg->detect_extn_cable) {
+			if (!wcd9xxx_swch_level_remove(mbhc)) {
+				/*
+				 * extension cable is still plugged in
+				 * report it as LINEOUT device
+				 */
+				wcd9xxx_report_plug(mbhc, 1, SND_JACK_LINEOUT);
+				wcd9xxx_cleanup_hs_polling(mbhc);
+				wcd9xxx_enable_hs_detect(mbhc, 1,
+							 MBHC_USE_MB_TRIGGER,
+							 false);
+			}
+		} else {
+			/* Cancel possibly running hs_detect_work */
+			wcd9xxx_cancel_hs_detect_plug(mbhc,
+						    &mbhc->correct_plug_noswch);
+			/*
+			 * If this removal is not false, first check the micbias
+			 * switch status and switch it to LDOH if it is already
+			 * switched to VDDIO.
+			 */
+			wcd9xxx_switch_micbias(mbhc, 0);
+
+			wcd9xxx_report_plug(mbhc, 0, SND_JACK_HEADSET);
+			wcd9xxx_cleanup_hs_polling(mbhc);
+			wcd9xxx_enable_hs_detect(mbhc, 1, MBHC_USE_MB_TRIGGER |
+							  MBHC_USE_HPHL_TRIGGER,
+						 true);
+		}
+	} else {
+		wcd9xxx_start_hs_polling(mbhc);
+	}
+	pr_debug("%s: leave\n", __func__);
+}
+
+/* called only from interrupt which is under codec_resource_lock acquisition */
+static void wcd9xxx_hs_insert_irq_extn(struct wcd9xxx_mbhc *mbhc,
+				       bool is_mb_trigger)
+{
+	/* Cancel possibly running hs_detect_work */
+	wcd9xxx_cancel_hs_detect_plug(mbhc, &mbhc->correct_plug_swch);
+
+	if (is_mb_trigger) {
+		pr_debug("%s: Waiting for Headphone left trigger\n", __func__);
+		wcd9xxx_enable_hs_detect(mbhc, 1, MBHC_USE_HPHL_TRIGGER, false);
+	} else  {
+		pr_debug("%s: HPHL trigger received, detecting plug type\n",
+			 __func__);
+		wcd9xxx_mbhc_detect_plug_type(mbhc);
+	}
 }
 
 static irqreturn_t wcd9xxx_hs_remove_irq(int irq, void *data)
@@ -1415,7 +1569,11 @@
 	if (vddio)
 		__wcd9xxx_switch_micbias(mbhc, 0, false, true);
 
-	wcd9xxx_hs_remove_irq_swch(mbhc);
+	if (mbhc->mbhc_cfg->detect_extn_cable &&
+	    !wcd9xxx_swch_level_remove(mbhc))
+		wcd9xxx_hs_remove_irq_noswch(mbhc);
+	else
+		wcd9xxx_hs_remove_irq_swch(mbhc);
 
 	/*
 	 * if driver turned off vddio switch and headset is not removed,
@@ -1449,7 +1607,11 @@
 	snd_soc_update_bits(codec, WCD9XXX_A_MBHC_HPH, 0x13, 0x00);
 	snd_soc_update_bits(codec, mbhc->mbhc_bias_regs.ctl_reg, 0x01, 0x00);
 
-	wcd9xxx_hs_insert_irq_swch(mbhc, is_removal);
+	if (mbhc->mbhc_cfg->detect_extn_cable &&
+	    mbhc->current_plug == PLUG_TYPE_HIGH_HPH)
+		wcd9xxx_hs_insert_irq_extn(mbhc, is_mb_trigger);
+	else
+		wcd9xxx_hs_insert_irq_swch(mbhc, is_removal);
 
 	WCD9XXX_BCL_UNLOCK(mbhc->resmgr);
 	return IRQ_HANDLED;
@@ -1642,7 +1804,7 @@
 {
 	struct wcd9xxx_mbhc *mbhc;
 	struct snd_soc_codec *codec;
-	enum wcd9xxx_mbhc_plug_type plug_type;
+	enum wcd9xxx_mbhc_plug_type plug_type = PLUG_TYPE_INVALID;
 	unsigned long timeout;
 	int retry = 0, pt_gnd_mic_swap_cnt = 0;
 	bool correction = false;
@@ -1686,18 +1848,26 @@
 		plug_type = wcd9xxx_codec_get_plug_type(mbhc, true);
 		WCD9XXX_BCL_UNLOCK(mbhc->resmgr);
 
+		pr_debug("%s: attempt(%d) current_plug(%d) new_plug(%d)\n",
+			 __func__, retry, mbhc->current_plug, plug_type);
 		if (plug_type == PLUG_TYPE_INVALID) {
 			pr_debug("Invalid plug in attempt # %d\n", retry);
-			if (retry == NUM_ATTEMPTS_TO_REPORT &&
+			if (!mbhc->mbhc_cfg->detect_extn_cable &&
+			    retry == NUM_ATTEMPTS_TO_REPORT &&
 			    mbhc->current_plug == PLUG_TYPE_NONE) {
 				wcd9xxx_report_plug(mbhc, 1,
 						    SND_JACK_HEADPHONE);
 			}
 		} else if (plug_type == PLUG_TYPE_HEADPHONE) {
 			pr_debug("Good headphone detected, continue polling\n");
-			if (mbhc->current_plug == PLUG_TYPE_NONE)
+			if (mbhc->mbhc_cfg->detect_extn_cable) {
+				if (mbhc->current_plug != plug_type)
+					wcd9xxx_report_plug(mbhc, 1,
+							    SND_JACK_HEADPHONE);
+			} else if (mbhc->current_plug == PLUG_TYPE_NONE) {
 				wcd9xxx_report_plug(mbhc, 1,
 						    SND_JACK_HEADPHONE);
+			}
 		} else {
 			if (plug_type == PLUG_TYPE_GND_MIC_SWAP) {
 				pt_gnd_mic_swap_cnt++;
@@ -1741,7 +1911,20 @@
 		wcd9xxx_turn_onoff_override(codec, false);
 
 	wcd9xxx_onoff_ext_mclk(mbhc, false);
-	pr_debug("%s: leave\n", __func__);
+
+	if (mbhc->mbhc_cfg->detect_extn_cable) {
+		WCD9XXX_BCL_LOCK(mbhc->resmgr);
+		if (mbhc->current_plug == PLUG_TYPE_HEADPHONE ||
+		    mbhc->current_plug == PLUG_TYPE_GND_MIC_SWAP ||
+		    mbhc->current_plug == PLUG_TYPE_INVALID ||
+		    plug_type == PLUG_TYPE_INVALID) {
+			/* Enable removal detection */
+			wcd9xxx_cleanup_hs_polling(mbhc);
+			wcd9xxx_enable_hs_detect(mbhc, 0, 0, false);
+		}
+		WCD9XXX_BCL_UNLOCK(mbhc->resmgr);
+	}
+	pr_debug("%s: leave current_plug(%d)\n", __func__, mbhc->current_plug);
 	/* unlock sleep */
 	wcd9xxx_unlock_sleep(mbhc->resmgr->core);
 }
@@ -1799,6 +1982,9 @@
 			wcd9xxx_cleanup_hs_polling(mbhc);
 			wcd9xxx_report_plug(mbhc, 0, SND_JACK_HEADSET);
 			is_removed = true;
+		} else if (mbhc->current_plug == PLUG_TYPE_HIGH_HPH) {
+			wcd9xxx_report_plug(mbhc, 0, SND_JACK_LINEOUT);
+			is_removed = true;
 		}
 
 		if (is_removed) {