/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 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; if (!priv->first_stream) priv->first_stream = substream; else priv->second_stream = substream; 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");