ASoC: fsl-asoc-card: add AC'97 support

Add AC'97 support to fsl-asoc-card using generic
ASoC AC'97 CODEC.

The SSI controller will silently enable any TX
AC'97 slots that have their bits set in SLOTREQ
received from CODEC and then will redirect some
of playback samples there.

That's why it is important to make sure that
any of CODEC playback slots that can pull samples
are set to slots 3/4 (standard PCM playback slots).
Currently, this applies to S/PDIF slots as they
were seen to pull samples sometimes even with
S/PDIF output being disabled.

Signed-off-by: Maciej Szmigiero <mail@maciej.szmigiero.name>
Signed-off-by: Mark Brown <broonie@kernel.org>
diff --git a/sound/soc/fsl/fsl-asoc-card.c b/sound/soc/fsl/fsl-asoc-card.c
index 5aeb6ed..33628a0 100644
--- a/sound/soc/fsl/fsl-asoc-card.c
+++ b/sound/soc/fsl/fsl-asoc-card.c
@@ -14,6 +14,9 @@
 #include <linux/i2c.h>
 #include <linux/module.h>
 #include <linux/of_platform.h>
+#if IS_ENABLED(CONFIG_SND_AC97_CODEC)
+#include <sound/ac97_codec.h>
+#endif
 #include <sound/pcm_params.h>
 #include <sound/soc.h>
 
@@ -115,6 +118,11 @@
 	SND_SOC_DAPM_MIC("DMIC", NULL),
 };
 
+static bool fsl_asoc_card_is_ac97(struct fsl_asoc_card_priv *priv)
+{
+	return priv->dai_fmt == SND_SOC_DAIFMT_AC97;
+}
+
 static int fsl_asoc_card_hw_params(struct snd_pcm_substream *substream,
 				   struct snd_pcm_hw_params *params)
 {
@@ -133,7 +141,9 @@
 	 * set_bias_level(), bypass the remaining settings in hw_params().
 	 * Note: (dai_fmt & CBM_CFM) includes CBM_CFM and CBM_CFS.
 	 */
-	if (priv->card.set_bias_level && priv->dai_fmt & SND_SOC_DAIFMT_CBM_CFM)
+	if ((priv->card.set_bias_level &&
+	     priv->dai_fmt & SND_SOC_DAIFMT_CBM_CFM) ||
+	    fsl_asoc_card_is_ac97(priv))
 		return 0;
 
 	/* Specific configurations of DAIs starts from here */
@@ -300,7 +310,7 @@
 	ext_port--;
 
 	/*
-	 * Use asynchronous mode (6 wires) for all cases.
+	 * Use asynchronous mode (6 wires) for all cases except AC97.
 	 * If only 4 wires are needed, just set SSI into
 	 * synchronous mode and enable 4 PADs in IOMUX.
 	 */
@@ -346,15 +356,30 @@
 			   IMX_AUDMUX_V2_PTCR_TCLKDIR;
 		break;
 	default:
-		return -EINVAL;
+		if (!fsl_asoc_card_is_ac97(priv))
+			return -EINVAL;
+	}
+
+	if (fsl_asoc_card_is_ac97(priv)) {
+		int_ptcr = IMX_AUDMUX_V2_PTCR_SYN |
+			   IMX_AUDMUX_V2_PTCR_TCSEL(ext_port) |
+			   IMX_AUDMUX_V2_PTCR_TCLKDIR;
+		ext_ptcr = IMX_AUDMUX_V2_PTCR_SYN |
+			   IMX_AUDMUX_V2_PTCR_TFSEL(int_port) |
+			   IMX_AUDMUX_V2_PTCR_TFSDIR;
 	}
 
 	/* Asynchronous mode can not be set along with RCLKDIR */
-	ret = imx_audmux_v2_configure_port(int_port, 0,
-					   IMX_AUDMUX_V2_PDCR_RXDSEL(ext_port));
-	if (ret) {
-		dev_err(dev, "audmux internal port setup failed\n");
-		return ret;
+	if (!fsl_asoc_card_is_ac97(priv)) {
+		unsigned int pdcr =
+				IMX_AUDMUX_V2_PDCR_RXDSEL(ext_port);
+
+		ret = imx_audmux_v2_configure_port(int_port, 0,
+						   pdcr);
+		if (ret) {
+			dev_err(dev, "audmux internal port setup failed\n");
+			return ret;
+		}
 	}
 
 	ret = imx_audmux_v2_configure_port(int_port, int_ptcr,
@@ -364,11 +389,16 @@
 		return ret;
 	}
 
-	ret = imx_audmux_v2_configure_port(ext_port, 0,
-					   IMX_AUDMUX_V2_PDCR_RXDSEL(int_port));
-	if (ret) {
-		dev_err(dev, "audmux external port setup failed\n");
-		return ret;
+	if (!fsl_asoc_card_is_ac97(priv)) {
+		unsigned int pdcr =
+				IMX_AUDMUX_V2_PDCR_RXDSEL(int_port);
+
+		ret = imx_audmux_v2_configure_port(ext_port, 0,
+						   pdcr);
+		if (ret) {
+			dev_err(dev, "audmux external port setup failed\n");
+			return ret;
+		}
 	}
 
 	ret = imx_audmux_v2_configure_port(ext_port, ext_ptcr,
@@ -389,6 +419,23 @@
 	struct device *dev = card->dev;
 	int ret;
 
+	if (fsl_asoc_card_is_ac97(priv)) {
+#if IS_ENABLED(CONFIG_SND_AC97_CODEC)
+		struct snd_soc_codec *codec = card->rtd[0].codec;
+		struct snd_ac97 *ac97 = snd_soc_codec_get_drvdata(codec);
+
+		/*
+		 * Use slots 3/4 for S/PDIF so SSI won't try to enable
+		 * other slots and send some samples there
+		 * due to SLOTREQ bits for S/PDIF received from codec
+		 */
+		snd_ac97_update_bits(ac97, AC97_EXTENDED_STATUS,
+				     AC97_EA_SPSA_SLOT_MASK, AC97_EA_SPSA_3_4);
+#endif
+
+		return 0;
+	}
+
 	ret = snd_soc_dai_set_sysclk(codec_dai, codec_priv->mclk_id,
 				     codec_priv->mclk_freq, SND_SOC_CLOCK_IN);
 	if (ret) {
@@ -407,7 +454,6 @@
 	struct platform_device *cpu_pdev;
 	struct fsl_asoc_card_priv *priv;
 	struct i2c_client *codec_dev;
-	struct clk *codec_clk;
 	const char *codec_dai_name;
 	u32 width;
 	int ret;
@@ -420,9 +466,8 @@
 	/* Give a chance to old DT binding */
 	if (!cpu_np)
 		cpu_np = of_parse_phandle(np, "ssi-controller", 0);
-	codec_np = of_parse_phandle(np, "audio-codec", 0);
-	if (!cpu_np || !codec_np) {
-		dev_err(&pdev->dev, "phandle missing or invalid\n");
+	if (!cpu_np) {
+		dev_err(&pdev->dev, "CPU phandle missing or invalid\n");
 		ret = -EINVAL;
 		goto fail;
 	}
@@ -434,22 +479,24 @@
 		goto fail;
 	}
 
-	codec_dev = of_find_i2c_device_by_node(codec_np);
-	if (!codec_dev) {
-		dev_err(&pdev->dev, "failed to find codec platform device\n");
-		ret = -EINVAL;
-		goto fail;
-	}
+	codec_np = of_parse_phandle(np, "audio-codec", 0);
+	if (codec_np)
+		codec_dev = of_find_i2c_device_by_node(codec_np);
+	else
+		codec_dev = NULL;
 
 	asrc_np = of_parse_phandle(np, "audio-asrc", 0);
 	if (asrc_np)
 		asrc_pdev = of_find_device_by_node(asrc_np);
 
 	/* Get the MCLK rate only, and leave it controlled by CODEC drivers */
-	codec_clk = clk_get(&codec_dev->dev, NULL);
-	if (!IS_ERR(codec_clk)) {
-		priv->codec_priv.mclk_freq = clk_get_rate(codec_clk);
-		clk_put(codec_clk);
+	if (codec_dev) {
+		struct clk *codec_clk = clk_get(&codec_dev->dev, NULL);
+
+		if (!IS_ERR(codec_clk)) {
+			priv->codec_priv.mclk_freq = clk_get_rate(codec_clk);
+			clk_put(codec_clk);
+		}
 	}
 
 	/* Default sample rate and format, will be updated in hw_params() */
@@ -486,11 +533,21 @@
 		priv->codec_priv.fll_id = WM8960_SYSCLK_AUTO;
 		priv->codec_priv.pll_id = WM8960_SYSCLK_AUTO;
 		priv->dai_fmt |= SND_SOC_DAIFMT_CBM_CFM;
+	} else if (of_device_is_compatible(np, "fsl,imx-audio-ac97")) {
+		codec_dai_name = "ac97-hifi";
+		priv->card.set_bias_level = NULL;
+		priv->dai_fmt = SND_SOC_DAIFMT_AC97;
 	} else {
 		dev_err(&pdev->dev, "unknown Device Tree compatible\n");
 		return -EINVAL;
 	}
 
+	if (!fsl_asoc_card_is_ac97(priv) && !codec_dev) {
+		dev_err(&pdev->dev, "failed to find codec device\n");
+		ret = -EINVAL;
+		goto asrc_fail;
+	}
+
 	/* Common settings for corresponding Freescale CPU DAI driver */
 	if (strstr(cpu_np->name, "ssi")) {
 		/* Only SSI needs to configure AUDMUX */
@@ -507,7 +564,9 @@
 		priv->cpu_priv.sysclk_id[0] = FSL_SAI_CLK_MAST1;
 	}
 
-	sprintf(priv->name, "%s-audio", codec_dev->name);
+	snprintf(priv->name, sizeof(priv->name), "%s-audio",
+		 fsl_asoc_card_is_ac97(priv) ? "ac97" :
+		 codec_dev->name);
 
 	/* Initialize sound card */
 	priv->pdev = pdev;
@@ -531,8 +590,26 @@
 
 	/* Normal DAI Link */
 	priv->dai_link[0].cpu_of_node = cpu_np;
-	priv->dai_link[0].codec_of_node = codec_np;
 	priv->dai_link[0].codec_dai_name = codec_dai_name;
+
+	if (!fsl_asoc_card_is_ac97(priv))
+		priv->dai_link[0].codec_of_node = codec_np;
+	else {
+		u32 idx;
+
+		ret = of_property_read_u32(cpu_np, "cell-index", &idx);
+		if (ret) {
+			dev_err(&pdev->dev,
+				"cannot get CPU index property\n");
+			goto asrc_fail;
+		}
+
+		priv->dai_link[0].codec_name =
+				devm_kasprintf(&pdev->dev, GFP_KERNEL,
+					       "ac97-codec.%u",
+					       (unsigned int)idx);
+	}
+
 	priv->dai_link[0].platform_of_node = cpu_np;
 	priv->dai_link[0].dai_fmt = priv->dai_fmt;
 	priv->card.num_links = 1;
@@ -543,6 +620,8 @@
 		priv->dai_link[1].platform_of_node = asrc_np;
 		priv->dai_link[2].codec_dai_name = codec_dai_name;
 		priv->dai_link[2].codec_of_node = codec_np;
+		priv->dai_link[2].codec_name =
+				priv->dai_link[0].codec_name;
 		priv->dai_link[2].cpu_of_node = cpu_np;
 		priv->dai_link[2].dai_fmt = priv->dai_fmt;
 		priv->card.num_links = 3;
@@ -578,14 +657,15 @@
 
 asrc_fail:
 	of_node_put(asrc_np);
-fail:
 	of_node_put(codec_np);
+fail:
 	of_node_put(cpu_np);
 
 	return ret;
 }
 
 static const struct of_device_id fsl_asoc_card_dt_ids[] = {
+	{ .compatible = "fsl,imx-audio-ac97", },
 	{ .compatible = "fsl,imx-audio-cs42888", },
 	{ .compatible = "fsl,imx-audio-sgtl5000", },
 	{ .compatible = "fsl,imx-audio-wm8962", },