diff mbox

[v5,2/2] ASoC: samsung: Add machine driver for Exynos5433 based TM2 board

Message ID 1470752514-11409-2-git-send-email-s.nawrocki@samsung.com (mailing list archive)
State Not Applicable
Headers show

Commit Message

This patch adds the sound machine driver for TM2 and TM2E board.
Speaker and headphone playback, Main Mic capture, Bluetooth,
Voice call and external accessory are supported.

Signed-off-by: Inha Song <ideal.song@samsung.com>
[k.kozlowski: rebased on 4.1]
Signed-off-by: Krzysztof Kozlowski <k.kozlowski@samsung.com>
[s.nawrocki: rebased to 4.7, adjustment to the ASoC core changes,
 removed unused ops and direct calls to the max98504 function,
 added parsing of "audio-amplifier" and "audio-codec"
 properties, added TDM API calls, switched to gpiod API]
Signed-off-by: Sylwester Nawrocki <s.nawrocki@samsung.com>
---

Changes since v4 (addressing review comments from Charles):
 - changed the order of WM5110_FLL{1,2}, WM5110_FLL{1,2}_REFCLK setting,
 - ARIZONA_CLK_SYSCLK, ARIZONA_CLK_ASYNCCLK setting moved to late_probe,
 - added tm2_aif2_hw_free callback for disabling FLL2,
 - removed unneded card->dapm.bias_level assignment in tm2_mic_bias callback,
 - suspend_late, resume_early dev_pm_ops used instead of suspend_post,
   resume_pre struct snd_soc_card callbacks.

Changes since v3:
 - removed SND_SOC_SAMSUNG_AUDSS from Kconfig.

Changes since v2:
 - added missing Kconfig dependencies.

Changes since initial version:
 - added PDM Tx channels setup through TDM API
 - adaptation to renamed 'samsung,model', 'samsung,i2s-controller',
   'samsung,speaker-amplifier' properties,
 - removed some dev_dbg() calls,
 - cleaned up mic-bias GPIO handling and switched to gpiod API,
 - added parsing of 'audio-codec' property,
 - initialized codec_of_node of dai_link instead of codec_name,
 - switched to using clock, clock-names properties from the wm5110
   codec node,
 - fixed error paths in probe() (of_node reference counting).
---
 sound/soc/samsung/Kconfig      |   9 +
 sound/soc/samsung/Makefile     |   2 +
 sound/soc/samsung/tm2_wm5110.c | 604 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 615 insertions(+)
 create mode 100644 sound/soc/samsung/tm2_wm5110.c

--
1.9.1

--
To unsubscribe from this list: send the line "unsubscribe linux-samsung-soc" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Comments

Mark Brown Aug. 9, 2016, 4:02 p.m. UTC | #1
On Tue, Aug 09, 2016 at 04:21:54PM +0200, Sylwester Nawrocki wrote:

> +		card->dai_link[i].codec_of_node = codec_dai_node;
> +		card->dai_link[i].cpu_of_node = cpu_dai_node;
> +		card->dai_link[i].platform_of_node = cpu_dai_node;
> +	}
> +
> +	priv->codec_mclk1 = of_clk_get_by_name(codec_dai_node, "mclk1");
> +	if (IS_ERR(priv->codec_mclk1)) {

So, all this of_clk_get_by_name() stuff is messy and is going to need to
be duplicated for any cards using this CODEC which isn't excellent.  It
would be a lot nicer if we just did some regular clk_get() calls in the
MFD and then provided an interface for other drivers to get at those
clocks (or just had the CODEC driver DTRT internally).
Chanwoo Choi Aug. 19, 2016, 1:52 a.m. UTC | #2
Dear all,

I tested this patch with TM2 dt patches[1]
based on v4.8-rc2. The playback is well working.
[1] https://lkml.org/lkml/2016/8/16/61
: [PATCH 0/7] arm64: dts: Add the dts file for Exynos5433 and TM/TM2E board

Tested-by: Chanwoo Choi <cw00.choi@samsung.com>

Best Regards,
Chanwoo Choi

On 2016년 08월 09일 23:21, Sylwester Nawrocki wrote:
> This patch adds the sound machine driver for TM2 and TM2E board.
> Speaker and headphone playback, Main Mic capture, Bluetooth,
> Voice call and external accessory are supported.
> 
> Signed-off-by: Inha Song <ideal.song@samsung.com>
> [k.kozlowski: rebased on 4.1]
> Signed-off-by: Krzysztof Kozlowski <k.kozlowski@samsung.com>
> [s.nawrocki: rebased to 4.7, adjustment to the ASoC core changes,
>  removed unused ops and direct calls to the max98504 function,
>  added parsing of "audio-amplifier" and "audio-codec"
>  properties, added TDM API calls, switched to gpiod API]
> Signed-off-by: Sylwester Nawrocki <s.nawrocki@samsung.com>
> ---
> 
> Changes since v4 (addressing review comments from Charles):
>  - changed the order of WM5110_FLL{1,2}, WM5110_FLL{1,2}_REFCLK setting,
>  - ARIZONA_CLK_SYSCLK, ARIZONA_CLK_ASYNCCLK setting moved to late_probe,
>  - added tm2_aif2_hw_free callback for disabling FLL2,
>  - removed unneded card->dapm.bias_level assignment in tm2_mic_bias callback,
>  - suspend_late, resume_early dev_pm_ops used instead of suspend_post,
>    resume_pre struct snd_soc_card callbacks.
> 
> Changes since v3:
>  - removed SND_SOC_SAMSUNG_AUDSS from Kconfig.
> 
> Changes since v2:
>  - added missing Kconfig dependencies.
> 
> Changes since initial version:
>  - added PDM Tx channels setup through TDM API
>  - adaptation to renamed 'samsung,model', 'samsung,i2s-controller',
>    'samsung,speaker-amplifier' properties,
>  - removed some dev_dbg() calls,
>  - cleaned up mic-bias GPIO handling and switched to gpiod API,
>  - added parsing of 'audio-codec' property,
>  - initialized codec_of_node of dai_link instead of codec_name,
>  - switched to using clock, clock-names properties from the wm5110
>    codec node,
>  - fixed error paths in probe() (of_node reference counting).
> ---
>  sound/soc/samsung/Kconfig      |   9 +
>  sound/soc/samsung/Makefile     |   2 +
>  sound/soc/samsung/tm2_wm5110.c | 604 +++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 615 insertions(+)
>  create mode 100644 sound/soc/samsung/tm2_wm5110.c
> 
> diff --git a/sound/soc/samsung/Kconfig b/sound/soc/samsung/Kconfig
> index 7b722b0..1bed8a5 100644
> --- a/sound/soc/samsung/Kconfig
> +++ b/sound/soc/samsung/Kconfig
> @@ -229,3 +229,12 @@ config SND_SOC_ARNDALE_RT5631_ALC5631
>          depends on SND_SOC_SAMSUNG && I2C
>          select SND_SAMSUNG_I2S
>          select SND_SOC_RT5631
> +
> +config SND_SOC_SAMSUNG_TM2_WM5110
> +	tristate "SoC I2S Audio support for WM5110 on TM2 board"
> +	depends on SND_SOC_SAMSUNG && MFD_ARIZONA && I2C && SPI_MASTER
> +	select SND_SOC_MAX98504
> +	select SND_SOC_WM5110
> +	select SND_SAMSUNG_I2S
> +	help
> +	  Say Y if you want to add support for SoC audio on the TM2 board.
> diff --git a/sound/soc/samsung/Makefile b/sound/soc/samsung/Makefile
> index 5d03f5c..4444b9f 100644
> --- a/sound/soc/samsung/Makefile
> +++ b/sound/soc/samsung/Makefile
> @@ -44,6 +44,7 @@ snd-soc-lowland-objs := lowland.o
>  snd-soc-littlemill-objs := littlemill.o
>  snd-soc-bells-objs := bells.o
>  snd-soc-arndale-rt5631-objs := arndale_rt5631.o
> +snd-soc-tm2-wm5110-objs := tm2_wm5110.o
> 
>  obj-$(CONFIG_SND_SOC_SAMSUNG_JIVE_WM8750) += snd-soc-jive-wm8750.o
>  obj-$(CONFIG_SND_SOC_SAMSUNG_NEO1973_WM8753) += snd-soc-neo1973-wm8753.o
> @@ -69,3 +70,4 @@ obj-$(CONFIG_SND_SOC_LOWLAND) += snd-soc-lowland.o
>  obj-$(CONFIG_SND_SOC_LITTLEMILL) += snd-soc-littlemill.o
>  obj-$(CONFIG_SND_SOC_BELLS) += snd-soc-bells.o
>  obj-$(CONFIG_SND_SOC_ARNDALE_RT5631_ALC5631) += snd-soc-arndale-rt5631.o
> +obj-$(CONFIG_SND_SOC_SAMSUNG_TM2_WM5110) += snd-soc-tm2-wm5110.o
> diff --git a/sound/soc/samsung/tm2_wm5110.c b/sound/soc/samsung/tm2_wm5110.c
> new file mode 100644
> index 0000000..16c48fb
> --- /dev/null
> +++ b/sound/soc/samsung/tm2_wm5110.c
> @@ -0,0 +1,604 @@
> +/*
> + * Copyright (C) 2015 - 2016 Samsung Electronics Co., Ltd.
> + *
> + * Authors: Inha Song <ideal.song@samsung.com>
> + *          Sylwester Nawrocki <s.nawrocki@samsung.com>
> + *
> + * This program is free software; you can redistribute  it and/or modify it
> + * under  the terms of  the GNU General  Public License as published by the
> + * Free Software Foundation.
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/gpio.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <sound/pcm_params.h>
> +#include <sound/soc.h>
> +
> +#include "i2s.h"
> +#include "../codecs/wm5110.h"
> +
> +#define TM2_DAI_AIF1	0
> +#define TM2_DAI_AIF2	1
> +
> +struct tm2_machine_priv {
> +	struct snd_soc_codec *codec;
> +	struct clk *codec_mclk1;
> +	struct clk *codec_mclk2;
> +
> +	unsigned int sysclk_rate;
> +
> +	struct gpio_desc *gpio_mic_bias;
> +};
> +
> +static int tm2_start_sysclk(struct snd_soc_card *card)
> +{
> +	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
> +	struct snd_soc_codec *codec = priv->codec;
> +	unsigned long mclk_rate = clk_get_rate(priv->codec_mclk1);
> +	int ret;
> +
> +	ret = clk_prepare_enable(priv->codec_mclk1);
> +	if (ret < 0) {
> +		dev_err(card->dev, "Failed to enable mclk: %d\n", ret);
> +		return ret;
> +	}
> +
> +	ret = snd_soc_codec_set_pll(codec, WM5110_FLL1_REFCLK,
> +				    ARIZONA_FLL_SRC_MCLK1,
> +				    mclk_rate,
> +				    priv->sysclk_rate);
> +	if (ret < 0) {
> +		dev_err(codec->dev, "Failed to set FLL1 source: %d\n", ret);
> +		return ret;
> +	}
> +
> +	ret = snd_soc_codec_set_pll(codec, WM5110_FLL1,
> +				    ARIZONA_FLL_SRC_MCLK1,
> +				    mclk_rate,
> +				    priv->sysclk_rate);
> +	if (ret < 0) {
> +		dev_err(codec->dev, "Failed to start FLL1: %d\n", ret);
> +		return ret;
> +	}
> +
> +	ret = snd_soc_codec_set_sysclk(codec, ARIZONA_CLK_SYSCLK,
> +				       ARIZONA_CLK_SRC_FLL1,
> +				       priv->sysclk_rate,
> +				       SND_SOC_CLOCK_IN);
> +	if (ret < 0) {
> +		dev_err(codec->dev, "Failed to set SYSCLK source: %d\n", ret);
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +static int tm2_stop_sysclk(struct snd_soc_card *card)
> +{
> +	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
> +	struct snd_soc_codec *codec = priv->codec;
> +	int ret;
> +
> +	ret = snd_soc_codec_set_pll(codec, WM5110_FLL1, 0, 0, 0);
> +	if (ret < 0) {
> +		dev_err(codec->dev, "Failed to stop FLL1: %d\n", ret);
> +		return ret;
> +	}
> +
> +	ret = snd_soc_codec_set_sysclk(codec, ARIZONA_CLK_SYSCLK,
> +				       ARIZONA_CLK_SRC_FLL1, 0, 0);
> +	if (ret < 0) {
> +		dev_err(codec->dev, "Failed to stop SYSCLK: %d\n", ret);
> +		return ret;
> +	}
> +
> +	clk_disable_unprepare(priv->codec_mclk1);
> +
> +	return 0;
> +}
> +
> +static int tm2_aif1_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_codec *codec = rtd->codec;
> +	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(rtd->card);
> +
> +	switch (params_rate(params)) {
> +	case 4000:
> +	case 8000:
> +	case 12000:
> +	case 16000:
> +	case 24000:
> +	case 32000:
> +	case 48000:
> +	case 96000:
> +	case 192000:
> +		/* Highest possible SYSCLK frequency: 147.456MHz */
> +		priv->sysclk_rate = 147456000U;
> +		break;
> +	case 11025:
> +	case 22050:
> +	case 44100:
> +	case 88200:
> +	case 176400:
> +		/* Highest possible SYSCLK frequency: 135.4752 MHz */
> +		priv->sysclk_rate = 135475200U;
> +		break;
> +	default:
> +		dev_err(codec->dev, "Not supported sample rate: %d\n",
> +			params_rate(params));
> +		return -EINVAL;
> +	}
> +
> +	return tm2_start_sysclk(rtd->card);
> +}
> +
> +static struct snd_soc_ops tm2_aif1_ops = {
> +	.hw_params = tm2_aif1_hw_params,
> +};
> +
> +static int tm2_aif2_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_codec *codec = rtd->codec;
> +	struct tm2_machine_priv *priv =	snd_soc_card_get_drvdata(rtd->card);
> +	unsigned long mclk_rate = clk_get_rate(priv->codec_mclk1);
> +	unsigned int asyncclk_rate;
> +	int ret;
> +
> +	switch (params_rate(params)) {
> +	case 8000:
> +	case 12000:
> +	case 16000:
> +		/* Highest possible ASYNCCLK frequency: 49.152MHz */
> +		asyncclk_rate = 49152000U;
> +		break;
> +	case 11025:
> +		/* Highest possible ASYNCCLK frequency: 45.1584 MHz */
> +		asyncclk_rate = 45158400U;
> +		break;
> +	default:
> +		dev_err(codec->dev, "Not supported sample rate: %d\n",
> +			params_rate(params));
> +		return -EINVAL;
> +	}
> +
> +	ret = snd_soc_codec_set_pll(codec, WM5110_FLL2_REFCLK,
> +				    ARIZONA_FLL_SRC_MCLK1,
> +				    mclk_rate,
> +				    asyncclk_rate);
> +	if (ret < 0) {
> +		dev_err(codec->dev, "Failed to set FLL2 source: %d\n", ret);
> +		return ret;
> +	}
> +
> +	ret = snd_soc_codec_set_pll(codec, WM5110_FLL2,
> +				    ARIZONA_FLL_SRC_MCLK1,
> +				    mclk_rate,
> +				    asyncclk_rate);
> +	if (ret < 0) {
> +		dev_err(codec->dev, "Failed to start FLL2: %d\n", ret);
> +		return ret;
> +	}
> +
> +	ret = snd_soc_codec_set_sysclk(codec, ARIZONA_CLK_ASYNCCLK,
> +				       ARIZONA_CLK_SRC_FLL2,
> +				       asyncclk_rate,
> +				       SND_SOC_CLOCK_IN);
> +	if (ret < 0) {
> +		dev_err(codec->dev, "Failed to set ASYNCCLK source: %d\n", ret);
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +static int tm2_aif2_hw_free(struct snd_pcm_substream *substream)
> +{
> +	struct snd_soc_pcm_runtime *rtd = substream->private_data;
> +	struct snd_soc_codec *codec = rtd->codec;
> +	int ret;
> +
> +	/* disable FLL2 */
> +	ret = snd_soc_codec_set_pll(codec, WM5110_FLL2, ARIZONA_FLL_SRC_MCLK1,
> +				    0, 0);
> +	if (ret < 0)
> +		dev_err(codec->dev, "Failed to stop FLL2: %d\n", ret);
> +
> +	return ret;
> +}
> +
> +static struct snd_soc_ops tm2_aif2_ops = {
> +	.hw_params = tm2_aif2_hw_params,
> +	.hw_free = tm2_aif2_hw_free,
> +};
> +
> +static int tm2_mic_bias(struct snd_soc_dapm_widget *w,
> +				struct snd_kcontrol *kcontrol, int event)
> +{
> +	struct snd_soc_card *card = w->dapm->card;
> +	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
> +
> +	switch (event) {
> +	case SND_SOC_DAPM_PRE_PMU:
> +		gpiod_set_value_cansleep(priv->gpio_mic_bias,  1);
> +		break;
> +	case SND_SOC_DAPM_POST_PMD:
> +		gpiod_set_value_cansleep(priv->gpio_mic_bias,  0);
> +		break;
> +	}
> +
> +	return 0;
> +}
> +
> +static int tm2_set_bias_level(struct snd_soc_card *card,
> +				struct snd_soc_dapm_context *dapm,
> +				enum snd_soc_bias_level level)
> +{
> +	struct snd_soc_pcm_runtime *rtd;
> +
> +	rtd = snd_soc_get_pcm_runtime(card, card->dai_link[0].name);
> +
> +	if (dapm->dev != rtd->codec_dai->dev)
> +		return 0;
> +
> +	switch (level) {
> +	case SND_SOC_BIAS_STANDBY:
> +		if (card->dapm.bias_level == SND_SOC_BIAS_OFF)
> +			tm2_start_sysclk(card);
> +		break;
> +	case SND_SOC_BIAS_OFF:
> +		tm2_stop_sysclk(card);
> +		break;
> +	default:
> +		break;
> +	}
> +
> +	return 0;
> +}
> +
> +static struct snd_soc_aux_dev tm2_speaker_amp_dev;
> +
> +static int tm2_late_probe(struct snd_soc_card *card)
> +{
> +	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
> +	struct snd_soc_dai_link_component dlc = { 0 };
> +	unsigned int ch_map[] = { 0, 1 };
> +	struct snd_soc_dai *amp_pdm_dai;
> +	struct snd_soc_pcm_runtime *rtd;
> +	struct snd_soc_dai *aif1_dai;
> +	struct snd_soc_dai *aif2_dai;
> +	int ret;
> +
> +	rtd = snd_soc_get_pcm_runtime(card, card->dai_link[TM2_DAI_AIF1].name);
> +	aif1_dai = rtd->codec_dai;
> +	priv->codec = rtd->codec;
> +
> +	/* 32 kHz must be enabled for jack detection */
> +	if (!IS_ERR(priv->codec_mclk2))
> +		clk_prepare_enable(priv->codec_mclk2);
> +
> +	ret = snd_soc_dai_set_sysclk(aif1_dai, ARIZONA_CLK_SYSCLK, 0, 0);
> +	if (ret < 0) {
> +		dev_err(aif1_dai->dev, "Failed to set SYSCLK: %d\n", ret);
> +		return ret;
> +	}
> +
> +	rtd = snd_soc_get_pcm_runtime(card, card->dai_link[TM2_DAI_AIF2].name);
> +	aif2_dai = rtd->codec_dai;
> +
> +	ret = snd_soc_dai_set_sysclk(aif2_dai, ARIZONA_CLK_ASYNCCLK, 0, 0);
> +	if (ret < 0) {
> +		dev_err(aif2_dai->dev, "Failed to set ASYNCCLK: %d\n", ret);
> +		return ret;
> +	}
> +
> +	dlc.of_node = tm2_speaker_amp_dev.codec_of_node;
> +	amp_pdm_dai = snd_soc_find_dai(&dlc);
> +	if (!amp_pdm_dai)
> +		return -ENODEV;
> +
> +	/* Set the MAX98504 V/I sense PDM Tx DAI channel mapping */
> +	ret = snd_soc_dai_set_channel_map(amp_pdm_dai, ARRAY_SIZE(ch_map),
> +					  ch_map, 0, NULL);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = snd_soc_dai_set_tdm_slot(amp_pdm_dai, 0x3, 0x0, 2, 16);
> +	if (ret < 0)
> +		return ret;
> +
> +	return 0;
> +}
> +
> +static const struct snd_kcontrol_new tm2_controls[] = {
> +	SOC_DAPM_PIN_SWITCH("HP"),
> +	SOC_DAPM_PIN_SWITCH("SPK"),
> +	SOC_DAPM_PIN_SWITCH("RCV"),
> +	SOC_DAPM_PIN_SWITCH("VPS"),
> +	SOC_DAPM_PIN_SWITCH("HDMI"),
> +
> +	SOC_DAPM_PIN_SWITCH("Main Mic"),
> +	SOC_DAPM_PIN_SWITCH("Sub Mic"),
> +	SOC_DAPM_PIN_SWITCH("Third Mic"),
> +
> +	SOC_DAPM_PIN_SWITCH("Headset Mic"),
> +};
> +
> +const struct snd_soc_dapm_widget tm2_dapm_widgets[] = {
> +	SND_SOC_DAPM_HP("HP", NULL),
> +	SND_SOC_DAPM_SPK("SPK", NULL),
> +	SND_SOC_DAPM_SPK("RCV", NULL),
> +	SND_SOC_DAPM_LINE("VPS", NULL),
> +	SND_SOC_DAPM_LINE("HDMI", NULL),
> +
> +	SND_SOC_DAPM_MIC("Main Mic", tm2_mic_bias),
> +	SND_SOC_DAPM_MIC("Sub Mic", NULL),
> +	SND_SOC_DAPM_MIC("Third Mic", NULL),
> +
> +	SND_SOC_DAPM_MIC("Headset Mic", NULL),
> +};
> +
> +static const struct snd_soc_component_driver tm2_component = {
> +	.name	= "tm2-audio",
> +};
> +
> +static struct snd_soc_dai_driver tm2_ext_dai[] = {
> +	{
> +		.name = "Voice call",
> +		.playback = {
> +			.channels_min = 1,
> +			.channels_max = 4,
> +			.rate_min = 8000,
> +			.rate_max = 48000,
> +			.rates = (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 |
> +					SNDRV_PCM_RATE_48000),
> +			.formats = SNDRV_PCM_FMTBIT_S16_LE,
> +		},
> +		.capture = {
> +			.channels_min = 1,
> +			.channels_max = 4,
> +			.rate_min = 8000,
> +			.rate_max = 48000,
> +			.rates = (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 |
> +					SNDRV_PCM_RATE_48000),
> +			.formats = SNDRV_PCM_FMTBIT_S16_LE,
> +		},
> +	},
> +	{
> +		.name = "Bluetooth",
> +		.playback = {
> +			.channels_min = 1,
> +			.channels_max = 4,
> +			.rate_min = 8000,
> +			.rate_max = 16000,
> +			.rates = (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000),
> +			.formats = SNDRV_PCM_FMTBIT_S16_LE,
> +		},
> +		.capture = {
> +			.channels_min = 1,
> +			.channels_max = 2,
> +			.rate_min = 8000,
> +			.rate_max = 16000,
> +			.rates = (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000),
> +			.formats = SNDRV_PCM_FMTBIT_S16_LE,
> +		},
> +	},
> +};
> +
> +static struct snd_soc_dai_link tm2_dai_links[] = {
> +	{
> +		.name		= "WM5110 AIF1",
> +		.stream_name	= "HiFi Primary",
> +		.codec_dai_name = "wm5110-aif1",
> +		.ops		= &tm2_aif1_ops,
> +		.dai_fmt	= SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
> +				  SND_SOC_DAIFMT_CBM_CFM,
> +	}, {
> +		.name		= "WM5110 Voice",
> +		.stream_name	= "Voice call",
> +		.codec_dai_name = "wm5110-aif2",
> +		.ops		= &tm2_aif2_ops,
> +		.dai_fmt	= SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
> +				  SND_SOC_DAIFMT_CBM_CFM,
> +		.ignore_suspend = 1,
> +	}, {
> +		.name		= "WM5110 BT",
> +		.stream_name	= "Bluetooth",
> +		.codec_dai_name = "wm5110-aif3",
> +		.dai_fmt	= SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
> +				  SND_SOC_DAIFMT_CBM_CFM,
> +		.ignore_suspend = 1,
> +	}
> +};
> +
> +static struct snd_soc_card tm2_card = {
> +	.owner			= THIS_MODULE,
> +
> +	.dai_link		= tm2_dai_links,
> +	.num_links		= ARRAY_SIZE(tm2_dai_links),
> +	.controls		= tm2_controls,
> +	.num_controls		= ARRAY_SIZE(tm2_controls),
> +	.dapm_widgets		= tm2_dapm_widgets,
> +	.num_dapm_widgets	= ARRAY_SIZE(tm2_dapm_widgets),
> +	.aux_dev		= &tm2_speaker_amp_dev,
> +	.num_aux_devs		= 1,
> +
> +	.late_probe		= tm2_late_probe,
> +	.set_bias_level		= tm2_set_bias_level,
> +};
> +
> +static int tm2_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct snd_soc_card *card = &tm2_card;
> +	struct tm2_machine_priv *priv;
> +	struct device_node *cpu_dai_node, *codec_dai_node;
> +	int ret, i;
> +
> +	if (!dev->of_node) {
> +		dev_err(dev, "DT node is missing\n");
> +		return -ENODEV;
> +	}
> +
> +	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> +	if (!priv)
> +		return -ENOMEM;
> +
> +	snd_soc_card_set_drvdata(card, priv);
> +	card->dev = dev;
> +
> +	priv->gpio_mic_bias = devm_gpiod_get(dev, "mic-bias",
> +						GPIOF_OUT_INIT_LOW);
> +	if (IS_ERR(priv->gpio_mic_bias)) {
> +		dev_err(dev, "Failed to get mic bias gpio\n");
> +		return PTR_ERR(priv->gpio_mic_bias);
> +	}
> +
> +	ret = snd_soc_of_parse_card_name(card, "model");
> +	if (ret < 0) {
> +		dev_err(dev, "Card name is not specified\n");
> +		return ret;
> +	}
> +
> +	ret = snd_soc_of_parse_audio_routing(card, "samsung,audio-routing");
> +	if (ret < 0) {
> +		dev_err(dev, "Audio routing is not specified or invalid\n");
> +		return ret;
> +	}
> +
> +	card->aux_dev[0].codec_of_node = of_parse_phandle(dev->of_node,
> +							"audio-amplifier", 0);
> +	if (!card->aux_dev[0].codec_of_node) {
> +		dev_err(dev, "audio-amplifier property invalid or missing\n");
> +		return -EINVAL;
> +	}
> +
> +	cpu_dai_node = of_parse_phandle(dev->of_node, "i2s-controller", 0);
> +	if (!cpu_dai_node) {
> +		dev_err(dev, "i2s-controllers property invalid or missing\n");
> +		ret = -EINVAL;
> +		goto err_put_amp;
> +	}
> +
> +	codec_dai_node = of_parse_phandle(dev->of_node, "audio-codec", 0);
> +	if (!codec_dai_node) {
> +		dev_err(dev, "audio-codec property invalid or missing\n");
> +		ret = -EINVAL;
> +		goto err_put_cpu_dai;
> +	}
> +
> +	for (i = 0; i < card->num_links; i++) {
> +		card->dai_link[i].cpu_dai_name = NULL;
> +		card->dai_link[i].cpu_name = NULL;
> +		card->dai_link[i].platform_name = NULL;
> +		card->dai_link[i].codec_of_node = codec_dai_node;
> +		card->dai_link[i].cpu_of_node = cpu_dai_node;
> +		card->dai_link[i].platform_of_node = cpu_dai_node;
> +	}
> +
> +	priv->codec_mclk1 = of_clk_get_by_name(codec_dai_node, "mclk1");
> +	if (IS_ERR(priv->codec_mclk1)) {
> +		dev_err(dev, "Failed to get mclk1 clock\n");
> +		ret = PTR_ERR(priv->codec_mclk1);
> +		goto err_put_codec_dai;
> +	}
> +
> +	/* mclk2 is optional */
> +	priv->codec_mclk2 = of_clk_get_by_name(codec_dai_node, "mclk2");
> +	if (IS_ERR(priv->codec_mclk2))
> +		dev_info(dev, "Not using mclk2 clock\n");
> +
> +	ret = devm_snd_soc_register_component(dev, &tm2_component,
> +				tm2_ext_dai, ARRAY_SIZE(tm2_ext_dai));
> +	if (ret < 0) {
> +		dev_err(dev, "Failed to register component: %d\n", ret);
> +		goto err_put_mclk;
> +	}
> +
> +	ret = devm_snd_soc_register_card(dev, card);
> +	if (ret < 0) {
> +		dev_err(dev, "Failed to register card: %d\n", ret);
> +		goto err_put_mclk;
> +	}
> +
> +	return 0;
> +
> +err_put_mclk:
> +	clk_put(priv->codec_mclk1);
> +	if (!IS_ERR(priv->codec_mclk2))
> +		clk_put(priv->codec_mclk2);
> +err_put_codec_dai:
> +	of_node_put(codec_dai_node);
> +err_put_cpu_dai:
> +	of_node_put(cpu_dai_node);
> +err_put_amp:
> +	of_node_put(card->aux_dev[0].codec_of_node);
> +	return ret;
> +}
> +
> +static int tm2_remove(struct platform_device *pdev)
> +{
> +	struct snd_soc_card *card = &tm2_card;
> +	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
> +
> +	clk_put(priv->codec_mclk1);
> +	if (!IS_ERR(priv->codec_mclk2))
> +		clk_put(priv->codec_mclk2);
> +
> +	of_node_put(card->dai_link[0].codec_of_node);
> +	of_node_put(card->dai_link[0].cpu_of_node);
> +	of_node_put(card->aux_dev[0].codec_of_node);
> +
> +	return 0;
> +}
> +
> +static int tm2_suspend_late(struct device *dev)
> +{
> +	struct snd_soc_card *card = dev_get_drvdata(dev);
> +
> +	return tm2_stop_sysclk(card);
> +}
> +
> +static int tm2_resume_early(struct device *dev)
> +{
> +	struct snd_soc_card *card = dev_get_drvdata(dev);
> +
> +	return tm2_start_sysclk(card);
> +}
> +
> +const struct dev_pm_ops tm2_pm_ops = {
> +	.suspend	= snd_soc_suspend,
> +	.suspend_late	= tm2_suspend_late,
> +	.resume		= snd_soc_resume,
> +	.resume_early	= tm2_resume_early,
> +	.freeze		= snd_soc_suspend,
> +	.thaw		= snd_soc_resume,
> +	.poweroff	= snd_soc_poweroff,
> +	.restore	= snd_soc_resume,
> +};
> +
> +static const struct of_device_id tm2_of_match[] = {
> +	{ .compatible = "samsung,tm2-audio" },
> +	{ },
> +};
> +MODULE_DEVICE_TABLE(of, tm2_of_match);
> +
> +static struct platform_driver tm2_driver = {
> +	.driver = {
> +		.name		= "tm2-audio",
> +		.pm		= &tm2_pm_ops,
> +		.of_match_table	= tm2_of_match,
> +	},
> +	.probe	= tm2_probe,
> +	.remove	= tm2_remove,
> +};
> +module_platform_driver(tm2_driver);
> +
> +MODULE_AUTHOR("Inha Song <ideal.song@samsung.com>");
> +MODULE_DESCRIPTION("ALSA SoC Exynos TM2 Audio Support");
> +MODULE_LICENSE("GPL v2");
> --
> 1.9.1
> 
> --
> To unsubscribe from this list: send the line "unsubscribe linux-samsung-soc" in
> the body of a message to majordomo@vger.kernel.org
> More majordomo info at  http://vger.kernel.org/majordomo-info.html
> 
> 
> 

--
To unsubscribe from this list: send the line "unsubscribe linux-samsung-soc" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Charles Keepax Aug. 29, 2016, 3:02 p.m. UTC | #3
On Tue, Aug 09, 2016 at 04:21:54PM +0200, Sylwester Nawrocki wrote:
> This patch adds the sound machine driver for TM2 and TM2E board.
> Speaker and headphone playback, Main Mic capture, Bluetooth,
> Voice call and external accessory are supported.
> 
> Signed-off-by: Inha Song <ideal.song@samsung.com>
> [k.kozlowski: rebased on 4.1]
> Signed-off-by: Krzysztof Kozlowski <k.kozlowski@samsung.com>
> [s.nawrocki: rebased to 4.7, adjustment to the ASoC core changes,
>  removed unused ops and direct calls to the max98504 function,
>  added parsing of "audio-amplifier" and "audio-codec"
>  properties, added TDM API calls, switched to gpiod API]
> Signed-off-by: Sylwester Nawrocki <s.nawrocki@samsung.com>
> ---
> 
> Changes since v4 (addressing review comments from Charles):
>  - changed the order of WM5110_FLL{1,2}, WM5110_FLL{1,2}_REFCLK setting,
>  - ARIZONA_CLK_SYSCLK, ARIZONA_CLK_ASYNCCLK setting moved to late_probe,
>  - added tm2_aif2_hw_free callback for disabling FLL2,
>  - removed unneded card->dapm.bias_level assignment in tm2_mic_bias callback,
>  - suspend_late, resume_early dev_pm_ops used instead of suspend_post,
>    resume_pre struct snd_soc_card callbacks.
> 
> Changes since v3:
>  - removed SND_SOC_SAMSUNG_AUDSS from Kconfig.
> 
> Changes since v2:
>  - added missing Kconfig dependencies.
> 
> Changes since initial version:
>  - added PDM Tx channels setup through TDM API
>  - adaptation to renamed 'samsung,model', 'samsung,i2s-controller',
>    'samsung,speaker-amplifier' properties,
>  - removed some dev_dbg() calls,
>  - cleaned up mic-bias GPIO handling and switched to gpiod API,
>  - added parsing of 'audio-codec' property,
>  - initialized codec_of_node of dai_link instead of codec_name,
>  - switched to using clock, clock-names properties from the wm5110
>    codec node,
>  - fixed error paths in probe() (of_node reference counting).
> ---
<snip>
> +static int tm2_suspend_late(struct device *dev)
> +{
> +	struct snd_soc_card *card = dev_get_drvdata(dev);
> +
> +	return tm2_stop_sysclk(card);
> +}
> +
> +static int tm2_resume_early(struct device *dev)
> +{
> +	struct snd_soc_card *card = dev_get_drvdata(dev);
> +
> +	return tm2_start_sysclk(card);
> +}
> +
> +const struct dev_pm_ops tm2_pm_ops = {
> +	.suspend	= snd_soc_suspend,
> +	.suspend_late	= tm2_suspend_late,
> +	.resume		= snd_soc_resume,
> +	.resume_early	= tm2_resume_early,
> +	.freeze		= snd_soc_suspend,
> +	.thaw		= snd_soc_resume,
> +	.poweroff	= snd_soc_poweroff,
> +	.restore	= snd_soc_resume,
> +};

This bit still looks problematic to me, although admittedly I not
sure exactly why you don't seem to be hitting any issues with
it. Before you were effectively calling things from .suspend, now
its .suspend_late.  As we are now later in the suspend process
that means its even more likely the SPI will be unavailable when
we try to talk to the CODEC. You need to run before the SPI
has started suspending so you can still communicate with the
CODEC, I still think prepare/complete are likely to be the best
options for this, unless there are some complications there I
have missed.

Aside from that and the clock stuff we are already discussing
everything else looks totally fine to me.

Thanks,
Charles
--
To unsubscribe from this list: send the line "unsubscribe linux-samsung-soc" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
On 08/29/2016 05:02 PM, Charles Keepax wrote:
> On Tue, Aug 09, 2016 at 04:21:54PM +0200, Sylwester Nawrocki wrote:
>> ---
> <snip>
>> +static int tm2_suspend_late(struct device *dev)
>> +{
>> +	struct snd_soc_card *card = dev_get_drvdata(dev);
>> +
>> +	return tm2_stop_sysclk(card);
>> +}
>> +
>> +static int tm2_resume_early(struct device *dev)
>> +{
>> +	struct snd_soc_card *card = dev_get_drvdata(dev);
>> +
>> +	return tm2_start_sysclk(card);
>> +}
>> +
>> +const struct dev_pm_ops tm2_pm_ops = {
>> +	.suspend	= snd_soc_suspend,
>> +	.suspend_late	= tm2_suspend_late,
>> +	.resume		= snd_soc_resume,
>> +	.resume_early	= tm2_resume_early,
>> +	.freeze		= snd_soc_suspend,
>> +	.thaw		= snd_soc_resume,
>> +	.poweroff	= snd_soc_poweroff,
>> +	.restore	= snd_soc_resume,
>> +};
> 
> This bit still looks problematic to me, although admittedly I not
> sure exactly why you don't seem to be hitting any issues with
> it. Before you were effectively calling things from .suspend, now
> its .suspend_late.  As we are now later in the suspend process
> that means its even more likely the SPI will be unavailable when
> we try to talk to the CODEC. You need to run before the SPI
> has started suspending so you can still communicate with the
> CODEC, I still think prepare/complete are likely to be the best
> options for this, unless there are some complications there I
> have missed.
> 
> Aside from that and the clock stuff we are already discussing
> everything else looks totally fine to me.

Thanks for your review. First of all I'm testing system suspend
related changes on 4.1 kernel as there is no full support for the
exynos5433 SoC in mainline yet.
I will use prepare/complete as with late_suspend/early_resume power
sequences are wrong as you point out. I still need to debug why
there is no errors reported when the SPI controller is suspended
while there are supposed to be done SPI bus transfers.

It seems I missed the main point of making that pm_ops change, I
focused on the MCLK clock presence while the main issue was
communication with the codec over SPI bus.

I added some debug prints and below are logs when using
late_suspend/early_resume and prepare/complete. In both cases
tm2_stop_sysclk() returns 0.

I think suspend/resume sequences are still correct when snd_soc_pm_ops
are used thanks to order of linking drivers into kernel image and
adding device to dpm_list.

-------------------------------8<-----------------------------------
suspend_late/early_resume

# dmesg | grep "resume\|suspend\|arizona"

[   42.169691] Suspending console(s) (use no_console_suspend to debug)

[   42.211115] tm2-audio sound: snd_soc_suspend
[   42.213991] s3c64xx-spi 14d30000.spi: s3c64xx_spi_suspend

[   42.215097] PM: suspend of devices complete after 40.028 msecs
[   42.229764] tm2-audio sound: tm2_suspend_late: 0

[   42.231496] PM: late suspend of devices complete after 6.362 msecs
[   42.237318] PM: noirq suspend of devices complete after 5.804 msecs
[   42.819161] PM: noirq resume of devices complete after 62.500 msecs

[   43.063552] arizona spi1.0: FLL1: Timed out waiting for lock
[   43.063572] tm2-audio sound: tm2_resume_early: 0
[   43.066830] PM: early resume of devices complete after 247.302 msecs

[   43.078789] s3c64xx-spi 14d30000.spi: s3c64xx_spi_resume
[   43.080192] tm2-audio sound: snd_soc_resume
[   43.080212] tm2-audio sound: ASoC: starting resume work

[   43.319459] PM: resume of devices complete after 241.698 msecs
-------------------------------8<-----------------------------------


-------------------------------8<-----------------------------------
prepare/complete

# dmesg -c | grep "prepare\|complete\|suspend\|resume\|arizona"

[  172.398552] Suspending console(s) (use no_console_suspend to debug)
[  172.399431] tm2-audio sound: tm2_prepare: 0
[  172.441073] tm2-audio sound: snd_soc_suspend
[  172.443653] s3c64xx-spi 14d30000.spi: s3c64xx_spi_suspend

[  172.444766] PM: suspend of devices complete after 42.202 msecs
[  172.459500] PM: late suspend of devices complete after 6.407 msecs
[  172.465277] PM: noirq suspend of devices complete after 5.758 msecs
[  173.039052] PM: noirq resume of devices complete after 62.396 msecs
[  173.044578] PM: early resume of devices complete after 5.152 msecs

[  173.057042] s3c64xx-spi 14d30000.spi: s3c64xx_spi_resume
[  173.058423] tm2-audio sound: snd_soc_resume
[  173.058442] tm2-audio sound: ASoC: starting resume work

[  173.299428] PM: resume of devices complete after 243.393 msecs
[  173.329555] arizona spi1.0: Spurious HPDET IRQ
[  173.329704] arizona spi1.0: Mixer dropped sample
[  173.329758] tm2-audio sound: tm2_complete: 0
-------------------------------8<-----------------------------------
On 08/30/2016 04:57 PM, Sylwester Nawrocki wrote:
> I will use prepare/complete as with late_suspend/early_resume power
> sequences are wrong as you point out. I still need to debug why
> there is no errors reported when the SPI controller is suspended
> while there are supposed to be done SPI bus transfers.

As it turns out the reason there is no any errors with late_suspend/
early_resume is that the CODEC sets its regmap's "cache only" only flag
in its suspend() callback and then clears it in resume().  So any SPI
transfers which might happen when SPI controller is suspended
are prevented this way.
--
To unsubscribe from this list: send the line "unsubscribe linux-samsung-soc" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Charles Keepax Sept. 2, 2016, 10:56 a.m. UTC | #6
On Fri, Sep 02, 2016 at 12:49:51PM +0200, Sylwester Nawrocki wrote:
> On 08/30/2016 04:57 PM, Sylwester Nawrocki wrote:
> > I will use prepare/complete as with late_suspend/early_resume power
> > sequences are wrong as you point out. I still need to debug why
> > there is no errors reported when the SPI controller is suspended
> > while there are supposed to be done SPI bus transfers.
> 
> As it turns out the reason there is no any errors with late_suspend/
> early_resume is that the CODEC sets its regmap's "cache only" only flag
> in its suspend() callback and then clears it in resume().  So any SPI
> transfers which might happen when SPI controller is suspended
> are prevented this way.

Ah of course yeah should have realised that, but yeah you still
end up with the writes not actually reaching the hardware.

Thanks,
Charles
--
To unsubscribe from this list: send the line "unsubscribe linux-samsung-soc" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
diff mbox

Patch

diff --git a/sound/soc/samsung/Kconfig b/sound/soc/samsung/Kconfig
index 7b722b0..1bed8a5 100644
--- a/sound/soc/samsung/Kconfig
+++ b/sound/soc/samsung/Kconfig
@@ -229,3 +229,12 @@  config SND_SOC_ARNDALE_RT5631_ALC5631
         depends on SND_SOC_SAMSUNG && I2C
         select SND_SAMSUNG_I2S
         select SND_SOC_RT5631
+
+config SND_SOC_SAMSUNG_TM2_WM5110
+	tristate "SoC I2S Audio support for WM5110 on TM2 board"
+	depends on SND_SOC_SAMSUNG && MFD_ARIZONA && I2C && SPI_MASTER
+	select SND_SOC_MAX98504
+	select SND_SOC_WM5110
+	select SND_SAMSUNG_I2S
+	help
+	  Say Y if you want to add support for SoC audio on the TM2 board.
diff --git a/sound/soc/samsung/Makefile b/sound/soc/samsung/Makefile
index 5d03f5c..4444b9f 100644
--- a/sound/soc/samsung/Makefile
+++ b/sound/soc/samsung/Makefile
@@ -44,6 +44,7 @@  snd-soc-lowland-objs := lowland.o
 snd-soc-littlemill-objs := littlemill.o
 snd-soc-bells-objs := bells.o
 snd-soc-arndale-rt5631-objs := arndale_rt5631.o
+snd-soc-tm2-wm5110-objs := tm2_wm5110.o

 obj-$(CONFIG_SND_SOC_SAMSUNG_JIVE_WM8750) += snd-soc-jive-wm8750.o
 obj-$(CONFIG_SND_SOC_SAMSUNG_NEO1973_WM8753) += snd-soc-neo1973-wm8753.o
@@ -69,3 +70,4 @@  obj-$(CONFIG_SND_SOC_LOWLAND) += snd-soc-lowland.o
 obj-$(CONFIG_SND_SOC_LITTLEMILL) += snd-soc-littlemill.o
 obj-$(CONFIG_SND_SOC_BELLS) += snd-soc-bells.o
 obj-$(CONFIG_SND_SOC_ARNDALE_RT5631_ALC5631) += snd-soc-arndale-rt5631.o
+obj-$(CONFIG_SND_SOC_SAMSUNG_TM2_WM5110) += snd-soc-tm2-wm5110.o
diff --git a/sound/soc/samsung/tm2_wm5110.c b/sound/soc/samsung/tm2_wm5110.c
new file mode 100644
index 0000000..16c48fb
--- /dev/null
+++ b/sound/soc/samsung/tm2_wm5110.c
@@ -0,0 +1,604 @@ 
+/*
+ * Copyright (C) 2015 - 2016 Samsung Electronics Co., Ltd.
+ *
+ * Authors: Inha Song <ideal.song@samsung.com>
+ *          Sylwester Nawrocki <s.nawrocki@samsung.com>
+ *
+ * This program is free software; you can redistribute  it and/or modify it
+ * under  the terms of  the GNU General  Public License as published by the
+ * Free Software Foundation.
+ */
+
+#include <linux/clk.h>
+#include <linux/gpio.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+
+#include "i2s.h"
+#include "../codecs/wm5110.h"
+
+#define TM2_DAI_AIF1	0
+#define TM2_DAI_AIF2	1
+
+struct tm2_machine_priv {
+	struct snd_soc_codec *codec;
+	struct clk *codec_mclk1;
+	struct clk *codec_mclk2;
+
+	unsigned int sysclk_rate;
+
+	struct gpio_desc *gpio_mic_bias;
+};
+
+static int tm2_start_sysclk(struct snd_soc_card *card)
+{
+	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
+	struct snd_soc_codec *codec = priv->codec;
+	unsigned long mclk_rate = clk_get_rate(priv->codec_mclk1);
+	int ret;
+
+	ret = clk_prepare_enable(priv->codec_mclk1);
+	if (ret < 0) {
+		dev_err(card->dev, "Failed to enable mclk: %d\n", ret);
+		return ret;
+	}
+
+	ret = snd_soc_codec_set_pll(codec, WM5110_FLL1_REFCLK,
+				    ARIZONA_FLL_SRC_MCLK1,
+				    mclk_rate,
+				    priv->sysclk_rate);
+	if (ret < 0) {
+		dev_err(codec->dev, "Failed to set FLL1 source: %d\n", ret);
+		return ret;
+	}
+
+	ret = snd_soc_codec_set_pll(codec, WM5110_FLL1,
+				    ARIZONA_FLL_SRC_MCLK1,
+				    mclk_rate,
+				    priv->sysclk_rate);
+	if (ret < 0) {
+		dev_err(codec->dev, "Failed to start FLL1: %d\n", ret);
+		return ret;
+	}
+
+	ret = snd_soc_codec_set_sysclk(codec, ARIZONA_CLK_SYSCLK,
+				       ARIZONA_CLK_SRC_FLL1,
+				       priv->sysclk_rate,
+				       SND_SOC_CLOCK_IN);
+	if (ret < 0) {
+		dev_err(codec->dev, "Failed to set SYSCLK source: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int tm2_stop_sysclk(struct snd_soc_card *card)
+{
+	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
+	struct snd_soc_codec *codec = priv->codec;
+	int ret;
+
+	ret = snd_soc_codec_set_pll(codec, WM5110_FLL1, 0, 0, 0);
+	if (ret < 0) {
+		dev_err(codec->dev, "Failed to stop FLL1: %d\n", ret);
+		return ret;
+	}
+
+	ret = snd_soc_codec_set_sysclk(codec, ARIZONA_CLK_SYSCLK,
+				       ARIZONA_CLK_SRC_FLL1, 0, 0);
+	if (ret < 0) {
+		dev_err(codec->dev, "Failed to stop SYSCLK: %d\n", ret);
+		return ret;
+	}
+
+	clk_disable_unprepare(priv->codec_mclk1);
+
+	return 0;
+}
+
+static int tm2_aif1_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_codec *codec = rtd->codec;
+	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(rtd->card);
+
+	switch (params_rate(params)) {
+	case 4000:
+	case 8000:
+	case 12000:
+	case 16000:
+	case 24000:
+	case 32000:
+	case 48000:
+	case 96000:
+	case 192000:
+		/* Highest possible SYSCLK frequency: 147.456MHz */
+		priv->sysclk_rate = 147456000U;
+		break;
+	case 11025:
+	case 22050:
+	case 44100:
+	case 88200:
+	case 176400:
+		/* Highest possible SYSCLK frequency: 135.4752 MHz */
+		priv->sysclk_rate = 135475200U;
+		break;
+	default:
+		dev_err(codec->dev, "Not supported sample rate: %d\n",
+			params_rate(params));
+		return -EINVAL;
+	}
+
+	return tm2_start_sysclk(rtd->card);
+}
+
+static struct snd_soc_ops tm2_aif1_ops = {
+	.hw_params = tm2_aif1_hw_params,
+};
+
+static int tm2_aif2_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_codec *codec = rtd->codec;
+	struct tm2_machine_priv *priv =	snd_soc_card_get_drvdata(rtd->card);
+	unsigned long mclk_rate = clk_get_rate(priv->codec_mclk1);
+	unsigned int asyncclk_rate;
+	int ret;
+
+	switch (params_rate(params)) {
+	case 8000:
+	case 12000:
+	case 16000:
+		/* Highest possible ASYNCCLK frequency: 49.152MHz */
+		asyncclk_rate = 49152000U;
+		break;
+	case 11025:
+		/* Highest possible ASYNCCLK frequency: 45.1584 MHz */
+		asyncclk_rate = 45158400U;
+		break;
+	default:
+		dev_err(codec->dev, "Not supported sample rate: %d\n",
+			params_rate(params));
+		return -EINVAL;
+	}
+
+	ret = snd_soc_codec_set_pll(codec, WM5110_FLL2_REFCLK,
+				    ARIZONA_FLL_SRC_MCLK1,
+				    mclk_rate,
+				    asyncclk_rate);
+	if (ret < 0) {
+		dev_err(codec->dev, "Failed to set FLL2 source: %d\n", ret);
+		return ret;
+	}
+
+	ret = snd_soc_codec_set_pll(codec, WM5110_FLL2,
+				    ARIZONA_FLL_SRC_MCLK1,
+				    mclk_rate,
+				    asyncclk_rate);
+	if (ret < 0) {
+		dev_err(codec->dev, "Failed to start FLL2: %d\n", ret);
+		return ret;
+	}
+
+	ret = snd_soc_codec_set_sysclk(codec, ARIZONA_CLK_ASYNCCLK,
+				       ARIZONA_CLK_SRC_FLL2,
+				       asyncclk_rate,
+				       SND_SOC_CLOCK_IN);
+	if (ret < 0) {
+		dev_err(codec->dev, "Failed to set ASYNCCLK source: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int tm2_aif2_hw_free(struct snd_pcm_substream *substream)
+{
+	struct snd_soc_pcm_runtime *rtd = substream->private_data;
+	struct snd_soc_codec *codec = rtd->codec;
+	int ret;
+
+	/* disable FLL2 */
+	ret = snd_soc_codec_set_pll(codec, WM5110_FLL2, ARIZONA_FLL_SRC_MCLK1,
+				    0, 0);
+	if (ret < 0)
+		dev_err(codec->dev, "Failed to stop FLL2: %d\n", ret);
+
+	return ret;
+}
+
+static struct snd_soc_ops tm2_aif2_ops = {
+	.hw_params = tm2_aif2_hw_params,
+	.hw_free = tm2_aif2_hw_free,
+};
+
+static int tm2_mic_bias(struct snd_soc_dapm_widget *w,
+				struct snd_kcontrol *kcontrol, int event)
+{
+	struct snd_soc_card *card = w->dapm->card;
+	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
+
+	switch (event) {
+	case SND_SOC_DAPM_PRE_PMU:
+		gpiod_set_value_cansleep(priv->gpio_mic_bias,  1);
+		break;
+	case SND_SOC_DAPM_POST_PMD:
+		gpiod_set_value_cansleep(priv->gpio_mic_bias,  0);
+		break;
+	}
+
+	return 0;
+}
+
+static int tm2_set_bias_level(struct snd_soc_card *card,
+				struct snd_soc_dapm_context *dapm,
+				enum snd_soc_bias_level level)
+{
+	struct snd_soc_pcm_runtime *rtd;
+
+	rtd = snd_soc_get_pcm_runtime(card, card->dai_link[0].name);
+
+	if (dapm->dev != rtd->codec_dai->dev)
+		return 0;
+
+	switch (level) {
+	case SND_SOC_BIAS_STANDBY:
+		if (card->dapm.bias_level == SND_SOC_BIAS_OFF)
+			tm2_start_sysclk(card);
+		break;
+	case SND_SOC_BIAS_OFF:
+		tm2_stop_sysclk(card);
+		break;
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+static struct snd_soc_aux_dev tm2_speaker_amp_dev;
+
+static int tm2_late_probe(struct snd_soc_card *card)
+{
+	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
+	struct snd_soc_dai_link_component dlc = { 0 };
+	unsigned int ch_map[] = { 0, 1 };
+	struct snd_soc_dai *amp_pdm_dai;
+	struct snd_soc_pcm_runtime *rtd;
+	struct snd_soc_dai *aif1_dai;
+	struct snd_soc_dai *aif2_dai;
+	int ret;
+
+	rtd = snd_soc_get_pcm_runtime(card, card->dai_link[TM2_DAI_AIF1].name);
+	aif1_dai = rtd->codec_dai;
+	priv->codec = rtd->codec;
+
+	/* 32 kHz must be enabled for jack detection */
+	if (!IS_ERR(priv->codec_mclk2))
+		clk_prepare_enable(priv->codec_mclk2);
+
+	ret = snd_soc_dai_set_sysclk(aif1_dai, ARIZONA_CLK_SYSCLK, 0, 0);
+	if (ret < 0) {
+		dev_err(aif1_dai->dev, "Failed to set SYSCLK: %d\n", ret);
+		return ret;
+	}
+
+	rtd = snd_soc_get_pcm_runtime(card, card->dai_link[TM2_DAI_AIF2].name);
+	aif2_dai = rtd->codec_dai;
+
+	ret = snd_soc_dai_set_sysclk(aif2_dai, ARIZONA_CLK_ASYNCCLK, 0, 0);
+	if (ret < 0) {
+		dev_err(aif2_dai->dev, "Failed to set ASYNCCLK: %d\n", ret);
+		return ret;
+	}
+
+	dlc.of_node = tm2_speaker_amp_dev.codec_of_node;
+	amp_pdm_dai = snd_soc_find_dai(&dlc);
+	if (!amp_pdm_dai)
+		return -ENODEV;
+
+	/* Set the MAX98504 V/I sense PDM Tx DAI channel mapping */
+	ret = snd_soc_dai_set_channel_map(amp_pdm_dai, ARRAY_SIZE(ch_map),
+					  ch_map, 0, NULL);
+	if (ret < 0)
+		return ret;
+
+	ret = snd_soc_dai_set_tdm_slot(amp_pdm_dai, 0x3, 0x0, 2, 16);
+	if (ret < 0)
+		return ret;
+
+	return 0;
+}
+
+static const struct snd_kcontrol_new tm2_controls[] = {
+	SOC_DAPM_PIN_SWITCH("HP"),
+	SOC_DAPM_PIN_SWITCH("SPK"),
+	SOC_DAPM_PIN_SWITCH("RCV"),
+	SOC_DAPM_PIN_SWITCH("VPS"),
+	SOC_DAPM_PIN_SWITCH("HDMI"),
+
+	SOC_DAPM_PIN_SWITCH("Main Mic"),
+	SOC_DAPM_PIN_SWITCH("Sub Mic"),
+	SOC_DAPM_PIN_SWITCH("Third Mic"),
+
+	SOC_DAPM_PIN_SWITCH("Headset Mic"),
+};
+
+const struct snd_soc_dapm_widget tm2_dapm_widgets[] = {
+	SND_SOC_DAPM_HP("HP", NULL),
+	SND_SOC_DAPM_SPK("SPK", NULL),
+	SND_SOC_DAPM_SPK("RCV", NULL),
+	SND_SOC_DAPM_LINE("VPS", NULL),
+	SND_SOC_DAPM_LINE("HDMI", NULL),
+
+	SND_SOC_DAPM_MIC("Main Mic", tm2_mic_bias),
+	SND_SOC_DAPM_MIC("Sub Mic", NULL),
+	SND_SOC_DAPM_MIC("Third Mic", NULL),
+
+	SND_SOC_DAPM_MIC("Headset Mic", NULL),
+};
+
+static const struct snd_soc_component_driver tm2_component = {
+	.name	= "tm2-audio",
+};
+
+static struct snd_soc_dai_driver tm2_ext_dai[] = {
+	{
+		.name = "Voice call",
+		.playback = {
+			.channels_min = 1,
+			.channels_max = 4,
+			.rate_min = 8000,
+			.rate_max = 48000,
+			.rates = (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 |
+					SNDRV_PCM_RATE_48000),
+			.formats = SNDRV_PCM_FMTBIT_S16_LE,
+		},
+		.capture = {
+			.channels_min = 1,
+			.channels_max = 4,
+			.rate_min = 8000,
+			.rate_max = 48000,
+			.rates = (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 |
+					SNDRV_PCM_RATE_48000),
+			.formats = SNDRV_PCM_FMTBIT_S16_LE,
+		},
+	},
+	{
+		.name = "Bluetooth",
+		.playback = {
+			.channels_min = 1,
+			.channels_max = 4,
+			.rate_min = 8000,
+			.rate_max = 16000,
+			.rates = (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000),
+			.formats = SNDRV_PCM_FMTBIT_S16_LE,
+		},
+		.capture = {
+			.channels_min = 1,
+			.channels_max = 2,
+			.rate_min = 8000,
+			.rate_max = 16000,
+			.rates = (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000),
+			.formats = SNDRV_PCM_FMTBIT_S16_LE,
+		},
+	},
+};
+
+static struct snd_soc_dai_link tm2_dai_links[] = {
+	{
+		.name		= "WM5110 AIF1",
+		.stream_name	= "HiFi Primary",
+		.codec_dai_name = "wm5110-aif1",
+		.ops		= &tm2_aif1_ops,
+		.dai_fmt	= SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
+				  SND_SOC_DAIFMT_CBM_CFM,
+	}, {
+		.name		= "WM5110 Voice",
+		.stream_name	= "Voice call",
+		.codec_dai_name = "wm5110-aif2",
+		.ops		= &tm2_aif2_ops,
+		.dai_fmt	= SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
+				  SND_SOC_DAIFMT_CBM_CFM,
+		.ignore_suspend = 1,
+	}, {
+		.name		= "WM5110 BT",
+		.stream_name	= "Bluetooth",
+		.codec_dai_name = "wm5110-aif3",
+		.dai_fmt	= SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
+				  SND_SOC_DAIFMT_CBM_CFM,
+		.ignore_suspend = 1,
+	}
+};
+
+static struct snd_soc_card tm2_card = {
+	.owner			= THIS_MODULE,
+
+	.dai_link		= tm2_dai_links,
+	.num_links		= ARRAY_SIZE(tm2_dai_links),
+	.controls		= tm2_controls,
+	.num_controls		= ARRAY_SIZE(tm2_controls),
+	.dapm_widgets		= tm2_dapm_widgets,
+	.num_dapm_widgets	= ARRAY_SIZE(tm2_dapm_widgets),
+	.aux_dev		= &tm2_speaker_amp_dev,
+	.num_aux_devs		= 1,
+
+	.late_probe		= tm2_late_probe,
+	.set_bias_level		= tm2_set_bias_level,
+};
+
+static int tm2_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct snd_soc_card *card = &tm2_card;
+	struct tm2_machine_priv *priv;
+	struct device_node *cpu_dai_node, *codec_dai_node;
+	int ret, i;
+
+	if (!dev->of_node) {
+		dev_err(dev, "DT node is missing\n");
+		return -ENODEV;
+	}
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	snd_soc_card_set_drvdata(card, priv);
+	card->dev = dev;
+
+	priv->gpio_mic_bias = devm_gpiod_get(dev, "mic-bias",
+						GPIOF_OUT_INIT_LOW);
+	if (IS_ERR(priv->gpio_mic_bias)) {
+		dev_err(dev, "Failed to get mic bias gpio\n");
+		return PTR_ERR(priv->gpio_mic_bias);
+	}
+
+	ret = snd_soc_of_parse_card_name(card, "model");
+	if (ret < 0) {
+		dev_err(dev, "Card name is not specified\n");
+		return ret;
+	}
+
+	ret = snd_soc_of_parse_audio_routing(card, "samsung,audio-routing");
+	if (ret < 0) {
+		dev_err(dev, "Audio routing is not specified or invalid\n");
+		return ret;
+	}
+
+	card->aux_dev[0].codec_of_node = of_parse_phandle(dev->of_node,
+							"audio-amplifier", 0);
+	if (!card->aux_dev[0].codec_of_node) {
+		dev_err(dev, "audio-amplifier property invalid or missing\n");
+		return -EINVAL;
+	}
+
+	cpu_dai_node = of_parse_phandle(dev->of_node, "i2s-controller", 0);
+	if (!cpu_dai_node) {
+		dev_err(dev, "i2s-controllers property invalid or missing\n");
+		ret = -EINVAL;
+		goto err_put_amp;
+	}
+
+	codec_dai_node = of_parse_phandle(dev->of_node, "audio-codec", 0);
+	if (!codec_dai_node) {
+		dev_err(dev, "audio-codec property invalid or missing\n");
+		ret = -EINVAL;
+		goto err_put_cpu_dai;
+	}
+
+	for (i = 0; i < card->num_links; i++) {
+		card->dai_link[i].cpu_dai_name = NULL;
+		card->dai_link[i].cpu_name = NULL;
+		card->dai_link[i].platform_name = NULL;
+		card->dai_link[i].codec_of_node = codec_dai_node;
+		card->dai_link[i].cpu_of_node = cpu_dai_node;
+		card->dai_link[i].platform_of_node = cpu_dai_node;
+	}
+
+	priv->codec_mclk1 = of_clk_get_by_name(codec_dai_node, "mclk1");
+	if (IS_ERR(priv->codec_mclk1)) {
+		dev_err(dev, "Failed to get mclk1 clock\n");
+		ret = PTR_ERR(priv->codec_mclk1);
+		goto err_put_codec_dai;
+	}
+
+	/* mclk2 is optional */
+	priv->codec_mclk2 = of_clk_get_by_name(codec_dai_node, "mclk2");
+	if (IS_ERR(priv->codec_mclk2))
+		dev_info(dev, "Not using mclk2 clock\n");
+
+	ret = devm_snd_soc_register_component(dev, &tm2_component,
+				tm2_ext_dai, ARRAY_SIZE(tm2_ext_dai));
+	if (ret < 0) {
+		dev_err(dev, "Failed to register component: %d\n", ret);
+		goto err_put_mclk;
+	}
+
+	ret = devm_snd_soc_register_card(dev, card);
+	if (ret < 0) {
+		dev_err(dev, "Failed to register card: %d\n", ret);
+		goto err_put_mclk;
+	}
+
+	return 0;
+
+err_put_mclk:
+	clk_put(priv->codec_mclk1);
+	if (!IS_ERR(priv->codec_mclk2))
+		clk_put(priv->codec_mclk2);
+err_put_codec_dai:
+	of_node_put(codec_dai_node);
+err_put_cpu_dai:
+	of_node_put(cpu_dai_node);
+err_put_amp:
+	of_node_put(card->aux_dev[0].codec_of_node);
+	return ret;
+}
+
+static int tm2_remove(struct platform_device *pdev)
+{
+	struct snd_soc_card *card = &tm2_card;
+	struct tm2_machine_priv *priv = snd_soc_card_get_drvdata(card);
+
+	clk_put(priv->codec_mclk1);
+	if (!IS_ERR(priv->codec_mclk2))
+		clk_put(priv->codec_mclk2);
+
+	of_node_put(card->dai_link[0].codec_of_node);
+	of_node_put(card->dai_link[0].cpu_of_node);
+	of_node_put(card->aux_dev[0].codec_of_node);
+
+	return 0;
+}
+
+static int tm2_suspend_late(struct device *dev)
+{
+	struct snd_soc_card *card = dev_get_drvdata(dev);
+
+	return tm2_stop_sysclk(card);
+}
+
+static int tm2_resume_early(struct device *dev)
+{
+	struct snd_soc_card *card = dev_get_drvdata(dev);
+
+	return tm2_start_sysclk(card);
+}
+
+const struct dev_pm_ops tm2_pm_ops = {
+	.suspend	= snd_soc_suspend,
+	.suspend_late	= tm2_suspend_late,
+	.resume		= snd_soc_resume,
+	.resume_early	= tm2_resume_early,
+	.freeze		= snd_soc_suspend,
+	.thaw		= snd_soc_resume,
+	.poweroff	= snd_soc_poweroff,
+	.restore	= snd_soc_resume,
+};
+
+static const struct of_device_id tm2_of_match[] = {
+	{ .compatible = "samsung,tm2-audio" },
+	{ },
+};
+MODULE_DEVICE_TABLE(of, tm2_of_match);
+
+static struct platform_driver tm2_driver = {
+	.driver = {
+		.name		= "tm2-audio",
+		.pm		= &tm2_pm_ops,
+		.of_match_table	= tm2_of_match,
+	},
+	.probe	= tm2_probe,
+	.remove	= tm2_remove,
+};
+module_platform_driver(tm2_driver);
+
+MODULE_AUTHOR("Inha Song <ideal.song@samsung.com>");
+MODULE_DESCRIPTION("ALSA SoC Exynos TM2 Audio Support");
+MODULE_LICENSE("GPL v2");