linux_dsm_epyc7002/sound/soc/samsung/aries_wm8994.c
Jonathan Bakker 7a3a7671fa
ASoC: samsung: Add driver for Aries boards
Samsung Aries boards have a WM8994 codec connected to the Samsung
I2S controller, the BT codec, and the cellular modem.  Jack detection
is done by a combination of an ADC, GPIOs, and an extcon device for
the USB dock.  There is also a GPIO for selection between the Mic
path and the TV out path on the headphone jack.

There are two main variants, one with an FM radio and where the modem
is the master and one without a radio and the modem is the slave.

Signed-off-by: Jonathan Bakker <xc-racer2@live.ca>
Link: https://lore.kernel.org/r/BN6PR04MB06608CBF03EF27B70B175978A39F0@BN6PR04MB0660.namprd04.prod.outlook.com
Signed-off-by: Mark Brown <broonie@kernel.org>
2020-06-15 20:58:30 +01:00

696 lines
17 KiB
C

// SPDX-License-Identifier: GPL-2.0+
#include <linux/extcon.h>
#include <linux/iio/consumer.h>
#include <linux/iio/iio.h>
#include <linux/input-event-codes.h>
#include <linux/mfd/wm8994/registers.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_gpio.h>
#include <linux/regulator/consumer.h>
#include <sound/jack.h>
#include <sound/pcm_params.h>
#include <sound/soc.h>
#include "i2s.h"
#include "../codecs/wm8994.h"
#define ARIES_MCLK1_FREQ 24000000
struct aries_wm8994_variant {
unsigned int modem_dai_fmt;
bool has_fm_radio;
};
struct aries_wm8994_data {
struct extcon_dev *usb_extcon;
struct regulator *reg_main_micbias;
struct regulator *reg_headset_micbias;
struct gpio_desc *gpio_headset_detect;
struct gpio_desc *gpio_headset_key;
struct gpio_desc *gpio_earpath_sel;
struct iio_channel *adc;
const struct aries_wm8994_variant *variant;
};
/* USB dock */
static struct snd_soc_jack aries_dock;
static struct snd_soc_jack_pin dock_pins[] = {
{
.pin = "LINE",
.mask = SND_JACK_LINEOUT,
},
};
static int aries_extcon_notifier(struct notifier_block *this,
unsigned long connected, void *_cmd)
{
if (connected)
snd_soc_jack_report(&aries_dock, SND_JACK_LINEOUT,
SND_JACK_LINEOUT);
else
snd_soc_jack_report(&aries_dock, 0, SND_JACK_LINEOUT);
return NOTIFY_DONE;
}
static struct notifier_block aries_extcon_notifier_block = {
.notifier_call = aries_extcon_notifier,
};
/* Headset jack */
static struct snd_soc_jack aries_headset;
static struct snd_soc_jack_pin jack_pins[] = {
{
.pin = "HP",
.mask = SND_JACK_HEADPHONE,
}, {
.pin = "Headset Mic",
.mask = SND_JACK_MICROPHONE,
},
};
static struct snd_soc_jack_zone headset_zones[] = {
{
.min_mv = 0,
.max_mv = 241,
.jack_type = SND_JACK_HEADPHONE,
}, {
.min_mv = 242,
.max_mv = 2980,
.jack_type = SND_JACK_HEADSET,
}, {
.min_mv = 2981,
.max_mv = UINT_MAX,
.jack_type = SND_JACK_HEADPHONE,
},
};
static irqreturn_t headset_det_irq_thread(int irq, void *data)
{
struct aries_wm8994_data *priv = (struct aries_wm8994_data *) data;
int ret = 0;
int time_left_ms = 300;
int adc;
while (time_left_ms > 0) {
if (!gpiod_get_value(priv->gpio_headset_detect)) {
snd_soc_jack_report(&aries_headset, 0,
SND_JACK_HEADSET);
gpiod_set_value(priv->gpio_earpath_sel, 0);
return IRQ_HANDLED;
}
msleep(20);
time_left_ms -= 20;
}
/* Temporarily enable micbias and earpath selector */
ret = regulator_enable(priv->reg_headset_micbias);
if (ret)
pr_err("%s failed to enable micbias: %d", __func__, ret);
gpiod_set_value(priv->gpio_earpath_sel, 1);
ret = iio_read_channel_processed(priv->adc, &adc);
if (ret < 0) {
/* failed to read ADC, so assume headphone */
pr_err("%s failed to read ADC, assuming headphones", __func__);
snd_soc_jack_report(&aries_headset, SND_JACK_HEADPHONE,
SND_JACK_HEADSET);
} else {
snd_soc_jack_report(&aries_headset,
snd_soc_jack_get_type(&aries_headset, adc),
SND_JACK_HEADSET);
}
ret = regulator_disable(priv->reg_headset_micbias);
if (ret)
pr_err("%s failed disable micbias: %d", __func__, ret);
/* Disable earpath selector when no mic connected */
if (!(aries_headset.status & SND_JACK_MICROPHONE))
gpiod_set_value(priv->gpio_earpath_sel, 0);
return IRQ_HANDLED;
}
static int headset_button_check(void *data)
{
struct aries_wm8994_data *priv = (struct aries_wm8994_data *) data;
/* Filter out keypresses when 4 pole jack not detected */
if (gpiod_get_value_cansleep(priv->gpio_headset_key) &&
aries_headset.status & SND_JACK_MICROPHONE)
return SND_JACK_BTN_0;
return 0;
}
static struct snd_soc_jack_gpio headset_button_gpio[] = {
{
.name = "Media Button",
.report = SND_JACK_BTN_0,
.debounce_time = 30,
.jack_status_check = headset_button_check,
},
};
static int aries_spk_cfg(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *kcontrol, int event)
{
struct snd_soc_card *card = w->dapm->card;
struct snd_soc_pcm_runtime *rtd;
struct snd_soc_component *component;
int ret = 0;
rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[0]);
component = asoc_rtd_to_codec(rtd, 0)->component;
/**
* We have an odd setup - the SPKMODE pin is pulled up so
* we only have access to the left side SPK configs,
* but SPKOUTR isn't bridged so when playing back in
* stereo, we only get the left hand channel. The only
* option we're left with is to force the AIF into mono
* mode.
*/
switch (event) {
case SND_SOC_DAPM_POST_PMU:
ret = snd_soc_component_update_bits(component,
WM8994_AIF1_DAC1_FILTERS_1,
WM8994_AIF1DAC1_MONO, WM8994_AIF1DAC1_MONO);
break;
case SND_SOC_DAPM_PRE_PMD:
ret = snd_soc_component_update_bits(component,
WM8994_AIF1_DAC1_FILTERS_1,
WM8994_AIF1DAC1_MONO, 0);
break;
}
return ret;
}
static int aries_main_bias(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *kcontrol, int event)
{
struct snd_soc_card *card = w->dapm->card;
struct aries_wm8994_data *priv = snd_soc_card_get_drvdata(card);
int ret = 0;
switch (event) {
case SND_SOC_DAPM_PRE_PMU:
ret = regulator_enable(priv->reg_main_micbias);
break;
case SND_SOC_DAPM_POST_PMD:
ret = regulator_disable(priv->reg_main_micbias);
break;
}
return ret;
}
static int aries_headset_bias(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *kcontrol, int event)
{
struct snd_soc_card *card = w->dapm->card;
struct aries_wm8994_data *priv = snd_soc_card_get_drvdata(card);
int ret = 0;
switch (event) {
case SND_SOC_DAPM_PRE_PMU:
ret = regulator_enable(priv->reg_headset_micbias);
break;
case SND_SOC_DAPM_POST_PMD:
ret = regulator_disable(priv->reg_headset_micbias);
break;
}
return ret;
}
static const struct snd_kcontrol_new aries_controls[] = {
SOC_DAPM_PIN_SWITCH("Modem In"),
SOC_DAPM_PIN_SWITCH("Modem Out"),
};
static const struct snd_soc_dapm_widget aries_dapm_widgets[] = {
SND_SOC_DAPM_HP("HP", NULL),
SND_SOC_DAPM_SPK("SPK", aries_spk_cfg),
SND_SOC_DAPM_SPK("RCV", NULL),
SND_SOC_DAPM_LINE("LINE", NULL),
SND_SOC_DAPM_MIC("Main Mic", aries_main_bias),
SND_SOC_DAPM_MIC("Headset Mic", aries_headset_bias),
SND_SOC_DAPM_MIC("Bluetooth Mic", NULL),
SND_SOC_DAPM_SPK("Bluetooth SPK", NULL),
SND_SOC_DAPM_LINE("Modem In", NULL),
SND_SOC_DAPM_LINE("Modem Out", NULL),
/* This must be last as it is conditionally not used */
SND_SOC_DAPM_LINE("FM In", NULL),
};
static int aries_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
struct snd_soc_pcm_runtime *rtd = substream->private_data;
struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, 0);
unsigned int pll_out;
int ret;
/* AIF1CLK should be >=3MHz for optimal performance */
if (params_width(params) == 24)
pll_out = params_rate(params) * 384;
else if (params_rate(params) == 8000 || params_rate(params) == 11025)
pll_out = params_rate(params) * 512;
else
pll_out = params_rate(params) * 256;
ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, WM8994_FLL_SRC_MCLK1,
ARIES_MCLK1_FREQ, pll_out);
if (ret < 0)
return ret;
ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1,
pll_out, SND_SOC_CLOCK_IN);
if (ret < 0)
return ret;
return 0;
}
static int aries_hw_free(struct snd_pcm_substream *substream)
{
struct snd_soc_pcm_runtime *rtd = substream->private_data;
struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, 0);
int ret;
/* Switch sysclk to MCLK1 */
ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_MCLK1,
ARIES_MCLK1_FREQ, SND_SOC_CLOCK_IN);
if (ret < 0)
return ret;
/* Stop PLL */
ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, WM8994_FLL_SRC_MCLK1,
ARIES_MCLK1_FREQ, 0);
if (ret < 0)
return ret;
return 0;
}
/*
* Main DAI operations
*/
static struct snd_soc_ops aries_ops = {
.hw_params = aries_hw_params,
.hw_free = aries_hw_free,
};
static int aries_baseband_init(struct snd_soc_pcm_runtime *rtd)
{
struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, 0);
unsigned int pll_out;
int ret;
pll_out = 8000 * 512;
/* Set the codec FLL */
ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL2, WM8994_FLL_SRC_MCLK1,
ARIES_MCLK1_FREQ, pll_out);
if (ret < 0)
return ret;
/* Set the codec system clock */
ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL2,
pll_out, SND_SOC_CLOCK_IN);
if (ret < 0)
return ret;
return 0;
}
static int aries_late_probe(struct snd_soc_card *card)
{
struct aries_wm8994_data *priv = snd_soc_card_get_drvdata(card);
int ret, irq;
ret = snd_soc_card_jack_new(card, "Dock", SND_JACK_LINEOUT,
&aries_dock, dock_pins, ARRAY_SIZE(dock_pins));
if (ret)
return ret;
ret = devm_extcon_register_notifier(card->dev,
priv->usb_extcon, EXTCON_JACK_LINE_OUT,
&aries_extcon_notifier_block);
if (ret)
return ret;
if (extcon_get_state(priv->usb_extcon,
EXTCON_JACK_LINE_OUT) > 0)
snd_soc_jack_report(&aries_dock, SND_JACK_LINEOUT,
SND_JACK_LINEOUT);
else
snd_soc_jack_report(&aries_dock, 0, SND_JACK_LINEOUT);
ret = snd_soc_card_jack_new(card, "Headset",
SND_JACK_HEADSET | SND_JACK_BTN_0,
&aries_headset,
jack_pins, ARRAY_SIZE(jack_pins));
if (ret)
return ret;
ret = snd_soc_jack_add_zones(&aries_headset, ARRAY_SIZE(headset_zones),
headset_zones);
if (ret)
return ret;
irq = gpiod_to_irq(priv->gpio_headset_detect);
if (irq < 0) {
dev_err(card->dev, "Failed to map headset detect gpio to irq");
return -EINVAL;
}
ret = devm_request_threaded_irq(card->dev, irq, NULL,
headset_det_irq_thread,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING |
IRQF_ONESHOT, "headset_detect", priv);
if (ret) {
dev_err(card->dev, "Failed to request headset detect irq");
return ret;
}
headset_button_gpio[0].data = priv;
headset_button_gpio[0].desc = priv->gpio_headset_key;
snd_jack_set_key(aries_headset.jack, SND_JACK_BTN_0, KEY_MEDIA);
return snd_soc_jack_add_gpios(&aries_headset,
ARRAY_SIZE(headset_button_gpio), headset_button_gpio);
}
static const struct snd_soc_pcm_stream baseband_params = {
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rate_min = 8000,
.rate_max = 8000,
.channels_min = 1,
.channels_max = 1,
};
static const struct snd_soc_pcm_stream bluetooth_params = {
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rate_min = 8000,
.rate_max = 8000,
.channels_min = 1,
.channels_max = 2,
};
static const struct snd_soc_dapm_widget aries_modem_widgets[] = {
SND_SOC_DAPM_INPUT("Modem RX"),
SND_SOC_DAPM_OUTPUT("Modem TX"),
};
static const struct snd_soc_dapm_route aries_modem_routes[] = {
{ "Modem Capture", NULL, "Modem RX" },
{ "Modem TX", NULL, "Modem Playback" },
};
static const struct snd_soc_component_driver aries_component = {
.name = "aries-audio",
.dapm_widgets = aries_modem_widgets,
.num_dapm_widgets = ARRAY_SIZE(aries_modem_widgets),
.dapm_routes = aries_modem_routes,
.num_dapm_routes = ARRAY_SIZE(aries_modem_routes),
.idle_bias_on = 1,
.use_pmdown_time = 1,
.endianness = 1,
.non_legacy_dai_naming = 1,
};
static struct snd_soc_dai_driver aries_ext_dai[] = {
{
.name = "Voice call",
.playback = {
.stream_name = "Modem Playback",
.channels_min = 1,
.channels_max = 1,
.rate_min = 8000,
.rate_max = 8000,
.rates = SNDRV_PCM_RATE_8000,
.formats = SNDRV_PCM_FMTBIT_S16_LE,
},
.capture = {
.stream_name = "Modem Capture",
.channels_min = 1,
.channels_max = 1,
.rate_min = 8000,
.rate_max = 8000,
.rates = SNDRV_PCM_RATE_8000,
.formats = SNDRV_PCM_FMTBIT_S16_LE,
},
},
};
SND_SOC_DAILINK_DEFS(aif1,
DAILINK_COMP_ARRAY(COMP_CPU(SAMSUNG_I2S_DAI)),
DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif1")),
DAILINK_COMP_ARRAY(COMP_EMPTY()));
SND_SOC_DAILINK_DEFS(baseband,
DAILINK_COMP_ARRAY(COMP_CPU("Voice call")),
DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif2")));
SND_SOC_DAILINK_DEFS(bluetooth,
DAILINK_COMP_ARRAY(COMP_CPU("bt-sco-pcm")),
DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif3")));
static struct snd_soc_dai_link aries_dai[] = {
{
.name = "WM8994 AIF1",
.stream_name = "HiFi",
.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBM_CFM,
.ops = &aries_ops,
SND_SOC_DAILINK_REG(aif1),
},
{
.name = "WM8994 AIF2",
.stream_name = "Baseband",
.init = &aries_baseband_init,
.params = &baseband_params,
.ignore_suspend = 1,
SND_SOC_DAILINK_REG(baseband),
},
{
.name = "WM8994 AIF3",
.stream_name = "Bluetooth",
.params = &bluetooth_params,
.ignore_suspend = 1,
SND_SOC_DAILINK_REG(bluetooth),
},
};
static struct snd_soc_card aries_card = {
.name = "ARIES",
.owner = THIS_MODULE,
.dai_link = aries_dai,
.num_links = ARRAY_SIZE(aries_dai),
.controls = aries_controls,
.num_controls = ARRAY_SIZE(aries_controls),
.dapm_widgets = aries_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(aries_dapm_widgets),
.late_probe = aries_late_probe,
};
static const struct aries_wm8994_variant fascinate4g_variant = {
.modem_dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_CBS_CFS
| SND_SOC_DAIFMT_IB_NF,
.has_fm_radio = false,
};
static const struct aries_wm8994_variant aries_variant = {
.modem_dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_CBM_CFM
| SND_SOC_DAIFMT_IB_NF,
.has_fm_radio = true,
};
static const struct of_device_id samsung_wm8994_of_match[] = {
{
.compatible = "samsung,fascinate4g-wm8994",
.data = &fascinate4g_variant,
},
{
.compatible = "samsung,aries-wm8994",
.data = &aries_variant,
},
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, samsung_wm8994_of_match);
static int aries_audio_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct device_node *cpu, *codec, *extcon_np;
struct device *dev = &pdev->dev;
struct snd_soc_card *card = &aries_card;
struct aries_wm8994_data *priv;
struct snd_soc_dai_link *dai_link;
const struct of_device_id *match;
int ret, i;
if (!np)
return -EINVAL;
card->dev = dev;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
snd_soc_card_set_drvdata(card, priv);
match = of_match_node(samsung_wm8994_of_match, np);
priv->variant = match->data;
/* Remove FM widget if not present */
if (!priv->variant->has_fm_radio)
card->num_dapm_widgets--;
priv->reg_main_micbias = devm_regulator_get(dev, "main-micbias");
if (IS_ERR(priv->reg_main_micbias)) {
dev_err(dev, "Failed to get main micbias regulator\n");
return PTR_ERR(priv->reg_main_micbias);
}
priv->reg_headset_micbias = devm_regulator_get(dev, "headset-micbias");
if (IS_ERR(priv->reg_headset_micbias)) {
dev_err(dev, "Failed to get headset micbias regulator\n");
return PTR_ERR(priv->reg_headset_micbias);
}
priv->gpio_earpath_sel = devm_gpiod_get(dev, "earpath-sel",
GPIOD_OUT_LOW);
if (IS_ERR(priv->gpio_earpath_sel)) {
dev_err(dev, "Failed to get earpath selector gpio");
return PTR_ERR(priv->gpio_earpath_sel);
}
extcon_np = of_parse_phandle(np, "extcon", 0);
priv->usb_extcon = extcon_find_edev_by_node(extcon_np);
if (IS_ERR(priv->usb_extcon)) {
if (PTR_ERR(priv->usb_extcon) != -EPROBE_DEFER)
dev_err(dev, "Failed to get extcon device");
return PTR_ERR(priv->usb_extcon);
}
of_node_put(extcon_np);
priv->adc = devm_iio_channel_get(dev, "headset-detect");
if (IS_ERR(priv->adc)) {
if (PTR_ERR(priv->adc) != -EPROBE_DEFER)
dev_err(dev, "Failed to get ADC channel");
return PTR_ERR(priv->adc);
}
if (priv->adc->channel->type != IIO_VOLTAGE)
return -EINVAL;
priv->gpio_headset_key = devm_gpiod_get(dev, "headset-key",
GPIOD_IN);
if (IS_ERR(priv->gpio_headset_key)) {
dev_err(dev, "Failed to get headset key gpio");
return PTR_ERR(priv->gpio_headset_key);
}
priv->gpio_headset_detect = devm_gpiod_get(dev,
"headset-detect", GPIOD_IN);
if (IS_ERR(priv->gpio_headset_detect)) {
dev_err(dev, "Failed to get headset detect gpio");
return PTR_ERR(priv->gpio_headset_detect);
}
/* Update card-name if provided through DT, else use default name */
snd_soc_of_parse_card_name(card, "model");
ret = snd_soc_of_parse_audio_routing(card, "samsung,audio-routing");
if (ret < 0) {
dev_err(dev, "Audio routing invalid/unspecified\n");
return ret;
}
aries_dai[1].dai_fmt = priv->variant->modem_dai_fmt;
cpu = of_get_child_by_name(dev->of_node, "cpu");
if (!cpu)
return -EINVAL;
codec = of_get_child_by_name(dev->of_node, "codec");
if (!codec)
return -EINVAL;
for_each_card_prelinks(card, i, dai_link) {
dai_link->codecs->of_node = of_parse_phandle(codec,
"sound-dai", 0);
if (!dai_link->codecs->of_node) {
ret = -EINVAL;
goto out;
}
}
/* Set CPU and platform of_node for main DAI */
aries_dai[0].cpus->of_node = of_parse_phandle(cpu,
"sound-dai", 0);
if (!aries_dai[0].cpus->of_node) {
ret = -EINVAL;
goto out;
}
aries_dai[0].platforms->of_node = aries_dai[0].cpus->of_node;
/* Set CPU of_node for BT DAI */
aries_dai[2].cpus->of_node = of_parse_phandle(cpu,
"sound-dai", 1);
if (!aries_dai[2].cpus->of_node) {
ret = -EINVAL;
goto out;
}
ret = devm_snd_soc_register_component(dev, &aries_component,
aries_ext_dai, ARRAY_SIZE(aries_ext_dai));
if (ret < 0) {
dev_err(dev, "Failed to register component: %d\n", ret);
goto out;
}
ret = devm_snd_soc_register_card(dev, card);
if (ret)
dev_err(dev, "snd_soc_register_card() failed:%d\n", ret);
out:
of_node_put(cpu);
of_node_put(codec);
return ret;
}
static struct platform_driver aries_audio_driver = {
.driver = {
.name = "aries-audio-wm8994",
.of_match_table = of_match_ptr(samsung_wm8994_of_match),
.pm = &snd_soc_pm_ops,
},
.probe = aries_audio_probe,
};
module_platform_driver(aries_audio_driver);
MODULE_DESCRIPTION("ALSA SoC ARIES WM8994");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:aries-audio-wm8994");