summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Brown <broonie@kernel.org>2024-05-29 16:32:22 +0100
committerMark Brown <broonie@kernel.org>2024-05-29 16:32:22 +0100
commit52100401c17b8827c185c8b44fe473a3d9421836 (patch)
treefd59cfe11c54e4c16d3273e72954b1f46211044b
parente80613d6a6d528a265411556b0a84596fdc39959 (diff)
parent255009d22c185623140de600a5fb54f0fd541bb8 (diff)
ASoC: samsung: midas-audio: Add GPIO-based headset
Merge series from Artur Weber <aweber.kernel@gmail.com>: Many of Samsung's Exynos 4 devices share the same midas-audio driver to handle the codec setup. While most of these devices, including the Midas itself, use the jack detection provided by the WM8994 driver, other devices such as the Samsung Galaxy Tab 3 8.0 (lt01) use two GPIOs and an ADC channel to determine jack insertion, the jack's type, and button presses (for headsets with volume up/down/play buttons). In the downstream kernel, this behavior is implemented in the sec-jack driver[1], and the per-device settings are configured in *-jack.c files in the mach folder (see e.g. the Tab 3's implementation[2]). This patchset implements this mechanism in the midas_wm1811.c driver, and adds new DTS options to allow for its configuration. It also enables jack detection for the Samsung Galaxy Tab 3 8.0. A very similar mechanism was already present in the aries_wm8994.c driver[3]; this implementation heavily borrows from it, though there are a few extra cleanups as well.
-rw-r--r--Documentation/devicetree/bindings/sound/samsung,midas-audio.yaml33
-rw-r--r--sound/soc/samsung/Kconfig2
-rw-r--r--sound/soc/samsung/midas_wm1811.c348
3 files changed, 316 insertions, 67 deletions
diff --git a/Documentation/devicetree/bindings/sound/samsung,midas-audio.yaml b/Documentation/devicetree/bindings/sound/samsung,midas-audio.yaml
index 6ec80f529d84..69ddfd4afdcd 100644
--- a/Documentation/devicetree/bindings/sound/samsung,midas-audio.yaml
+++ b/Documentation/devicetree/bindings/sound/samsung,midas-audio.yaml
@@ -53,6 +53,9 @@ properties:
submic-bias-supply:
description: Supply for the micbias on the Sub microphone
+ headset-mic-bias-supply:
+ description: Supply for the micbias on the Headset microphone
+
fm-sel-gpios:
maxItems: 1
description: GPIO pin for FM selection
@@ -61,6 +64,36 @@ properties:
maxItems: 1
description: GPIO pin for line out selection
+ headset-detect-gpios:
+ maxItems: 1
+ description: GPIO for detection of headset insertion
+
+ headset-key-gpios:
+ maxItems: 1
+ description: GPIO for detection of headset key press
+
+ io-channels:
+ maxItems: 1
+ description: IO channel to read micbias voltage for headset detection
+
+ io-channel-names:
+ const: headset-detect
+
+ samsung,headset-4pole-threshold-microvolt:
+ minItems: 2
+ maxItems: 2
+ description:
+ Array containing minimum and maximum IO channel value for 4-pole
+ (with microphone/button) headsets. If the IO channel value is
+ outside of this range, a 3-pole headset is assumed.
+
+ samsung,headset-button-threshold-microvolt:
+ minItems: 3
+ maxItems: 3
+ description: |
+ Array of minimum (inclusive) IO channel values for headset button
+ detection, in order: "Media", "Volume Up" and "Volume Down".
+
required:
- compatible
- cpu
diff --git a/sound/soc/samsung/Kconfig b/sound/soc/samsung/Kconfig
index 93c2b1b08d0a..4b1ea7b2c796 100644
--- a/sound/soc/samsung/Kconfig
+++ b/sound/soc/samsung/Kconfig
@@ -140,7 +140,7 @@ config SND_SOC_SAMSUNG_ARIES_WM8994
config SND_SOC_SAMSUNG_MIDAS_WM1811
tristate "SoC I2S Audio support for Midas boards"
- depends on SND_SOC_SAMSUNG
+ depends on SND_SOC_SAMSUNG && IIO
select SND_SAMSUNG_I2S
select SND_SOC_WM8994
help
diff --git a/sound/soc/samsung/midas_wm1811.c b/sound/soc/samsung/midas_wm1811.c
index 0841e2e6f8ce..3a269c7de169 100644
--- a/sound/soc/samsung/midas_wm1811.c
+++ b/sound/soc/samsung/midas_wm1811.c
@@ -7,10 +7,11 @@
#include <linux/clk.h>
#include <linux/gpio/consumer.h>
+#include <linux/iio/consumer.h>
#include <linux/mfd/wm8994/registers.h>
+#include <linux/input-event-codes.h>
#include <linux/module.h>
#include <linux/of.h>
-#include <linux/regulator/consumer.h>
#include <sound/jack.h>
#include <sound/soc.h>
#include <sound/soc-dapm.h>
@@ -27,10 +28,11 @@
#define DEFAULT_FLL1_RATE 11289600U
struct midas_priv {
- struct regulator *reg_mic_bias;
- struct regulator *reg_submic_bias;
struct gpio_desc *gpio_fm_sel;
struct gpio_desc *gpio_lineout_sel;
+ struct gpio_desc *gpio_headset_detect;
+ struct gpio_desc *gpio_headset_key;
+ struct iio_channel *adc_headset_detect;
unsigned int fll1_rate;
struct snd_soc_jack headset_jack;
@@ -47,6 +49,117 @@ static struct snd_soc_jack_pin headset_jack_pins[] = {
},
};
+/*
+ * min_mv/max_mv values in this struct are set up based on DT values.
+ */
+static struct snd_soc_jack_zone headset_jack_zones[] = {
+ { .jack_type = SND_JACK_HEADPHONE, },
+ { .jack_type = SND_JACK_HEADSET, },
+ { .jack_type = SND_JACK_HEADPHONE, },
+};
+
+/*
+ * This is used for manual detection in headset_key_check, we reuse the
+ * structure since it's convenient.
+ *
+ * min_mv/max_mv values in this struct are set up based on DT values.
+ */
+static struct snd_soc_jack_zone headset_key_zones[] = {
+ { .jack_type = SND_JACK_BTN_0, }, /* Media */
+ { .jack_type = SND_JACK_BTN_1, }, /* Volume Up */
+ { .jack_type = SND_JACK_BTN_2, }, /* Volume Down */
+};
+
+static int headset_jack_check(void *data)
+{
+ struct snd_soc_component *codec = data;
+ struct snd_soc_dapm_context *dapm = snd_soc_component_get_dapm(codec);
+ struct midas_priv *priv = snd_soc_card_get_drvdata(codec->card);
+ int adc, ret;
+ int jack_type = 0;
+
+ if (!gpiod_get_value_cansleep(priv->gpio_headset_detect))
+ return 0;
+
+ /* Enable headset mic bias regulator so that the ADC reading works */
+ ret = snd_soc_dapm_force_enable_pin(dapm, "headset-mic-bias");
+ if (ret < 0) {
+ pr_err("%s: Failed to enable headset mic bias regulator (%d), assuming headphones\n",
+ __func__, ret);
+ return SND_JACK_HEADPHONE;
+ }
+ snd_soc_dapm_sync(dapm);
+
+ /* Sleep for a small amount of time to get the value to stabilize */
+ msleep(20);
+
+ ret = iio_read_channel_processed(priv->adc_headset_detect, &adc);
+ if (ret) {
+ pr_err("%s: Failed to read ADC (%d), assuming headphones\n",
+ __func__, ret);
+ jack_type = SND_JACK_HEADPHONE;
+ goto out;
+ }
+ pr_debug("%s: ADC value is %d\n", __func__, adc);
+
+ jack_type = snd_soc_jack_get_type(&priv->headset_jack, adc);
+
+out:
+ ret = snd_soc_dapm_disable_pin(dapm, "headset-mic-bias");
+ if (ret < 0)
+ pr_err("%s: Failed to disable headset mic bias regulator (%d)\n",
+ __func__, ret);
+ snd_soc_dapm_sync(dapm);
+
+ return jack_type;
+}
+
+static int headset_key_check(void *data)
+{
+ struct snd_soc_component *codec = data;
+ struct midas_priv *priv = snd_soc_card_get_drvdata(codec->card);
+ int adc, i, ret;
+
+ if (!gpiod_get_value_cansleep(priv->gpio_headset_key))
+ return 0;
+
+ /* Filter out keypresses when 4 pole jack not detected */
+ if (!(priv->headset_jack.status & SND_JACK_MICROPHONE))
+ return 0;
+
+ ret = iio_read_channel_processed(priv->adc_headset_detect, &adc);
+ if (ret) {
+ pr_err("%s: Failed to read ADC (%d), can't detect key type\n",
+ __func__, ret);
+ return 0;
+ }
+ pr_debug("%s: ADC value is %d\n", __func__, adc);
+
+ for (i = 0; i < ARRAY_SIZE(headset_key_zones); i++) {
+ if (adc >= headset_key_zones[i].min_mv &&
+ adc <= headset_key_zones[i].max_mv) {
+ return headset_key_zones[i].jack_type;
+ }
+ }
+
+ return 0;
+}
+
+static struct snd_soc_jack_gpio headset_gpio[] = {
+ {
+ .name = "Headset Jack",
+ .report = SND_JACK_HEADSET,
+ .debounce_time = 150,
+ .jack_status_check = headset_jack_check,
+ },
+ {
+ .name = "Headset Key",
+ .report = SND_JACK_BTN_0 | SND_JACK_BTN_1 | SND_JACK_BTN_2,
+ .debounce_time = 30,
+ .jack_status_check = headset_key_check,
+ },
+};
+
static int midas_start_fll1(struct snd_soc_pcm_runtime *rtd, unsigned int rate)
{
struct snd_soc_card *card = rtd->card;
@@ -169,38 +282,6 @@ static int midas_ext_spkmode(struct snd_soc_dapm_widget *w,
return ret;
}
-static int midas_mic_bias(struct snd_soc_dapm_widget *w,
- struct snd_kcontrol *kcontrol, int event)
-{
- struct snd_soc_card *card = w->dapm->card;
- struct midas_priv *priv = snd_soc_card_get_drvdata(card);
-
- switch (event) {
- case SND_SOC_DAPM_PRE_PMU:
- return regulator_enable(priv->reg_mic_bias);
- case SND_SOC_DAPM_POST_PMD:
- return regulator_disable(priv->reg_mic_bias);
- }
-
- return 0;
-}
-
-static int midas_submic_bias(struct snd_soc_dapm_widget *w,
- struct snd_kcontrol *kcontrol, int event)
-{
- struct snd_soc_card *card = w->dapm->card;
- struct midas_priv *priv = snd_soc_card_get_drvdata(card);
-
- switch (event) {
- case SND_SOC_DAPM_PRE_PMU:
- return regulator_enable(priv->reg_submic_bias);
- case SND_SOC_DAPM_POST_PMD:
- return regulator_disable(priv->reg_submic_bias);
- }
-
- return 0;
-}
-
static int midas_fm_set(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *kcontrol, int event)
{
@@ -272,8 +353,19 @@ static const struct snd_soc_dapm_widget midas_dapm_widgets[] = {
SND_SOC_DAPM_HP("Headphone", NULL),
SND_SOC_DAPM_MIC("Headset Mic", NULL),
- SND_SOC_DAPM_MIC("Main Mic", midas_mic_bias),
- SND_SOC_DAPM_MIC("Sub Mic", midas_submic_bias),
+ SND_SOC_DAPM_REGULATOR_SUPPLY("headset-mic-bias", 0, 0),
+ SND_SOC_DAPM_MIC("Main Mic", NULL),
+ SND_SOC_DAPM_REGULATOR_SUPPLY("mic-bias", 0, 0),
+ SND_SOC_DAPM_MIC("Sub Mic", NULL),
+ SND_SOC_DAPM_REGULATOR_SUPPLY("submic-bias", 0, 0),
+};
+
+/* Default routing; supplemented by audio-routing DT property */
+static const struct snd_soc_dapm_route midas_dapm_routes[] = {
+ /* Bind microphones with their respective regulator supplies */
+ {"Main Mic", NULL, "mic-bias"},
+ {"Sub Mic", NULL, "submic-bias"},
+ {"Headset Mic", NULL, "headset-mic-bias"},
};
static int midas_set_bias_level(struct snd_soc_card *card,
@@ -315,18 +407,67 @@ static int midas_late_probe(struct snd_soc_card *card)
return ret;
}
- ret = snd_soc_card_jack_new_pins(card, "Headset",
- SND_JACK_HEADSET | SND_JACK_MECHANICAL |
- SND_JACK_BTN_0 | SND_JACK_BTN_1 | SND_JACK_BTN_2 |
- SND_JACK_BTN_3 | SND_JACK_BTN_4 | SND_JACK_BTN_5,
- &priv->headset_jack,
- headset_jack_pins,
- ARRAY_SIZE(headset_jack_pins));
- if (ret)
+ if (!priv->gpio_headset_detect) {
+ ret = snd_soc_card_jack_new_pins(card, "Headset",
+ SND_JACK_HEADSET | SND_JACK_MECHANICAL |
+ SND_JACK_BTN_0 | SND_JACK_BTN_1 |
+ SND_JACK_BTN_2 | SND_JACK_BTN_3 |
+ SND_JACK_BTN_4 | SND_JACK_BTN_5,
+ &priv->headset_jack,
+ headset_jack_pins,
+ ARRAY_SIZE(headset_jack_pins));
+ if (ret)
+ return ret;
+
+ wm8958_mic_detect(aif1_dai->component, &priv->headset_jack,
+ NULL, NULL, NULL, NULL);
+ } else {
+ /* Some devices (n8000, t310) use a GPIO to detect the jack. */
+ ret = snd_soc_card_jack_new_pins(card, "Headset",
+ SND_JACK_HEADSET | SND_JACK_BTN_0 |
+ SND_JACK_BTN_1 | SND_JACK_BTN_2,
+ &priv->headset_jack,
+ headset_jack_pins,
+ ARRAY_SIZE(headset_jack_pins));
+ if (ret) {
+ dev_err(card->dev,
+ "Failed to set up headset pins: %d\n", ret);
+ return ret;
+ }
+
+ ret = snd_soc_jack_add_zones(&priv->headset_jack,
+ ARRAY_SIZE(headset_jack_zones),
+ headset_jack_zones);
+ if (ret) {
+ dev_err(card->dev,
+ "Failed to set up headset zones: %d\n", ret);
+ return ret;
+ }
+
+ headset_gpio[0].data = aif1_dai->component;
+ headset_gpio[0].desc = priv->gpio_headset_detect;
+
+ headset_gpio[1].data = aif1_dai->component;
+ headset_gpio[1].desc = priv->gpio_headset_key;
+
+ snd_jack_set_key(priv->headset_jack.jack,
+ SND_JACK_BTN_0, KEY_MEDIA);
+ snd_jack_set_key(priv->headset_jack.jack,
+ SND_JACK_BTN_1, KEY_VOLUMEUP);
+ snd_jack_set_key(priv->headset_jack.jack,
+ SND_JACK_BTN_2, KEY_VOLUMEDOWN);
+
+ ret = snd_soc_jack_add_gpios(&priv->headset_jack,
+ ARRAY_SIZE(headset_gpio),
+ headset_gpio);
+ if (ret)
+ dev_err(card->dev,
+ "Failed to set up headset jack GPIOs: %d\n",
+ ret);
+
return ret;
+ }
- wm8958_mic_detect(aif1_dai->component, &priv->headset_jack,
- NULL, NULL, NULL, NULL);
return 0;
}
@@ -421,6 +562,8 @@ static struct snd_soc_card midas_card = {
.num_controls = ARRAY_SIZE(midas_controls),
.dapm_widgets = midas_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(midas_dapm_widgets),
+ .dapm_routes = midas_dapm_routes,
+ .num_dapm_routes = ARRAY_SIZE(midas_dapm_routes),
.set_bias_level = midas_set_bias_level,
.late_probe = midas_late_probe,
@@ -433,6 +576,9 @@ static int midas_probe(struct platform_device *pdev)
struct snd_soc_card *card = &midas_card;
struct device *dev = &pdev->dev;
static struct snd_soc_dai_link *dai_link;
+ enum iio_chan_type channel_type;
+ u32 fourpole_threshold[2];
+ u32 button_threshold[3];
struct midas_priv *priv;
int ret, i;
@@ -443,29 +589,99 @@ static int midas_probe(struct platform_device *pdev)
snd_soc_card_set_drvdata(card, priv);
card->dev = dev;
- priv->reg_mic_bias = devm_regulator_get(dev, "mic-bias");
- if (IS_ERR(priv->reg_mic_bias)) {
- dev_err(dev, "Failed to get mic bias regulator\n");
- return PTR_ERR(priv->reg_mic_bias);
- }
-
- priv->reg_submic_bias = devm_regulator_get(dev, "submic-bias");
- if (IS_ERR(priv->reg_submic_bias)) {
- dev_err(dev, "Failed to get submic bias regulator\n");
- return PTR_ERR(priv->reg_submic_bias);
- }
-
priv->gpio_fm_sel = devm_gpiod_get_optional(dev, "fm-sel", GPIOD_OUT_HIGH);
- if (IS_ERR(priv->gpio_fm_sel)) {
- dev_err(dev, "Failed to get FM selection GPIO\n");
- return PTR_ERR(priv->gpio_fm_sel);
- }
+ if (IS_ERR(priv->gpio_fm_sel))
+ return dev_err_probe(dev, PTR_ERR(priv->gpio_fm_sel),
+ "Failed to get FM selection GPIO\n");
priv->gpio_lineout_sel = devm_gpiod_get_optional(dev, "lineout-sel",
GPIOD_OUT_HIGH);
- if (IS_ERR(priv->gpio_lineout_sel)) {
- dev_err(dev, "Failed to get line out selection GPIO\n");
- return PTR_ERR(priv->gpio_lineout_sel);
+ if (IS_ERR(priv->gpio_lineout_sel))
+ return dev_err_probe(dev, PTR_ERR(priv->gpio_lineout_sel),
+ "Failed to get line out selection GPIO\n");
+
+ priv->gpio_headset_detect = devm_gpiod_get_optional(dev,
+ "headset-detect", GPIOD_IN);
+ if (IS_ERR(priv->gpio_headset_detect))
+ return dev_err_probe(dev, PTR_ERR(priv->gpio_headset_detect),
+ "Failed to get headset jack detect GPIO\n");
+
+ if (priv->gpio_headset_detect) {
+ priv->adc_headset_detect = devm_iio_channel_get(dev,
+ "headset-detect");
+ if (IS_ERR(priv->adc_headset_detect))
+ return dev_err_probe(dev,
+ PTR_ERR(priv->adc_headset_detect),
+ "Failed to get ADC channel\n");
+
+ ret = iio_get_channel_type(priv->adc_headset_detect,
+ &channel_type);
+ if (ret) {
+ dev_err(dev, "Failed to get ADC channel type\n");
+ return ret;
+ }
+
+ if (channel_type != IIO_VOLTAGE) {
+ dev_err(dev, "ADC channel is not voltage\n");
+ return ret;
+ }
+
+ priv->gpio_headset_key = devm_gpiod_get(dev, "headset-key",
+ GPIOD_IN);
+ if (IS_ERR(priv->gpio_headset_key))
+ return dev_err_probe(dev,
+ PTR_ERR(priv->gpio_headset_key),
+ "Failed to get headset key GPIO\n");
+
+ ret = of_property_read_u32_array(dev->of_node,
+ "samsung,headset-4pole-threshold-microvolt",
+ fourpole_threshold,
+ ARRAY_SIZE(fourpole_threshold));
+ if (ret) {
+ dev_err(dev, "Failed to get 4-pole jack detection threshold\n");
+ return ret;
+ }
+
+ if (fourpole_threshold[0] > fourpole_threshold[1]) {
+ dev_err(dev, "Invalid 4-pole jack detection threshold value\n");
+ return -EINVAL;
+ }
+
+ headset_jack_zones[0].max_mv = (fourpole_threshold[0]);
+ headset_jack_zones[1].min_mv = (fourpole_threshold[0] + 1);
+
+ headset_jack_zones[1].max_mv = (fourpole_threshold[1]);
+ headset_jack_zones[2].min_mv = (fourpole_threshold[1] + 1);
+
+ ret = of_property_read_u32_array(dev->of_node,
+ "samsung,headset-button-threshold-microvolt",
+ button_threshold,
+ ARRAY_SIZE(button_threshold));
+ if (ret) {
+ dev_err(dev, "Failed to get headset button detection threshold\n");
+ return ret;
+ }
+
+ if (button_threshold[0] > button_threshold[1] ||
+ button_threshold[1] > button_threshold[2]) {
+ dev_err(dev, "Invalid headset button detection threshold value\n");
+ return -EINVAL;
+ }
+
+ for (i = 0; i < 3; i++) {
+ if (i != 0 && button_threshold[i] <= 0) {
+ dev_err(dev, "Invalid headset button detection threshold value\n");
+ return -EINVAL;
+ }
+
+ headset_key_zones[i].min_mv = button_threshold[i];
+
+ if (i == 2)
+ headset_key_zones[i].max_mv = UINT_MAX;
+ else
+ headset_key_zones[i].max_mv = \
+ (button_threshold[i+1] - 1);
+ }
}
ret = snd_soc_of_parse_card_name(card, "model");