Patchwork [5/7] ASoC: tegra: add ac97 host driver

login
register
mail settings
Submitter Lucas Stach
Date Dec. 19, 2012, 11:17 p.m.
Message ID <1355959056-6009-5-git-send-email-dev@lynxeye.de>
Download mbox | patch
Permalink /patch/207545/
State Not Applicable, archived
Headers show

Comments

Lucas Stach - Dec. 19, 2012, 11:17 p.m.
This adds the driver for the Tegra 2x AC97 host controller.

Signed-off-by: Marcel Ziswiler <marcel.ziswiler@toradex.com>
Signed-off-by: Lucas Stach <dev@lynxeye.de>
---
 sound/soc/tegra/Kconfig        |   8 +
 sound/soc/tegra/Makefile       |   2 +
 sound/soc/tegra/tegra20_ac97.c | 479 +++++++++++++++++++++++++++++++++++++++++
 sound/soc/tegra/tegra20_ac97.h |  95 ++++++++
 4 Dateien geändert, 584 Zeilen hinzugefügt(+)
 create mode 100644 sound/soc/tegra/tegra20_ac97.c
 create mode 100644 sound/soc/tegra/tegra20_ac97.h
Stephen Warren - Dec. 20, 2012, 7:44 p.m.
On 12/19/2012 04:17 PM, Lucas Stach wrote:
> This adds the driver for the Tegra 2x AC97 host controller.

The AC'97 DT binding file should really be added as part of this patch.

I'm not at all familiar with AC'97, but this mostly looks fine. I have a
bunch of comments below though, mostly for my own understanding.

> diff --git a/sound/soc/tegra/Kconfig b/sound/soc/tegra/Kconfig

> +config SND_SOC_TEGRA20_AC97
> +	tristate
> +	depends on SND_SOC_TEGRA && ARCH_TEGRA_2x_SOC

This should also select SND_SOC_TEGRA20_DAS, just like the I2S driver.

> diff --git a/sound/soc/tegra/tegra20_ac97.c b/sound/soc/tegra/tegra20_ac97.c

> +static struct tegra20_ac97 *workdata;

Why not put that into the device's drvdata? Oh, is there no way to stash
driver-private data into the struct snd_ac97?

> +static void tegra20_ac97_codec_reset(struct snd_ac97 *ac97)
> +{
> +	u32 readback;
> +	unsigned long timeout;
> +
> +	/* reset line is not driven by DAC pad group, have to toggle GPIO */
> +	gpio_set_value(workdata->reset_gpio, 0);
> +	udelay(2);

I'm not quite sure what that implies. Might some (Tegra) HW designs use
a dedicated signal from the AC'97 HW to reset the CODEC and others not?
That would make the GPIO optional. Or, does the comment mean that Tegra
doesn't ever have a dedicated CODEC reset signal, so we always have to
use a GPIO? If so, this comment might be more appropriate inside probe()
where I asked my related questions.

> +static void tegra20_ac97_codec_warm_reset(struct snd_ac97 *ac97)
> +{
> +	u32 readback;
> +	unsigned long timeout;
> +
> +	/*
> +	 * although sync line is driven by the DAC pad group warm reset using
> +	 * the controller cmd is not working, have to toggle sync line
> +	 * manually.
> +	 */
> +	gpio_request(workdata->sync_gpio, "codec-sync");

Hmm. There's an AC'97 command to reset the CODEC and we don't implement
it? Uggh.

> +static const struct snd_soc_dai_ops tegra20_ac97_dai_ops = {
> +	.trigger	= tegra20_ac97_trigger,
> +};

No .set_fmt or .hw_params? Does the AC'97 ASoC core code handle that
somehow? I guess that's what soc_ac97_ops is for?

> +static struct snd_soc_dai_driver tegra20_ac97_dai = {
> +	.name = "tegra-ac97-pcm",
> +	.ac97_control = 1,
> +	.probe = tegra20_ac97_probe,
> +	.playback = {
> +		.stream_name = "PCM Playback",

Out of curiosity, why "PCM Playback" not just "Playback"?

> +static bool tegra20_ac97_volatile_reg(struct device *dev, unsigned int reg)
> +{
> +	switch (reg) {
> +	case TEGRA20_AC97_CMD:
...
> +		return true;

CMD is volatile; did we put status bits in there?

> +static int tegra20_ac97_platform_probe(struct platform_device *pdev)

> +	ac97->reset_gpio = of_get_named_gpio(pdev->dev.of_node,
> +					     "nvidia,codec-reset-gpio", 0);
> +	if (gpio_is_valid(ac97->reset_gpio)) {
> +		ret = devm_gpio_request_one(&pdev->dev, ac97->reset_gpio,
> +					    GPIOF_OUT_INIT_HIGH, "codec-reset");

Shouldn't this get the flags from the GPIO specifier, and deal with
active-high/active-low resets, or is the polarity set by the AC'97 spec?

> +		if (ret) {
> +			dev_err(&pdev->dev, "could not get codec-reset GPIO\n");
> +			goto err_clk_put;
> +		}
> +	} else {
> +		dev_err(&pdev->dev, "no codec-reset GPIO supplied\n");
> +		goto err_clk_put;
> +	}

Is a reset GPIO necessarily required? What if a board arranged to reset
the CODEC during power-on-reset, and there was no software control over
the reset?

> +	ac97->sync_gpio = of_get_named_gpio(pdev->dev.of_node,
> +					    "nvidia,codec-sync-gpio", 0);
> +	if (!gpio_is_valid(ac97->sync_gpio)) {
> +		dev_err(&pdev->dev, "no codec-sync GPIO supplied\n");
> +		goto err_clk_put;
> +	}

I don't know what this is, so I'll ask if it's strictly required too.

> +	ret = snd_soc_register_dais(&pdev->dev, &tegra20_ac97_dai, 1);

snd_soc_register_dai() would be marginally simpler.

> +	ret = clk_prepare_enable(ac97->clk_ac97);
> +	if (ret) {
> +		dev_err(&pdev->dev, "clk_enable failed: %d\n", ret);
> +		goto err_asoc_utils_fini;
> +	}

Can you do runtime PM instead? See the I2S driver for an example.

> +MODULE_LICENSE("GPL");

"GPL v2"

--
To unsubscribe from this list: send the line "unsubscribe linux-tegra" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Lucas Stach - Dec. 20, 2012, 8:10 p.m.
Am Donnerstag, den 20.12.2012, 12:44 -0700 schrieb Stephen Warren:
> On 12/19/2012 04:17 PM, Lucas Stach wrote:
> > This adds the driver for the Tegra 2x AC97 host controller.
> 
> The AC'97 DT binding file should really be added as part of this patch.
> 
Ok, will squash this in.

> I'm not at all familiar with AC'97, but this mostly looks fine. I have a
> bunch of comments below though, mostly for my own understanding.
> 
> > diff --git a/sound/soc/tegra/Kconfig b/sound/soc/tegra/Kconfig
> 
> > +config SND_SOC_TEGRA20_AC97
> > +	tristate
> > +	depends on SND_SOC_TEGRA && ARCH_TEGRA_2x_SOC
> 
> This should also select SND_SOC_TEGRA20_DAS, just like the I2S driver.
> 
> > diff --git a/sound/soc/tegra/tegra20_ac97.c b/sound/soc/tegra/tegra20_ac97.c
> 
> > +static struct tegra20_ac97 *workdata;
> 
> Why not put that into the device's drvdata? Oh, is there no way to stash
> driver-private data into the struct snd_ac97?
> 
I'll have to look up if I can get this right when really using ac97_bus,
which might be needed for the hotplug stuff Mark mentioned.

> > +static void tegra20_ac97_codec_reset(struct snd_ac97 *ac97)
> > +{
> > +	u32 readback;
> > +	unsigned long timeout;
> > +
> > +	/* reset line is not driven by DAC pad group, have to toggle GPIO */
> > +	gpio_set_value(workdata->reset_gpio, 0);
> > +	udelay(2);
> 
> I'm not quite sure what that implies. Might some (Tegra) HW designs use
> a dedicated signal from the AC'97 HW to reset the CODEC and others not?
> That would make the GPIO optional. Or, does the comment mean that Tegra
> doesn't ever have a dedicated CODEC reset signal, so we always have to
> use a GPIO? If so, this comment might be more appropriate inside probe()
> where I asked my related questions.
> 
We always have to use a GPIO for that. CODEC reset is not part of the
AC97 digital link and therefore not connected to the controller
directly.

> > +static void tegra20_ac97_codec_warm_reset(struct snd_ac97 *ac97)
> > +{
> > +	u32 readback;
> > +	unsigned long timeout;
> > +
> > +	/*
> > +	 * although sync line is driven by the DAC pad group warm reset using
> > +	 * the controller cmd is not working, have to toggle sync line
> > +	 * manually.
> > +	 */
> > +	gpio_request(workdata->sync_gpio, "codec-sync");
> 
> Hmm. There's an AC'97 command to reset the CODEC and we don't implement
> it? Uggh.
> 
As far as I could figure from the downstream register doc there is a
command implemented in the host controller that's supposed to do the
warm reset, but in my testing it didn't work. Sadly I have not received
any documentation about the host controller from NVIDIA up until now, so
I don't now if we could make this work somehow. Bug Submission ID#:
196075

> > +static const struct snd_soc_dai_ops tegra20_ac97_dai_ops = {
> > +	.trigger	= tegra20_ac97_trigger,
> > +};
> 
> No .set_fmt or .hw_params? Does the AC'97 ASoC core code handle that
> somehow? I guess that's what soc_ac97_ops is for?
> 
I don't think it's needed. The codec code handles those two.
> > +static struct snd_soc_dai_driver tegra20_ac97_dai = {
> > +	.name = "tegra-ac97-pcm",
> > +	.ac97_control = 1,
> > +	.probe = tegra20_ac97_probe,
> > +	.playback = {
> > +		.stream_name = "PCM Playback",
> 
> Out of curiosity, why "PCM Playback" not just "Playback"?
> 
AC97 provides a second playback DAI named "AUX Playback". I want to add
this later, but could not test it until now so excluded it from the
patch. To provide clear distinction between the two, I would like to
call this one PCM.

> > +static bool tegra20_ac97_volatile_reg(struct device *dev, unsigned int reg)
> > +{
> > +	switch (reg) {
> > +	case TEGRA20_AC97_CMD:
> ...
> > +		return true;
> 
> CMD is volatile; did we put status bits in there?
> 
No, this is an oversight from my debugging. Will remove this.

> > +static int tegra20_ac97_platform_probe(struct platform_device *pdev)
> 
> > +	ac97->reset_gpio = of_get_named_gpio(pdev->dev.of_node,
> > +					     "nvidia,codec-reset-gpio", 0);
> > +	if (gpio_is_valid(ac97->reset_gpio)) {
> > +		ret = devm_gpio_request_one(&pdev->dev, ac97->reset_gpio,
> > +					    GPIOF_OUT_INIT_HIGH, "codec-reset");
> 
> Shouldn't this get the flags from the GPIO specifier, and deal with
> active-high/active-low resets, or is the polarity set by the AC'97 spec?
> 
Polarity is mandated by the AC97 spec.

> > +		if (ret) {
> > +			dev_err(&pdev->dev, "could not get codec-reset GPIO\n");
> > +			goto err_clk_put;
> > +		}
> > +	} else {
> > +		dev_err(&pdev->dev, "no codec-reset GPIO supplied\n");
> > +		goto err_clk_put;
> > +	}
> 
> Is a reset GPIO necessarily required? What if a board arranged to reset
> the CODEC during power-on-reset, and there was no software control over
> the reset?
> 
Codec reset is needed after refclock is stable. Although it might be
possible to omit this line, the AC97 spec assumes a reset line connected
to the host, so I don't think any implementation would omit this.

> > +	ac97->sync_gpio = of_get_named_gpio(pdev->dev.of_node,
> > +					    "nvidia,codec-sync-gpio", 0);
> > +	if (!gpio_is_valid(ac97->sync_gpio)) {
> > +		dev_err(&pdev->dev, "no codec-sync GPIO supplied\n");
> > +		goto err_clk_put;
> > +	}
> 
> I don't know what this is, so I'll ask if it's strictly required too.
> 
Until we manage to get the host controller warm reset command working
this is fixed to be the _FS line of the DAP connected to the codec, so
strictly required.

> > +	ret = snd_soc_register_dais(&pdev->dev, &tegra20_ac97_dai, 1);
> 
> snd_soc_register_dai() would be marginally simpler.
> 
> > +	ret = clk_prepare_enable(ac97->clk_ac97);
> > +	if (ret) {
> > +		dev_err(&pdev->dev, "clk_enable failed: %d\n", ret);
> > +		goto err_asoc_utils_fini;
> > +	}
> 
> Can you do runtime PM instead? See the I2S driver for an example.
> 
Not really possible as for AC97 host and codec are intertwined. The
codec drivers assume the host controller is always up to service
commands. Maybe this could be fixed by using ac97_bus properly.

> > +MODULE_LICENSE("GPL");
> 
> "GPL v2"
> 


--
To unsubscribe from this list: send the line "unsubscribe linux-tegra" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Stephen Warren - Dec. 20, 2012, 8:19 p.m.
On 12/20/2012 01:10 PM, Lucas Stach wrote:
> Am Donnerstag, den 20.12.2012, 12:44 -0700 schrieb Stephen Warren:
>> On 12/19/2012 04:17 PM, Lucas Stach wrote:
>>> This adds the driver for the Tegra 2x AC97 host controller.

>>> +static void tegra20_ac97_codec_warm_reset(struct snd_ac97 *ac97)
>>> +{
>>> +	u32 readback;
>>> +	unsigned long timeout;
>>> +
>>> +	/*
>>> +	 * although sync line is driven by the DAC pad group warm reset using
>>> +	 * the controller cmd is not working, have to toggle sync line
>>> +	 * manually.
>>> +	 */
>>> +	gpio_request(workdata->sync_gpio, "codec-sync");
>>
>> Hmm. There's an AC'97 command to reset the CODEC and we don't implement
>> it? Uggh.
>
> As far as I could figure from the downstream register doc there is a
> command implemented in the host controller that's supposed to do the
> warm reset, but in my testing it didn't work. Sadly I have not received
> any documentation about the host controller from NVIDIA up until now, so
> I don't now if we could make this work somehow. Bug Submission ID#:
> 196075

What system was that bug submitted into? It's not a regular NVIDIA
internal bug number. I'll see who I can ask about this feature...

>>> +	ac97->sync_gpio = of_get_named_gpio(pdev->dev.of_node,
>>> +					    "nvidia,codec-sync-gpio", 0);
>>> +	if (!gpio_is_valid(ac97->sync_gpio)) {
>>> +		dev_err(&pdev->dev, "no codec-sync GPIO supplied\n");
>>> +		goto err_clk_put;
>>> +	}
>>
>> I don't know what this is, so I'll ask if it's strictly required too.
>
> Until we manage to get the host controller warm reset command working
> this is fixed to be the _FS line of the DAP connected to the codec, so
> strictly required.

I'd like to investigate whether the warm reset command can be made to
work before actually committing to a DT binding that requires this GPIO
to be specified; the DT binding is supposed to be basically static.
--
To unsubscribe from this list: send the line "unsubscribe linux-tegra" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Mark Brown - Dec. 20, 2012, 8:23 p.m.
On Thu, Dec 20, 2012 at 12:44:24PM -0700, Stephen Warren wrote:
> On 12/19/2012 04:17 PM, Lucas Stach wrote:

> > +	/* reset line is not driven by DAC pad group, have to toggle GPIO */
> > +	gpio_set_value(workdata->reset_gpio, 0);
> > +	udelay(2);

> I'm not quite sure what that implies. Might some (Tegra) HW designs use
> a dedicated signal from the AC'97 HW to reset the CODEC and others not?
> That would make the GPIO optional. Or, does the comment mean that Tegra
> doesn't ever have a dedicated CODEC reset signal, so we always have to
> use a GPIO? If so, this comment might be more appropriate inside probe()
> where I asked my related questions.

The traditional approach, pioneered by almost everyone who made an AC'97
controller, is to fill the thing with bugs.  Failing to drive the reset
line correctly is just one of the common bugs.

> > +static const struct snd_soc_dai_ops tegra20_ac97_dai_ops = {
> > +	.trigger	= tegra20_ac97_trigger,
> > +};

> No .set_fmt or .hw_params? Does the AC'97 ASoC core code handle that
> somehow? I guess that's what soc_ac97_ops is for?

There should be no controller side configurations for these things.
AC'97 defines the clocking format completely and variable sample rates
are done by the CODEC flagging when data is present in a given timeslot.

> > +	ac97->reset_gpio = of_get_named_gpio(pdev->dev.of_node,
> > +					     "nvidia,codec-reset-gpio", 0);
> > +	if (gpio_is_valid(ac97->reset_gpio)) {
> > +		ret = devm_gpio_request_one(&pdev->dev, ac97->reset_gpio,
> > +					    GPIOF_OUT_INIT_HIGH, "codec-reset");

> Shouldn't this get the flags from the GPIO specifier, and deal with
> active-high/active-low resets, or is the polarity set by the AC'97 spec?

Spec.
Lucas Stach - Dec. 20, 2012, 8:32 p.m.
Am Donnerstag, den 20.12.2012, 13:19 -0700 schrieb Stephen Warren:
> On 12/20/2012 01:10 PM, Lucas Stach wrote:
> > Am Donnerstag, den 20.12.2012, 12:44 -0700 schrieb Stephen Warren:
> >> On 12/19/2012 04:17 PM, Lucas Stach wrote:
> >>> This adds the driver for the Tegra 2x AC97 host controller.
> 
> >>> +static void tegra20_ac97_codec_warm_reset(struct snd_ac97 *ac97)
> >>> +{
> >>> +	u32 readback;
> >>> +	unsigned long timeout;
> >>> +
> >>> +	/*
> >>> +	 * although sync line is driven by the DAC pad group warm reset using
> >>> +	 * the controller cmd is not working, have to toggle sync line
> >>> +	 * manually.
> >>> +	 */
> >>> +	gpio_request(workdata->sync_gpio, "codec-sync");
> >>
> >> Hmm. There's an AC'97 command to reset the CODEC and we don't implement
> >> it? Uggh.
> >
> > As far as I could figure from the downstream register doc there is a
> > command implemented in the host controller that's supposed to do the
> > warm reset, but in my testing it didn't work. Sadly I have not received
> > any documentation about the host controller from NVIDIA up until now, so
> > I don't now if we could make this work somehow. Bug Submission ID#:
> > 196075
> 
> What system was that bug submitted into? It's not a regular NVIDIA
> internal bug number. I'll see who I can ask about this feature...
> 
Submitted through the Tegra Registered Developer Program webpage. I got
a reply from someone saying they are looking if they can get anything
out, but haven't heard back until now.

> >>> +	ac97->sync_gpio = of_get_named_gpio(pdev->dev.of_node,
> >>> +					    "nvidia,codec-sync-gpio", 0);
> >>> +	if (!gpio_is_valid(ac97->sync_gpio)) {
> >>> +		dev_err(&pdev->dev, "no codec-sync GPIO supplied\n");
> >>> +		goto err_clk_put;
> >>> +	}
> >>
> >> I don't know what this is, so I'll ask if it's strictly required too.
> >
> > Until we manage to get the host controller warm reset command working
> > this is fixed to be the _FS line of the DAP connected to the codec, so
> > strictly required.
> 
> I'd like to investigate whether the warm reset command can be made to
> work before actually committing to a DT binding that requires this GPIO
> to be specified; the DT binding is supposed to be basically static.
> 
Ok with me. We still have plenty of time left for the 3.9 merge window
and that's what I'm targeting with those patches.


--
To unsubscribe from this list: send the line "unsubscribe linux-tegra" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Patch

diff --git a/sound/soc/tegra/Kconfig b/sound/soc/tegra/Kconfig
index 19e5fe7..763c68d 100644
--- a/sound/soc/tegra/Kconfig
+++ b/sound/soc/tegra/Kconfig
@@ -6,6 +6,14 @@  config SND_SOC_TEGRA
 	help
 	  Say Y or M here if you want support for SoC audio on Tegra.
 
+config SND_SOC_TEGRA20_AC97
+	tristate
+	depends on SND_SOC_TEGRA && ARCH_TEGRA_2x_SOC
+	help
+	  Say Y or M if you want to add support for codecs attached to the
+	  Tegra20 AC97 interface. You will also need to select the individual
+	  machine drivers to support below.
+
 config SND_SOC_TEGRA20_DAS
 	tristate
 	depends on SND_SOC_TEGRA && ARCH_TEGRA_2x_SOC
diff --git a/sound/soc/tegra/Makefile b/sound/soc/tegra/Makefile
index 391e78a..02513d9 100644
--- a/sound/soc/tegra/Makefile
+++ b/sound/soc/tegra/Makefile
@@ -1,6 +1,7 @@ 
 # Tegra platform Support
 snd-soc-tegra-pcm-objs := tegra_pcm.o
 snd-soc-tegra-utils-objs += tegra_asoc_utils.o
+snd-soc-tegra20-ac97-objs := tegra20_ac97.o
 snd-soc-tegra20-das-objs := tegra20_das.o
 snd-soc-tegra20-i2s-objs := tegra20_i2s.o
 snd-soc-tegra20-spdif-objs := tegra20_spdif.o
@@ -9,6 +10,7 @@  snd-soc-tegra30-i2s-objs := tegra30_i2s.o
 
 obj-$(CONFIG_SND_SOC_TEGRA) += snd-soc-tegra-pcm.o
 obj-$(CONFIG_SND_SOC_TEGRA) += snd-soc-tegra-utils.o
+obj-$(CONFIG_SND_SOC_TEGRA20_AC97) += snd-soc-tegra20-ac97.o
 obj-$(CONFIG_SND_SOC_TEGRA20_DAS) += snd-soc-tegra20-das.o
 obj-$(CONFIG_SND_SOC_TEGRA20_I2S) += snd-soc-tegra20-i2s.o
 obj-$(CONFIG_SND_SOC_TEGRA20_SPDIF) += snd-soc-tegra20-spdif.o
diff --git a/sound/soc/tegra/tegra20_ac97.c b/sound/soc/tegra/tegra20_ac97.c
new file mode 100644
index 0000000..a2f71cf
--- /dev/null
+++ b/sound/soc/tegra/tegra20_ac97.c
@@ -0,0 +1,479 @@ 
+/*
+ * tegra20_ac97.c - Tegra20 AC97 platform driver
+ *
+ * Copyright (c) 2012 Lucas Stach <dev@lynxeye.de>
+ *
+ * Partly based on code copyright/by:
+ *
+ * Copyright (c) 2011,2012 Toradex Inc.
+ *
+ * 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ */
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/gpio.h>
+#include <linux/io.h>
+#include <linux/jiffies.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_gpio.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <sound/core.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+
+#include "tegra_asoc_utils.h"
+#include "tegra20_ac97.h"
+
+#define DRV_NAME "tegra20-ac97"
+
+static struct tegra20_ac97 *workdata;
+
+static void tegra20_ac97_codec_reset(struct snd_ac97 *ac97)
+{
+	u32 readback;
+	unsigned long timeout;
+
+	/* reset line is not driven by DAC pad group, have to toggle GPIO */
+	gpio_set_value(workdata->reset_gpio, 0);
+	udelay(2);
+
+	gpio_set_value(workdata->reset_gpio, 1);
+	udelay(2);
+
+	timeout = jiffies + msecs_to_jiffies(100);
+
+	do {
+		regmap_read(workdata->regmap, TEGRA20_AC97_STATUS1, &readback);
+		if (readback & TEGRA20_AC97_STATUS1_CODEC1_RDY)
+			break;
+		usleep_range(1000, 2000);
+	} while (!time_after(jiffies, timeout));
+}
+
+static void tegra20_ac97_codec_warm_reset(struct snd_ac97 *ac97)
+{
+	u32 readback;
+	unsigned long timeout;
+
+	/*
+	 * although sync line is driven by the DAC pad group warm reset using
+	 * the controller cmd is not working, have to toggle sync line
+	 * manually.
+	 */
+	gpio_request(workdata->sync_gpio, "codec-sync");
+
+	gpio_direction_output(workdata->sync_gpio, 1);
+
+	udelay(2);
+	gpio_set_value(workdata->sync_gpio, 0);
+	udelay(2);
+	gpio_free(workdata->sync_gpio);
+
+	timeout = jiffies + msecs_to_jiffies(100);
+
+	do {
+		regmap_read(workdata->regmap, TEGRA20_AC97_STATUS1, &readback);
+		if (readback & TEGRA20_AC97_STATUS1_CODEC1_RDY)
+			break;
+		usleep_range(1000, 2000);
+	} while (!time_after(jiffies, timeout));
+}
+
+static unsigned short tegra20_ac97_codec_read(struct snd_ac97 *ac97_snd,
+					      unsigned short reg)
+{
+	u32 readback;
+	unsigned long timeout;
+
+	regmap_write(workdata->regmap, TEGRA20_AC97_CMD,
+		     (((reg | 0x80) << TEGRA20_AC97_CMD_CMD_ADDR_SHIFT) &
+		      TEGRA20_AC97_CMD_CMD_ADDR_MASK) |
+		     TEGRA20_AC97_CMD_BUSY);
+
+	timeout = jiffies + msecs_to_jiffies(100);
+
+	do {
+		regmap_read(workdata->regmap, TEGRA20_AC97_STATUS1, &readback);
+		if (readback & TEGRA20_AC97_STATUS1_STA_VALID1)
+			break;
+		usleep_range(1000, 2000);
+	} while (!time_after(jiffies, timeout));
+
+	return ((readback & TEGRA20_AC97_STATUS1_STA_DATA1_MASK) >>
+		TEGRA20_AC97_STATUS1_STA_DATA1_SHIFT);
+}
+
+static void tegra20_ac97_codec_write(struct snd_ac97 *ac97_snd,
+				     unsigned short reg, unsigned short val)
+{
+	u32 readback;
+	unsigned long timeout;
+
+	regmap_write(workdata->regmap, TEGRA20_AC97_CMD,
+		     ((reg << TEGRA20_AC97_CMD_CMD_ADDR_SHIFT) &
+		      TEGRA20_AC97_CMD_CMD_ADDR_MASK) |
+		     ((val << TEGRA20_AC97_CMD_CMD_DATA_SHIFT) &
+		      TEGRA20_AC97_CMD_CMD_DATA_MASK) |
+		     TEGRA20_AC97_CMD_BUSY);
+
+	timeout = jiffies + msecs_to_jiffies(100);
+
+	do {
+		regmap_read(workdata->regmap, TEGRA20_AC97_CMD, &readback);
+		if (!(readback & TEGRA20_AC97_CMD_BUSY))
+			break;
+		usleep_range(1000, 2000);
+	} while (!time_after(jiffies, timeout));
+}
+
+struct snd_ac97_bus_ops soc_ac97_ops = {
+	.read		= tegra20_ac97_codec_read,
+	.write		= tegra20_ac97_codec_write,
+	.reset		= tegra20_ac97_codec_reset,
+	.warm_reset	= tegra20_ac97_codec_warm_reset,
+};
+EXPORT_SYMBOL_GPL(soc_ac97_ops);
+
+static inline void tegra20_ac97_start_playback(struct tegra20_ac97 *ac97)
+{
+	regmap_update_bits(ac97->regmap, TEGRA20_AC97_FIFO1_SCR,
+			   TEGRA20_AC97_FIFO_SCR_PB_QRT_MT_EN,
+			   TEGRA20_AC97_FIFO_SCR_PB_QRT_MT_EN);
+
+	regmap_update_bits(ac97->regmap, TEGRA20_AC97_CTRL,
+			   TEGRA20_AC97_CTRL_PCM_DAC_EN | TEGRA20_AC97_CTRL_STM_EN,
+			   TEGRA20_AC97_CTRL_PCM_DAC_EN | TEGRA20_AC97_CTRL_STM_EN);
+}
+
+static inline void tegra20_ac97_stop_playback(struct tegra20_ac97 *ac97)
+{
+	regmap_update_bits(ac97->regmap, TEGRA20_AC97_FIFO1_SCR,
+			   TEGRA20_AC97_FIFO_SCR_PB_QRT_MT_EN, 0);
+
+	regmap_update_bits(ac97->regmap, TEGRA20_AC97_CTRL,
+			   TEGRA20_AC97_CTRL_PCM_DAC_EN, 0);
+}
+
+static inline void tegra20_ac97_start_capture(struct tegra20_ac97 *ac97)
+{
+	regmap_update_bits(ac97->regmap, TEGRA20_AC97_FIFO1_SCR,
+			   TEGRA20_AC97_FIFO_SCR_REC_FULL_EN,
+			   TEGRA20_AC97_FIFO_SCR_REC_FULL_EN);
+}
+
+static inline void tegra20_ac97_stop_capture(struct tegra20_ac97 *ac97)
+{
+	regmap_update_bits(ac97->regmap, TEGRA20_AC97_FIFO1_SCR,
+			   TEGRA20_AC97_FIFO_SCR_REC_FULL_EN, 0);
+}
+
+static int tegra20_ac97_trigger(struct snd_pcm_substream *substream, int cmd,
+				struct snd_soc_dai *dai)
+{
+	struct tegra20_ac97 *ac97 = snd_soc_dai_get_drvdata(dai);
+
+	switch (cmd) {
+	case SNDRV_PCM_TRIGGER_START:
+	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+	case SNDRV_PCM_TRIGGER_RESUME:
+		if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+			tegra20_ac97_start_playback(ac97);
+		else
+			tegra20_ac97_start_capture(ac97);
+		break;
+	case SNDRV_PCM_TRIGGER_STOP:
+	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+	case SNDRV_PCM_TRIGGER_SUSPEND:
+		if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
+			tegra20_ac97_stop_playback(ac97);
+		else
+			tegra20_ac97_stop_capture(ac97);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static const struct snd_soc_dai_ops tegra20_ac97_dai_ops = {
+	.trigger	= tegra20_ac97_trigger,
+};
+
+static int tegra20_ac97_probe(struct snd_soc_dai *dai)
+{
+	struct tegra20_ac97 *ac97 = snd_soc_dai_get_drvdata(dai);
+
+	dai->capture_dma_data = &ac97->capture_dma_data;
+	dai->playback_dma_data = &ac97->playback_dma_data;
+
+	return 0;
+}
+
+static struct snd_soc_dai_driver tegra20_ac97_dai = {
+	.name = "tegra-ac97-pcm",
+	.ac97_control = 1,
+	.probe = tegra20_ac97_probe,
+	.playback = {
+		.stream_name = "PCM Playback",
+		.channels_min = 2,
+		.channels_max = 2,
+		.rates = SNDRV_PCM_RATE_8000_48000,
+		.formats = SNDRV_PCM_FMTBIT_S16_LE,
+	},
+	.capture = {
+		.stream_name = "PCM Capture",
+		.channels_min = 2,
+		.channels_max = 2,
+		.rates = SNDRV_PCM_RATE_8000_48000,
+		.formats = SNDRV_PCM_FMTBIT_S16_LE,
+	},
+	.ops = &tegra20_ac97_dai_ops,
+};
+
+static bool tegra20_ac97_wr_rd_reg(struct device *dev, unsigned int reg)
+{
+	switch (reg) {
+	case TEGRA20_AC97_CTRL:
+	case TEGRA20_AC97_CMD:
+	case TEGRA20_AC97_STATUS1:
+	case TEGRA20_AC97_FIFO1_SCR:
+	case TEGRA20_AC97_FIFO_TX1:
+	case TEGRA20_AC97_FIFO_RX1:
+		return true;
+	default:
+		break;
+	}
+
+	return false;
+}
+
+static bool tegra20_ac97_volatile_reg(struct device *dev, unsigned int reg)
+{
+	switch (reg) {
+	case TEGRA20_AC97_CMD:
+	case TEGRA20_AC97_STATUS1:
+	case TEGRA20_AC97_FIFO1_SCR:
+	case TEGRA20_AC97_FIFO_TX1:
+	case TEGRA20_AC97_FIFO_RX1:
+		return true;
+	default:
+		break;
+	}
+
+	return false;
+}
+
+static bool tegra20_ac97_precious_reg(struct device *dev, unsigned int reg)
+{
+	switch (reg) {
+	case TEGRA20_AC97_FIFO_TX1:
+	case TEGRA20_AC97_FIFO_RX1:
+		return true;
+	default:
+		break;
+	}
+
+	return false;
+}
+
+static const struct regmap_config tegra20_ac97_regmap_config = {
+	.reg_bits = 32,
+	.reg_stride = 4,
+	.val_bits = 32,
+	.max_register = TEGRA20_AC97_FIFO_RX1,
+	.writeable_reg = tegra20_ac97_wr_rd_reg,
+	.readable_reg = tegra20_ac97_wr_rd_reg,
+	.volatile_reg = tegra20_ac97_volatile_reg,
+	.precious_reg = tegra20_ac97_precious_reg,
+	.cache_type = REGCACHE_RBTREE,
+};
+
+static int tegra20_ac97_platform_probe(struct platform_device *pdev)
+{
+	struct tegra20_ac97 *ac97;
+	struct resource *mem, *memregion;
+	u32 of_dma[2];
+	void __iomem *regs;
+	int ret = 0;
+
+	ac97 = devm_kzalloc(&pdev->dev, sizeof(struct tegra20_ac97),
+			    GFP_KERNEL);
+	if (!ac97) {
+		dev_err(&pdev->dev, "Can't allocate tegra20_ac97\n");
+		ret = -ENOMEM;
+		goto err;
+	}
+	dev_set_drvdata(&pdev->dev, ac97);
+
+	ac97->clk_ac97 = clk_get(&pdev->dev, NULL);
+	if (IS_ERR(ac97->clk_ac97)) {
+		dev_err(&pdev->dev, "Can't retrieve ac97 clock\n");
+		ret = PTR_ERR(ac97->clk_ac97);
+		goto err;
+	}
+
+	mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	if (!mem) {
+		dev_err(&pdev->dev, "No memory resource\n");
+		ret = -ENODEV;
+		goto err_clk_put;
+	}
+
+	memregion = devm_request_mem_region(&pdev->dev, mem->start,
+					    resource_size(mem), DRV_NAME);
+	if (!memregion) {
+		dev_err(&pdev->dev, "Memory region already claimed\n");
+		ret = -EBUSY;
+		goto err_clk_put;
+	}
+
+	regs = devm_ioremap(&pdev->dev, mem->start, resource_size(mem));
+	if (!regs) {
+		dev_err(&pdev->dev, "ioremap failed\n");
+		ret = -ENOMEM;
+		goto err_clk_put;
+	}
+
+	ac97->regmap = devm_regmap_init_mmio(&pdev->dev, regs,
+					    &tegra20_ac97_regmap_config);
+	if (IS_ERR(ac97->regmap)) {
+		dev_err(&pdev->dev, "regmap init failed\n");
+		ret = PTR_ERR(ac97->regmap);
+		goto err_clk_put;
+	}
+
+	if (of_property_read_u32_array(pdev->dev.of_node,
+				       "nvidia,dma-request-selector",
+				       of_dma, 2) < 0) {
+		dev_err(&pdev->dev, "No DMA resource\n");
+		ret = -ENODEV;
+		goto err_clk_put;
+	}
+
+	ac97->reset_gpio = of_get_named_gpio(pdev->dev.of_node,
+					     "nvidia,codec-reset-gpio", 0);
+	if (gpio_is_valid(ac97->reset_gpio)) {
+		ret = devm_gpio_request_one(&pdev->dev, ac97->reset_gpio,
+					    GPIOF_OUT_INIT_HIGH, "codec-reset");
+		if (ret) {
+			dev_err(&pdev->dev, "could not get codec-reset GPIO\n");
+			goto err_clk_put;
+		}
+	} else {
+		dev_err(&pdev->dev, "no codec-reset GPIO supplied\n");
+		goto err_clk_put;
+	}
+
+	ac97->sync_gpio = of_get_named_gpio(pdev->dev.of_node,
+					    "nvidia,codec-sync-gpio", 0);
+	if (!gpio_is_valid(ac97->sync_gpio)) {
+		dev_err(&pdev->dev, "no codec-sync GPIO supplied\n");
+		goto err_clk_put;
+	}
+
+	ac97->capture_dma_data.addr = mem->start + TEGRA20_AC97_FIFO_RX1;
+	ac97->capture_dma_data.wrap = 4;
+	ac97->capture_dma_data.width = 32;
+	ac97->capture_dma_data.req_sel = of_dma[1];
+
+	ac97->playback_dma_data.addr = mem->start + TEGRA20_AC97_FIFO_TX1;
+	ac97->playback_dma_data.wrap = 4;
+	ac97->playback_dma_data.width = 32;
+	ac97->playback_dma_data.req_sel = of_dma[1];
+
+	ret = snd_soc_register_dais(&pdev->dev, &tegra20_ac97_dai, 1);
+	if (ret) {
+		dev_err(&pdev->dev, "Could not register DAI: %d\n", ret);
+		ret = -ENOMEM;
+		goto err_clk_put;
+	}
+
+	ret = tegra_pcm_platform_register(&pdev->dev);
+	if (ret) {
+		dev_err(&pdev->dev, "Could not register PCM: %d\n", ret);
+		goto err_unregister_dai;
+	}
+
+	ret = tegra_asoc_utils_init(&ac97->util_data, &pdev->dev);
+	if (ret)
+		goto err_unregister_pcm;
+
+	ret = tegra_asoc_utils_set_ac97_rate(&ac97->util_data);
+	if (ret)
+		goto err_asoc_utils_fini;
+
+	ret = clk_prepare_enable(ac97->clk_ac97);
+	if (ret) {
+		dev_err(&pdev->dev, "clk_enable failed: %d\n", ret);
+		goto err_asoc_utils_fini;
+	}
+
+	/* XXX: crufty ASoC AC97 API - only one AC97 codec allowed */
+	workdata = ac97;
+
+	return 0;
+
+err_asoc_utils_fini:
+	tegra_asoc_utils_fini(&ac97->util_data);
+err_unregister_pcm:
+	tegra_pcm_platform_unregister(&pdev->dev);
+err_unregister_dai:
+	snd_soc_unregister_dai(&pdev->dev);
+err_clk_put:
+	clk_put(ac97->clk_ac97);
+err:
+	return ret;
+}
+
+static int tegra20_ac97_platform_remove(struct platform_device *pdev)
+{
+	struct tegra20_ac97 *ac97 = dev_get_drvdata(&pdev->dev);
+
+	tegra_pcm_platform_unregister(&pdev->dev);
+	snd_soc_unregister_dai(&pdev->dev);
+
+	tegra_asoc_utils_fini(&ac97->util_data);
+
+	clk_disable_unprepare(ac97->clk_ac97);
+	clk_put(ac97->clk_ac97);
+
+	return 0;
+}
+
+static const struct of_device_id tegra20_ac97_of_match[] __devinitconst = {
+	{ .compatible = "nvidia,tegra20-ac97", },
+	{},
+};
+
+static struct platform_driver tegra20_ac97_driver = {
+	.driver = {
+		.name = DRV_NAME,
+		.owner = THIS_MODULE,
+		.of_match_table = tegra20_ac97_of_match,
+	},
+	.probe = tegra20_ac97_platform_probe,
+	.remove = tegra20_ac97_platform_remove,
+};
+module_platform_driver(tegra20_ac97_driver);
+
+MODULE_AUTHOR("Lucas Stach");
+MODULE_DESCRIPTION("Tegra20 AC97 ASoC driver");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:" DRV_NAME);
+MODULE_DEVICE_TABLE(of, tegra20_ac97_of_match);
diff --git a/sound/soc/tegra/tegra20_ac97.h b/sound/soc/tegra/tegra20_ac97.h
new file mode 100644
index 0000000..a805aae
--- /dev/null
+++ b/sound/soc/tegra/tegra20_ac97.h
@@ -0,0 +1,95 @@ 
+/*
+ * tegra20_ac97.c - Definitions for the Tegra20 AC97 controller driver
+ *
+ * Copyright (c) 2012 Lucas Stach <dev@lynxeye.de>
+ *
+ * Partly based on code copyright/by:
+ *
+ * Copyright (c) 2011,2012 Toradex Inc.
+ *
+ * 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ */
+
+#ifndef __TEGRA20_AC97_H__
+#define __TEGRA20_AC97_H__
+
+#include "tegra_pcm.h"
+
+#define TEGRA20_AC97_CTRL				0x00
+#define TEGRA20_AC97_CMD				0x04
+#define TEGRA20_AC97_STATUS1				0x08
+/* ... */
+#define TEGRA20_AC97_FIFO1_SCR				0x1c
+/* ... */
+#define TEGRA20_AC97_FIFO_TX1				0x40
+#define TEGRA20_AC97_FIFO_RX1				0x80
+
+/* TEGRA20_AC97_CTRL */
+#define TEGRA20_AC97_CTRL_STM2_EN			(1 << 16)
+#define TEGRA20_AC97_CTRL_DOUBLE_SAMPLING_EN		(1 << 11)
+#define TEGRA20_AC97_CTRL_IO_CNTRL_EN			(1 << 10)
+#define TEGRA20_AC97_CTRL_HSET_DAC_EN			(1 << 9)
+#define TEGRA20_AC97_CTRL_LINE2_DAC_EN			(1 << 8)
+#define TEGRA20_AC97_CTRL_PCM_LFE_EN			(1 << 7)
+#define TEGRA20_AC97_CTRL_PCM_SUR_EN			(1 << 6)
+#define TEGRA20_AC97_CTRL_PCM_CEN_DAC_EN		(1 << 5)
+#define TEGRA20_AC97_CTRL_LINE1_DAC_EN			(1 << 4)
+#define TEGRA20_AC97_CTRL_PCM_DAC_EN			(1 << 3)
+#define TEGRA20_AC97_CTRL_COLD_RESET			(1 << 2)
+#define TEGRA20_AC97_CTRL_WARM_RESET			(1 << 1)
+#define TEGRA20_AC97_CTRL_STM_EN			(1 << 0)
+
+/* TEGRA20_AC97_CMD */
+#define TEGRA20_AC97_CMD_CMD_ADDR_SHIFT			24
+#define TEGRA20_AC97_CMD_CMD_ADDR_MASK			(0xff << TEGRA20_AC97_CMD_CMD_ADDR_SHIFT)
+#define TEGRA20_AC97_CMD_CMD_DATA_SHIFT			8
+#define TEGRA20_AC97_CMD_CMD_DATA_MASK			(0xffff << TEGRA20_AC97_CMD_CMD_DATA_SHIFT)
+#define TEGRA20_AC97_CMD_CMD_ID_SHIFT			2
+#define TEGRA20_AC97_CMD_CMD_ID_MASK			(0x3 << TEGRA20_AC97_CMD_CMD_ID_SHIFT)
+#define TEGRA20_AC97_CMD_BUSY				(1 << 0)
+
+/* TEGRA20_AC97_STATUS1 */
+#define TEGRA20_AC97_STATUS1_STA_ADDR1_SHIFT		24
+#define TEGRA20_AC97_STATUS1_STA_ADDR1_MASK		(0xff << TEGRA20_AC97_STATUS1_STA_ADDR1_SHIFT)
+#define TEGRA20_AC97_STATUS1_STA_DATA1_SHIFT		8
+#define TEGRA20_AC97_STATUS1_STA_DATA1_MASK		(0xffff << TEGRA20_AC97_STATUS1_STA_DATA1_SHIFT)
+#define TEGRA20_AC97_STATUS1_STA_VALID1			(1 << 2)
+#define TEGRA20_AC97_STATUS1_STANDBY1			(1 << 1)
+#define TEGRA20_AC97_STATUS1_CODEC1_RDY			(1 << 0)
+
+/* TEGRA20_AC97_FIFO1_SCR */
+#define TEGRA20_AC97_FIFO_SCR_REC_MT_CNT_SHIFT		27
+#define TEGRA20_AC97_FIFO_SCR_REC_MT_CNT_MASK		(0x1f << TEGRA20_AC97_FIFO_SCR_REC_MT_CNT_SHIFT)
+#define TEGRA20_AC97_FIFO_SCR_PB_MT_CNT_SHIFT		22
+#define TEGRA20_AC97_FIFO_SCR_PB_MT_CNT_MASK		(0x1f << TEGRA20_AC97_FIFO_SCR_PB_MT_CNT_SHIFT)
+#define TEGRA20_AC97_FIFO_SCR_REC_OVERRUN_INT_STA	(1 << 19)
+#define TEGRA20_AC97_FIFO_SCR_PB_UNDERRUN_INT_STA	(1 << 18)
+#define TEGRA20_AC97_FIFO_SCR_REC_FORCE_MT		(1 << 17)
+#define TEGRA20_AC97_FIFO_SCR_PB_FORCE_MT		(1 << 16)
+#define TEGRA20_AC97_FIFO_SCR_REC_FULL_EN		(1 << 15)
+#define TEGRA20_AC97_FIFO_SCR_REC_3QRT_FULL_EN		(1 << 14)
+#define TEGRA20_AC97_FIFO_SCR_REC_QRT_FULL_EN		(1 << 13)
+#define TEGRA20_AC97_FIFO_SCR_REC_EMPTY_EN		(1 << 12)
+#define TEGRA20_AC97_FIFO_SCR_PB_NOT_FULL_EN		(1 << 11)
+#define TEGRA20_AC97_FIFO_SCR_PB_QRT_MT_EN		(1 << 10)
+#define TEGRA20_AC97_FIFO_SCR_PB_3QRT_MT_EN		(1 << 9)
+#define TEGRA20_AC97_FIFO_SCR_PB_EMPTY_MT_EN		(1 << 8)
+
+struct tegra20_ac97 {
+	struct clk *clk_ac97;
+	struct tegra_pcm_dma_params capture_dma_data;
+	struct tegra_pcm_dma_params playback_dma_data;
+	struct regmap *regmap;
+	int reset_gpio;
+	int sync_gpio;
+	struct tegra_asoc_utils_data util_data;
+};
+#endif /* __TEGRA20_AC97_H__ */