diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-11 08:27:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-11 08:27:49 +0000 |
commit | ace9429bb58fd418f0c81d4c2835699bddf6bde6 (patch) | |
tree | b2d64bc10158fdd5497876388cd68142ca374ed3 /sound/soc/samsung | |
parent | Initial commit. (diff) | |
download | linux-ace9429bb58fd418f0c81d4c2835699bddf6bde6.tar.xz linux-ace9429bb58fd418f0c81d4c2835699bddf6bde6.zip |
Adding upstream version 6.6.15.upstream/6.6.15
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sound/soc/samsung')
27 files changed, 8771 insertions, 0 deletions
diff --git a/sound/soc/samsung/Kconfig b/sound/soc/samsung/Kconfig new file mode 100644 index 0000000000..93c2b1b08d --- /dev/null +++ b/sound/soc/samsung/Kconfig @@ -0,0 +1,149 @@ +# SPDX-License-Identifier: GPL-2.0-only +menuconfig SND_SOC_SAMSUNG + tristate "ASoC support for Samsung" + depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST + depends on COMMON_CLK + select SND_SOC_GENERIC_DMAENGINE_PCM + help + Say Y or M if you want to add support for codecs attached to + the Samsung SoCs' Audio interfaces. You will also need to + select the audio interfaces to support below. + +if SND_SOC_SAMSUNG + +config SND_SAMSUNG_PCM + tristate "Samsung PCM interface support" + +config SND_SAMSUNG_SPDIF + tristate "Samsung SPDIF transmitter support" + select SND_SOC_SPDIF + +config SND_SAMSUNG_I2S + tristate "Samsung I2S interface support" + +config SND_SOC_SAMSUNG_SMDK_WM8994 + tristate "SoC I2S Audio support for WM8994 on SMDK" + depends on I2C=y + select MFD_WM8994 + select SND_SOC_WM8994 + select SND_SAMSUNG_I2S + help + Say Y if you want to add support for SoC audio on the SMDKs. + +config SND_SOC_SAMSUNG_SMDK_SPDIF + tristate "SoC S/PDIF Audio support for SMDK" + select SND_SAMSUNG_SPDIF + help + Say Y if you want to add support for SoC S/PDIF audio on the SMDK. + +config SND_SOC_SMDK_WM8994_PCM + tristate "SoC PCM Audio support for WM8994 on SMDK" + depends on I2C=y + select MFD_WM8994 + select SND_SOC_WM8994 + select SND_SAMSUNG_PCM + help + Say Y if you want to add support for SoC audio on the SMDK + +config SND_SOC_SPEYSIDE + tristate "Audio support for Wolfson Speyside" + depends on I2C && SPI_MASTER + depends on MACH_WLF_CRAGG_6410 || COMPILE_TEST + select SND_SAMSUNG_I2S + select SND_SOC_WM8996 + select SND_SOC_WM9081 + select SND_SOC_WM0010 + select SND_SOC_WM1250_EV1 + +config SND_SOC_TOBERMORY + tristate "Audio support for Wolfson Tobermory" + depends on INPUT && I2C + depends on MACH_WLF_CRAGG_6410 || COMPILE_TEST + select SND_SAMSUNG_I2S + select SND_SOC_WM8962 + +config SND_SOC_BELLS + tristate "Audio support for Wolfson Bells" + depends on MFD_ARIZONA && MFD_WM5102 && MFD_WM5110 && I2C && SPI_MASTER + depends on MACH_WLF_CRAGG_6410 || COMPILE_TEST + select SND_SAMSUNG_I2S + select SND_SOC_WM5102 + select SND_SOC_WM5110 + select SND_SOC_WM9081 + select SND_SOC_WM0010 + select SND_SOC_WM1250_EV1 + +config SND_SOC_LOWLAND + tristate "Audio support for Wolfson Lowland" + depends on I2C + depends on MACH_WLF_CRAGG_6410 || COMPILE_TEST + select SND_SAMSUNG_I2S + select SND_SOC_WM5100 + select SND_SOC_WM9081 + +config SND_SOC_LITTLEMILL + tristate "Audio support for Wolfson Littlemill" + depends on I2C + depends on MACH_WLF_CRAGG_6410 || COMPILE_TEST + select SND_SAMSUNG_I2S + select MFD_WM8994 + select SND_SOC_WM8994 + +config SND_SOC_SNOW + tristate "Audio support for Google Snow boards" + depends on I2C + select SND_SOC_MAX98090 + select SND_SOC_MAX98095 + select SND_SAMSUNG_I2S + help + Say Y if you want to add audio support for various Snow + boards based on Exynos5 series of SoCs. + +config SND_SOC_ODROID + tristate "Audio support for Odroid XU3/XU4" + depends on SND_SOC_SAMSUNG && I2C + select SND_SOC_MAX98090 + select SND_SAMSUNG_I2S + help + Say Y here to enable audio support for the Odroid XU3/XU4. + +config SND_SOC_ARNDALE + tristate "Audio support for Arndale Board" + depends on I2C + select SND_SAMSUNG_I2S + select SND_SOC_RT5631 + select MFD_WM8994 + select SND_SOC_WM8994 + +config SND_SOC_SAMSUNG_TM2_WM5110 + tristate "SoC I2S Audio support for WM5110 on TM2 board" + depends on SND_SOC_SAMSUNG && MFD_ARIZONA && MFD_WM5110 && I2C && SPI_MASTER + depends on GPIOLIB || COMPILE_TEST + 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. + +config SND_SOC_SAMSUNG_ARIES_WM8994 + tristate "SoC I2S Audio support for WM8994 on Aries" + depends on SND_SOC_SAMSUNG && MFD_WM8994 && IIO && EXTCON + select SND_SOC_BT_SCO + select SND_SOC_WM8994 + select SND_SAMSUNG_I2S + help + Say Y if you want to add support for SoC audio on Aries boards, + which has a WM8994 codec connected to a BT codec, a cellular + modem, and the Samsung I2S controller. Jack detection is done + via ADC, GPIOs, and an extcon device. Switching between the Mic + and TV-Out path is also handled. + +config SND_SOC_SAMSUNG_MIDAS_WM1811 + tristate "SoC I2S Audio support for Midas boards" + depends on SND_SOC_SAMSUNG + select SND_SAMSUNG_I2S + select SND_SOC_WM8994 + help + Say Y if you want to add support for SoC audio on the Midas boards. + +endif #SND_SOC_SAMSUNG diff --git a/sound/soc/samsung/Makefile b/sound/soc/samsung/Makefile new file mode 100644 index 0000000000..f5d327b90a --- /dev/null +++ b/sound/soc/samsung/Makefile @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: GPL-2.0 +# S3c24XX Platform Support +snd-soc-s3c-dma-objs := dmaengine.o +snd-soc-idma-objs := idma.o +snd-soc-samsung-spdif-objs := spdif.o +snd-soc-pcm-objs := pcm.o +snd-soc-i2s-objs := i2s.o + +obj-$(CONFIG_SND_SOC_SAMSUNG) += snd-soc-s3c-dma.o +obj-$(CONFIG_SND_SAMSUNG_SPDIF) += snd-soc-samsung-spdif.o +obj-$(CONFIG_SND_SAMSUNG_PCM) += snd-soc-pcm.o +obj-$(CONFIG_SND_SAMSUNG_I2S) += snd-soc-i2s.o +obj-$(CONFIG_SND_SAMSUNG_I2S) += snd-soc-idma.o + +# S3C24XX Machine Support +snd-soc-smdk-wm8994-objs := smdk_wm8994.o +snd-soc-snow-objs := snow.o +snd-soc-smdk-spdif-objs := smdk_spdif.o +snd-soc-smdk-wm8994pcm-objs := smdk_wm8994pcm.o +snd-soc-speyside-objs := speyside.o +snd-soc-tobermory-objs := tobermory.o +snd-soc-lowland-objs := lowland.o +snd-soc-littlemill-objs := littlemill.o +snd-soc-bells-objs := bells.o +snd-soc-odroid-objs := odroid.o +snd-soc-arndale-objs := arndale.o +snd-soc-tm2-wm5110-objs := tm2_wm5110.o +snd-soc-aries-wm8994-objs := aries_wm8994.o +snd-soc-midas-wm1811-objs := midas_wm1811.o + +obj-$(CONFIG_SND_SOC_SAMSUNG_SMDK_WM8994) += snd-soc-smdk-wm8994.o +obj-$(CONFIG_SND_SOC_SNOW) += snd-soc-snow.o +obj-$(CONFIG_SND_SOC_SAMSUNG_SMDK_SPDIF) += snd-soc-smdk-spdif.o +obj-$(CONFIG_SND_SOC_SMDK_WM8994_PCM) += snd-soc-smdk-wm8994pcm.o +obj-$(CONFIG_SND_SOC_SPEYSIDE) += snd-soc-speyside.o +obj-$(CONFIG_SND_SOC_TOBERMORY) += snd-soc-tobermory.o +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_ODROID) += snd-soc-odroid.o +obj-$(CONFIG_SND_SOC_ARNDALE) += snd-soc-arndale.o +obj-$(CONFIG_SND_SOC_SAMSUNG_TM2_WM5110) += snd-soc-tm2-wm5110.o +obj-$(CONFIG_SND_SOC_SAMSUNG_ARIES_WM8994) += snd-soc-aries-wm8994.o +obj-$(CONFIG_SND_SOC_SAMSUNG_MIDAS_WM1811) += snd-soc-midas-wm1811.o diff --git a/sound/soc/samsung/aries_wm8994.c b/sound/soc/samsung/aries_wm8994.c new file mode 100644 index 0000000000..dd3cd2c964 --- /dev/null +++ b/sound/soc/samsung/aries_wm8994.c @@ -0,0 +1,703 @@ +// SPDX-License-Identifier: GPL-2.0+ +#include <linux/extcon.h> +#include <linux/iio/consumer.h> +#include <linux/input-event-codes.h> +#include <linux/mfd/wm8994/registers.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/of_gpio.h> +#include <linux/regulator/consumer.h> +#include <sound/jack.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> + +#include "i2s.h" +#include "../codecs/wm8994.h" + +#define ARIES_MCLK1_FREQ 24000000 + +struct aries_wm8994_variant { + unsigned int modem_dai_fmt; + bool has_fm_radio; +}; + +struct aries_wm8994_data { + struct extcon_dev *usb_extcon; + struct regulator *reg_main_micbias; + struct regulator *reg_headset_micbias; + struct gpio_desc *gpio_headset_detect; + struct gpio_desc *gpio_headset_key; + struct gpio_desc *gpio_earpath_sel; + struct iio_channel *adc; + const struct aries_wm8994_variant *variant; +}; + +/* USB dock */ +static struct snd_soc_jack aries_dock; + +static struct snd_soc_jack_pin dock_pins[] = { + { + .pin = "LINE", + .mask = SND_JACK_LINEOUT, + }, +}; + +static int aries_extcon_notifier(struct notifier_block *this, + unsigned long connected, void *_cmd) +{ + if (connected) + snd_soc_jack_report(&aries_dock, SND_JACK_LINEOUT, + SND_JACK_LINEOUT); + else + snd_soc_jack_report(&aries_dock, 0, SND_JACK_LINEOUT); + + return NOTIFY_DONE; +} + +static struct notifier_block aries_extcon_notifier_block = { + .notifier_call = aries_extcon_notifier, +}; + +/* Headset jack */ +static struct snd_soc_jack aries_headset; + +static struct snd_soc_jack_pin jack_pins[] = { + { + .pin = "HP", + .mask = SND_JACK_HEADPHONE, + }, { + .pin = "Headset Mic", + .mask = SND_JACK_MICROPHONE, + }, +}; + +static struct snd_soc_jack_zone headset_zones[] = { + { + .min_mv = 0, + .max_mv = 241, + .jack_type = SND_JACK_HEADPHONE, + }, { + .min_mv = 242, + .max_mv = 2980, + .jack_type = SND_JACK_HEADSET, + }, { + .min_mv = 2981, + .max_mv = UINT_MAX, + .jack_type = SND_JACK_HEADPHONE, + }, +}; + +static irqreturn_t headset_det_irq_thread(int irq, void *data) +{ + struct aries_wm8994_data *priv = (struct aries_wm8994_data *) data; + int ret = 0; + int time_left_ms = 300; + int adc; + + while (time_left_ms > 0) { + if (!gpiod_get_value(priv->gpio_headset_detect)) { + snd_soc_jack_report(&aries_headset, 0, + SND_JACK_HEADSET); + gpiod_set_value(priv->gpio_earpath_sel, 0); + return IRQ_HANDLED; + } + msleep(20); + time_left_ms -= 20; + } + + /* Temporarily enable micbias and earpath selector */ + ret = regulator_enable(priv->reg_headset_micbias); + if (ret) + pr_err("%s failed to enable micbias: %d", __func__, ret); + + gpiod_set_value(priv->gpio_earpath_sel, 1); + + ret = iio_read_channel_processed(priv->adc, &adc); + if (ret < 0) { + /* failed to read ADC, so assume headphone */ + pr_err("%s failed to read ADC, assuming headphones", __func__); + snd_soc_jack_report(&aries_headset, SND_JACK_HEADPHONE, + SND_JACK_HEADSET); + } else { + snd_soc_jack_report(&aries_headset, + snd_soc_jack_get_type(&aries_headset, adc), + SND_JACK_HEADSET); + } + + ret = regulator_disable(priv->reg_headset_micbias); + if (ret) + pr_err("%s failed disable micbias: %d", __func__, ret); + + /* Disable earpath selector when no mic connected */ + if (!(aries_headset.status & SND_JACK_MICROPHONE)) + gpiod_set_value(priv->gpio_earpath_sel, 0); + + return IRQ_HANDLED; +} + +static int headset_button_check(void *data) +{ + struct aries_wm8994_data *priv = (struct aries_wm8994_data *) data; + + /* Filter out keypresses when 4 pole jack not detected */ + if (gpiod_get_value_cansleep(priv->gpio_headset_key) && + aries_headset.status & SND_JACK_MICROPHONE) + return SND_JACK_BTN_0; + + return 0; +} + +static struct snd_soc_jack_gpio headset_button_gpio[] = { + { + .name = "Media Button", + .report = SND_JACK_BTN_0, + .debounce_time = 30, + .jack_status_check = headset_button_check, + }, +}; + +static int aries_spk_cfg(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_card *card = w->dapm->card; + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_component *component; + int ret = 0; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[0]); + component = asoc_rtd_to_codec(rtd, 0)->component; + + /** + * We have an odd setup - the SPKMODE pin is pulled up so + * we only have access to the left side SPK configs, + * but SPKOUTR isn't bridged so when playing back in + * stereo, we only get the left hand channel. The only + * option we're left with is to force the AIF into mono + * mode. + */ + switch (event) { + case SND_SOC_DAPM_POST_PMU: + ret = snd_soc_component_update_bits(component, + WM8994_AIF1_DAC1_FILTERS_1, + WM8994_AIF1DAC1_MONO, WM8994_AIF1DAC1_MONO); + break; + case SND_SOC_DAPM_PRE_PMD: + ret = snd_soc_component_update_bits(component, + WM8994_AIF1_DAC1_FILTERS_1, + WM8994_AIF1DAC1_MONO, 0); + break; + } + + return ret; +} + +static int aries_main_bias(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_card *card = w->dapm->card; + struct aries_wm8994_data *priv = snd_soc_card_get_drvdata(card); + int ret = 0; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + ret = regulator_enable(priv->reg_main_micbias); + break; + case SND_SOC_DAPM_POST_PMD: + ret = regulator_disable(priv->reg_main_micbias); + break; + } + + return ret; +} + +static int aries_headset_bias(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_card *card = w->dapm->card; + struct aries_wm8994_data *priv = snd_soc_card_get_drvdata(card); + int ret = 0; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + ret = regulator_enable(priv->reg_headset_micbias); + break; + case SND_SOC_DAPM_POST_PMD: + ret = regulator_disable(priv->reg_headset_micbias); + break; + } + + return ret; +} + +static const struct snd_kcontrol_new aries_controls[] = { + SOC_DAPM_PIN_SWITCH("Modem In"), + SOC_DAPM_PIN_SWITCH("Modem Out"), +}; + +static const struct snd_soc_dapm_widget aries_dapm_widgets[] = { + SND_SOC_DAPM_HP("HP", NULL), + + SND_SOC_DAPM_SPK("SPK", aries_spk_cfg), + SND_SOC_DAPM_SPK("RCV", NULL), + + SND_SOC_DAPM_LINE("LINE", NULL), + + SND_SOC_DAPM_MIC("Main Mic", aries_main_bias), + SND_SOC_DAPM_MIC("Headset Mic", aries_headset_bias), + + SND_SOC_DAPM_MIC("Bluetooth Mic", NULL), + SND_SOC_DAPM_SPK("Bluetooth SPK", NULL), + + SND_SOC_DAPM_LINE("Modem In", NULL), + SND_SOC_DAPM_LINE("Modem Out", NULL), + + /* This must be last as it is conditionally not used */ + SND_SOC_DAPM_LINE("FM In", NULL), +}; + +static int aries_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, 0); + unsigned int pll_out; + int ret; + + /* AIF1CLK should be >=3MHz for optimal performance */ + if (params_width(params) == 24) + pll_out = params_rate(params) * 384; + else if (params_rate(params) == 8000 || params_rate(params) == 11025) + pll_out = params_rate(params) * 512; + else + pll_out = params_rate(params) * 256; + + ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, WM8994_FLL_SRC_MCLK1, + ARIES_MCLK1_FREQ, pll_out); + if (ret < 0) + return ret; + + ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1, + pll_out, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + return 0; +} + +static int aries_hw_free(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, 0); + int ret; + + /* Switch sysclk to MCLK1 */ + ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_MCLK1, + ARIES_MCLK1_FREQ, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + /* Stop PLL */ + ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, WM8994_FLL_SRC_MCLK1, + ARIES_MCLK1_FREQ, 0); + if (ret < 0) + return ret; + + return 0; +} + +/* + * Main DAI operations + */ +static const struct snd_soc_ops aries_ops = { + .hw_params = aries_hw_params, + .hw_free = aries_hw_free, +}; + +static int aries_baseband_init(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, 0); + unsigned int pll_out; + int ret; + + pll_out = 8000 * 512; + + /* Set the codec FLL */ + ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL2, WM8994_FLL_SRC_MCLK1, + ARIES_MCLK1_FREQ, pll_out); + if (ret < 0) + return ret; + + /* Set the codec system clock */ + ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL2, + pll_out, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + return 0; +} + +static int aries_late_probe(struct snd_soc_card *card) +{ + struct aries_wm8994_data *priv = snd_soc_card_get_drvdata(card); + int ret, irq; + + ret = snd_soc_card_jack_new_pins(card, "Dock", SND_JACK_LINEOUT, + &aries_dock, dock_pins, ARRAY_SIZE(dock_pins)); + if (ret) + return ret; + + ret = devm_extcon_register_notifier(card->dev, + priv->usb_extcon, EXTCON_JACK_LINE_OUT, + &aries_extcon_notifier_block); + if (ret) + return ret; + + if (extcon_get_state(priv->usb_extcon, + EXTCON_JACK_LINE_OUT) > 0) + snd_soc_jack_report(&aries_dock, SND_JACK_LINEOUT, + SND_JACK_LINEOUT); + else + snd_soc_jack_report(&aries_dock, 0, SND_JACK_LINEOUT); + + ret = snd_soc_card_jack_new_pins(card, "Headset", + SND_JACK_HEADSET | SND_JACK_BTN_0, + &aries_headset, + jack_pins, ARRAY_SIZE(jack_pins)); + if (ret) + return ret; + + ret = snd_soc_jack_add_zones(&aries_headset, ARRAY_SIZE(headset_zones), + headset_zones); + if (ret) + return ret; + + irq = gpiod_to_irq(priv->gpio_headset_detect); + if (irq < 0) { + dev_err(card->dev, "Failed to map headset detect gpio to irq"); + return -EINVAL; + } + + ret = devm_request_threaded_irq(card->dev, irq, NULL, + headset_det_irq_thread, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | + IRQF_ONESHOT, "headset_detect", priv); + if (ret) { + dev_err(card->dev, "Failed to request headset detect irq"); + return ret; + } + + headset_button_gpio[0].data = priv; + headset_button_gpio[0].desc = priv->gpio_headset_key; + + snd_jack_set_key(aries_headset.jack, SND_JACK_BTN_0, KEY_MEDIA); + + return snd_soc_jack_add_gpios(&aries_headset, + ARRAY_SIZE(headset_button_gpio), headset_button_gpio); +} + +static const struct snd_soc_pcm_stream baseband_params = { + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rate_min = 8000, + .rate_max = 8000, + .channels_min = 1, + .channels_max = 1, +}; + +static const struct snd_soc_pcm_stream bluetooth_params = { + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rate_min = 8000, + .rate_max = 8000, + .channels_min = 1, + .channels_max = 2, +}; + +static const struct snd_soc_dapm_widget aries_modem_widgets[] = { + SND_SOC_DAPM_INPUT("Modem RX"), + SND_SOC_DAPM_OUTPUT("Modem TX"), +}; + +static const struct snd_soc_dapm_route aries_modem_routes[] = { + { "Modem Capture", NULL, "Modem RX" }, + { "Modem TX", NULL, "Modem Playback" }, +}; + +static const struct snd_soc_component_driver aries_component = { + .name = "aries-audio", + .dapm_widgets = aries_modem_widgets, + .num_dapm_widgets = ARRAY_SIZE(aries_modem_widgets), + .dapm_routes = aries_modem_routes, + .num_dapm_routes = ARRAY_SIZE(aries_modem_routes), + .idle_bias_on = 1, + .use_pmdown_time = 1, + .endianness = 1, +}; + +static struct snd_soc_dai_driver aries_ext_dai[] = { + { + .name = "Voice call", + .playback = { + .stream_name = "Modem Playback", + .channels_min = 1, + .channels_max = 1, + .rate_min = 8000, + .rate_max = 8000, + .rates = SNDRV_PCM_RATE_8000, + .formats = SNDRV_PCM_FMTBIT_S16_LE, + }, + .capture = { + .stream_name = "Modem Capture", + .channels_min = 1, + .channels_max = 1, + .rate_min = 8000, + .rate_max = 8000, + .rates = SNDRV_PCM_RATE_8000, + .formats = SNDRV_PCM_FMTBIT_S16_LE, + }, + }, +}; + +SND_SOC_DAILINK_DEFS(aif1, + DAILINK_COMP_ARRAY(COMP_CPU(SAMSUNG_I2S_DAI)), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif1")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +SND_SOC_DAILINK_DEFS(baseband, + DAILINK_COMP_ARRAY(COMP_CPU("Voice call")), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif2"))); + +SND_SOC_DAILINK_DEFS(bluetooth, + DAILINK_COMP_ARRAY(COMP_CPU("bt-sco-pcm")), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif3"))); + +static struct snd_soc_dai_link aries_dai[] = { + { + .name = "WM8994 AIF1", + .stream_name = "HiFi", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + .ops = &aries_ops, + SND_SOC_DAILINK_REG(aif1), + }, + { + .name = "WM8994 AIF2", + .stream_name = "Baseband", + .init = &aries_baseband_init, + .c2c_params = &baseband_params, + .num_c2c_params = 1, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(baseband), + }, + { + .name = "WM8994 AIF3", + .stream_name = "Bluetooth", + .c2c_params = &bluetooth_params, + .num_c2c_params = 1, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(bluetooth), + }, +}; + +static struct snd_soc_card aries_card = { + .name = "ARIES", + .owner = THIS_MODULE, + .dai_link = aries_dai, + .num_links = ARRAY_SIZE(aries_dai), + .controls = aries_controls, + .num_controls = ARRAY_SIZE(aries_controls), + .dapm_widgets = aries_dapm_widgets, + .num_dapm_widgets = ARRAY_SIZE(aries_dapm_widgets), + .late_probe = aries_late_probe, +}; + +static const struct aries_wm8994_variant fascinate4g_variant = { + .modem_dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_CBS_CFS + | SND_SOC_DAIFMT_IB_NF, + .has_fm_radio = false, +}; + +static const struct aries_wm8994_variant aries_variant = { + .modem_dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_CBM_CFM + | SND_SOC_DAIFMT_IB_NF, + .has_fm_radio = true, +}; + +static const struct of_device_id samsung_wm8994_of_match[] = { + { + .compatible = "samsung,fascinate4g-wm8994", + .data = &fascinate4g_variant, + }, + { + .compatible = "samsung,aries-wm8994", + .data = &aries_variant, + }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, samsung_wm8994_of_match); + +static int aries_audio_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct device_node *cpu, *codec, *extcon_np; + struct device *dev = &pdev->dev; + struct snd_soc_card *card = &aries_card; + struct aries_wm8994_data *priv; + struct snd_soc_dai_link *dai_link; + const struct of_device_id *match; + enum iio_chan_type channel_type; + int ret, i; + + if (!np) + return -EINVAL; + + card->dev = dev; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + snd_soc_card_set_drvdata(card, priv); + + match = of_match_node(samsung_wm8994_of_match, np); + priv->variant = match->data; + + /* Remove FM widget if not present */ + if (!priv->variant->has_fm_radio) + card->num_dapm_widgets--; + + priv->reg_main_micbias = devm_regulator_get(dev, "main-micbias"); + if (IS_ERR(priv->reg_main_micbias)) { + dev_err(dev, "Failed to get main micbias regulator\n"); + return PTR_ERR(priv->reg_main_micbias); + } + + priv->reg_headset_micbias = devm_regulator_get(dev, "headset-micbias"); + if (IS_ERR(priv->reg_headset_micbias)) { + dev_err(dev, "Failed to get headset micbias regulator\n"); + return PTR_ERR(priv->reg_headset_micbias); + } + + priv->gpio_earpath_sel = devm_gpiod_get(dev, "earpath-sel", + GPIOD_OUT_LOW); + if (IS_ERR(priv->gpio_earpath_sel)) { + dev_err(dev, "Failed to get earpath selector gpio"); + return PTR_ERR(priv->gpio_earpath_sel); + } + + extcon_np = of_parse_phandle(np, "extcon", 0); + priv->usb_extcon = extcon_find_edev_by_node(extcon_np); + of_node_put(extcon_np); + if (IS_ERR(priv->usb_extcon)) + return dev_err_probe(dev, PTR_ERR(priv->usb_extcon), + "Failed to get extcon device"); + + priv->adc = devm_iio_channel_get(dev, "headset-detect"); + if (IS_ERR(priv->adc)) + return dev_err_probe(dev, PTR_ERR(priv->adc), + "Failed to get ADC channel"); + + ret = iio_get_channel_type(priv->adc, &channel_type); + if (ret) + return dev_err_probe(dev, ret, + "Failed to get ADC channel type"); + if (channel_type != IIO_VOLTAGE) + return -EINVAL; + + priv->gpio_headset_key = devm_gpiod_get(dev, "headset-key", + GPIOD_IN); + if (IS_ERR(priv->gpio_headset_key)) { + dev_err(dev, "Failed to get headset key gpio"); + return PTR_ERR(priv->gpio_headset_key); + } + + priv->gpio_headset_detect = devm_gpiod_get(dev, + "headset-detect", GPIOD_IN); + if (IS_ERR(priv->gpio_headset_detect)) { + dev_err(dev, "Failed to get headset detect gpio"); + return PTR_ERR(priv->gpio_headset_detect); + } + + /* Update card-name if provided through DT, else use default name */ + snd_soc_of_parse_card_name(card, "model"); + + ret = snd_soc_of_parse_audio_routing(card, "audio-routing"); + if (ret < 0) { + /* Backwards compatible way */ + ret = snd_soc_of_parse_audio_routing(card, "samsung,audio-routing"); + if (ret < 0) { + dev_err(dev, "Audio routing invalid/unspecified\n"); + return ret; + } + } + + aries_dai[1].dai_fmt = priv->variant->modem_dai_fmt; + + cpu = of_get_child_by_name(dev->of_node, "cpu"); + if (!cpu) + return -EINVAL; + + codec = of_get_child_by_name(dev->of_node, "codec"); + if (!codec) { + ret = -EINVAL; + goto out; + } + + for_each_card_prelinks(card, i, dai_link) { + dai_link->codecs->of_node = of_parse_phandle(codec, + "sound-dai", 0); + if (!dai_link->codecs->of_node) { + ret = -EINVAL; + goto out; + } + } + + /* Set CPU and platform of_node for main DAI */ + aries_dai[0].cpus->of_node = of_parse_phandle(cpu, + "sound-dai", 0); + if (!aries_dai[0].cpus->of_node) { + ret = -EINVAL; + goto out; + } + + aries_dai[0].platforms->of_node = aries_dai[0].cpus->of_node; + + /* Set CPU of_node for BT DAI */ + aries_dai[2].cpus->of_node = of_parse_phandle(cpu, + "sound-dai", 1); + if (!aries_dai[2].cpus->of_node) { + ret = -EINVAL; + goto out; + } + + ret = devm_snd_soc_register_component(dev, &aries_component, + aries_ext_dai, ARRAY_SIZE(aries_ext_dai)); + if (ret < 0) { + dev_err(dev, "Failed to register component: %d\n", ret); + goto out; + } + + ret = devm_snd_soc_register_card(dev, card); + if (ret) + dev_err(dev, "snd_soc_register_card() failed:%d\n", ret); + +out: + of_node_put(cpu); + of_node_put(codec); + + return ret; +} + +static struct platform_driver aries_audio_driver = { + .driver = { + .name = "aries-audio-wm8994", + .of_match_table = of_match_ptr(samsung_wm8994_of_match), + .pm = &snd_soc_pm_ops, + }, + .probe = aries_audio_probe, +}; + +module_platform_driver(aries_audio_driver); + +MODULE_DESCRIPTION("ALSA SoC ARIES WM8994"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:aries-audio-wm8994"); diff --git a/sound/soc/samsung/arndale.c b/sound/soc/samsung/arndale.c new file mode 100644 index 0000000000..fdff83e72d --- /dev/null +++ b/sound/soc/samsung/arndale.c @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Copyright (c) 2014, Insignal Co., Ltd. +// +// Author: Claude <claude@insginal.co.kr> + +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/clk.h> + +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> + +#include "../codecs/wm8994.h" +#include "i2s.h" + +static int arndale_rt5631_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 *cpu_dai = asoc_rtd_to_cpu(rtd, 0); + struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, 0); + int rfs, ret; + unsigned long rclk; + + rfs = 256; + + rclk = params_rate(params) * rfs; + + ret = snd_soc_dai_set_sysclk(cpu_dai, SAMSUNG_I2S_CDCLK, + 0, SND_SOC_CLOCK_OUT); + if (ret < 0) + return ret; + + ret = snd_soc_dai_set_sysclk(cpu_dai, SAMSUNG_I2S_RCLKSRC_0, + 0, SND_SOC_CLOCK_OUT); + + if (ret < 0) + return ret; + + ret = snd_soc_dai_set_sysclk(codec_dai, 0, rclk, SND_SOC_CLOCK_OUT); + if (ret < 0) + return ret; + + return 0; +} + +static const struct snd_soc_ops arndale_rt5631_ops = { + .hw_params = arndale_rt5631_hw_params, +}; + +static int arndale_wm1811_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); + unsigned int rfs, rclk; + + /* Ensure AIF1CLK is >= 3 MHz for optimal performance */ + if (params_width(params) == 24) + rfs = 384; + else if (params_rate(params) == 8000 || params_rate(params) == 11025) + rfs = 512; + else + rfs = 256; + + rclk = params_rate(params) * rfs; + + /* + * We add 1 to the frequency value to ensure proper EPLL setting + * for each audio sampling rate (see epll_24mhz_tbl in drivers/clk/ + * samsung/clk-exynos5250.c for list of available EPLL rates). + * The CODEC uses clk API and the value will be rounded hence the MCLK1 + * clock's frequency will still be exact multiple of the sample rate. + */ + return snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_MCLK1, + rclk + 1, SND_SOC_CLOCK_IN); +} + +static const struct snd_soc_ops arndale_wm1811_ops = { + .hw_params = arndale_wm1811_hw_params, +}; + +SND_SOC_DAILINK_DEFS(rt5631_hifi, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "rt5631-aif1")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +static struct snd_soc_dai_link arndale_rt5631_dai[] = { + { + .name = "RT5631 HiFi", + .stream_name = "Primary", + .dai_fmt = SND_SOC_DAIFMT_I2S + | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBS_CFS, + .ops = &arndale_rt5631_ops, + SND_SOC_DAILINK_REG(rt5631_hifi), + }, +}; + +SND_SOC_DAILINK_DEFS(wm1811_hifi, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif1")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +static struct snd_soc_dai_link arndale_wm1811_dai[] = { + { + .name = "WM1811 HiFi", + .stream_name = "Primary", + .dai_fmt = SND_SOC_DAIFMT_I2S + | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .ops = &arndale_wm1811_ops, + SND_SOC_DAILINK_REG(wm1811_hifi), + }, +}; + +static struct snd_soc_card arndale_rt5631 = { + .name = "Arndale RT5631", + .owner = THIS_MODULE, + .dai_link = arndale_rt5631_dai, + .num_links = ARRAY_SIZE(arndale_rt5631_dai), +}; + +static struct snd_soc_card arndale_wm1811 = { + .name = "Arndale WM1811", + .owner = THIS_MODULE, + .dai_link = arndale_wm1811_dai, + .num_links = ARRAY_SIZE(arndale_wm1811_dai), +}; + +static void arndale_put_of_nodes(struct snd_soc_card *card) +{ + struct snd_soc_dai_link *dai_link; + int i; + + for_each_card_prelinks(card, i, dai_link) { + of_node_put(dai_link->cpus->of_node); + of_node_put(dai_link->codecs->of_node); + } +} + +static int arndale_audio_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct snd_soc_card *card; + struct snd_soc_dai_link *dai_link; + int ret; + + card = (struct snd_soc_card *)of_device_get_match_data(&pdev->dev); + card->dev = &pdev->dev; + dai_link = card->dai_link; + + dai_link->cpus->of_node = of_parse_phandle(np, "samsung,audio-cpu", 0); + if (!dai_link->cpus->of_node) { + dev_err(&pdev->dev, + "Property 'samsung,audio-cpu' missing or invalid\n"); + return -EINVAL; + } + + if (!dai_link->platforms->name) + dai_link->platforms->of_node = dai_link->cpus->of_node; + + dai_link->codecs->of_node = of_parse_phandle(np, "samsung,audio-codec", 0); + if (!dai_link->codecs->of_node) { + dev_err(&pdev->dev, + "Property 'samsung,audio-codec' missing or invalid\n"); + ret = -EINVAL; + goto err_put_of_nodes; + } + + ret = devm_snd_soc_register_card(card->dev, card); + if (ret) { + dev_err_probe(&pdev->dev, ret, + "snd_soc_register_card() failed\n"); + goto err_put_of_nodes; + } + return 0; + +err_put_of_nodes: + arndale_put_of_nodes(card); + return ret; +} + +static void arndale_audio_remove(struct platform_device *pdev) +{ + struct snd_soc_card *card = platform_get_drvdata(pdev); + + arndale_put_of_nodes(card); +} + +static const struct of_device_id arndale_audio_of_match[] = { + { .compatible = "samsung,arndale-rt5631", .data = &arndale_rt5631 }, + { .compatible = "samsung,arndale-alc5631", .data = &arndale_rt5631 }, + { .compatible = "samsung,arndale-wm1811", .data = &arndale_wm1811 }, + {}, +}; +MODULE_DEVICE_TABLE(of, arndale_audio_of_match); + +static struct platform_driver arndale_audio_driver = { + .driver = { + .name = "arndale-audio", + .pm = &snd_soc_pm_ops, + .of_match_table = arndale_audio_of_match, + }, + .probe = arndale_audio_probe, + .remove_new = arndale_audio_remove, +}; + +module_platform_driver(arndale_audio_driver); + +MODULE_AUTHOR("Claude <claude@insignal.co.kr>"); +MODULE_DESCRIPTION("ALSA SoC Driver for Arndale Board"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/samsung/bells.c b/sound/soc/samsung/bells.c new file mode 100644 index 0000000000..70b63d4faa --- /dev/null +++ b/sound/soc/samsung/bells.c @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Bells audio support +// +// Copyright 2012 Wolfson Microelectronics + +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/jack.h> +#include <linux/gpio.h> +#include <linux/module.h> + +#include "../codecs/wm5102.h" +#include "../codecs/wm9081.h" + +/* BCLK2 is fixed at this currently */ +#define BCLK2_RATE (64 * 8000) + +/* + * Expect a 24.576MHz crystal if one is fitted (the driver will function + * if this is not fitted). + */ +#define MCLK_RATE 24576000 + +#define SYS_AUDIO_RATE 44100 +#define SYS_MCLK_RATE (SYS_AUDIO_RATE * 512) + +#define DAI_AP_DSP 0 +#define DAI_DSP_CODEC 1 +#define DAI_CODEC_CP 2 +#define DAI_CODEC_SUB 3 + +struct bells_drvdata { + int sysclk_rate; + int asyncclk_rate; +}; + +static struct bells_drvdata wm2200_drvdata = { + .sysclk_rate = 22579200, +}; + +static struct bells_drvdata wm5102_drvdata = { + .sysclk_rate = 45158400, + .asyncclk_rate = 49152000, +}; + +static struct bells_drvdata wm5110_drvdata = { + .sysclk_rate = 135475200, + .asyncclk_rate = 147456000, +}; + +static int bells_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; + struct snd_soc_dai *codec_dai; + struct snd_soc_component *component; + struct bells_drvdata *bells = card->drvdata; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[DAI_DSP_CODEC]); + codec_dai = asoc_rtd_to_codec(rtd, 0); + component = codec_dai->component; + + if (dapm->dev != codec_dai->dev) + return 0; + + switch (level) { + case SND_SOC_BIAS_PREPARE: + if (dapm->bias_level != SND_SOC_BIAS_STANDBY) + break; + + ret = snd_soc_component_set_pll(component, WM5102_FLL1, + ARIZONA_FLL_SRC_MCLK1, + MCLK_RATE, + bells->sysclk_rate); + if (ret < 0) + pr_err("Failed to start FLL: %d\n", ret); + + if (bells->asyncclk_rate) { + ret = snd_soc_component_set_pll(component, WM5102_FLL2, + ARIZONA_FLL_SRC_AIF2BCLK, + BCLK2_RATE, + bells->asyncclk_rate); + if (ret < 0) + pr_err("Failed to start FLL: %d\n", ret); + } + break; + + default: + break; + } + + return 0; +} + +static int bells_set_bias_level_post(struct snd_soc_card *card, + struct snd_soc_dapm_context *dapm, + enum snd_soc_bias_level level) +{ + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_dai *codec_dai; + struct snd_soc_component *component; + struct bells_drvdata *bells = card->drvdata; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[DAI_DSP_CODEC]); + codec_dai = asoc_rtd_to_codec(rtd, 0); + component = codec_dai->component; + + if (dapm->dev != codec_dai->dev) + return 0; + + switch (level) { + case SND_SOC_BIAS_STANDBY: + ret = snd_soc_component_set_pll(component, WM5102_FLL1, 0, 0, 0); + if (ret < 0) { + pr_err("Failed to stop FLL: %d\n", ret); + return ret; + } + + if (bells->asyncclk_rate) { + ret = snd_soc_component_set_pll(component, WM5102_FLL2, + 0, 0, 0); + if (ret < 0) { + pr_err("Failed to stop FLL: %d\n", ret); + return ret; + } + } + break; + + default: + break; + } + + dapm->bias_level = level; + + return 0; +} + +static int bells_late_probe(struct snd_soc_card *card) +{ + struct bells_drvdata *bells = card->drvdata; + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_component *wm0010; + struct snd_soc_component *component; + struct snd_soc_dai *aif1_dai; + struct snd_soc_dai *aif2_dai; + struct snd_soc_dai *aif3_dai; + struct snd_soc_dai *wm9081_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[DAI_AP_DSP]); + wm0010 = asoc_rtd_to_codec(rtd, 0)->component; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[DAI_DSP_CODEC]); + component = asoc_rtd_to_codec(rtd, 0)->component; + aif1_dai = asoc_rtd_to_codec(rtd, 0); + + ret = snd_soc_component_set_sysclk(component, ARIZONA_CLK_SYSCLK, + ARIZONA_CLK_SRC_FLL1, + bells->sysclk_rate, + SND_SOC_CLOCK_IN); + if (ret != 0) { + dev_err(component->dev, "Failed to set SYSCLK: %d\n", ret); + return ret; + } + + ret = snd_soc_component_set_sysclk(wm0010, 0, 0, SYS_MCLK_RATE, 0); + if (ret != 0) { + dev_err(wm0010->dev, "Failed to set WM0010 clock: %d\n", ret); + return ret; + } + + ret = snd_soc_dai_set_sysclk(aif1_dai, ARIZONA_CLK_SYSCLK, 0, 0); + if (ret != 0) + dev_err(aif1_dai->dev, "Failed to set AIF1 clock: %d\n", ret); + + ret = snd_soc_component_set_sysclk(component, ARIZONA_CLK_OPCLK, 0, + SYS_MCLK_RATE, SND_SOC_CLOCK_OUT); + if (ret != 0) + dev_err(component->dev, "Failed to set OPCLK: %d\n", ret); + + if (card->num_rtd == DAI_CODEC_CP) + return 0; + + ret = snd_soc_component_set_sysclk(component, ARIZONA_CLK_ASYNCCLK, + ARIZONA_CLK_SRC_FLL2, + bells->asyncclk_rate, + SND_SOC_CLOCK_IN); + if (ret != 0) { + dev_err(component->dev, "Failed to set ASYNCCLK: %d\n", ret); + return ret; + } + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[DAI_CODEC_CP]); + aif2_dai = asoc_rtd_to_cpu(rtd, 0); + + ret = snd_soc_dai_set_sysclk(aif2_dai, ARIZONA_CLK_ASYNCCLK, 0, 0); + if (ret != 0) { + dev_err(aif2_dai->dev, "Failed to set AIF2 clock: %d\n", ret); + return ret; + } + + if (card->num_rtd == DAI_CODEC_SUB) + return 0; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[DAI_CODEC_SUB]); + aif3_dai = asoc_rtd_to_cpu(rtd, 0); + wm9081_dai = asoc_rtd_to_codec(rtd, 0); + + ret = snd_soc_dai_set_sysclk(aif3_dai, ARIZONA_CLK_SYSCLK, 0, 0); + if (ret != 0) { + dev_err(aif1_dai->dev, "Failed to set AIF1 clock: %d\n", ret); + return ret; + } + + ret = snd_soc_component_set_sysclk(wm9081_dai->component, WM9081_SYSCLK_MCLK, + 0, SYS_MCLK_RATE, 0); + if (ret != 0) { + dev_err(wm9081_dai->dev, "Failed to set MCLK: %d\n", ret); + return ret; + } + + return 0; +} + +static const struct snd_soc_pcm_stream baseband_params = { + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .rate_min = 8000, + .rate_max = 8000, + .channels_min = 2, + .channels_max = 2, +}; + +static const struct snd_soc_pcm_stream sub_params = { + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .rate_min = SYS_AUDIO_RATE, + .rate_max = SYS_AUDIO_RATE, + .channels_min = 2, + .channels_max = 2, +}; + +SND_SOC_DAILINK_DEFS(wm2200_cpu_dsp, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s.0")), + DAILINK_COMP_ARRAY(COMP_CODEC("spi0.0", "wm0010-sdi1")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s.0"))); + +SND_SOC_DAILINK_DEFS(wm2200_dsp_codec, + DAILINK_COMP_ARRAY(COMP_CPU("wm0010-sdi2")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm2200.1-003a", "wm2200"))); + +static struct snd_soc_dai_link bells_dai_wm2200[] = { + { + .name = "CPU-DSP", + .stream_name = "CPU-DSP", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + SND_SOC_DAILINK_REG(wm2200_cpu_dsp), + }, + { + .name = "DSP-CODEC", + .stream_name = "DSP-CODEC", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .c2c_params = &sub_params, + .num_c2c_params = 1, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(wm2200_dsp_codec), + }, +}; + +SND_SOC_DAILINK_DEFS(wm5102_cpu_dsp, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s.0")), + DAILINK_COMP_ARRAY(COMP_CODEC("spi0.0", "wm0010-sdi1")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s.0"))); + +SND_SOC_DAILINK_DEFS(wm5102_dsp_codec, + DAILINK_COMP_ARRAY(COMP_CPU("wm0010-sdi2")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm5102-codec", "wm5102-aif1"))); + +SND_SOC_DAILINK_DEFS(wm5102_baseband, + DAILINK_COMP_ARRAY(COMP_CPU("wm5102-aif2")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm1250-ev1.1-0027", "wm1250-ev1"))); + +SND_SOC_DAILINK_DEFS(wm5102_sub, + DAILINK_COMP_ARRAY(COMP_CPU("wm5102-aif3")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm9081.1-006c", "wm9081-hifi"))); + +static struct snd_soc_dai_link bells_dai_wm5102[] = { + { + .name = "CPU-DSP", + .stream_name = "CPU-DSP", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + SND_SOC_DAILINK_REG(wm5102_cpu_dsp), + }, + { + .name = "DSP-CODEC", + .stream_name = "DSP-CODEC", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .c2c_params = &sub_params, + .num_c2c_params = 1, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(wm5102_dsp_codec), + }, + { + .name = "Baseband", + .stream_name = "Baseband", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .ignore_suspend = 1, + .c2c_params = &baseband_params, + .num_c2c_params = 1, + SND_SOC_DAILINK_REG(wm5102_baseband), + }, + { + .name = "Sub", + .stream_name = "Sub", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBS_CFS, + .ignore_suspend = 1, + .c2c_params = &sub_params, + .num_c2c_params = 1, + SND_SOC_DAILINK_REG(wm5102_sub), + }, +}; + +SND_SOC_DAILINK_DEFS(wm5110_cpu_dsp, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s.0")), + DAILINK_COMP_ARRAY(COMP_CODEC("spi0.0", "wm0010-sdi1")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s.0"))); + +SND_SOC_DAILINK_DEFS(wm5110_dsp_codec, + DAILINK_COMP_ARRAY(COMP_CPU("wm0010-sdi2")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm5110-codec", "wm5110-aif1"))); + +SND_SOC_DAILINK_DEFS(wm5110_baseband, + DAILINK_COMP_ARRAY(COMP_CPU("wm5110-aif2")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm1250-ev1.1-0027", "wm1250-ev1"))); + + +SND_SOC_DAILINK_DEFS(wm5110_sub, + DAILINK_COMP_ARRAY(COMP_CPU("wm5110-aif3")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm9081.1-006c", "wm9081-hifi"))); + +static struct snd_soc_dai_link bells_dai_wm5110[] = { + { + .name = "CPU-DSP", + .stream_name = "CPU-DSP", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + SND_SOC_DAILINK_REG(wm5110_cpu_dsp), + }, + { + .name = "DSP-CODEC", + .stream_name = "DSP-CODEC", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .c2c_params = &sub_params, + .num_c2c_params = 1, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(wm5110_dsp_codec), + }, + { + .name = "Baseband", + .stream_name = "Baseband", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .ignore_suspend = 1, + .c2c_params = &baseband_params, + .num_c2c_params = 1, + SND_SOC_DAILINK_REG(wm5110_baseband), + }, + { + .name = "Sub", + .stream_name = "Sub", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBS_CFS, + .ignore_suspend = 1, + .c2c_params = &sub_params, + .num_c2c_params = 1, + SND_SOC_DAILINK_REG(wm5110_sub), + }, +}; + +static struct snd_soc_codec_conf bells_codec_conf[] = { + { + .dlc = COMP_CODEC_CONF("wm9081.1-006c"), + .name_prefix = "Sub", + }, +}; + +static const struct snd_soc_dapm_widget bells_widgets[] = { + SND_SOC_DAPM_MIC("DMIC", NULL), +}; + +static const struct snd_soc_dapm_route bells_routes[] = { + { "Sub CLK_SYS", NULL, "OPCLK" }, + { "CLKIN", NULL, "OPCLK" }, + + { "DMIC", NULL, "MICBIAS2" }, + { "IN2L", NULL, "DMIC" }, + { "IN2R", NULL, "DMIC" }, +}; + +static struct snd_soc_card bells_cards[] = { + { + .name = "Bells WM2200", + .owner = THIS_MODULE, + .dai_link = bells_dai_wm2200, + .num_links = ARRAY_SIZE(bells_dai_wm2200), + .codec_conf = bells_codec_conf, + .num_configs = ARRAY_SIZE(bells_codec_conf), + + .late_probe = bells_late_probe, + + .dapm_widgets = bells_widgets, + .num_dapm_widgets = ARRAY_SIZE(bells_widgets), + .dapm_routes = bells_routes, + .num_dapm_routes = ARRAY_SIZE(bells_routes), + + .set_bias_level = bells_set_bias_level, + .set_bias_level_post = bells_set_bias_level_post, + + .drvdata = &wm2200_drvdata, + }, + { + .name = "Bells WM5102", + .owner = THIS_MODULE, + .dai_link = bells_dai_wm5102, + .num_links = ARRAY_SIZE(bells_dai_wm5102), + .codec_conf = bells_codec_conf, + .num_configs = ARRAY_SIZE(bells_codec_conf), + + .late_probe = bells_late_probe, + + .dapm_widgets = bells_widgets, + .num_dapm_widgets = ARRAY_SIZE(bells_widgets), + .dapm_routes = bells_routes, + .num_dapm_routes = ARRAY_SIZE(bells_routes), + + .set_bias_level = bells_set_bias_level, + .set_bias_level_post = bells_set_bias_level_post, + + .drvdata = &wm5102_drvdata, + }, + { + .name = "Bells WM5110", + .owner = THIS_MODULE, + .dai_link = bells_dai_wm5110, + .num_links = ARRAY_SIZE(bells_dai_wm5110), + .codec_conf = bells_codec_conf, + .num_configs = ARRAY_SIZE(bells_codec_conf), + + .late_probe = bells_late_probe, + + .dapm_widgets = bells_widgets, + .num_dapm_widgets = ARRAY_SIZE(bells_widgets), + .dapm_routes = bells_routes, + .num_dapm_routes = ARRAY_SIZE(bells_routes), + + .set_bias_level = bells_set_bias_level, + .set_bias_level_post = bells_set_bias_level_post, + + .drvdata = &wm5110_drvdata, + }, +}; + +static int bells_probe(struct platform_device *pdev) +{ + int ret; + + bells_cards[pdev->id].dev = &pdev->dev; + + ret = devm_snd_soc_register_card(&pdev->dev, &bells_cards[pdev->id]); + if (ret) + dev_err(&pdev->dev, + "snd_soc_register_card(%s) failed: %d\n", + bells_cards[pdev->id].name, ret); + + return ret; +} + +static struct platform_driver bells_driver = { + .driver = { + .name = "bells", + .pm = &snd_soc_pm_ops, + }, + .probe = bells_probe, +}; + +module_platform_driver(bells_driver); + +MODULE_DESCRIPTION("Bells audio support"); +MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:bells"); diff --git a/sound/soc/samsung/dma.h b/sound/soc/samsung/dma.h new file mode 100644 index 0000000000..7b5d4556e0 --- /dev/null +++ b/sound/soc/samsung/dma.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * ALSA PCM interface for the Samsung SoC + */ + +#ifndef _SAMSUNG_DMA_H +#define _SAMSUNG_DMA_H + +#include <sound/dmaengine_pcm.h> + +/* + * @tx, @rx arguments can be NULL if the DMA channel names are "tx", "rx", + * otherwise actual DMA channel names must be passed to this function. + */ +int samsung_asoc_dma_platform_register(struct device *dev, dma_filter_fn filter, + const char *tx, const char *rx, + struct device *dma_dev); +#endif /* _SAMSUNG_DMA_H */ diff --git a/sound/soc/samsung/dmaengine.c b/sound/soc/samsung/dmaengine.c new file mode 100644 index 0000000000..2802789a32 --- /dev/null +++ b/sound/soc/samsung/dmaengine.c @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// dmaengine.c - Samsung dmaengine wrapper +// +// Author: Mark Brown <broonie@linaro.org> +// Copyright 2013 Linaro + +#include <linux/module.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/dmaengine_pcm.h> +#include <sound/soc.h> + +#include "dma.h" + +int samsung_asoc_dma_platform_register(struct device *dev, dma_filter_fn filter, + const char *tx, const char *rx, + struct device *dma_dev) +{ + struct snd_dmaengine_pcm_config *pcm_conf; + + pcm_conf = devm_kzalloc(dev, sizeof(*pcm_conf), GFP_KERNEL); + if (!pcm_conf) + return -ENOMEM; + + pcm_conf->prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config; + pcm_conf->compat_filter_fn = filter; + pcm_conf->dma_dev = dma_dev; + + pcm_conf->chan_names[SNDRV_PCM_STREAM_PLAYBACK] = tx; + pcm_conf->chan_names[SNDRV_PCM_STREAM_CAPTURE] = rx; + + return devm_snd_dmaengine_pcm_register(dev, pcm_conf, + SND_DMAENGINE_PCM_FLAG_COMPAT); +} +EXPORT_SYMBOL_GPL(samsung_asoc_dma_platform_register); + +MODULE_AUTHOR("Mark Brown <broonie@linaro.org>"); +MODULE_DESCRIPTION("Samsung dmaengine ASoC driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/samsung/i2s-regs.h b/sound/soc/samsung/i2s-regs.h new file mode 100644 index 0000000000..138e955819 --- /dev/null +++ b/sound/soc/samsung/i2s-regs.h @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Copyright (c) 2011 Samsung Electronics Co., Ltd. + * http://www.samsung.com + * + * Samsung I2S driver's register header + */ + +#ifndef __SND_SOC_SAMSUNG_I2S_REGS_H +#define __SND_SOC_SAMSUNG_I2S_REGS_H + +#define I2SCON 0x0 +#define I2SMOD 0x4 +#define I2SFIC 0x8 +#define I2SPSR 0xc +#define I2STXD 0x10 +#define I2SRXD 0x14 +#define I2SFICS 0x18 +#define I2STXDS 0x1c +#define I2SAHB 0x20 +#define I2SSTR0 0x24 +#define I2SSIZE 0x28 +#define I2STRNCNT 0x2c +#define I2SLVL0ADDR 0x30 +#define I2SLVL1ADDR 0x34 +#define I2SLVL2ADDR 0x38 +#define I2SLVL3ADDR 0x3c +#define I2SSTR1 0x40 +#define I2SVER 0x44 +#define I2SFIC1 0x48 +#define I2STDM 0x4c +#define I2SFSTA 0x50 + +#define CON_RSTCLR (1 << 31) +#define CON_FRXOFSTATUS (1 << 26) +#define CON_FRXORINTEN (1 << 25) +#define CON_FTXSURSTAT (1 << 24) +#define CON_FTXSURINTEN (1 << 23) +#define CON_TXSDMA_PAUSE (1 << 20) +#define CON_TXSDMA_ACTIVE (1 << 18) + +#define CON_FTXURSTATUS (1 << 17) +#define CON_FTXURINTEN (1 << 16) +#define CON_TXFIFO2_EMPTY (1 << 15) +#define CON_TXFIFO1_EMPTY (1 << 14) +#define CON_TXFIFO2_FULL (1 << 13) +#define CON_TXFIFO1_FULL (1 << 12) + +#define CON_LRINDEX (1 << 11) +#define CON_TXFIFO_EMPTY (1 << 10) +#define CON_RXFIFO_EMPTY (1 << 9) +#define CON_TXFIFO_FULL (1 << 8) +#define CON_RXFIFO_FULL (1 << 7) +#define CON_TXDMA_PAUSE (1 << 6) +#define CON_RXDMA_PAUSE (1 << 5) +#define CON_TXCH_PAUSE (1 << 4) +#define CON_RXCH_PAUSE (1 << 3) +#define CON_TXDMA_ACTIVE (1 << 2) +#define CON_RXDMA_ACTIVE (1 << 1) +#define CON_ACTIVE (1 << 0) + +#define MOD_OPCLK_SHIFT 30 +#define MOD_OPCLK_CDCLK_OUT (0 << MOD_OPCLK_SHIFT) +#define MOD_OPCLK_CDCLK_IN (1 << MOD_OPCLK_SHIFT) +#define MOD_OPCLK_BCLK_OUT (2 << MOD_OPCLK_SHIFT) +#define MOD_OPCLK_PCLK (3 << MOD_OPCLK_SHIFT) +#define MOD_OPCLK_MASK (3 << MOD_OPCLK_SHIFT) +#define MOD_TXS_IDMA (1 << 28) /* Sec_TXFIFO use I-DMA */ + +#define MOD_BLCS_SHIFT 26 +#define MOD_BLCS_16BIT (0 << MOD_BLCS_SHIFT) +#define MOD_BLCS_8BIT (1 << MOD_BLCS_SHIFT) +#define MOD_BLCS_24BIT (2 << MOD_BLCS_SHIFT) +#define MOD_BLCS_MASK (3 << MOD_BLCS_SHIFT) +#define MOD_BLCP_SHIFT 24 +#define MOD_BLCP_16BIT (0 << MOD_BLCP_SHIFT) +#define MOD_BLCP_8BIT (1 << MOD_BLCP_SHIFT) +#define MOD_BLCP_24BIT (2 << MOD_BLCP_SHIFT) +#define MOD_BLCP_MASK (3 << MOD_BLCP_SHIFT) + +#define MOD_C2DD_HHALF (1 << 21) /* Discard Higher-half */ +#define MOD_C2DD_LHALF (1 << 20) /* Discard Lower-half */ +#define MOD_C1DD_HHALF (1 << 19) +#define MOD_C1DD_LHALF (1 << 18) +#define MOD_DC2_EN (1 << 17) +#define MOD_DC1_EN (1 << 16) +#define MOD_BLC_16BIT (0 << 13) +#define MOD_BLC_8BIT (1 << 13) +#define MOD_BLC_24BIT (2 << 13) +#define MOD_BLC_MASK (3 << 13) + +#define MOD_TXONLY (0 << 8) +#define MOD_RXONLY (1 << 8) +#define MOD_TXRX (2 << 8) +#define MOD_MASK (3 << 8) +#define MOD_LRP_SHIFT 7 +#define MOD_LR_LLOW 0 +#define MOD_LR_RLOW 1 +#define MOD_SDF_SHIFT 5 +#define MOD_SDF_IIS 0 +#define MOD_SDF_MSB 1 +#define MOD_SDF_LSB 2 +#define MOD_SDF_MASK 3 +#define MOD_RCLK_SHIFT 3 +#define MOD_RCLK_256FS 0 +#define MOD_RCLK_512FS 1 +#define MOD_RCLK_384FS 2 +#define MOD_RCLK_768FS 3 +#define MOD_RCLK_MASK 3 +#define MOD_BCLK_SHIFT 1 +#define MOD_BCLK_32FS 0 +#define MOD_BCLK_48FS 1 +#define MOD_BCLK_16FS 2 +#define MOD_BCLK_24FS 3 +#define MOD_BCLK_MASK 3 +#define MOD_8BIT (1 << 0) + +#define EXYNOS5420_MOD_LRP_SHIFT 15 +#define EXYNOS5420_MOD_SDF_SHIFT 6 +#define EXYNOS5420_MOD_RCLK_SHIFT 4 +#define EXYNOS5420_MOD_BCLK_SHIFT 0 +#define EXYNOS5420_MOD_BCLK_64FS 4 +#define EXYNOS5420_MOD_BCLK_96FS 5 +#define EXYNOS5420_MOD_BCLK_128FS 6 +#define EXYNOS5420_MOD_BCLK_192FS 7 +#define EXYNOS5420_MOD_BCLK_256FS 8 +#define EXYNOS5420_MOD_BCLK_MASK 0xf + +#define EXYNOS7_MOD_RCLK_64FS 4 +#define EXYNOS7_MOD_RCLK_128FS 5 +#define EXYNOS7_MOD_RCLK_96FS 6 +#define EXYNOS7_MOD_RCLK_192FS 7 + +#define PSR_PSREN (1 << 15) +#define PSR_PSVAL(x) ((((x) - 1) << 8) & 0x3f00) + +#define FIC_TX2COUNT(x) (((x) >> 24) & 0xf) +#define FIC_TX1COUNT(x) (((x) >> 16) & 0xf) + +#define FIC_TXFLUSH (1 << 15) +#define FIC_RXFLUSH (1 << 7) + +#define FIC_TXCOUNT(x) (((x) >> 8) & 0xf) +#define FIC_RXCOUNT(x) (((x) >> 0) & 0xf) +#define FICS_TXCOUNT(x) (((x) >> 8) & 0x7f) + +#define AHB_INTENLVL0 (1 << 24) +#define AHB_LVL0INT (1 << 20) +#define AHB_CLRLVL0INT (1 << 16) +#define AHB_DMARLD (1 << 5) +#define AHB_INTMASK (1 << 3) +#define AHB_DMAEN (1 << 0) +#define AHB_LVLINTMASK (0xf << 20) + +#define I2SSIZE_TRNMSK (0xffff) +#define I2SSIZE_SHIFT (16) + +#endif /* __SND_SOC_SAMSUNG_I2S_REGS_H */ diff --git a/sound/soc/samsung/i2s.c b/sound/soc/samsung/i2s.c new file mode 100644 index 0000000000..3af48c9b5a --- /dev/null +++ b/sound/soc/samsung/i2s.c @@ -0,0 +1,1761 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// ALSA SoC Audio Layer - Samsung I2S Controller driver +// +// Copyright (c) 2010 Samsung Electronics Co. Ltd. +// Jaswinder Singh <jassisinghbrar@gmail.com> + +#include <dt-bindings/sound/samsung-i2s.h> +#include <linux/delay.h> +#include <linux/slab.h> +#include <linux/clk.h> +#include <linux/clk-provider.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/of_gpio.h> +#include <linux/pm_runtime.h> + +#include <sound/soc.h> +#include <sound/pcm_params.h> + +#include <linux/platform_data/asoc-s3c.h> + +#include "dma.h" +#include "idma.h" +#include "i2s.h" +#include "i2s-regs.h" + +#define msecs_to_loops(t) (loops_per_jiffy / 1000 * HZ * t) + +#define SAMSUNG_I2S_ID_PRIMARY 1 +#define SAMSUNG_I2S_ID_SECONDARY 2 + +struct samsung_i2s_variant_regs { + unsigned int bfs_off; + unsigned int rfs_off; + unsigned int sdf_off; + unsigned int txr_off; + unsigned int rclksrc_off; + unsigned int mss_off; + unsigned int cdclkcon_off; + unsigned int lrp_off; + unsigned int bfs_mask; + unsigned int rfs_mask; + unsigned int ftx0cnt_off; +}; + +struct samsung_i2s_dai_data { + u32 quirks; + unsigned int pcm_rates; + const struct samsung_i2s_variant_regs *i2s_variant_regs; + void (*fixup_early)(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai); + void (*fixup_late)(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai); +}; + +struct i2s_dai { + /* Platform device for this DAI */ + struct platform_device *pdev; + + /* Frame clock */ + unsigned frmclk; + /* + * Specifically requested RCLK, BCLK by machine driver. + * 0 indicates CPU driver is free to choose any value. + */ + unsigned rfs, bfs; + /* Pointer to the Primary_Fifo if this is Sec_Fifo, NULL otherwise */ + struct i2s_dai *pri_dai; + /* Pointer to the Secondary_Fifo if it has one, NULL otherwise */ + struct i2s_dai *sec_dai; + +#define DAI_OPENED (1 << 0) /* DAI is opened */ +#define DAI_MANAGER (1 << 1) /* DAI is the manager */ + unsigned mode; + + /* Driver for this DAI */ + struct snd_soc_dai_driver *drv; + + /* DMA parameters */ + struct snd_dmaengine_dai_dma_data dma_playback; + struct snd_dmaengine_dai_dma_data dma_capture; + struct snd_dmaengine_dai_dma_data idma_playback; + dma_filter_fn filter; + + struct samsung_i2s_priv *priv; +}; + +struct samsung_i2s_priv { + struct platform_device *pdev; + struct platform_device *pdev_sec; + + /* Lock for cross interface checks */ + spinlock_t pcm_lock; + + /* CPU DAIs and their corresponding drivers */ + struct i2s_dai *dai; + struct snd_soc_dai_driver *dai_drv; + int num_dais; + + /* The I2S controller's core clock */ + struct clk *clk; + + /* Clock for generating I2S signals */ + struct clk *op_clk; + + /* Rate of RCLK source clock */ + unsigned long rclk_srcrate; + + /* Cache of selected I2S registers for system suspend */ + u32 suspend_i2smod; + u32 suspend_i2scon; + u32 suspend_i2spsr; + + const struct samsung_i2s_variant_regs *variant_regs; + void (*fixup_early)(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai); + void (*fixup_late)(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai); + u32 quirks; + + /* The clock provider's data */ + struct clk *clk_table[3]; + struct clk_onecell_data clk_data; + + /* Spinlock protecting member fields below */ + spinlock_t lock; + + /* Memory mapped SFR region */ + void __iomem *addr; + + /* A flag indicating the I2S slave mode operation */ + bool slave_mode; +}; + +/* Returns true if this is the 'overlay' stereo DAI */ +static inline bool is_secondary(struct i2s_dai *i2s) +{ + return i2s->drv->id == SAMSUNG_I2S_ID_SECONDARY; +} + +/* If this interface of the controller is transmitting data */ +static inline bool tx_active(struct i2s_dai *i2s) +{ + u32 active; + + if (!i2s) + return false; + + active = readl(i2s->priv->addr + I2SCON); + + if (is_secondary(i2s)) + active &= CON_TXSDMA_ACTIVE; + else + active &= CON_TXDMA_ACTIVE; + + return active ? true : false; +} + +/* Return pointer to the other DAI */ +static inline struct i2s_dai *get_other_dai(struct i2s_dai *i2s) +{ + return i2s->pri_dai ? : i2s->sec_dai; +} + +/* If the other interface of the controller is transmitting data */ +static inline bool other_tx_active(struct i2s_dai *i2s) +{ + struct i2s_dai *other = get_other_dai(i2s); + + return tx_active(other); +} + +/* If any interface of the controller is transmitting data */ +static inline bool any_tx_active(struct i2s_dai *i2s) +{ + return tx_active(i2s) || other_tx_active(i2s); +} + +/* If this interface of the controller is receiving data */ +static inline bool rx_active(struct i2s_dai *i2s) +{ + u32 active; + + if (!i2s) + return false; + + active = readl(i2s->priv->addr + I2SCON) & CON_RXDMA_ACTIVE; + + return active ? true : false; +} + +/* If the other interface of the controller is receiving data */ +static inline bool other_rx_active(struct i2s_dai *i2s) +{ + struct i2s_dai *other = get_other_dai(i2s); + + return rx_active(other); +} + +/* If any interface of the controller is receiving data */ +static inline bool any_rx_active(struct i2s_dai *i2s) +{ + return rx_active(i2s) || other_rx_active(i2s); +} + +/* If the other DAI is transmitting or receiving data */ +static inline bool other_active(struct i2s_dai *i2s) +{ + return other_rx_active(i2s) || other_tx_active(i2s); +} + +/* If this DAI is transmitting or receiving data */ +static inline bool this_active(struct i2s_dai *i2s) +{ + return tx_active(i2s) || rx_active(i2s); +} + +/* If the controller is active anyway */ +static inline bool any_active(struct i2s_dai *i2s) +{ + return this_active(i2s) || other_active(i2s); +} + +static inline struct i2s_dai *to_info(struct snd_soc_dai *dai) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + + return &priv->dai[dai->id - 1]; +} + +static inline bool is_opened(struct i2s_dai *i2s) +{ + if (i2s && (i2s->mode & DAI_OPENED)) + return true; + else + return false; +} + +static inline bool is_manager(struct i2s_dai *i2s) +{ + if (is_opened(i2s) && (i2s->mode & DAI_MANAGER)) + return true; + else + return false; +} + +/* Read RCLK of I2S (in multiples of LRCLK) */ +static inline unsigned get_rfs(struct i2s_dai *i2s) +{ + struct samsung_i2s_priv *priv = i2s->priv; + u32 rfs; + + rfs = readl(priv->addr + I2SMOD) >> priv->variant_regs->rfs_off; + rfs &= priv->variant_regs->rfs_mask; + + switch (rfs) { + case 7: return 192; + case 6: return 96; + case 5: return 128; + case 4: return 64; + case 3: return 768; + case 2: return 384; + case 1: return 512; + default: return 256; + } +} + +/* Write RCLK of I2S (in multiples of LRCLK) */ +static inline void set_rfs(struct i2s_dai *i2s, unsigned rfs) +{ + struct samsung_i2s_priv *priv = i2s->priv; + u32 mod = readl(priv->addr + I2SMOD); + int rfs_shift = priv->variant_regs->rfs_off; + + mod &= ~(priv->variant_regs->rfs_mask << rfs_shift); + + switch (rfs) { + case 192: + mod |= (EXYNOS7_MOD_RCLK_192FS << rfs_shift); + break; + case 96: + mod |= (EXYNOS7_MOD_RCLK_96FS << rfs_shift); + break; + case 128: + mod |= (EXYNOS7_MOD_RCLK_128FS << rfs_shift); + break; + case 64: + mod |= (EXYNOS7_MOD_RCLK_64FS << rfs_shift); + break; + case 768: + mod |= (MOD_RCLK_768FS << rfs_shift); + break; + case 512: + mod |= (MOD_RCLK_512FS << rfs_shift); + break; + case 384: + mod |= (MOD_RCLK_384FS << rfs_shift); + break; + default: + mod |= (MOD_RCLK_256FS << rfs_shift); + break; + } + + writel(mod, priv->addr + I2SMOD); +} + +/* Read bit-clock of I2S (in multiples of LRCLK) */ +static inline unsigned get_bfs(struct i2s_dai *i2s) +{ + struct samsung_i2s_priv *priv = i2s->priv; + u32 bfs; + + bfs = readl(priv->addr + I2SMOD) >> priv->variant_regs->bfs_off; + bfs &= priv->variant_regs->bfs_mask; + + switch (bfs) { + case 8: return 256; + case 7: return 192; + case 6: return 128; + case 5: return 96; + case 4: return 64; + case 3: return 24; + case 2: return 16; + case 1: return 48; + default: return 32; + } +} + +/* Write bit-clock of I2S (in multiples of LRCLK) */ +static inline void set_bfs(struct i2s_dai *i2s, unsigned bfs) +{ + struct samsung_i2s_priv *priv = i2s->priv; + u32 mod = readl(priv->addr + I2SMOD); + int tdm = priv->quirks & QUIRK_SUPPORTS_TDM; + int bfs_shift = priv->variant_regs->bfs_off; + + /* Non-TDM I2S controllers do not support BCLK > 48 * FS */ + if (!tdm && bfs > 48) { + dev_err(&i2s->pdev->dev, "Unsupported BCLK divider\n"); + return; + } + + mod &= ~(priv->variant_regs->bfs_mask << bfs_shift); + + switch (bfs) { + case 48: + mod |= (MOD_BCLK_48FS << bfs_shift); + break; + case 32: + mod |= (MOD_BCLK_32FS << bfs_shift); + break; + case 24: + mod |= (MOD_BCLK_24FS << bfs_shift); + break; + case 16: + mod |= (MOD_BCLK_16FS << bfs_shift); + break; + case 64: + mod |= (EXYNOS5420_MOD_BCLK_64FS << bfs_shift); + break; + case 96: + mod |= (EXYNOS5420_MOD_BCLK_96FS << bfs_shift); + break; + case 128: + mod |= (EXYNOS5420_MOD_BCLK_128FS << bfs_shift); + break; + case 192: + mod |= (EXYNOS5420_MOD_BCLK_192FS << bfs_shift); + break; + case 256: + mod |= (EXYNOS5420_MOD_BCLK_256FS << bfs_shift); + break; + default: + dev_err(&i2s->pdev->dev, "Wrong BCLK Divider!\n"); + return; + } + + writel(mod, priv->addr + I2SMOD); +} + +/* Sample size */ +static inline int get_blc(struct i2s_dai *i2s) +{ + int blc = readl(i2s->priv->addr + I2SMOD); + + blc = (blc >> 13) & 0x3; + + switch (blc) { + case 2: return 24; + case 1: return 8; + default: return 16; + } +} + +/* TX channel control */ +static void i2s_txctrl(struct i2s_dai *i2s, int on) +{ + struct samsung_i2s_priv *priv = i2s->priv; + void __iomem *addr = priv->addr; + int txr_off = priv->variant_regs->txr_off; + u32 con = readl(addr + I2SCON); + u32 mod = readl(addr + I2SMOD) & ~(3 << txr_off); + + if (on) { + con |= CON_ACTIVE; + con &= ~CON_TXCH_PAUSE; + + if (is_secondary(i2s)) { + con |= CON_TXSDMA_ACTIVE; + con &= ~CON_TXSDMA_PAUSE; + } else { + con |= CON_TXDMA_ACTIVE; + con &= ~CON_TXDMA_PAUSE; + } + + if (any_rx_active(i2s)) + mod |= 2 << txr_off; + else + mod |= 0 << txr_off; + } else { + if (is_secondary(i2s)) { + con |= CON_TXSDMA_PAUSE; + con &= ~CON_TXSDMA_ACTIVE; + } else { + con |= CON_TXDMA_PAUSE; + con &= ~CON_TXDMA_ACTIVE; + } + + if (other_tx_active(i2s)) { + writel(con, addr + I2SCON); + return; + } + + con |= CON_TXCH_PAUSE; + + if (any_rx_active(i2s)) + mod |= 1 << txr_off; + else + con &= ~CON_ACTIVE; + } + + writel(mod, addr + I2SMOD); + writel(con, addr + I2SCON); +} + +/* RX Channel Control */ +static void i2s_rxctrl(struct i2s_dai *i2s, int on) +{ + struct samsung_i2s_priv *priv = i2s->priv; + void __iomem *addr = priv->addr; + int txr_off = priv->variant_regs->txr_off; + u32 con = readl(addr + I2SCON); + u32 mod = readl(addr + I2SMOD) & ~(3 << txr_off); + + if (on) { + con |= CON_RXDMA_ACTIVE | CON_ACTIVE; + con &= ~(CON_RXDMA_PAUSE | CON_RXCH_PAUSE); + + if (any_tx_active(i2s)) + mod |= 2 << txr_off; + else + mod |= 1 << txr_off; + } else { + con |= CON_RXDMA_PAUSE | CON_RXCH_PAUSE; + con &= ~CON_RXDMA_ACTIVE; + + if (any_tx_active(i2s)) + mod |= 0 << txr_off; + else + con &= ~CON_ACTIVE; + } + + writel(mod, addr + I2SMOD); + writel(con, addr + I2SCON); +} + +/* Flush FIFO of an interface */ +static inline void i2s_fifo(struct i2s_dai *i2s, u32 flush) +{ + void __iomem *fic; + u32 val; + + if (!i2s) + return; + + if (is_secondary(i2s)) + fic = i2s->priv->addr + I2SFICS; + else + fic = i2s->priv->addr + I2SFIC; + + /* Flush the FIFO */ + writel(readl(fic) | flush, fic); + + /* Be patient */ + val = msecs_to_loops(1) / 1000; /* 1 usec */ + while (--val) + cpu_relax(); + + writel(readl(fic) & ~flush, fic); +} + +static int i2s_set_sysclk(struct snd_soc_dai *dai, int clk_id, unsigned int rfs, + int dir) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + struct i2s_dai *i2s = to_info(dai); + struct i2s_dai *other = get_other_dai(i2s); + const struct samsung_i2s_variant_regs *i2s_regs = priv->variant_regs; + unsigned int cdcon_mask = 1 << i2s_regs->cdclkcon_off; + unsigned int rsrc_mask = 1 << i2s_regs->rclksrc_off; + u32 mod, mask, val = 0; + unsigned long flags; + int ret = 0; + + pm_runtime_get_sync(dai->dev); + + spin_lock_irqsave(&priv->lock, flags); + mod = readl(priv->addr + I2SMOD); + spin_unlock_irqrestore(&priv->lock, flags); + + switch (clk_id) { + case SAMSUNG_I2S_OPCLK: + mask = MOD_OPCLK_MASK; + val = (dir << MOD_OPCLK_SHIFT) & MOD_OPCLK_MASK; + break; + case SAMSUNG_I2S_CDCLK: + mask = 1 << i2s_regs->cdclkcon_off; + /* Shouldn't matter in GATING(CLOCK_IN) mode */ + if (dir == SND_SOC_CLOCK_IN) + rfs = 0; + + if ((rfs && other && other->rfs && (other->rfs != rfs)) || + (any_active(i2s) && + (((dir == SND_SOC_CLOCK_IN) + && !(mod & cdcon_mask)) || + ((dir == SND_SOC_CLOCK_OUT) + && (mod & cdcon_mask))))) { + dev_err(&i2s->pdev->dev, + "%s:%d Other DAI busy\n", __func__, __LINE__); + ret = -EAGAIN; + goto err; + } + + if (dir == SND_SOC_CLOCK_IN) + val = 1 << i2s_regs->cdclkcon_off; + + i2s->rfs = rfs; + break; + + case SAMSUNG_I2S_RCLKSRC_0: /* clock corrsponding to IISMOD[10] := 0 */ + case SAMSUNG_I2S_RCLKSRC_1: /* clock corrsponding to IISMOD[10] := 1 */ + mask = 1 << i2s_regs->rclksrc_off; + + if ((priv->quirks & QUIRK_NO_MUXPSR) + || (clk_id == SAMSUNG_I2S_RCLKSRC_0)) + clk_id = 0; + else + clk_id = 1; + + if (!any_active(i2s)) { + if (priv->op_clk && !IS_ERR(priv->op_clk)) { + if ((clk_id && !(mod & rsrc_mask)) || + (!clk_id && (mod & rsrc_mask))) { + clk_disable_unprepare(priv->op_clk); + clk_put(priv->op_clk); + } else { + priv->rclk_srcrate = + clk_get_rate(priv->op_clk); + goto done; + } + } + + if (clk_id) + priv->op_clk = clk_get(&i2s->pdev->dev, + "i2s_opclk1"); + else + priv->op_clk = clk_get(&i2s->pdev->dev, + "i2s_opclk0"); + + if (WARN_ON(IS_ERR(priv->op_clk))) { + ret = PTR_ERR(priv->op_clk); + priv->op_clk = NULL; + goto err; + } + + ret = clk_prepare_enable(priv->op_clk); + if (ret) { + clk_put(priv->op_clk); + priv->op_clk = NULL; + goto err; + } + priv->rclk_srcrate = clk_get_rate(priv->op_clk); + + } else if ((!clk_id && (mod & rsrc_mask)) + || (clk_id && !(mod & rsrc_mask))) { + dev_err(&i2s->pdev->dev, + "%s:%d Other DAI busy\n", __func__, __LINE__); + ret = -EAGAIN; + goto err; + } else { + /* Call can't be on the active DAI */ + goto done; + } + + if (clk_id == 1) + val = 1 << i2s_regs->rclksrc_off; + break; + default: + dev_err(&i2s->pdev->dev, "We don't serve that!\n"); + ret = -EINVAL; + goto err; + } + + spin_lock_irqsave(&priv->lock, flags); + mod = readl(priv->addr + I2SMOD); + mod = (mod & ~mask) | val; + writel(mod, priv->addr + I2SMOD); + spin_unlock_irqrestore(&priv->lock, flags); +done: + pm_runtime_put(dai->dev); + + return 0; +err: + pm_runtime_put(dai->dev); + return ret; +} + +static int i2s_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + struct i2s_dai *i2s = to_info(dai); + int lrp_shift, sdf_shift, sdf_mask, lrp_rlow, mod_slave; + u32 mod, tmp = 0; + unsigned long flags; + + lrp_shift = priv->variant_regs->lrp_off; + sdf_shift = priv->variant_regs->sdf_off; + mod_slave = 1 << priv->variant_regs->mss_off; + + sdf_mask = MOD_SDF_MASK << sdf_shift; + lrp_rlow = MOD_LR_RLOW << lrp_shift; + + /* Format is priority */ + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_RIGHT_J: + tmp |= lrp_rlow; + tmp |= (MOD_SDF_MSB << sdf_shift); + break; + case SND_SOC_DAIFMT_LEFT_J: + tmp |= lrp_rlow; + tmp |= (MOD_SDF_LSB << sdf_shift); + break; + case SND_SOC_DAIFMT_I2S: + tmp |= (MOD_SDF_IIS << sdf_shift); + break; + default: + dev_err(&i2s->pdev->dev, "Format not supported\n"); + return -EINVAL; + } + + /* + * INV flag is relative to the FORMAT flag - if set it simply + * flips the polarity specified by the Standard + */ + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + break; + case SND_SOC_DAIFMT_NB_IF: + if (tmp & lrp_rlow) + tmp &= ~lrp_rlow; + else + tmp |= lrp_rlow; + break; + default: + dev_err(&i2s->pdev->dev, "Polarity not supported\n"); + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) { + case SND_SOC_DAIFMT_BC_FC: + tmp |= mod_slave; + break; + case SND_SOC_DAIFMT_BP_FP: + /* + * Set default source clock in Master mode, only when the + * CLK_I2S_RCLK_SRC clock is not exposed so we ensure any + * clock configuration assigned in DT is not overwritten. + */ + if (priv->rclk_srcrate == 0 && priv->clk_data.clks == NULL) + i2s_set_sysclk(dai, SAMSUNG_I2S_RCLKSRC_0, + 0, SND_SOC_CLOCK_IN); + break; + default: + dev_err(&i2s->pdev->dev, "master/slave format not supported\n"); + return -EINVAL; + } + + pm_runtime_get_sync(dai->dev); + spin_lock_irqsave(&priv->lock, flags); + mod = readl(priv->addr + I2SMOD); + /* + * Don't change the I2S mode if any controller is active on this + * channel. + */ + if (any_active(i2s) && + ((mod & (sdf_mask | lrp_rlow | mod_slave)) != tmp)) { + spin_unlock_irqrestore(&priv->lock, flags); + pm_runtime_put(dai->dev); + dev_err(&i2s->pdev->dev, + "%s:%d Other DAI busy\n", __func__, __LINE__); + return -EAGAIN; + } + + mod &= ~(sdf_mask | lrp_rlow | mod_slave); + mod |= tmp; + writel(mod, priv->addr + I2SMOD); + priv->slave_mode = (mod & mod_slave); + spin_unlock_irqrestore(&priv->lock, flags); + pm_runtime_put(dai->dev); + + return 0; +} + +static int i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, struct snd_soc_dai *dai) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + struct i2s_dai *i2s = to_info(dai); + u32 mod, mask = 0, val = 0; + struct clk *rclksrc; + unsigned long flags; + + WARN_ON(!pm_runtime_active(dai->dev)); + + if (!is_secondary(i2s)) + mask |= (MOD_DC2_EN | MOD_DC1_EN); + + switch (params_channels(params)) { + case 6: + val |= MOD_DC2_EN; + fallthrough; + case 4: + val |= MOD_DC1_EN; + break; + case 2: + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + i2s->dma_playback.addr_width = 4; + else + i2s->dma_capture.addr_width = 4; + break; + case 1: + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + i2s->dma_playback.addr_width = 2; + else + i2s->dma_capture.addr_width = 2; + + break; + default: + dev_err(&i2s->pdev->dev, "%d channels not supported\n", + params_channels(params)); + return -EINVAL; + } + + if (is_secondary(i2s)) + mask |= MOD_BLCS_MASK; + else + mask |= MOD_BLCP_MASK; + + if (is_manager(i2s)) + mask |= MOD_BLC_MASK; + + switch (params_width(params)) { + case 8: + if (is_secondary(i2s)) + val |= MOD_BLCS_8BIT; + else + val |= MOD_BLCP_8BIT; + if (is_manager(i2s)) + val |= MOD_BLC_8BIT; + break; + case 16: + if (is_secondary(i2s)) + val |= MOD_BLCS_16BIT; + else + val |= MOD_BLCP_16BIT; + if (is_manager(i2s)) + val |= MOD_BLC_16BIT; + break; + case 24: + if (is_secondary(i2s)) + val |= MOD_BLCS_24BIT; + else + val |= MOD_BLCP_24BIT; + if (is_manager(i2s)) + val |= MOD_BLC_24BIT; + break; + default: + dev_err(&i2s->pdev->dev, "Format(%d) not supported\n", + params_format(params)); + return -EINVAL; + } + + spin_lock_irqsave(&priv->lock, flags); + mod = readl(priv->addr + I2SMOD); + mod = (mod & ~mask) | val; + writel(mod, priv->addr + I2SMOD); + spin_unlock_irqrestore(&priv->lock, flags); + + snd_soc_dai_init_dma_data(dai, &i2s->dma_playback, &i2s->dma_capture); + + i2s->frmclk = params_rate(params); + + rclksrc = priv->clk_table[CLK_I2S_RCLK_SRC]; + if (rclksrc && !IS_ERR(rclksrc)) + priv->rclk_srcrate = clk_get_rate(rclksrc); + + return 0; +} + +/* We set constraints on the substream according to the version of I2S */ +static int i2s_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + struct i2s_dai *i2s = to_info(dai); + struct i2s_dai *other = get_other_dai(i2s); + unsigned long flags; + + pm_runtime_get_sync(dai->dev); + + spin_lock_irqsave(&priv->pcm_lock, flags); + + i2s->mode |= DAI_OPENED; + + if (is_manager(other)) + i2s->mode &= ~DAI_MANAGER; + else + i2s->mode |= DAI_MANAGER; + + if (!any_active(i2s) && (priv->quirks & QUIRK_NEED_RSTCLR)) + writel(CON_RSTCLR, i2s->priv->addr + I2SCON); + + spin_unlock_irqrestore(&priv->pcm_lock, flags); + + return 0; +} + +static void i2s_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + struct i2s_dai *i2s = to_info(dai); + struct i2s_dai *other = get_other_dai(i2s); + unsigned long flags; + + spin_lock_irqsave(&priv->pcm_lock, flags); + + i2s->mode &= ~DAI_OPENED; + i2s->mode &= ~DAI_MANAGER; + + if (is_opened(other)) + other->mode |= DAI_MANAGER; + + /* Reset any constraint on RFS and BFS */ + i2s->rfs = 0; + i2s->bfs = 0; + + spin_unlock_irqrestore(&priv->pcm_lock, flags); + + pm_runtime_put(dai->dev); +} + +static int config_setup(struct i2s_dai *i2s) +{ + struct samsung_i2s_priv *priv = i2s->priv; + struct i2s_dai *other = get_other_dai(i2s); + unsigned rfs, bfs, blc; + u32 psr; + + blc = get_blc(i2s); + + bfs = i2s->bfs; + + if (!bfs && other) + bfs = other->bfs; + + /* Select least possible multiple(2) if no constraint set */ + if (!bfs) + bfs = blc * 2; + + rfs = i2s->rfs; + + if (!rfs && other) + rfs = other->rfs; + + if ((rfs == 256 || rfs == 512) && (blc == 24)) { + dev_err(&i2s->pdev->dev, + "%d-RFS not supported for 24-blc\n", rfs); + return -EINVAL; + } + + if (!rfs) { + if (bfs == 16 || bfs == 32) + rfs = 256; + else + rfs = 384; + } + + /* If already setup and running */ + if (any_active(i2s) && (get_rfs(i2s) != rfs || get_bfs(i2s) != bfs)) { + dev_err(&i2s->pdev->dev, + "%s:%d Other DAI busy\n", __func__, __LINE__); + return -EAGAIN; + } + + set_bfs(i2s, bfs); + set_rfs(i2s, rfs); + + /* Don't bother with PSR in Slave mode */ + if (priv->slave_mode) + return 0; + + if (!(priv->quirks & QUIRK_NO_MUXPSR)) { + psr = priv->rclk_srcrate / i2s->frmclk / rfs; + writel(((psr - 1) << 8) | PSR_PSREN, priv->addr + I2SPSR); + dev_dbg(&i2s->pdev->dev, + "RCLK_SRC=%luHz PSR=%u, RCLK=%dfs, BCLK=%dfs\n", + priv->rclk_srcrate, psr, rfs, bfs); + } + + return 0; +} + +static int i2s_trigger(struct snd_pcm_substream *substream, + int cmd, struct snd_soc_dai *dai) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + int capture = (substream->stream == SNDRV_PCM_STREAM_CAPTURE); + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct i2s_dai *i2s = to_info(asoc_rtd_to_cpu(rtd, 0)); + unsigned long flags; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + pm_runtime_get_sync(dai->dev); + + if (priv->fixup_early) + priv->fixup_early(substream, dai); + + spin_lock_irqsave(&priv->lock, flags); + + if (config_setup(i2s)) { + spin_unlock_irqrestore(&priv->lock, flags); + return -EINVAL; + } + + if (priv->fixup_late) + priv->fixup_late(substream, dai); + + if (capture) + i2s_rxctrl(i2s, 1); + else + i2s_txctrl(i2s, 1); + + spin_unlock_irqrestore(&priv->lock, flags); + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + spin_lock_irqsave(&priv->lock, flags); + + if (capture) { + i2s_rxctrl(i2s, 0); + i2s_fifo(i2s, FIC_RXFLUSH); + } else { + i2s_txctrl(i2s, 0); + i2s_fifo(i2s, FIC_TXFLUSH); + } + + spin_unlock_irqrestore(&priv->lock, flags); + pm_runtime_put(dai->dev); + break; + } + + return 0; +} + +static int i2s_set_clkdiv(struct snd_soc_dai *dai, + int div_id, int div) +{ + struct i2s_dai *i2s = to_info(dai); + struct i2s_dai *other = get_other_dai(i2s); + + switch (div_id) { + case SAMSUNG_I2S_DIV_BCLK: + pm_runtime_get_sync(dai->dev); + if ((any_active(i2s) && div && (get_bfs(i2s) != div)) + || (other && other->bfs && (other->bfs != div))) { + pm_runtime_put(dai->dev); + dev_err(&i2s->pdev->dev, + "%s:%d Other DAI busy\n", __func__, __LINE__); + return -EAGAIN; + } + i2s->bfs = div; + pm_runtime_put(dai->dev); + break; + default: + dev_err(&i2s->pdev->dev, + "Invalid clock divider(%d)\n", div_id); + return -EINVAL; + } + + return 0; +} + +static snd_pcm_sframes_t +i2s_delay(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + struct i2s_dai *i2s = to_info(dai); + u32 reg = readl(priv->addr + I2SFIC); + snd_pcm_sframes_t delay; + + WARN_ON(!pm_runtime_active(dai->dev)); + + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) + delay = FIC_RXCOUNT(reg); + else if (is_secondary(i2s)) + delay = FICS_TXCOUNT(readl(priv->addr + I2SFICS)); + else + delay = (reg >> priv->variant_regs->ftx0cnt_off) & 0x7f; + + return delay; +} + +#ifdef CONFIG_PM +static int i2s_suspend(struct snd_soc_component *component) +{ + return pm_runtime_force_suspend(component->dev); +} + +static int i2s_resume(struct snd_soc_component *component) +{ + return pm_runtime_force_resume(component->dev); +} +#else +#define i2s_suspend NULL +#define i2s_resume NULL +#endif + +static int samsung_i2s_dai_probe(struct snd_soc_dai *dai) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + struct i2s_dai *i2s = to_info(dai); + struct i2s_dai *other = get_other_dai(i2s); + unsigned long flags; + + pm_runtime_get_sync(dai->dev); + + if (is_secondary(i2s)) { + /* If this is probe on the secondary DAI */ + snd_soc_dai_init_dma_data(dai, &i2s->dma_playback, NULL); + } else { + snd_soc_dai_init_dma_data(dai, &i2s->dma_playback, + &i2s->dma_capture); + + if (priv->quirks & QUIRK_NEED_RSTCLR) + writel(CON_RSTCLR, priv->addr + I2SCON); + + if (priv->quirks & QUIRK_SUPPORTS_IDMA) + idma_reg_addr_init(priv->addr, + other->idma_playback.addr); + } + + /* Reset any constraint on RFS and BFS */ + i2s->rfs = 0; + i2s->bfs = 0; + + spin_lock_irqsave(&priv->lock, flags); + i2s_txctrl(i2s, 0); + i2s_rxctrl(i2s, 0); + i2s_fifo(i2s, FIC_TXFLUSH); + i2s_fifo(other, FIC_TXFLUSH); + i2s_fifo(i2s, FIC_RXFLUSH); + spin_unlock_irqrestore(&priv->lock, flags); + + /* Gate CDCLK by default */ + if (!is_opened(other)) + i2s_set_sysclk(dai, SAMSUNG_I2S_CDCLK, + 0, SND_SOC_CLOCK_IN); + pm_runtime_put(dai->dev); + + return 0; +} + +static int samsung_i2s_dai_remove(struct snd_soc_dai *dai) +{ + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + struct i2s_dai *i2s = to_info(dai); + unsigned long flags; + + pm_runtime_get_sync(dai->dev); + + if (!is_secondary(i2s)) { + if (priv->quirks & QUIRK_NEED_RSTCLR) { + spin_lock_irqsave(&priv->lock, flags); + writel(0, priv->addr + I2SCON); + spin_unlock_irqrestore(&priv->lock, flags); + } + } + + pm_runtime_put(dai->dev); + + return 0; +} + +static const struct snd_soc_dai_ops samsung_i2s_dai_ops = { + .probe = samsung_i2s_dai_probe, + .remove = samsung_i2s_dai_remove, + .trigger = i2s_trigger, + .hw_params = i2s_hw_params, + .set_fmt = i2s_set_fmt, + .set_clkdiv = i2s_set_clkdiv, + .set_sysclk = i2s_set_sysclk, + .startup = i2s_startup, + .shutdown = i2s_shutdown, + .delay = i2s_delay, +}; + +static const struct snd_soc_dapm_widget samsung_i2s_widgets[] = { + /* Backend DAI */ + SND_SOC_DAPM_AIF_OUT("Mixer DAI TX", NULL, 0, SND_SOC_NOPM, 0, 0), + SND_SOC_DAPM_AIF_IN("Mixer DAI RX", NULL, 0, SND_SOC_NOPM, 0, 0), + + /* Playback Mixer */ + SND_SOC_DAPM_MIXER("Playback Mixer", SND_SOC_NOPM, 0, 0, NULL, 0), +}; + +static const struct snd_soc_dapm_route samsung_i2s_dapm_routes[] = { + { "Playback Mixer", NULL, "Primary Playback" }, + { "Playback Mixer", NULL, "Secondary Playback" }, + + { "Mixer DAI TX", NULL, "Playback Mixer" }, + { "Primary Capture", NULL, "Mixer DAI RX" }, +}; + +static const struct snd_soc_component_driver samsung_i2s_component = { + .name = "samsung-i2s", + + .dapm_widgets = samsung_i2s_widgets, + .num_dapm_widgets = ARRAY_SIZE(samsung_i2s_widgets), + + .dapm_routes = samsung_i2s_dapm_routes, + .num_dapm_routes = ARRAY_SIZE(samsung_i2s_dapm_routes), + + .suspend = i2s_suspend, + .resume = i2s_resume, + + .legacy_dai_naming = 1, +}; + +#define SAMSUNG_I2S_FMTS (SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE | \ + SNDRV_PCM_FMTBIT_S24_LE) + +static int i2s_alloc_dais(struct samsung_i2s_priv *priv, + const struct samsung_i2s_dai_data *i2s_dai_data, + int num_dais) +{ + static const char *dai_names[] = { "samsung-i2s", "samsung-i2s-sec" }; + static const char *stream_names[] = { "Primary Playback", + "Secondary Playback" }; + struct snd_soc_dai_driver *dai_drv; + int i; + + priv->dai = devm_kcalloc(&priv->pdev->dev, num_dais, + sizeof(struct i2s_dai), GFP_KERNEL); + if (!priv->dai) + return -ENOMEM; + + priv->dai_drv = devm_kcalloc(&priv->pdev->dev, num_dais, + sizeof(*dai_drv), GFP_KERNEL); + if (!priv->dai_drv) + return -ENOMEM; + + for (i = 0; i < num_dais; i++) { + dai_drv = &priv->dai_drv[i]; + + dai_drv->symmetric_rate = 1; + dai_drv->ops = &samsung_i2s_dai_ops; + + dai_drv->playback.channels_min = 1; + dai_drv->playback.channels_max = 2; + dai_drv->playback.rates = i2s_dai_data->pcm_rates; + dai_drv->playback.formats = SAMSUNG_I2S_FMTS; + dai_drv->playback.stream_name = stream_names[i]; + + dai_drv->id = i + 1; + dai_drv->name = dai_names[i]; + + priv->dai[i].drv = &priv->dai_drv[i]; + priv->dai[i].pdev = priv->pdev; + } + + /* Initialize capture only for the primary DAI */ + dai_drv = &priv->dai_drv[SAMSUNG_I2S_ID_PRIMARY - 1]; + + dai_drv->capture.channels_min = 1; + dai_drv->capture.channels_max = 2; + dai_drv->capture.rates = i2s_dai_data->pcm_rates; + dai_drv->capture.formats = SAMSUNG_I2S_FMTS; + dai_drv->capture.stream_name = "Primary Capture"; + + return 0; +} + +#ifdef CONFIG_PM +static int i2s_runtime_suspend(struct device *dev) +{ + struct samsung_i2s_priv *priv = dev_get_drvdata(dev); + + priv->suspend_i2smod = readl(priv->addr + I2SMOD); + priv->suspend_i2scon = readl(priv->addr + I2SCON); + priv->suspend_i2spsr = readl(priv->addr + I2SPSR); + + clk_disable_unprepare(priv->op_clk); + clk_disable_unprepare(priv->clk); + + return 0; +} + +static int i2s_runtime_resume(struct device *dev) +{ + struct samsung_i2s_priv *priv = dev_get_drvdata(dev); + int ret; + + ret = clk_prepare_enable(priv->clk); + if (ret) + return ret; + + if (priv->op_clk) { + ret = clk_prepare_enable(priv->op_clk); + if (ret) { + clk_disable_unprepare(priv->clk); + return ret; + } + } + + writel(priv->suspend_i2scon, priv->addr + I2SCON); + writel(priv->suspend_i2smod, priv->addr + I2SMOD); + writel(priv->suspend_i2spsr, priv->addr + I2SPSR); + + return 0; +} +#endif /* CONFIG_PM */ + +static void i2s_unregister_clocks(struct samsung_i2s_priv *priv) +{ + int i; + + for (i = 0; i < priv->clk_data.clk_num; i++) { + if (!IS_ERR(priv->clk_table[i])) + clk_unregister(priv->clk_table[i]); + } +} + +static void i2s_unregister_clock_provider(struct samsung_i2s_priv *priv) +{ + of_clk_del_provider(priv->pdev->dev.of_node); + i2s_unregister_clocks(priv); +} + + +static int i2s_register_clock_provider(struct samsung_i2s_priv *priv) +{ + + const char * const i2s_clk_desc[] = { "cdclk", "rclk_src", "prescaler" }; + const char *clk_name[2] = { "i2s_opclk0", "i2s_opclk1" }; + const char *p_names[2] = { NULL }; + struct device *dev = &priv->pdev->dev; + const struct samsung_i2s_variant_regs *reg_info = priv->variant_regs; + const char *i2s_clk_name[ARRAY_SIZE(i2s_clk_desc)]; + struct clk *rclksrc; + int ret, i; + + /* Register the clock provider only if it's expected in the DTB */ + if (!of_property_present(dev->of_node, "#clock-cells")) + return 0; + + /* Get the RCLKSRC mux clock parent clock names */ + for (i = 0; i < ARRAY_SIZE(p_names); i++) { + rclksrc = clk_get(dev, clk_name[i]); + if (IS_ERR(rclksrc)) + continue; + p_names[i] = __clk_get_name(rclksrc); + clk_put(rclksrc); + } + + for (i = 0; i < ARRAY_SIZE(i2s_clk_desc); i++) { + i2s_clk_name[i] = devm_kasprintf(dev, GFP_KERNEL, "%s_%s", + dev_name(dev), i2s_clk_desc[i]); + if (!i2s_clk_name[i]) + return -ENOMEM; + } + + if (!(priv->quirks & QUIRK_NO_MUXPSR)) { + /* Activate the prescaler */ + u32 val = readl(priv->addr + I2SPSR); + writel(val | PSR_PSREN, priv->addr + I2SPSR); + + priv->clk_table[CLK_I2S_RCLK_SRC] = clk_register_mux(dev, + i2s_clk_name[CLK_I2S_RCLK_SRC], p_names, + ARRAY_SIZE(p_names), + CLK_SET_RATE_NO_REPARENT | CLK_SET_RATE_PARENT, + priv->addr + I2SMOD, reg_info->rclksrc_off, + 1, 0, &priv->lock); + + priv->clk_table[CLK_I2S_RCLK_PSR] = clk_register_divider(dev, + i2s_clk_name[CLK_I2S_RCLK_PSR], + i2s_clk_name[CLK_I2S_RCLK_SRC], + CLK_SET_RATE_PARENT, + priv->addr + I2SPSR, 8, 6, 0, &priv->lock); + + p_names[0] = i2s_clk_name[CLK_I2S_RCLK_PSR]; + priv->clk_data.clk_num = 2; + } + + priv->clk_table[CLK_I2S_CDCLK] = clk_register_gate(dev, + i2s_clk_name[CLK_I2S_CDCLK], p_names[0], + CLK_SET_RATE_PARENT, + priv->addr + I2SMOD, reg_info->cdclkcon_off, + CLK_GATE_SET_TO_DISABLE, &priv->lock); + + priv->clk_data.clk_num += 1; + priv->clk_data.clks = priv->clk_table; + + ret = of_clk_add_provider(dev->of_node, of_clk_src_onecell_get, + &priv->clk_data); + if (ret < 0) { + dev_err(dev, "failed to add clock provider: %d\n", ret); + i2s_unregister_clocks(priv); + } + + return ret; +} + +/* Create platform device for the secondary PCM */ +static int i2s_create_secondary_device(struct samsung_i2s_priv *priv) +{ + struct platform_device *pdev_sec; + const char *devname; + int ret; + + devname = devm_kasprintf(&priv->pdev->dev, GFP_KERNEL, "%s-sec", + dev_name(&priv->pdev->dev)); + if (!devname) + return -ENOMEM; + + pdev_sec = platform_device_alloc(devname, -1); + if (!pdev_sec) + return -ENOMEM; + + pdev_sec->driver_override = kstrdup("samsung-i2s", GFP_KERNEL); + if (!pdev_sec->driver_override) { + platform_device_put(pdev_sec); + return -ENOMEM; + } + + ret = platform_device_add(pdev_sec); + if (ret < 0) { + platform_device_put(pdev_sec); + return ret; + } + + ret = device_attach(&pdev_sec->dev); + if (ret <= 0) { + platform_device_unregister(priv->pdev_sec); + dev_info(&pdev_sec->dev, "device_attach() failed\n"); + return ret; + } + + priv->pdev_sec = pdev_sec; + + return 0; +} + +static void i2s_delete_secondary_device(struct samsung_i2s_priv *priv) +{ + platform_device_unregister(priv->pdev_sec); + priv->pdev_sec = NULL; +} + +static int samsung_i2s_probe(struct platform_device *pdev) +{ + struct i2s_dai *pri_dai, *sec_dai = NULL; + struct s3c_audio_pdata *i2s_pdata = pdev->dev.platform_data; + u32 regs_base, idma_addr = 0; + struct device_node *np = pdev->dev.of_node; + const struct samsung_i2s_dai_data *i2s_dai_data; + const struct platform_device_id *id; + struct samsung_i2s_priv *priv; + struct resource *res; + int num_dais, ret; + + if (IS_ENABLED(CONFIG_OF) && pdev->dev.of_node) { + i2s_dai_data = of_device_get_match_data(&pdev->dev); + } else { + id = platform_get_device_id(pdev); + + /* Nothing to do if it is the secondary device probe */ + if (!id) + return 0; + + i2s_dai_data = (struct samsung_i2s_dai_data *)id->driver_data; + } + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + if (np) { + priv->quirks = i2s_dai_data->quirks; + priv->fixup_early = i2s_dai_data->fixup_early; + priv->fixup_late = i2s_dai_data->fixup_late; + } else { + if (!i2s_pdata) { + dev_err(&pdev->dev, "Missing platform data\n"); + return -EINVAL; + } + priv->quirks = i2s_pdata->type.quirks; + } + + num_dais = (priv->quirks & QUIRK_SEC_DAI) ? 2 : 1; + priv->pdev = pdev; + priv->variant_regs = i2s_dai_data->i2s_variant_regs; + + ret = i2s_alloc_dais(priv, i2s_dai_data, num_dais); + if (ret < 0) + return ret; + + pri_dai = &priv->dai[SAMSUNG_I2S_ID_PRIMARY - 1]; + + spin_lock_init(&priv->lock); + spin_lock_init(&priv->pcm_lock); + + if (!np) { + pri_dai->dma_playback.filter_data = i2s_pdata->dma_playback; + pri_dai->dma_capture.filter_data = i2s_pdata->dma_capture; + pri_dai->filter = i2s_pdata->dma_filter; + + idma_addr = i2s_pdata->type.idma_addr; + } else { + if (of_property_read_u32(np, "samsung,idma-addr", + &idma_addr)) { + if (priv->quirks & QUIRK_SUPPORTS_IDMA) { + dev_info(&pdev->dev, "idma address is not"\ + "specified"); + } + } + } + + priv->addr = devm_platform_get_and_ioremap_resource(pdev, 0, &res); + if (IS_ERR(priv->addr)) + return PTR_ERR(priv->addr); + + regs_base = res->start; + + priv->clk = devm_clk_get(&pdev->dev, "iis"); + if (IS_ERR(priv->clk)) { + dev_err(&pdev->dev, "Failed to get iis clock\n"); + return PTR_ERR(priv->clk); + } + + ret = clk_prepare_enable(priv->clk); + if (ret != 0) { + dev_err(&pdev->dev, "failed to enable clock: %d\n", ret); + return ret; + } + pri_dai->dma_playback.addr = regs_base + I2STXD; + pri_dai->dma_capture.addr = regs_base + I2SRXD; + pri_dai->dma_playback.chan_name = "tx"; + pri_dai->dma_capture.chan_name = "rx"; + pri_dai->dma_playback.addr_width = 4; + pri_dai->dma_capture.addr_width = 4; + pri_dai->priv = priv; + + if (priv->quirks & QUIRK_PRI_6CHAN) + pri_dai->drv->playback.channels_max = 6; + + ret = samsung_asoc_dma_platform_register(&pdev->dev, pri_dai->filter, + "tx", "rx", NULL); + if (ret < 0) + goto err_disable_clk; + + if (priv->quirks & QUIRK_SEC_DAI) { + sec_dai = &priv->dai[SAMSUNG_I2S_ID_SECONDARY - 1]; + + sec_dai->dma_playback.addr = regs_base + I2STXDS; + sec_dai->dma_playback.chan_name = "tx-sec"; + + if (!np) { + sec_dai->dma_playback.filter_data = i2s_pdata->dma_play_sec; + sec_dai->filter = i2s_pdata->dma_filter; + } + + sec_dai->dma_playback.addr_width = 4; + sec_dai->idma_playback.addr = idma_addr; + sec_dai->pri_dai = pri_dai; + sec_dai->priv = priv; + pri_dai->sec_dai = sec_dai; + + ret = i2s_create_secondary_device(priv); + if (ret < 0) + goto err_disable_clk; + + ret = samsung_asoc_dma_platform_register(&priv->pdev_sec->dev, + sec_dai->filter, "tx-sec", NULL, + &pdev->dev); + if (ret < 0) + goto err_del_sec; + + } + + if (i2s_pdata && i2s_pdata->cfg_gpio && i2s_pdata->cfg_gpio(pdev)) { + dev_err(&pdev->dev, "Unable to configure gpio\n"); + ret = -EINVAL; + goto err_del_sec; + } + + dev_set_drvdata(&pdev->dev, priv); + + ret = devm_snd_soc_register_component(&pdev->dev, + &samsung_i2s_component, + priv->dai_drv, num_dais); + if (ret < 0) + goto err_del_sec; + + pm_runtime_set_active(&pdev->dev); + pm_runtime_enable(&pdev->dev); + + ret = i2s_register_clock_provider(priv); + if (ret < 0) + goto err_disable_pm; + + priv->op_clk = clk_get_parent(priv->clk_table[CLK_I2S_RCLK_SRC]); + + return 0; + +err_disable_pm: + pm_runtime_disable(&pdev->dev); +err_del_sec: + i2s_delete_secondary_device(priv); +err_disable_clk: + clk_disable_unprepare(priv->clk); + return ret; +} + +static void samsung_i2s_remove(struct platform_device *pdev) +{ + struct samsung_i2s_priv *priv = dev_get_drvdata(&pdev->dev); + + /* The secondary device has no driver data assigned */ + if (!priv) + return; + + pm_runtime_get_sync(&pdev->dev); + pm_runtime_disable(&pdev->dev); + + i2s_unregister_clock_provider(priv); + i2s_delete_secondary_device(priv); + clk_disable_unprepare(priv->clk); + + pm_runtime_put_noidle(&pdev->dev); +} + +static void fsd_i2s_fixup_early(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct i2s_dai *i2s = to_info(asoc_rtd_to_cpu(rtd, 0)); + struct i2s_dai *other = get_other_dai(i2s); + + if (!is_opened(other)) { + i2s_set_sysclk(dai, SAMSUNG_I2S_CDCLK, 0, SND_SOC_CLOCK_OUT); + i2s_set_sysclk(dai, SAMSUNG_I2S_OPCLK, 0, MOD_OPCLK_PCLK); + } +} + +static void fsd_i2s_fixup_late(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct samsung_i2s_priv *priv = snd_soc_dai_get_drvdata(dai); + struct i2s_dai *i2s = to_info(asoc_rtd_to_cpu(rtd, 0)); + struct i2s_dai *other = get_other_dai(i2s); + + if (!is_opened(other)) + writel(PSR_PSVAL(2) | PSR_PSREN, priv->addr + I2SPSR); +} + +static const struct samsung_i2s_variant_regs i2sv3_regs = { + .bfs_off = 1, + .rfs_off = 3, + .sdf_off = 5, + .txr_off = 8, + .rclksrc_off = 10, + .mss_off = 11, + .cdclkcon_off = 12, + .lrp_off = 7, + .bfs_mask = 0x3, + .rfs_mask = 0x3, + .ftx0cnt_off = 8, +}; + +static const struct samsung_i2s_variant_regs i2sv6_regs = { + .bfs_off = 0, + .rfs_off = 4, + .sdf_off = 6, + .txr_off = 8, + .rclksrc_off = 10, + .mss_off = 11, + .cdclkcon_off = 12, + .lrp_off = 15, + .bfs_mask = 0xf, + .rfs_mask = 0x3, + .ftx0cnt_off = 8, +}; + +static const struct samsung_i2s_variant_regs i2sv7_regs = { + .bfs_off = 0, + .rfs_off = 4, + .sdf_off = 7, + .txr_off = 9, + .rclksrc_off = 11, + .mss_off = 12, + .cdclkcon_off = 22, + .lrp_off = 15, + .bfs_mask = 0xf, + .rfs_mask = 0x7, + .ftx0cnt_off = 0, +}; + +static const struct samsung_i2s_variant_regs i2sv5_i2s1_regs = { + .bfs_off = 0, + .rfs_off = 3, + .sdf_off = 6, + .txr_off = 8, + .rclksrc_off = 10, + .mss_off = 11, + .cdclkcon_off = 12, + .lrp_off = 15, + .bfs_mask = 0x7, + .rfs_mask = 0x7, + .ftx0cnt_off = 8, +}; + +static const struct samsung_i2s_dai_data i2sv3_dai_type = { + .quirks = QUIRK_NO_MUXPSR, + .pcm_rates = SNDRV_PCM_RATE_8000_96000, + .i2s_variant_regs = &i2sv3_regs, +}; + +static const struct samsung_i2s_dai_data i2sv5_dai_type __maybe_unused = { + .quirks = QUIRK_PRI_6CHAN | QUIRK_SEC_DAI | QUIRK_NEED_RSTCLR | + QUIRK_SUPPORTS_IDMA, + .pcm_rates = SNDRV_PCM_RATE_8000_96000, + .i2s_variant_regs = &i2sv3_regs, +}; + +static const struct samsung_i2s_dai_data i2sv6_dai_type __maybe_unused = { + .quirks = QUIRK_PRI_6CHAN | QUIRK_SEC_DAI | QUIRK_NEED_RSTCLR | + QUIRK_SUPPORTS_TDM | QUIRK_SUPPORTS_IDMA, + .pcm_rates = SNDRV_PCM_RATE_8000_96000, + .i2s_variant_regs = &i2sv6_regs, +}; + +static const struct samsung_i2s_dai_data i2sv7_dai_type __maybe_unused = { + .quirks = QUIRK_PRI_6CHAN | QUIRK_SEC_DAI | QUIRK_NEED_RSTCLR | + QUIRK_SUPPORTS_TDM, + .pcm_rates = SNDRV_PCM_RATE_8000_192000, + .i2s_variant_regs = &i2sv7_regs, +}; + +static const struct samsung_i2s_dai_data i2sv5_dai_type_i2s1 __maybe_unused = { + .quirks = QUIRK_PRI_6CHAN | QUIRK_NEED_RSTCLR, + .pcm_rates = SNDRV_PCM_RATE_8000_96000, + .i2s_variant_regs = &i2sv5_i2s1_regs, +}; + +static const struct samsung_i2s_dai_data fsd_dai_type __maybe_unused = { + .quirks = QUIRK_SEC_DAI | QUIRK_NEED_RSTCLR | QUIRK_SUPPORTS_TDM, + .pcm_rates = SNDRV_PCM_RATE_8000_192000, + .i2s_variant_regs = &i2sv7_regs, + .fixup_early = fsd_i2s_fixup_early, + .fixup_late = fsd_i2s_fixup_late, +}; + +static const struct platform_device_id samsung_i2s_driver_ids[] = { + { + .name = "samsung-i2s", + .driver_data = (kernel_ulong_t)&i2sv3_dai_type, + }, + {}, +}; +MODULE_DEVICE_TABLE(platform, samsung_i2s_driver_ids); + +#ifdef CONFIG_OF +static const struct of_device_id exynos_i2s_match[] = { + { + .compatible = "samsung,s3c6410-i2s", + .data = &i2sv3_dai_type, + }, { + .compatible = "samsung,s5pv210-i2s", + .data = &i2sv5_dai_type, + }, { + .compatible = "samsung,exynos5420-i2s", + .data = &i2sv6_dai_type, + }, { + .compatible = "samsung,exynos7-i2s", + .data = &i2sv7_dai_type, + }, { + .compatible = "samsung,exynos7-i2s1", + .data = &i2sv5_dai_type_i2s1, + }, { + .compatible = "tesla,fsd-i2s", + .data = &fsd_dai_type, + }, + {}, +}; +MODULE_DEVICE_TABLE(of, exynos_i2s_match); +#endif + +static const struct dev_pm_ops samsung_i2s_pm = { + SET_RUNTIME_PM_OPS(i2s_runtime_suspend, + i2s_runtime_resume, NULL) + SET_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, + pm_runtime_force_resume) +}; + +static struct platform_driver samsung_i2s_driver = { + .probe = samsung_i2s_probe, + .remove_new = samsung_i2s_remove, + .id_table = samsung_i2s_driver_ids, + .driver = { + .name = "samsung-i2s", + .of_match_table = of_match_ptr(exynos_i2s_match), + .pm = &samsung_i2s_pm, + }, +}; + +module_platform_driver(samsung_i2s_driver); + +/* Module information */ +MODULE_AUTHOR("Jaswinder Singh, <jassisinghbrar@gmail.com>"); +MODULE_DESCRIPTION("Samsung I2S Interface"); +MODULE_ALIAS("platform:samsung-i2s"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/samsung/i2s.h b/sound/soc/samsung/i2s.h new file mode 100644 index 0000000000..78b475ef98 --- /dev/null +++ b/sound/soc/samsung/i2s.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * ALSA SoC Audio Layer - Samsung I2S Controller driver + * + * Copyright (c) 2010 Samsung Electronics Co. Ltd. + * Jaswinder Singh <jassisinghbrar@gmail.com> + */ + +#ifndef __SND_SOC_SAMSUNG_I2S_H +#define __SND_SOC_SAMSUNG_I2S_H + +#define SAMSUNG_I2S_DAI "samsung-i2s" +#define SAMSUNG_I2S_DAI_SEC "samsung-i2s-sec" + +#define SAMSUNG_I2S_DIV_BCLK 1 + +#define SAMSUNG_I2S_RCLKSRC_0 0 +#define SAMSUNG_I2S_RCLKSRC_1 1 +#define SAMSUNG_I2S_CDCLK 2 +/* Operation clock for IIS logic */ +#define SAMSUNG_I2S_OPCLK 3 +#define SAMSUNG_I2S_OPCLK_CDCLK_OUT 0 /* CODEC clock out */ +#define SAMSUNG_I2S_OPCLK_CDCLK_IN 1 /* CODEC clock in */ +#define SAMSUNG_I2S_OPCLK_BCLK_OUT 2 /* Bit clock out */ +#define SAMSUNG_I2S_OPCLK_PCLK 3 /* Audio bus clock */ + +#endif /* __SND_SOC_SAMSUNG_I2S_H */ diff --git a/sound/soc/samsung/idma.c b/sound/soc/samsung/idma.c new file mode 100644 index 0000000000..402ccadad4 --- /dev/null +++ b/sound/soc/samsung/idma.c @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// idma.c - I2S0 internal DMA driver +// +// Copyright (c) 2011 Samsung Electronics Co., Ltd. +// http://www.samsung.com + +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/dma-mapping.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> + +#include "i2s.h" +#include "idma.h" +#include "i2s-regs.h" + +#define ST_RUNNING (1<<0) +#define ST_OPENED (1<<1) + +static const struct snd_pcm_hardware idma_hardware = { + .info = SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_PAUSE | + SNDRV_PCM_INFO_RESUME, + .buffer_bytes_max = MAX_IDMA_BUFFER, + .period_bytes_min = 128, + .period_bytes_max = MAX_IDMA_PERIOD, + .periods_min = 1, + .periods_max = 2, +}; + +struct idma_ctrl { + spinlock_t lock; + int state; + dma_addr_t start; + dma_addr_t pos; + dma_addr_t end; + dma_addr_t period; + dma_addr_t periodsz; + void *token; + void (*cb)(void *dt, int bytes_xfer); +}; + +static struct idma_info { + spinlock_t lock; + void __iomem *regs; + dma_addr_t lp_tx_addr; +} idma; + +static int idma_irq; + +static void idma_getpos(dma_addr_t *src) +{ + *src = idma.lp_tx_addr + + (readl(idma.regs + I2STRNCNT) & 0xffffff) * 4; +} + +static int idma_enqueue(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct idma_ctrl *prtd = substream->runtime->private_data; + u32 val; + + spin_lock(&prtd->lock); + prtd->token = (void *) substream; + spin_unlock(&prtd->lock); + + /* Internal DMA Level0 Interrupt Address */ + val = idma.lp_tx_addr + prtd->periodsz; + writel(val, idma.regs + I2SLVL0ADDR); + + /* Start address0 of I2S internal DMA operation. */ + val = idma.lp_tx_addr; + writel(val, idma.regs + I2SSTR0); + + /* + * Transfer block size for I2S internal DMA. + * Should decide transfer size before start dma operation + */ + val = readl(idma.regs + I2SSIZE); + val &= ~(I2SSIZE_TRNMSK << I2SSIZE_SHIFT); + val |= (((runtime->dma_bytes >> 2) & + I2SSIZE_TRNMSK) << I2SSIZE_SHIFT); + writel(val, idma.regs + I2SSIZE); + + val = readl(idma.regs + I2SAHB); + val |= AHB_INTENLVL0; + writel(val, idma.regs + I2SAHB); + + return 0; +} + +static void idma_setcallbk(struct snd_pcm_substream *substream, + void (*cb)(void *, int)) +{ + struct idma_ctrl *prtd = substream->runtime->private_data; + + spin_lock(&prtd->lock); + prtd->cb = cb; + spin_unlock(&prtd->lock); +} + +static void idma_control(int op) +{ + u32 val = readl(idma.regs + I2SAHB); + + spin_lock(&idma.lock); + + switch (op) { + case LPAM_DMA_START: + val |= (AHB_INTENLVL0 | AHB_DMAEN); + break; + case LPAM_DMA_STOP: + val &= ~(AHB_INTENLVL0 | AHB_DMAEN); + break; + default: + spin_unlock(&idma.lock); + return; + } + + writel(val, idma.regs + I2SAHB); + spin_unlock(&idma.lock); +} + +static void idma_done(void *id, int bytes_xfer) +{ + struct snd_pcm_substream *substream = id; + struct idma_ctrl *prtd = substream->runtime->private_data; + + if (prtd && (prtd->state & ST_RUNNING)) + snd_pcm_period_elapsed(substream); +} + +static int idma_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct idma_ctrl *prtd = substream->runtime->private_data; + u32 mod = readl(idma.regs + I2SMOD); + u32 ahb = readl(idma.regs + I2SAHB); + + ahb |= (AHB_DMARLD | AHB_INTMASK); + mod |= MOD_TXS_IDMA; + writel(ahb, idma.regs + I2SAHB); + writel(mod, idma.regs + I2SMOD); + + snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); + runtime->dma_bytes = params_buffer_bytes(params); + + prtd->start = prtd->pos = runtime->dma_addr; + prtd->period = params_periods(params); + prtd->periodsz = params_period_bytes(params); + prtd->end = runtime->dma_addr + runtime->dma_bytes; + + idma_setcallbk(substream, idma_done); + + return 0; +} + +static int idma_hw_free(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + snd_pcm_set_runtime_buffer(substream, NULL); + + return 0; +} + +static int idma_prepare(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct idma_ctrl *prtd = substream->runtime->private_data; + + prtd->pos = prtd->start; + + /* flush the DMA channel */ + idma_control(LPAM_DMA_STOP); + idma_enqueue(substream); + + return 0; +} + +static int idma_trigger(struct snd_soc_component *component, + struct snd_pcm_substream *substream, int cmd) +{ + struct idma_ctrl *prtd = substream->runtime->private_data; + int ret = 0; + + spin_lock(&prtd->lock); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + prtd->state |= ST_RUNNING; + idma_control(LPAM_DMA_START); + break; + + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + prtd->state &= ~ST_RUNNING; + idma_control(LPAM_DMA_STOP); + break; + + default: + ret = -EINVAL; + break; + } + + spin_unlock(&prtd->lock); + + return ret; +} + +static snd_pcm_uframes_t +idma_pointer(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct idma_ctrl *prtd = runtime->private_data; + dma_addr_t src; + unsigned long res; + + spin_lock(&prtd->lock); + + idma_getpos(&src); + res = src - prtd->start; + + spin_unlock(&prtd->lock); + + return bytes_to_frames(substream->runtime, res); +} + +static int idma_mmap(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct vm_area_struct *vma) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + unsigned long size, offset; + + /* From snd_pcm_lib_mmap_iomem */ + vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); + size = vma->vm_end - vma->vm_start; + offset = vma->vm_pgoff << PAGE_SHIFT; + return io_remap_pfn_range(vma, vma->vm_start, + (runtime->dma_addr + offset) >> PAGE_SHIFT, + size, vma->vm_page_prot); +} + +static irqreturn_t iis_irq(int irqno, void *dev_id) +{ + struct idma_ctrl *prtd = (struct idma_ctrl *)dev_id; + u32 iisahb, val, addr; + + iisahb = readl(idma.regs + I2SAHB); + + val = (iisahb & AHB_LVL0INT) ? AHB_CLRLVL0INT : 0; + + if (val) { + iisahb |= val; + writel(iisahb, idma.regs + I2SAHB); + + addr = readl(idma.regs + I2SLVL0ADDR) - idma.lp_tx_addr; + addr += prtd->periodsz; + addr %= (u32)(prtd->end - prtd->start); + addr += idma.lp_tx_addr; + + writel(addr, idma.regs + I2SLVL0ADDR); + + if (prtd->cb) + prtd->cb(prtd->token, prtd->period); + } + + return IRQ_HANDLED; +} + +static int idma_open(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct idma_ctrl *prtd; + int ret; + + snd_soc_set_runtime_hwparams(substream, &idma_hardware); + + prtd = kzalloc(sizeof(struct idma_ctrl), GFP_KERNEL); + if (prtd == NULL) + return -ENOMEM; + + ret = request_irq(idma_irq, iis_irq, 0, "i2s", prtd); + if (ret < 0) { + pr_err("fail to claim i2s irq , ret = %d\n", ret); + kfree(prtd); + return ret; + } + + spin_lock_init(&prtd->lock); + + runtime->private_data = prtd; + + return 0; +} + +static int idma_close(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct idma_ctrl *prtd = runtime->private_data; + + free_irq(idma_irq, prtd); + + if (!prtd) + pr_err("idma_close called with prtd == NULL\n"); + + kfree(prtd); + + return 0; +} + +static void idma_free(struct snd_soc_component *component, + struct snd_pcm *pcm) +{ + struct snd_pcm_substream *substream; + struct snd_dma_buffer *buf; + + substream = pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream; + if (!substream) + return; + + buf = &substream->dma_buffer; + if (!buf->area) + return; + + iounmap((void __iomem *)buf->area); + + buf->area = NULL; + buf->addr = 0; +} + +static int preallocate_idma_buffer(struct snd_pcm *pcm, int stream) +{ + struct snd_pcm_substream *substream = pcm->streams[stream].substream; + struct snd_dma_buffer *buf = &substream->dma_buffer; + + buf->dev.dev = pcm->card->dev; + buf->private_data = NULL; + + /* Assign PCM buffer pointers */ + buf->dev.type = SNDRV_DMA_TYPE_CONTINUOUS; + buf->addr = idma.lp_tx_addr; + buf->bytes = idma_hardware.buffer_bytes_max; + buf->area = (unsigned char * __force)ioremap(buf->addr, buf->bytes); + if (!buf->area) + return -ENOMEM; + + return 0; +} + +static int idma_new(struct snd_soc_component *component, + struct snd_soc_pcm_runtime *rtd) +{ + struct snd_card *card = rtd->card->snd_card; + struct snd_pcm *pcm = rtd->pcm; + int ret; + + ret = dma_coerce_mask_and_coherent(card->dev, DMA_BIT_MASK(32)); + if (ret) + return ret; + + if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) { + ret = preallocate_idma_buffer(pcm, + SNDRV_PCM_STREAM_PLAYBACK); + } + + return ret; +} + +void idma_reg_addr_init(void __iomem *regs, dma_addr_t addr) +{ + spin_lock_init(&idma.lock); + idma.regs = regs; + idma.lp_tx_addr = addr; +} +EXPORT_SYMBOL_GPL(idma_reg_addr_init); + +static const struct snd_soc_component_driver asoc_idma_platform = { + .open = idma_open, + .close = idma_close, + .trigger = idma_trigger, + .pointer = idma_pointer, + .mmap = idma_mmap, + .hw_params = idma_hw_params, + .hw_free = idma_hw_free, + .prepare = idma_prepare, + .pcm_construct = idma_new, + .pcm_destruct = idma_free, +}; + +static int asoc_idma_platform_probe(struct platform_device *pdev) +{ + idma_irq = platform_get_irq(pdev, 0); + if (idma_irq < 0) + return idma_irq; + + return devm_snd_soc_register_component(&pdev->dev, &asoc_idma_platform, + NULL, 0); +} + +static struct platform_driver asoc_idma_driver = { + .driver = { + .name = "samsung-idma", + }, + + .probe = asoc_idma_platform_probe, +}; + +module_platform_driver(asoc_idma_driver); + +MODULE_AUTHOR("Jaswinder Singh, <jassisinghbrar@gmail.com>"); +MODULE_DESCRIPTION("Samsung ASoC IDMA Driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/samsung/idma.h b/sound/soc/samsung/idma.h new file mode 100644 index 0000000000..8a46a918ed --- /dev/null +++ b/sound/soc/samsung/idma.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2011 Samsung Electronics Co., Ltd + * http://www.samsung.com + */ + +#ifndef __SND_SOC_SAMSUNG_IDMA_H_ +#define __SND_SOC_SAMSUNG_IDMA_H_ + +extern void idma_reg_addr_init(void __iomem *regs, dma_addr_t addr); + +/* dma_state */ +#define LPAM_DMA_STOP 0 +#define LPAM_DMA_START 1 + +#define MAX_IDMA_PERIOD (128 * 1024) +#define MAX_IDMA_BUFFER (160 * 1024) + +#endif /* __SND_SOC_SAMSUNG_IDMA_H_ */ diff --git a/sound/soc/samsung/littlemill.c b/sound/soc/samsung/littlemill.c new file mode 100644 index 0000000000..fafadcef23 --- /dev/null +++ b/sound/soc/samsung/littlemill.c @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Littlemill audio support +// +// Copyright 2011 Wolfson Microelectronics + +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/jack.h> +#include <linux/gpio.h> +#include <linux/module.h> + +#include "../codecs/wm8994.h" + +static int sample_rate = 44100; + +static int littlemill_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; + struct snd_soc_dai *aif1_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[0]); + aif1_dai = asoc_rtd_to_codec(rtd, 0); + + if (dapm->dev != aif1_dai->dev) + return 0; + + switch (level) { + case SND_SOC_BIAS_PREPARE: + /* + * If we've not already clocked things via hw_params() + * then do so now, otherwise these are noops. + */ + if (dapm->bias_level == SND_SOC_BIAS_STANDBY) { + ret = snd_soc_dai_set_pll(aif1_dai, WM8994_FLL1, + WM8994_FLL_SRC_MCLK2, 32768, + sample_rate * 512); + if (ret < 0) { + pr_err("Failed to start FLL: %d\n", ret); + return ret; + } + + ret = snd_soc_dai_set_sysclk(aif1_dai, + WM8994_SYSCLK_FLL1, + sample_rate * 512, + SND_SOC_CLOCK_IN); + if (ret < 0) { + pr_err("Failed to set SYSCLK: %d\n", ret); + return ret; + } + } + break; + + default: + break; + } + + return 0; +} + +static int littlemill_set_bias_level_post(struct snd_soc_card *card, + struct snd_soc_dapm_context *dapm, + enum snd_soc_bias_level level) +{ + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_dai *aif1_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[0]); + aif1_dai = asoc_rtd_to_codec(rtd, 0); + + if (dapm->dev != aif1_dai->dev) + return 0; + + switch (level) { + case SND_SOC_BIAS_STANDBY: + ret = snd_soc_dai_set_sysclk(aif1_dai, WM8994_SYSCLK_MCLK2, + 32768, SND_SOC_CLOCK_IN); + if (ret < 0) { + pr_err("Failed to switch away from FLL1: %d\n", ret); + return ret; + } + + ret = snd_soc_dai_set_pll(aif1_dai, WM8994_FLL1, + 0, 0, 0); + if (ret < 0) { + pr_err("Failed to stop FLL1: %d\n", ret); + return ret; + } + break; + + default: + break; + } + + dapm->bias_level = level; + + return 0; +} + +static int littlemill_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); + int ret; + + sample_rate = params_rate(params); + + ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, + WM8994_FLL_SRC_MCLK2, 32768, + sample_rate * 512); + if (ret < 0) { + pr_err("Failed to start FLL: %d\n", ret); + return ret; + } + + ret = snd_soc_dai_set_sysclk(codec_dai, + WM8994_SYSCLK_FLL1, + sample_rate * 512, + SND_SOC_CLOCK_IN); + if (ret < 0) { + pr_err("Failed to set SYSCLK: %d\n", ret); + return ret; + } + + return 0; +} + +static const struct snd_soc_ops littlemill_ops = { + .hw_params = littlemill_hw_params, +}; + +static const struct snd_soc_pcm_stream baseband_params = { + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .rate_min = 8000, + .rate_max = 8000, + .channels_min = 2, + .channels_max = 2, +}; + +SND_SOC_DAILINK_DEFS(cpu, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s.0")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm8994-codec", "wm8994-aif1")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s.0"))); + +SND_SOC_DAILINK_DEFS(baseband, + DAILINK_COMP_ARRAY(COMP_CPU("wm8994-aif2")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm1250-ev1.1-0027", + "wm1250-ev1"))); + +static struct snd_soc_dai_link littlemill_dai[] = { + { + .name = "CPU", + .stream_name = "CPU", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .ops = &littlemill_ops, + SND_SOC_DAILINK_REG(cpu), + }, + { + .name = "Baseband", + .stream_name = "Baseband", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .ignore_suspend = 1, + .c2c_params = &baseband_params, + .num_c2c_params = 1, + SND_SOC_DAILINK_REG(baseband), + }, +}; + +static int bbclk_ev(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_card *card = w->dapm->card; + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_dai *aif2_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[1]); + aif2_dai = asoc_rtd_to_cpu(rtd, 0); + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + ret = snd_soc_dai_set_pll(aif2_dai, WM8994_FLL2, + WM8994_FLL_SRC_BCLK, 64 * 8000, + 8000 * 256); + if (ret < 0) { + pr_err("Failed to start FLL: %d\n", ret); + return ret; + } + + ret = snd_soc_dai_set_sysclk(aif2_dai, WM8994_SYSCLK_FLL2, + 8000 * 256, + SND_SOC_CLOCK_IN); + if (ret < 0) { + pr_err("Failed to set SYSCLK: %d\n", ret); + return ret; + } + break; + case SND_SOC_DAPM_POST_PMD: + ret = snd_soc_dai_set_sysclk(aif2_dai, WM8994_SYSCLK_MCLK2, + 32768, SND_SOC_CLOCK_IN); + if (ret < 0) { + pr_err("Failed to switch away from FLL2: %d\n", ret); + return ret; + } + + ret = snd_soc_dai_set_pll(aif2_dai, WM8994_FLL2, + 0, 0, 0); + if (ret < 0) { + pr_err("Failed to stop FLL2: %d\n", ret); + return ret; + } + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct snd_kcontrol_new controls[] = { + SOC_DAPM_PIN_SWITCH("Headphone"), + SOC_DAPM_PIN_SWITCH("Headset Mic"), + SOC_DAPM_PIN_SWITCH("WM1250 Input"), + SOC_DAPM_PIN_SWITCH("WM1250 Output"), +}; + +static const struct snd_soc_dapm_widget widgets[] = { + SND_SOC_DAPM_HP("Headphone", NULL), + SND_SOC_DAPM_HP("Headset Mic", NULL), + + SND_SOC_DAPM_MIC("AMIC", NULL), + SND_SOC_DAPM_MIC("DMIC", NULL), + + SND_SOC_DAPM_SUPPLY_S("Baseband Clock", -1, SND_SOC_NOPM, 0, 0, + bbclk_ev, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD), +}; + +static const struct snd_soc_dapm_route audio_paths[] = { + { "Headphone", NULL, "HPOUT1L" }, + { "Headphone", NULL, "HPOUT1R" }, + + { "AMIC", NULL, "MICBIAS1" }, /* Default for AMICBIAS jumper */ + { "IN1LN", NULL, "AMIC" }, + + { "DMIC", NULL, "MICBIAS2" }, /* Default for DMICBIAS jumper */ + { "DMIC1DAT", NULL, "DMIC" }, + { "DMIC2DAT", NULL, "DMIC" }, + + { "AIF2CLK", NULL, "Baseband Clock" }, +}; + +static struct snd_soc_jack littlemill_headset; +static struct snd_soc_jack_pin littlemill_headset_pins[] = { + { + .pin = "Headphone", + .mask = SND_JACK_HEADPHONE, + }, + { + .pin = "Headset Mic", + .mask = SND_JACK_MICROPHONE, + }, +}; + +static int littlemill_late_probe(struct snd_soc_card *card) +{ + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_component *component; + struct snd_soc_dai *aif1_dai; + struct snd_soc_dai *aif2_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[0]); + component = asoc_rtd_to_codec(rtd, 0)->component; + aif1_dai = asoc_rtd_to_codec(rtd, 0); + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[1]); + aif2_dai = asoc_rtd_to_cpu(rtd, 0); + + ret = snd_soc_dai_set_sysclk(aif1_dai, WM8994_SYSCLK_MCLK2, + 32768, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + ret = snd_soc_dai_set_sysclk(aif2_dai, WM8994_SYSCLK_MCLK2, + 32768, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + ret = snd_soc_card_jack_new_pins(card, "Headset", + SND_JACK_HEADSET | SND_JACK_MECHANICAL | + SND_JACK_BTN_0 | SND_JACK_BTN_1 | + SND_JACK_BTN_2 | SND_JACK_BTN_3 | + SND_JACK_BTN_4 | SND_JACK_BTN_5, + &littlemill_headset, + littlemill_headset_pins, + ARRAY_SIZE(littlemill_headset_pins)); + if (ret) + return ret; + + /* This will check device compatibility itself */ + wm8958_mic_detect(component, &littlemill_headset, NULL, NULL, NULL, NULL); + + /* As will this */ + wm8994_mic_detect(component, &littlemill_headset, 1); + + return 0; +} + +static struct snd_soc_card littlemill = { + .name = "Littlemill", + .owner = THIS_MODULE, + .dai_link = littlemill_dai, + .num_links = ARRAY_SIZE(littlemill_dai), + + .set_bias_level = littlemill_set_bias_level, + .set_bias_level_post = littlemill_set_bias_level_post, + + .controls = controls, + .num_controls = ARRAY_SIZE(controls), + .dapm_widgets = widgets, + .num_dapm_widgets = ARRAY_SIZE(widgets), + .dapm_routes = audio_paths, + .num_dapm_routes = ARRAY_SIZE(audio_paths), + + .late_probe = littlemill_late_probe, +}; + +static int littlemill_probe(struct platform_device *pdev) +{ + struct snd_soc_card *card = &littlemill; + int ret; + + card->dev = &pdev->dev; + + ret = devm_snd_soc_register_card(&pdev->dev, card); + if (ret) + dev_err_probe(&pdev->dev, ret, "snd_soc_register_card() failed\n"); + + return ret; +} + +static struct platform_driver littlemill_driver = { + .driver = { + .name = "littlemill", + .pm = &snd_soc_pm_ops, + }, + .probe = littlemill_probe, +}; + +module_platform_driver(littlemill_driver); + +MODULE_DESCRIPTION("Littlemill audio support"); +MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:littlemill"); diff --git a/sound/soc/samsung/lowland.c b/sound/soc/samsung/lowland.c new file mode 100644 index 0000000000..a79df871ea --- /dev/null +++ b/sound/soc/samsung/lowland.c @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Lowland audio support +// +// Copyright 2011 Wolfson Microelectronics + +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/jack.h> +#include <linux/gpio.h> +#include <linux/module.h> + +#include "../codecs/wm5100.h" +#include "../codecs/wm9081.h" + +#define MCLK1_RATE (44100 * 512) +#define CLKOUT_RATE (44100 * 256) + +static struct snd_soc_jack lowland_headset; + +/* Headset jack detection DAPM pins */ +static struct snd_soc_jack_pin lowland_headset_pins[] = { + { + .pin = "Headphone", + .mask = SND_JACK_HEADPHONE, + }, + { + .pin = "Headset Mic", + .mask = SND_JACK_MICROPHONE, + }, + { + .pin = "Line Out", + .mask = SND_JACK_LINEOUT, + }, +}; + +static int lowland_wm5100_init(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_component *component = asoc_rtd_to_codec(rtd, 0)->component; + int ret; + + ret = snd_soc_component_set_sysclk(component, WM5100_CLK_SYSCLK, + WM5100_CLKSRC_MCLK1, MCLK1_RATE, + SND_SOC_CLOCK_IN); + if (ret < 0) { + pr_err("Failed to set SYSCLK clock source: %d\n", ret); + return ret; + } + + /* Clock OPCLK, used by the other audio components. */ + ret = snd_soc_component_set_sysclk(component, WM5100_CLK_OPCLK, 0, + CLKOUT_RATE, 0); + if (ret < 0) { + pr_err("Failed to set OPCLK rate: %d\n", ret); + return ret; + } + + ret = snd_soc_card_jack_new_pins(rtd->card, "Headset", + SND_JACK_LINEOUT | SND_JACK_HEADSET | + SND_JACK_BTN_0, + &lowland_headset, lowland_headset_pins, + ARRAY_SIZE(lowland_headset_pins)); + if (ret) + return ret; + + wm5100_detect(component, &lowland_headset); + + return 0; +} + +static int lowland_wm9081_init(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_component *component = asoc_rtd_to_codec(rtd, 0)->component; + + snd_soc_dapm_nc_pin(&rtd->card->dapm, "LINEOUT"); + + /* At any time the WM9081 is active it will have this clock */ + return snd_soc_component_set_sysclk(component, WM9081_SYSCLK_MCLK, 0, + CLKOUT_RATE, 0); +} + +static const struct snd_soc_pcm_stream sub_params = { + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .rate_min = 44100, + .rate_max = 44100, + .channels_min = 2, + .channels_max = 2, +}; + +SND_SOC_DAILINK_DEFS(cpu, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s.0")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm5100.1-001a", "wm5100-aif1")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s.0"))); + +SND_SOC_DAILINK_DEFS(baseband, + DAILINK_COMP_ARRAY(COMP_CPU("wm5100-aif2")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm1250-ev1.1-0027", "wm1250-ev1"))); + +SND_SOC_DAILINK_DEFS(speaker, + DAILINK_COMP_ARRAY(COMP_CPU("wm5100-aif3")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm9081.1-006c", "wm9081-hifi"))); + +static struct snd_soc_dai_link lowland_dai[] = { + { + .name = "CPU", + .stream_name = "CPU", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + .init = lowland_wm5100_init, + SND_SOC_DAILINK_REG(cpu), + }, + { + .name = "Baseband", + .stream_name = "Baseband", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(baseband), + }, + { + .name = "Sub Speaker", + .stream_name = "Sub Speaker", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + .ignore_suspend = 1, + .c2c_params = &sub_params, + .num_c2c_params = 1, + .init = lowland_wm9081_init, + SND_SOC_DAILINK_REG(speaker), + }, +}; + +static struct snd_soc_codec_conf lowland_codec_conf[] = { + { + .dlc = COMP_CODEC_CONF("wm9081.1-006c"), + .name_prefix = "Sub", + }, +}; + +static const struct snd_kcontrol_new controls[] = { + SOC_DAPM_PIN_SWITCH("Main Speaker"), + SOC_DAPM_PIN_SWITCH("Main DMIC"), + SOC_DAPM_PIN_SWITCH("Main AMIC"), + SOC_DAPM_PIN_SWITCH("WM1250 Input"), + SOC_DAPM_PIN_SWITCH("WM1250 Output"), + SOC_DAPM_PIN_SWITCH("Headphone"), + SOC_DAPM_PIN_SWITCH("Line Out"), +}; + +static const struct snd_soc_dapm_widget widgets[] = { + SND_SOC_DAPM_HP("Headphone", NULL), + SND_SOC_DAPM_MIC("Headset Mic", NULL), + SND_SOC_DAPM_LINE("Line Out", NULL), + + SND_SOC_DAPM_SPK("Main Speaker", NULL), + + SND_SOC_DAPM_MIC("Main AMIC", NULL), + SND_SOC_DAPM_MIC("Main DMIC", NULL), +}; + +static const struct snd_soc_dapm_route audio_paths[] = { + { "Sub IN1", NULL, "HPOUT2L" }, + { "Sub IN2", NULL, "HPOUT2R" }, + + { "Main Speaker", NULL, "Sub SPKN" }, + { "Main Speaker", NULL, "Sub SPKP" }, + { "Main Speaker", NULL, "SPKDAT1" }, +}; + +static struct snd_soc_card lowland = { + .name = "Lowland", + .owner = THIS_MODULE, + .dai_link = lowland_dai, + .num_links = ARRAY_SIZE(lowland_dai), + .codec_conf = lowland_codec_conf, + .num_configs = ARRAY_SIZE(lowland_codec_conf), + + .controls = controls, + .num_controls = ARRAY_SIZE(controls), + .dapm_widgets = widgets, + .num_dapm_widgets = ARRAY_SIZE(widgets), + .dapm_routes = audio_paths, + .num_dapm_routes = ARRAY_SIZE(audio_paths), +}; + +static int lowland_probe(struct platform_device *pdev) +{ + struct snd_soc_card *card = &lowland; + int ret; + + card->dev = &pdev->dev; + + ret = devm_snd_soc_register_card(&pdev->dev, card); + if (ret) + dev_err_probe(&pdev->dev, ret, "snd_soc_register_card() failed\n"); + + return ret; +} + +static struct platform_driver lowland_driver = { + .driver = { + .name = "lowland", + .pm = &snd_soc_pm_ops, + }, + .probe = lowland_probe, +}; + +module_platform_driver(lowland_driver); + +MODULE_DESCRIPTION("Lowland audio support"); +MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:lowland"); diff --git a/sound/soc/samsung/midas_wm1811.c b/sound/soc/samsung/midas_wm1811.c new file mode 100644 index 0000000000..2ec7e16ddf --- /dev/null +++ b/sound/soc/samsung/midas_wm1811.c @@ -0,0 +1,561 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Midas audio support +// +// Copyright (C) 2018 Simon Shields <simon@lineageos.org> +// Copyright (C) 2020 Samsung Electronics Co., Ltd. + +#include <linux/clk.h> +#include <linux/gpio/consumer.h> +#include <linux/mfd/wm8994/registers.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/of_gpio.h> +#include <linux/regulator/consumer.h> +#include <sound/jack.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> + +#include "i2s.h" +#include "../codecs/wm8994.h" + +/* + * The MCLK1 clock source is XCLKOUT with its mux set to the external fixed rate + * oscillator (XXTI). + */ +#define MCLK1_RATE 24000000U +#define MCLK2_RATE 32768U +#define DEFAULT_FLL1_RATE 11289600U + +struct midas_priv { + struct regulator *reg_mic_bias; + struct regulator *reg_submic_bias; + struct gpio_desc *gpio_fm_sel; + struct gpio_desc *gpio_lineout_sel; + unsigned int fll1_rate; + + struct snd_soc_jack headset_jack; +}; + +static struct snd_soc_jack_pin headset_jack_pins[] = { + { + .pin = "Headphone", + .mask = SND_JACK_HEADPHONE, + }, + { + .pin = "Headset Mic", + .mask = SND_JACK_MICROPHONE, + }, +}; + +static int midas_start_fll1(struct snd_soc_pcm_runtime *rtd, unsigned int rate) +{ + struct snd_soc_card *card = rtd->card; + struct midas_priv *priv = snd_soc_card_get_drvdata(card); + struct snd_soc_dai *aif1_dai = asoc_rtd_to_codec(rtd, 0); + struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0); + int ret; + + if (!rate) + rate = priv->fll1_rate; + /* + * If no new rate is requested, set FLL1 to a sane default for jack + * detection. + */ + if (!rate) + rate = DEFAULT_FLL1_RATE; + + if (rate != priv->fll1_rate && priv->fll1_rate) { + /* while reconfiguring, switch to MCLK2 for SYSCLK */ + ret = snd_soc_dai_set_sysclk(aif1_dai, WM8994_SYSCLK_MCLK2, + MCLK2_RATE, SND_SOC_CLOCK_IN); + if (ret < 0) { + dev_err(card->dev, "Unable to switch to MCLK2: %d\n", ret); + return ret; + } + } + + ret = snd_soc_dai_set_pll(aif1_dai, WM8994_FLL1, WM8994_FLL_SRC_MCLK1, + MCLK1_RATE, rate); + if (ret < 0) { + dev_err(card->dev, "Failed to set FLL1 rate: %d\n", ret); + return ret; + } + priv->fll1_rate = rate; + + ret = snd_soc_dai_set_sysclk(aif1_dai, WM8994_SYSCLK_FLL1, + priv->fll1_rate, SND_SOC_CLOCK_IN); + if (ret < 0) { + dev_err(card->dev, "Failed to set SYSCLK source: %d\n", ret); + return ret; + } + + ret = snd_soc_dai_set_sysclk(cpu_dai, SAMSUNG_I2S_OPCLK, 0, + SAMSUNG_I2S_OPCLK_PCLK); + if (ret < 0) { + dev_err(card->dev, "Failed to set OPCLK source: %d\n", ret); + return ret; + } + + return 0; +} + +static int midas_stop_fll1(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_card *card = rtd->card; + struct midas_priv *priv = snd_soc_card_get_drvdata(card); + struct snd_soc_dai *aif1_dai = asoc_rtd_to_codec(rtd, 0); + int ret; + + ret = snd_soc_dai_set_sysclk(aif1_dai, WM8994_SYSCLK_MCLK2, + MCLK2_RATE, SND_SOC_CLOCK_IN); + if (ret < 0) { + dev_err(card->dev, "Unable to switch to MCLK2: %d\n", ret); + return ret; + } + + ret = snd_soc_dai_set_pll(aif1_dai, WM8994_FLL1, 0, 0, 0); + if (ret < 0) { + dev_err(card->dev, "Unable to stop FLL1: %d\n", ret); + return ret; + } + + priv->fll1_rate = 0; + + return 0; +} + +static int midas_aif1_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + unsigned int pll_out; + + /* AIF1CLK should be at least 3MHz for "optimal performance" */ + if (params_rate(params) == 8000 || params_rate(params) == 11025) + pll_out = params_rate(params) * 512; + else + pll_out = params_rate(params) * 256; + + return midas_start_fll1(rtd, pll_out); +} + +static const struct snd_soc_ops midas_aif1_ops = { + .hw_params = midas_aif1_hw_params, +}; + +/* + * We only have a single external speaker, so mix stereo data + * to a single mono stream. + */ +static int midas_ext_spkmode(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_component *codec = snd_soc_dapm_to_component(w->dapm); + int ret = 0; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + ret = snd_soc_component_update_bits(codec, WM8994_SPKOUT_MIXERS, + WM8994_SPKMIXR_TO_SPKOUTL_MASK, + WM8994_SPKMIXR_TO_SPKOUTL); + break; + case SND_SOC_DAPM_POST_PMD: + ret = snd_soc_component_update_bits(codec, WM8994_SPKOUT_MIXERS, + WM8994_SPKMIXR_TO_SPKOUTL_MASK, + 0); + break; + } + + return ret; +} + +static int midas_mic_bias(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_card *card = w->dapm->card; + struct midas_priv *priv = snd_soc_card_get_drvdata(card); + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + return regulator_enable(priv->reg_mic_bias); + case SND_SOC_DAPM_POST_PMD: + return regulator_disable(priv->reg_mic_bias); + } + + return 0; +} + +static int midas_submic_bias(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_card *card = w->dapm->card; + struct midas_priv *priv = snd_soc_card_get_drvdata(card); + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + return regulator_enable(priv->reg_submic_bias); + case SND_SOC_DAPM_POST_PMD: + return regulator_disable(priv->reg_submic_bias); + } + + return 0; +} + +static int midas_fm_set(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_card *card = w->dapm->card; + struct midas_priv *priv = snd_soc_card_get_drvdata(card); + + if (!priv->gpio_fm_sel) + return 0; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + gpiod_set_value_cansleep(priv->gpio_fm_sel, 1); + break; + case SND_SOC_DAPM_POST_PMD: + gpiod_set_value_cansleep(priv->gpio_fm_sel, 0); + break; + } + + return 0; +} + +static int midas_line_set(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, int event) +{ + struct snd_soc_card *card = w->dapm->card; + struct midas_priv *priv = snd_soc_card_get_drvdata(card); + + if (!priv->gpio_lineout_sel) + return 0; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + gpiod_set_value_cansleep(priv->gpio_lineout_sel, 1); + break; + case SND_SOC_DAPM_POST_PMD: + gpiod_set_value_cansleep(priv->gpio_lineout_sel, 0); + break; + } + + return 0; +} + +static const struct snd_kcontrol_new midas_controls[] = { + SOC_DAPM_PIN_SWITCH("HP"), + + SOC_DAPM_PIN_SWITCH("SPK"), + SOC_DAPM_PIN_SWITCH("RCV"), + + SOC_DAPM_PIN_SWITCH("LINE"), + SOC_DAPM_PIN_SWITCH("HDMI"), + + SOC_DAPM_PIN_SWITCH("Main Mic"), + SOC_DAPM_PIN_SWITCH("Sub Mic"), + SOC_DAPM_PIN_SWITCH("Headset Mic"), + + SOC_DAPM_PIN_SWITCH("FM In"), +}; + +static const struct snd_soc_dapm_widget midas_dapm_widgets[] = { + SND_SOC_DAPM_HP("HP", NULL), + + SND_SOC_DAPM_SPK("SPK", midas_ext_spkmode), + SND_SOC_DAPM_SPK("RCV", NULL), + + /* FIXME: toggle MAX77693 on i9300/i9305 */ + SND_SOC_DAPM_LINE("LINE", midas_line_set), + SND_SOC_DAPM_LINE("HDMI", NULL), + SND_SOC_DAPM_LINE("FM In", midas_fm_set), + + SND_SOC_DAPM_HP("Headphone", NULL), + SND_SOC_DAPM_MIC("Headset Mic", NULL), + SND_SOC_DAPM_MIC("Main Mic", midas_mic_bias), + SND_SOC_DAPM_MIC("Sub Mic", midas_submic_bias), +}; + +static int midas_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 = snd_soc_get_pcm_runtime(card, + &card->dai_link[0]); + struct snd_soc_dai *aif1_dai = asoc_rtd_to_codec(rtd, 0); + + if (dapm->dev != aif1_dai->dev) + return 0; + + switch (level) { + case SND_SOC_BIAS_STANDBY: + return midas_stop_fll1(rtd); + case SND_SOC_BIAS_PREPARE: + return midas_start_fll1(rtd, 0); + default: + break; + } + + return 0; +} + +static int midas_late_probe(struct snd_soc_card *card) +{ + struct snd_soc_pcm_runtime *rtd = snd_soc_get_pcm_runtime(card, + &card->dai_link[0]); + struct snd_soc_dai *aif1_dai = asoc_rtd_to_codec(rtd, 0); + struct midas_priv *priv = snd_soc_card_get_drvdata(card); + int ret; + + /* Use MCLK2 as SYSCLK for boot */ + ret = snd_soc_dai_set_sysclk(aif1_dai, WM8994_SYSCLK_MCLK2, MCLK2_RATE, + SND_SOC_CLOCK_IN); + if (ret < 0) { + dev_err(aif1_dai->dev, "Failed to switch to MCLK2: %d\n", ret); + return ret; + } + + ret = snd_soc_card_jack_new_pins(card, "Headset", + SND_JACK_HEADSET | SND_JACK_MECHANICAL | + SND_JACK_BTN_0 | SND_JACK_BTN_1 | SND_JACK_BTN_2 | + SND_JACK_BTN_3 | SND_JACK_BTN_4 | SND_JACK_BTN_5, + &priv->headset_jack, + headset_jack_pins, + ARRAY_SIZE(headset_jack_pins)); + if (ret) + return ret; + + wm8958_mic_detect(aif1_dai->component, &priv->headset_jack, + NULL, NULL, NULL, NULL); + return 0; +} + +static struct snd_soc_dai_driver midas_ext_dai[] = { + { + .name = "Voice call", + .playback = { + .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, + }, + .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, + }, + }, + { + .name = "Bluetooth", + .playback = { + .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, + }, + .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 const struct snd_soc_component_driver midas_component = { + .name = "midas-audio", +}; + +SND_SOC_DAILINK_DEFS(wm1811_hifi, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif1")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +SND_SOC_DAILINK_DEFS(wm1811_voice, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif2")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +SND_SOC_DAILINK_DEFS(wm1811_bt, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm8994-aif3")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +static struct snd_soc_dai_link midas_dai[] = { + { + .name = "WM8994 AIF1", + .stream_name = "HiFi Primary", + .ops = &midas_aif1_ops, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + SND_SOC_DAILINK_REG(wm1811_hifi), + }, { + .name = "WM1811 Voice", + .stream_name = "Voice call", + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(wm1811_voice), + }, { + .name = "WM1811 BT", + .stream_name = "Bluetooth", + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(wm1811_bt), + }, +}; + +static struct snd_soc_card midas_card = { + .name = "Midas WM1811", + .owner = THIS_MODULE, + + .dai_link = midas_dai, + .num_links = ARRAY_SIZE(midas_dai), + .controls = midas_controls, + .num_controls = ARRAY_SIZE(midas_controls), + .dapm_widgets = midas_dapm_widgets, + .num_dapm_widgets = ARRAY_SIZE(midas_dapm_widgets), + + .set_bias_level = midas_set_bias_level, + .late_probe = midas_late_probe, +}; + +static int midas_probe(struct platform_device *pdev) +{ + struct device_node *cpu_dai_node = NULL, *codec_dai_node = NULL; + struct device_node *cpu = NULL, *codec = NULL; + struct snd_soc_card *card = &midas_card; + struct device *dev = &pdev->dev; + static struct snd_soc_dai_link *dai_link; + struct midas_priv *priv; + int ret, i; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + snd_soc_card_set_drvdata(card, priv); + card->dev = dev; + + priv->reg_mic_bias = devm_regulator_get(dev, "mic-bias"); + if (IS_ERR(priv->reg_mic_bias)) { + dev_err(dev, "Failed to get mic bias regulator\n"); + return PTR_ERR(priv->reg_mic_bias); + } + + priv->reg_submic_bias = devm_regulator_get(dev, "submic-bias"); + if (IS_ERR(priv->reg_submic_bias)) { + dev_err(dev, "Failed to get submic bias regulator\n"); + return PTR_ERR(priv->reg_submic_bias); + } + + priv->gpio_fm_sel = devm_gpiod_get_optional(dev, "fm-sel", GPIOD_OUT_HIGH); + if (IS_ERR(priv->gpio_fm_sel)) { + dev_err(dev, "Failed to get FM selection GPIO\n"); + return PTR_ERR(priv->gpio_fm_sel); + } + + priv->gpio_lineout_sel = devm_gpiod_get_optional(dev, "lineout-sel", + GPIOD_OUT_HIGH); + if (IS_ERR(priv->gpio_lineout_sel)) { + dev_err(dev, "Failed to get line out selection GPIO\n"); + return PTR_ERR(priv->gpio_lineout_sel); + } + + 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, "audio-routing"); + if (ret < 0) { + /* Backwards compatible way */ + ret = snd_soc_of_parse_audio_routing(card, "samsung,audio-routing"); + if (ret < 0) { + dev_err(dev, "Audio routing invalid/unspecified\n"); + return ret; + } + } + + cpu = of_get_child_by_name(dev->of_node, "cpu"); + if (!cpu) + return -EINVAL; + + codec = of_get_child_by_name(dev->of_node, "codec"); + if (!codec) { + of_node_put(cpu); + return -EINVAL; + } + + cpu_dai_node = of_parse_phandle(cpu, "sound-dai", 0); + of_node_put(cpu); + if (!cpu_dai_node) { + dev_err(dev, "parsing cpu/sound-dai failed\n"); + of_node_put(codec); + return -EINVAL; + } + + codec_dai_node = of_parse_phandle(codec, "sound-dai", 0); + of_node_put(codec); + if (!codec_dai_node) { + dev_err(dev, "audio-codec property invalid/missing\n"); + ret = -EINVAL; + goto put_cpu_dai_node; + } + + for_each_card_prelinks(card, i, dai_link) { + dai_link->codecs->of_node = codec_dai_node; + dai_link->cpus->of_node = cpu_dai_node; + dai_link->platforms->of_node = cpu_dai_node; + } + + ret = devm_snd_soc_register_component(dev, &midas_component, + midas_ext_dai, ARRAY_SIZE(midas_ext_dai)); + if (ret < 0) { + dev_err(dev, "Failed to register component: %d\n", ret); + goto put_codec_dai_node; + } + + ret = devm_snd_soc_register_card(dev, card); + if (ret < 0) { + dev_err(dev, "Failed to register card: %d\n", ret); + goto put_codec_dai_node; + } + + return 0; + +put_codec_dai_node: + of_node_put(codec_dai_node); +put_cpu_dai_node: + of_node_put(cpu_dai_node); + return ret; +} + +static const struct of_device_id midas_of_match[] = { + { .compatible = "samsung,midas-audio" }, + { }, +}; +MODULE_DEVICE_TABLE(of, midas_of_match); + +static struct platform_driver midas_driver = { + .driver = { + .name = "midas-audio", + .of_match_table = midas_of_match, + .pm = &snd_soc_pm_ops, + }, + .probe = midas_probe, +}; +module_platform_driver(midas_driver); + +MODULE_AUTHOR("Simon Shields <simon@lineageos.org>"); +MODULE_DESCRIPTION("ASoC support for Midas"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/samsung/odroid.c b/sound/soc/samsung/odroid.c new file mode 100644 index 0000000000..c93cb5a864 --- /dev/null +++ b/sound/soc/samsung/odroid.c @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Copyright (C) 2017 Samsung Electronics Co., Ltd. + +#include <linux/clk.h> +#include <linux/clk-provider.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/module.h> +#include <sound/soc.h> +#include <sound/pcm_params.h> +#include "i2s.h" +#include "i2s-regs.h" + +struct odroid_priv { + struct snd_soc_card card; + struct clk *clk_i2s_bus; + struct clk *sclk_i2s; + + /* Spinlock protecting fields below */ + spinlock_t lock; + unsigned int be_sample_rate; + bool be_active; +}; + +static int odroid_card_fe_startup(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + + snd_pcm_hw_constraint_single(runtime, SNDRV_PCM_HW_PARAM_CHANNELS, 2); + + return 0; +} + +static int odroid_card_fe_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 odroid_priv *priv = snd_soc_card_get_drvdata(rtd->card); + unsigned long flags; + int ret = 0; + + spin_lock_irqsave(&priv->lock, flags); + if (priv->be_active && priv->be_sample_rate != params_rate(params)) + ret = -EINVAL; + spin_unlock_irqrestore(&priv->lock, flags); + + return ret; +} + +static const struct snd_soc_ops odroid_card_fe_ops = { + .startup = odroid_card_fe_startup, + .hw_params = odroid_card_fe_hw_params, +}; + +static int odroid_card_be_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 odroid_priv *priv = snd_soc_card_get_drvdata(rtd->card); + unsigned int pll_freq, rclk_freq, rfs; + unsigned long flags; + int ret; + + switch (params_rate(params)) { + case 64000: + pll_freq = 196608001U; + rfs = 384; + break; + case 44100: + case 88200: + pll_freq = 180633609U; + rfs = 512; + break; + case 32000: + case 48000: + case 96000: + pll_freq = 196608001U; + rfs = 512; + break; + default: + return -EINVAL; + } + + ret = clk_set_rate(priv->clk_i2s_bus, pll_freq / 2 + 1); + if (ret < 0) + return ret; + + /* + * We add 2 to the rclk_freq value in order to avoid too low clock + * frequency values due to the EPLL output frequency not being exact + * multiple of the audio sampling rate. + */ + rclk_freq = params_rate(params) * rfs + 2; + + ret = clk_set_rate(priv->sclk_i2s, rclk_freq); + if (ret < 0) + return ret; + + if (rtd->dai_link->num_codecs > 1) { + struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, 1); + + ret = snd_soc_dai_set_sysclk(codec_dai, 0, rclk_freq, + SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + } + + spin_lock_irqsave(&priv->lock, flags); + priv->be_sample_rate = params_rate(params); + spin_unlock_irqrestore(&priv->lock, flags); + + return 0; +} + +static int odroid_card_be_trigger(struct snd_pcm_substream *substream, int cmd) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct odroid_priv *priv = snd_soc_card_get_drvdata(rtd->card); + unsigned long flags; + + spin_lock_irqsave(&priv->lock, flags); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + priv->be_active = true; + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + priv->be_active = false; + break; + } + + spin_unlock_irqrestore(&priv->lock, flags); + + return 0; +} + +static const struct snd_soc_ops odroid_card_be_ops = { + .hw_params = odroid_card_be_hw_params, + .trigger = odroid_card_be_trigger, +}; + +/* DAPM routes for backward compatibility with old DTS */ +static const struct snd_soc_dapm_route odroid_dapm_routes[] = { + { "I2S Playback", NULL, "Mixer DAI TX" }, + { "HiFi Playback", NULL, "Mixer DAI TX" }, +}; + +SND_SOC_DAILINK_DEFS(primary, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_DUMMY()), + DAILINK_COMP_ARRAY(COMP_PLATFORM("3830000.i2s"))); + +SND_SOC_DAILINK_DEFS(mixer, + DAILINK_COMP_ARRAY(COMP_DUMMY()), + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_DUMMY())); + +SND_SOC_DAILINK_DEFS(secondary, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_DUMMY()), + DAILINK_COMP_ARRAY(COMP_PLATFORM("3830000.i2s-sec"))); + +static struct snd_soc_dai_link odroid_card_dais[] = { + { + /* Primary FE <-> BE link */ + .ops = &odroid_card_fe_ops, + .name = "Primary", + .stream_name = "Primary", + .dynamic = 1, + .dpcm_playback = 1, + SND_SOC_DAILINK_REG(primary), + }, { + /* BE <-> CODECs link */ + .name = "I2S Mixer", + .ops = &odroid_card_be_ops, + .no_pcm = 1, + .dpcm_playback = 1, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBS_CFS, + SND_SOC_DAILINK_REG(mixer), + }, { + /* Secondary FE <-> BE link */ + .playback_only = 1, + .ops = &odroid_card_fe_ops, + .name = "Secondary", + .stream_name = "Secondary", + .dynamic = 1, + .dpcm_playback = 1, + SND_SOC_DAILINK_REG(secondary), + } +}; + +static int odroid_audio_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *cpu_dai = NULL; + struct device_node *cpu, *codec; + struct odroid_priv *priv; + struct snd_soc_card *card; + struct snd_soc_dai_link *link, *codec_link; + int num_pcms, ret, i; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + card = &priv->card; + card->dev = dev; + + card->owner = THIS_MODULE; + card->fully_routed = true; + + spin_lock_init(&priv->lock); + snd_soc_card_set_drvdata(card, priv); + + ret = snd_soc_of_parse_card_name(card, "model"); + if (ret < 0) + return ret; + + if (of_property_present(dev->of_node, "samsung,audio-widgets")) { + ret = snd_soc_of_parse_audio_simple_widgets(card, + "samsung,audio-widgets"); + if (ret < 0) + return ret; + } + + ret = 0; + if (of_property_present(dev->of_node, "audio-routing")) + ret = snd_soc_of_parse_audio_routing(card, "audio-routing"); + else if (of_property_present(dev->of_node, "samsung,audio-routing")) + ret = snd_soc_of_parse_audio_routing(card, "samsung,audio-routing"); + if (ret < 0) + return ret; + + card->dai_link = odroid_card_dais; + card->num_links = ARRAY_SIZE(odroid_card_dais); + + cpu = of_get_child_by_name(dev->of_node, "cpu"); + codec = of_get_child_by_name(dev->of_node, "codec"); + link = card->dai_link; + codec_link = &card->dai_link[1]; + + /* + * For backwards compatibility create the secondary CPU DAI link only + * if there are 2 CPU DAI entries in the cpu sound-dai property in DT. + * Also add required DAPM routes not available in old DTS. + */ + num_pcms = of_count_phandle_with_args(cpu, "sound-dai", + "#sound-dai-cells"); + if (num_pcms == 1) { + card->dapm_routes = odroid_dapm_routes; + card->num_dapm_routes = ARRAY_SIZE(odroid_dapm_routes); + card->num_links--; + } + + for (i = 0; i < num_pcms; i++, link += 2) { + ret = snd_soc_of_get_dai_name(cpu, &link->cpus->dai_name, i); + if (ret < 0) + break; + } + if (ret == 0) { + cpu_dai = of_parse_phandle(cpu, "sound-dai", 0); + if (!cpu_dai) + ret = -EINVAL; + } + + of_node_put(cpu); + if (ret < 0) + goto err_put_node; + + ret = snd_soc_of_get_dai_link_codecs(dev, codec, codec_link); + if (ret < 0) + goto err_put_cpu_dai; + + /* Set capture capability only for boards with the MAX98090 CODEC */ + if (codec_link->num_codecs > 1) { + card->dai_link[0].dpcm_capture = 1; + card->dai_link[1].dpcm_capture = 1; + } + + priv->sclk_i2s = of_clk_get_by_name(cpu_dai, "i2s_opclk1"); + if (IS_ERR(priv->sclk_i2s)) { + ret = PTR_ERR(priv->sclk_i2s); + goto err_put_cpu_dai; + } + + priv->clk_i2s_bus = of_clk_get_by_name(cpu_dai, "iis"); + if (IS_ERR(priv->clk_i2s_bus)) { + ret = PTR_ERR(priv->clk_i2s_bus); + goto err_put_sclk; + } + + ret = devm_snd_soc_register_card(dev, card); + if (ret < 0) { + dev_err_probe(dev, ret, "snd_soc_register_card() failed\n"); + goto err_put_clk_i2s; + } + + of_node_put(cpu_dai); + of_node_put(codec); + return 0; + +err_put_clk_i2s: + clk_put(priv->clk_i2s_bus); +err_put_sclk: + clk_put(priv->sclk_i2s); +err_put_cpu_dai: + of_node_put(cpu_dai); + snd_soc_of_put_dai_link_codecs(codec_link); +err_put_node: + of_node_put(codec); + return ret; +} + +static void odroid_audio_remove(struct platform_device *pdev) +{ + struct odroid_priv *priv = platform_get_drvdata(pdev); + + snd_soc_of_put_dai_link_codecs(&priv->card.dai_link[1]); + clk_put(priv->sclk_i2s); + clk_put(priv->clk_i2s_bus); +} + +static const struct of_device_id odroid_audio_of_match[] = { + { .compatible = "hardkernel,odroid-xu3-audio" }, + { .compatible = "hardkernel,odroid-xu4-audio" }, + { .compatible = "samsung,odroid-xu3-audio" }, + { .compatible = "samsung,odroid-xu4-audio" }, + { }, +}; +MODULE_DEVICE_TABLE(of, odroid_audio_of_match); + +static struct platform_driver odroid_audio_driver = { + .driver = { + .name = "odroid-audio", + .of_match_table = odroid_audio_of_match, + .pm = &snd_soc_pm_ops, + }, + .probe = odroid_audio_probe, + .remove_new = odroid_audio_remove, +}; +module_platform_driver(odroid_audio_driver); + +MODULE_AUTHOR("Sylwester Nawrocki <s.nawrocki@samsung.com>"); +MODULE_DESCRIPTION("Odroid XU3/XU4 audio support"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/samsung/pcm.c b/sound/soc/samsung/pcm.c new file mode 100644 index 0000000000..d2cdc5c8e0 --- /dev/null +++ b/sound/soc/samsung/pcm.c @@ -0,0 +1,605 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// ALSA SoC Audio Layer - S3C PCM-Controller driver +// +// Copyright (c) 2009 Samsung Electronics Co. Ltd +// Author: Jaswinder Singh <jassisinghbrar@gmail.com> +// based upon I2S drivers by Ben Dooks. + +#include <linux/clk.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/pm_runtime.h> + +#include <sound/soc.h> +#include <sound/pcm_params.h> + +#include <linux/platform_data/asoc-s3c.h> + +#include "dma.h" +#include "pcm.h" + +/*Register Offsets */ +#define S3C_PCM_CTL 0x00 +#define S3C_PCM_CLKCTL 0x04 +#define S3C_PCM_TXFIFO 0x08 +#define S3C_PCM_RXFIFO 0x0C +#define S3C_PCM_IRQCTL 0x10 +#define S3C_PCM_IRQSTAT 0x14 +#define S3C_PCM_FIFOSTAT 0x18 +#define S3C_PCM_CLRINT 0x20 + +/* PCM_CTL Bit-Fields */ +#define S3C_PCM_CTL_TXDIPSTICK_MASK 0x3f +#define S3C_PCM_CTL_TXDIPSTICK_SHIFT 13 +#define S3C_PCM_CTL_RXDIPSTICK_MASK 0x3f +#define S3C_PCM_CTL_RXDIPSTICK_SHIFT 7 +#define S3C_PCM_CTL_TXDMA_EN (0x1 << 6) +#define S3C_PCM_CTL_RXDMA_EN (0x1 << 5) +#define S3C_PCM_CTL_TXMSB_AFTER_FSYNC (0x1 << 4) +#define S3C_PCM_CTL_RXMSB_AFTER_FSYNC (0x1 << 3) +#define S3C_PCM_CTL_TXFIFO_EN (0x1 << 2) +#define S3C_PCM_CTL_RXFIFO_EN (0x1 << 1) +#define S3C_PCM_CTL_ENABLE (0x1 << 0) + +/* PCM_CLKCTL Bit-Fields */ +#define S3C_PCM_CLKCTL_SERCLK_EN (0x1 << 19) +#define S3C_PCM_CLKCTL_SERCLKSEL_PCLK (0x1 << 18) +#define S3C_PCM_CLKCTL_SCLKDIV_MASK 0x1ff +#define S3C_PCM_CLKCTL_SYNCDIV_MASK 0x1ff +#define S3C_PCM_CLKCTL_SCLKDIV_SHIFT 9 +#define S3C_PCM_CLKCTL_SYNCDIV_SHIFT 0 + +/* PCM_TXFIFO Bit-Fields */ +#define S3C_PCM_TXFIFO_DVALID (0x1 << 16) +#define S3C_PCM_TXFIFO_DATA_MSK (0xffff << 0) + +/* PCM_RXFIFO Bit-Fields */ +#define S3C_PCM_RXFIFO_DVALID (0x1 << 16) +#define S3C_PCM_RXFIFO_DATA_MSK (0xffff << 0) + +/* PCM_IRQCTL Bit-Fields */ +#define S3C_PCM_IRQCTL_IRQEN (0x1 << 14) +#define S3C_PCM_IRQCTL_WRDEN (0x1 << 12) +#define S3C_PCM_IRQCTL_TXEMPTYEN (0x1 << 11) +#define S3C_PCM_IRQCTL_TXALMSTEMPTYEN (0x1 << 10) +#define S3C_PCM_IRQCTL_TXFULLEN (0x1 << 9) +#define S3C_PCM_IRQCTL_TXALMSTFULLEN (0x1 << 8) +#define S3C_PCM_IRQCTL_TXSTARVEN (0x1 << 7) +#define S3C_PCM_IRQCTL_TXERROVRFLEN (0x1 << 6) +#define S3C_PCM_IRQCTL_RXEMPTEN (0x1 << 5) +#define S3C_PCM_IRQCTL_RXALMSTEMPTEN (0x1 << 4) +#define S3C_PCM_IRQCTL_RXFULLEN (0x1 << 3) +#define S3C_PCM_IRQCTL_RXALMSTFULLEN (0x1 << 2) +#define S3C_PCM_IRQCTL_RXSTARVEN (0x1 << 1) +#define S3C_PCM_IRQCTL_RXERROVRFLEN (0x1 << 0) + +/* PCM_IRQSTAT Bit-Fields */ +#define S3C_PCM_IRQSTAT_IRQPND (0x1 << 13) +#define S3C_PCM_IRQSTAT_WRD_XFER (0x1 << 12) +#define S3C_PCM_IRQSTAT_TXEMPTY (0x1 << 11) +#define S3C_PCM_IRQSTAT_TXALMSTEMPTY (0x1 << 10) +#define S3C_PCM_IRQSTAT_TXFULL (0x1 << 9) +#define S3C_PCM_IRQSTAT_TXALMSTFULL (0x1 << 8) +#define S3C_PCM_IRQSTAT_TXSTARV (0x1 << 7) +#define S3C_PCM_IRQSTAT_TXERROVRFL (0x1 << 6) +#define S3C_PCM_IRQSTAT_RXEMPT (0x1 << 5) +#define S3C_PCM_IRQSTAT_RXALMSTEMPT (0x1 << 4) +#define S3C_PCM_IRQSTAT_RXFULL (0x1 << 3) +#define S3C_PCM_IRQSTAT_RXALMSTFULL (0x1 << 2) +#define S3C_PCM_IRQSTAT_RXSTARV (0x1 << 1) +#define S3C_PCM_IRQSTAT_RXERROVRFL (0x1 << 0) + +/* PCM_FIFOSTAT Bit-Fields */ +#define S3C_PCM_FIFOSTAT_TXCNT_MSK (0x3f << 14) +#define S3C_PCM_FIFOSTAT_TXFIFOEMPTY (0x1 << 13) +#define S3C_PCM_FIFOSTAT_TXFIFOALMSTEMPTY (0x1 << 12) +#define S3C_PCM_FIFOSTAT_TXFIFOFULL (0x1 << 11) +#define S3C_PCM_FIFOSTAT_TXFIFOALMSTFULL (0x1 << 10) +#define S3C_PCM_FIFOSTAT_RXCNT_MSK (0x3f << 4) +#define S3C_PCM_FIFOSTAT_RXFIFOEMPTY (0x1 << 3) +#define S3C_PCM_FIFOSTAT_RXFIFOALMSTEMPTY (0x1 << 2) +#define S3C_PCM_FIFOSTAT_RXFIFOFULL (0x1 << 1) +#define S3C_PCM_FIFOSTAT_RXFIFOALMSTFULL (0x1 << 0) + +/** + * struct s3c_pcm_info - S3C PCM Controller information + * @lock: Spin lock + * @dev: The parent device passed to use from the probe. + * @regs: The pointer to the device register block. + * @sclk_per_fs: number of sclk per frame sync + * @idleclk: Whether to keep PCMSCLK enabled even when idle (no active xfer) + * @pclk: the PCLK_PCM (pcm) clock pointer + * @cclk: the SCLK_AUDIO (audio-bus) clock pointer + * @dma_playback: DMA information for playback channel. + * @dma_capture: DMA information for capture channel. + */ +struct s3c_pcm_info { + spinlock_t lock; + struct device *dev; + void __iomem *regs; + + unsigned int sclk_per_fs; + + /* Whether to keep PCMSCLK enabled even when idle(no active xfer) */ + unsigned int idleclk; + + struct clk *pclk; + struct clk *cclk; + + struct snd_dmaengine_dai_dma_data *dma_playback; + struct snd_dmaengine_dai_dma_data *dma_capture; +}; + +static struct snd_dmaengine_dai_dma_data s3c_pcm_stereo_out[] = { + [0] = { + .addr_width = 4, + }, + [1] = { + .addr_width = 4, + }, +}; + +static struct snd_dmaengine_dai_dma_data s3c_pcm_stereo_in[] = { + [0] = { + .addr_width = 4, + }, + [1] = { + .addr_width = 4, + }, +}; + +static struct s3c_pcm_info s3c_pcm[2]; + +static void s3c_pcm_snd_txctrl(struct s3c_pcm_info *pcm, int on) +{ + void __iomem *regs = pcm->regs; + u32 ctl, clkctl; + + clkctl = readl(regs + S3C_PCM_CLKCTL); + ctl = readl(regs + S3C_PCM_CTL); + ctl &= ~(S3C_PCM_CTL_TXDIPSTICK_MASK + << S3C_PCM_CTL_TXDIPSTICK_SHIFT); + + if (on) { + ctl |= S3C_PCM_CTL_TXDMA_EN; + ctl |= S3C_PCM_CTL_TXFIFO_EN; + ctl |= S3C_PCM_CTL_ENABLE; + ctl |= (0x4<<S3C_PCM_CTL_TXDIPSTICK_SHIFT); + clkctl |= S3C_PCM_CLKCTL_SERCLK_EN; + } else { + ctl &= ~S3C_PCM_CTL_TXDMA_EN; + ctl &= ~S3C_PCM_CTL_TXFIFO_EN; + + if (!(ctl & S3C_PCM_CTL_RXFIFO_EN)) { + ctl &= ~S3C_PCM_CTL_ENABLE; + if (!pcm->idleclk) + clkctl |= S3C_PCM_CLKCTL_SERCLK_EN; + } + } + + writel(clkctl, regs + S3C_PCM_CLKCTL); + writel(ctl, regs + S3C_PCM_CTL); +} + +static void s3c_pcm_snd_rxctrl(struct s3c_pcm_info *pcm, int on) +{ + void __iomem *regs = pcm->regs; + u32 ctl, clkctl; + + ctl = readl(regs + S3C_PCM_CTL); + clkctl = readl(regs + S3C_PCM_CLKCTL); + ctl &= ~(S3C_PCM_CTL_RXDIPSTICK_MASK + << S3C_PCM_CTL_RXDIPSTICK_SHIFT); + + if (on) { + ctl |= S3C_PCM_CTL_RXDMA_EN; + ctl |= S3C_PCM_CTL_RXFIFO_EN; + ctl |= S3C_PCM_CTL_ENABLE; + ctl |= (0x20<<S3C_PCM_CTL_RXDIPSTICK_SHIFT); + clkctl |= S3C_PCM_CLKCTL_SERCLK_EN; + } else { + ctl &= ~S3C_PCM_CTL_RXDMA_EN; + ctl &= ~S3C_PCM_CTL_RXFIFO_EN; + + if (!(ctl & S3C_PCM_CTL_TXFIFO_EN)) { + ctl &= ~S3C_PCM_CTL_ENABLE; + if (!pcm->idleclk) + clkctl |= S3C_PCM_CLKCTL_SERCLK_EN; + } + } + + writel(clkctl, regs + S3C_PCM_CLKCTL); + writel(ctl, regs + S3C_PCM_CTL); +} + +static int s3c_pcm_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct s3c_pcm_info *pcm = snd_soc_dai_get_drvdata(asoc_rtd_to_cpu(rtd, 0)); + unsigned long flags; + + dev_dbg(pcm->dev, "Entered %s\n", __func__); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + spin_lock_irqsave(&pcm->lock, flags); + + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) + s3c_pcm_snd_rxctrl(pcm, 1); + else + s3c_pcm_snd_txctrl(pcm, 1); + + spin_unlock_irqrestore(&pcm->lock, flags); + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + spin_lock_irqsave(&pcm->lock, flags); + + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) + s3c_pcm_snd_rxctrl(pcm, 0); + else + s3c_pcm_snd_txctrl(pcm, 0); + + spin_unlock_irqrestore(&pcm->lock, flags); + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int s3c_pcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *socdai) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct s3c_pcm_info *pcm = snd_soc_dai_get_drvdata(asoc_rtd_to_cpu(rtd, 0)); + void __iomem *regs = pcm->regs; + struct clk *clk; + int sclk_div, sync_div; + unsigned long flags; + u32 clkctl; + + dev_dbg(pcm->dev, "Entered %s\n", __func__); + + /* Strictly check for sample size */ + switch (params_width(params)) { + case 16: + break; + default: + return -EINVAL; + } + + spin_lock_irqsave(&pcm->lock, flags); + + /* Get hold of the PCMSOURCE_CLK */ + clkctl = readl(regs + S3C_PCM_CLKCTL); + if (clkctl & S3C_PCM_CLKCTL_SERCLKSEL_PCLK) + clk = pcm->pclk; + else + clk = pcm->cclk; + + /* Set the SCLK divider */ + sclk_div = clk_get_rate(clk) / pcm->sclk_per_fs / + params_rate(params) / 2 - 1; + + clkctl &= ~(S3C_PCM_CLKCTL_SCLKDIV_MASK + << S3C_PCM_CLKCTL_SCLKDIV_SHIFT); + clkctl |= ((sclk_div & S3C_PCM_CLKCTL_SCLKDIV_MASK) + << S3C_PCM_CLKCTL_SCLKDIV_SHIFT); + + /* Set the SYNC divider */ + sync_div = pcm->sclk_per_fs - 1; + + clkctl &= ~(S3C_PCM_CLKCTL_SYNCDIV_MASK + << S3C_PCM_CLKCTL_SYNCDIV_SHIFT); + clkctl |= ((sync_div & S3C_PCM_CLKCTL_SYNCDIV_MASK) + << S3C_PCM_CLKCTL_SYNCDIV_SHIFT); + + writel(clkctl, regs + S3C_PCM_CLKCTL); + + spin_unlock_irqrestore(&pcm->lock, flags); + + dev_dbg(pcm->dev, "PCMSOURCE_CLK-%lu SCLK=%ufs SCLK_DIV=%d SYNC_DIV=%d\n", + clk_get_rate(clk), pcm->sclk_per_fs, + sclk_div, sync_div); + + return 0; +} + +static int s3c_pcm_set_fmt(struct snd_soc_dai *cpu_dai, + unsigned int fmt) +{ + struct s3c_pcm_info *pcm = snd_soc_dai_get_drvdata(cpu_dai); + void __iomem *regs = pcm->regs; + unsigned long flags; + int ret = 0; + u32 ctl; + + dev_dbg(pcm->dev, "Entered %s\n", __func__); + + spin_lock_irqsave(&pcm->lock, flags); + + ctl = readl(regs + S3C_PCM_CTL); + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_IB_NF: + /* Nothing to do, IB_NF by default */ + break; + default: + dev_err(pcm->dev, "Unsupported clock inversion!\n"); + ret = -EINVAL; + goto exit; + } + + switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) { + case SND_SOC_DAIFMT_BP_FP: + /* Nothing to do, Master by default */ + break; + default: + dev_err(pcm->dev, "Unsupported master/slave format!\n"); + ret = -EINVAL; + goto exit; + } + + switch (fmt & SND_SOC_DAIFMT_CLOCK_MASK) { + case SND_SOC_DAIFMT_CONT: + pcm->idleclk = 1; + break; + case SND_SOC_DAIFMT_GATED: + pcm->idleclk = 0; + break; + default: + dev_err(pcm->dev, "Invalid Clock gating request!\n"); + ret = -EINVAL; + goto exit; + } + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_DSP_A: + ctl |= S3C_PCM_CTL_TXMSB_AFTER_FSYNC; + ctl |= S3C_PCM_CTL_RXMSB_AFTER_FSYNC; + break; + case SND_SOC_DAIFMT_DSP_B: + ctl &= ~S3C_PCM_CTL_TXMSB_AFTER_FSYNC; + ctl &= ~S3C_PCM_CTL_RXMSB_AFTER_FSYNC; + break; + default: + dev_err(pcm->dev, "Unsupported data format!\n"); + ret = -EINVAL; + goto exit; + } + + writel(ctl, regs + S3C_PCM_CTL); + +exit: + spin_unlock_irqrestore(&pcm->lock, flags); + + return ret; +} + +static int s3c_pcm_set_clkdiv(struct snd_soc_dai *cpu_dai, + int div_id, int div) +{ + struct s3c_pcm_info *pcm = snd_soc_dai_get_drvdata(cpu_dai); + + switch (div_id) { + case S3C_PCM_SCLK_PER_FS: + pcm->sclk_per_fs = div; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int s3c_pcm_set_sysclk(struct snd_soc_dai *cpu_dai, + int clk_id, unsigned int freq, int dir) +{ + struct s3c_pcm_info *pcm = snd_soc_dai_get_drvdata(cpu_dai); + void __iomem *regs = pcm->regs; + u32 clkctl = readl(regs + S3C_PCM_CLKCTL); + + switch (clk_id) { + case S3C_PCM_CLKSRC_PCLK: + clkctl |= S3C_PCM_CLKCTL_SERCLKSEL_PCLK; + break; + + case S3C_PCM_CLKSRC_MUX: + clkctl &= ~S3C_PCM_CLKCTL_SERCLKSEL_PCLK; + + if (clk_get_rate(pcm->cclk) != freq) + clk_set_rate(pcm->cclk, freq); + + break; + + default: + return -EINVAL; + } + + writel(clkctl, regs + S3C_PCM_CLKCTL); + + return 0; +} + +static int s3c_pcm_dai_probe(struct snd_soc_dai *dai) +{ + struct s3c_pcm_info *pcm = snd_soc_dai_get_drvdata(dai); + + snd_soc_dai_init_dma_data(dai, pcm->dma_playback, pcm->dma_capture); + + return 0; +} + +static const struct snd_soc_dai_ops s3c_pcm_dai_ops = { + .probe = s3c_pcm_dai_probe, + .set_sysclk = s3c_pcm_set_sysclk, + .set_clkdiv = s3c_pcm_set_clkdiv, + .trigger = s3c_pcm_trigger, + .hw_params = s3c_pcm_hw_params, + .set_fmt = s3c_pcm_set_fmt, +}; + +#define S3C_PCM_RATES SNDRV_PCM_RATE_8000_96000 + +#define S3C_PCM_DAI_DECLARE \ + .symmetric_rate = 1, \ + .ops = &s3c_pcm_dai_ops, \ + .playback = { \ + .channels_min = 2, \ + .channels_max = 2, \ + .rates = S3C_PCM_RATES, \ + .formats = SNDRV_PCM_FMTBIT_S16_LE, \ + }, \ + .capture = { \ + .channels_min = 2, \ + .channels_max = 2, \ + .rates = S3C_PCM_RATES, \ + .formats = SNDRV_PCM_FMTBIT_S16_LE, \ + } + +static struct snd_soc_dai_driver s3c_pcm_dai[] = { + [0] = { + .name = "samsung-pcm.0", + S3C_PCM_DAI_DECLARE, + }, + [1] = { + .name = "samsung-pcm.1", + S3C_PCM_DAI_DECLARE, + }, +}; + +static const struct snd_soc_component_driver s3c_pcm_component = { + .name = "s3c-pcm", + .legacy_dai_naming = 1, +}; + +static int s3c_pcm_dev_probe(struct platform_device *pdev) +{ + struct s3c_pcm_info *pcm; + struct resource *mem_res; + struct s3c_audio_pdata *pcm_pdata; + dma_filter_fn filter; + int ret; + + /* Check for valid device index */ + if ((pdev->id < 0) || pdev->id >= ARRAY_SIZE(s3c_pcm)) { + dev_err(&pdev->dev, "id %d out of range\n", pdev->id); + return -EINVAL; + } + + pcm_pdata = pdev->dev.platform_data; + + if (pcm_pdata && pcm_pdata->cfg_gpio && pcm_pdata->cfg_gpio(pdev)) { + dev_err(&pdev->dev, "Unable to configure gpio\n"); + return -EINVAL; + } + + pcm = &s3c_pcm[pdev->id]; + pcm->dev = &pdev->dev; + + spin_lock_init(&pcm->lock); + + /* Default is 128fs */ + pcm->sclk_per_fs = 128; + + pcm->regs = devm_platform_get_and_ioremap_resource(pdev, 0, &mem_res); + if (IS_ERR(pcm->regs)) + return PTR_ERR(pcm->regs); + + pcm->cclk = devm_clk_get(&pdev->dev, "audio-bus"); + if (IS_ERR(pcm->cclk)) { + dev_err(&pdev->dev, "failed to get audio-bus clock\n"); + return PTR_ERR(pcm->cclk); + } + ret = clk_prepare_enable(pcm->cclk); + if (ret) + return ret; + + /* record our pcm structure for later use in the callbacks */ + dev_set_drvdata(&pdev->dev, pcm); + + pcm->pclk = devm_clk_get(&pdev->dev, "pcm"); + if (IS_ERR(pcm->pclk)) { + dev_err(&pdev->dev, "failed to get pcm clock\n"); + ret = PTR_ERR(pcm->pclk); + goto err_dis_cclk; + } + ret = clk_prepare_enable(pcm->pclk); + if (ret) + goto err_dis_cclk; + + s3c_pcm_stereo_in[pdev->id].addr = mem_res->start + S3C_PCM_RXFIFO; + s3c_pcm_stereo_out[pdev->id].addr = mem_res->start + S3C_PCM_TXFIFO; + + filter = NULL; + if (pcm_pdata) { + s3c_pcm_stereo_in[pdev->id].filter_data = pcm_pdata->dma_capture; + s3c_pcm_stereo_out[pdev->id].filter_data = pcm_pdata->dma_playback; + filter = pcm_pdata->dma_filter; + } + + pcm->dma_capture = &s3c_pcm_stereo_in[pdev->id]; + pcm->dma_playback = &s3c_pcm_stereo_out[pdev->id]; + + ret = samsung_asoc_dma_platform_register(&pdev->dev, filter, + NULL, NULL, NULL); + if (ret) { + dev_err(&pdev->dev, "failed to get register DMA: %d\n", ret); + goto err_dis_pclk; + } + + pm_runtime_enable(&pdev->dev); + + ret = devm_snd_soc_register_component(&pdev->dev, &s3c_pcm_component, + &s3c_pcm_dai[pdev->id], 1); + if (ret != 0) { + dev_err(&pdev->dev, "failed to get register DAI: %d\n", ret); + goto err_dis_pm; + } + + return 0; + +err_dis_pm: + pm_runtime_disable(&pdev->dev); +err_dis_pclk: + clk_disable_unprepare(pcm->pclk); +err_dis_cclk: + clk_disable_unprepare(pcm->cclk); + return ret; +} + +static void s3c_pcm_dev_remove(struct platform_device *pdev) +{ + struct s3c_pcm_info *pcm = &s3c_pcm[pdev->id]; + + pm_runtime_disable(&pdev->dev); + clk_disable_unprepare(pcm->cclk); + clk_disable_unprepare(pcm->pclk); +} + +static struct platform_driver s3c_pcm_driver = { + .probe = s3c_pcm_dev_probe, + .remove_new = s3c_pcm_dev_remove, + .driver = { + .name = "samsung-pcm", + }, +}; + +module_platform_driver(s3c_pcm_driver); + +/* Module information */ +MODULE_AUTHOR("Jaswinder Singh, <jassisinghbrar@gmail.com>"); +MODULE_DESCRIPTION("S3C PCM Controller Driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:samsung-pcm"); diff --git a/sound/soc/samsung/pcm.h b/sound/soc/samsung/pcm.h new file mode 100644 index 0000000000..208d8da27d --- /dev/null +++ b/sound/soc/samsung/pcm.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +#ifndef __S3C_PCM_H +#define __S3C_PCM_H __FILE__ + +#define S3C_PCM_CLKSRC_PCLK 0 +#define S3C_PCM_CLKSRC_MUX 1 + +#define S3C_PCM_SCLK_PER_FS 0 + +#endif /* __S3C_PCM_H */ diff --git a/sound/soc/samsung/smdk_spdif.c b/sound/soc/samsung/smdk_spdif.c new file mode 100644 index 0000000000..6f3eeb7bc8 --- /dev/null +++ b/sound/soc/samsung/smdk_spdif.c @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// smdk_spdif.c - S/PDIF audio for SMDK +// +// Copyright (C) 2010 Samsung Electronics Co., Ltd. + +#include <linux/clk.h> +#include <linux/module.h> + +#include <sound/soc.h> + +#include "spdif.h" + +/* Audio clock settings are belonged to board specific part. Every + * board can set audio source clock setting which is matched with H/W + * like this function-'set_audio_clock_heirachy'. + */ +static int set_audio_clock_heirachy(struct platform_device *pdev) +{ + struct clk *fout_epll, *mout_epll, *sclk_audio0, *sclk_spdif; + int ret = 0; + + fout_epll = clk_get(NULL, "fout_epll"); + if (IS_ERR(fout_epll)) { + printk(KERN_WARNING "%s: Cannot find fout_epll.\n", + __func__); + return -EINVAL; + } + + mout_epll = clk_get(NULL, "mout_epll"); + if (IS_ERR(mout_epll)) { + printk(KERN_WARNING "%s: Cannot find mout_epll.\n", + __func__); + ret = -EINVAL; + goto out1; + } + + sclk_audio0 = clk_get(&pdev->dev, "sclk_audio"); + if (IS_ERR(sclk_audio0)) { + printk(KERN_WARNING "%s: Cannot find sclk_audio.\n", + __func__); + ret = -EINVAL; + goto out2; + } + + sclk_spdif = clk_get(NULL, "sclk_spdif"); + if (IS_ERR(sclk_spdif)) { + printk(KERN_WARNING "%s: Cannot find sclk_spdif.\n", + __func__); + ret = -EINVAL; + goto out3; + } + + /* Set audio clock hierarchy for S/PDIF */ + clk_set_parent(mout_epll, fout_epll); + clk_set_parent(sclk_audio0, mout_epll); + clk_set_parent(sclk_spdif, sclk_audio0); + + clk_put(sclk_spdif); +out3: + clk_put(sclk_audio0); +out2: + clk_put(mout_epll); +out1: + clk_put(fout_epll); + + return ret; +} + +/* We should haved to set clock directly on this part because of clock + * scheme of Samsudng SoCs did not support to set rates from abstrct + * clock of it's hierarchy. + */ +static int set_audio_clock_rate(unsigned long epll_rate, + unsigned long audio_rate) +{ + struct clk *fout_epll, *sclk_spdif; + + fout_epll = clk_get(NULL, "fout_epll"); + if (IS_ERR(fout_epll)) { + printk(KERN_ERR "%s: failed to get fout_epll\n", __func__); + return -ENOENT; + } + + clk_set_rate(fout_epll, epll_rate); + clk_put(fout_epll); + + sclk_spdif = clk_get(NULL, "sclk_spdif"); + if (IS_ERR(sclk_spdif)) { + printk(KERN_ERR "%s: failed to get sclk_spdif\n", __func__); + return -ENOENT; + } + + clk_set_rate(sclk_spdif, audio_rate); + clk_put(sclk_spdif); + + return 0; +} + +static int smdk_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 *cpu_dai = asoc_rtd_to_cpu(rtd, 0); + unsigned long pll_out, rclk_rate; + int ret, ratio; + + switch (params_rate(params)) { + case 44100: + pll_out = 45158400; + break; + case 32000: + case 48000: + case 96000: + pll_out = 49152000; + break; + default: + return -EINVAL; + } + + /* Setting ratio to 512fs helps to use S/PDIF with HDMI without + * modify S/PDIF ASoC machine driver. + */ + ratio = 512; + rclk_rate = params_rate(params) * ratio; + + /* Set audio source clock rates */ + ret = set_audio_clock_rate(pll_out, rclk_rate); + if (ret < 0) + return ret; + + /* Set S/PDIF uses internal source clock */ + ret = snd_soc_dai_set_sysclk(cpu_dai, SND_SOC_SPDIF_INT_MCLK, + rclk_rate, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + return ret; +} + +static const struct snd_soc_ops smdk_spdif_ops = { + .hw_params = smdk_hw_params, +}; + +SND_SOC_DAILINK_DEFS(spdif, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-spdif")), + DAILINK_COMP_ARRAY(COMP_CODEC("spdif-dit", "dit-hifi")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-spdif"))); + +static struct snd_soc_dai_link smdk_dai = { + .name = "S/PDIF", + .stream_name = "S/PDIF PCM Playback", + .ops = &smdk_spdif_ops, + SND_SOC_DAILINK_REG(spdif), +}; + +static struct snd_soc_card smdk = { + .name = "SMDK-S/PDIF", + .owner = THIS_MODULE, + .dai_link = &smdk_dai, + .num_links = 1, +}; + +static struct platform_device *smdk_snd_spdif_dit_device; +static struct platform_device *smdk_snd_spdif_device; + +static int __init smdk_init(void) +{ + int ret; + + smdk_snd_spdif_dit_device = platform_device_alloc("spdif-dit", -1); + if (!smdk_snd_spdif_dit_device) + return -ENOMEM; + + ret = platform_device_add(smdk_snd_spdif_dit_device); + if (ret) + goto err1; + + smdk_snd_spdif_device = platform_device_alloc("soc-audio", -1); + if (!smdk_snd_spdif_device) { + ret = -ENOMEM; + goto err2; + } + + platform_set_drvdata(smdk_snd_spdif_device, &smdk); + + ret = platform_device_add(smdk_snd_spdif_device); + if (ret) + goto err3; + + /* Set audio clock hierarchy manually */ + ret = set_audio_clock_heirachy(smdk_snd_spdif_device); + if (ret) + goto err4; + + return 0; +err4: + platform_device_del(smdk_snd_spdif_device); +err3: + platform_device_put(smdk_snd_spdif_device); +err2: + platform_device_del(smdk_snd_spdif_dit_device); +err1: + platform_device_put(smdk_snd_spdif_dit_device); + return ret; +} + +static void __exit smdk_exit(void) +{ + platform_device_unregister(smdk_snd_spdif_device); + platform_device_unregister(smdk_snd_spdif_dit_device); +} + +module_init(smdk_init); +module_exit(smdk_exit); + +MODULE_AUTHOR("Seungwhan Youn, <sw.youn@samsung.com>"); +MODULE_DESCRIPTION("ALSA SoC SMDK+S/PDIF"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/samsung/smdk_wm8994.c b/sound/soc/samsung/smdk_wm8994.c new file mode 100644 index 0000000000..821ad1eb1b --- /dev/null +++ b/sound/soc/samsung/smdk_wm8994.c @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include "../codecs/wm8994.h" +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> + + /* + * Default CFG switch settings to use this driver: + * SMDKV310: CFG5-1000, CFG7-111111 + */ + + /* + * Configure audio route as :- + * $ amixer sset 'DAC1' on,on + * $ amixer sset 'Right Headphone Mux' 'DAC' + * $ amixer sset 'Left Headphone Mux' 'DAC' + * $ amixer sset 'DAC1R Mixer AIF1.1' on + * $ amixer sset 'DAC1L Mixer AIF1.1' on + * $ amixer sset 'IN2L' on + * $ amixer sset 'IN2L PGA IN2LN' on + * $ amixer sset 'MIXINL IN2L' on + * $ amixer sset 'AIF1ADC1L Mixer ADC/DMIC' on + * $ amixer sset 'IN2R' on + * $ amixer sset 'IN2R PGA IN2RN' on + * $ amixer sset 'MIXINR IN2R' on + * $ amixer sset 'AIF1ADC1R Mixer ADC/DMIC' on + */ + +/* SMDK has a 16.934MHZ crystal attached to WM8994 */ +#define SMDK_WM8994_FREQ 16934000 + +struct smdk_wm8994_data { + int mclk1_rate; +}; + +/* Default SMDKs */ +static struct smdk_wm8994_data smdk_board_data = { + .mclk1_rate = SMDK_WM8994_FREQ, +}; + +static int smdk_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); + unsigned int pll_out; + int ret; + + /* AIF1CLK should be >=3MHz for optimal performance */ + if (params_width(params) == 24) + pll_out = params_rate(params) * 384; + else if (params_rate(params) == 8000 || params_rate(params) == 11025) + pll_out = params_rate(params) * 512; + else + pll_out = params_rate(params) * 256; + + ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, WM8994_FLL_SRC_MCLK1, + SMDK_WM8994_FREQ, pll_out); + if (ret < 0) + return ret; + + ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1, + pll_out, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + return 0; +} + +/* + * SMDK WM8994 DAI operations. + */ +static const struct snd_soc_ops smdk_ops = { + .hw_params = smdk_hw_params, +}; + +static int smdk_wm8994_init_paiftx(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_dapm_context *dapm = &rtd->card->dapm; + + /* Other pins NC */ + snd_soc_dapm_nc_pin(dapm, "HPOUT2P"); + snd_soc_dapm_nc_pin(dapm, "HPOUT2N"); + snd_soc_dapm_nc_pin(dapm, "SPKOUTLN"); + snd_soc_dapm_nc_pin(dapm, "SPKOUTLP"); + snd_soc_dapm_nc_pin(dapm, "SPKOUTRP"); + snd_soc_dapm_nc_pin(dapm, "SPKOUTRN"); + snd_soc_dapm_nc_pin(dapm, "LINEOUT1N"); + snd_soc_dapm_nc_pin(dapm, "LINEOUT1P"); + snd_soc_dapm_nc_pin(dapm, "LINEOUT2N"); + snd_soc_dapm_nc_pin(dapm, "LINEOUT2P"); + snd_soc_dapm_nc_pin(dapm, "IN1LP"); + snd_soc_dapm_nc_pin(dapm, "IN2LP:VXRN"); + snd_soc_dapm_nc_pin(dapm, "IN1RP"); + snd_soc_dapm_nc_pin(dapm, "IN2RP:VXRP"); + + return 0; +} + +SND_SOC_DAILINK_DEFS(aif1, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s.0")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm8994-codec", "wm8994-aif1")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s.0"))); + +SND_SOC_DAILINK_DEFS(fifo_tx, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s-sec")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm8994-codec", "wm8994-aif1")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s-sec"))); + +static struct snd_soc_dai_link smdk_dai[] = { + { /* Primary DAI i/f */ + .name = "WM8994 AIF1", + .stream_name = "Pri_Dai", + .init = smdk_wm8994_init_paiftx, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + .ops = &smdk_ops, + SND_SOC_DAILINK_REG(aif1), + }, { /* Sec_Fifo Playback i/f */ + .name = "Sec_FIFO TX", + .stream_name = "Sec_Dai", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + .ops = &smdk_ops, + SND_SOC_DAILINK_REG(fifo_tx), + }, +}; + +static struct snd_soc_card smdk = { + .name = "SMDK-I2S", + .owner = THIS_MODULE, + .dai_link = smdk_dai, + .num_links = ARRAY_SIZE(smdk_dai), +}; + +static const struct of_device_id samsung_wm8994_of_match[] __maybe_unused = { + { .compatible = "samsung,smdk-wm8994", .data = &smdk_board_data }, + {}, +}; +MODULE_DEVICE_TABLE(of, samsung_wm8994_of_match); + +static int smdk_audio_probe(struct platform_device *pdev) +{ + int ret; + struct device_node *np = pdev->dev.of_node; + struct snd_soc_card *card = &smdk; + struct smdk_wm8994_data *board; + const struct of_device_id *id; + + card->dev = &pdev->dev; + + board = devm_kzalloc(&pdev->dev, sizeof(*board), GFP_KERNEL); + if (!board) + return -ENOMEM; + + if (np) { + smdk_dai[0].cpus->dai_name = NULL; + smdk_dai[0].cpus->of_node = of_parse_phandle(np, + "samsung,i2s-controller", 0); + if (!smdk_dai[0].cpus->of_node) { + dev_err(&pdev->dev, + "Property 'samsung,i2s-controller' missing or invalid\n"); + ret = -EINVAL; + return ret; + } + + smdk_dai[0].platforms->name = NULL; + smdk_dai[0].platforms->of_node = smdk_dai[0].cpus->of_node; + } + + id = of_match_device(samsung_wm8994_of_match, &pdev->dev); + if (id) + *board = *((struct smdk_wm8994_data *)id->data); + + platform_set_drvdata(pdev, board); + + ret = devm_snd_soc_register_card(&pdev->dev, card); + + if (ret) + dev_err_probe(&pdev->dev, ret, "snd_soc_register_card() failed\n"); + + return ret; +} + +static struct platform_driver smdk_audio_driver = { + .driver = { + .name = "smdk-audio-wm8994", + .of_match_table = of_match_ptr(samsung_wm8994_of_match), + .pm = &snd_soc_pm_ops, + }, + .probe = smdk_audio_probe, +}; + +module_platform_driver(smdk_audio_driver); + +MODULE_DESCRIPTION("ALSA SoC SMDK WM8994"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:smdk-audio-wm8994"); diff --git a/sound/soc/samsung/smdk_wm8994pcm.c b/sound/soc/samsung/smdk_wm8994pcm.c new file mode 100644 index 0000000000..d77dc54cae --- /dev/null +++ b/sound/soc/samsung/smdk_wm8994pcm.c @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Copyright (c) 2011 Samsung Electronics Co., Ltd +// http://www.samsung.com + +#include <linux/module.h> +#include <sound/soc.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> + +#include "../codecs/wm8994.h" +#include "pcm.h" + +/* + * Board Settings: + * o '1' means 'ON' + * o '0' means 'OFF' + * o 'X' means 'Don't care' + * + * SMDKC210, SMDKV310: CFG3- 1001, CFG5-1000, CFG7-111111 + */ + +/* + * Configure audio route as :- + * $ amixer sset 'DAC1' on,on + * $ amixer sset 'Right Headphone Mux' 'DAC' + * $ amixer sset 'Left Headphone Mux' 'DAC' + * $ amixer sset 'DAC1R Mixer AIF1.1' on + * $ amixer sset 'DAC1L Mixer AIF1.1' on + * $ amixer sset 'IN2L' on + * $ amixer sset 'IN2L PGA IN2LN' on + * $ amixer sset 'MIXINL IN2L' on + * $ amixer sset 'AIF1ADC1L Mixer ADC/DMIC' on + * $ amixer sset 'IN2R' on + * $ amixer sset 'IN2R PGA IN2RN' on + * $ amixer sset 'MIXINR IN2R' on + * $ amixer sset 'AIF1ADC1R Mixer ADC/DMIC' on + */ + +/* SMDK has a 16.9344MHZ crystal attached to WM8994 */ +#define SMDK_WM8994_FREQ 16934400 + +static int smdk_wm8994_pcm_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); + unsigned long mclk_freq; + int rfs, ret; + + switch(params_rate(params)) { + case 8000: + rfs = 512; + break; + default: + dev_err(cpu_dai->dev, "%s:%d Sampling Rate %u not supported!\n", + __func__, __LINE__, params_rate(params)); + return -EINVAL; + } + + mclk_freq = params_rate(params) * rfs; + + ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1, + mclk_freq, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, WM8994_FLL_SRC_MCLK1, + SMDK_WM8994_FREQ, mclk_freq); + if (ret < 0) + return ret; + + /* Set PCM source clock on CPU */ + ret = snd_soc_dai_set_sysclk(cpu_dai, S3C_PCM_CLKSRC_MUX, + mclk_freq, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + /* Set SCLK_DIV for making bclk */ + ret = snd_soc_dai_set_clkdiv(cpu_dai, S3C_PCM_SCLK_PER_FS, rfs); + if (ret < 0) + return ret; + + return 0; +} + +static const struct snd_soc_ops smdk_wm8994_pcm_ops = { + .hw_params = smdk_wm8994_pcm_hw_params, +}; + +SND_SOC_DAILINK_DEFS(paif_pcm, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-pcm.0")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm8994-codec", "wm8994-aif1")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-pcm.0"))); + +static struct snd_soc_dai_link smdk_dai[] = { + { + .name = "WM8994 PAIF PCM", + .stream_name = "Primary PCM", + .dai_fmt = SND_SOC_DAIFMT_DSP_B | SND_SOC_DAIFMT_IB_NF | + SND_SOC_DAIFMT_CBS_CFS, + .ops = &smdk_wm8994_pcm_ops, + SND_SOC_DAILINK_REG(paif_pcm), + }, +}; + +static struct snd_soc_card smdk_pcm = { + .name = "SMDK-PCM", + .owner = THIS_MODULE, + .dai_link = smdk_dai, + .num_links = 1, +}; + +static int snd_smdk_probe(struct platform_device *pdev) +{ + int ret = 0; + + smdk_pcm.dev = &pdev->dev; + ret = devm_snd_soc_register_card(&pdev->dev, &smdk_pcm); + if (ret) + dev_err_probe(&pdev->dev, ret, "snd_soc_register_card failed\n"); + + return ret; +} + +static struct platform_driver snd_smdk_driver = { + .driver = { + .name = "samsung-smdk-pcm", + }, + .probe = snd_smdk_probe, +}; + +module_platform_driver(snd_smdk_driver); + +MODULE_AUTHOR("Sangbeom Kim, <sbkim73@samsung.com>"); +MODULE_DESCRIPTION("ALSA SoC SMDK WM8994 for PCM"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/samsung/snow.c b/sound/soc/samsung/snow.c new file mode 100644 index 0000000000..334080e631 --- /dev/null +++ b/sound/soc/samsung/snow.c @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// ASoC machine driver for Snow boards + +#include <linux/clk.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> + +#include "i2s.h" + +#define FIN_PLL_RATE 24000000 + +SND_SOC_DAILINK_DEFS(links, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +struct snow_priv { + struct snd_soc_dai_link dai_link; + struct clk *clk_i2s_bus; +}; + +static int snow_card_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + static const unsigned int pll_rate[] = { + 73728000U, 67737602U, 49152000U, 45158401U, 32768001U + }; + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct snow_priv *priv = snd_soc_card_get_drvdata(rtd->card); + int bfs, psr, rfs, bitwidth; + unsigned long int rclk; + long int freq = -EINVAL; + int ret, i; + + bitwidth = snd_pcm_format_width(params_format(params)); + if (bitwidth < 0) { + dev_err(rtd->card->dev, "Invalid bit-width: %d\n", bitwidth); + return bitwidth; + } + + if (bitwidth != 16 && bitwidth != 24) { + dev_err(rtd->card->dev, "Unsupported bit-width: %d\n", bitwidth); + return -EINVAL; + } + + bfs = 2 * bitwidth; + + switch (params_rate(params)) { + case 16000: + case 22050: + case 24000: + case 32000: + case 44100: + case 48000: + case 88200: + case 96000: + rfs = 8 * bfs; + break; + case 64000: + rfs = 384; + break; + case 8000: + case 11025: + case 12000: + rfs = 16 * bfs; + break; + default: + return -EINVAL; + } + + rclk = params_rate(params) * rfs; + + for (psr = 8; psr > 0; psr /= 2) { + for (i = 0; i < ARRAY_SIZE(pll_rate); i++) { + if ((pll_rate[i] - rclk * psr) <= 2) { + freq = pll_rate[i]; + break; + } + } + } + if (freq < 0) { + dev_err(rtd->card->dev, "Unsupported RCLK rate: %lu\n", rclk); + return -EINVAL; + } + + ret = clk_set_rate(priv->clk_i2s_bus, freq); + if (ret < 0) { + dev_err(rtd->card->dev, "I2S bus clock rate set failed\n"); + return ret; + } + + return 0; +} + +static const struct snd_soc_ops snow_card_ops = { + .hw_params = snow_card_hw_params, +}; + +static int snow_late_probe(struct snd_soc_card *card) +{ + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_dai *codec_dai; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[0]); + + /* In the multi-codec case codec_dais 0 is MAX98095 and 1 is HDMI. */ + codec_dai = asoc_rtd_to_codec(rtd, 0); + + /* Set the MCLK rate for the codec */ + return snd_soc_dai_set_sysclk(codec_dai, 0, + FIN_PLL_RATE, SND_SOC_CLOCK_IN); +} + +static struct snd_soc_card snow_snd = { + .name = "Snow-I2S", + .owner = THIS_MODULE, + .late_probe = snow_late_probe, +}; + +static int snow_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct snd_soc_card *card = &snow_snd; + struct device_node *cpu, *codec; + struct snd_soc_dai_link *link; + struct snow_priv *priv; + int ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + link = &priv->dai_link; + + link->dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBS_CFS; + + link->name = "Primary"; + link->stream_name = link->name; + + link->cpus = links_cpus; + link->num_cpus = ARRAY_SIZE(links_cpus); + link->codecs = links_codecs; + link->num_codecs = ARRAY_SIZE(links_codecs); + link->platforms = links_platforms; + link->num_platforms = ARRAY_SIZE(links_platforms); + + card->dai_link = link; + card->num_links = 1; + card->dev = dev; + + /* Try new DT bindings with HDMI support first. */ + cpu = of_get_child_by_name(dev->of_node, "cpu"); + + if (cpu) { + link->ops = &snow_card_ops; + + link->cpus->of_node = of_parse_phandle(cpu, "sound-dai", 0); + of_node_put(cpu); + + if (!link->cpus->of_node) { + dev_err(dev, "Failed parsing cpu/sound-dai property\n"); + return -EINVAL; + } + + codec = of_get_child_by_name(dev->of_node, "codec"); + ret = snd_soc_of_get_dai_link_codecs(dev, codec, link); + of_node_put(codec); + + if (ret < 0) { + of_node_put(link->cpus->of_node); + dev_err(dev, "Failed parsing codec node\n"); + return ret; + } + + priv->clk_i2s_bus = of_clk_get_by_name(link->cpus->of_node, + "i2s_opclk0"); + if (IS_ERR(priv->clk_i2s_bus)) { + snd_soc_of_put_dai_link_codecs(link); + of_node_put(link->cpus->of_node); + return PTR_ERR(priv->clk_i2s_bus); + } + } else { + link->codecs->dai_name = "HiFi"; + + link->cpus->of_node = of_parse_phandle(dev->of_node, + "samsung,i2s-controller", 0); + if (!link->cpus->of_node) { + dev_err(dev, "i2s-controller property parse error\n"); + return -EINVAL; + } + + link->codecs->of_node = of_parse_phandle(dev->of_node, + "samsung,audio-codec", 0); + if (!link->codecs->of_node) { + of_node_put(link->cpus->of_node); + dev_err(dev, "audio-codec property parse error\n"); + return -EINVAL; + } + } + + link->platforms->of_node = link->cpus->of_node; + + /* Update card-name if provided through DT, else use default name */ + snd_soc_of_parse_card_name(card, "samsung,model"); + + snd_soc_card_set_drvdata(card, priv); + + ret = devm_snd_soc_register_card(dev, card); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "snd_soc_register_card failed\n"); + + return 0; +} + +static void snow_remove(struct platform_device *pdev) +{ + struct snow_priv *priv = platform_get_drvdata(pdev); + struct snd_soc_dai_link *link = &priv->dai_link; + + of_node_put(link->cpus->of_node); + of_node_put(link->codecs->of_node); + snd_soc_of_put_dai_link_codecs(link); + + clk_put(priv->clk_i2s_bus); +} + +static const struct of_device_id snow_of_match[] = { + { .compatible = "google,snow-audio-max98090", }, + { .compatible = "google,snow-audio-max98091", }, + { .compatible = "google,snow-audio-max98095", }, + {}, +}; +MODULE_DEVICE_TABLE(of, snow_of_match); + +static struct platform_driver snow_driver = { + .driver = { + .name = "snow-audio", + .pm = &snd_soc_pm_ops, + .of_match_table = snow_of_match, + }, + .probe = snow_probe, + .remove_new = snow_remove, +}; + +module_platform_driver(snow_driver); + +MODULE_DESCRIPTION("ALSA SoC Audio machine driver for Snow"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/samsung/spdif.c b/sound/soc/samsung/spdif.c new file mode 100644 index 0000000000..28dc1bbfc8 --- /dev/null +++ b/sound/soc/samsung/spdif.c @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// ALSA SoC Audio Layer - Samsung S/PDIF Controller driver +// +// Copyright (c) 2010 Samsung Electronics Co. Ltd +// http://www.samsung.com/ + +#include <linux/clk.h> +#include <linux/io.h> +#include <linux/module.h> + +#include <sound/soc.h> +#include <sound/pcm_params.h> + +#include <linux/platform_data/asoc-s3c.h> + +#include "dma.h" +#include "spdif.h" + +/* Registers */ +#define CLKCON 0x00 +#define CON 0x04 +#define BSTAS 0x08 +#define CSTAS 0x0C +#define DATA_OUTBUF 0x10 +#define DCNT 0x14 +#define BSTAS_S 0x18 +#define DCNT_S 0x1C + +#define CLKCTL_MASK 0x7 +#define CLKCTL_MCLK_EXT (0x1 << 2) +#define CLKCTL_PWR_ON (0x1 << 0) + +#define CON_MASK 0x3ffffff +#define CON_FIFO_TH_SHIFT 19 +#define CON_FIFO_TH_MASK (0x7 << 19) +#define CON_USERDATA_23RDBIT (0x1 << 12) + +#define CON_SW_RESET (0x1 << 5) + +#define CON_MCLKDIV_MASK (0x3 << 3) +#define CON_MCLKDIV_256FS (0x0 << 3) +#define CON_MCLKDIV_384FS (0x1 << 3) +#define CON_MCLKDIV_512FS (0x2 << 3) + +#define CON_PCM_MASK (0x3 << 1) +#define CON_PCM_16BIT (0x0 << 1) +#define CON_PCM_20BIT (0x1 << 1) +#define CON_PCM_24BIT (0x2 << 1) + +#define CON_PCM_DATA (0x1 << 0) + +#define CSTAS_MASK 0x3fffffff +#define CSTAS_SAMP_FREQ_MASK (0xF << 24) +#define CSTAS_SAMP_FREQ_44 (0x0 << 24) +#define CSTAS_SAMP_FREQ_48 (0x2 << 24) +#define CSTAS_SAMP_FREQ_32 (0x3 << 24) +#define CSTAS_SAMP_FREQ_96 (0xA << 24) + +#define CSTAS_CATEGORY_MASK (0xFF << 8) +#define CSTAS_CATEGORY_CODE_CDP (0x01 << 8) + +#define CSTAS_NO_COPYRIGHT (0x1 << 2) + +/** + * struct samsung_spdif_info - Samsung S/PDIF Controller information + * @lock: Spin lock for S/PDIF. + * @dev: The parent device passed to use from the probe. + * @regs: The pointer to the device register block. + * @clk_rate: Current clock rate for calcurate ratio. + * @pclk: The peri-clock pointer for spdif master operation. + * @sclk: The source clock pointer for making sync signals. + * @saved_clkcon: Backup clkcon reg. in suspend. + * @saved_con: Backup con reg. in suspend. + * @saved_cstas: Backup cstas reg. in suspend. + * @dma_playback: DMA information for playback channel. + */ +struct samsung_spdif_info { + spinlock_t lock; + struct device *dev; + void __iomem *regs; + unsigned long clk_rate; + struct clk *pclk; + struct clk *sclk; + u32 saved_clkcon; + u32 saved_con; + u32 saved_cstas; + struct snd_dmaengine_dai_dma_data *dma_playback; +}; + +static struct snd_dmaengine_dai_dma_data spdif_stereo_out; +static struct samsung_spdif_info spdif_info; + +static inline struct samsung_spdif_info +*component_to_info(struct snd_soc_component *component) +{ + return snd_soc_component_get_drvdata(component); +} + +static inline struct samsung_spdif_info *to_info(struct snd_soc_dai *cpu_dai) +{ + return snd_soc_dai_get_drvdata(cpu_dai); +} + +static void spdif_snd_txctrl(struct samsung_spdif_info *spdif, int on) +{ + void __iomem *regs = spdif->regs; + u32 clkcon; + + dev_dbg(spdif->dev, "Entered %s\n", __func__); + + clkcon = readl(regs + CLKCON) & CLKCTL_MASK; + if (on) + writel(clkcon | CLKCTL_PWR_ON, regs + CLKCON); + else + writel(clkcon & ~CLKCTL_PWR_ON, regs + CLKCON); +} + +static int spdif_set_sysclk(struct snd_soc_dai *cpu_dai, + int clk_id, unsigned int freq, int dir) +{ + struct samsung_spdif_info *spdif = to_info(cpu_dai); + u32 clkcon; + + dev_dbg(spdif->dev, "Entered %s\n", __func__); + + clkcon = readl(spdif->regs + CLKCON); + + if (clk_id == SND_SOC_SPDIF_INT_MCLK) + clkcon &= ~CLKCTL_MCLK_EXT; + else + clkcon |= CLKCTL_MCLK_EXT; + + writel(clkcon, spdif->regs + CLKCON); + + spdif->clk_rate = freq; + + return 0; +} + +static int spdif_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct samsung_spdif_info *spdif = to_info(asoc_rtd_to_cpu(rtd, 0)); + unsigned long flags; + + dev_dbg(spdif->dev, "Entered %s\n", __func__); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + spin_lock_irqsave(&spdif->lock, flags); + spdif_snd_txctrl(spdif, 1); + spin_unlock_irqrestore(&spdif->lock, flags); + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + spin_lock_irqsave(&spdif->lock, flags); + spdif_snd_txctrl(spdif, 0); + spin_unlock_irqrestore(&spdif->lock, flags); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int spdif_sysclk_ratios[] = { + 512, 384, 256, +}; + +static int spdif_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *socdai) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct samsung_spdif_info *spdif = to_info(asoc_rtd_to_cpu(rtd, 0)); + void __iomem *regs = spdif->regs; + struct snd_dmaengine_dai_dma_data *dma_data; + u32 con, clkcon, cstas; + unsigned long flags; + int i, ratio; + + dev_dbg(spdif->dev, "Entered %s\n", __func__); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + dma_data = spdif->dma_playback; + else { + dev_err(spdif->dev, "Capture is not supported\n"); + return -EINVAL; + } + + snd_soc_dai_set_dma_data(asoc_rtd_to_cpu(rtd, 0), substream, dma_data); + + spin_lock_irqsave(&spdif->lock, flags); + + con = readl(regs + CON) & CON_MASK; + cstas = readl(regs + CSTAS) & CSTAS_MASK; + clkcon = readl(regs + CLKCON) & CLKCTL_MASK; + + con &= ~CON_FIFO_TH_MASK; + con |= (0x7 << CON_FIFO_TH_SHIFT); + con |= CON_USERDATA_23RDBIT; + con |= CON_PCM_DATA; + + con &= ~CON_PCM_MASK; + switch (params_width(params)) { + case 16: + con |= CON_PCM_16BIT; + break; + default: + dev_err(spdif->dev, "Unsupported data size.\n"); + goto err; + } + + ratio = spdif->clk_rate / params_rate(params); + for (i = 0; i < ARRAY_SIZE(spdif_sysclk_ratios); i++) + if (ratio == spdif_sysclk_ratios[i]) + break; + if (i == ARRAY_SIZE(spdif_sysclk_ratios)) { + dev_err(spdif->dev, "Invalid clock ratio %ld/%d\n", + spdif->clk_rate, params_rate(params)); + goto err; + } + + con &= ~CON_MCLKDIV_MASK; + switch (ratio) { + case 256: + con |= CON_MCLKDIV_256FS; + break; + case 384: + con |= CON_MCLKDIV_384FS; + break; + case 512: + con |= CON_MCLKDIV_512FS; + break; + } + + cstas &= ~CSTAS_SAMP_FREQ_MASK; + switch (params_rate(params)) { + case 44100: + cstas |= CSTAS_SAMP_FREQ_44; + break; + case 48000: + cstas |= CSTAS_SAMP_FREQ_48; + break; + case 32000: + cstas |= CSTAS_SAMP_FREQ_32; + break; + case 96000: + cstas |= CSTAS_SAMP_FREQ_96; + break; + default: + dev_err(spdif->dev, "Invalid sampling rate %d\n", + params_rate(params)); + goto err; + } + + cstas &= ~CSTAS_CATEGORY_MASK; + cstas |= CSTAS_CATEGORY_CODE_CDP; + cstas |= CSTAS_NO_COPYRIGHT; + + writel(con, regs + CON); + writel(cstas, regs + CSTAS); + writel(clkcon, regs + CLKCON); + + spin_unlock_irqrestore(&spdif->lock, flags); + + return 0; +err: + spin_unlock_irqrestore(&spdif->lock, flags); + return -EINVAL; +} + +static void spdif_shutdown(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct samsung_spdif_info *spdif = to_info(asoc_rtd_to_cpu(rtd, 0)); + void __iomem *regs = spdif->regs; + u32 con, clkcon; + + dev_dbg(spdif->dev, "Entered %s\n", __func__); + + con = readl(regs + CON) & CON_MASK; + clkcon = readl(regs + CLKCON) & CLKCTL_MASK; + + writel(con | CON_SW_RESET, regs + CON); + cpu_relax(); + + writel(clkcon & ~CLKCTL_PWR_ON, regs + CLKCON); +} + +#ifdef CONFIG_PM +static int spdif_suspend(struct snd_soc_component *component) +{ + struct samsung_spdif_info *spdif = component_to_info(component); + u32 con = spdif->saved_con; + + dev_dbg(spdif->dev, "Entered %s\n", __func__); + + spdif->saved_clkcon = readl(spdif->regs + CLKCON) & CLKCTL_MASK; + spdif->saved_con = readl(spdif->regs + CON) & CON_MASK; + spdif->saved_cstas = readl(spdif->regs + CSTAS) & CSTAS_MASK; + + writel(con | CON_SW_RESET, spdif->regs + CON); + cpu_relax(); + + return 0; +} + +static int spdif_resume(struct snd_soc_component *component) +{ + struct samsung_spdif_info *spdif = component_to_info(component); + + dev_dbg(spdif->dev, "Entered %s\n", __func__); + + writel(spdif->saved_clkcon, spdif->regs + CLKCON); + writel(spdif->saved_con, spdif->regs + CON); + writel(spdif->saved_cstas, spdif->regs + CSTAS); + + return 0; +} +#else +#define spdif_suspend NULL +#define spdif_resume NULL +#endif + +static const struct snd_soc_dai_ops spdif_dai_ops = { + .set_sysclk = spdif_set_sysclk, + .trigger = spdif_trigger, + .hw_params = spdif_hw_params, + .shutdown = spdif_shutdown, +}; + +static struct snd_soc_dai_driver samsung_spdif_dai = { + .name = "samsung-spdif", + .playback = { + .stream_name = "S/PDIF Playback", + .channels_min = 2, + .channels_max = 2, + .rates = (SNDRV_PCM_RATE_32000 | + SNDRV_PCM_RATE_44100 | + SNDRV_PCM_RATE_48000 | + SNDRV_PCM_RATE_96000), + .formats = SNDRV_PCM_FMTBIT_S16_LE, }, + .ops = &spdif_dai_ops, +}; + +static const struct snd_soc_component_driver samsung_spdif_component = { + .name = "samsung-spdif", + .suspend = spdif_suspend, + .resume = spdif_resume, + .legacy_dai_naming = 1, +}; + +static int spdif_probe(struct platform_device *pdev) +{ + struct s3c_audio_pdata *spdif_pdata; + struct resource *mem_res; + struct samsung_spdif_info *spdif; + dma_filter_fn filter; + int ret; + + spdif_pdata = pdev->dev.platform_data; + + dev_dbg(&pdev->dev, "Entered %s\n", __func__); + + mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!mem_res) { + dev_err(&pdev->dev, "Unable to get register resource.\n"); + return -ENXIO; + } + + if (spdif_pdata && spdif_pdata->cfg_gpio + && spdif_pdata->cfg_gpio(pdev)) { + dev_err(&pdev->dev, "Unable to configure GPIO pins\n"); + return -EINVAL; + } + + spdif = &spdif_info; + spdif->dev = &pdev->dev; + + spin_lock_init(&spdif->lock); + + spdif->pclk = devm_clk_get(&pdev->dev, "spdif"); + if (IS_ERR(spdif->pclk)) { + dev_err(&pdev->dev, "failed to get peri-clock\n"); + ret = -ENOENT; + goto err0; + } + ret = clk_prepare_enable(spdif->pclk); + if (ret) + goto err0; + + spdif->sclk = devm_clk_get(&pdev->dev, "sclk_spdif"); + if (IS_ERR(spdif->sclk)) { + dev_err(&pdev->dev, "failed to get internal source clock\n"); + ret = -ENOENT; + goto err1; + } + ret = clk_prepare_enable(spdif->sclk); + if (ret) + goto err1; + + /* Request S/PDIF Register's memory region */ + if (!request_mem_region(mem_res->start, + resource_size(mem_res), "samsung-spdif")) { + dev_err(&pdev->dev, "Unable to request register region\n"); + ret = -EBUSY; + goto err2; + } + + spdif->regs = ioremap(mem_res->start, 0x100); + if (spdif->regs == NULL) { + dev_err(&pdev->dev, "Cannot ioremap registers\n"); + ret = -ENXIO; + goto err3; + } + + spdif_stereo_out.addr_width = 2; + spdif_stereo_out.addr = mem_res->start + DATA_OUTBUF; + filter = NULL; + if (spdif_pdata) { + spdif_stereo_out.filter_data = spdif_pdata->dma_playback; + filter = spdif_pdata->dma_filter; + } + spdif->dma_playback = &spdif_stereo_out; + + ret = samsung_asoc_dma_platform_register(&pdev->dev, filter, + NULL, NULL, NULL); + if (ret) { + dev_err(&pdev->dev, "failed to register DMA: %d\n", ret); + goto err4; + } + + dev_set_drvdata(&pdev->dev, spdif); + + ret = devm_snd_soc_register_component(&pdev->dev, + &samsung_spdif_component, &samsung_spdif_dai, 1); + if (ret != 0) { + dev_err(&pdev->dev, "fail to register dai\n"); + goto err4; + } + + return 0; +err4: + iounmap(spdif->regs); +err3: + release_mem_region(mem_res->start, resource_size(mem_res)); +err2: + clk_disable_unprepare(spdif->sclk); +err1: + clk_disable_unprepare(spdif->pclk); +err0: + return ret; +} + +static void spdif_remove(struct platform_device *pdev) +{ + struct samsung_spdif_info *spdif = &spdif_info; + struct resource *mem_res; + + iounmap(spdif->regs); + + mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + release_mem_region(mem_res->start, resource_size(mem_res)); + + clk_disable_unprepare(spdif->sclk); + clk_disable_unprepare(spdif->pclk); +} + +static struct platform_driver samsung_spdif_driver = { + .probe = spdif_probe, + .remove_new = spdif_remove, + .driver = { + .name = "samsung-spdif", + }, +}; + +module_platform_driver(samsung_spdif_driver); + +MODULE_AUTHOR("Seungwhan Youn, <sw.youn@samsung.com>"); +MODULE_DESCRIPTION("Samsung S/PDIF Controller Driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:samsung-spdif"); diff --git a/sound/soc/samsung/spdif.h b/sound/soc/samsung/spdif.h new file mode 100644 index 0000000000..461da60ab0 --- /dev/null +++ b/sound/soc/samsung/spdif.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * ALSA SoC Audio Layer - Samsung S/PDIF Controller driver + * + * Copyright (c) 2010 Samsung Electronics Co. Ltd + * http://www.samsung.com/ + */ + +#ifndef __SND_SOC_SAMSUNG_SPDIF_H +#define __SND_SOC_SAMSUNG_SPDIF_H __FILE__ + +#define SND_SOC_SPDIF_INT_MCLK 0 +#define SND_SOC_SPDIF_EXT_MCLK 1 + +#endif /* __SND_SOC_SAMSUNG_SPDIF_H */ diff --git a/sound/soc/samsung/speyside.c b/sound/soc/samsung/speyside.c new file mode 100644 index 0000000000..22e2ad63d6 --- /dev/null +++ b/sound/soc/samsung/speyside.c @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Speyside audio support +// +// Copyright 2011 Wolfson Microelectronics + +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/jack.h> +#include <linux/gpio.h> +#include <linux/module.h> + +#include "../codecs/wm8996.h" +#include "../codecs/wm9081.h" + +#define WM8996_HPSEL_GPIO 214 +#define MCLK_AUDIO_RATE (512 * 48000) + +static int speyside_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; + struct snd_soc_dai *codec_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[1]); + codec_dai = asoc_rtd_to_codec(rtd, 0); + + if (dapm->dev != codec_dai->dev) + return 0; + + switch (level) { + case SND_SOC_BIAS_STANDBY: + ret = snd_soc_dai_set_sysclk(codec_dai, WM8996_SYSCLK_MCLK2, + 32768, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + ret = snd_soc_dai_set_pll(codec_dai, WM8996_FLL_MCLK2, + 0, 0, 0); + if (ret < 0) { + pr_err("Failed to stop FLL\n"); + return ret; + } + break; + + default: + break; + } + + return 0; +} + +static int speyside_set_bias_level_post(struct snd_soc_card *card, + struct snd_soc_dapm_context *dapm, + enum snd_soc_bias_level level) +{ + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_dai *codec_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[1]); + codec_dai = asoc_rtd_to_codec(rtd, 0); + + if (dapm->dev != codec_dai->dev) + return 0; + + switch (level) { + case SND_SOC_BIAS_PREPARE: + if (card->dapm.bias_level == SND_SOC_BIAS_STANDBY) { + ret = snd_soc_dai_set_pll(codec_dai, 0, + WM8996_FLL_MCLK2, + 32768, MCLK_AUDIO_RATE); + if (ret < 0) { + pr_err("Failed to start FLL\n"); + return ret; + } + + ret = snd_soc_dai_set_sysclk(codec_dai, + WM8996_SYSCLK_FLL, + MCLK_AUDIO_RATE, + SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + } + break; + + default: + break; + } + + card->dapm.bias_level = level; + + return 0; +} + +static struct snd_soc_jack speyside_headset; + +/* Headset jack detection DAPM pins */ +static struct snd_soc_jack_pin speyside_headset_pins[] = { + { + .pin = "Headset Mic", + .mask = SND_JACK_MICROPHONE, + }, +}; + +/* Default the headphone selection to active high */ +static int speyside_jack_polarity; + +static int speyside_get_micbias(struct snd_soc_dapm_widget *source, + struct snd_soc_dapm_widget *sink) +{ + if (speyside_jack_polarity && (strcmp(source->name, "MICB1") == 0)) + return 1; + if (!speyside_jack_polarity && (strcmp(source->name, "MICB2") == 0)) + return 1; + + return 0; +} + +static void speyside_set_polarity(struct snd_soc_component *component, + int polarity) +{ + speyside_jack_polarity = !polarity; + gpio_direction_output(WM8996_HPSEL_GPIO, speyside_jack_polarity); + + /* Re-run DAPM to make sure we're using the correct mic bias */ + snd_soc_dapm_sync(snd_soc_component_get_dapm(component)); +} + +static int speyside_wm0010_init(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_dai *dai = asoc_rtd_to_codec(rtd, 0); + int ret; + + ret = snd_soc_dai_set_sysclk(dai, 0, MCLK_AUDIO_RATE, 0); + if (ret < 0) + return ret; + + return 0; +} + +static int speyside_wm8996_init(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_dai *dai = asoc_rtd_to_codec(rtd, 0); + struct snd_soc_component *component = dai->component; + int ret; + + ret = snd_soc_dai_set_sysclk(dai, WM8996_SYSCLK_MCLK2, 32768, 0); + if (ret < 0) + return ret; + + ret = gpio_request(WM8996_HPSEL_GPIO, "HP_SEL"); + if (ret != 0) + pr_err("Failed to request HP_SEL GPIO: %d\n", ret); + gpio_direction_output(WM8996_HPSEL_GPIO, speyside_jack_polarity); + + ret = snd_soc_card_jack_new_pins(rtd->card, "Headset", + SND_JACK_LINEOUT | SND_JACK_HEADSET | + SND_JACK_BTN_0, + &speyside_headset, + speyside_headset_pins, + ARRAY_SIZE(speyside_headset_pins)); + if (ret) + return ret; + + wm8996_detect(component, &speyside_headset, speyside_set_polarity); + + return 0; +} + +static int speyside_late_probe(struct snd_soc_card *card) +{ + snd_soc_dapm_ignore_suspend(&card->dapm, "Headphone"); + snd_soc_dapm_ignore_suspend(&card->dapm, "Headset Mic"); + snd_soc_dapm_ignore_suspend(&card->dapm, "Main AMIC"); + snd_soc_dapm_ignore_suspend(&card->dapm, "Main DMIC"); + snd_soc_dapm_ignore_suspend(&card->dapm, "Main Speaker"); + snd_soc_dapm_ignore_suspend(&card->dapm, "WM1250 Output"); + snd_soc_dapm_ignore_suspend(&card->dapm, "WM1250 Input"); + + return 0; +} + +static const struct snd_soc_pcm_stream dsp_codec_params = { + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .rate_min = 48000, + .rate_max = 48000, + .channels_min = 2, + .channels_max = 2, +}; + +SND_SOC_DAILINK_DEFS(cpu_dsp, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s.0")), + DAILINK_COMP_ARRAY(COMP_CODEC("spi0.0", "wm0010-sdi1")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s.0"))); + +SND_SOC_DAILINK_DEFS(dsp_codec, + DAILINK_COMP_ARRAY(COMP_CPU("wm0010-sdi2")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm8996.1-001a", "wm8996-aif1"))); + +SND_SOC_DAILINK_DEFS(baseband, + DAILINK_COMP_ARRAY(COMP_CPU("wm8996-aif2")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm1250-ev1.1-0027", "wm1250-ev1"))); + +static struct snd_soc_dai_link speyside_dai[] = { + { + .name = "CPU-DSP", + .stream_name = "CPU-DSP", + .init = speyside_wm0010_init, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + SND_SOC_DAILINK_REG(cpu_dsp), + }, + { + .name = "DSP-CODEC", + .stream_name = "DSP-CODEC", + .init = speyside_wm8996_init, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .c2c_params = &dsp_codec_params, + .num_c2c_params = 1, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(dsp_codec), + }, + { + .name = "Baseband", + .stream_name = "Baseband", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(baseband), + }, +}; + +static int speyside_wm9081_init(struct snd_soc_component *component) +{ + /* At any time the WM9081 is active it will have this clock */ + return snd_soc_component_set_sysclk(component, WM9081_SYSCLK_MCLK, 0, + MCLK_AUDIO_RATE, 0); +} + +static struct snd_soc_aux_dev speyside_aux_dev[] = { + { + .dlc = COMP_AUX("wm9081.1-006c"), + .init = speyside_wm9081_init, + }, +}; + +static struct snd_soc_codec_conf speyside_codec_conf[] = { + { + .dlc = COMP_CODEC_CONF("wm9081.1-006c"), + .name_prefix = "Sub", + }, +}; + +static const struct snd_kcontrol_new controls[] = { + SOC_DAPM_PIN_SWITCH("Main Speaker"), + SOC_DAPM_PIN_SWITCH("Main DMIC"), + SOC_DAPM_PIN_SWITCH("Main AMIC"), + SOC_DAPM_PIN_SWITCH("WM1250 Input"), + SOC_DAPM_PIN_SWITCH("WM1250 Output"), + SOC_DAPM_PIN_SWITCH("Headphone"), +}; + +static const struct snd_soc_dapm_widget widgets[] = { + SND_SOC_DAPM_HP("Headphone", NULL), + SND_SOC_DAPM_MIC("Headset Mic", NULL), + + SND_SOC_DAPM_SPK("Main Speaker", NULL), + + SND_SOC_DAPM_MIC("Main AMIC", NULL), + SND_SOC_DAPM_MIC("Main DMIC", NULL), +}; + +static const struct snd_soc_dapm_route audio_paths[] = { + { "IN1RN", NULL, "MICB1" }, + { "IN1RP", NULL, "MICB1" }, + { "IN1RN", NULL, "MICB2" }, + { "IN1RP", NULL, "MICB2" }, + { "MICB1", NULL, "Headset Mic", speyside_get_micbias }, + { "MICB2", NULL, "Headset Mic", speyside_get_micbias }, + + { "IN1LP", NULL, "MICB2" }, + { "IN1RN", NULL, "MICB1" }, + { "MICB2", NULL, "Main AMIC" }, + + { "DMIC1DAT", NULL, "MICB1" }, + { "DMIC2DAT", NULL, "MICB1" }, + { "MICB1", NULL, "Main DMIC" }, + + { "Headphone", NULL, "HPOUT1L" }, + { "Headphone", NULL, "HPOUT1R" }, + + { "Sub IN1", NULL, "HPOUT2L" }, + { "Sub IN2", NULL, "HPOUT2R" }, + + { "Main Speaker", NULL, "Sub SPKN" }, + { "Main Speaker", NULL, "Sub SPKP" }, + { "Main Speaker", NULL, "SPKDAT" }, +}; + +static struct snd_soc_card speyside = { + .name = "Speyside", + .owner = THIS_MODULE, + .dai_link = speyside_dai, + .num_links = ARRAY_SIZE(speyside_dai), + .aux_dev = speyside_aux_dev, + .num_aux_devs = ARRAY_SIZE(speyside_aux_dev), + .codec_conf = speyside_codec_conf, + .num_configs = ARRAY_SIZE(speyside_codec_conf), + + .set_bias_level = speyside_set_bias_level, + .set_bias_level_post = speyside_set_bias_level_post, + + .controls = controls, + .num_controls = ARRAY_SIZE(controls), + .dapm_widgets = widgets, + .num_dapm_widgets = ARRAY_SIZE(widgets), + .dapm_routes = audio_paths, + .num_dapm_routes = ARRAY_SIZE(audio_paths), + .fully_routed = true, + + .late_probe = speyside_late_probe, +}; + +static int speyside_probe(struct platform_device *pdev) +{ + struct snd_soc_card *card = &speyside; + int ret; + + card->dev = &pdev->dev; + + ret = devm_snd_soc_register_card(&pdev->dev, card); + if (ret) + dev_err_probe(&pdev->dev, ret, "snd_soc_register_card() failed\n"); + + return ret; +} + +static struct platform_driver speyside_driver = { + .driver = { + .name = "speyside", + .pm = &snd_soc_pm_ops, + }, + .probe = speyside_probe, +}; + +module_platform_driver(speyside_driver); + +MODULE_DESCRIPTION("Speyside audio support"); +MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:speyside"); diff --git a/sound/soc/samsung/tm2_wm5110.c b/sound/soc/samsung/tm2_wm5110.c new file mode 100644 index 0000000000..5ebf17f3de --- /dev/null +++ b/sound/soc/samsung/tm2_wm5110.c @@ -0,0 +1,677 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Copyright (C) 2015 - 2016 Samsung Electronics Co., Ltd. +// +// Authors: Inha Song <ideal.song@samsung.com> +// Sylwester Nawrocki <s.nawrocki@samsung.com> + +#include <linux/clk.h> +#include <linux/gpio.h> +#include <linux/gpio/consumer.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" + +/* + * The source clock is XCLKOUT with its mux set to the external fixed rate + * oscillator (XXTI). + */ +#define MCLK_RATE 24000000U + +#define TM2_DAI_AIF1 0 +#define TM2_DAI_AIF2 1 + +struct tm2_machine_priv { + struct snd_soc_component *component; + 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_component *component = priv->component; + int ret; + + ret = snd_soc_component_set_pll(component, WM5110_FLL1_REFCLK, + ARIZONA_FLL_SRC_MCLK1, + MCLK_RATE, + priv->sysclk_rate); + if (ret < 0) { + dev_err(component->dev, "Failed to set FLL1 source: %d\n", ret); + return ret; + } + + ret = snd_soc_component_set_pll(component, WM5110_FLL1, + ARIZONA_FLL_SRC_MCLK1, + MCLK_RATE, + priv->sysclk_rate); + if (ret < 0) { + dev_err(component->dev, "Failed to start FLL1: %d\n", ret); + return ret; + } + + ret = snd_soc_component_set_sysclk(component, ARIZONA_CLK_SYSCLK, + ARIZONA_CLK_SRC_FLL1, + priv->sysclk_rate, + SND_SOC_CLOCK_IN); + if (ret < 0) { + dev_err(component->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_component *component = priv->component; + int ret; + + ret = snd_soc_component_set_pll(component, WM5110_FLL1, 0, 0, 0); + if (ret < 0) { + dev_err(component->dev, "Failed to stop FLL1: %d\n", ret); + return ret; + } + + ret = snd_soc_component_set_sysclk(component, ARIZONA_CLK_SYSCLK, + ARIZONA_CLK_SRC_FLL1, 0, 0); + if (ret < 0) { + dev_err(component->dev, "Failed to stop SYSCLK: %d\n", ret); + return ret; + } + + 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 = asoc_substream_to_rtd(substream); + struct snd_soc_component *component = asoc_rtd_to_codec(rtd, 0)->component; + 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(component->dev, "Not supported sample rate: %d\n", + params_rate(params)); + return -EINVAL; + } + + return tm2_start_sysclk(rtd->card); +} + +static const 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 = asoc_substream_to_rtd(substream); + struct snd_soc_component *component = asoc_rtd_to_codec(rtd, 0)->component; + 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(component->dev, "Not supported sample rate: %d\n", + params_rate(params)); + return -EINVAL; + } + + ret = snd_soc_component_set_pll(component, WM5110_FLL2_REFCLK, + ARIZONA_FLL_SRC_MCLK1, + MCLK_RATE, + asyncclk_rate); + if (ret < 0) { + dev_err(component->dev, "Failed to set FLL2 source: %d\n", ret); + return ret; + } + + ret = snd_soc_component_set_pll(component, WM5110_FLL2, + ARIZONA_FLL_SRC_MCLK1, + MCLK_RATE, + asyncclk_rate); + if (ret < 0) { + dev_err(component->dev, "Failed to start FLL2: %d\n", ret); + return ret; + } + + ret = snd_soc_component_set_sysclk(component, ARIZONA_CLK_ASYNCCLK, + ARIZONA_CLK_SRC_FLL2, + asyncclk_rate, + SND_SOC_CLOCK_IN); + if (ret < 0) { + dev_err(component->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 = asoc_substream_to_rtd(substream); + struct snd_soc_component *component = asoc_rtd_to_codec(rtd, 0)->component; + int ret; + + /* disable FLL2 */ + ret = snd_soc_component_set_pll(component, WM5110_FLL2, ARIZONA_FLL_SRC_MCLK1, + 0, 0); + if (ret < 0) + dev_err(component->dev, "Failed to stop FLL2: %d\n", ret); + + return ret; +} + +static const struct snd_soc_ops tm2_aif2_ops = { + .hw_params = tm2_aif2_hw_params, + .hw_free = tm2_aif2_hw_free, +}; + +static int tm2_hdmi_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 *cpu_dai = asoc_rtd_to_cpu(rtd, 0); + unsigned int bfs; + int bitwidth, ret; + + bitwidth = snd_pcm_format_width(params_format(params)); + if (bitwidth < 0) { + dev_err(rtd->card->dev, "Invalid bit-width: %d\n", bitwidth); + return bitwidth; + } + + switch (bitwidth) { + case 48: + bfs = 64; + break; + case 16: + bfs = 32; + break; + default: + dev_err(rtd->card->dev, "Unsupported bit-width: %d\n", bitwidth); + return -EINVAL; + } + + switch (params_rate(params)) { + case 48000: + case 96000: + case 192000: + break; + default: + dev_err(rtd->card->dev, "Unsupported sample rate: %d\n", + params_rate(params)); + return -EINVAL; + } + + ret = snd_soc_dai_set_sysclk(cpu_dai, SAMSUNG_I2S_OPCLK, + 0, SAMSUNG_I2S_OPCLK_PCLK); + if (ret < 0) + return ret; + + ret = snd_soc_dai_set_clkdiv(cpu_dai, SAMSUNG_I2S_DIV_BCLK, bfs); + if (ret < 0) + return ret; + + return 0; +} + +static const struct snd_soc_ops tm2_hdmi_ops = { + .hw_params = tm2_hdmi_hw_params, +}; + +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]); + + if (dapm->dev != asoc_rtd_to_codec(rtd, 0)->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); + 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]); + aif1_dai = asoc_rtd_to_codec(rtd, 0); + priv->component = asoc_rtd_to_codec(rtd, 0)->component; + + 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]); + aif2_dai = asoc_rtd_to_codec(rtd, 0); + + 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; + } + + amp_pdm_dai = snd_soc_find_dai(&tm2_speaker_amp_dev.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"), +}; + +static 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, + }, + }, +}; + +SND_SOC_DAILINK_DEFS(aif1, + DAILINK_COMP_ARRAY(COMP_CPU(SAMSUNG_I2S_DAI)), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm5110-aif1")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +SND_SOC_DAILINK_DEFS(voice, + DAILINK_COMP_ARRAY(COMP_CPU(SAMSUNG_I2S_DAI)), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm5110-aif2")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +SND_SOC_DAILINK_DEFS(bt, + DAILINK_COMP_ARRAY(COMP_CPU(SAMSUNG_I2S_DAI)), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "wm5110-aif3")), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +SND_SOC_DAILINK_DEFS(hdmi, + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_EMPTY()), + DAILINK_COMP_ARRAY(COMP_EMPTY())); + +static struct snd_soc_dai_link tm2_dai_links[] = { + { + .name = "WM5110 AIF1", + .stream_name = "HiFi Primary", + .ops = &tm2_aif1_ops, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + SND_SOC_DAILINK_REG(aif1), + }, { + .name = "WM5110 Voice", + .stream_name = "Voice call", + .ops = &tm2_aif2_ops, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(voice), + }, { + .name = "WM5110 BT", + .stream_name = "Bluetooth", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBM_CFM, + .ignore_suspend = 1, + SND_SOC_DAILINK_REG(bt), + }, { + .name = "HDMI", + .stream_name = "i2s1", + .ops = &tm2_hdmi_ops, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBS_CFS, + SND_SOC_DAILINK_REG(hdmi), + } +}; + +static struct snd_soc_card tm2_card = { + .owner = THIS_MODULE, + + .dai_link = 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_node *cpu_dai_node[2] = {}; + struct device_node *codec_dai_node[2] = {}; + const char *cells_name = NULL; + struct device *dev = &pdev->dev; + struct snd_soc_card *card = &tm2_card; + struct tm2_machine_priv *priv; + struct snd_soc_dai_link *dai_link; + int num_codecs, ret, i; + + 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", GPIOD_OUT_HIGH); + 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, "audio-routing"); + if (ret < 0) { + /* Backwards compatible way */ + 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].dlc.of_node = of_parse_phandle(dev->of_node, + "audio-amplifier", 0); + if (!card->aux_dev[0].dlc.of_node) { + dev_err(dev, "audio-amplifier property invalid or missing\n"); + return -EINVAL; + } + + num_codecs = of_count_phandle_with_args(dev->of_node, "audio-codec", + NULL); + + /* Skip the HDMI link if not specified in DT */ + if (num_codecs > 1) { + card->num_links = ARRAY_SIZE(tm2_dai_links); + cells_name = "#sound-dai-cells"; + } else { + card->num_links = ARRAY_SIZE(tm2_dai_links) - 1; + } + + for (i = 0; i < num_codecs; i++) { + struct of_phandle_args args; + + ret = of_parse_phandle_with_args(dev->of_node, "i2s-controller", + cells_name, i, &args); + if (ret) { + dev_err(dev, "i2s-controller property parse error: %d\n", i); + ret = -EINVAL; + goto dai_node_put; + } + cpu_dai_node[i] = args.np; + + codec_dai_node[i] = of_parse_phandle(dev->of_node, + "audio-codec", i); + if (!codec_dai_node[i]) { + dev_err(dev, "audio-codec property parse error\n"); + ret = -EINVAL; + goto dai_node_put; + } + } + + /* Initialize WM5110 - I2S and HDMI - I2S1 DAI links */ + for_each_card_prelinks(card, i, dai_link) { + unsigned int dai_index = 0; /* WM5110 */ + + dai_link->cpus->name = NULL; + dai_link->platforms->name = NULL; + + if (num_codecs > 1 && i == card->num_links - 1) + dai_index = 1; /* HDMI */ + + dai_link->codecs->of_node = codec_dai_node[dai_index]; + dai_link->cpus->of_node = cpu_dai_node[dai_index]; + dai_link->platforms->of_node = cpu_dai_node[dai_index]; + } + + if (num_codecs > 1) { + struct of_phandle_args args; + + /* HDMI DAI link (I2S1) */ + i = card->num_links - 1; + + ret = of_parse_phandle_with_fixed_args(dev->of_node, + "audio-codec", 0, 1, &args); + if (ret) { + dev_err(dev, "audio-codec property parse error\n"); + goto dai_node_put; + } + + ret = snd_soc_get_dai_name(&args, &card->dai_link[i].codecs->dai_name); + if (ret) { + dev_err(dev, "Unable to get codec_dai_name\n"); + goto dai_node_put; + } + } + + 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 dai_node_put; + } + + ret = devm_snd_soc_register_card(dev, card); + if (ret < 0) { + dev_err_probe(dev, ret, "Failed to register card\n"); + goto dai_node_put; + } + +dai_node_put: + for (i = 0; i < num_codecs; i++) { + of_node_put(codec_dai_node[i]); + of_node_put(cpu_dai_node[i]); + } + + of_node_put(card->aux_dev[0].dlc.of_node); + + return ret; +} + +static int tm2_pm_prepare(struct device *dev) +{ + struct snd_soc_card *card = dev_get_drvdata(dev); + + return tm2_stop_sysclk(card); +} + +static void tm2_pm_complete(struct device *dev) +{ + struct snd_soc_card *card = dev_get_drvdata(dev); + + tm2_start_sysclk(card); +} + +static const struct dev_pm_ops tm2_pm_ops = { + .prepare = tm2_pm_prepare, + .suspend = snd_soc_suspend, + .resume = snd_soc_resume, + .complete = tm2_pm_complete, + .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, +}; +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"); diff --git a/sound/soc/samsung/tobermory.c b/sound/soc/samsung/tobermory.c new file mode 100644 index 0000000000..9287a1d0ee --- /dev/null +++ b/sound/soc/samsung/tobermory.c @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Tobermory audio support +// +// Copyright 2011 Wolfson Microelectronics + +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/jack.h> +#include <linux/gpio.h> +#include <linux/module.h> + +#include "../codecs/wm8962.h" + +static int sample_rate = 44100; + +static int tobermory_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; + struct snd_soc_dai *codec_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[0]); + codec_dai = asoc_rtd_to_codec(rtd, 0); + + if (dapm->dev != codec_dai->dev) + return 0; + + switch (level) { + case SND_SOC_BIAS_PREPARE: + if (dapm->bias_level == SND_SOC_BIAS_STANDBY) { + ret = snd_soc_dai_set_pll(codec_dai, WM8962_FLL, + WM8962_FLL_MCLK, 32768, + sample_rate * 512); + if (ret < 0) + pr_err("Failed to start FLL: %d\n", ret); + + ret = snd_soc_dai_set_sysclk(codec_dai, + WM8962_SYSCLK_FLL, + sample_rate * 512, + SND_SOC_CLOCK_IN); + if (ret < 0) { + pr_err("Failed to set SYSCLK: %d\n", ret); + snd_soc_dai_set_pll(codec_dai, WM8962_FLL, + 0, 0, 0); + return ret; + } + } + break; + + default: + break; + } + + return 0; +} + +static int tobermory_set_bias_level_post(struct snd_soc_card *card, + struct snd_soc_dapm_context *dapm, + enum snd_soc_bias_level level) +{ + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_dai *codec_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[0]); + codec_dai = asoc_rtd_to_codec(rtd, 0); + + if (dapm->dev != codec_dai->dev) + return 0; + + switch (level) { + case SND_SOC_BIAS_STANDBY: + ret = snd_soc_dai_set_sysclk(codec_dai, WM8962_SYSCLK_MCLK, + 32768, SND_SOC_CLOCK_IN); + if (ret < 0) { + pr_err("Failed to switch away from FLL: %d\n", ret); + return ret; + } + + ret = snd_soc_dai_set_pll(codec_dai, WM8962_FLL, + 0, 0, 0); + if (ret < 0) { + pr_err("Failed to stop FLL: %d\n", ret); + return ret; + } + break; + + default: + break; + } + + dapm->bias_level = level; + + return 0; +} + +static int tobermory_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + sample_rate = params_rate(params); + + return 0; +} + +static const struct snd_soc_ops tobermory_ops = { + .hw_params = tobermory_hw_params, +}; + +SND_SOC_DAILINK_DEFS(cpu, + DAILINK_COMP_ARRAY(COMP_CPU("samsung-i2s.0")), + DAILINK_COMP_ARRAY(COMP_CODEC("wm8962.1-001a", "wm8962")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("samsung-i2s.0"))); + +static struct snd_soc_dai_link tobermory_dai[] = { + { + .name = "CPU", + .stream_name = "CPU", + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF + | SND_SOC_DAIFMT_CBM_CFM, + .ops = &tobermory_ops, + SND_SOC_DAILINK_REG(cpu), + }, +}; + +static const struct snd_kcontrol_new controls[] = { + SOC_DAPM_PIN_SWITCH("Main Speaker"), + SOC_DAPM_PIN_SWITCH("DMIC"), +}; + +static const struct snd_soc_dapm_widget widgets[] = { + SND_SOC_DAPM_HP("Headphone", NULL), + SND_SOC_DAPM_MIC("Headset Mic", NULL), + + SND_SOC_DAPM_MIC("DMIC", NULL), + SND_SOC_DAPM_MIC("AMIC", NULL), + + SND_SOC_DAPM_SPK("Main Speaker", NULL), +}; + +static const struct snd_soc_dapm_route audio_paths[] = { + { "Headphone", NULL, "HPOUTL" }, + { "Headphone", NULL, "HPOUTR" }, + + { "Main Speaker", NULL, "SPKOUTL" }, + { "Main Speaker", NULL, "SPKOUTR" }, + + { "Headset Mic", NULL, "MICBIAS" }, + { "IN4L", NULL, "Headset Mic" }, + { "IN4R", NULL, "Headset Mic" }, + + { "AMIC", NULL, "MICBIAS" }, + { "IN1L", NULL, "AMIC" }, + { "IN1R", NULL, "AMIC" }, + + { "DMIC", NULL, "MICBIAS" }, + { "DMICDAT", NULL, "DMIC" }, +}; + +static struct snd_soc_jack tobermory_headset; + +/* Headset jack detection DAPM pins */ +static struct snd_soc_jack_pin tobermory_headset_pins[] = { + { + .pin = "Headset Mic", + .mask = SND_JACK_MICROPHONE, + }, + { + .pin = "Headphone", + .mask = SND_JACK_MICROPHONE, + }, +}; + +static int tobermory_late_probe(struct snd_soc_card *card) +{ + struct snd_soc_pcm_runtime *rtd; + struct snd_soc_component *component; + struct snd_soc_dai *codec_dai; + int ret; + + rtd = snd_soc_get_pcm_runtime(card, &card->dai_link[0]); + component = asoc_rtd_to_codec(rtd, 0)->component; + codec_dai = asoc_rtd_to_codec(rtd, 0); + + ret = snd_soc_dai_set_sysclk(codec_dai, WM8962_SYSCLK_MCLK, + 32768, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + ret = snd_soc_card_jack_new_pins(card, "Headset", SND_JACK_HEADSET | + SND_JACK_BTN_0, &tobermory_headset, + tobermory_headset_pins, + ARRAY_SIZE(tobermory_headset_pins)); + if (ret) + return ret; + + wm8962_mic_detect(component, &tobermory_headset); + + return 0; +} + +static struct snd_soc_card tobermory = { + .name = "Tobermory", + .owner = THIS_MODULE, + .dai_link = tobermory_dai, + .num_links = ARRAY_SIZE(tobermory_dai), + + .set_bias_level = tobermory_set_bias_level, + .set_bias_level_post = tobermory_set_bias_level_post, + + .controls = controls, + .num_controls = ARRAY_SIZE(controls), + .dapm_widgets = widgets, + .num_dapm_widgets = ARRAY_SIZE(widgets), + .dapm_routes = audio_paths, + .num_dapm_routes = ARRAY_SIZE(audio_paths), + .fully_routed = true, + + .late_probe = tobermory_late_probe, +}; + +static int tobermory_probe(struct platform_device *pdev) +{ + struct snd_soc_card *card = &tobermory; + int ret; + + card->dev = &pdev->dev; + + ret = devm_snd_soc_register_card(&pdev->dev, card); + if (ret) + dev_err_probe(&pdev->dev, ret, "snd_soc_register_card() failed\n"); + + return ret; +} + +static struct platform_driver tobermory_driver = { + .driver = { + .name = "tobermory", + .pm = &snd_soc_pm_ops, + }, + .probe = tobermory_probe, +}; + +module_platform_driver(tobermory_driver); + +MODULE_DESCRIPTION("Tobermory audio support"); +MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:tobermory"); |