]> git.kernelconcepts.de Git - karo-tx-linux.git/commitdiff
ASoC: atmel: tse850: add ASoC driver for the Axentia TSE-850
authorPeter Rosin <peda@axentia.se>
Tue, 15 Nov 2016 18:38:15 +0000 (19:38 +0100)
committerMark Brown <broonie@kernel.org>
Wed, 16 Nov 2016 11:12:42 +0000 (11:12 +0000)
The TSE-850 is an FM Transmitter Station Equipment, designed to generate
baseband signals for FM, mainly the DARC subcarrier, but other signals
are also possible.

Signed-off-by: Peter Rosin <peda@axentia.se>
Signed-off-by: Mark Brown <broonie@kernel.org>
MAINTAINERS
sound/soc/atmel/Kconfig
sound/soc/atmel/Makefile
sound/soc/atmel/tse850-pcm5142.c [new file with mode: 0644]

index 35485fd86e73c3bad236e60c81f9d98147d2e9f8..0dc36a9c61cd6f05ea5f589bdfba5ed651f7f135 100644 (file)
@@ -2322,6 +2322,7 @@ M:        Peter Rosin <peda@axentia.se>
 L:     alsa-devel@alsa-project.org (moderated for non-subscribers)
 S:     Maintained
 F:     Documentation/devicetree/bindings/sound/axentia,*
+F:     sound/soc/atmel/tse850-pcm5142.c
 
 AZ6007 DVB DRIVER
 M:     Mauro Carvalho Chehab <mchehab@s-opensource.com>
index 22aec9a1e9a4991026e98b40879e2064997b92a1..4a56f3dfba5132a01bbb49eb985540d16a944ecf 100644 (file)
@@ -78,4 +78,14 @@ config SND_ATMEL_SOC_PDMIC
        help
          Say Y if you want to add support for Atmel ASoC driver for boards using
          PDMIC.
+
+config SND_ATMEL_SOC_TSE850_PCM5142
+       tristate "ASoC driver for the Axentia TSE-850"
+       depends on ARCH_AT91 && OF
+       depends on ATMEL_SSC && I2C
+       select SND_ATMEL_SOC_SSC_DMA
+       select SND_SOC_PCM512x_I2C
+       help
+         Say Y if you want to add support for the ASoC driver for the
+         Axentia TSE-850 with a PCM5142 codec.
 endif
index a2b127bd9c8717ac6c1501eff669014c306058a5..67e10cbd4ed70a39c536371f3be977022a5673e0 100644 (file)
@@ -13,9 +13,11 @@ snd-atmel-soc-wm8904-objs := atmel_wm8904.o
 snd-soc-sam9x5-wm8731-objs := sam9x5_wm8731.o
 snd-atmel-soc-classd-objs := atmel-classd.o
 snd-atmel-soc-pdmic-objs := atmel-pdmic.o
+snd-atmel-soc-tse850-pcm5142-objs := tse850-pcm5142.o
 
 obj-$(CONFIG_SND_AT91_SOC_SAM9G20_WM8731) += snd-soc-sam9g20-wm8731.o
 obj-$(CONFIG_SND_ATMEL_SOC_WM8904) += snd-atmel-soc-wm8904.o
 obj-$(CONFIG_SND_AT91_SOC_SAM9X5_WM8731) += snd-soc-sam9x5-wm8731.o
 obj-$(CONFIG_SND_ATMEL_SOC_CLASSD) += snd-atmel-soc-classd.o
 obj-$(CONFIG_SND_ATMEL_SOC_PDMIC) += snd-atmel-soc-pdmic.o
+obj-$(CONFIG_SND_ATMEL_SOC_TSE850_PCM5142) += snd-atmel-soc-tse850-pcm5142.o
diff --git a/sound/soc/atmel/tse850-pcm5142.c b/sound/soc/atmel/tse850-pcm5142.c
new file mode 100644 (file)
index 0000000..ac6a814
--- /dev/null
@@ -0,0 +1,472 @@
+/*
+ * TSE-850 audio - ASoC driver for the Axentia TSE-850 with a PCM5142 codec
+ *
+ * Copyright (C) 2016 Axentia Technologies AB
+ *
+ * Author: Peter Rosin <peda@axentia.se>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+/*
+ *               loop1 relays
+ *   IN1 +---o  +------------+  o---+ OUT1
+ *            \                /
+ *             +              +
+ *             |   /          |
+ *             +--o  +--.     |
+ *             |  add   |     |
+ *             |        V     |
+ *             |      .---.   |
+ *   DAC +----------->|Sum|---+
+ *             |      '---'   |
+ *             |              |
+ *             +              +
+ *
+ *   IN2 +---o--+------------+--o---+ OUT2
+ *               loop2 relays
+ *
+ * The 'loop1' gpio pin controlls two relays, which are either in loop
+ * position, meaning that input and output are directly connected, or
+ * they are in mixer position, meaning that the signal is passed through
+ * the 'Sum' mixer. Similarly for 'loop2'.
+ *
+ * In the above, the 'loop1' relays are inactive, thus feeding IN1 to the
+ * mixer (if 'add' is active) and feeding the mixer output to OUT1. The
+ * 'loop2' relays are active, short-cutting the TSE-850 from channel 2.
+ * IN1, IN2, OUT1 and OUT2 are TSE-850 connectors and DAC is the PCB name
+ * of the (filtered) output from the PCM5142 codec.
+ */
+
+#include <linux/clk.h>
+#include <linux/gpio.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/soc.h>
+#include <sound/pcm_params.h>
+
+#include "atmel_ssc_dai.h"
+
+struct tse850_priv {
+       int ssc_id;
+
+       struct gpio_desc *add;
+       struct gpio_desc *loop1;
+       struct gpio_desc *loop2;
+
+       struct regulator *ana;
+
+       int add_cache;
+       int loop1_cache;
+       int loop2_cache;
+};
+
+static int tse850_get_mux1(struct snd_kcontrol *kctrl,
+                          struct snd_ctl_elem_value *ucontrol)
+{
+       struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kctrl);
+       struct snd_soc_card *card = dapm->card;
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+
+       ucontrol->value.enumerated.item[0] = tse850->loop1_cache;
+
+       return 0;
+}
+
+static int tse850_put_mux1(struct snd_kcontrol *kctrl,
+                          struct snd_ctl_elem_value *ucontrol)
+{
+       struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kctrl);
+       struct snd_soc_card *card = dapm->card;
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+       struct soc_enum *e = (struct soc_enum *)kctrl->private_value;
+       unsigned int val = ucontrol->value.enumerated.item[0];
+
+       if (val >= e->items)
+               return -EINVAL;
+
+       gpiod_set_value_cansleep(tse850->loop1, val);
+       tse850->loop1_cache = val;
+
+       return snd_soc_dapm_put_enum_double(kctrl, ucontrol);
+}
+
+static int tse850_get_mux2(struct snd_kcontrol *kctrl,
+                          struct snd_ctl_elem_value *ucontrol)
+{
+       struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kctrl);
+       struct snd_soc_card *card = dapm->card;
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+
+       ucontrol->value.enumerated.item[0] = tse850->loop2_cache;
+
+       return 0;
+}
+
+static int tse850_put_mux2(struct snd_kcontrol *kctrl,
+                          struct snd_ctl_elem_value *ucontrol)
+{
+       struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kctrl);
+       struct snd_soc_card *card = dapm->card;
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+       struct soc_enum *e = (struct soc_enum *)kctrl->private_value;
+       unsigned int val = ucontrol->value.enumerated.item[0];
+
+       if (val >= e->items)
+               return -EINVAL;
+
+       gpiod_set_value_cansleep(tse850->loop2, val);
+       tse850->loop2_cache = val;
+
+       return snd_soc_dapm_put_enum_double(kctrl, ucontrol);
+}
+
+int tse850_get_mix(struct snd_kcontrol *kctrl,
+                  struct snd_ctl_elem_value *ucontrol)
+{
+       struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kctrl);
+       struct snd_soc_card *card = dapm->card;
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+
+       ucontrol->value.enumerated.item[0] = tse850->add_cache;
+
+       return 0;
+}
+
+int tse850_put_mix(struct snd_kcontrol *kctrl,
+                  struct snd_ctl_elem_value *ucontrol)
+{
+       struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kctrl);
+       struct snd_soc_card *card = dapm->card;
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+       int connect = !!ucontrol->value.integer.value[0];
+
+       if (tse850->add_cache == connect)
+               return 0;
+
+       /*
+        * Hmmm, this gpiod_set_value_cansleep call should probably happen
+        * inside snd_soc_dapm_mixer_update_power in the loop.
+        */
+       gpiod_set_value_cansleep(tse850->add, connect);
+       tse850->add_cache = connect;
+
+       snd_soc_dapm_mixer_update_power(dapm, kctrl, connect, NULL);
+       return 1;
+}
+
+int tse850_get_ana(struct snd_kcontrol *kctrl,
+                  struct snd_ctl_elem_value *ucontrol)
+{
+       struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kctrl);
+       struct snd_soc_card *card = dapm->card;
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+       int ret;
+
+       ret = regulator_get_voltage(tse850->ana);
+       if (ret < 0)
+               return ret;
+
+       /*
+        * Map regulator output values like so:
+        *      -11.5V to "Low" (enum 0)
+        * 11.5V-12.5V to "12V" (enum 1)
+        * 12.5V-13.5V to "13V" (enum 2)
+        *     ...
+        * 18.5V-19.5V to "19V" (enum 8)
+        * 19.5V-      to "20V" (enum 9)
+        */
+       if (ret < 11000000)
+               ret = 11000000;
+       else if (ret > 20000000)
+               ret = 20000000;
+       ret -= 11000000;
+       ret = (ret + 500000) / 1000000;
+
+       ucontrol->value.enumerated.item[0] = ret;
+
+       return 0;
+}
+
+int tse850_put_ana(struct snd_kcontrol *kctrl,
+                  struct snd_ctl_elem_value *ucontrol)
+{
+       struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kctrl);
+       struct snd_soc_card *card = dapm->card;
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+       struct soc_enum *e = (struct soc_enum *)kctrl->private_value;
+       unsigned int uV = ucontrol->value.enumerated.item[0];
+       int ret;
+
+       if (uV >= e->items)
+               return -EINVAL;
+
+       /*
+        * Map enum zero (Low) to 2 volts on the regulator, do this since
+        * the ana regulator is supplied by the system 12V voltage and
+        * requesting anything below the system voltage causes the system
+        * voltage to be passed through the regulator. Also, the ana
+        * regulator induces noise when requesting voltages near the
+        * system voltage. So, by mapping Low to 2V, that noise is
+        * eliminated when all that is needed is 12V (the system voltage).
+        */
+       if (uV)
+               uV = 11000000 + (1000000 * uV);
+       else
+               uV = 2000000;
+
+       ret = regulator_set_voltage(tse850->ana, uV, uV);
+       if (ret < 0)
+               return ret;
+
+       return snd_soc_dapm_put_enum_double(kctrl, ucontrol);
+}
+
+static const char * const mux_text[] = { "Mixer", "Loop" };
+
+static const struct soc_enum mux_enum =
+       SOC_ENUM_SINGLE(SND_SOC_NOPM, 0, 2, mux_text);
+
+static const struct snd_kcontrol_new mux1 =
+       SOC_DAPM_ENUM_EXT("MUX1", mux_enum, tse850_get_mux1, tse850_put_mux1);
+
+static const struct snd_kcontrol_new mux2 =
+       SOC_DAPM_ENUM_EXT("MUX2", mux_enum, tse850_get_mux2, tse850_put_mux2);
+
+#define TSE850_DAPM_SINGLE_EXT(xname, reg, shift, max, invert, xget, xput) \
+{      .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
+       .info = snd_soc_info_volsw, \
+       .get = xget, \
+       .put = xput, \
+       .private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }
+
+static const struct snd_kcontrol_new mix[] = {
+       TSE850_DAPM_SINGLE_EXT("IN Switch", SND_SOC_NOPM, 0, 1, 0,
+                              tse850_get_mix, tse850_put_mix),
+};
+
+static const char * const ana_text[] = {
+       "Low", "12V", "13V", "14V", "15V", "16V", "17V", "18V", "19V", "20V"
+};
+
+static const struct soc_enum ana_enum =
+       SOC_ENUM_SINGLE(SND_SOC_NOPM, 0, 9, ana_text);
+
+static const struct snd_kcontrol_new out =
+       SOC_DAPM_ENUM_EXT("ANA", ana_enum, tse850_get_ana, tse850_put_ana);
+
+static const struct snd_soc_dapm_widget tse850_dapm_widgets[] = {
+       SND_SOC_DAPM_LINE("OUT1", NULL),
+       SND_SOC_DAPM_LINE("OUT2", NULL),
+       SND_SOC_DAPM_LINE("IN1", NULL),
+       SND_SOC_DAPM_LINE("IN2", NULL),
+       SND_SOC_DAPM_INPUT("DAC"),
+       SND_SOC_DAPM_AIF_IN("AIFINL", "Playback", 0, SND_SOC_NOPM, 0, 0),
+       SND_SOC_DAPM_AIF_IN("AIFINR", "Playback", 1, SND_SOC_NOPM, 0, 0),
+       SOC_MIXER_ARRAY("MIX", SND_SOC_NOPM, 0, 0, mix),
+       SND_SOC_DAPM_MUX("MUX1", SND_SOC_NOPM, 0, 0, &mux1),
+       SND_SOC_DAPM_MUX("MUX2", SND_SOC_NOPM, 0, 0, &mux2),
+       SND_SOC_DAPM_OUT_DRV("OUT", SND_SOC_NOPM, 0, 0, &out, 1),
+};
+
+/*
+ * These connections are not entirely correct, since both IN1 and IN2
+ * are always fed to MIX (if the "IN switch" is set so), i.e. without
+ * regard to the loop1 and loop2 relays that according to this only
+ * control MUX1 and MUX2 but in fact also control how the input signals
+ * are routed.
+ * But, 1) I don't know how to do it right, and 2) it doesn't seem to
+ * matter in practice since nothing is powered in those sections anyway.
+ */
+static const struct snd_soc_dapm_route tse850_intercon[] = {
+       { "OUT1", NULL, "MUX1" },
+       { "OUT2", NULL, "MUX2" },
+
+       { "MUX1", "Loop",  "IN1" },
+       { "MUX1", "Mixer", "OUT" },
+
+       { "MUX2", "Loop",  "IN2" },
+       { "MUX2", "Mixer", "OUT" },
+
+       { "OUT", NULL, "MIX" },
+
+       { "MIX", NULL, "DAC" },
+       { "MIX", "IN Switch", "IN1" },
+       { "MIX", "IN Switch", "IN2" },
+
+       /* connect board input to the codec left channel output pin */
+       { "DAC", NULL, "OUTL" },
+};
+
+static struct snd_soc_dai_link tse850_dailink = {
+       .name = "TSE-850",
+       .stream_name = "TSE-850-PCM",
+       .codec_dai_name = "pcm512x-hifi",
+       .dai_fmt = SND_SOC_DAIFMT_I2S
+                | SND_SOC_DAIFMT_NB_NF
+                | SND_SOC_DAIFMT_CBM_CFS,
+};
+
+static struct snd_soc_card tse850_card = {
+       .name = "TSE-850-ASoC",
+       .owner = THIS_MODULE,
+       .dai_link = &tse850_dailink,
+       .num_links = 1,
+       .dapm_widgets = tse850_dapm_widgets,
+       .num_dapm_widgets = ARRAY_SIZE(tse850_dapm_widgets),
+       .dapm_routes = tse850_intercon,
+       .num_dapm_routes = ARRAY_SIZE(tse850_intercon),
+       .fully_routed = true,
+};
+
+static int tse850_dt_init(struct platform_device *pdev)
+{
+       struct device_node *np = pdev->dev.of_node;
+       struct device_node *codec_np, *cpu_np;
+       struct snd_soc_card *card = &tse850_card;
+       struct snd_soc_dai_link *dailink = &tse850_dailink;
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+
+       if (!np) {
+               dev_err(&pdev->dev, "only device tree supported\n");
+               return -EINVAL;
+       }
+
+       cpu_np = of_parse_phandle(np, "axentia,ssc-controller", 0);
+       if (!cpu_np) {
+               dev_err(&pdev->dev, "failed to get dai and pcm info\n");
+               return -EINVAL;
+       }
+       dailink->cpu_of_node = cpu_np;
+       dailink->platform_of_node = cpu_np;
+       tse850->ssc_id = of_alias_get_id(cpu_np, "ssc");
+       of_node_put(cpu_np);
+
+       codec_np = of_parse_phandle(np, "axentia,audio-codec", 0);
+       if (!codec_np) {
+               dev_err(&pdev->dev, "failed to get codec info\n");
+               return -EINVAL;
+       }
+       dailink->codec_of_node = codec_np;
+       of_node_put(codec_np);
+
+       return 0;
+}
+
+static int tse850_probe(struct platform_device *pdev)
+{
+       struct snd_soc_card *card = &tse850_card;
+       struct device *dev = card->dev = &pdev->dev;
+       struct tse850_priv *tse850;
+       int ret;
+
+       tse850 = devm_kzalloc(dev, sizeof(*tse850), GFP_KERNEL);
+       if (!tse850)
+               return -ENOMEM;
+
+       snd_soc_card_set_drvdata(card, tse850);
+
+       ret = tse850_dt_init(pdev);
+       if (ret) {
+               dev_err(dev, "failed to init dt info\n");
+               return ret;
+       }
+
+       tse850->add = devm_gpiod_get(dev, "axentia,add", GPIOD_OUT_HIGH);
+       if (IS_ERR(tse850->add)) {
+               if (PTR_ERR(tse850->add) != -EPROBE_DEFER)
+                       dev_err(dev, "failed to get 'add' gpio\n");
+               return PTR_ERR(tse850->add);
+       }
+       tse850->add_cache = 1;
+
+       tse850->loop1 = devm_gpiod_get(dev, "axentia,loop1", GPIOD_OUT_HIGH);
+       if (IS_ERR(tse850->loop1)) {
+               if (PTR_ERR(tse850->loop1) != -EPROBE_DEFER)
+                       dev_err(dev, "failed to get 'loop1' gpio\n");
+               return PTR_ERR(tse850->loop1);
+       }
+       tse850->loop1_cache = 1;
+
+       tse850->loop2 = devm_gpiod_get(dev, "axentia,loop2", GPIOD_OUT_HIGH);
+       if (IS_ERR(tse850->loop2)) {
+               if (PTR_ERR(tse850->loop2) != -EPROBE_DEFER)
+                       dev_err(dev, "failed to get 'loop2' gpio\n");
+               return PTR_ERR(tse850->loop2);
+       }
+       tse850->loop2_cache = 1;
+
+       tse850->ana = devm_regulator_get(dev, "axentia,ana");
+       if (IS_ERR(tse850->ana)) {
+               if (PTR_ERR(tse850->ana) != -EPROBE_DEFER)
+                       dev_err(dev, "failed to get 'ana' regulator\n");
+               return PTR_ERR(tse850->ana);
+       }
+
+       ret = regulator_enable(tse850->ana);
+       if (ret < 0) {
+               dev_err(dev, "failed to enable the 'ana' regulator\n");
+               return ret;
+       }
+
+       ret = atmel_ssc_set_audio(tse850->ssc_id);
+       if (ret != 0) {
+               dev_err(dev,
+                       "failed to set SSC %d for audio\n", tse850->ssc_id);
+               goto err_disable_ana;
+       }
+
+       ret = snd_soc_register_card(card);
+       if (ret) {
+               dev_err(dev, "snd_soc_register_card failed\n");
+               goto err_put_audio;
+       }
+
+       return 0;
+
+err_put_audio:
+       atmel_ssc_put_audio(tse850->ssc_id);
+err_disable_ana:
+       regulator_disable(tse850->ana);
+       return ret;
+}
+
+static int tse850_remove(struct platform_device *pdev)
+{
+       struct snd_soc_card *card = platform_get_drvdata(pdev);
+       struct tse850_priv *tse850 = snd_soc_card_get_drvdata(card);
+
+       snd_soc_unregister_card(card);
+       atmel_ssc_put_audio(tse850->ssc_id);
+       regulator_disable(tse850->ana);
+
+       return 0;
+}
+
+static const struct of_device_id tse850_dt_ids[] = {
+       { .compatible = "axentia,tse850-pcm5142", },
+       { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, tse850_dt_ids);
+
+static struct platform_driver tse850_driver = {
+       .driver = {
+               .name = "axentia-tse850-pcm5142",
+               .of_match_table = of_match_ptr(tse850_dt_ids),
+       },
+       .probe = tse850_probe,
+       .remove = tse850_remove,
+};
+
+module_platform_driver(tse850_driver);
+
+/* Module information */
+MODULE_AUTHOR("Peter Rosin <peda@axentia.se>");
+MODULE_DESCRIPTION("ALSA SoC driver for TSE-850 with PCM5142 codec");
+MODULE_LICENSE("GPL");