[alsa-devel] [PATCH v3] ASoC: tda998x: add a codec to the HDMI transmitter
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 --- v3: fix bad rate (Andrew Jackson) v2: check double stream start (Mark Brown) --- .../devicetree/bindings/drm/i2c/tda998x.txt | 13 ++ drivers/gpu/drm/i2c/Makefile | 2 +- drivers/gpu/drm/i2c/tda998x_codec.c | 247 +++++++++++++++++++++ drivers/gpu/drm/i2c/tda998x_drv.c | 74 ++++-- drivers/gpu/drm/i2c/tda998x_drv.h | 32 +++ include/drm/i2c/tda998x.h | 1 + 6 files changed, 348 insertions(+), 21 deletions(-) create mode 100644 drivers/gpu/drm/i2c/tda998x_codec.c create mode 100644 drivers/gpu/drm/i2c/tda998x_drv.h
diff --git a/Documentation/devicetree/bindings/drm/i2c/tda998x.txt b/Documentation/devicetree/bindings/drm/i2c/tda998x.txt index d7df01c..7ce4eac 100644 --- a/Documentation/devicetree/bindings/drm/i2c/tda998x.txt +++ b/Documentation/devicetree/bindings/drm/i2c/tda998x.txt @@ -15,6 +15,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 +33,8 @@ 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>; }; diff --git a/drivers/gpu/drm/i2c/Makefile b/drivers/gpu/drm/i2c/Makefile index 43aa33b..f2d625c 100644 --- a/drivers/gpu/drm/i2c/Makefile +++ b/drivers/gpu/drm/i2c/Makefile @@ -6,5 +6,5 @@ obj-$(CONFIG_DRM_I2C_CH7006) += ch7006.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 index 0000000..e22f974 --- /dev/null +++ b/drivers/gpu/drm/i2c/tda998x_codec.c @@ -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); +} diff --git a/drivers/gpu/drm/i2c/tda998x_drv.c b/drivers/gpu/drm/i2c/tda998x_drv.c index 240c331..7e9f9dc 100644 --- a/drivers/gpu/drm/i2c/tda998x_drv.c +++ b/drivers/gpu/drm/i2c/tda998x_drv.c @@ -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> @@ -28,24 +29,9 @@ #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 @@ -1149,6 +1168,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); }
@@ -1191,6 +1215,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); @@ -1239,10 +1265,15 @@ tda998x_encoder_init(struct i2c_client *client, if (!priv) return -ENOMEM;
+ i2c_set_clientdata(client, priv); + priv->vip_cntrl_0 = VIP_CNTRL_0_SWAP_A(2) | VIP_CNTRL_0_SWAP_B(3); 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; priv->cec = i2c_new_dummy(client->adapter, 0x34); @@ -1340,6 +1371,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 index 0000000..e6c8dd5 --- /dev/null +++ b/drivers/gpu/drm/i2c/tda998x_drv.h @@ -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); diff --git a/include/drm/i2c/tda998x.h b/include/drm/i2c/tda998x.h index 3e419d9..31757df 100644 --- a/include/drm/i2c/tda998x.h +++ b/include/drm/i2c/tda998x.h @@ -20,6 +20,7 @@ struct tda998x_encoder_params { u8 audio_frame[6];
enum { + AFMT_NO_AUDIO = 0, AFMT_SPDIF, AFMT_I2S } audio_format;
On 07/08/14 10:40, Jean-Francois Moine wrote:
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
Shouldn't the patch also capture the current audio rate in tda998x_codec.c:tda_hw_params? Otherwise, when tda998x_drv.c:tda998x_audio_start invokes tda998x_drv.c:tda998x_configure_audio, the value of 'N' will be calculated incorrectly.
So something similar to:
diff --git a/drivers/gpu/drm/i2c/tda998x_codec.c b/drivers/gpu/drm/i2c/tda998x_codec.c index e7f223d..b6c4fb3 100755 --- a/drivers/gpu/drm/i2c/tda998x_codec.c +++ b/drivers/gpu/drm/i2c/tda998x_codec.c @@ -111,6 +111,7 @@ static int tda_hw_params(struct snd_pcm_substream *substream, tda998x_audio_start(priv, 0); return 0; } + priv->params.audio_sample_rate = params_rate(params); priv->params.audio_format = dai->id; priv->audio_sample_format = params_format(params); params.audio_cfg =
Andrew
On Tue, Jul 08, 2014 at 11:40:57AM +0200, Jean-Francois Moine wrote:
.../devicetree/bindings/drm/i2c/tda998x.txt | 13 ++ drivers/gpu/drm/i2c/Makefile | 2 +- drivers/gpu/drm/i2c/tda998x_codec.c | 247 +++++++++++++++++++++ drivers/gpu/drm/i2c/tda998x_drv.c | 74 ++++-- drivers/gpu/drm/i2c/tda998x_drv.h | 32 +++ include/drm/i2c/tda998x.h | 1 +
I'd expect to see the ASoC parts of this appear under sound/soc (even if just a library that the DRM driver links against rather than as a MFD). This should help avoid cross tree issues and surprises when people do API changes.
- audio-ports: one or two values corresponding to entries in
- the audio-port-names property.
What are these values?
- audio-port-names: must contain "i2s", "spdif" entries
- matching entries in the audio-ports property.
So it's mandatory that both I2S and S/PDIF be specified?
The code looks broadly sensible in so far as it
participants (3)
-
Andrew Jackson
-
Jean-Francois Moine
-
Mark Brown