ASoC: tda998x: add a codec to the HDMI transmitter
authorJean-Francois Moine <moinejf@free.fr>
Fri, 29 Aug 2014 14:51:40 +0000 (15:51 +0100)
committerLiviu Dudau <Liviu.Dudau@arm.com>
Tue, 7 Oct 2014 11:10:46 +0000 (12:10 +0100)
This patch adds a CODEC function to the NXP TDA998x HDMI transmitter.

The CODEC handles both I2S and S/PDIF input and does dynamic input
switch in the TDA998x I2C driver on start/stop audio streaming.

Signed-off-by: Jean-Francois Moine <moinejf@free.fr>
[added reg documentation for the DT bindings]
Signed-off-by: Andrew Jackson <Andrew.Jackson@arm.com>
Signed-off-by: Liviu Dudau <Liviu.Dudau@arm.com>
Documentation/devicetree/bindings/drm/i2c/tda998x.txt
drivers/gpu/drm/i2c/Makefile
drivers/gpu/drm/i2c/tda998x_codec.c [new file with mode: 0644]
drivers/gpu/drm/i2c/tda998x_drv.c
drivers/gpu/drm/i2c/tda998x_drv.h [new file with mode: 0644]
include/drm/i2c/tda998x.h

index d7df01c5bb3ab8945938b2e47cf9e3a8ea94b97e..adac4dd2de5f257d380b70e4e22d7f8634de78be 100644 (file)
@@ -3,6 +3,8 @@ Device-Tree bindings for the NXP TDA998x HDMI transmitter
 Required properties;
   - compatible: must be "nxp,tda998x"
 
+  - reg: I2C address
+
 Optional properties:
   - interrupts: interrupt number and trigger type
        default: polling
@@ -15,6 +17,15 @@ Optional properties:
   - video-ports: 24 bits value which defines how the video controller
        output is wired to the TDA998x input - default: <0x230145>
 
+  - audio-ports: one or two values corresponding to entries in
+       the audio-port-names property.
+
+  - audio-port-names: must contain "i2s", "spdif" entries
+       matching entries in the audio-ports property.
+
+  - #sound-dai-cells: must be set to <1> for use with the simple-card.
+       The DAI 0 is the I2S input and the DAI 1 is the S/PDIF input.
+
 Example:
 
        tda998x: hdmi-encoder {
@@ -24,4 +35,9 @@ Example:
                interrupts = <27 2>;            /* falling edge */
                pinctrl-0 = <&pmx_camera>;
                pinctrl-names = "default";
+
+               audio-ports = <0x03>, <0x04>;
+               audio-port-names = "i2s", "spdif";
+               #sound-dai-cells = <1>;
+         };
        };
index 62613e20aafb996ff44a9d5b6ec7765959790cff..c1e2ee0004c3633b667950f9b0440ad4f4f76379 100644 (file)
@@ -9,5 +9,5 @@ obj-$(CONFIG_DRM_I2C_SII9022) += sii9022.o
 sil164-y := sil164_drv.o
 obj-$(CONFIG_DRM_I2C_SIL164) += sil164.o
 
-tda998x-y := tda998x_drv.o
+tda998x-y := tda998x_drv.o tda998x_codec.o
 obj-$(CONFIG_DRM_I2C_NXP_TDA998X) += tda998x.o
diff --git a/drivers/gpu/drm/i2c/tda998x_codec.c b/drivers/gpu/drm/i2c/tda998x_codec.c
new file mode 100644 (file)
index 0000000..4f1d759
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+ * ALSA SoC TDA998X CODEC
+ *
+ * Copyright (C) 2014 Jean-Francois Moine
+ *
+ * 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.
+ */
+
+#include <linux/module.h>
+#include <sound/soc.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <linux/of.h>
+#include <linux/i2c.h>
+#include <drm/drm_encoder_slave.h>
+#include <drm/i2c/tda998x.h>
+
+#include "tda998x_drv.h"
+
+#define TDA998X_FORMATS        (SNDRV_PCM_FMTBIT_S16_LE | \
+                       SNDRV_PCM_FMTBIT_S20_3LE | \
+                       SNDRV_PCM_FMTBIT_S24_LE | \
+                       SNDRV_PCM_FMTBIT_S32_LE)
+
+static int tda_startup(struct snd_pcm_substream *substream,
+                       struct snd_soc_dai *dai)
+{
+       struct tda998x_priv *priv = snd_soc_codec_get_drvdata(dai->codec);
+       u8 *eld = priv->eld;
+       struct snd_pcm_runtime *runtime = substream->runtime;
+       u8 *sad;
+       int sad_count;
+       unsigned eld_ver, mnl, rate_mask;
+       unsigned max_channels, fmt;
+       u64 formats;
+       struct snd_pcm_hw_constraint_list *rate_constraints =
+                       &priv->rate_constraints;
+       static const u32 hdmi_rates[] = {
+               32000, 44100, 48000, 88200, 96000, 176400, 192000
+       };
+
+       /* check if streaming is already active */
+       if (priv->dai_id != AFMT_NO_AUDIO)
+               return -EBUSY;
+       priv->dai_id = dai->id;
+
+       if (!eld)
+               return 0;
+
+       /* adjust the hw params from the ELD (EDID) */
+       eld_ver = eld[0] >> 3;
+       if (eld_ver != 2 && eld_ver != 31)
+               return 0;
+
+       mnl = eld[4] & 0x1f;
+       if (mnl > 16)
+               return 0;
+
+       sad_count = eld[5] >> 4;
+       sad = eld + 20 + mnl;
+
+       /* Start from the basic audio settings */
+       max_channels = 2;
+       rate_mask = 0;
+       fmt = 0;
+       while (sad_count--) {
+               switch (sad[0] & 0x78) {
+               case 0x08: /* PCM */
+                       max_channels = max(max_channels, (sad[0] & 7) + 1u);
+                       rate_mask |= sad[1];
+                       fmt |= sad[2] & 0x07;
+                       break;
+               }
+               sad += 3;
+       }
+
+       /* set the constraints */
+       rate_constraints->list = hdmi_rates;
+       rate_constraints->count = ARRAY_SIZE(hdmi_rates);
+       rate_constraints->mask = rate_mask;
+       snd_pcm_hw_constraint_list(runtime, 0,
+                                       SNDRV_PCM_HW_PARAM_RATE,
+                                       rate_constraints);
+
+       formats = 0;
+       if (fmt & 1)
+               formats |= SNDRV_PCM_FMTBIT_S16_LE;
+       if (fmt & 2)
+               formats |= SNDRV_PCM_FMTBIT_S20_3LE;
+       if (fmt & 4)
+               formats |= SNDRV_PCM_FMTBIT_S24_LE;
+       snd_pcm_hw_constraint_mask64(runtime,
+                               SNDRV_PCM_HW_PARAM_FORMAT,
+                               formats);
+
+       snd_pcm_hw_constraint_minmax(runtime,
+                               SNDRV_PCM_HW_PARAM_CHANNELS,
+                               1, max_channels);
+       return 0;
+}
+
+static int tda_hw_params(struct snd_pcm_substream *substream,
+                       struct snd_pcm_hw_params *params,
+                       struct snd_soc_dai *dai)
+{
+       struct tda998x_priv *priv = snd_soc_codec_get_drvdata(dai->codec);
+
+       /* Requires an attached display */
+       if (!priv->encoder->crtc)
+               return -ENODEV;
+
+       /* if same input and same parameters, do not do a full switch */
+       if (dai->id == priv->params.audio_format &&
+           params_format(params) == priv->audio_sample_format) {
+               tda998x_audio_start(priv, 0);
+               return 0;
+       }
+       priv->params.audio_format = dai->id;
+       priv->audio_sample_format = params_format(params);
+       priv->params.audio_cfg =
+               priv->audio_ports[dai->id == AFMT_I2S ? 0 : 1];
+       tda998x_audio_start(priv, 1);
+       return 0;
+}
+
+static void tda_shutdown(struct snd_pcm_substream *substream,
+                       struct snd_soc_dai *dai)
+{
+       struct tda998x_priv *priv = snd_soc_codec_get_drvdata(dai->codec);
+
+       tda998x_audio_stop(priv);
+       priv->dai_id = AFMT_NO_AUDIO;
+}
+
+static const struct snd_soc_dai_ops tda_ops = {
+       .startup = tda_startup,
+       .hw_params = tda_hw_params,
+       .shutdown = tda_shutdown,
+};
+
+static struct snd_soc_dai_driver tda998x_dai[] = {
+       {
+               .name = "i2s-hifi",
+               .id = AFMT_I2S,
+               .playback = {
+                       .stream_name    = "HDMI I2S Playback",
+                       .channels_min   = 1,
+                       .channels_max   = 8,
+                       .rates          = SNDRV_PCM_RATE_CONTINUOUS,
+                       .rate_min       = 5512,
+                       .rate_max       = 192000,
+                       .formats        = TDA998X_FORMATS,
+               },
+               .ops = &tda_ops,
+       },
+       {
+               .name = "spdif-hifi",
+               .id = AFMT_SPDIF,
+               .playback = {
+                       .stream_name    = "HDMI SPDIF Playback",
+                       .channels_min   = 1,
+                       .channels_max   = 2,
+                       .rates          = SNDRV_PCM_RATE_CONTINUOUS,
+                       .rate_min       = 22050,
+                       .rate_max       = 192000,
+                       .formats        = TDA998X_FORMATS,
+               },
+               .ops = &tda_ops,
+       },
+};
+
+static const struct snd_soc_dapm_widget tda_widgets[] = {
+       SND_SOC_DAPM_OUTPUT("hdmi-out"),
+};
+static const struct snd_soc_dapm_route tda_routes[] = {
+       { "hdmi-out", NULL, "HDMI I2S Playback" },
+       { "hdmi-out", NULL, "HDMI SPDIF Playback" },
+};
+
+static int tda_probe(struct snd_soc_codec *codec)
+{
+       struct i2c_client *i2c_client = to_i2c_client(codec->dev);
+       struct tda998x_priv *priv = i2c_get_clientdata(i2c_client);
+       struct device_node *np = codec->dev->of_node;
+       int i, j, ret;
+       const char *p;
+
+       if (!priv)
+               return -ENODEV;
+       snd_soc_codec_set_drvdata(codec, priv);
+
+       if (!np)
+               return 0;
+
+       /* get the audio input ports*/
+       for (i = 0; i < 2; i++) {
+               u32 port;
+
+               ret = of_property_read_u32_index(np, "audio-ports", i, &port);
+               if (ret) {
+                       if (i == 0)
+                               dev_err(codec->dev,
+                                       "bad or missing audio-ports\n");
+                       break;
+               }
+               ret = of_property_read_string_index(np, "audio-port-names",
+                                               i, &p);
+               if (ret) {
+                       dev_err(codec->dev,
+                               "missing audio-port-names[%d]\n", i);
+                       break;
+               }
+               if (strcmp(p, "i2s") == 0) {
+                       j = 0;
+               } else if (strcmp(p, "spdif") == 0) {
+                       j = 1;
+               } else {
+                       dev_err(codec->dev,
+                               "bad audio-port-names '%s'\n", p);
+                       break;
+               }
+               priv->audio_ports[j] = port;
+       }
+       return 0;
+}
+
+static const struct snd_soc_codec_driver soc_codec_tda998x = {
+       .probe = tda_probe,
+       .dapm_widgets = tda_widgets,
+       .num_dapm_widgets = ARRAY_SIZE(tda_widgets),
+       .dapm_routes = tda_routes,
+       .num_dapm_routes = ARRAY_SIZE(tda_routes),
+};
+
+int tda998x_codec_register(struct device *dev)
+{
+       return snd_soc_register_codec(dev,
+                               &soc_codec_tda998x,
+                               tda998x_dai, ARRAY_SIZE(tda998x_dai));
+}
+
+void tda998x_codec_unregister(struct device *dev)
+{
+       snd_soc_unregister_codec(dev);
+}
index 27632aa61ea0c7678d071fd18f3d6897320097da..10da2ca0a1d205f8ca2288f366fad940c74498fe 100644 (file)
@@ -21,6 +21,7 @@
 #include <linux/module.h>
 #include <linux/irq.h>
 #include <sound/asoundef.h>
+#include <sound/pcm_params.h>
 
 #include <drm/drmP.h>
 #include <drm/drm_crtc_helper.h>
 #include <drm/drm_edid.h>
 #include <drm/i2c/tda998x.h>
 
-#define DBG(fmt, ...) DRM_DEBUG(fmt"\n", ##__VA_ARGS__)
+#include "tda998x_drv.h"
 
-struct tda998x_priv {
-       struct i2c_client *cec;
-       struct i2c_client *hdmi;
-       uint16_t rev;
-       uint8_t current_page;
-       int dpms;
-       bool is_hdmi_sink;
-       u8 vip_cntrl_0;
-       u8 vip_cntrl_1;
-       u8 vip_cntrl_2;
-       struct tda998x_encoder_params params;
-
-       wait_queue_head_t wq_edid;
-       volatile int wq_edid_wait;
-       struct drm_encoder *encoder;
-};
+#define DBG(fmt, ...) DRM_DEBUG(fmt"\n", ##__VA_ARGS__)
 
 #define to_tda998x_priv(x)  ((struct tda998x_priv *)to_encoder_slave(x)->slave_priv)
 
@@ -640,12 +626,11 @@ static void
 tda998x_configure_audio(struct tda998x_priv *priv,
                struct drm_display_mode *mode, struct tda998x_encoder_params *p)
 {
-       uint8_t buf[6], clksel_aip, clksel_fs, cts_n, adiv;
+       uint8_t buf[6], clksel_aip, clksel_fs, cts_n, adiv, aclk;
        uint32_t n;
 
        /* Enable audio ports */
        reg_write(priv, REG_ENA_AP, p->audio_cfg);
-       reg_write(priv, REG_ENA_ACLK, p->audio_clk_cfg);
 
        /* Set audio input source */
        switch (p->audio_format) {
@@ -654,13 +639,28 @@ tda998x_configure_audio(struct tda998x_priv *priv,
                clksel_aip = AIP_CLKSEL_AIP_SPDIF;
                clksel_fs = AIP_CLKSEL_FS_FS64SPDIF;
                cts_n = CTS_N_M(3) | CTS_N_K(3);
+               aclk = 0;                               /* no clock */
                break;
 
        case AFMT_I2S:
                reg_write(priv, REG_MUX_AP, MUX_AP_SELECT_I2S);
                clksel_aip = AIP_CLKSEL_AIP_I2S;
                clksel_fs = AIP_CLKSEL_FS_ACLK;
-               cts_n = CTS_N_M(3) | CTS_N_K(3);
+               /* with I2S input, the CTS_N predivider depends on
+                * the sample width */
+               switch (priv->audio_sample_format) {
+               case SNDRV_PCM_FORMAT_S16_LE:
+                       cts_n = CTS_N_M(3) | CTS_N_K(1);
+                       break;
+               case SNDRV_PCM_FORMAT_S24_LE:
+                       cts_n = CTS_N_M(3) | CTS_N_K(2);
+                       break;
+               default:
+               case SNDRV_PCM_FORMAT_S32_LE:
+                       cts_n = CTS_N_M(3) | CTS_N_K(3);
+                       break;
+               }
+               aclk = 1;                               /* clock enable */
                break;
 
        default:
@@ -672,6 +672,7 @@ tda998x_configure_audio(struct tda998x_priv *priv,
        reg_clear(priv, REG_AIP_CNTRL_0, AIP_CNTRL_0_LAYOUT |
                                        AIP_CNTRL_0_ACR_MAN);   /* auto CTS */
        reg_write(priv, REG_CTS_N, cts_n);
+       reg_write(priv, REG_ENA_ACLK, aclk);
 
        /*
         * Audio input somehow depends on HDMI line rate which is
@@ -728,6 +729,24 @@ tda998x_configure_audio(struct tda998x_priv *priv,
        tda998x_write_aif(priv, p);
 }
 
+/* tda998x codec interface */
+void tda998x_audio_start(struct tda998x_priv *priv,
+                        int full)
+{
+       struct tda998x_encoder_params *p = &priv->params;
+
+       if (!full) {
+               reg_write(priv, REG_ENA_AP, p->audio_cfg);
+               return;
+       }
+       tda998x_configure_audio(priv, &priv->encoder->crtc->hwmode, p);
+}
+
+void tda998x_audio_stop(struct tda998x_priv *priv)
+{
+       reg_write(priv, REG_ENA_AP, 0);
+}
+
 /* DRM encoder functions */
 
 static void
@@ -1168,6 +1187,11 @@ tda998x_encoder_get_modes(struct drm_encoder *encoder,
                drm_mode_connector_update_edid_property(connector, edid);
                n = drm_add_edid_modes(connector, edid);
                priv->is_hdmi_sink = drm_detect_hdmi_monitor(edid);
+
+               /* keep the EDID as ELD for the audio subsystem */
+               drm_edid_to_eld(connector, edid);
+               priv->eld = connector->eld;
+
                kfree(edid);
        }
 
@@ -1210,6 +1234,8 @@ tda998x_encoder_destroy(struct drm_encoder *encoder)
        if (priv->hdmi->irq)
                free_irq(priv->hdmi->irq, priv);
 
+       tda998x_codec_unregister(&priv->hdmi->dev);
+
        if (priv->cec)
                i2c_unregister_device(priv->cec);
        kfree(priv);
@@ -1263,6 +1289,9 @@ tda998x_encoder_init(struct i2c_client *client,
        priv->vip_cntrl_1 = VIP_CNTRL_1_SWAP_C(0) | VIP_CNTRL_1_SWAP_D(1);
        priv->vip_cntrl_2 = VIP_CNTRL_2_SWAP_E(4) | VIP_CNTRL_2_SWAP_F(5);
 
+       priv->params.audio_frame[1] = 1;                /* channels - 1 */
+       priv->params.audio_sample_rate = 48000;         /* 48kHz */
+
        priv->current_page = 0xff;
        priv->hdmi = client;
        /* CEC I2C address is bound to TDA998x I2C address by configuration pins */
@@ -1278,6 +1307,7 @@ tda998x_encoder_init(struct i2c_client *client,
 
        encoder_slave->slave_priv = priv;
        encoder_slave->slave_funcs = &tda998x_encoder_funcs;
+       i2c_set_clientdata(client, priv);
 
        /* wake up the device: */
        cec_write(priv, REG_CEC_ENAMODS,
@@ -1362,6 +1392,9 @@ tda998x_encoder_init(struct i2c_client *client,
        /* enable EDID read irq: */
        reg_set(priv, REG_INT_FLAGS_2, INT_FLAGS_2_EDID_BLK_RD);
 
+       /* register the audio CODEC */
+       tda998x_codec_register(&client->dev);
+
        if (!np)
                return 0;               /* non-DT */
 
diff --git a/drivers/gpu/drm/i2c/tda998x_drv.h b/drivers/gpu/drm/i2c/tda998x_drv.h
new file mode 100644 (file)
index 0000000..e6c8dd5
--- /dev/null
@@ -0,0 +1,32 @@
+/* tda998x private data */
+
+struct tda998x_priv {
+       struct i2c_client *cec;
+       struct i2c_client *hdmi;
+       uint16_t rev;
+       uint8_t current_page;
+       int dpms;
+       bool is_hdmi_sink;
+       u8 vip_cntrl_0;
+       u8 vip_cntrl_1;
+       u8 vip_cntrl_2;
+       struct tda998x_encoder_params params;
+
+       wait_queue_head_t wq_edid;
+       volatile int wq_edid_wait;
+       struct drm_encoder *encoder;
+
+       u8 audio_ports[2];
+       int audio_sample_format;
+       int dai_id;                     /* DAI ID when streaming active */
+
+       u8 *eld;
+
+       struct snd_pcm_hw_constraint_list rate_constraints;
+};
+
+int tda998x_codec_register(struct device *dev);
+void tda998x_codec_unregister(struct device *dev);
+
+void tda998x_audio_start(struct tda998x_priv *priv, int full);
+void tda998x_audio_stop(struct tda998x_priv *priv);
index 3e419d92cf5ab7740b2ae4ac36da2522fcf9a021..31757dff5e91a283ba055deeb22552393957ff09 100644 (file)
@@ -20,6 +20,7 @@ struct tda998x_encoder_params {
        u8 audio_frame[6];
 
        enum {
+               AFMT_NO_AUDIO = 0,
                AFMT_SPDIF,
                AFMT_I2S
        } audio_format;