diff options
Diffstat (limited to 'sound/soc/au1x/psc-ac97.c')
-rw-r--r-- | sound/soc/au1x/psc-ac97.c | 518 |
1 files changed, 518 insertions, 0 deletions
diff --git a/sound/soc/au1x/psc-ac97.c b/sound/soc/au1x/psc-ac97.c new file mode 100644 index 00000000..476b79a1 --- /dev/null +++ b/sound/soc/au1x/psc-ac97.c @@ -0,0 +1,518 @@ +/* + * Au12x0/Au1550 PSC ALSA ASoC audio support. + * + * (c) 2007-2009 MSC Vertriebsges.m.b.H., + * Manuel Lauss <manuel.lauss@gmail.com> + * + * 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. + * + * Au1xxx-PSC AC97 glue. + * + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/device.h> +#include <linux/delay.h> +#include <linux/mutex.h> +#include <linux/suspend.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/initval.h> +#include <sound/soc.h> +#include <asm/mach-au1x00/au1000.h> +#include <asm/mach-au1x00/au1xxx_psc.h> + +#include "psc.h" + +/* how often to retry failed codec register reads/writes */ +#define AC97_RW_RETRIES 5 + +#define AC97_DIR \ + (SND_SOC_DAIDIR_PLAYBACK | SND_SOC_DAIDIR_CAPTURE) + +#define AC97_RATES \ + SNDRV_PCM_RATE_8000_48000 + +#define AC97_FMTS \ + (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3BE) + +#define AC97PCR_START(stype) \ + ((stype) == SNDRV_PCM_STREAM_PLAYBACK ? PSC_AC97PCR_TS : PSC_AC97PCR_RS) +#define AC97PCR_STOP(stype) \ + ((stype) == SNDRV_PCM_STREAM_PLAYBACK ? PSC_AC97PCR_TP : PSC_AC97PCR_RP) +#define AC97PCR_CLRFIFO(stype) \ + ((stype) == SNDRV_PCM_STREAM_PLAYBACK ? PSC_AC97PCR_TC : PSC_AC97PCR_RC) + +#define AC97STAT_BUSY(stype) \ + ((stype) == SNDRV_PCM_STREAM_PLAYBACK ? PSC_AC97STAT_TB : PSC_AC97STAT_RB) + +/* instance data. There can be only one, MacLeod!!!! */ +static struct au1xpsc_audio_data *au1xpsc_ac97_workdata; + +#if 0 + +/* this could theoretically work, but ac97->bus->card->private_data can be NULL + * when snd_ac97_mixer() is called; I don't know if the rest further down the + * chain are always valid either. + */ +static inline struct au1xpsc_audio_data *ac97_to_pscdata(struct snd_ac97 *x) +{ + struct snd_soc_card *c = x->bus->card->private_data; + return snd_soc_dai_get_drvdata(c->rtd->cpu_dai); +} + +#else + +#define ac97_to_pscdata(x) au1xpsc_ac97_workdata + +#endif + +/* AC97 controller reads codec register */ +static unsigned short au1xpsc_ac97_read(struct snd_ac97 *ac97, + unsigned short reg) +{ + struct au1xpsc_audio_data *pscdata = ac97_to_pscdata(ac97); + unsigned short retry, tmo; + unsigned long data; + + au_writel(PSC_AC97EVNT_CD, AC97_EVNT(pscdata)); + au_sync(); + + retry = AC97_RW_RETRIES; + do { + mutex_lock(&pscdata->lock); + + au_writel(PSC_AC97CDC_RD | PSC_AC97CDC_INDX(reg), + AC97_CDC(pscdata)); + au_sync(); + + tmo = 20; + do { + udelay(21); + if (au_readl(AC97_EVNT(pscdata)) & PSC_AC97EVNT_CD) + break; + } while (--tmo); + + data = au_readl(AC97_CDC(pscdata)); + + au_writel(PSC_AC97EVNT_CD, AC97_EVNT(pscdata)); + au_sync(); + + mutex_unlock(&pscdata->lock); + + if (reg != ((data >> 16) & 0x7f)) + tmo = 1; /* wrong register, try again */ + + } while (--retry && !tmo); + + return retry ? data & 0xffff : 0xffff; +} + +/* AC97 controller writes to codec register */ +static void au1xpsc_ac97_write(struct snd_ac97 *ac97, unsigned short reg, + unsigned short val) +{ + struct au1xpsc_audio_data *pscdata = ac97_to_pscdata(ac97); + unsigned int tmo, retry; + + au_writel(PSC_AC97EVNT_CD, AC97_EVNT(pscdata)); + au_sync(); + + retry = AC97_RW_RETRIES; + do { + mutex_lock(&pscdata->lock); + + au_writel(PSC_AC97CDC_INDX(reg) | (val & 0xffff), + AC97_CDC(pscdata)); + au_sync(); + + tmo = 20; + do { + udelay(21); + if (au_readl(AC97_EVNT(pscdata)) & PSC_AC97EVNT_CD) + break; + } while (--tmo); + + au_writel(PSC_AC97EVNT_CD, AC97_EVNT(pscdata)); + au_sync(); + + mutex_unlock(&pscdata->lock); + } while (--retry && !tmo); +} + +/* AC97 controller asserts a warm reset */ +static void au1xpsc_ac97_warm_reset(struct snd_ac97 *ac97) +{ + struct au1xpsc_audio_data *pscdata = ac97_to_pscdata(ac97); + + au_writel(PSC_AC97RST_SNC, AC97_RST(pscdata)); + au_sync(); + msleep(10); + au_writel(0, AC97_RST(pscdata)); + au_sync(); +} + +static void au1xpsc_ac97_cold_reset(struct snd_ac97 *ac97) +{ + struct au1xpsc_audio_data *pscdata = ac97_to_pscdata(ac97); + int i; + + /* disable PSC during cold reset */ + au_writel(0, AC97_CFG(au1xpsc_ac97_workdata)); + au_sync(); + au_writel(PSC_CTRL_DISABLE, PSC_CTRL(pscdata)); + au_sync(); + + /* issue cold reset */ + au_writel(PSC_AC97RST_RST, AC97_RST(pscdata)); + au_sync(); + msleep(500); + au_writel(0, AC97_RST(pscdata)); + au_sync(); + + /* enable PSC */ + au_writel(PSC_CTRL_ENABLE, PSC_CTRL(pscdata)); + au_sync(); + + /* wait for PSC to indicate it's ready */ + i = 1000; + while (!((au_readl(AC97_STAT(pscdata)) & PSC_AC97STAT_SR)) && (--i)) + msleep(1); + + if (i == 0) { + printk(KERN_ERR "au1xpsc-ac97: PSC not ready!\n"); + return; + } + + /* enable the ac97 function */ + au_writel(pscdata->cfg | PSC_AC97CFG_DE_ENABLE, AC97_CFG(pscdata)); + au_sync(); + + /* wait for AC97 core to become ready */ + i = 1000; + while (!((au_readl(AC97_STAT(pscdata)) & PSC_AC97STAT_DR)) && (--i)) + msleep(1); + if (i == 0) + printk(KERN_ERR "au1xpsc-ac97: AC97 ctrl not ready\n"); +} + +/* AC97 controller operations */ +struct snd_ac97_bus_ops soc_ac97_ops = { + .read = au1xpsc_ac97_read, + .write = au1xpsc_ac97_write, + .reset = au1xpsc_ac97_cold_reset, + .warm_reset = au1xpsc_ac97_warm_reset, +}; +EXPORT_SYMBOL_GPL(soc_ac97_ops); + +static int au1xpsc_ac97_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct au1xpsc_audio_data *pscdata = snd_soc_dai_get_drvdata(dai); + unsigned long r, ro, stat; + int chans, t, stype = substream->stream; + + chans = params_channels(params); + + r = ro = au_readl(AC97_CFG(pscdata)); + stat = au_readl(AC97_STAT(pscdata)); + + /* already active? */ + if (stat & (PSC_AC97STAT_TB | PSC_AC97STAT_RB)) { + /* reject parameters not currently set up */ + if ((PSC_AC97CFG_GET_LEN(r) != params->msbits) || + (pscdata->rate != params_rate(params))) + return -EINVAL; + } else { + + /* set sample bitdepth: REG[24:21]=(BITS-2)/2 */ + r &= ~PSC_AC97CFG_LEN_MASK; + r |= PSC_AC97CFG_SET_LEN(params->msbits); + + /* channels: enable slots for front L/R channel */ + if (stype == SNDRV_PCM_STREAM_PLAYBACK) { + r &= ~PSC_AC97CFG_TXSLOT_MASK; + r |= PSC_AC97CFG_TXSLOT_ENA(3); + r |= PSC_AC97CFG_TXSLOT_ENA(4); + } else { + r &= ~PSC_AC97CFG_RXSLOT_MASK; + r |= PSC_AC97CFG_RXSLOT_ENA(3); + r |= PSC_AC97CFG_RXSLOT_ENA(4); + } + + /* do we need to poke the hardware? */ + if (!(r ^ ro)) + goto out; + + /* ac97 engine is about to be disabled */ + mutex_lock(&pscdata->lock); + + /* disable AC97 device controller first... */ + au_writel(r & ~PSC_AC97CFG_DE_ENABLE, AC97_CFG(pscdata)); + au_sync(); + + /* ...wait for it... */ + t = 100; + while ((au_readl(AC97_STAT(pscdata)) & PSC_AC97STAT_DR) && --t) + msleep(1); + + if (!t) + printk(KERN_ERR "PSC-AC97: can't disable!\n"); + + /* ...write config... */ + au_writel(r, AC97_CFG(pscdata)); + au_sync(); + + /* ...enable the AC97 controller again... */ + au_writel(r | PSC_AC97CFG_DE_ENABLE, AC97_CFG(pscdata)); + au_sync(); + + /* ...and wait for ready bit */ + t = 100; + while ((!(au_readl(AC97_STAT(pscdata)) & PSC_AC97STAT_DR)) && --t) + msleep(1); + + if (!t) + printk(KERN_ERR "PSC-AC97: can't enable!\n"); + + mutex_unlock(&pscdata->lock); + + pscdata->cfg = r; + pscdata->rate = params_rate(params); + } + +out: + return 0; +} + +static int au1xpsc_ac97_trigger(struct snd_pcm_substream *substream, + int cmd, struct snd_soc_dai *dai) +{ + struct au1xpsc_audio_data *pscdata = snd_soc_dai_get_drvdata(dai); + int ret, stype = substream->stream; + + ret = 0; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + au_writel(AC97PCR_CLRFIFO(stype), AC97_PCR(pscdata)); + au_sync(); + au_writel(AC97PCR_START(stype), AC97_PCR(pscdata)); + au_sync(); + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + au_writel(AC97PCR_STOP(stype), AC97_PCR(pscdata)); + au_sync(); + + while (au_readl(AC97_STAT(pscdata)) & AC97STAT_BUSY(stype)) + asm volatile ("nop"); + + au_writel(AC97PCR_CLRFIFO(stype), AC97_PCR(pscdata)); + au_sync(); + + break; + default: + ret = -EINVAL; + } + return ret; +} + +static int au1xpsc_ac97_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct au1xpsc_audio_data *pscdata = snd_soc_dai_get_drvdata(dai); + snd_soc_dai_set_dma_data(dai, substream, &pscdata->dmaids[0]); + return 0; +} + +static int au1xpsc_ac97_probe(struct snd_soc_dai *dai) +{ + return au1xpsc_ac97_workdata ? 0 : -ENODEV; +} + +static const struct snd_soc_dai_ops au1xpsc_ac97_dai_ops = { + .startup = au1xpsc_ac97_startup, + .trigger = au1xpsc_ac97_trigger, + .hw_params = au1xpsc_ac97_hw_params, +}; + +static const struct snd_soc_dai_driver au1xpsc_ac97_dai_template = { + .ac97_control = 1, + .probe = au1xpsc_ac97_probe, + .playback = { + .rates = AC97_RATES, + .formats = AC97_FMTS, + .channels_min = 2, + .channels_max = 2, + }, + .capture = { + .rates = AC97_RATES, + .formats = AC97_FMTS, + .channels_min = 2, + .channels_max = 2, + }, + .ops = &au1xpsc_ac97_dai_ops, +}; + +static int __devinit au1xpsc_ac97_drvprobe(struct platform_device *pdev) +{ + int ret; + struct resource *iores, *dmares; + unsigned long sel; + struct au1xpsc_audio_data *wd; + + wd = devm_kzalloc(&pdev->dev, sizeof(struct au1xpsc_audio_data), + GFP_KERNEL); + if (!wd) + return -ENOMEM; + + mutex_init(&wd->lock); + + iores = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!iores) + return -ENODEV; + + if (!devm_request_mem_region(&pdev->dev, iores->start, + resource_size(iores), + pdev->name)) + return -EBUSY; + + wd->mmio = devm_ioremap(&pdev->dev, iores->start, + resource_size(iores)); + if (!wd->mmio) + return -EBUSY; + + dmares = platform_get_resource(pdev, IORESOURCE_DMA, 0); + if (!dmares) + return -EBUSY; + wd->dmaids[SNDRV_PCM_STREAM_PLAYBACK] = dmares->start; + + dmares = platform_get_resource(pdev, IORESOURCE_DMA, 1); + if (!dmares) + return -EBUSY; + wd->dmaids[SNDRV_PCM_STREAM_CAPTURE] = dmares->start; + + /* configuration: max dma trigger threshold, enable ac97 */ + wd->cfg = PSC_AC97CFG_RT_FIFO8 | PSC_AC97CFG_TT_FIFO8 | + PSC_AC97CFG_DE_ENABLE; + + /* preserve PSC clock source set up by platform */ + sel = au_readl(PSC_SEL(wd)) & PSC_SEL_CLK_MASK; + au_writel(PSC_CTRL_DISABLE, PSC_CTRL(wd)); + au_sync(); + au_writel(0, PSC_SEL(wd)); + au_sync(); + au_writel(PSC_SEL_PS_AC97MODE | sel, PSC_SEL(wd)); + au_sync(); + + /* name the DAI like this device instance ("au1xpsc-ac97.PSCINDEX") */ + memcpy(&wd->dai_drv, &au1xpsc_ac97_dai_template, + sizeof(struct snd_soc_dai_driver)); + wd->dai_drv.name = dev_name(&pdev->dev); + + platform_set_drvdata(pdev, wd); + + ret = snd_soc_register_dai(&pdev->dev, &wd->dai_drv); + if (ret) + return ret; + + au1xpsc_ac97_workdata = wd; + return 0; +} + +static int __devexit au1xpsc_ac97_drvremove(struct platform_device *pdev) +{ + struct au1xpsc_audio_data *wd = platform_get_drvdata(pdev); + + snd_soc_unregister_dai(&pdev->dev); + + /* disable PSC completely */ + au_writel(0, AC97_CFG(wd)); + au_sync(); + au_writel(PSC_CTRL_DISABLE, PSC_CTRL(wd)); + au_sync(); + + au1xpsc_ac97_workdata = NULL; /* MDEV */ + + return 0; +} + +#ifdef CONFIG_PM +static int au1xpsc_ac97_drvsuspend(struct device *dev) +{ + struct au1xpsc_audio_data *wd = dev_get_drvdata(dev); + + /* save interesting registers and disable PSC */ + wd->pm[0] = au_readl(PSC_SEL(wd)); + + au_writel(0, AC97_CFG(wd)); + au_sync(); + au_writel(PSC_CTRL_DISABLE, PSC_CTRL(wd)); + au_sync(); + + return 0; +} + +static int au1xpsc_ac97_drvresume(struct device *dev) +{ + struct au1xpsc_audio_data *wd = dev_get_drvdata(dev); + + /* restore PSC clock config */ + au_writel(wd->pm[0] | PSC_SEL_PS_AC97MODE, PSC_SEL(wd)); + au_sync(); + + /* after this point the ac97 core will cold-reset the codec. + * During cold-reset the PSC is reinitialized and the last + * configuration set up in hw_params() is restored. + */ + return 0; +} + +static struct dev_pm_ops au1xpscac97_pmops = { + .suspend = au1xpsc_ac97_drvsuspend, + .resume = au1xpsc_ac97_drvresume, +}; + +#define AU1XPSCAC97_PMOPS &au1xpscac97_pmops + +#else + +#define AU1XPSCAC97_PMOPS NULL + +#endif + +static struct platform_driver au1xpsc_ac97_driver = { + .driver = { + .name = "au1xpsc_ac97", + .owner = THIS_MODULE, + .pm = AU1XPSCAC97_PMOPS, + }, + .probe = au1xpsc_ac97_drvprobe, + .remove = __devexit_p(au1xpsc_ac97_drvremove), +}; + +static int __init au1xpsc_ac97_load(void) +{ + au1xpsc_ac97_workdata = NULL; + return platform_driver_register(&au1xpsc_ac97_driver); +} + +static void __exit au1xpsc_ac97_unload(void) +{ + platform_driver_unregister(&au1xpsc_ac97_driver); +} + +module_init(au1xpsc_ac97_load); +module_exit(au1xpsc_ac97_unload); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Au12x0/Au1550 PSC AC97 ALSA ASoC audio driver"); +MODULE_AUTHOR("Manuel Lauss"); + |