aboutsummaryrefslogtreecommitdiffstats
path: root/sound/soc/imx/imx-wm8962.c
diff options
context:
space:
mode:
Diffstat (limited to 'sound/soc/imx/imx-wm8962.c')
-rw-r--r--sound/soc/imx/imx-wm8962.c678
1 files changed, 678 insertions, 0 deletions
diff --git a/sound/soc/imx/imx-wm8962.c b/sound/soc/imx/imx-wm8962.c
new file mode 100644
index 00000000..d6ab88d7
--- /dev/null
+++ b/sound/soc/imx/imx-wm8962.c
@@ -0,0 +1,678 @@
+/*
+ * imx-wm8962.c
+ *
+ * Copyright (C) 2012-2013 Freescale Semiconductor, Inc. All Rights Reserved.
+ */
+
+/*
+ * The code contained herein is licensed under the GNU General Public
+ * License. You may obtain a copy of the GNU General Public License
+ * Version 2 or later at the following locations:
+ *
+ * http://www.opensource.org/licenses/gpl-license.html
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/init.h>
+#include <linux/delay.h>
+#include <linux/pm.h>
+#include <linux/bitops.h>
+#include <linux/platform_device.h>
+#include <linux/i2c.h>
+#include <linux/err.h>
+#include <linux/irq.h>
+#include <linux/io.h>
+#include <linux/fsl_devices.h>
+#include <linux/slab.h>
+#include <linux/clk.h>
+#include <linux/kthread.h>
+#include <sound/core.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/soc-dapm.h>
+#include <sound/initval.h>
+#include <sound/jack.h>
+#include <mach/dma.h>
+#include <mach/clock.h>
+#include <mach/audmux.h>
+#include <mach/gpio.h>
+#include <asm/mach-types.h>
+
+#include "imx-ssi.h"
+#include "../codecs/wm8962.h"
+
+struct imx_priv {
+ int sysclk; /*mclk from the outside*/
+ int codec_sysclk;
+ int dai_hifi;
+ int hp_irq;
+ int hp_status;
+ int amic_irq;
+ int amic_status;
+ struct platform_device *pdev;
+ struct snd_pcm_substream *first_stream;
+ struct snd_pcm_substream *second_stream;
+};
+unsigned int sample_format = SNDRV_PCM_FMTBIT_S16_LE;
+static struct imx_priv card_priv;
+static struct snd_soc_card snd_soc_card_imx;
+static struct snd_soc_codec *gcodec;
+
+static struct snd_soc_jack imx_hp_jack;
+static struct snd_soc_jack_pin imx_hp_jack_pins[] = {
+ {
+ .pin = "Ext Spk",
+ .mask = SND_JACK_HEADPHONE,
+ },
+};
+static struct snd_soc_jack_gpio imx_hp_jack_gpio = {
+ .name = "headphone detect",
+ .report = SND_JACK_HEADPHONE,
+ .debounce_time = 150,
+ .invert = 0,
+};
+
+static struct snd_soc_jack imx_mic_jack;
+static struct snd_soc_jack_pin imx_mic_jack_pins[] = {
+ {
+ .pin = "DMIC",
+ .mask = SND_JACK_MICROPHONE,
+ },
+};
+static struct snd_soc_jack_gpio imx_mic_jack_gpio = {
+ .name = "micphone detect",
+ .report = SND_JACK_MICROPHONE,
+ .debounce_time = 150,
+ .invert = 0,
+};
+
+static int imx_hifi_startup(struct snd_pcm_substream *substream)
+{
+ struct snd_soc_pcm_runtime *rtd = substream->private_data;
+ struct snd_soc_dai *codec_dai = rtd->codec_dai;
+ struct imx_priv *priv = &card_priv;
+ struct mxc_audio_platform_data *plat = priv->pdev->dev.platform_data;
+
+ if (!codec_dai->active)
+ plat->clock_enable(1);
+
+ return 0;
+}
+
+static void imx_hifi_shutdown(struct snd_pcm_substream *substream)
+{
+ struct snd_soc_pcm_runtime *rtd = substream->private_data;
+ struct snd_soc_dai *codec_dai = rtd->codec_dai;
+ struct imx_priv *priv = &card_priv;
+ struct mxc_audio_platform_data *plat = priv->pdev->dev.platform_data;
+
+ if (!codec_dai->active)
+ plat->clock_enable(0);
+
+ return;
+}
+
+static int check_hw_params(struct snd_pcm_substream *substream,
+ struct snd_pcm_hw_params *params)
+{
+ struct imx_priv *priv = &card_priv;
+ unsigned int channels = params_channels(params);
+ unsigned int sample_rate = params_rate(params);
+ snd_pcm_format_t sample_format = params_format(params);
+
+ substream->runtime->sample_bits =
+ snd_pcm_format_physical_width(sample_format);
+ substream->runtime->rate = sample_rate;
+ substream->runtime->format = sample_format;
+ substream->runtime->channels = channels;
+
+ if (!priv->first_stream) {
+ priv->first_stream = substream;
+ } else {
+ priv->second_stream = substream;
+
+ /* Check two sample rates of two streams */
+ if (priv->first_stream->runtime->rate !=
+ priv->second_stream->runtime->rate) {
+ pr_err("\n!KEEP THE SAME SAMPLE RATE: %d!\n",
+ priv->first_stream->runtime->rate);
+ return -EINVAL;
+ }
+
+ /* Check two sample bits of two streams */
+ if (priv->first_stream->runtime->sample_bits !=
+ priv->second_stream->runtime->sample_bits) {
+ snd_pcm_format_t first_format =
+ priv->first_stream->runtime->format;
+
+ pr_err("\n!KEEP THE SAME FORMAT: %s!\n",
+ snd_pcm_format_name(first_format));
+ return -EINVAL;
+ }
+
+ /* Check two channel numbers of two streams */
+ if (priv->first_stream->runtime->channels !=
+ priv->second_stream->runtime->channels) {
+ pr_err("\n!KEEP THE SAME CHANNEL NUMBER: %d!\n",
+ priv->first_stream->runtime->channels);
+ return -EINVAL;
+ }
+ }
+
+ return 0;
+}
+
+static int imx_hifi_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 *cpu_dai = rtd->cpu_dai;
+ struct snd_soc_dai *codec_dai = rtd->codec_dai;
+ struct imx_priv *priv = &card_priv;
+ unsigned int channels = params_channels(params);
+ unsigned int sample_rate = 44100;
+ int ret = 0;
+ u32 dai_format;
+ unsigned int pll_out;
+
+ /*
+ * WM8962 doesn't support two substreams in different parameters
+ * (i.e. different sample rates, audio formats, channel numbers)
+ * So we here check the three parameters above of two substreams
+ * if they are running in the same time.
+ */
+ ret = check_hw_params(substream, params);
+ if (ret < 0) {
+ pr_err("Failed to match hw params: %d\n", ret);
+ return ret;
+ }
+
+ dai_format = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
+ SND_SOC_DAIFMT_CBM_CFM;
+
+ /* set codec DAI configuration */
+ ret = snd_soc_dai_set_fmt(codec_dai, dai_format);
+ if (ret < 0)
+ return ret;
+
+ /* set i.MX active slot mask */
+ snd_soc_dai_set_tdm_slot(cpu_dai,
+ channels == 1 ? 0xfffffffe : 0xfffffffc,
+ channels == 1 ? 0xfffffffe : 0xfffffffc,
+ 2, 32);
+
+ dai_format = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_IF |
+ SND_SOC_DAIFMT_CBM_CFM;
+
+ /* set cpu DAI configuration */
+ ret = snd_soc_dai_set_fmt(cpu_dai, dai_format);
+ if (ret < 0)
+ return ret;
+
+ sample_rate = params_rate(params);
+ sample_format = params_format(params);
+
+ if (sample_format == SNDRV_PCM_FORMAT_S24_LE)
+ pll_out = sample_rate * 192;
+ else
+ pll_out = sample_rate * 256;
+
+ ret = snd_soc_dai_set_pll(codec_dai, WM8962_FLL_MCLK,
+ WM8962_FLL_MCLK, priv->sysclk,
+ pll_out);
+ if (ret < 0)
+ pr_err("Failed to start FLL: %d\n", ret);
+
+ ret = snd_soc_dai_set_sysclk(codec_dai,
+ WM8962_SYSCLK_FLL,
+ pll_out,
+ SND_SOC_CLOCK_IN);
+ if (ret < 0) {
+ pr_err("Failed to set SYSCLK: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+
+static int imx_hifi_hw_free(struct snd_pcm_substream *substream)
+{
+ struct snd_soc_pcm_runtime *rtd = substream->private_data;
+ struct snd_soc_dai *codec_dai = rtd->codec_dai;
+ struct imx_priv *priv = &card_priv;
+ int ret;
+
+ if (priv->first_stream == substream)
+ priv->first_stream = priv->second_stream;
+ priv->second_stream = NULL;
+
+ if (!priv->first_stream) {
+ /*
+ * wm8962 doesn't allow us to continuously setting FLL,
+ * So we set MCLK as sysclk once, which'd remove the limitation.
+ */
+ ret = snd_soc_dai_set_sysclk(codec_dai, WM8962_SYSCLK_MCLK,
+ 0, SND_SOC_CLOCK_IN);
+ if (ret < 0) {
+ pr_err("Failed to set SYSCLK: %d\n", ret);
+ return ret;
+ }
+
+ /*
+ * Continuously setting FLL would cause playback distortion.
+ * We can fix it just by mute codec after playback.
+ */
+ ret = snd_soc_dai_digital_mute(codec_dai, 1);
+ if (ret < 0) {
+ pr_err("Failed to set MUTE: %d\n", ret);
+ return ret;
+ }
+ }
+ return 0;
+}
+
+static void imx_resume_event(struct work_struct *wor)
+{
+ struct imx_priv *priv = &card_priv;
+ struct platform_device *pdev = priv->pdev;
+ struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
+ struct snd_soc_jack *jack;
+ int enable;
+ int report;
+
+ if (plat->hp_gpio != -1) {
+ jack = imx_hp_jack_gpio.jack;
+
+ enable = gpio_get_value_cansleep(imx_hp_jack_gpio.gpio);
+ if (imx_hp_jack_gpio.invert)
+ enable = !enable;
+
+ if (enable)
+ report = imx_hp_jack_gpio.report;
+ else
+ report = 0;
+
+ snd_soc_jack_report(jack, report, imx_hp_jack_gpio.report);
+ }
+
+ if (plat->mic_gpio != -1) {
+ jack = imx_mic_jack_gpio.jack;
+
+ enable = gpio_get_value_cansleep(imx_mic_jack_gpio.gpio);
+ if (imx_mic_jack_gpio.invert)
+ enable = !enable;
+
+ if (enable)
+ report = imx_mic_jack_gpio.report;
+ else
+ report = 0;
+
+ snd_soc_jack_report(jack, report, imx_mic_jack_gpio.report);
+ }
+
+ return;
+}
+
+static int imx_event_hp(struct snd_soc_dapm_widget *w,
+ struct snd_kcontrol *kcontrol, int event)
+{
+ struct imx_priv *priv = &card_priv;
+ struct platform_device *pdev = priv->pdev;
+ struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
+ char *envp[3];
+ char *buf;
+
+ if (plat->hp_gpio != -1) {
+ priv->hp_status = gpio_get_value(plat->hp_gpio);
+
+ buf = kmalloc(32, GFP_ATOMIC);
+ if (!buf) {
+ pr_err("%s kmalloc failed\n", __func__);
+ return -ENOMEM;
+ }
+
+ if (priv->hp_status != plat->hp_active_low)
+ snprintf(buf, 32, "STATE=%d", 2);
+ else
+ snprintf(buf, 32, "STATE=%d", 0);
+
+ envp[0] = "NAME=headphone";
+ envp[1] = buf;
+ envp[2] = NULL;
+ kobject_uevent_env(&pdev->dev.kobj, KOBJ_CHANGE, envp);
+ kfree(buf);
+ }
+
+ return 0;
+}
+
+static int imx_event_mic(struct snd_soc_dapm_widget *w,
+ struct snd_kcontrol *kcontrol, int event)
+{
+ struct imx_priv *priv = &card_priv;
+ struct platform_device *pdev = priv->pdev;
+ struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
+ char *envp[3];
+ char *buf;
+
+ if (plat->mic_gpio != -1) {
+ priv->amic_status = gpio_get_value(plat->mic_gpio);
+
+ buf = kmalloc(32, GFP_ATOMIC);
+ if (!buf) {
+ pr_err("%s kmalloc failed\n", __func__);
+ return -ENOMEM;
+ }
+
+ if (priv->amic_status == 0)
+ snprintf(buf, 32, "STATE=%d", 2);
+ else
+ snprintf(buf, 32, "STATE=%d", 0);
+
+ envp[0] = "NAME=amic";
+ envp[1] = buf;
+ envp[2] = NULL;
+ kobject_uevent_env(&pdev->dev.kobj, KOBJ_CHANGE, envp);
+ kfree(buf);
+ }
+
+ return 0;
+}
+
+
+static const struct snd_kcontrol_new controls[] = {
+ SOC_DAPM_PIN_SWITCH("Ext Spk"),
+};
+
+/* imx card dapm widgets */
+static const struct snd_soc_dapm_widget imx_dapm_widgets[] = {
+ SND_SOC_DAPM_HP("Headphone Jack", NULL),
+ SND_SOC_DAPM_SPK("Ext Spk", imx_event_hp),
+ SND_SOC_DAPM_MIC("AMIC", NULL),
+ SND_SOC_DAPM_MIC("DMIC", imx_event_mic),
+};
+
+/* imx machine connections to the codec pins */
+static const struct snd_soc_dapm_route audio_map[] = {
+ { "Headphone Jack", NULL, "HPOUTL" },
+ { "Headphone Jack", NULL, "HPOUTR" },
+
+ { "Ext Spk", NULL, "SPKOUTL" },
+ { "Ext Spk", NULL, "SPKOUTR" },
+
+ { "MICBIAS", NULL, "AMIC" },
+ { "IN3R", NULL, "MICBIAS" },
+
+ { "DMIC", NULL, "MICBIAS" },
+ { "DMICDAT", NULL, "DMIC" },
+
+};
+
+static ssize_t show_headphone(struct device_driver *dev, char *buf)
+{
+ struct imx_priv *priv = &card_priv;
+ struct platform_device *pdev = priv->pdev;
+ struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
+
+ /* determine whether hp is plugged in */
+ priv->hp_status = gpio_get_value(plat->hp_gpio);
+
+ if (priv->hp_status != plat->hp_active_low)
+ strcpy(buf, "headphone\n");
+ else
+ strcpy(buf, "speaker\n");
+
+ return strlen(buf);
+}
+
+static DRIVER_ATTR(headphone, S_IRUGO | S_IWUSR, show_headphone, NULL);
+
+static ssize_t show_amic(struct device_driver *dev, char *buf)
+{
+ struct imx_priv *priv = &card_priv;
+ struct platform_device *pdev = priv->pdev;
+ struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
+
+ /* determine whether amic is plugged in */
+ priv->amic_status = gpio_get_value(plat->hp_gpio);
+
+ if (priv->amic_status != plat->mic_active_low)
+ strcpy(buf, "amic\n");
+ else
+ strcpy(buf, "dmic\n");
+
+ return strlen(buf);
+}
+
+static DRIVER_ATTR(amic, S_IRUGO | S_IWUSR, show_amic, NULL);
+
+static DECLARE_DELAYED_WORK(resume_hp_event, imx_resume_event);
+
+int imx_hifi_trigger(struct snd_pcm_substream *substream, int cmd)
+{
+ struct imx_priv *priv = &card_priv;
+ struct platform_device *pdev = priv->pdev;
+ struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
+
+ if (SNDRV_PCM_TRIGGER_RESUME == cmd) {
+ if ((plat->hp_gpio != -1) || (plat->mic_gpio != -1))
+ schedule_delayed_work(&resume_hp_event,
+ msecs_to_jiffies(200));
+ }
+
+ return 0;
+}
+
+static int imx_wm8962_init(struct snd_soc_pcm_runtime *rtd)
+{
+ struct snd_soc_codec *codec = rtd->codec;
+ struct imx_priv *priv = &card_priv;
+ struct platform_device *pdev = priv->pdev;
+ struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
+ int ret = 0;
+
+ gcodec = rtd->codec;
+
+ /* Add imx specific widgets */
+ snd_soc_dapm_new_controls(&codec->dapm, imx_dapm_widgets,
+ ARRAY_SIZE(imx_dapm_widgets));
+
+ /* Set up imx specific audio path audio_map */
+ snd_soc_dapm_add_routes(&codec->dapm, audio_map, ARRAY_SIZE(audio_map));
+
+ snd_soc_dapm_enable_pin(&codec->dapm, "Headphone Jack");
+ snd_soc_dapm_enable_pin(&codec->dapm, "AMIC");
+
+ if (plat->hp_gpio != -1) {
+ imx_hp_jack_gpio.gpio = plat->hp_gpio;
+ snd_soc_jack_new(codec, "Ext Spk", SND_JACK_LINEOUT,
+ &imx_hp_jack);
+ snd_soc_jack_add_pins(&imx_hp_jack,
+ ARRAY_SIZE(imx_hp_jack_pins),
+ imx_hp_jack_pins);
+ snd_soc_jack_add_gpios(&imx_hp_jack,
+ 1, &imx_hp_jack_gpio);
+
+ ret = driver_create_file(pdev->dev.driver,
+ &driver_attr_headphone);
+ if (ret < 0) {
+ ret = -EINVAL;
+ return ret;
+ }
+ }
+
+ if (plat->mic_gpio != -1) {
+ imx_mic_jack_gpio.gpio = plat->mic_gpio;
+ snd_soc_jack_new(codec, "DMIC", SND_JACK_MICROPHONE,
+ &imx_mic_jack);
+ snd_soc_jack_add_pins(&imx_mic_jack,
+ ARRAY_SIZE(imx_mic_jack_pins),
+ imx_mic_jack_pins);
+ snd_soc_jack_add_gpios(&imx_mic_jack,
+ 1, &imx_mic_jack_gpio);
+
+ ret = driver_create_file(pdev->dev.driver,
+ &driver_attr_amic);
+ if (ret < 0) {
+ ret = -EINVAL;
+ return ret;
+ }
+ } else {
+ snd_soc_dapm_nc_pin(&codec->dapm, "DMIC");
+ }
+
+ snd_soc_dapm_sync(&codec->dapm);
+
+ return 0;
+}
+
+static struct snd_soc_ops imx_hifi_ops = {
+ .startup = imx_hifi_startup,
+ .shutdown = imx_hifi_shutdown,
+ .hw_params = imx_hifi_hw_params,
+ .hw_free = imx_hifi_hw_free,
+ .trigger = imx_hifi_trigger,
+};
+
+static struct snd_soc_dai_link imx_dai[] = {
+ {
+ .name = "HiFi",
+ .stream_name = "HiFi",
+ .codec_dai_name = "wm8962",
+ .codec_name = "wm8962.0-001a",
+ .cpu_dai_name = "imx-ssi.1",
+ .platform_name = "imx-pcm-audio.1",
+ .init = imx_wm8962_init,
+ .ops = &imx_hifi_ops,
+ },
+};
+
+static struct snd_soc_card snd_soc_card_imx = {
+ .name = "wm8962-audio",
+ .dai_link = imx_dai,
+ .num_links = ARRAY_SIZE(imx_dai),
+};
+
+static int imx_audmux_config(int slave, int master)
+{
+ unsigned int ptcr, pdcr;
+ slave = slave - 1;
+ master = master - 1;
+
+ ptcr = MXC_AUDMUX_V2_PTCR_SYN |
+ MXC_AUDMUX_V2_PTCR_TFSDIR |
+ MXC_AUDMUX_V2_PTCR_TFSEL(master) |
+ MXC_AUDMUX_V2_PTCR_TCLKDIR |
+ MXC_AUDMUX_V2_PTCR_TCSEL(master);
+ pdcr = MXC_AUDMUX_V2_PDCR_RXDSEL(master);
+ mxc_audmux_v2_configure_port(slave, ptcr, pdcr);
+
+ ptcr = MXC_AUDMUX_V2_PTCR_SYN;
+ pdcr = MXC_AUDMUX_V2_PDCR_RXDSEL(slave);
+ mxc_audmux_v2_configure_port(master, ptcr, pdcr);
+
+ return 0;
+}
+
+/*
+ * This function will register the snd_soc_pcm_link drivers.
+ */
+static int __devinit imx_wm8962_probe(struct platform_device *pdev)
+{
+
+ struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
+ struct imx_priv *priv = &card_priv;
+ int ret = 0;
+
+ priv->pdev = pdev;
+
+ imx_audmux_config(plat->src_port, plat->ext_port);
+
+ if (plat->init && plat->init()) {
+ ret = -EINVAL;
+ return ret;
+ }
+
+ priv->sysclk = plat->sysclk;
+
+ priv->first_stream = NULL;
+ priv->second_stream = NULL;
+
+ return ret;
+}
+
+static int __devexit imx_wm8962_remove(struct platform_device *pdev)
+{
+ struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
+ struct imx_priv *priv = &card_priv;
+
+ if (plat->finit)
+ plat->finit();
+
+ if (priv->hp_irq)
+ free_irq(priv->hp_irq, priv);
+ if (priv->amic_irq)
+ free_irq(priv->amic_irq, priv);
+
+ return 0;
+}
+
+static struct platform_driver imx_wm8962_driver = {
+ .probe = imx_wm8962_probe,
+ .remove = imx_wm8962_remove,
+ .driver = {
+ .name = "imx-wm8962",
+ .owner = THIS_MODULE,
+ },
+};
+
+static struct platform_device *imx_snd_device;
+
+static int __init imx_asoc_init(void)
+{
+ int ret;
+
+ ret = platform_driver_register(&imx_wm8962_driver);
+ if (ret < 0)
+ goto exit;
+
+ if (machine_is_mx6q_sabresd())
+ imx_dai[0].codec_name = "wm8962.0-001a";
+ else if (machine_is_mx6sl_arm2() | machine_is_mx6sl_evk())
+ imx_dai[0].codec_name = "wm8962.1-001a";
+
+ imx_snd_device = platform_device_alloc("soc-audio", 5);
+ if (!imx_snd_device)
+ goto err_device_alloc;
+
+ platform_set_drvdata(imx_snd_device, &snd_soc_card_imx);
+
+ ret = platform_device_add(imx_snd_device);
+
+ if (0 == ret)
+ goto exit;
+
+ platform_device_put(imx_snd_device);
+
+err_device_alloc:
+ platform_driver_unregister(&imx_wm8962_driver);
+exit:
+ return ret;
+}
+
+static void __exit imx_asoc_exit(void)
+{
+ platform_driver_unregister(&imx_wm8962_driver);
+ platform_device_unregister(imx_snd_device);
+}
+
+module_init(imx_asoc_init);
+module_exit(imx_asoc_exit);
+
+/* Module information */
+MODULE_DESCRIPTION("ALSA SoC imx wm8962");
+MODULE_LICENSE("GPL");