From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20Jirman?= <megi@xff.cz>
Date: Wed, 12 May 2021 21:37:03 +0200
Subject: ASoC: sunxi: sun8i-codec: Improve jack button handling and mic
 detection

Setup ADC so that it samples with period of 16ms, disable smoothing
and enable MDATA threshold (should be below idle voltage/HMIC_DATA
value). Also enable HMIC_N, which makes sure we get HMIC_N samples
after HMIC_DATA crosses the threshold.

This allows us to perform steady state detection of HMIC_DATA, by
comparing current and previous ADC samples, to detect end of the
transient when the user de-presses the button. Otherwise ADC could
sample anywhere within the transient, and the driver may mis-issue
key-press events for other buttons attached to the resistor ladder.

Signed-off-by: Ondrej Jirman <megi@xff.cz>
---
 sound/soc/sunxi/sun50i-codec-analog.c |  12 +
 sound/soc/sunxi/sun8i-codec.c         | 241 +++++++---
 2 files changed, 188 insertions(+), 65 deletions(-)

diff --git a/sound/soc/sunxi/sun50i-codec-analog.c b/sound/soc/sunxi/sun50i-codec-analog.c
index 7ee72c0537c1..b78af30961f2 100644
--- a/sound/soc/sunxi/sun50i-codec-analog.c
+++ b/sound/soc/sunxi/sun50i-codec-analog.c
@@ -116,6 +116,11 @@
 #define SUN50I_ADDA_HS_MBIAS_CTRL	0x0e
 #define SUN50I_ADDA_HS_MBIAS_CTRL_MMICBIASEN	7
 
+#define SUN50I_ADDA_MDET_CTRL		0x1c
+#define SUN50I_ADDA_MDET_CTRL_SELDETADC_FS	4
+#define SUN50I_ADDA_MDET_CTRL_SELDETADC_DB	2
+#define SUN50I_ADDA_MDET_CTRL_SELDETADC_BF	0
+
 #define SUN50I_ADDA_JACK_MIC_CTRL	0x1d
 #define SUN50I_ADDA_JACK_MIC_CTRL_JACKDETEN	7
 #define SUN50I_ADDA_JACK_MIC_CTRL_INNERRESEN	6
@@ -501,6 +506,13 @@ static int sun50i_a64_codec_probe(struct snd_soc_component *component)
 			   codec->internal_bias_resistor <<
 				SUN50I_ADDA_JACK_MIC_CTRL_INNERRESEN);
 
+	/* Select sample interval of the ADC sample to 32ms */
+	regmap_update_bits(component->regmap, SUN50I_ADDA_MDET_CTRL,
+			   0x7 << SUN50I_ADDA_MDET_CTRL_SELDETADC_FS |
+			   0x3 << SUN50I_ADDA_MDET_CTRL_SELDETADC_BF,
+			   0x3 << SUN50I_ADDA_MDET_CTRL_SELDETADC_FS |
+			   0x3 << SUN50I_ADDA_MDET_CTRL_SELDETADC_BF);
+
 	return 0;
 }
 
diff --git a/sound/soc/sunxi/sun8i-codec.c b/sound/soc/sunxi/sun8i-codec.c
index da86e93346ea..f71923eef95c 100644
--- a/sound/soc/sunxi/sun8i-codec.c
+++ b/sound/soc/sunxi/sun8i-codec.c
@@ -15,6 +15,7 @@
 #include <linux/input.h>
 #include <linux/io.h>
 #include <linux/irq.h>
+#include <linux/mutex.h>
 #include <linux/of_device.h>
 #include <linux/pm_runtime.h>
 #include <linux/regmap.h>
@@ -124,14 +125,18 @@
 #define SUN8I_ADC_VOL_CTRL_ADCL_VOL			8
 #define SUN8I_ADC_VOL_CTRL_ADCR_VOL			0
 #define SUN8I_HMIC_CTRL1				0x110
+#define SUN8I_HMIC_CTRL1_HMIC_M				12
 #define SUN8I_HMIC_CTRL1_HMIC_N				8
+#define SUN8I_HMIC_CTRL1_MDATA_THRESHOLD_DB		5
 #define SUN8I_HMIC_CTRL1_JACK_OUT_IRQ_EN		4
 #define SUN8I_HMIC_CTRL1_JACK_IN_IRQ_EN			3
 #define SUN8I_HMIC_CTRL1_HMIC_DATA_IRQ_EN		0
 #define SUN8I_HMIC_CTRL2				0x114
 #define SUN8I_HMIC_CTRL2_HMIC_SAMPLE			14
+#define SUN8I_HMIC_CTRL2_HMIC_MDATA_THRESHOLD		8
 #define SUN8I_HMIC_CTRL2_HMIC_SF			6
 #define SUN8I_HMIC_STS					0x118
+#define SUN8I_HMIC_STS_MDATA_DISCARD			13
 #define SUN8I_HMIC_STS_HMIC_DATA			8
 #define SUN8I_HMIC_STS_JACK_OUT_IRQ_ST			4
 #define SUN8I_HMIC_STS_JACK_IN_IRQ_ST			3
@@ -161,7 +166,9 @@
 #define SUN8I_AIF_CLK_CTRL_WORD_SIZ_MASK	GENMASK(5, 4)
 #define SUN8I_AIF_CLK_CTRL_DATA_FMT_MASK	GENMASK(3, 2)
 #define SUN8I_AIF3_CLK_CTRL_AIF3_CLK_SRC_MASK	GENMASK(1, 0)
+#define SUN8I_HMIC_CTRL1_HMIC_M_MASK		GENMASK(15, 12)
 #define SUN8I_HMIC_CTRL1_HMIC_N_MASK		GENMASK(11, 8)
+#define SUN8I_HMIC_CTRL1_MDATA_THRESHOLD_DB_MASK GENMASK(6, 5)
 #define SUN8I_HMIC_CTRL2_HMIC_SAMPLE_MASK	GENMASK(15, 14)
 #define SUN8I_HMIC_CTRL2_HMIC_SF_MASK		GENMASK(7, 6)
 #define SUN8I_HMIC_STS_HMIC_DATA_MASK		GENMASK(12, 8)
@@ -209,6 +216,12 @@ struct sun8i_codec_quirks {
 	bool	lrck_inversion	: 1;
 };
 
+enum {
+	SUN8I_JACK_STATUS_DISCONNECTED,
+	SUN8I_JACK_STATUS_WAITING_HBIAS,
+	SUN8I_JACK_STATUS_CONNECTED,
+};
+
 #define AC100_NUM_SUPPLIES 4
 
 struct sun8i_codec {
@@ -221,8 +234,13 @@ struct sun8i_codec {
 	struct snd_soc_jack		jack;
 	struct delayed_work		jack_work;
 	int				jack_irq;
-	int				jack_pending;
+	int				jack_status;
+	int				jack_last_sample;
+	int				jack_last_btn;
+	ktime_t				jack_hbias_ready;
 	int				jack_type;
+	int				last_hmic_irq;
+	struct mutex			jack_mutex;
 	unsigned int			sysclk_rate;
 	int				sysclk_refcnt;
 
@@ -1419,16 +1437,19 @@ static int sun8i_codec_component_probe(struct snd_soc_component *component)
 			       BIT(SUN8I_HMIC_CTRL1_JACK_IN_IRQ_EN);
 
 		/* Reserved value required for jack IRQs to trigger. */
-		regmap_update_bits(scodec->regmap, SUN8I_HMIC_CTRL1,
-				   SUN8I_HMIC_CTRL1_HMIC_N_MASK,
-				   0xf << SUN8I_HMIC_CTRL1_HMIC_N);
+		regmap_write(scodec->regmap, SUN8I_HMIC_CTRL1,
+				   0xf << SUN8I_HMIC_CTRL1_HMIC_N |
+				   0x0 << SUN8I_HMIC_CTRL1_MDATA_THRESHOLD_DB |
+				   0x4 << SUN8I_HMIC_CTRL1_HMIC_M);
 
-		/* Sample the ADC at 64 Hz; average across 2 samples. */
-		regmap_update_bits(scodec->regmap, SUN8I_HMIC_CTRL2,
-				   SUN8I_HMIC_CTRL2_HMIC_SAMPLE_MASK |
-				   SUN8I_HMIC_CTRL2_HMIC_SF_MASK,
-				   0x1 << SUN8I_HMIC_CTRL2_HMIC_SAMPLE |
-				   0x1 << SUN8I_HMIC_CTRL2_HMIC_SF);
+		/* Sample the ADC at 128 Hz; bypass smooth filter. */
+		regmap_write(scodec->regmap, SUN8I_HMIC_CTRL2,
+				   0x0 << SUN8I_HMIC_CTRL2_HMIC_SAMPLE |
+				   0x17 << SUN8I_HMIC_CTRL2_HMIC_MDATA_THRESHOLD |
+				   0x0 << SUN8I_HMIC_CTRL2_HMIC_SF);
+
+		/* Do not discard any MDATA, enable user written MDATA threshold. */
+		regmap_write(scodec->regmap, SUN8I_HMIC_STS, 0);
 
 		regmap_update_bits(scodec->regmap, SUN8I_HMIC_CTRL1,
 				   irq_mask, irq_mask);
@@ -1485,29 +1506,6 @@ static struct regmap_config sun8i_codec_regmap_config = {
 	.cache_type	= REGCACHE_FLAT,
 };
 
-static int sun8i_codec_read_hmic_button(struct sun8i_codec *scodec)
-{
-	unsigned int value;
-	int button;
-
-	regmap_read(scodec->regmap, SUN8I_HMIC_STS, &value);
-	value  &= SUN8I_HMIC_STS_HMIC_DATA_MASK;
-	value >>= SUN8I_HMIC_STS_HMIC_DATA;
-
-	if (value < 0x2)
-		button = SND_JACK_BTN_0;
-	else if (value < 0x7)
-		button = SND_JACK_BTN_1;
-	else if (value < 0x10)
-		button = SND_JACK_BTN_2;
-	else
-		button = 0;
-
-	dev_dbg(scodec->card->dev, "HMIC ADC read %u => %#x\n", value, button);
-
-	return button;
-}
-
 static void sun8i_codec_set_hmic_bias(struct sun8i_codec *scodec, bool enable)
 {
 	struct snd_soc_dapm_context *dapm = &scodec->card->dapm;
@@ -1529,21 +1527,106 @@ static void sun8i_codec_jack_work(struct work_struct *work)
 {
 	struct sun8i_codec *scodec = container_of(work, struct sun8i_codec,
 						  jack_work.work);
+	unsigned int mdata;
 	int type;
 
-	/* Prevent a well-timed button press from affecting detection. */
-	synchronize_irq(scodec->jack_irq);
-	if (!scodec->jack_pending)
-		return;
+	mutex_lock(&scodec->jack_mutex);
 
-	if (sun8i_codec_read_hmic_button(scodec)) {
-		sun8i_codec_set_hmic_bias(scodec, false);
-		type = SND_JACK_HEADPHONE;
-	} else {
-		type = SND_JACK_HEADSET;
+	if (scodec->jack_status == SUN8I_JACK_STATUS_DISCONNECTED) {
+		if (scodec->last_hmic_irq != SUN8I_HMIC_STS_JACK_IN_IRQ_ST)
+			goto out_unlock;
+
+		scodec->jack_last_sample = -1;
+		scodec->jack_last_btn = 0;
+
+		if (scodec->jack_type & SND_JACK_MICROPHONE) {
+			/*
+			 * If we were in disconnected state, we just enable HBIAS and
+			 * wait 500ms before reading initial HDATA value.
+			 */
+			scodec->jack_hbias_ready = ktime_add_ms(ktime_get(), 600);
+			scodec->jack_status = SUN8I_JACK_STATUS_WAITING_HBIAS;
+			sun8i_codec_set_hmic_bias(scodec, true);
+			queue_delayed_work(system_power_efficient_wq,
+					   &scodec->jack_work,
+					   msecs_to_jiffies(610));
+		} else {
+			type = SND_JACK_HEADPHONE;
+			snd_soc_jack_report(&scodec->jack, 0, scodec->jack_type);
+			scodec->jack_status = SUN8I_JACK_STATUS_CONNECTED;
+		}
+	} else if (scodec->jack_status == SUN8I_JACK_STATUS_WAITING_HBIAS) {
+		/*
+		 * If we're waiting for HBIAS to stabilize, and we get plug-out
+		 * interrupt and nothing more for > 100ms, just cancel the
+		 * initialization.
+		 */
+		if (scodec->last_hmic_irq == SUN8I_HMIC_STS_JACK_OUT_IRQ_ST) {
+			scodec->jack_status = SUN8I_JACK_STATUS_DISCONNECTED;
+			sun8i_codec_set_hmic_bias(scodec, false);
+			goto out_unlock;
+		}
+
+		/*
+		 * If we're not done waiting for things to stabilize, wait more.
+		 */
+		if (!ktime_after(ktime_get(), scodec->jack_hbias_ready)) {
+			s64 msecs = ktime_ms_delta(scodec->jack_hbias_ready, ktime_get());
+
+			queue_delayed_work(system_power_efficient_wq,
+					   &scodec->jack_work,
+					   msecs_to_jiffies(msecs + 10));
+			goto out_unlock;
+		}
+
+		/*
+		 * Everything is stabilized, determine jack type and report it.
+		 */
+		regmap_read(scodec->regmap, SUN8I_HMIC_STS, &mdata);
+		mdata &= SUN8I_HMIC_STS_HMIC_DATA_MASK;
+		mdata >>= SUN8I_HMIC_STS_HMIC_DATA;
+
+		regmap_write(scodec->regmap, SUN8I_HMIC_STS, 0);
+
+		if (mdata < 0x10) {
+			type = SND_JACK_HEADPHONE;
+
+			sun8i_codec_set_hmic_bias(scodec, false);
+		} else {
+			type = SND_JACK_HEADSET;
+
+			/* Set MDATA threshold for triggering DATA interrupts
+			 * slightly bellow the initial value read at connection
+			 * time. This assumes that user is not pressing a button
+			 * when connecting the jack cable. It's an unlinkely
+			 * situation, unless the person has 3 hands or more.
+			 */
+			/*
+			pr_err("jack: new mdata threshold: %#x\n", mdata);
+			regmap_write(scodec->regmap, SUN8I_HMIC_CTRL2,
+					   0x0 << SUN8I_HMIC_CTRL2_HMIC_SAMPLE |
+					   (mdata - 3) << SUN8I_HMIC_CTRL2_HMIC_MDATA_THRESHOLD |
+					   0x0 << SUN8I_HMIC_CTRL2_HMIC_SF);
+			   */
+		}
+
+		snd_soc_jack_report(&scodec->jack, type, scodec->jack_type);
+		scodec->jack_status = SUN8I_JACK_STATUS_CONNECTED;
+
+		pr_err("jack: plug-in reported\n");
+	} else if (scodec->jack_status == SUN8I_JACK_STATUS_CONNECTED) {
+		if (scodec->last_hmic_irq == SUN8I_HMIC_STS_JACK_OUT_IRQ_ST) {
+			scodec->jack_status = SUN8I_JACK_STATUS_DISCONNECTED;
+			if (scodec->jack_type & SND_JACK_MICROPHONE)
+				sun8i_codec_set_hmic_bias(scodec, false);
+
+			snd_soc_jack_report(&scodec->jack, 0, scodec->jack_type);
+			pr_err("jack: plug-out reported\n");
+		}
 	}
 
-	snd_soc_jack_report(&scodec->jack, type, scodec->jack_type);
+out_unlock:
+	mutex_unlock(&scodec->jack_mutex);
 }
 
 static irqreturn_t sun8i_codec_jack_irq(int irq, void *dev_id)
@@ -1551,40 +1634,67 @@ static irqreturn_t sun8i_codec_jack_irq(int irq, void *dev_id)
 	struct sun8i_codec *scodec = dev_id;
 	unsigned int status;
 
+	mutex_lock(&scodec->jack_mutex);
+
 	regmap_read(scodec->regmap, SUN8I_HMIC_STS, &status);
+	regmap_write(scodec->regmap, SUN8I_HMIC_STS, status);
 
 	if (status & BIT(SUN8I_HMIC_STS_JACK_OUT_IRQ_ST)) {
-		if (scodec->jack_type & SND_JACK_MICROPHONE) {
-			sun8i_codec_set_hmic_bias(scodec, false);
-			scodec->jack_pending = false;
-		}
+		pr_err("jack: irq plug-out\n");
 
-		dev_dbg(scodec->card->dev, "jack out\n");
-		snd_soc_jack_report(&scodec->jack, 0, scodec->jack_type);
+		scodec->last_hmic_irq = SUN8I_HMIC_STS_JACK_OUT_IRQ_ST;
+		queue_delayed_work(system_power_efficient_wq,
+				   &scodec->jack_work,
+				   msecs_to_jiffies(100));
 	} else if (status & BIT(SUN8I_HMIC_STS_JACK_IN_IRQ_ST)) {
-		int type = SND_JACK_HEADPHONE;
+		pr_err("jack: irq plug-in\n");
 
-		if (scodec->jack_type & SND_JACK_MICROPHONE) {
-			sun8i_codec_set_hmic_bias(scodec, true);
-			scodec->jack_pending = true;
-			queue_delayed_work(system_power_efficient_wq,
-					   &scodec->jack_work,
-					   msecs_to_jiffies(600));
-		}
-
-		dev_dbg(scodec->card->dev, "jack in\n");
-		snd_soc_jack_report(&scodec->jack, type, type);
+		scodec->last_hmic_irq = SUN8I_HMIC_STS_JACK_IN_IRQ_ST;
+		queue_delayed_work(system_power_efficient_wq,
+				   &scodec->jack_work,
+				   msecs_to_jiffies(100));
 	} else if (status & BIT(SUN8I_HMIC_STS_HMIC_DATA_IRQ_ST)) {
-		int type = SND_JACK_HEADSET | sun8i_codec_read_hmic_button(scodec);
+		if (scodec->jack_status == SUN8I_JACK_STATUS_CONNECTED) {
+			unsigned int value;
+			int type = SND_JACK_HEADSET;
+			int btn_chg = 0;
+
+			regmap_read(scodec->regmap, SUN8I_HMIC_STS, &value);
+			value  &= SUN8I_HMIC_STS_HMIC_DATA_MASK;
+			value >>= SUN8I_HMIC_STS_HMIC_DATA;
+
+			if (value < 0x2)
+				type |= SND_JACK_BTN_0;
+			else if (value < 0x7)
+				type |= SND_JACK_BTN_1;
+			else if (value < 0x10)
+				type |= SND_JACK_BTN_2;
+
+			if (scodec->jack_last_sample >= 0 && scodec->jack_last_sample == value) {
+				btn_chg = (scodec->jack_last_btn ^ type) & 0x7000;
+				scodec->jack_last_btn = type;
+
+				//XXX: temporary for debugging
+				if (btn_chg) {
+					if (btn_chg & SND_JACK_BTN_0)
+						pr_err("jack: key_%spress BTN_0 (%#x)\n", type & SND_JACK_BTN_0 ? "" : "de", value);
+					if (btn_chg & SND_JACK_BTN_1)
+						pr_err("jack: key_%spress BTN_1 (%#x)\n", type & SND_JACK_BTN_1 ? "" : "de", value);
+					if (btn_chg & SND_JACK_BTN_2)
+						pr_err("jack: key_%spress BTN_2 (%#x)\n", type & SND_JACK_BTN_2 ? "" : "de", value);
+				}
+
+				snd_soc_jack_report(&scodec->jack, type, scodec->jack_type);
+			}
 
-		scodec->jack_pending = false;
-		snd_soc_jack_report(&scodec->jack, type, scodec->jack_type);
+			scodec->jack_last_sample = value;
+		}
 	} else {
+		mutex_unlock(&scodec->jack_mutex);
 		return IRQ_NONE;
 	}
 
-	regmap_write(scodec->regmap, SUN8I_HMIC_STS, status);
-
+	mutex_unlock(&scodec->jack_mutex);
 	return IRQ_HANDLED;
 }
 
@@ -1799,6 +1909,7 @@ static int sun8i_codec_probe(struct platform_device *pdev)
 			return ret;
 
 		INIT_DELAYED_WORK(&scodec->jack_work, sun8i_codec_jack_work);
+		mutex_init(&scodec->jack_mutex);
 	}
 
 	regcache_cache_only(scodec->regmap, true);
-- 
Armbian

