|  | // SPDX-License-Identifier: GPL-2.0-only | 
|  | /* | 
|  | * Copyright (C) ST-Ericsson SA 2012 | 
|  | * | 
|  | * Author: Ola Lilja <ola.o.lilja@stericsson.com>, | 
|  | *         Kristoffer Karlsson <kristoffer.karlsson@stericsson.com> | 
|  | *         for ST-Ericsson. | 
|  | */ | 
|  |  | 
|  | #include <linux/module.h> | 
|  | #include <linux/device.h> | 
|  | #include <linux/io.h> | 
|  | #include <linux/clk.h> | 
|  | #include <linux/mutex.h> | 
|  |  | 
|  | #include <sound/soc.h> | 
|  | #include <sound/soc-dapm.h> | 
|  | #include <sound/pcm.h> | 
|  | #include <sound/pcm_params.h> | 
|  |  | 
|  | #include "ux500_pcm.h" | 
|  | #include "ux500_msp_dai.h" | 
|  | #include "mop500_ab8500.h" | 
|  | #include "../codecs/ab8500-codec.h" | 
|  |  | 
|  | #define TX_SLOT_MONO	0x0008 | 
|  | #define TX_SLOT_STEREO	0x000a | 
|  | #define RX_SLOT_MONO	0x0001 | 
|  | #define RX_SLOT_STEREO	0x0003 | 
|  | #define TX_SLOT_8CH	0x00FF | 
|  | #define RX_SLOT_8CH	0x00FF | 
|  |  | 
|  | #define DEF_TX_SLOTS	TX_SLOT_STEREO | 
|  | #define DEF_RX_SLOTS	RX_SLOT_MONO | 
|  |  | 
|  | #define DRIVERMODE_NORMAL	0 | 
|  | #define DRIVERMODE_CODEC_ONLY	1 | 
|  |  | 
|  | /* Slot configuration */ | 
|  | static unsigned int tx_slots = DEF_TX_SLOTS; | 
|  | static unsigned int rx_slots = DEF_RX_SLOTS; | 
|  |  | 
|  | /* Configuration consistency parameters */ | 
|  | static DEFINE_MUTEX(mop500_ab8500_params_lock); | 
|  | static unsigned long mop500_ab8500_usage; | 
|  | static int mop500_ab8500_rate; | 
|  | static int mop500_ab8500_channels; | 
|  |  | 
|  | /* Clocks */ | 
|  | static const char * const enum_mclk[] = { | 
|  | "SYSCLK", | 
|  | "ULPCLK" | 
|  | }; | 
|  | enum mclk { | 
|  | MCLK_SYSCLK, | 
|  | MCLK_ULPCLK, | 
|  | }; | 
|  |  | 
|  | static SOC_ENUM_SINGLE_EXT_DECL(soc_enum_mclk, enum_mclk); | 
|  |  | 
|  | /* Private data for machine-part MOP500<->AB8500 */ | 
|  | struct mop500_ab8500_drvdata { | 
|  | /* Clocks */ | 
|  | enum mclk mclk_sel; | 
|  | struct clk *clk_ptr_intclk; | 
|  | struct clk *clk_ptr_sysclk; | 
|  | struct clk *clk_ptr_ulpclk; | 
|  | }; | 
|  |  | 
|  | static inline const char *get_mclk_str(enum mclk mclk_sel) | 
|  | { | 
|  | switch (mclk_sel) { | 
|  | case MCLK_SYSCLK: | 
|  | return "SYSCLK"; | 
|  | case MCLK_ULPCLK: | 
|  | return "ULPCLK"; | 
|  | default: | 
|  | return "Unknown"; | 
|  | } | 
|  | } | 
|  |  | 
|  | static int mop500_ab8500_set_mclk(struct device *dev, | 
|  | struct mop500_ab8500_drvdata *drvdata) | 
|  | { | 
|  | int status; | 
|  | struct clk *clk_ptr; | 
|  |  | 
|  | if (IS_ERR(drvdata->clk_ptr_intclk)) { | 
|  | dev_err(dev, | 
|  | "%s: ERROR: intclk not initialized!\n", __func__); | 
|  | return -EIO; | 
|  | } | 
|  |  | 
|  | switch (drvdata->mclk_sel) { | 
|  | case MCLK_SYSCLK: | 
|  | clk_ptr = drvdata->clk_ptr_sysclk; | 
|  | break; | 
|  | case MCLK_ULPCLK: | 
|  | clk_ptr = drvdata->clk_ptr_ulpclk; | 
|  | break; | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | if (IS_ERR(clk_ptr)) { | 
|  | dev_err(dev, "%s: ERROR: %s not initialized!\n", __func__, | 
|  | get_mclk_str(drvdata->mclk_sel)); | 
|  | return -EIO; | 
|  | } | 
|  |  | 
|  | status = clk_set_parent(drvdata->clk_ptr_intclk, clk_ptr); | 
|  | if (status) | 
|  | dev_err(dev, | 
|  | "%s: ERROR: Setting intclk parent to %s failed (ret = %d)!", | 
|  | __func__, get_mclk_str(drvdata->mclk_sel), status); | 
|  | else | 
|  | dev_dbg(dev, | 
|  | "%s: intclk parent changed to %s.\n", | 
|  | __func__, get_mclk_str(drvdata->mclk_sel)); | 
|  |  | 
|  | return status; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Control-events | 
|  | */ | 
|  |  | 
|  | static int mclk_input_control_get(struct snd_kcontrol *kcontrol, | 
|  | struct snd_ctl_elem_value *ucontrol) | 
|  | { | 
|  | struct snd_soc_card *card = snd_kcontrol_chip(kcontrol); | 
|  | struct mop500_ab8500_drvdata *drvdata = | 
|  | snd_soc_card_get_drvdata(card); | 
|  |  | 
|  | ucontrol->value.enumerated.item[0] = drvdata->mclk_sel; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int mclk_input_control_put(struct snd_kcontrol *kcontrol, | 
|  | struct snd_ctl_elem_value *ucontrol) | 
|  | { | 
|  | struct snd_soc_card *card = snd_kcontrol_chip(kcontrol); | 
|  | struct mop500_ab8500_drvdata *drvdata = | 
|  | snd_soc_card_get_drvdata(card); | 
|  | unsigned int val = ucontrol->value.enumerated.item[0]; | 
|  |  | 
|  | if (val > (unsigned int)MCLK_ULPCLK) | 
|  | return -EINVAL; | 
|  | if (drvdata->mclk_sel == val) | 
|  | return 0; | 
|  |  | 
|  | drvdata->mclk_sel = val; | 
|  |  | 
|  | return 1; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Controls | 
|  | */ | 
|  |  | 
|  | static struct snd_kcontrol_new mop500_ab8500_ctrls[] = { | 
|  | SOC_ENUM_EXT("Master Clock Select", | 
|  | soc_enum_mclk, | 
|  | mclk_input_control_get, mclk_input_control_put), | 
|  | SOC_DAPM_PIN_SWITCH("Headset Left"), | 
|  | SOC_DAPM_PIN_SWITCH("Headset Right"), | 
|  | SOC_DAPM_PIN_SWITCH("Earpiece"), | 
|  | SOC_DAPM_PIN_SWITCH("Speaker Left"), | 
|  | SOC_DAPM_PIN_SWITCH("Speaker Right"), | 
|  | SOC_DAPM_PIN_SWITCH("LineOut Left"), | 
|  | SOC_DAPM_PIN_SWITCH("LineOut Right"), | 
|  | SOC_DAPM_PIN_SWITCH("Vibra 1"), | 
|  | SOC_DAPM_PIN_SWITCH("Vibra 2"), | 
|  | SOC_DAPM_PIN_SWITCH("Mic 1"), | 
|  | SOC_DAPM_PIN_SWITCH("Mic 2"), | 
|  | SOC_DAPM_PIN_SWITCH("LineIn Left"), | 
|  | SOC_DAPM_PIN_SWITCH("LineIn Right"), | 
|  | SOC_DAPM_PIN_SWITCH("DMic 1"), | 
|  | SOC_DAPM_PIN_SWITCH("DMic 2"), | 
|  | SOC_DAPM_PIN_SWITCH("DMic 3"), | 
|  | SOC_DAPM_PIN_SWITCH("DMic 4"), | 
|  | SOC_DAPM_PIN_SWITCH("DMic 5"), | 
|  | SOC_DAPM_PIN_SWITCH("DMic 6"), | 
|  | }; | 
|  |  | 
|  | /* ASoC */ | 
|  |  | 
|  | static int mop500_ab8500_startup(struct snd_pcm_substream *substream) | 
|  | { | 
|  | struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); | 
|  |  | 
|  | /* Set audio-clock source */ | 
|  | return mop500_ab8500_set_mclk(rtd->card->dev, | 
|  | snd_soc_card_get_drvdata(rtd->card)); | 
|  | } | 
|  |  | 
|  | static void mop500_ab8500_shutdown(struct snd_pcm_substream *substream) | 
|  | { | 
|  | struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); | 
|  | struct device *dev = rtd->card->dev; | 
|  |  | 
|  | dev_dbg(dev, "%s: Enter\n", __func__); | 
|  |  | 
|  | /* Reset slots configuration to default(s) */ | 
|  | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) | 
|  | tx_slots = DEF_TX_SLOTS; | 
|  | else | 
|  | rx_slots = DEF_RX_SLOTS; | 
|  | } | 
|  |  | 
|  | static int mop500_ab8500_hw_params(struct snd_pcm_substream *substream, | 
|  | struct snd_pcm_hw_params *params) | 
|  | { | 
|  | struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); | 
|  | struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, 0); | 
|  | struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0); | 
|  | struct device *dev = rtd->card->dev; | 
|  | unsigned int fmt; | 
|  | int channels, ret = 0, driver_mode, slots; | 
|  | unsigned int sw_codec, sw_cpu; | 
|  | bool is_playback; | 
|  |  | 
|  | dev_dbg(dev, "%s: Enter\n", __func__); | 
|  |  | 
|  | dev_dbg(dev, "%s: substream->pcm->name = %s\n" | 
|  | "substream->pcm->id = %s.\n" | 
|  | "substream->name = %s.\n" | 
|  | "substream->number = %d.\n", | 
|  | __func__, | 
|  | substream->pcm->name, | 
|  | substream->pcm->id, | 
|  | substream->name, | 
|  | substream->number); | 
|  |  | 
|  | /* Ensure configuration consistency between DAIs */ | 
|  | mutex_lock(&mop500_ab8500_params_lock); | 
|  | if (mop500_ab8500_usage) { | 
|  | if (mop500_ab8500_rate != params_rate(params) || | 
|  | mop500_ab8500_channels != params_channels(params)) { | 
|  | mutex_unlock(&mop500_ab8500_params_lock); | 
|  | return -EBUSY; | 
|  | } | 
|  | } else { | 
|  | mop500_ab8500_rate = params_rate(params); | 
|  | mop500_ab8500_channels = params_channels(params); | 
|  | } | 
|  | __set_bit(cpu_dai->id, &mop500_ab8500_usage); | 
|  | mutex_unlock(&mop500_ab8500_params_lock); | 
|  |  | 
|  | channels = params_channels(params); | 
|  |  | 
|  | switch (params_format(params)) { | 
|  | case SNDRV_PCM_FORMAT_S32_LE: | 
|  | sw_cpu = 32; | 
|  | break; | 
|  |  | 
|  | case SNDRV_PCM_FORMAT_S16_LE: | 
|  | sw_cpu = 16; | 
|  | break; | 
|  |  | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | /* Setup codec depending on driver-mode */ | 
|  | if (channels == 8) | 
|  | driver_mode = DRIVERMODE_CODEC_ONLY; | 
|  | else | 
|  | driver_mode = DRIVERMODE_NORMAL; | 
|  | dev_dbg(dev, "%s: Driver-mode: %s.\n", __func__, | 
|  | (driver_mode == DRIVERMODE_NORMAL) ? "NORMAL" : "CODEC_ONLY"); | 
|  |  | 
|  | /* Setup format */ | 
|  |  | 
|  | if (driver_mode == DRIVERMODE_NORMAL) { | 
|  | fmt = SND_SOC_DAIFMT_DSP_A | | 
|  | SND_SOC_DAIFMT_CBM_CFM | | 
|  | SND_SOC_DAIFMT_NB_NF | | 
|  | SND_SOC_DAIFMT_CONT; | 
|  | } else { | 
|  | fmt = SND_SOC_DAIFMT_DSP_A | | 
|  | SND_SOC_DAIFMT_CBM_CFM | | 
|  | SND_SOC_DAIFMT_NB_NF | | 
|  | SND_SOC_DAIFMT_GATED; | 
|  | } | 
|  |  | 
|  | ret = snd_soc_runtime_set_dai_fmt(rtd, fmt); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | /* Setup TDM-slots */ | 
|  |  | 
|  | is_playback = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK); | 
|  | switch (channels) { | 
|  | case 1: | 
|  | slots = 16; | 
|  | tx_slots = (is_playback) ? TX_SLOT_MONO : 0; | 
|  | rx_slots = (is_playback) ? 0 : RX_SLOT_MONO; | 
|  | break; | 
|  | case 2: | 
|  | slots = 16; | 
|  | tx_slots = (is_playback) ? TX_SLOT_STEREO : 0; | 
|  | rx_slots = (is_playback) ? 0 : RX_SLOT_STEREO; | 
|  | break; | 
|  | case 8: | 
|  | slots = 16; | 
|  | tx_slots = (is_playback) ? TX_SLOT_8CH : 0; | 
|  | rx_slots = (is_playback) ? 0 : RX_SLOT_8CH; | 
|  | break; | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | if (driver_mode == DRIVERMODE_NORMAL) | 
|  | sw_codec = sw_cpu; | 
|  | else | 
|  | sw_codec = 20; | 
|  |  | 
|  | dev_dbg(dev, "%s: CPU-DAI TDM: TX=0x%04X RX=0x%04x\n", __func__, | 
|  | tx_slots, rx_slots); | 
|  | ret = snd_soc_dai_set_tdm_slot(cpu_dai, tx_slots, rx_slots, slots, | 
|  | sw_cpu); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | dev_dbg(dev, "%s: CODEC-DAI TDM: TX=0x%04X RX=0x%04x\n", __func__, | 
|  | tx_slots, rx_slots); | 
|  | ret = snd_soc_dai_set_tdm_slot(codec_dai, tx_slots, rx_slots, slots, | 
|  | sw_codec); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int mop500_ab8500_hw_free(struct snd_pcm_substream *substream) | 
|  | { | 
|  | struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); | 
|  | struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0); | 
|  |  | 
|  | mutex_lock(&mop500_ab8500_params_lock); | 
|  | __clear_bit(cpu_dai->id, &mop500_ab8500_usage); | 
|  | mutex_unlock(&mop500_ab8500_params_lock); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | const struct snd_soc_ops mop500_ab8500_ops[] = { | 
|  | { | 
|  | .hw_params = mop500_ab8500_hw_params, | 
|  | .hw_free = mop500_ab8500_hw_free, | 
|  | .startup = mop500_ab8500_startup, | 
|  | .shutdown = mop500_ab8500_shutdown, | 
|  | } | 
|  | }; | 
|  |  | 
|  | int mop500_ab8500_machine_init(struct snd_soc_pcm_runtime *rtd) | 
|  | { | 
|  | struct snd_soc_dapm_context *dapm = &rtd->card->dapm; | 
|  | struct device *dev = rtd->card->dev; | 
|  | struct mop500_ab8500_drvdata *drvdata; | 
|  | int ret; | 
|  |  | 
|  | dev_dbg(dev, "%s Enter.\n", __func__); | 
|  |  | 
|  | /* Create driver private-data struct */ | 
|  | drvdata = devm_kzalloc(dev, sizeof(struct mop500_ab8500_drvdata), | 
|  | GFP_KERNEL); | 
|  |  | 
|  | if (!drvdata) | 
|  | return -ENOMEM; | 
|  |  | 
|  | snd_soc_card_set_drvdata(rtd->card, drvdata); | 
|  |  | 
|  | /* Setup clocks */ | 
|  |  | 
|  | drvdata->clk_ptr_sysclk = clk_get(dev, "sysclk"); | 
|  | if (IS_ERR(drvdata->clk_ptr_sysclk)) | 
|  | dev_warn(dev, "%s: WARNING: clk_get failed for 'sysclk'!\n", | 
|  | __func__); | 
|  | drvdata->clk_ptr_ulpclk = clk_get(dev, "ulpclk"); | 
|  | if (IS_ERR(drvdata->clk_ptr_ulpclk)) | 
|  | dev_warn(dev, "%s: WARNING: clk_get failed for 'ulpclk'!\n", | 
|  | __func__); | 
|  | drvdata->clk_ptr_intclk = clk_get(dev, "intclk"); | 
|  | if (IS_ERR(drvdata->clk_ptr_intclk)) | 
|  | dev_warn(dev, "%s: WARNING: clk_get failed for 'intclk'!\n", | 
|  | __func__); | 
|  |  | 
|  | /* Set intclk default parent to ulpclk */ | 
|  | drvdata->mclk_sel = MCLK_ULPCLK; | 
|  | ret = mop500_ab8500_set_mclk(dev, drvdata); | 
|  | if (ret < 0) | 
|  | dev_warn(dev, "%s: WARNING: mop500_ab8500_set_mclk!\n", | 
|  | __func__); | 
|  |  | 
|  | drvdata->mclk_sel = MCLK_ULPCLK; | 
|  |  | 
|  | /* Add controls */ | 
|  | ret = snd_soc_add_card_controls(rtd->card, mop500_ab8500_ctrls, | 
|  | ARRAY_SIZE(mop500_ab8500_ctrls)); | 
|  | if (ret < 0) { | 
|  | pr_err("%s: Failed to add machine-controls (%d)!\n", | 
|  | __func__, ret); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | ret = snd_soc_dapm_disable_pin(dapm, "Earpiece"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "Speaker Left"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "Speaker Right"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "LineOut Left"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "LineOut Right"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "Vibra 1"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "Vibra 2"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "Mic 1"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "Mic 2"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "LineIn Left"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "LineIn Right"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "DMic 1"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "DMic 2"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "DMic 3"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "DMic 4"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "DMic 5"); | 
|  | ret |= snd_soc_dapm_disable_pin(dapm, "DMic 6"); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | void mop500_ab8500_remove(struct snd_soc_card *card) | 
|  | { | 
|  | struct mop500_ab8500_drvdata *drvdata = snd_soc_card_get_drvdata(card); | 
|  |  | 
|  | clk_put(drvdata->clk_ptr_sysclk); | 
|  | clk_put(drvdata->clk_ptr_ulpclk); | 
|  | clk_put(drvdata->clk_ptr_intclk); | 
|  |  | 
|  | snd_soc_card_set_drvdata(card, drvdata); | 
|  | } |