diff options
author | James Bottomley <jejb@mulgrave.il.steeleye.com> | 2007-02-10 13:45:43 -0600 |
---|---|---|
committer | James Bottomley <jejb@mulgrave.il.steeleye.com> | 2007-02-10 13:45:43 -0600 |
commit | 81b7bbd1932a04869d4c8635a75222dfc6089f96 (patch) | |
tree | 285ae868a1e3a41fb0dbfe346c28e380949bcb55 /sound/soc | |
parent | 98051995ab44b993f992946055edc6115351f725 (diff) | |
parent | 66efc5a7e3061c3597ac43a8bb1026488d57e66b (diff) |
Merge branch 'linus'
Conflicts:
drivers/scsi/ipr.c
Signed-off-by: James Bottomley <James.Bottomley@SteelEye.com>
Diffstat (limited to 'sound/soc')
33 files changed, 10211 insertions, 0 deletions
diff --git a/sound/soc/Kconfig b/sound/soc/Kconfig new file mode 100644 index 00000000000..ec821a57f84 --- /dev/null +++ b/sound/soc/Kconfig @@ -0,0 +1,32 @@ +# +# SoC audio configuration +# + +menu "SoC audio support" + depends on SND!=n + +config SND_SOC_AC97_BUS + bool + +config SND_SOC + tristate "SoC audio support" + ---help--- + + If you want SoC support, you should say Y here and also to the + specific driver for your SoC below. You will also need to select the + specific codec(s) attached to the SoC + + This SoC audio support can also be built as a module. If so, the module + will be called snd-soc-core. + +# All the supported Soc's +menu "SoC Platforms" +depends on SND_SOC +source "sound/soc/at91/Kconfig" +source "sound/soc/pxa/Kconfig" +endmenu + +# Supported codecs +source "sound/soc/codecs/Kconfig" + +endmenu diff --git a/sound/soc/Makefile b/sound/soc/Makefile new file mode 100644 index 00000000000..98e6f49dafc --- /dev/null +++ b/sound/soc/Makefile @@ -0,0 +1,4 @@ +snd-soc-core-objs := soc-core.o soc-dapm.o + +obj-$(CONFIG_SND_SOC) += snd-soc-core.o +obj-$(CONFIG_SND_SOC) += codecs/ at91/ pxa/ diff --git a/sound/soc/at91/Kconfig b/sound/soc/at91/Kconfig new file mode 100644 index 00000000000..5bcf08b728b --- /dev/null +++ b/sound/soc/at91/Kconfig @@ -0,0 +1,32 @@ +menu "SoC Audio for the Atmel AT91" + +config SND_AT91_SOC + tristate "SoC Audio for the Atmel AT91 System-on-Chip" + depends on ARCH_AT91 && SND + select SND_PCM + help + Say Y or M if you want to add support for codecs attached to + the AT91 SSC interface. You will also need + to select the audio interfaces to support below. + +config SND_AT91_SOC_I2S + tristate + +config SND_AT91_SOC_ETI_B1_WM8731 + tristate "SoC I2S Audio support for WM8731-based Endrelia ETI-B1 boards" + depends on SND_AT91_SOC && (MACH_ETI_B1 || MACH_ETI_C1) + select SND_AT91_SOC_I2S + select SND_SOC_WM8731 + help + Say Y if you want to add support for SoC audio on WM8731-based + Endrelia Technologies Inc ETI-B1 or ETI-C1 boards. + +config SND_AT91_SOC_ETI_SLAVE + bool "Run codec in slave Mode on Endrelia boards" + depends on SND_AT91_SOC_ETI_B1_WM8731 + default n + help + Say Y if you want to run with the AT91 SSC generating the BCLK + and LRC signals on Endrelia boards. + +endmenu diff --git a/sound/soc/at91/Makefile b/sound/soc/at91/Makefile new file mode 100644 index 00000000000..b77b01ab202 --- /dev/null +++ b/sound/soc/at91/Makefile @@ -0,0 +1,11 @@ +# AT91 Platform Support +snd-soc-at91-objs := at91-pcm.o +snd-soc-at91-i2s-objs := at91-i2s.o + +obj-$(CONFIG_SND_AT91_SOC) += snd-soc-at91.o +obj-$(CONFIG_SND_AT91_SOC_I2S) += snd-soc-at91-i2s.o + +# AT91 Machine Support +snd-soc-eti-b1-wm8731-objs := eti_b1_wm8731.o + +obj-$(CONFIG_SND_AT91_SOC_ETI_B1_WM8731) += snd-soc-eti-b1-wm8731.o diff --git a/sound/soc/at91/at91-i2s.c b/sound/soc/at91/at91-i2s.c new file mode 100644 index 00000000000..fcc544a96ba --- /dev/null +++ b/sound/soc/at91/at91-i2s.c @@ -0,0 +1,720 @@ +/* + * at91-i2s.c -- ALSA SoC I2S Audio Layer Platform driver + * + * Author: Frank Mandarino <fmandarino@endrelia.com> + * Endrelia Technologies Inc. + * + * Based on pxa2xx Platform drivers by + * Liam Girdwood <liam.girdwood@wolfsonmicro.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/interrupt.h> +#include <linux/device.h> +#include <linux/delay.h> +#include <linux/clk.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/initval.h> +#include <sound/soc.h> + +#include <asm/arch/hardware.h> +#include <asm/arch/at91_pmc.h> +#include <asm/arch/at91_ssc.h> +#include <asm/arch/at91_pdc.h> + +#include "at91-pcm.h" +#include "at91-i2s.h" + +#if 0 +#define DBG(x...) printk(KERN_DEBUG "at91-i2s:" x) +#else +#define DBG(x...) +#endif + +#if defined(CONFIG_ARCH_AT91SAM9260) +#define NUM_SSC_DEVICES 1 +#else +#define NUM_SSC_DEVICES 3 +#endif + + +/* + * SSC PDC registers required by the PCM DMA engine. + */ +static struct at91_pdc_regs pdc_tx_reg = { + .xpr = AT91_PDC_TPR, + .xcr = AT91_PDC_TCR, + .xnpr = AT91_PDC_TNPR, + .xncr = AT91_PDC_TNCR, +}; + +static struct at91_pdc_regs pdc_rx_reg = { + .xpr = AT91_PDC_RPR, + .xcr = AT91_PDC_RCR, + .xnpr = AT91_PDC_RNPR, + .xncr = AT91_PDC_RNCR, +}; + +/* + * SSC & PDC status bits for transmit and receive. + */ +static struct at91_ssc_mask ssc_tx_mask = { + .ssc_enable = AT91_SSC_TXEN, + .ssc_disable = AT91_SSC_TXDIS, + .ssc_endx = AT91_SSC_ENDTX, + .ssc_endbuf = AT91_SSC_TXBUFE, + .pdc_enable = AT91_PDC_TXTEN, + .pdc_disable = AT91_PDC_TXTDIS, +}; + +static struct at91_ssc_mask ssc_rx_mask = { + .ssc_enable = AT91_SSC_RXEN, + .ssc_disable = AT91_SSC_RXDIS, + .ssc_endx = AT91_SSC_ENDRX, + .ssc_endbuf = AT91_SSC_RXBUFF, + .pdc_enable = AT91_PDC_RXTEN, + .pdc_disable = AT91_PDC_RXTDIS, +}; + + +/* + * DMA parameters. + */ +static struct at91_pcm_dma_params ssc_dma_params[NUM_SSC_DEVICES][2] = { + {{ + .name = "SSC0/I2S PCM Stereo out", + .pdc = &pdc_tx_reg, + .mask = &ssc_tx_mask, + }, + { + .name = "SSC0/I2S PCM Stereo in", + .pdc = &pdc_rx_reg, + .mask = &ssc_rx_mask, + }}, +#if NUM_SSC_DEVICES == 3 + {{ + .name = "SSC1/I2S PCM Stereo out", + .pdc = &pdc_tx_reg, + .mask = &ssc_tx_mask, + }, + { + .name = "SSC1/I2S PCM Stereo in", + .pdc = &pdc_rx_reg, + .mask = &ssc_rx_mask, + }}, + {{ + .name = "SSC2/I2S PCM Stereo out", + .pdc = &pdc_tx_reg, + .mask = &ssc_tx_mask, + }, + { + .name = "SSC1/I2S PCM Stereo in", + .pdc = &pdc_rx_reg, + .mask = &ssc_rx_mask, + }}, +#endif +}; + +struct at91_ssc_state { + u32 ssc_cmr; + u32 ssc_rcmr; + u32 ssc_rfmr; + u32 ssc_tcmr; + u32 ssc_tfmr; + u32 ssc_sr; + u32 ssc_imr; +}; + +static struct at91_ssc_info { + char *name; + struct at91_ssc_periph ssc; + spinlock_t lock; /* lock for dir_mask */ + unsigned short dir_mask; /* 0=unused, 1=playback, 2=capture */ + unsigned short initialized; /* 1=SSC has been initialized */ + unsigned short daifmt; + unsigned short cmr_div; + unsigned short tcmr_period; + unsigned short rcmr_period; + struct at91_pcm_dma_params *dma_params[2]; + struct at91_ssc_state ssc_state; + +} ssc_info[NUM_SSC_DEVICES] = { + { + .name = "ssc0", + .lock = SPIN_LOCK_UNLOCKED, + .dir_mask = 0, + .initialized = 0, + }, +#if NUM_SSC_DEVICES == 3 + { + .name = "ssc1", + .lock = SPIN_LOCK_UNLOCKED, + .dir_mask = 0, + .initialized = 0, + }, + { + .name = "ssc2", + .lock = SPIN_LOCK_UNLOCKED, + .dir_mask = 0, + .initialized = 0, + }, +#endif +}; + +static unsigned int at91_i2s_sysclk; + +/* + * SSC interrupt handler. Passes PDC interrupts to the DMA + * interrupt handler in the PCM driver. + */ +static irqreturn_t at91_i2s_interrupt(int irq, void *dev_id) +{ + struct at91_ssc_info *ssc_p = dev_id; + struct at91_pcm_dma_params *dma_params; + u32 ssc_sr; + int i; + + ssc_sr = at91_ssc_read(ssc_p->ssc.base + AT91_SSC_SR) + & at91_ssc_read(ssc_p->ssc.base + AT91_SSC_IMR); + + /* + * Loop through the substreams attached to this SSC. If + * a DMA-related interrupt occurred on that substream, call + * the DMA interrupt handler function, if one has been + * registered in the dma_params structure by the PCM driver. + */ + for (i = 0; i < ARRAY_SIZE(ssc_p->dma_params); i++) { + dma_params = ssc_p->dma_params[i]; + + if (dma_params != NULL && dma_params->dma_intr_handler != NULL && + (ssc_sr & + (dma_params->mask->ssc_endx | dma_params->mask->ssc_endbuf))) + + dma_params->dma_intr_handler(ssc_sr, dma_params->substream); + } + + return IRQ_HANDLED; +} + +/* + * Startup. Only that one substream allowed in each direction. + */ +static int at91_i2s_startup(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct at91_ssc_info *ssc_p = &ssc_info[rtd->dai->cpu_dai->id]; + int dir_mask; + + DBG("i2s_startup: SSC_SR=0x%08lx\n", + at91_ssc_read(ssc_p->ssc.base + AT91_SSC_SR)); + dir_mask = substream->stream == SNDRV_PCM_STREAM_PLAYBACK ? 0x1 : 0x2; + + spin_lock_irq(&ssc_p->lock); + if (ssc_p->dir_mask & dir_mask) { + spin_unlock_irq(&ssc_p->lock); + return -EBUSY; + } + ssc_p->dir_mask |= dir_mask; + spin_unlock_irq(&ssc_p->lock); + + return 0; +} + +/* + * Shutdown. Clear DMA parameters and shutdown the SSC if there + * are no other substreams open. + */ +static void at91_i2s_shutdown(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct at91_ssc_info *ssc_p = &ssc_info[rtd->dai->cpu_dai->id]; + struct at91_pcm_dma_params *dma_params; + int dir, dir_mask; + + dir = substream->stream == SNDRV_PCM_STREAM_PLAYBACK ? 0 : 1; + dma_params = ssc_p->dma_params[dir]; + + if (dma_params != NULL) { + at91_ssc_write(dma_params->ssc_base + AT91_SSC_CR, + dma_params->mask->ssc_disable); + DBG("%s disabled SSC_SR=0x%08lx\n", (dir ? "receive" : "transmit"), + at91_ssc_read(ssc_p->ssc.base + AT91_SSC_SR)); + + dma_params->ssc_base = NULL; + dma_params->substream = NULL; + ssc_p->dma_params[dir] = NULL; + } + + dir_mask = 1 << dir; + + spin_lock_irq(&ssc_p->lock); + ssc_p->dir_mask &= ~dir_mask; + if (!ssc_p->dir_mask) { + /* Shutdown the SSC clock. */ + DBG("Stopping pid %d clock\n", ssc_p->ssc.pid); + at91_sys_write(AT91_PMC_PCDR, 1<<ssc_p->ssc.pid); + + if (ssc_p->initialized) { + free_irq(ssc_p->ssc.pid, ssc_p); + ssc_p->initialized = 0; + } + + /* Reset the SSC */ + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_CR, AT91_SSC_SWRST); + + /* Clear the SSC dividers */ + ssc_p->cmr_div = ssc_p->tcmr_period = ssc_p->rcmr_period = 0; + } + spin_unlock_irq(&ssc_p->lock); +} + +/* + * Record the SSC system clock rate. + */ +static int at91_i2s_set_dai_sysclk(struct snd_soc_cpu_dai *cpu_dai, + int clk_id, unsigned int freq, int dir) +{ + /* + * The only clock supplied to the SSC is the AT91 master clock, + * which is only used if the SSC is generating BCLK and/or + * LRC clocks. + */ + switch (clk_id) { + case AT91_SYSCLK_MCK: + at91_i2s_sysclk = freq; + break; + default: + return -EINVAL; + } + + return 0; +} + +/* + * Record the DAI format for use in hw_params(). + */ +static int at91_i2s_set_dai_fmt(struct snd_soc_cpu_dai *cpu_dai, + unsigned int fmt) +{ + struct at91_ssc_info *ssc_p = &ssc_info[cpu_dai->id]; + + if ((fmt & SND_SOC_DAIFMT_FORMAT_MASK) != SND_SOC_DAIFMT_I2S) + return -EINVAL; + + ssc_p->daifmt = fmt; + return 0; +} + +/* + * Record SSC clock dividers for use in hw_params(). + */ +static int at91_i2s_set_dai_clkdiv(struct snd_soc_cpu_dai *cpu_dai, + int div_id, int div) +{ + struct at91_ssc_info *ssc_p = &ssc_info[cpu_dai->id]; + + switch (div_id) { + case AT91SSC_CMR_DIV: + /* + * The same master clock divider is used for both + * transmit and receive, so if a value has already + * been set, it must match this value. + */ + if (ssc_p->cmr_div == 0) + ssc_p->cmr_div = div; + else + if (div != ssc_p->cmr_div) + return -EBUSY; + break; + + case AT91SSC_TCMR_PERIOD: + ssc_p->tcmr_period = div; + break; + + case AT91SSC_RCMR_PERIOD: + ssc_p->rcmr_period = div; + break; + + default: + return -EINVAL; + } + + return 0; +} + +/* + * Configure the SSC. + */ +static int at91_i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + int id = rtd->dai->cpu_dai->id; + struct at91_ssc_info *ssc_p = &ssc_info[id]; + struct at91_pcm_dma_params *dma_params; + int dir, channels, bits; + u32 tfmr, rfmr, tcmr, rcmr; + int start_event; + int ret; + + /* + * Currently, there is only one set of dma params for + * each direction. If more are added, this code will + * have to be changed to select the proper set. + */ + dir = substream->stream == SNDRV_PCM_STREAM_PLAYBACK ? 0 : 1; + + dma_params = &ssc_dma_params[id][dir]; + dma_params->ssc_base = ssc_p->ssc.base; + dma_params->substream = substream; + + ssc_p->dma_params[dir] = dma_params; + + /* + * The cpu_dai->dma_data field is only used to communicate the + * appropriate DMA parameters to the pcm driver hw_params() + * function. It should not be used for other purposes + * as it is common to all substreams. + */ + rtd->dai->cpu_dai->dma_data = dma_params; + + channels = params_channels(params); + + /* + * The SSC only supports up to 16-bit samples in I2S format, due + * to the size of the Frame Mode Register FSLEN field. Also, I2S + * implies signed data. + */ + bits = 16; + dma_params->pdc_xfer_size = 2; + + /* + * Compute SSC register settings. + */ + switch (ssc_p->daifmt) { + case SND_SOC_DAIFMT_CBS_CFS: + /* + * SSC provides BCLK and LRC clocks. + * + * The SSC transmit and receive clocks are generated from the + * MCK divider, and the BCLK signal is output on the SSC TK line. + */ + rcmr = (( ssc_p->rcmr_period << 24) & AT91_SSC_PERIOD) + | (( 1 << 16) & AT91_SSC_STTDLY) + | (( AT91_SSC_START_FALLING_RF ) & AT91_SSC_START) + | (( AT91_SSC_CK_RISING ) & AT91_SSC_CKI) + | (( AT91_SSC_CKO_NONE ) & AT91_SSC_CKO) + | (( AT91_SSC_CKS_DIV ) & AT91_SSC_CKS); + + rfmr = (( AT91_SSC_FSEDGE_POSITIVE ) & AT91_SSC_FSEDGE) + | (( AT91_SSC_FSOS_NEGATIVE ) & AT91_SSC_FSOS) + | (((bits - 1) << 16) & AT91_SSC_FSLEN) + | (((channels - 1) << 8) & AT91_SSC_DATNB) + | (( 1 << 7) & AT91_SSC_MSBF) + | (( 0 << 5) & AT91_SSC_LOOP) + | (((bits - 1) << 0) & AT91_SSC_DATALEN); + + tcmr = (( ssc_p->tcmr_period << 24) & AT91_SSC_PERIOD) + | (( 1 << 16) & AT91_SSC_STTDLY) + | (( AT91_SSC_START_FALLING_RF ) & AT91_SSC_START) + | (( AT91_SSC_CKI_FALLING ) & AT91_SSC_CKI) + | (( AT91_SSC_CKO_CONTINUOUS ) & AT91_SSC_CKO) + | (( AT91_SSC_CKS_DIV ) & AT91_SSC_CKS); + + tfmr = (( AT91_SSC_FSEDGE_POSITIVE ) & AT91_SSC_FSEDGE) + | (( 0 << 23) & AT91_SSC_FSDEN) + | (( AT91_SSC_FSOS_NEGATIVE ) & AT91_SSC_FSOS) + | (((bits - 1) << 16) & AT91_SSC_FSLEN) + | (((channels - 1) << 8) & AT91_SSC_DATNB) + | (( 1 << 7) & AT91_SSC_MSBF) + | (( 0 << 5) & AT91_SSC_DATDEF) + | (((bits - 1) << 0) & AT91_SSC_DATALEN); + break; + + case SND_SOC_DAIFMT_CBM_CFM: + + /* + * CODEC supplies BCLK and LRC clocks. + * + * The SSC transmit clock is obtained from the BCLK signal on + * on the TK line, and the SSC receive clock is generated from the + * transmit clock. + * + * For single channel data, one sample is transferred on the falling + * edge of the LRC clock. For two channel data, one sample is + * transferred on both edges of the LRC clock. + */ + start_event = channels == 1 + ? AT91_SSC_START_FALLING_RF + : AT91_SSC_START_EDGE_RF; + + rcmr = (( 0 << 24) & AT91_SSC_PERIOD) + | (( 1 << 16) & AT91_SSC_STTDLY) + | (( start_event ) & AT91_SSC_START) + | (( AT91_SSC_CK_RISING ) & AT91_SSC_CKI) + | (( AT91_SSC_CKO_NONE ) & AT91_SSC_CKO) + | (( AT91_SSC_CKS_CLOCK ) & AT91_SSC_CKS); + + rfmr = (( AT91_SSC_FSEDGE_POSITIVE ) & AT91_SSC_FSEDGE) + | (( AT91_SSC_FSOS_NONE ) & AT91_SSC_FSOS) + | (( 0 << 16) & AT91_SSC_FSLEN) + | (( 0 << 8) & AT91_SSC_DATNB) + | (( 1 << 7) & AT91_SSC_MSBF) + | (( 0 << 5) & AT91_SSC_LOOP) + | (((bits - 1) << 0) & AT91_SSC_DATALEN); + + tcmr = (( 0 << 24) & AT91_SSC_PERIOD) + | (( 1 << 16) & AT91_SSC_STTDLY) + | (( start_event ) & AT91_SSC_START) + | (( AT91_SSC_CKI_FALLING ) & AT91_SSC_CKI) + | (( AT91_SSC_CKO_NONE ) & AT91_SSC_CKO) + | (( AT91_SSC_CKS_PIN ) & AT91_SSC_CKS); + + tfmr = (( AT91_SSC_FSEDGE_POSITIVE ) & AT91_SSC_FSEDGE) + | (( 0 << 23) & AT91_SSC_FSDEN) + | (( AT91_SSC_FSOS_NONE ) & AT91_SSC_FSOS) + | (( 0 << 16) & AT91_SSC_FSLEN) + | (( 0 << 8) & AT91_SSC_DATNB) + | (( 1 << 7) & AT91_SSC_MSBF) + | (( 0 << 5) & AT91_SSC_DATDEF) + | (((bits - 1) << 0) & AT91_SSC_DATALEN); + break; + + case SND_SOC_DAIFMT_CBS_CFM: + case SND_SOC_DAIFMT_CBM_CFS: + default: + printk(KERN_WARNING "at91-i2s: unsupported DAI format 0x%x.\n", + ssc_p->daifmt); + return -EINVAL; + break; + } + DBG("RCMR=%08x RFMR=%08x TCMR=%08x TFMR=%08x\n", rcmr, rfmr, tcmr, tfmr); + + if (!ssc_p->initialized) { + + /* Enable PMC peripheral clock for this SSC */ + DBG("Starting pid %d clock\n", ssc_p->ssc.pid); + at91_sys_write(AT91_PMC_PCER, 1<<ssc_p->ssc.pid); + + /* Reset the SSC and its PDC registers */ + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_CR, AT91_SSC_SWRST); + + at91_ssc_write(ssc_p->ssc.base + AT91_PDC_RPR, 0); + at91_ssc_write(ssc_p->ssc.base + AT91_PDC_RCR, 0); + at91_ssc_write(ssc_p->ssc.base + AT91_PDC_RNPR, 0); + at91_ssc_write(ssc_p->ssc.base + AT91_PDC_RNCR, 0); + at91_ssc_write(ssc_p->ssc.base + AT91_PDC_TPR, 0); + at91_ssc_write(ssc_p->ssc.base + AT91_PDC_TCR, 0); + at91_ssc_write(ssc_p->ssc.base + AT91_PDC_TNPR, 0); + at91_ssc_write(ssc_p->ssc.base + AT91_PDC_TNCR, 0); + + if ((ret = request_irq(ssc_p->ssc.pid, at91_i2s_interrupt, + 0, ssc_p->name, ssc_p)) < 0) { + printk(KERN_WARNING "at91-i2s: request_irq failure\n"); + + DBG("Stopping pid %d clock\n", ssc_p->ssc.pid); + at91_sys_write(AT91_PMC_PCER, 1<<ssc_p->ssc.pid); + return ret; + } + + ssc_p->initialized = 1; + } + + /* set SSC clock mode register */ + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_CMR, ssc_p->cmr_div); + + /* set receive clock mode and format */ + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_RCMR, rcmr); + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_RFMR, rfmr); + + /* set transmit clock mode and format */ + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_TCMR, tcmr); + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_TFMR, tfmr); + + DBG("hw_params: SSC initialized\n"); + return 0; +} + + +static int at91_i2s_prepare(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct at91_ssc_info *ssc_p = &ssc_info[rtd->dai->cpu_dai->id]; + struct at91_pcm_dma_params *dma_params; + int dir; + + dir = substream->stream == SNDRV_PCM_STREAM_PLAYBACK ? 0 : 1; + dma_params = ssc_p->dma_params[dir]; + + at91_ssc_write(dma_params->ssc_base + AT91_SSC_CR, + dma_params->mask->ssc_enable); + + DBG("%s enabled SSC_SR=0x%08lx\n", dir ? "receive" : "transmit", + at91_ssc_read(dma_params->ssc_base + AT91_SSC_SR)); + return 0; +} + + +#ifdef CONFIG_PM +static int at91_i2s_suspend(struct platform_device *pdev, + struct snd_soc_cpu_dai *cpu_dai) +{ + struct at91_ssc_info *ssc_p; + + if(!cpu_dai->active) + return 0; + + ssc_p = &ssc_info[cpu_dai->id]; + + /* Save the status register before disabling transmit and receive. */ + ssc_p->ssc_state.ssc_sr = at91_ssc_read(ssc_p->ssc.base + AT91_SSC_SR); + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_CR, + AT91_SSC_TXDIS | AT91_SSC_RXDIS); + + /* Save the current interrupt mask, then disable unmasked interrupts. */ + ssc_p->ssc_state.ssc_imr = at91_ssc_read(ssc_p->ssc.base + AT91_SSC_IMR); + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_IDR, ssc_p->ssc_state.ssc_imr); + + ssc_p->ssc_state.ssc_cmr = at91_ssc_read(ssc_p->ssc.base + AT91_SSC_CMR); + ssc_p->ssc_state.ssc_rcmr = at91_ssc_read(ssc_p->ssc.base + AT91_SSC_RCMR); + ssc_p->ssc_state.ssc_rfmr = at91_ssc_read(ssc_p->ssc.base + AT91_SSC_RFMR); + ssc_p->ssc_state.ssc_tcmr = at91_ssc_read(ssc_p->ssc.base + AT91_SSC_TCMR); + ssc_p->ssc_state.ssc_tfmr = at91_ssc_read(ssc_p->ssc.base + AT91_SSC_TFMR); + + return 0; +} + +static int at91_i2s_resume(struct platform_device *pdev, + struct snd_soc_cpu_dai *cpu_dai) +{ + struct at91_ssc_info *ssc_p; + + if(!cpu_dai->active) + return 0; + + ssc_p = &ssc_info[cpu_dai->id]; + + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_TFMR, ssc_p->ssc_state.ssc_tfmr); + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_TCMR, ssc_p->ssc_state.ssc_tcmr); + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_RFMR, ssc_p->ssc_state.ssc_rfmr); + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_RCMR, ssc_p->ssc_state.ssc_rcmr); + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_CMR, ssc_p->ssc_state.ssc_cmr); + + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_IER, ssc_p->ssc_state.ssc_imr); + + at91_ssc_write(ssc_p->ssc.base + AT91_SSC_CR, + ((ssc_p->ssc_state.ssc_sr & AT91_SSC_RXENA) ? AT91_SSC_RXEN : 0) | + ((ssc_p->ssc_state.ssc_sr & AT91_SSC_TXENA) ? AT91_SSC_TXEN : 0)); + + return 0; +} + +#else +#define at91_i2s_suspend NULL +#define at91_i2s_resume NULL +#endif + +#define AT91_I2S_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 |\ + SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 |\ + SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_44100 |\ + SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_88200 |\ + SNDRV_PCM_RATE_96000) + +struct snd_soc_cpu_dai at91_i2s_dai[NUM_SSC_DEVICES] = { + { .name = "at91_ssc0/i2s", + .id = 0, + .type = SND_SOC_DAI_I2S, + .suspend = at91_i2s_suspend, + .resume = at91_i2s_resume, + .playback = { + .channels_min = 1, + .channels_max = 2, + .rates = AT91_I2S_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .capture = { + .channels_min = 1, + .channels_max = 2, + .rates = AT91_I2S_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .startup = at91_i2s_startup, + .shutdown = at91_i2s_shutdown, + .prepare = at91_i2s_prepare, + .hw_params = at91_i2s_hw_params,}, + .dai_ops = { + .set_sysclk = at91_i2s_set_dai_sysclk, + .set_fmt = at91_i2s_set_dai_fmt, + .set_clkdiv = at91_i2s_set_dai_clkdiv,}, + .private_data = &ssc_info[0].ssc, + }, +#if NUM_SSC_DEVICES == 3 + { .name = "at91_ssc1/i2s", + .id = 1, + .type = SND_SOC_DAI_I2S, + .suspend = at91_i2s_suspend, + .resume = at91_i2s_resume, + .playback = { + .channels_min = 1, + .channels_max = 2, + .rates = AT91_I2S_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .capture = { + .channels_min = 1, + .channels_max = 2, + .rates = AT91_I2S_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .startup = at91_i2s_startup, + .shutdown = at91_i2s_shutdown, + .prepare = at91_i2s_prepare, + .hw_params = at91_i2s_hw_params,}, + .dai_ops = { + .set_sysclk = at91_i2s_set_dai_sysclk, + .set_fmt = at91_i2s_set_dai_fmt, + .set_clkdiv = at91_i2s_set_dai_clkdiv,}, + .private_data = &ssc_info[1].ssc, + }, + { .name = "at91_ssc2/i2s", + .id = 2, + .type = SND_SOC_DAI_I2S, + .suspend = at91_i2s_suspend, + .resume = at91_i2s_resume, + .playback = { + .channels_min = 1, + .channels_max = 2, + .rates = AT91_I2S_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .capture = { + .channels_min = 1, + .channels_max = 2, + .rates = AT91_I2S_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .startup = at91_i2s_startup, + .shutdown = at91_i2s_shutdown, + .prepare = at91_i2s_prepare, + .hw_params = at91_i2s_hw_params,}, + .dai_ops = { + .set_sysclk = at91_i2s_set_dai_sysclk, + .set_fmt = at91_i2s_set_dai_fmt, + .set_clkdiv = at91_i2s_set_dai_clkdiv,}, + .private_data = &ssc_info[2].ssc, + }, +#endif +}; + +EXPORT_SYMBOL_GPL(at91_i2s_dai); + +/* Module information */ +MODULE_AUTHOR("Frank Mandarino, fmandarino@endrelia.com, www.endrelia.com"); +MODULE_DESCRIPTION("AT91 I2S ASoC Interface"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/at91/at91-i2s.h b/sound/soc/at91/at91-i2s.h new file mode 100644 index 00000000000..f8a875ba0cc --- /dev/null +++ b/sound/soc/at91/at91-i2s.h @@ -0,0 +1,27 @@ +/* + * at91-i2s.h - ALSA I2S interface for the Atmel AT91 SoC + * + * Author: Frank Mandarino <fmandarino@endrelia.com> + * Endrelia Technologies Inc. + * Created: Jan 9, 2007 + * + * 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. + */ + +#ifndef _AT91_I2S_H +#define _AT91_I2S_H + +/* I2S system clock ids */ +#define AT91_SYSCLK_MCK 0 /* SSC uses AT91 MCK as system clock */ + +/* I2S divider ids */ +#define AT91SSC_CMR_DIV 0 /* MCK divider for BCLK */ +#define AT91SSC_TCMR_PERIOD 1 /* BCLK divider for transmit FS */ +#define AT91SSC_RCMR_PERIOD 2 /* BCLK divider for receive FS */ + +extern struct snd_soc_cpu_dai at91_i2s_dai[]; + +#endif /* _AT91_I2S_H */ + diff --git a/sound/soc/at91/at91-pcm.c b/sound/soc/at91/at91-pcm.c new file mode 100644 index 00000000000..e88b12e7cc4 --- /dev/null +++ b/sound/soc/at91/at91-pcm.c @@ -0,0 +1,432 @@ +/* + * at91-pcm.c -- ALSA PCM interface for the Atmel AT91 SoC + * + * Author: Frank Mandarino <fmandarino@endrelia.com> + * Endrelia Technologies Inc. + * Created: Mar 3, 2006 + * + * Based on pxa2xx-pcm.c by: + * + * Author: Nicolas Pitre + * Created: Nov 30, 2004 + * Copyright: (C) 2004 MontaVista Software, 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. + */ + +#include <linux/module.h> +#include <linux/init.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/dma-mapping.h> + +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> + +#include <asm/arch/hardware.h> +#include <asm/arch/at91_ssc.h> +#include <asm/arch/at91_pdc.h> + +#include "at91-pcm.h" + +#if 0 +#define DBG(x...) printk(KERN_INFO "at91-pcm: " x) +#else +#define DBG(x...) +#endif + +static const struct snd_pcm_hardware at91_pcm_hardware = { + .info = SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_PAUSE, + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .period_bytes_min = 32, + .period_bytes_max = 8192, + .periods_min = 2, + .periods_max = 1024, + .buffer_bytes_max = 32 * 1024, +}; + +struct at91_runtime_data { + struct at91_pcm_dma_params *params; + dma_addr_t dma_buffer; /* physical address of dma buffer */ + dma_addr_t dma_buffer_end; /* first address beyond DMA buffer */ + size_t period_size; + dma_addr_t period_ptr; /* physical address of next period */ + u32 pdc_xpr_save; /* PDC register save */ + u32 pdc_xcr_save; + u32 pdc_xnpr_save; + u32 pdc_xncr_save; +}; + +static void at91_pcm_dma_irq(u32 ssc_sr, + struct snd_pcm_substream *substream) +{ + struct at91_runtime_data *prtd = substream->runtime->private_data; + struct at91_pcm_dma_params *params = prtd->params; + static int count = 0; + + count++; + + if (ssc_sr & params->mask->ssc_endbuf) { + + printk(KERN_WARNING + "at91-pcm: buffer %s on %s (SSC_SR=%#x, count=%d)\n", + substream->stream == SNDRV_PCM_STREAM_PLAYBACK + ? "underrun" : "overrun", + params->name, ssc_sr, count); + + /* re-start the PDC */ + at91_ssc_write(params->ssc_base + AT91_PDC_PTCR, params->mask->pdc_disable); + + prtd->period_ptr += prtd->period_size; + if (prtd->period_ptr >= prtd->dma_buffer_end) { + prtd->period_ptr = prtd->dma_buffer; + } + + at91_ssc_write(params->ssc_base + params->pdc->xpr, prtd->period_ptr); + at91_ssc_write(params->ssc_base + params->pdc->xcr, + prtd->period_size / params->pdc_xfer_size); + + at91_ssc_write(params->ssc_base + AT91_PDC_PTCR, params->mask->pdc_enable); + } + + if (ssc_sr & params->mask->ssc_endx) { + + /* Load the PDC next pointer and counter registers */ + prtd->period_ptr += prtd->period_size; + if (prtd->period_ptr >= prtd->dma_buffer_end) { + prtd->period_ptr = prtd->dma_buffer; + } + at91_ssc_write(params->ssc_base + params->pdc->xnpr, prtd->period_ptr); + at91_ssc_write(params->ssc_base + params->pdc->xncr, + prtd->period_size / params->pdc_xfer_size); + } + + snd_pcm_period_elapsed(substream); +} + +static int at91_pcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct at91_runtime_data *prtd = runtime->private_data; + struct snd_soc_pcm_runtime *rtd = substream->private_data; + + /* this may get called several times by oss emulation + * with different params */ + + snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); + runtime->dma_bytes = params_buffer_bytes(params); + + prtd->params = rtd->dai->cpu_dai->dma_data; + prtd->params->dma_intr_handler = at91_pcm_dma_irq; + + prtd->dma_buffer = runtime->dma_addr; + prtd->dma_buffer_end = runtime->dma_addr + runtime->dma_bytes; + prtd->period_size = params_period_bytes(params); + + DBG("hw_params: DMA for %s initialized (dma_bytes=%d, period_size=%d)\n", + prtd->params->name, runtime->dma_bytes, prtd->period_size); + return 0; +} + +static int at91_pcm_hw_free(struct snd_pcm_substream *substream) +{ + struct at91_runtime_data *prtd = substream->runtime->private_data; + struct at91_pcm_dma_params *params = prtd->params; + + if (params != NULL) { + at91_ssc_write(params->ssc_base + AT91_PDC_PTCR, params->mask->pdc_disable); + prtd->params->dma_intr_handler = NULL; + } + + return 0; +} + +static int at91_pcm_prepare(struct snd_pcm_substream *substream) +{ + struct at91_runtime_data *prtd = substream->runtime->private_data; + struct at91_pcm_dma_params *params = prtd->params; + + at91_ssc_write(params->ssc_base + AT91_SSC_IDR, + params->mask->ssc_endx | params->mask->ssc_endbuf); + + at91_ssc_write(params->ssc_base + AT91_PDC_PTCR, params->mask->pdc_disable); + return 0; +} + +static int at91_pcm_trigger(struct snd_pcm_substream *substream, + int cmd) +{ + struct at91_runtime_data *prtd = substream->runtime->private_data; + struct at91_pcm_dma_params *params = prtd->params; + int ret = 0; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + prtd->period_ptr = prtd->dma_buffer; + + at91_ssc_write(params->ssc_base + params->pdc->xpr, prtd->period_ptr); + at91_ssc_write(params->ssc_base + params->pdc->xcr, + prtd->period_size / params->pdc_xfer_size); + + prtd->period_ptr += prtd->period_size; + at91_ssc_write(params->ssc_base + params->pdc->xnpr, prtd->period_ptr); + at91_ssc_write(params->ssc_base + params->pdc->xncr, + prtd->period_size / params->pdc_xfer_size); + + DBG("trigger: period_ptr=%lx, xpr=%lx, xcr=%ld, xnpr=%lx, xncr=%ld\n", + (unsigned long) prtd->period_ptr, + at91_ssc_read(params->ssc_base + params->pdc->xpr), + at91_ssc_read(params->ssc_base + params->pdc->xcr), + at91_ssc_read(params->ssc_base + params->pdc->xnpr), + at91_ssc_read(params->ssc_base + params->pdc->xncr)); + + at91_ssc_write(params->ssc_base + AT91_SSC_IER, + params->mask->ssc_endx | params->mask->ssc_endbuf); + + at91_ssc_write(params->ssc_base + AT91_PDC_PTCR, params->mask->pdc_enable); + + DBG("sr=%lx imr=%lx\n", at91_ssc_read(params->ssc_base + AT91_SSC_SR), + at91_ssc_read(params->ssc_base + AT91_SSC_IER)); + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + at91_ssc_write(params->ssc_base + AT91_PDC_PTCR, params->mask->pdc_disable); + break; + + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + at91_ssc_write(params->ssc_base + AT91_PDC_PTCR, params->mask->pdc_enable); + break; + + default: + ret = -EINVAL; + } + + return ret; +} + +static snd_pcm_uframes_t at91_pcm_pointer( + struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct at91_runtime_data *prtd = runtime->private_data; + struct at91_pcm_dma_params *params = prtd->params; + dma_addr_t ptr; + snd_pcm_uframes_t x; + + ptr = (dma_addr_t) at91_ssc_read(params->ssc_base + params->pdc->xpr); + x = bytes_to_frames(runtime, ptr - prtd->dma_buffer); + + if (x == runtime->buffer_size) + x = 0; + return x; +} + +static int at91_pcm_open(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct at91_runtime_data *prtd; + int ret = 0; + + snd_soc_set_runtime_hwparams(substream, &at91_pcm_hardware); + + /* ensure that buffer size is a multiple of period size */ + ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS); + if (ret < 0) + goto out; + + prtd = kzalloc(sizeof(struct at91_runtime_data), GFP_KERNEL); + if (prtd == NULL) { + ret = -ENOMEM; + goto out; + } + runtime->private_data = prtd; + + out: + return ret; +} + +static int at91_pcm_close(struct snd_pcm_substream *substream) +{ + struct at91_runtime_data *prtd = substream->runtime->private_data; + + kfree(prtd); + return 0; +} + +static int at91_pcm_mmap(struct snd_pcm_substream *substream, + struct vm_area_struct *vma) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + + return dma_mmap_writecombine(substream->pcm->card->dev, vma, + runtime->dma_area, + runtime->dma_addr, + runtime->dma_bytes); +} + +struct snd_pcm_ops at91_pcm_ops = { + .open = at91_pcm_open, + .close = at91_pcm_close, + .ioctl = snd_pcm_lib_ioctl, + .hw_params = at91_pcm_hw_params, + .hw_free = at91_pcm_hw_free, + .prepare = at91_pcm_prepare, + .trigger = at91_pcm_trigger, + .pointer = at91_pcm_pointer, + .mmap = at91_pcm_mmap, +}; + +static int at91_pcm_preallocate_dma_buffer(struct snd_pcm *pcm, + int stream) +{ + struct snd_pcm_substream *substream = pcm->streams[stream].substream; + struct snd_dma_buffer *buf = &substream->dma_buffer; + size_t size = at91_pcm_hardware.buffer_bytes_max; + + buf->dev.type = SNDRV_DMA_TYPE_DEV; + buf->dev.dev = pcm->card->dev; + buf->private_data = NULL; + buf->area = dma_alloc_writecombine(pcm->card->dev, size, + &buf->addr, GFP_KERNEL); + + DBG("preallocate_dma_buffer: area=%p, addr=%p, size=%d\n", + (void *) buf->area, + (void *) buf->addr, + size); + + if (!buf->area) + return -ENOMEM; + + buf->bytes = size; + return 0; +} + +static u64 at91_pcm_dmamask = 0xffffffff; + +static int at91_pcm_new(struct snd_card *card, + struct snd_soc_codec_dai *dai, struct snd_pcm *pcm) +{ + int ret = 0; + + if (!card->dev->dma_mask) + card->dev->dma_mask = &at91_pcm_dmamask; + if (!card->dev->coherent_dma_mask) + card->dev->coherent_dma_mask = 0xffffffff; + + if (dai->playback.channels_min) { + ret = at91_pcm_preallocate_dma_buffer(pcm, + SNDRV_PCM_STREAM_PLAYBACK); + if (ret) + goto out; + } + + if (dai->capture.channels_min) { + ret = at91_pcm_preallocate_dma_buffer(pcm, + SNDRV_PCM_STREAM_CAPTURE); + if (ret) + goto out; + } + out: + return ret; +} + +static void at91_pcm_free_dma_buffers(struct snd_pcm *pcm) +{ + struct snd_pcm_substream *substream; + struct snd_dma_buffer *buf; + int stream; + + for (stream = 0; stream < 2; stream++) { + substream = pcm->streams[stream].substream; + if (!substream) + continue; + + buf = &substream->dma_buffer; + if (!buf->area) + continue; + + dma_free_writecombine(pcm->card->dev, buf->bytes, + buf->area, buf->addr); + buf->area = NULL; + } +} + +#ifdef CONFIG_PM +static int at91_pcm_suspend(struct platform_device *pdev, + struct snd_soc_cpu_dai *dai) +{ + struct snd_pcm_runtime *runtime = dai->runtime; + struct at91_runtime_data *prtd; + struct at91_pcm_dma_params *params; + + if (!runtime) + return 0; + + prtd = runtime->private_data; + params = prtd->params; + + /* disable the PDC and save the PDC registers */ + + at91_ssc_write(params->ssc_base + AT91_PDC_PTCR, params->mask->pdc_disable); + + prtd->pdc_xpr_save = at91_ssc_read(params->ssc_base + params->pdc->xpr); + prtd->pdc_xcr_save = at91_ssc_read(params->ssc_base + params->pdc->xcr); + prtd->pdc_xnpr_save = at91_ssc_read(params->ssc_base + params->pdc->xnpr); + prtd->pdc_xncr_save = at91_ssc_read(params->ssc_base + params->pdc->xncr); + + return 0; +} + +static int at91_pcm_resume(struct platform_device *pdev, + struct snd_soc_cpu_dai *dai) +{ + struct snd_pcm_runtime *runtime = dai->runtime; + struct at91_runtime_data *prtd; + struct at91_pcm_dma_params *params; + + if (!runtime) + return 0; + + prtd = runtime->private_data; + params = prtd->params; + + /* restore the PDC registers and enable the PDC */ + at91_ssc_write(params->ssc_base + params->pdc->xpr, prtd->pdc_xpr_save); + at91_ssc_write(params->ssc_base + params->pdc->xcr, prtd->pdc_xcr_save); + at91_ssc_write(params->ssc_base + params->pdc->xnpr, prtd->pdc_xnpr_save); + at91_ssc_write(params->ssc_base + params->pdc->xncr, prtd->pdc_xncr_save); + + at91_ssc_write(params->ssc_base + AT91_PDC_PTCR, params->mask->pdc_enable); + return 0; +} +#else +#define at91_pcm_suspend NULL +#define at91_pcm_resume NULL +#endif + +struct snd_soc_platform at91_soc_platform = { + .name = "at91-audio", + .pcm_ops = &at91_pcm_ops, + .pcm_new = at91_pcm_new, + .pcm_free = at91_pcm_free_dma_buffers, + .suspend = at91_pcm_suspend, + .resume = at91_pcm_resume, +}; + +EXPORT_SYMBOL_GPL(at91_soc_platform); + +MODULE_AUTHOR("Frank Mandarino <fmandarino@endrelia.com>"); +MODULE_DESCRIPTION("Atmel AT91 PCM module"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/at91/at91-pcm.h b/sound/soc/at91/at91-pcm.h new file mode 100644 index 00000000000..58d0f00a07b --- /dev/null +++ b/sound/soc/at91/at91-pcm.h @@ -0,0 +1,72 @@ +/* + * at91-pcm.h - ALSA PCM interface for the Atmel AT91 SoC + * + * Author: Frank Mandarino <fmandarino@endrelia.com> + * Endrelia Technologies Inc. + * Created: Mar 3, 2006 + * + * Based on pxa2xx-pcm.h by: + * + * Author: Nicolas Pitre + * Created: Nov 30, 2004 + * Copyright: MontaVista Software, 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. + */ + +#ifndef _AT91_PCM_H +#define _AT91_PCM_H + +#include <asm/arch/hardware.h> + +struct at91_ssc_periph { + void __iomem *base; + u32 pid; +}; + +/* + * Registers and status bits that are required by the PCM driver. + */ +struct at91_pdc_regs { + unsigned int xpr; /* PDC recv/trans pointer */ + unsigned int xcr; /* PDC recv/trans counter */ + unsigned int xnpr; /* PDC next recv/trans pointer */ + unsigned int xncr; /* PDC next recv/trans counter */ + unsigned int ptcr; /* PDC transfer control */ +}; + +struct at91_ssc_mask { + u32 ssc_enable; /* SSC recv/trans enable */ + u32 ssc_disable; /* SSC recv/trans disable */ + u32 ssc_endx; /* SSC ENDTX or ENDRX */ + u32 ssc_endbuf; /* SSC TXBUFE or RXBUFF */ + u32 pdc_enable; /* PDC recv/trans enable */ + u32 pdc_disable; /* PDC recv/trans disable */ +}; + +/* + * This structure, shared between the PCM driver and the interface, + * contains all information required by the PCM driver to perform the + * PDC DMA operation. All fields except dma_intr_handler() are initialized + * by the interface. The dms_intr_handler() pointer is set by the PCM + * driver and called by the interface SSC interrupt handler if it is + * non-NULL. + */ +struct at91_pcm_dma_params { + char *name; /* stream identifier */ + int pdc_xfer_size; /* PDC counter increment in bytes */ + void __iomem *ssc_base; /* SSC base address */ + struct at91_pdc_regs *pdc; /* PDC receive or transmit registers */ + struct at91_ssc_mask *mask;/* SSC & PDC status bits */ + struct snd_pcm_substream *substream; + void (*dma_intr_handler)(u32, struct snd_pcm_substream *); +}; + +extern struct snd_soc_platform at91_soc_platform; + +#define at91_ssc_read(a) ((unsigned long) __raw_readl(a)) +#define at91_ssc_write(a,v) __raw_writel((v),(a)) + +#endif /* _AT91_PCM_H */ diff --git a/sound/soc/at91/eti_b1_wm8731.c b/sound/soc/at91/eti_b1_wm8731.c new file mode 100644 index 00000000000..8179df3bb2f --- /dev/null +++ b/sound/soc/at91/eti_b1_wm8731.c @@ -0,0 +1,375 @@ +/* + * eti_b1_wm8731 -- SoC audio for AT91RM9200-based Endrelia ETI_B1 board. + * + * Author: Frank Mandarino <fmandarino@endrelia.com> + * Endrelia Technologies Inc. + * Created: Mar 29, 2006 + * + * Based on corgi.c by: + * + * Copyright 2005 Wolfson Microelectronics PLC. + * Copyright 2005 Openedhand Ltd. + * + * Authors: Liam Girdwood <liam.girdwood@wolfsonmicro.com> + * Richard Purdie <richard@openedhand.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + */ + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/version.h> +#include <linux/kernel.h> +#include <linux/clk.h> +#include <linux/timer.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> + +#include <asm/arch/hardware.h> +#include <asm/arch/at91_pio.h> +#include <asm/arch/gpio.h> + +#include "../codecs/wm8731.h" +#include "at91-pcm.h" +#include "at91-i2s.h" + +#if 0 +#define DBG(x...) printk(KERN_INFO "eti_b1_wm8731: " x) +#else +#define DBG(x...) +#endif + +#define AT91_PIO_TF1 (1 << (AT91_PIN_PB6 - PIN_BASE) % 32) +#define AT91_PIO_TK1 (1 << (AT91_PIN_PB7 - PIN_BASE) % 32) +#define AT91_PIO_TD1 (1 << (AT91_PIN_PB8 - PIN_BASE) % 32) +#define AT91_PIO_RD1 (1 << (AT91_PIN_PB9 - PIN_BASE) % 32) +#define AT91_PIO_RK1 (1 << (AT91_PIN_PB10 - PIN_BASE) % 32) +#define AT91_PIO_RF1 (1 << (AT91_PIN_PB11 - PIN_BASE) % 32) + +static struct clk *pck1_clk; +static struct clk *pllb_clk; + + +static int eti_b1_startup(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec_dai *codec_dai = rtd->dai->codec_dai; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + int ret; + + /* cpu clock is the AT91 master clock sent to the SSC */ + ret = cpu_dai->dai_ops.set_sysclk(cpu_dai, AT91_SYSCLK_MCK, + 60000000, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + /* codec system clock is supplied by PCK1, set to 12MHz */ + ret = codec_dai->dai_ops.set_sysclk(codec_dai, WM8731_SYSCLK, + 12000000, SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + /* Start PCK1 clock. */ + clk_enable(pck1_clk); + DBG("pck1 started\n"); + + return 0; +} + +static void eti_b1_shutdown(struct snd_pcm_substream *substream) +{ + /* Stop PCK1 clock. */ + clk_disable(pck1_clk); + DBG("pck1 stopped\n"); +} + +static int eti_b1_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec_dai *codec_dai = rtd->dai->codec_dai; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + int ret; + +#ifdef CONFIG_SND_AT91_SOC_ETI_SLAVE + unsigned int rate; + int cmr_div, period; + + /* set codec DAI configuration */ + ret = codec_dai->dai_ops.set_fmt(codec_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS); + if (ret < 0) + return ret; + + /* set cpu DAI configuration */ + ret = cpu_dai->dai_ops.set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS); + if (ret < 0) + return ret; + + /* + * The SSC clock dividers depend on the sample rate. The CMR.DIV + * field divides the system master clock MCK to drive the SSC TK + * signal which provides the codec BCLK. The TCMR.PERIOD and + * RCMR.PERIOD fields further divide the BCLK signal to drive + * the SSC TF and RF signals which provide the codec DACLRC and + * ADCLRC clocks. + * + * The dividers were determined through trial and error, where a + * CMR.DIV value is chosen such that the resulting BCLK value is + * divisible, or almost divisible, by (2 * sample rate), and then + * the TCMR.PERIOD or RCMR.PERIOD is BCLK / (2 * sample rate) - 1. + */ + rate = params_rate(params); + + switch (rate) { + case 8000: + cmr_div = 25; /* BCLK = 60MHz/(2*25) = 1.2MHz */ + period = 74; /* LRC = BCLK/(2*(74+1)) = 8000Hz */ + break; + case 32000: + cmr_div = 7; /* BCLK = 60MHz/(2*7) ~= 4.28571428MHz */ + period = 66; /* LRC = BCLK/(2*(66+1)) = 31982.942Hz */ + break; + case 48000: + cmr_div = 13; /* BCLK = 60MHz/(2*13) ~= 2.3076923MHz */ + period = 23; /* LRC = BCLK/(2*(23+1)) = 48076.923Hz */ + break; + default: + printk(KERN_WARNING "unsupported rate %d on ETI-B1 board\n", rate); + return -EINVAL; + } + + /* set the MCK divider for BCLK */ + ret = cpu_dai->dai_ops.set_clkdiv(cpu_dai, AT91SSC_CMR_DIV, cmr_div); + if (ret < 0) + return ret; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + /* set the BCLK divider for DACLRC */ + ret = cpu_dai->dai_ops.set_clkdiv(cpu_dai, + AT91SSC_TCMR_PERIOD, period); + } else { + /* set the BCLK divider for ADCLRC */ + ret = cpu_dai->dai_ops.set_clkdiv(cpu_dai, + AT91SSC_RCMR_PERIOD, period); + } + if (ret < 0) + return ret; + +#else /* CONFIG_SND_AT91_SOC_ETI_SLAVE */ + /* + * Codec in Master Mode. + */ + + /* set codec DAI configuration */ + ret = codec_dai->dai_ops.set_fmt(codec_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM); + if (ret < 0) + return ret; + + /* set cpu DAI configuration */ + ret = cpu_dai->dai_ops.set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM); + if (ret < 0) + return ret; + +#endif /* CONFIG_SND_AT91_SOC_ETI_SLAVE */ + + return 0; +} + +static struct snd_soc_ops eti_b1_ops = { + .startup = eti_b1_startup, + .hw_params = eti_b1_hw_params, + .shutdown = eti_b1_shutdown, +}; + + +static const struct snd_soc_dapm_widget eti_b1_dapm_widgets[] = { + SND_SOC_DAPM_MIC("Int Mic", NULL), + SND_SOC_DAPM_SPK("Ext Spk", NULL), +}; + +static const char *intercon[][3] = { + + /* speaker connected to LHPOUT */ + {"Ext Spk", NULL, "LHPOUT"}, + + /* mic is connected to Mic Jack, with WM8731 Mic Bias */ + {"MICIN", NULL, "Mic Bias"}, + {"Mic Bias", NULL, "Int Mic"}, + + /* terminator */ + {NULL, NULL, NULL}, +}; + +/* + * Logic for a wm8731 as connected on a Endrelia ETI-B1 board. + */ +static int eti_b1_wm8731_init(struct snd_soc_codec *codec) +{ + int i; + + DBG("eti_b1_wm8731_init() called\n"); + + /* Add specific widgets */ + for(i = 0; i < ARRAY_SIZE(eti_b1_dapm_widgets); i++) { + snd_soc_dapm_new_control(codec, &eti_b1_dapm_widgets[i]); + } + + /* Set up specific audio path interconnects */ + for(i = 0; intercon[i][0] != NULL; i++) { + snd_soc_dapm_connect_input(codec, intercon[i][0], + intercon[i][1], intercon[i][2]); + } + + /* not connected */ + snd_soc_dapm_set_endpoint(codec, "RLINEIN", 0); + snd_soc_dapm_set_endpoint(codec, "LLINEIN", 0); + + /* always connected */ + snd_soc_dapm_set_endpoint(codec, "Int Mic", 1); + snd_soc_dapm_set_endpoint(codec, "Ext Spk", 1); + + snd_soc_dapm_sync_endpoints(codec); + + return 0; +} + +static struct snd_soc_dai_link eti_b1_dai = { + .name = "WM8731", + .stream_name = "WM8731", + .cpu_dai = &at91_i2s_dai[1], + .codec_dai = &wm8731_dai, + .init = eti_b1_wm8731_init, + .ops = &eti_b1_ops, +}; + +static struct snd_soc_machine snd_soc_machine_eti_b1 = { + .name = "ETI_B1", + .dai_link = &eti_b1_dai, + .num_links = 1, +}; + +static struct wm8731_setup_data eti_b1_wm8731_setup = { + .i2c_address = 0x1a, +}; + +static struct snd_soc_device eti_b1_snd_devdata = { + .machine = &snd_soc_machine_eti_b1, + .platform = &at91_soc_platform, + .codec_dev = &soc_codec_dev_wm8731, + .codec_data = &eti_b1_wm8731_setup, +}; + +static struct platform_device *eti_b1_snd_device; + +static int __init eti_b1_init(void) +{ + int ret; + u32 ssc_pio_lines; + struct at91_ssc_periph *ssc = eti_b1_dai.cpu_dai->private_data; + + if (!request_mem_region(AT91RM9200_BASE_SSC1, SZ_16K, "soc-audio")) { + DBG("SSC1 memory region is busy\n"); + return -EBUSY; + } + + ssc->base = ioremap(AT91RM9200_BASE_SSC1, SZ_16K); + if (!ssc->base) { + DBG("SSC1 memory ioremap failed\n"); + ret = -ENOMEM; + goto fail_release_mem; + } + + ssc->pid = AT91RM9200_ID_SSC1; + + eti_b1_snd_device = platform_device_alloc("soc-audio", -1); + if (!eti_b1_snd_device) { + DBG("platform device allocation failed\n"); + ret = -ENOMEM; + goto fail_io_unmap; + } + + platform_set_drvdata(eti_b1_snd_device, &eti_b1_snd_devdata); + eti_b1_snd_devdata.dev = &eti_b1_snd_device->dev; + + ret = platform_device_add(eti_b1_snd_device); + if (ret) { + DBG("platform device add failed\n"); + platform_device_put(eti_b1_snd_device); + goto fail_io_unmap; + } + + ssc_pio_lines = AT91_PIO_TF1 | AT91_PIO_TK1 | AT91_PIO_TD1 + | AT91_PIO_RD1 /* | AT91_PIO_RK1 */ | AT91_PIO_RF1; + + /* Reset all PIO registers and assign lines to peripheral A */ + at91_sys_write(AT91_PIOB + PIO_PDR, ssc_pio_lines); + at91_sys_write(AT91_PIOB + PIO_ODR, ssc_pio_lines); + at91_sys_write(AT91_PIOB + PIO_IFDR, ssc_pio_lines); + at91_sys_write(AT91_PIOB + PIO_CODR, ssc_pio_lines); + at91_sys_write(AT91_PIOB + PIO_IDR, ssc_pio_lines); + at91_sys_write(AT91_PIOB + PIO_MDDR, ssc_pio_lines); + at91_sys_write(AT91_PIOB + PIO_PUDR, ssc_pio_lines); + at91_sys_write(AT91_PIOB + PIO_ASR, ssc_pio_lines); + at91_sys_write(AT91_PIOB + PIO_OWDR, ssc_pio_lines); + + /* + * Set PCK1 parent to PLLB and its rate to 12 Mhz. + */ + pllb_clk = clk_get(NULL, "pllb"); + pck1_clk = clk_get(NULL, "pck1"); + + clk_set_parent(pck1_clk, pllb_clk); + clk_set_rate(pck1_clk, 12000000); + + DBG("MCLK rate %luHz\n", clk_get_rate(pck1_clk)); + + /* assign the GPIO pin to PCK1 */ + at91_set_B_periph(AT91_PIN_PA24, 0); + +#ifdef CONFIG_SND_AT91_SOC_ETI_SLAVE + printk(KERN_INFO "eti_b1_wm8731: Codec in Slave Mode\n"); +#else + printk(KERN_INFO "eti_b1_wm8731: Codec in Master Mode\n"); +#endif + return ret; + +fail_io_unmap: + iounmap(ssc->base); +fail_release_mem: + release_mem_region(AT91RM9200_BASE_SSC1, SZ_16K); + return ret; +} + +static void __exit eti_b1_exit(void) +{ + struct at91_ssc_periph *ssc = eti_b1_dai.cpu_dai->private_data; + + clk_put(pck1_clk); + clk_put(pllb_clk); + + platform_device_unregister(eti_b1_snd_device); + + iounmap(ssc->base); + release_mem_region(AT91RM9200_BASE_SSC1, SZ_16K); +} + +module_init(eti_b1_init); +module_exit(eti_b1_exit); + +/* Module information */ +MODULE_AUTHOR("Frank Mandarino <fmandarino@endrelia.com>"); +MODULE_DESCRIPTION("ALSA SoC ETI-B1-WM8731"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig new file mode 100644 index 00000000000..78ac2688e12 --- /dev/null +++ b/sound/soc/codecs/Kconfig @@ -0,0 +1,15 @@ +config SND_SOC_AC97_CODEC + tristate + depends SND_SOC + +config SND_SOC_WM8731 + tristate + depends SND_SOC + +config SND_SOC_WM8750 + tristate + depends SND_SOC + +config SND_SOC_WM9712 + tristate + depends SND_SOC diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile new file mode 100644 index 00000000000..3249a6e4f1d --- /dev/null +++ b/sound/soc/codecs/Makefile @@ -0,0 +1,9 @@ +snd-soc-ac97-objs := ac97.o +snd-soc-wm8731-objs := wm8731.o +snd-soc-wm8750-objs := wm8750.o +snd-soc-wm9712-objs := wm9712.o + +obj-$(CONFIG_SND_SOC_AC97_CODEC) += snd-soc-ac97.o +obj-$(CONFIG_SND_SOC_WM8731) += snd-soc-wm8731.o +obj-$(CONFIG_SND_SOC_WM8750) += snd-soc-wm8750.o +obj-$(CONFIG_SND_SOC_WM9712) += snd-soc-wm9712.o diff --git a/sound/soc/codecs/ac97.c b/sound/soc/codecs/ac97.c new file mode 100644 index 00000000000..55bc55eb6e2 --- /dev/null +++ b/sound/soc/codecs/ac97.c @@ -0,0 +1,156 @@ +/* + * ac97.c -- ALSA Soc AC97 codec support + * + * Copyright 2005 Wolfson Microelectronics PLC. + * Author: Liam Girdwood + * liam.girdwood@wolfsonmicro.com or linux@wolfsonmicro.com + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Revision history + * 17th Oct 2005 Initial version. + * + * Generic AC97 support. + */ + +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/device.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/ac97_codec.h> +#include <sound/initval.h> +#include <sound/soc.h> + +#define AC97_VERSION "0.6" + +static int ac97_prepare(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_codec *codec = socdev->codec; + + int reg = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) ? + AC97_PCM_FRONT_DAC_RATE : AC97_PCM_LR_ADC_RATE; + return snd_ac97_set_rate(codec->ac97, reg, runtime->rate); +} + +#define STD_AC97_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 |\ + SNDRV_PCM_RATE_22050 | SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) + +static struct snd_soc_codec_dai ac97_dai = { + .name = "AC97 HiFi", + .playback = { + .stream_name = "AC97 Playback", + .channels_min = 1, + .channels_max = 2, + .rates = STD_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .capture = { + .stream_name = "AC97 Capture", + .channels_min = 1, + .channels_max = 2, + .rates = STD_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .prepare = ac97_prepare,}, +}; + +static unsigned int ac97_read(struct snd_soc_codec *codec, + unsigned int reg) +{ + return soc_ac97_ops.read(codec->ac97, reg); +} + +static int ac97_write(struct snd_soc_codec *codec, unsigned int reg, + unsigned int val) +{ + soc_ac97_ops.write(codec->ac97, reg, val); + return 0; +} + +static int ac97_soc_probe(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec; + struct snd_ac97_bus *ac97_bus; + struct snd_ac97_template ac97_template; + int ret = 0; + + printk(KERN_INFO "AC97 SoC Audio Codec %s\n", AC97_VERSION); + + socdev->codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL); + if (socdev->codec == NULL) + return -ENOMEM; + codec = socdev->codec; + mutex_init(&codec->mutex); + + codec->name = "AC97"; + codec->owner = THIS_MODULE; + codec->dai = &ac97_dai; + codec->num_dai = 1; + codec->write = ac97_write; + codec->read = ac97_read; + INIT_LIST_HEAD(&codec->dapm_widgets); + INIT_LIST_HEAD(&codec->dapm_paths); + + /* register pcms */ + ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1); + if(ret < 0) + goto err; + + /* add codec as bus device for standard ac97 */ + ret = snd_ac97_bus(codec->card, 0, &soc_ac97_ops, NULL, &ac97_bus); + if(ret < 0) + goto bus_err; + + memset(&ac97_template, 0, sizeof(struct snd_ac97_template)); + ret = snd_ac97_mixer(ac97_bus, &ac97_template, &codec->ac97); + if(ret < 0) + goto bus_err; + + ret = snd_soc_register_card(socdev); + if (ret < 0) + goto bus_err; + return 0; + +bus_err: + snd_soc_free_pcms(socdev); + +err: + kfree(socdev->codec->reg_cache); + kfree(socdev->codec); + socdev->codec = NULL; + return ret; +} + +static int ac97_soc_remove(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + + if(codec == NULL) + return 0; + + snd_soc_free_pcms(socdev); + kfree(socdev->codec->reg_cache); + kfree(socdev->codec); + + return 0; +} + +struct snd_soc_codec_device soc_codec_dev_ac97= { + .probe = ac97_soc_probe, + .remove = ac97_soc_remove, +}; + +EXPORT_SYMBOL_GPL(soc_codec_dev_ac97); + +MODULE_DESCRIPTION("Soc Generic AC97 driver"); +MODULE_AUTHOR("Liam Girdwood"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/ac97.h b/sound/soc/codecs/ac97.h new file mode 100644 index 00000000000..930ddfc2321 --- /dev/null +++ b/sound/soc/codecs/ac97.h @@ -0,0 +1,18 @@ +/* + * linux/sound/codecs/ac97.h -- ALSA SoC Layer + * + * Author: Liam Girdwood + * Created: Dec 1st 2005 + * Copyright: Wolfson Microelectronics. PLC. + * + * 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. + */ + +#ifndef __LINUX_SND_SOC_AC97_H +#define __LINUX_SND_SOC_AC97_H + +extern struct snd_soc_codec_device soc_codec_dev_ac97; + +#endif diff --git a/sound/soc/codecs/wm8731.c b/sound/soc/codecs/wm8731.c new file mode 100644 index 00000000000..7ca0b526828 --- /dev/null +++ b/sound/soc/codecs/wm8731.c @@ -0,0 +1,758 @@ +/* + * wm8731.c -- WM8731 ALSA SoC Audio driver + * + * Copyright 2005 Openedhand Ltd. + * + * Author: Richard Purdie <richard@openedhand.com> + * + * Based on wm8753.c by Liam Girdwood + * + * 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 <linux/moduleparam.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/pm.h> +#include <linux/i2c.h> +#include <linux/platform_device.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/initval.h> + +#include "wm8731.h" + +#define AUDIO_NAME "wm8731" +#define WM8731_VERSION "0.13" + +/* + * Debug + */ + +#define WM8731_DEBUG 0 + +#ifdef WM8731_DEBUG +#define dbg(format, arg...) \ + printk(KERN_DEBUG AUDIO_NAME ": " format "\n" , ## arg) +#else +#define dbg(format, arg...) do {} while (0) +#endif +#define err(format, arg...) \ + printk(KERN_ERR AUDIO_NAME ": " format "\n" , ## arg) +#define info(format, arg...) \ + printk(KERN_INFO AUDIO_NAME ": " format "\n" , ## arg) +#define warn(format, arg...) \ + printk(KERN_WARNING AUDIO_NAME ": " format "\n" , ## arg) + +struct snd_soc_codec_device soc_codec_dev_wm8731; + +/* codec private data */ +struct wm8731_priv { + unsigned int sysclk; +}; + +/* + * wm8731 register cache + * We can't read the WM8731 register space when we are + * using 2 wire for device control, so we cache them instead. + * There is no point in caching the reset register + */ +static const u16 wm8731_reg[WM8731_CACHEREGNUM] = { + 0x0097, 0x0097, 0x0079, 0x0079, + 0x000a, 0x0008, 0x009f, 0x000a, + 0x0000, 0x0000 +}; + +/* + * read wm8731 register cache + */ +static inline unsigned int wm8731_read_reg_cache(struct snd_soc_codec *codec, + unsigned int reg) +{ + u16 *cache = codec->reg_cache; + if (reg == WM8731_RESET) + return 0; + if (reg >= WM8731_CACHEREGNUM) + return -1; + return cache[reg]; +} + +/* + * write wm8731 register cache + */ +static inline void wm8731_write_reg_cache(struct snd_soc_codec *codec, + u16 reg, unsigned int value) +{ + u16 *cache = codec->reg_cache; + if (reg >= WM8731_CACHEREGNUM) + return; + cache[reg] = value; +} + +/* + * write to the WM8731 register space + */ +static int wm8731_write(struct snd_soc_codec *codec, unsigned int reg, + unsigned int value) +{ + u8 data[2]; + + /* data is + * D15..D9 WM8731 register offset + * D8...D0 register data + */ + data[0] = (reg << 1) | ((value >> 8) & 0x0001); + data[1] = value & 0x00ff; + + wm8731_write_reg_cache (codec, reg, value); + if (codec->hw_write(codec->control_data, data, 2) == 2) + return 0; + else + return -EIO; +} + +#define wm8731_reset(c) wm8731_write(c, WM8731_RESET, 0) + +static const char *wm8731_input_select[] = {"Line In", "Mic"}; +static const char *wm8731_deemph[] = {"None", "32Khz", "44.1Khz", "48Khz"}; + +static const struct soc_enum wm8731_enum[] = { + SOC_ENUM_SINGLE(WM8731_APANA, 2, 2, wm8731_input_select), + SOC_ENUM_SINGLE(WM8731_APDIGI, 1, 4, wm8731_deemph), +}; + +static const struct snd_kcontrol_new wm8731_snd_controls[] = { + +SOC_DOUBLE_R("Master Playback Volume", WM8731_LOUT1V, WM8731_ROUT1V, + 0, 127, 0), +SOC_DOUBLE_R("Master Playback ZC Switch", WM8731_LOUT1V, WM8731_ROUT1V, + 7, 1, 0), + +SOC_DOUBLE_R("Capture Volume", WM8731_LINVOL, WM8731_RINVOL, 0, 31, 0), +SOC_DOUBLE_R("Line Capture Switch", WM8731_LINVOL, WM8731_RINVOL, 7, 1, 1), + +SOC_SINGLE("Mic Boost (+20dB)", WM8731_APANA, 0, 1, 0), +SOC_SINGLE("Capture Mic Switch", WM8731_APANA, 1, 1, 1), + +SOC_SINGLE("Sidetone Playback Volume", WM8731_APANA, 6, 3, 1), + +SOC_SINGLE("ADC High Pass Filter Switch", WM8731_APDIGI, 0, 1, 1), +SOC_SINGLE("Store DC Offset Switch", WM8731_APDIGI, 4, 1, 0), + +SOC_ENUM("Playback De-emphasis", wm8731_enum[1]), +}; + +/* add non dapm controls */ +static int wm8731_add_controls(struct snd_soc_codec *codec) +{ + int err, i; + + for (i = 0; i < ARRAY_SIZE(wm8731_snd_controls); i++) { + if ((err = snd_ctl_add(codec->card, + snd_soc_cnew(&wm8731_snd_controls[i],codec, NULL))) < 0) + return err; + } + + return 0; +} + +/* Output Mixer */ +static const struct snd_kcontrol_new wm8731_output_mixer_controls[] = { +SOC_DAPM_SINGLE("Line Bypass Switch", WM8731_APANA, 3, 1, 0), +SOC_DAPM_SINGLE("Mic Sidetone Switch", WM8731_APANA, 5, 1, 0), +SOC_DAPM_SINGLE("HiFi Playback Switch", WM8731_APANA, 4, 1, 0), +}; + +/* Input mux */ +static const struct snd_kcontrol_new wm8731_input_mux_controls = +SOC_DAPM_ENUM("Input Select", wm8731_enum[0]); + +static const struct snd_soc_dapm_widget wm8731_dapm_widgets[] = { +SND_SOC_DAPM_MIXER("Output Mixer", WM8731_PWR, 4, 1, + &wm8731_output_mixer_controls[0], + ARRAY_SIZE(wm8731_output_mixer_controls)), +SND_SOC_DAPM_DAC("DAC", "HiFi Playback", WM8731_PWR, 3, 1), +SND_SOC_DAPM_OUTPUT("LOUT"), +SND_SOC_DAPM_OUTPUT("LHPOUT"), +SND_SOC_DAPM_OUTPUT("ROUT"), +SND_SOC_DAPM_OUTPUT("RHPOUT"), +SND_SOC_DAPM_ADC("ADC", "HiFi Capture", WM8731_PWR, 2, 1), +SND_SOC_DAPM_MUX("Input Mux", SND_SOC_NOPM, 0, 0, &wm8731_input_mux_controls), +SND_SOC_DAPM_PGA("Line Input", WM8731_PWR, 0, 1, NULL, 0), +SND_SOC_DAPM_MICBIAS("Mic Bias", WM8731_PWR, 1, 1), +SND_SOC_DAPM_INPUT("MICIN"), +SND_SOC_DAPM_INPUT("RLINEIN"), +SND_SOC_DAPM_INPUT("LLINEIN"), +}; + +static const char *intercon[][3] = { + /* output mixer */ + {"Output Mixer", "Line Bypass Switch", "Line Input"}, + {"Output Mixer", "HiFi Playback Switch", "DAC"}, + {"Output Mixer", "Mic Sidetone Switch", "Mic Bias"}, + + /* outputs */ + {"RHPOUT", NULL, "Output Mixer"}, + {"ROUT", NULL, "Output Mixer"}, + {"LHPOUT", NULL, "Output Mixer"}, + {"LOUT", NULL, "Output Mixer"}, + + /* input mux */ + {"Input Mux", "Line In", "Line Input"}, + {"Input Mux", "Mic", "Mic Bias"}, + {"ADC", NULL, "Input Mux"}, + + /* inputs */ + {"Line Input", NULL, "LLINEIN"}, + {"Line Input", NULL, "RLINEIN"}, + {"Mic Bias", NULL, "MICIN"}, + + /* terminator */ + {NULL, NULL, NULL}, +}; + +static int wm8731_add_widgets(struct snd_soc_codec *codec) +{ + int i; + + for(i = 0; i < ARRAY_SIZE(wm8731_dapm_widgets); i++) { + snd_soc_dapm_new_control(codec, &wm8731_dapm_widgets[i]); + } + + /* set up audio path interconnects */ + for(i = 0; intercon[i][0] != NULL; i++) { + snd_soc_dapm_connect_input(codec, intercon[i][0], + intercon[i][1], intercon[i][2]); + } + + snd_soc_dapm_new_widgets(codec); + return 0; +} + +struct _coeff_div { + u32 mclk; + u32 rate; + u16 fs; + u8 sr:4; + u8 bosr:1; + u8 usb:1; +}; + +/* codec mclk clock divider coefficients */ +static const struct _coeff_div coeff_div[] = { + /* 48k */ + {12288000, 48000, 256, 0x0, 0x0, 0x0}, + {18432000, 48000, 384, 0x0, 0x1, 0x0}, + {12000000, 48000, 250, 0x0, 0x0, 0x1}, + + /* 32k */ + {12288000, 32000, 384, 0x6, 0x0, 0x0}, + {18432000, 32000, 576, 0x6, 0x1, 0x0}, + {12000000, 32000, 375, 0x6, 0x0, 0x1}, + + /* 8k */ + {12288000, 8000, 1536, 0x3, 0x0, 0x0}, + {18432000, 8000, 2304, 0x3, 0x1, 0x0}, + {11289600, 8000, 1408, 0xb, 0x0, 0x0}, + {16934400, 8000, 2112, 0xb, 0x1, 0x0}, + {12000000, 8000, 1500, 0x3, 0x0, 0x1}, + + /* 96k */ + {12288000, 96000, 128, 0x7, 0x0, 0x0}, + {18432000, 96000, 192, 0x7, 0x1, 0x0}, + {12000000, 96000, 125, 0x7, 0x0, 0x1}, + + /* 44.1k */ + {11289600, 44100, 256, 0x8, 0x0, 0x0}, + {16934400, 44100, 384, 0x8, 0x1, 0x0}, + {12000000, 44100, 272, 0x8, 0x1, 0x1}, + + /* 88.2k */ + {11289600, 88200, 128, 0xf, 0x0, 0x0}, + {16934400, 88200, 192, 0xf, 0x1, 0x0}, + {12000000, 88200, 136, 0xf, 0x1, 0x1}, +}; + +static inline int get_coeff(int mclk, int rate) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(coeff_div); i++) { + if (coeff_div[i].rate == rate && coeff_div[i].mclk == mclk) + return i; + } + return 0; +} + +static int wm8731_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_codec *codec = socdev->codec; + struct wm8731_priv *wm8731 = codec->private_data; + u16 iface = wm8731_read_reg_cache(codec, WM8731_IFACE) & 0xfff3; + int i = get_coeff(wm8731->sysclk, params_rate(params)); + u16 srate = (coeff_div[i].sr << 2) | + (coeff_div[i].bosr << 1) | coeff_div[i].usb; + + wm8731_write(codec, WM8731_SRATE, srate); + + /* bit size */ + switch (params_format(params)) { + case SNDRV_PCM_FORMAT_S16_LE: + break; + case SNDRV_PCM_FORMAT_S20_3LE: + iface |= 0x0004; + break; + case SNDRV_PCM_FORMAT_S24_LE: + iface |= 0x0008; + break; + } + + wm8731_write(codec, WM8731_IFACE, iface); + return 0; +} + +static int wm8731_pcm_prepare(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_codec *codec = socdev->codec; + + /* set active */ + wm8731_write(codec, WM8731_ACTIVE, 0x0001); + + return 0; +} + +static void wm8731_shutdown(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_codec *codec = socdev->codec; + + /* deactivate */ + if (!codec->active) { + udelay(50); + wm8731_write(codec, WM8731_ACTIVE, 0x0); + } +} + +static int wm8731_mute(struct snd_soc_codec_dai *dai, int mute) +{ + struct snd_soc_codec *codec = dai->codec; + u16 mute_reg = wm8731_read_reg_cache(codec, WM8731_APDIGI) & 0xfff7; + + if (mute) + wm8731_write(codec, WM8731_APDIGI, mute_reg | 0x8); + else + wm8731_write(codec, WM8731_APDIGI, mute_reg); + return 0; +} + +static int wm8731_set_dai_sysclk(struct snd_soc_codec_dai *codec_dai, + int clk_id, unsigned int freq, int dir) +{ + struct snd_soc_codec *codec = codec_dai->codec; + struct wm8731_priv *wm8731 = codec->private_data; + + switch (freq) { + case 11289600: + case 12000000: + case 12288000: + case 16934400: + case 18432000: + wm8731->sysclk = freq; + return 0; + } + return -EINVAL; +} + + +static int wm8731_set_dai_fmt(struct snd_soc_codec_dai *codec_dai, + unsigned int fmt) +{ + struct snd_soc_codec *codec = codec_dai->codec; + u16 iface = 0; + + /* set master/slave audio interface */ + switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBM_CFM: + iface |= 0x0040; + break; + case SND_SOC_DAIFMT_CBS_CFS: + break; + default: + return -EINVAL; + } + + /* interface format */ + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + iface |= 0x0002; + break; + case SND_SOC_DAIFMT_RIGHT_J: + break; + case SND_SOC_DAIFMT_LEFT_J: + iface |= 0x0001; + break; + case SND_SOC_DAIFMT_DSP_A: + iface |= 0x0003; + break; + case SND_SOC_DAIFMT_DSP_B: + iface |= 0x0013; + break; + default: + return -EINVAL; + } + + /* clock inversion */ + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + break; + case SND_SOC_DAIFMT_IB_IF: + iface |= 0x0090; + break; + case SND_SOC_DAIFMT_IB_NF: + iface |= 0x0080; + break; + case SND_SOC_DAIFMT_NB_IF: + iface |= 0x0010; + break; + default: + return -EINVAL; + } + + /* set iface */ + wm8731_write(codec, WM8731_IFACE, iface); + return 0; +} + +static int wm8731_dapm_event(struct snd_soc_codec *codec, int event) +{ + u16 reg = wm8731_read_reg_cache(codec, WM8731_PWR) & 0xff7f; + + switch (event) { + case SNDRV_CTL_POWER_D0: /* full On */ + /* vref/mid, osc on, dac unmute */ + wm8731_write(codec, WM8731_PWR, reg); + break; + case SNDRV_CTL_POWER_D1: /* partial On */ + case SNDRV_CTL_POWER_D2: /* partial On */ + break; + case SNDRV_CTL_POWER_D3hot: /* Off, with power */ + /* everything off except vref/vmid, */ + wm8731_write(codec, WM8731_PWR, reg | 0x0040); + break; + case SNDRV_CTL_POWER_D3cold: /* Off, without power */ + /* everything off, dac mute, inactive */ + wm8731_write(codec, WM8731_ACTIVE, 0x0); + wm8731_write(codec, WM8731_PWR, 0xffff); + break; + } + codec->dapm_state = event; + return 0; +} + +#define WM8731_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 |\ + SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 |\ + SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_44100 |\ + SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_88200 |\ + SNDRV_PCM_RATE_96000) + +#define WM8731_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE |\ + SNDRV_PCM_FMTBIT_S24_LE) + +struct snd_soc_codec_dai wm8731_dai = { + .name = "WM8731", + .playback = { + .stream_name = "Playback", + .channels_min = 1, + .channels_max = 2, + .rates = WM8731_RATES, + .formats = WM8731_FORMATS,}, + .capture = { + .stream_name = "Capture", + .channels_min = 1, + .channels_max = 2, + .rates = WM8731_RATES, + .formats = WM8731_FORMATS,}, + .ops = { + .prepare = wm8731_pcm_prepare, + .hw_params = wm8731_hw_params, + .shutdown = wm8731_shutdown, + }, + .dai_ops = { + .digital_mute = wm8731_mute, + .set_sysclk = wm8731_set_dai_sysclk, + .set_fmt = wm8731_set_dai_fmt, + } +}; +EXPORT_SYMBOL_GPL(wm8731_dai); + +static int wm8731_suspend(struct platform_device *pdev, pm_message_t state) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + + wm8731_write(codec, WM8731_ACTIVE, 0x0); + wm8731_dapm_event(codec, SNDRV_CTL_POWER_D3cold); + return 0; +} + +static int wm8731_resume(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + int i; + u8 data[2]; + u16 *cache = codec->reg_cache; + + /* Sync reg_cache with the hardware */ + for (i = 0; i < ARRAY_SIZE(wm8731_reg); i++) { + data[0] = (i << 1) | ((cache[i] >> 8) & 0x0001); + data[1] = cache[i] & 0x00ff; + codec->hw_write(codec->control_data, data, 2); + } + wm8731_dapm_event(codec, SNDRV_CTL_POWER_D3hot); + wm8731_dapm_event(codec, codec->suspend_dapm_state); + return 0; +} + +/* + * initialise the WM8731 driver + * register the mixer and dsp interfaces with the kernel + */ +static int wm8731_init(struct snd_soc_device *socdev) +{ + struct snd_soc_codec *codec = socdev->codec; + int reg, ret = 0; + + codec->name = "WM8731"; + codec->owner = THIS_MODULE; + codec->read = wm8731_read_reg_cache; + codec->write = wm8731_write; + codec->dapm_event = wm8731_dapm_event; + codec->dai = &wm8731_dai; + codec->num_dai = 1; + codec->reg_cache_size = sizeof(wm8731_reg); + codec->reg_cache = kmemdup(wm8731_reg, sizeof(wm8731_reg), GFP_KERNEL); + if (codec->reg_cache == NULL) + return -ENOMEM; + + wm8731_reset(codec); + + /* register pcms */ + ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1); + if (ret < 0) { + printk(KERN_ERR "wm8731: failed to create pcms\n"); + goto pcm_err; + } + + /* power on device */ + wm8731_dapm_event(codec, SNDRV_CTL_POWER_D3hot); + + /* set the update bits */ + reg = wm8731_read_reg_cache(codec, WM8731_LOUT1V); + wm8731_write(codec, WM8731_LOUT1V, reg | 0x0100); + reg = wm8731_read_reg_cache(codec, WM8731_ROUT1V); + wm8731_write(codec, WM8731_ROUT1V, reg | 0x0100); + reg = wm8731_read_reg_cache(codec, WM8731_LINVOL); + wm8731_write(codec, WM8731_LINVOL, reg | 0x0100); + reg = wm8731_read_reg_cache(codec, WM8731_RINVOL); + wm8731_write(codec, WM8731_RINVOL, reg | 0x0100); + + wm8731_add_controls(codec); + wm8731_add_widgets(codec); + ret = snd_soc_register_card(socdev); + if (ret < 0) { + printk(KERN_ERR "wm8731: failed to register card\n"); + goto card_err; + } + + return ret; + +card_err: + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); +pcm_err: + kfree(codec->reg_cache); + return ret; +} + +static struct snd_soc_device *wm8731_socdev; + +#if defined (CONFIG_I2C) || defined (CONFIG_I2C_MODULE) + +/* + * WM8731 2 wire address is determined by GPIO5 + * state during powerup. + * low = 0x1a + * high = 0x1b + */ +static unsigned short normal_i2c[] = { 0, I2C_CLIENT_END }; + +/* Magic definition of all other variables and things */ +I2C_CLIENT_INSMOD; + +static struct i2c_driver wm8731_i2c_driver; +static struct i2c_client client_template; + +/* If the i2c layer weren't so broken, we could pass this kind of data + around */ + +static int wm8731_codec_probe(struct i2c_adapter *adap, int addr, int kind) +{ + struct snd_soc_device *socdev = wm8731_socdev; + struct wm8731_setup_data *setup = socdev->codec_data; + struct snd_soc_codec *codec = socdev->codec; + struct i2c_client *i2c; + int ret; + + if (addr != setup->i2c_address) + return -ENODEV; + + client_template.adapter = adap; + client_template.addr = addr; + + i2c = kmemdup(&client_template, sizeof(client_template), GFP_KERNEL); + if (i2c == NULL) { + kfree(codec); + return -ENOMEM; + } + i2c_set_clientdata(i2c, codec); + codec->control_data = i2c; + + ret = i2c_attach_client(i2c); + if (ret < 0) { + err("failed to attach codec at addr %x\n", addr); + goto err; + } + + ret = wm8731_init(socdev); + if (ret < 0) { + err("failed to initialise WM8731\n"); + goto err; + } + return ret; + +err: + kfree(codec); + kfree(i2c); + return ret; +} + +static int wm8731_i2c_detach(struct i2c_client *client) +{ + struct snd_soc_codec* codec = i2c_get_clientdata(client); + i2c_detach_client(client); + kfree(codec->reg_cache); + kfree(client); + return 0; +} + +static int wm8731_i2c_attach(struct i2c_adapter *adap) +{ + return i2c_probe(adap, &addr_data, wm8731_codec_probe); +} + +/* corgi i2c codec control layer */ +static struct i2c_driver wm8731_i2c_driver = { + .driver = { + .name = "WM8731 I2C Codec", + .owner = THIS_MODULE, + }, + .id = I2C_DRIVERID_WM8731, + .attach_adapter = wm8731_i2c_attach, + .detach_client = wm8731_i2c_detach, + .command = NULL, +}; + +static struct i2c_client client_template = { + .name = "WM8731", + .driver = &wm8731_i2c_driver, +}; +#endif + +static int wm8731_probe(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct wm8731_setup_data *setup; + struct snd_soc_codec *codec; + struct wm8731_priv *wm8731; + int ret = 0; + + info("WM8731 Audio Codec %s", WM8731_VERSION); + + setup = socdev->codec_data; + codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL); + if (codec == NULL) + return -ENOMEM; + + wm8731 = kzalloc(sizeof(struct wm8731_priv), GFP_KERNEL); + if (wm8731 == NULL) { + kfree(codec); + return -ENOMEM; + } + + codec->private_data = wm8731; + socdev->codec = codec; + mutex_init(&codec->mutex); + INIT_LIST_HEAD(&codec->dapm_widgets); + INIT_LIST_HEAD(&codec->dapm_paths); + + wm8731_socdev = socdev; +#if defined (CONFIG_I2C) || defined (CONFIG_I2C_MODULE) + if (setup->i2c_address) { + normal_i2c[0] = setup->i2c_address; + codec->hw_write = (hw_write_t)i2c_master_send; + ret = i2c_add_driver(&wm8731_i2c_driver); + if (ret != 0) + printk(KERN_ERR "can't add i2c driver"); + } +#else + /* Add other interfaces here */ +#endif + return ret; +} + +/* power down chip */ +static int wm8731_remove(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + + if (codec->control_data) + wm8731_dapm_event(codec, SNDRV_CTL_POWER_D3cold); + + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); +#if defined (CONFIG_I2C) || defined (CONFIG_I2C_MODULE) + i2c_del_driver(&wm8731_i2c_driver); +#endif + kfree(codec->private_data); + kfree(codec); + + return 0; +} + +struct snd_soc_codec_device soc_codec_dev_wm8731 = { + .probe = wm8731_probe, + .remove = wm8731_remove, + .suspend = wm8731_suspend, + .resume = wm8731_resume, +}; + +EXPORT_SYMBOL_GPL(soc_codec_dev_wm8731); + +MODULE_DESCRIPTION("ASoC WM8731 driver"); +MODULE_AUTHOR("Richard Purdie"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wm8731.h b/sound/soc/codecs/wm8731.h new file mode 100644 index 00000000000..5bcab6a7afb --- /dev/null +++ b/sound/soc/codecs/wm8731.h @@ -0,0 +1,44 @@ +/* + * wm8731.h -- WM8731 Soc Audio driver + * + * Copyright 2005 Openedhand Ltd. + * + * Author: Richard Purdie <richard@openedhand.com> + * + * Based on wm8753.h + * + * 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. + */ + +#ifndef _WM8731_H +#define _WM8731_H + +/* WM8731 register space */ + +#define WM8731_LINVOL 0x00 +#define WM8731_RINVOL 0x01 +#define WM8731_LOUT1V 0x02 +#define WM8731_ROUT1V 0x03 +#define WM8731_APANA 0x04 +#define WM8731_APDIGI 0x05 +#define WM8731_PWR 0x06 +#define WM8731_IFACE 0x07 +#define WM8731_SRATE 0x08 +#define WM8731_ACTIVE 0x09 +#define WM8731_RESET 0x0f + +#define WM8731_CACHEREGNUM 10 + +#define WM8731_SYSCLK 0 +#define WM8731_DAI 0 + +struct wm8731_setup_data { + unsigned short i2c_address; +}; + +extern struct snd_soc_codec_dai wm8731_dai; +extern struct snd_soc_codec_device soc_codec_dev_wm8731; + +#endif diff --git a/sound/soc/codecs/wm8750.c b/sound/soc/codecs/wm8750.c new file mode 100644 index 00000000000..7073e8e294f --- /dev/null +++ b/sound/soc/codecs/wm8750.c @@ -0,0 +1,1049 @@ +/* + * wm8750.c -- WM8750 ALSA SoC audio driver + * + * Copyright 2005 Openedhand Ltd. + * + * Author: Richard Purdie <richard@openedhand.com> + * + * Based on WM8753.c + * + * 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 <linux/moduleparam.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/pm.h> +#include <linux/i2c.h> +#include <linux/platform_device.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/initval.h> + +#include "wm8750.h" + +#define AUDIO_NAME "WM8750" +#define WM8750_VERSION "0.12" + +/* + * Debug + */ + +#define WM8750_DEBUG 0 + +#ifdef WM8750_DEBUG +#define dbg(format, arg...) \ + printk(KERN_DEBUG AUDIO_NAME ": " format "\n" , ## arg) +#else +#define dbg(format, arg...) do {} while (0) +#endif +#define err(format, arg...) \ + printk(KERN_ERR AUDIO_NAME ": " format "\n" , ## arg) +#define info(format, arg...) \ + printk(KERN_INFO AUDIO_NAME ": " format "\n" , ## arg) +#define warn(format, arg...) \ + printk(KERN_WARNING AUDIO_NAME ": " format "\n" , ## arg) + +/* codec private data */ +struct wm8750_priv { + unsigned int sysclk; +}; + +/* + * wm8750 register cache + * We can't read the WM8750 register space when we + * are using 2 wire for device control, so we cache them instead. + */ +static const u16 wm8750_reg[] = { + 0x0097, 0x0097, 0x0079, 0x0079, /* 0 */ + 0x0000, 0x0008, 0x0000, 0x000a, /* 4 */ + 0x0000, 0x0000, 0x00ff, 0x00ff, /* 8 */ + 0x000f, 0x000f, 0x0000, 0x0000, /* 12 */ + 0x0000, 0x007b, 0x0000, 0x0032, /* 16 */ + 0x0000, 0x00c3, 0x00c3, 0x00c0, /* 20 */ + 0x0000, 0x0000, 0x0000, 0x0000, /* 24 */ + 0x0000, 0x0000, 0x0000, 0x0000, /* 28 */ + 0x0000, 0x0000, 0x0050, 0x0050, /* 32 */ + 0x0050, 0x0050, 0x0050, 0x0050, /* 36 */ + 0x0079, 0x0079, 0x0079, /* 40 */ +}; + +/* + * read wm8750 register cache + */ +static inline unsigned int wm8750_read_reg_cache(struct snd_soc_codec *codec, + unsigned int reg) +{ + u16 *cache = codec->reg_cache; + if (reg > WM8750_CACHE_REGNUM) + return -1; + return cache[reg]; +} + +/* + * write wm8750 register cache + */ +static inline void wm8750_write_reg_cache(struct snd_soc_codec *codec, + unsigned int reg, unsigned int value) +{ + u16 *cache = codec->reg_cache; + if (reg > WM8750_CACHE_REGNUM) + return; + cache[reg] = value; +} + +static int wm8750_write(struct snd_soc_codec *codec, unsigned int reg, + unsigned int value) +{ + u8 data[2]; + + /* data is + * D15..D9 WM8753 register offset + * D8...D0 register data + */ + data[0] = (reg << 1) | ((value >> 8) & 0x0001); + data[1] = value & 0x00ff; + + wm8750_write_reg_cache (codec, reg, value); + if (codec->hw_write(codec->control_data, data, 2) == 2) + return 0; + else + return -EIO; +} + +#define wm8750_reset(c) wm8750_write(c, WM8750_RESET, 0) + +/* + * WM8750 Controls + */ +static const char *wm8750_bass[] = {"Linear Control", "Adaptive Boost"}; +static const char *wm8750_bass_filter[] = { "130Hz @ 48kHz", "200Hz @ 48kHz" }; +static const char *wm8750_treble[] = {"8kHz", "4kHz"}; +static const char *wm8750_3d_lc[] = {"200Hz", "500Hz"}; +static const char *wm8750_3d_uc[] = {"2.2kHz", "1.5kHz"}; +static const char *wm8750_3d_func[] = {"Capture", "Playback"}; +static const char *wm8750_alc_func[] = {"Off", "Right", "Left", "Stereo"}; +static const char *wm8750_ng_type[] = {"Constant PGA Gain", + "Mute ADC Output"}; +static const char *wm8750_line_mux[] = {"Line 1", "Line 2", "Line 3", "PGA", + "Differential"}; +static const char *wm8750_pga_sel[] = {"Line 1", "Line 2", "Line 3", + "Differential"}; +static const char *wm8750_out3[] = {"VREF", "ROUT1 + Vol", "MonoOut", + "ROUT1"}; +static const char *wm8750_diff_sel[] = {"Line 1", "Line 2"}; +static const char *wm8750_adcpol[] = {"Normal", "L Invert", "R Invert", + "L + R Invert"}; +static const char *wm8750_deemph[] = {"None", "32Khz", "44.1Khz", "48Khz"}; +static const char *wm8750_mono_mux[] = {"Stereo", "Mono (Left)", + "Mono (Right)", "Digital Mono"}; + +static const struct soc_enum wm8750_enum[] = { +SOC_ENUM_SINGLE(WM8750_BASS, 7, 2, wm8750_bass), +SOC_ENUM_SINGLE(WM8750_BASS, 6, 2, wm8750_bass_filter), +SOC_ENUM_SINGLE(WM8750_TREBLE, 6, 2, wm8750_treble), +SOC_ENUM_SINGLE(WM8750_3D, 5, 2, wm8750_3d_lc), +SOC_ENUM_SINGLE(WM8750_3D, 6, 2, wm8750_3d_uc), +SOC_ENUM_SINGLE(WM8750_3D, 7, 2, wm8750_3d_func), +SOC_ENUM_SINGLE(WM8750_ALC1, 7, 4, wm8750_alc_func), +SOC_ENUM_SINGLE(WM8750_NGATE, 1, 2, wm8750_ng_type), +SOC_ENUM_SINGLE(WM8750_LOUTM1, 0, 5, wm8750_line_mux), +SOC_ENUM_SINGLE(WM8750_ROUTM1, 0, 5, wm8750_line_mux), +SOC_ENUM_SINGLE(WM8750_LADCIN, 6, 4, wm8750_pga_sel), /* 10 */ +SOC_ENUM_SINGLE(WM8750_RADCIN, 6, 4, wm8750_pga_sel), +SOC_ENUM_SINGLE(WM8750_ADCTL2, 7, 4, wm8750_out3), +SOC_ENUM_SINGLE(WM8750_ADCIN, 8, 2, wm8750_diff_sel), +SOC_ENUM_SINGLE(WM8750_ADCDAC, 5, 4, wm8750_adcpol), +SOC_ENUM_SINGLE(WM8750_ADCDAC, 1, 4, wm8750_deemph), +SOC_ENUM_SINGLE(WM8750_ADCIN, 6, 4, wm8750_mono_mux), /* 16 */ + +}; + +static const struct snd_kcontrol_new wm8750_snd_controls[] = { + +SOC_DOUBLE_R("Capture Volume", WM8750_LINVOL, WM8750_RINVOL, 0, 63, 0), +SOC_DOUBLE_R("Capture ZC Switch", WM8750_LINVOL, WM8750_RINVOL, 6, 1, 0), +SOC_DOUBLE_R("Capture Switch", WM8750_LINVOL, WM8750_RINVOL, 7, 1, 1), + +SOC_DOUBLE_R("Headphone Playback ZC Switch", WM8750_LOUT1V, + WM8750_ROUT1V, 7, 1, 0), +SOC_DOUBLE_R("Speaker Playback ZC Switch", WM8750_LOUT2V, + WM8750_ROUT2V, 7, 1, 0), + +SOC_ENUM("Playback De-emphasis", wm8750_enum[15]), + +SOC_ENUM("Capture Polarity", wm8750_enum[14]), +SOC_SINGLE("Playback 6dB Attenuate", WM8750_ADCDAC, 7, 1, 0), +SOC_SINGLE("Capture 6dB Attenuate", WM8750_ADCDAC, 8, 1, 0), + +SOC_DOUBLE_R("PCM Volume", WM8750_LDAC, WM8750_RDAC, 0, 255, 0), + +SOC_ENUM("Bass Boost", wm8750_enum[0]), +SOC_ENUM("Bass Filter", wm8750_enum[1]), +SOC_SINGLE("Bass Volume", WM8750_BASS, 0, 15, 1), + +SOC_SINGLE("Treble Volume", WM8750_TREBLE, 0, 15, 0), +SOC_ENUM("Treble Cut-off", wm8750_enum[2]), + +SOC_SINGLE("3D Switch", WM8750_3D, 0, 1, 0), +SOC_SINGLE("3D Volume", WM8750_3D, 1, 15, 0), +SOC_ENUM("3D Lower Cut-off", wm8750_enum[3]), +SOC_ENUM("3D Upper Cut-off", wm8750_enum[4]), +SOC_ENUM("3D Mode", wm8750_enum[5]), + +SOC_SINGLE("ALC Capture Target Volume", WM8750_ALC1, 0, 7, 0), +SOC_SINGLE("ALC Capture Max Volume", WM8750_ALC1, 4, 7, 0), +SOC_ENUM("ALC Capture Function", wm8750_enum[6]), +SOC_SINGLE("ALC Capture ZC Switch", WM8750_ALC2, 7, 1, 0), +SOC_SINGLE("ALC Capture Hold Time", WM8750_ALC2, 0, 15, 0), +SOC_SINGLE("ALC Capture Decay Time", WM8750_ALC3, 4, 15, 0), +SOC_SINGLE("ALC Capture Attack Time", WM8750_ALC3, 0, 15, 0), +SOC_SINGLE("ALC Capture NG Threshold", WM8750_NGATE, 3, 31, 0), +SOC_ENUM("ALC Capture NG Type", wm8750_enum[4]), +SOC_SINGLE("ALC Capture NG Switch", WM8750_NGATE, 0, 1, 0), + +SOC_SINGLE("Left ADC Capture Volume", WM8750_LADC, 0, 255, 0), +SOC_SINGLE("Right ADC Capture Volume", WM8750_RADC, 0, 255, 0), + +SOC_SINGLE("ZC Timeout Switch", WM8750_ADCTL1, 0, 1, 0), +SOC_SINGLE("Playback Invert Switch", WM8750_ADCTL1, 1, 1, 0), + +SOC_SINGLE("Right Speaker Playback Invert Switch", WM8750_ADCTL2, 4, 1, 0), + +/* Unimplemented */ +/* ADCDAC Bit 0 - ADCHPD */ +/* ADCDAC Bit 4 - HPOR */ +/* ADCTL1 Bit 2,3 - DATSEL */ +/* ADCTL1 Bit 4,5 - DMONOMIX */ +/* ADCTL1 Bit 6,7 - VSEL */ +/* ADCTL2 Bit 2 - LRCM */ +/* ADCTL2 Bit 3 - TRI */ +/* ADCTL3 Bit 5 - HPFLREN */ +/* ADCTL3 Bit 6 - VROI */ +/* ADCTL3 Bit 7,8 - ADCLRM */ +/* ADCIN Bit 4 - LDCM */ +/* ADCIN Bit 5 - RDCM */ + +SOC_DOUBLE_R("Mic Boost", WM8750_LADCIN, WM8750_RADCIN, 4, 3, 0), + +SOC_DOUBLE_R("Bypass Left Playback Volume", WM8750_LOUTM1, + WM8750_LOUTM2, 4, 7, 1), +SOC_DOUBLE_R("Bypass Right Playback Volume", WM8750_ROUTM1, + WM8750_ROUTM2, 4, 7, 1), +SOC_DOUBLE_R("Bypass Mono Playback Volume", WM8750_MOUTM1, + WM8750_MOUTM2, 4, 7, 1), + +SOC_SINGLE("Mono Playback ZC Switch", WM8750_MOUTV, 7, 1, 0), + +SOC_DOUBLE_R("Headphone Playback Volume", WM8750_LOUT1V, WM8750_ROUT1V, + 0, 127, 0), +SOC_DOUBLE_R("Speaker Playback Volume", WM8750_LOUT2V, WM8750_ROUT2V, + 0, 127, 0), + +SOC_SINGLE("Mono Playback Volume", WM8750_MOUTV, 0, 127, 0), + +}; + +/* add non dapm controls */ +static int wm8750_add_controls(struct snd_soc_codec *codec) +{ + int err, i; + + for (i = 0; i < ARRAY_SIZE(wm8750_snd_controls); i++) { + err = snd_ctl_add(codec->card, + snd_soc_cnew(&wm8750_snd_controls[i],codec, NULL)); + if (err < 0) + return err; + } + return 0; +} + +/* + * DAPM Controls + */ + +/* Left Mixer */ +static const struct snd_kcontrol_new wm8750_left_mixer_controls[] = { +SOC_DAPM_SINGLE("Playback Switch", WM8750_LOUTM1, 8, 1, 0), +SOC_DAPM_SINGLE("Left Bypass Switch", WM8750_LOUTM1, 7, 1, 0), +SOC_DAPM_SINGLE("Right Playback Switch", WM8750_LOUTM2, 8, 1, 0), +SOC_DAPM_SINGLE("Right Bypass Switch", WM8750_LOUTM2, 7, 1, 0), +}; + +/* Right Mixer */ +static const struct snd_kcontrol_new wm8750_right_mixer_controls[] = { +SOC_DAPM_SINGLE("Left Playback Switch", WM8750_ROUTM1, 8, 1, 0), +SOC_DAPM_SINGLE("Left Bypass Switch", WM8750_ROUTM1, 7, 1, 0), +SOC_DAPM_SINGLE("Playback Switch", WM8750_ROUTM2, 8, 1, 0), +SOC_DAPM_SINGLE("Right Bypass Switch", WM8750_ROUTM2, 7, 1, 0), +}; + +/* Mono Mixer */ +static const struct snd_kcontrol_new wm8750_mono_mixer_controls[] = { +SOC_DAPM_SINGLE("Left Playback Switch", WM8750_MOUTM1, 8, 1, 0), +SOC_DAPM_SINGLE("Left Bypass Switch", WM8750_MOUTM1, 7, 1, 0), +SOC_DAPM_SINGLE("Right Playback Switch", WM8750_MOUTM2, 8, 1, 0), +SOC_DAPM_SINGLE("Right Bypass Switch", WM8750_MOUTM2, 7, 1, 0), +}; + +/* Left Line Mux */ +static const struct snd_kcontrol_new wm8750_left_line_controls = +SOC_DAPM_ENUM("Route", wm8750_enum[8]); + +/* Right Line Mux */ +static const struct snd_kcontrol_new wm8750_right_line_controls = +SOC_DAPM_ENUM("Route", wm8750_enum[9]); + +/* Left PGA Mux */ +static const struct snd_kcontrol_new wm8750_left_pga_controls = +SOC_DAPM_ENUM("Route", wm8750_enum[10]); + +/* Right PGA Mux */ +static const struct snd_kcontrol_new wm8750_right_pga_controls = +SOC_DAPM_ENUM("Route", wm8750_enum[11]); + +/* Out 3 Mux */ +static const struct snd_kcontrol_new wm8750_out3_controls = +SOC_DAPM_ENUM("Route", wm8750_enum[12]); + +/* Differential Mux */ +static const struct snd_kcontrol_new wm8750_diffmux_controls = +SOC_DAPM_ENUM("Route", wm8750_enum[13]); + +/* Mono ADC Mux */ +static const struct snd_kcontrol_new wm8750_monomux_controls = +SOC_DAPM_ENUM("Route", wm8750_enum[16]); + +static const struct snd_soc_dapm_widget wm8750_dapm_widgets[] = { + SND_SOC_DAPM_MIXER("Left Mixer", SND_SOC_NOPM, 0, 0, + &wm8750_left_mixer_controls[0], + ARRAY_SIZE(wm8750_left_mixer_controls)), + SND_SOC_DAPM_MIXER("Right Mixer", SND_SOC_NOPM, 0, 0, + &wm8750_right_mixer_controls[0], + ARRAY_SIZE(wm8750_right_mixer_controls)), + SND_SOC_DAPM_MIXER("Mono Mixer", WM8750_PWR2, 2, 0, + &wm8750_mono_mixer_controls[0], + ARRAY_SIZE(wm8750_mono_mixer_controls)), + + SND_SOC_DAPM_PGA("Right Out 2", WM8750_PWR2, 3, 0, NULL, 0), + SND_SOC_DAPM_PGA("Left Out 2", WM8750_PWR2, 4, 0, NULL, 0), + SND_SOC_DAPM_PGA("Right Out 1", WM8750_PWR2, 5, 0, NULL, 0), + SND_SOC_DAPM_PGA("Left Out 1", WM8750_PWR2, 6, 0, NULL, 0), + SND_SOC_DAPM_DAC("Right DAC", "Right Playback", WM8750_PWR2, 7, 0), + SND_SOC_DAPM_DAC("Left DAC", "Left Playback", WM8750_PWR2, 8, 0), + + SND_SOC_DAPM_MICBIAS("Mic Bias", WM8750_PWR1, 1, 0), + SND_SOC_DAPM_ADC("Right ADC", "Right Capture", WM8750_PWR1, 2, 0), + SND_SOC_DAPM_ADC("Left ADC", "Left Capture", WM8750_PWR1, 3, 0), + + SND_SOC_DAPM_MUX("Left PGA Mux", WM8750_PWR1, 5, 0, + &wm8750_left_pga_controls), + SND_SOC_DAPM_MUX("Right PGA Mux", WM8750_PWR1, 4, 0, + &wm8750_right_pga_controls), + SND_SOC_DAPM_MUX("Left Line Mux", SND_SOC_NOPM, 0, 0, + &wm8750_left_line_controls), + SND_SOC_DAPM_MUX("Right Line Mux", SND_SOC_NOPM, 0, 0, + &wm8750_right_line_controls), + + SND_SOC_DAPM_MUX("Out3 Mux", SND_SOC_NOPM, 0, 0, &wm8750_out3_controls), + SND_SOC_DAPM_PGA("Out 3", WM8750_PWR2, 1, 0, NULL, 0), + SND_SOC_DAPM_PGA("Mono Out 1", WM8750_PWR2, 2, 0, NULL, 0), + + SND_SOC_DAPM_MUX("Differential Mux", SND_SOC_NOPM, 0, 0, + &wm8750_diffmux_controls), + SND_SOC_DAPM_MUX("Left ADC Mux", SND_SOC_NOPM, 0, 0, + &wm8750_monomux_controls), + SND_SOC_DAPM_MUX("Right ADC Mux", SND_SOC_NOPM, 0, 0, + &wm8750_monomux_controls), + + SND_SOC_DAPM_OUTPUT("LOUT1"), + SND_SOC_DAPM_OUTPUT("ROUT1"), + SND_SOC_DAPM_OUTPUT("LOUT2"), + SND_SOC_DAPM_OUTPUT("ROUT2"), + SND_SOC_DAPM_OUTPUT("MONO"), + SND_SOC_DAPM_OUTPUT("OUT3"), + + SND_SOC_DAPM_INPUT("LINPUT1"), + SND_SOC_DAPM_INPUT("LINPUT2"), + SND_SOC_DAPM_INPUT("LINPUT3"), + SND_SOC_DAPM_INPUT("RINPUT1"), + SND_SOC_DAPM_INPUT("RINPUT2"), + SND_SOC_DAPM_INPUT("RINPUT3"), +}; + +static const char *audio_map[][3] = { + /* left mixer */ + {"Left Mixer", "Playback Switch", "Left DAC"}, + {"Left Mixer", "Left Bypass Switch", "Left Line Mux"}, + {"Left Mixer", "Right Playback Switch", "Right DAC"}, + {"Left Mixer", "Right Bypass Switch", "Right Line Mux"}, + + /* right mixer */ + {"Right Mixer", "Left Playback Switch", "Left DAC"}, + {"Right Mixer", "Left Bypass Switch", "Left Line Mux"}, + {"Right Mixer", "Playback Switch", "Right DAC"}, + {"Right Mixer", "Right Bypass Switch", "Right Line Mux"}, + + /* left out 1 */ + {"Left Out 1", NULL, "Left Mixer"}, + {"LOUT1", NULL, "Left Out 1"}, + + /* left out 2 */ + {"Left Out 2", NULL, "Left Mixer"}, + {"LOUT2", NULL, "Left Out 2"}, + + /* right out 1 */ + {"Right Out 1", NULL, "Right Mixer"}, + {"ROUT1", NULL, "Right Out 1"}, + + /* right out 2 */ + {"Right Out 2", NULL, "Right Mixer"}, + {"ROUT2", NULL, "Right Out 2"}, + + /* mono mixer */ + {"Mono Mixer", "Left Playback Switch", "Left DAC"}, + {"Mono Mixer", "Left Bypass Switch", "Left Line Mux"}, + {"Mono Mixer", "Right Playback Switch", "Right DAC"}, + {"Mono Mixer", "Right Bypass Switch", "Right Line Mux"}, + + /* mono out */ + {"Mono Out 1", NULL, "Mono Mixer"}, + {"MONO1", NULL, "Mono Out 1"}, + + /* out 3 */ + {"Out3 Mux", "VREF", "VREF"}, + {"Out3 Mux", "ROUT1 + Vol", "ROUT1"}, + {"Out3 Mux", "ROUT1", "Right Mixer"}, + {"Out3 Mux", "MonoOut", "MONO1"}, + {"Out 3", NULL, "Out3 Mux"}, + {"OUT3", NULL, "Out 3"}, + + /* Left Line Mux */ + {"Left Line Mux", "Line 1", "LINPUT1"}, + {"Left Line Mux", "Line 2", "LINPUT2"}, + {"Left Line Mux", "Line 3", "LINPUT3"}, + {"Left Line Mux", "PGA", "Left PGA Mux"}, + {"Left Line Mux", "Differential", "Differential Mux"}, + + /* Right Line Mux */ + {"Right Line Mux", "Line 1", "RINPUT1"}, + {"Right Line Mux", "Line 2", "RINPUT2"}, + {"Right Line Mux", "Line 3", "RINPUT3"}, + {"Right Line Mux", "PGA", "Right PGA Mux"}, + {"Right Line Mux", "Differential", "Differential Mux"}, + + /* Left PGA Mux */ + {"Left PGA Mux", "Line 1", "LINPUT1"}, + {"Left PGA Mux", "Line 2", "LINPUT2"}, + {"Left PGA Mux", "Line 3", "LINPUT3"}, + {"Left PGA Mux", "Differential", "Differential Mux"}, + + /* Right PGA Mux */ + {"Right PGA Mux", "Line 1", "RINPUT1"}, + {"Right PGA Mux", "Line 2", "RINPUT2"}, + {"Right PGA Mux", "Line 3", "RINPUT3"}, + {"Right PGA Mux", "Differential", "Differential Mux"}, + + /* Differential Mux */ + {"Differential Mux", "Line 1", "LINPUT1"}, + {"Differential Mux", "Line 1", "RINPUT1"}, + {"Differential Mux", "Line 2", "LINPUT2"}, + {"Differential Mux", "Line 2", "RINPUT2"}, + + /* Left ADC Mux */ + {"Left ADC Mux", "Stereo", "Left PGA Mux"}, + {"Left ADC Mux", "Mono (Left)", "Left PGA Mux"}, + {"Left ADC Mux", "Digital Mono", "Left PGA Mux"}, + + /* Right ADC Mux */ + {"Right ADC Mux", "Stereo", "Right PGA Mux"}, + {"Right ADC Mux", "Mono (Right)", "Right PGA Mux"}, + {"Right ADC Mux", "Digital Mono", "Right PGA Mux"}, + + /* ADC */ + {"Left ADC", NULL, "Left ADC Mux"}, + {"Right ADC", NULL, "Right ADC Mux"}, + + /* terminator */ + {NULL, NULL, NULL}, +}; + +static int wm8750_add_widgets(struct snd_soc_codec *codec) +{ + int i; + + for(i = 0; i < ARRAY_SIZE(wm8750_dapm_widgets); i++) { + snd_soc_dapm_new_control(codec, &wm8750_dapm_widgets[i]); + } + + /* set up audio path audio_mapnects */ + for(i = 0; audio_map[i][0] != NULL; i++) { + snd_soc_dapm_connect_input(codec, audio_map[i][0], + audio_map[i][1], audio_map[i][2]); + } + + snd_soc_dapm_new_widgets(codec); + return 0; +} + +struct _coeff_div { + u32 mclk; + u32 rate; + u16 fs; + u8 sr:5; + u8 usb:1; +}; + +/* codec hifi mclk clock divider coefficients */ +static const struct _coeff_div coeff_div[] = { + /* 8k */ + {12288000, 8000, 1536, 0x6, 0x0}, + {11289600, 8000, 1408, 0x16, 0x0}, + {18432000, 8000, 2304, 0x7, 0x0}, + {16934400, 8000, 2112, 0x17, 0x0}, + {12000000, 8000, 1500, 0x6, 0x1}, + + /* 11.025k */ + {11289600, 11025, 1024, 0x18, 0x0}, + {16934400, 11025, 1536, 0x19, 0x0}, + {12000000, 11025, 1088, 0x19, 0x1}, + + /* 16k */ + {12288000, 16000, 768, 0xa, 0x0}, + {18432000, 16000, 1152, 0xb, 0x0}, + {12000000, 16000, 750, 0xa, 0x1}, + + /* 22.05k */ + {11289600, 22050, 512, 0x1a, 0x0}, + {16934400, 22050, 768, 0x1b, 0x0}, + {12000000, 22050, 544, 0x1b, 0x1}, + + /* 32k */ + {12288000, 32000, 384, 0xc, 0x0}, + {18432000, 32000, 576, 0xd, 0x0}, + {12000000, 32000, 375, 0xa, 0x1}, + + /* 44.1k */ + {11289600, 44100, 256, 0x10, 0x0}, + {16934400, 44100, 384, 0x11, 0x0}, + {12000000, 44100, 272, 0x11, 0x1}, + + /* 48k */ + {12288000, 48000, 256, 0x0, 0x0}, + {18432000, 48000, 384, 0x1, 0x0}, + {12000000, 48000, 250, 0x0, 0x1}, + + /* 88.2k */ + {11289600, 88200, 128, 0x1e, 0x0}, + {16934400, 88200, 192, 0x1f, 0x0}, + {12000000, 88200, 136, 0x1f, 0x1}, + + /* 96k */ + {12288000, 96000, 128, 0xe, 0x0}, + {18432000, 96000, 192, 0xf, 0x0}, + {12000000, 96000, 125, 0xe, 0x1}, +}; + +static inline int get_coeff(int mclk, int rate) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(coeff_div); i++) { + if (coeff_div[i].rate == rate && coeff_div[i].mclk == mclk) + return i; + } + + printk(KERN_ERR "wm8750: could not get coeff for mclk %d @ rate %d\n", + mclk, rate); + return -EINVAL; +} + +static int wm8750_set_dai_sysclk(struct snd_soc_codec_dai *codec_dai, + int clk_id, unsigned int freq, int dir) +{ + struct snd_soc_codec *codec = codec_dai->codec; + struct wm8750_priv *wm8750 = codec->private_data; + + switch (freq) { + case 11289600: + case 12000000: + case 12288000: + case 16934400: + case 18432000: + wm8750->sysclk = freq; + return 0; + } + return -EINVAL; +} + +static int wm8750_set_dai_fmt(struct snd_soc_codec_dai *codec_dai, + unsigned int fmt) +{ + struct snd_soc_codec *codec = codec_dai->codec; + u16 iface = 0; + + /* set master/slave audio interface */ + switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBM_CFM: + iface = 0x0040; + break; + case SND_SOC_DAIFMT_CBS_CFS: + break; + default: + return -EINVAL; + } + + /* interface format */ + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + iface |= 0x0002; + break; + case SND_SOC_DAIFMT_RIGHT_J: + break; + case SND_SOC_DAIFMT_LEFT_J: + iface |= 0x0001; + break; + case SND_SOC_DAIFMT_DSP_A: + iface |= 0x0003; + break; + case SND_SOC_DAIFMT_DSP_B: + iface |= 0x0013; + break; + default: + return -EINVAL; + } + + /* clock inversion */ + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + break; + case SND_SOC_DAIFMT_IB_IF: + iface |= 0x0090; + break; + case SND_SOC_DAIFMT_IB_NF: + iface |= 0x0080; + break; + case SND_SOC_DAIFMT_NB_IF: + iface |= 0x0010; + break; + default: + return -EINVAL; + } + + wm8750_write(codec, WM8750_IFACE, iface); + return 0; +} + +static int wm8750_pcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_codec *codec = socdev->codec; + struct wm8750_priv *wm8750 = codec->private_data; + u16 iface = wm8750_read_reg_cache(codec, WM8750_IFACE) & 0x1f3; + u16 srate = wm8750_read_reg_cache(codec, WM8750_SRATE) & 0x1c0; + int coeff = get_coeff(wm8750->sysclk, params_rate(params)); + + /* bit size */ + switch (params_format(params)) { + case SNDRV_PCM_FORMAT_S16_LE: + break; + case SNDRV_PCM_FORMAT_S20_3LE: + iface |= 0x0004; + break; + case SNDRV_PCM_FORMAT_S24_LE: + iface |= 0x0008; + break; + case SNDRV_PCM_FORMAT_S32_LE: + iface |= 0x000c; + break; + } + + /* set iface & srate */ + wm8750_write(codec, WM8750_IFACE, iface); + if (coeff >= 0) + wm8750_write(codec, WM8750_SRATE, srate | + (coeff_div[coeff].sr << 1) | coeff_div[coeff].usb); + + return 0; +} + +static int wm8750_mute(struct snd_soc_codec_dai *dai, int mute) +{ + struct snd_soc_codec *codec = dai->codec; + u16 mute_reg = wm8750_read_reg_cache(codec, WM8750_ADCDAC) & 0xfff7; + + if (mute) + wm8750_write(codec, WM8750_ADCDAC, mute_reg | 0x8); + else + wm8750_write(codec, WM8750_ADCDAC, mute_reg); + return 0; +} + +static int wm8750_dapm_event(struct snd_soc_codec *codec, int event) +{ + u16 pwr_reg = wm8750_read_reg_cache(codec, WM8750_PWR1) & 0xfe3e; + + switch (event) { + case SNDRV_CTL_POWER_D0: /* full On */ + /* set vmid to 50k and unmute dac */ + wm8750_write(codec, WM8750_PWR1, pwr_reg | 0x00c0); + break; + case SNDRV_CTL_POWER_D1: /* partial On */ + case SNDRV_CTL_POWER_D2: /* partial On */ + /* set vmid to 5k for quick power up */ + wm8750_write(codec, WM8750_PWR1, pwr_reg | 0x01c1); + break; + case SNDRV_CTL_POWER_D3hot: /* Off, with power */ + /* mute dac and set vmid to 500k, enable VREF */ + wm8750_write(codec, WM8750_PWR1, pwr_reg | 0x0141); + break; + case SNDRV_CTL_POWER_D3cold: /* Off, without power */ + wm8750_write(codec, WM8750_PWR1, 0x0001); + break; + } + codec->dapm_state = event; + return 0; +} + +#define WM8750_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 |\ + SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 | SNDRV_PCM_RATE_44100 | \ + SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_88200 | SNDRV_PCM_RATE_96000) + +#define WM8750_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE |\ + SNDRV_PCM_FMTBIT_S24_LE) + +struct snd_soc_codec_dai wm8750_dai = { + .name = "WM8750", + .playback = { + .stream_name = "Playback", + .channels_min = 1, + .channels_max = 2, + .rates = WM8750_RATES, + .formats = WM8750_FORMATS,}, + .capture = { + .stream_name = "Capture", + .channels_min = 1, + .channels_max = 2, + .rates = WM8750_RATES, + .formats = WM8750_FORMATS,}, + .ops = { + .hw_params = wm8750_pcm_hw_params, + }, + .dai_ops = { + .digital_mute = wm8750_mute, + .set_fmt = wm8750_set_dai_fmt, + .set_sysclk = wm8750_set_dai_sysclk, + }, +}; +EXPORT_SYMBOL_GPL(wm8750_dai); + +static void wm8750_work(struct work_struct *work) +{ + struct snd_soc_codec *codec = + container_of(work, struct snd_soc_codec, delayed_work.work); + wm8750_dapm_event(codec, codec->dapm_state); +} + +static int wm8750_suspend(struct platform_device *pdev, pm_message_t state) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + + wm8750_dapm_event(codec, SNDRV_CTL_POWER_D3cold); + return 0; +} + +static int wm8750_resume(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + int i; + u8 data[2]; + u16 *cache = codec->reg_cache; + + /* Sync reg_cache with the hardware */ + for (i = 0; i < ARRAY_SIZE(wm8750_reg); i++) { + if (i == WM8750_RESET) + continue; + data[0] = (i << 1) | ((cache[i] >> 8) & 0x0001); + data[1] = cache[i] & 0x00ff; + codec->hw_write(codec->control_data, data, 2); + } + + wm8750_dapm_event(codec, SNDRV_CTL_POWER_D3hot); + + /* charge wm8750 caps */ + if (codec->suspend_dapm_state == SNDRV_CTL_POWER_D0) { + wm8750_dapm_event(codec, SNDRV_CTL_POWER_D2); + codec->dapm_state = SNDRV_CTL_POWER_D0; + schedule_delayed_work(&codec->delayed_work, msecs_to_jiffies(1000)); + } + + return 0; +} + +/* + * initialise the WM8750 driver + * register the mixer and dsp interfaces with the kernel + */ +static int wm8750_init(struct snd_soc_device *socdev) +{ + struct snd_soc_codec *codec = socdev->codec; + int reg, ret = 0; + + codec->name = "WM8750"; + codec->owner = THIS_MODULE; + codec->read = wm8750_read_reg_cache; + codec->write = wm8750_write; + codec->dapm_event = wm8750_dapm_event; + codec->dai = &wm8750_dai; + codec->num_dai = 1; + codec->reg_cache_size = sizeof(wm8750_reg); + codec->reg_cache = kmemdup(wm8750_reg, sizeof(wm8750_reg), GFP_KRENEL); + if (codec->reg_cache == NULL) + return -ENOMEM; + + wm8750_reset(codec); + + /* register pcms */ + ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1); + if (ret < 0) { + printk(KERN_ERR "wm8750: failed to create pcms\n"); + goto pcm_err; + } + + /* charge output caps */ + wm8750_dapm_event(codec, SNDRV_CTL_POWER_D2); + codec->dapm_state = SNDRV_CTL_POWER_D3hot; + schedule_delayed_work(&codec->delayed_work, msecs_to_jiffies(1000)); + + /* set the update bits */ + reg = wm8750_read_reg_cache(codec, WM8750_LDAC); + wm8750_write(codec, WM8750_LDAC, reg | 0x0100); + reg = wm8750_read_reg_cache(codec, WM8750_RDAC); + wm8750_write(codec, WM8750_RDAC, reg | 0x0100); + reg = wm8750_read_reg_cache(codec, WM8750_LOUT1V); + wm8750_write(codec, WM8750_LOUT1V, reg | 0x0100); + reg = wm8750_read_reg_cache(codec, WM8750_ROUT1V); + wm8750_write(codec, WM8750_ROUT1V, reg | 0x0100); + reg = wm8750_read_reg_cache(codec, WM8750_LOUT2V); + wm8750_write(codec, WM8750_LOUT2V, reg | 0x0100); + reg = wm8750_read_reg_cache(codec, WM8750_ROUT2V); + wm8750_write(codec, WM8750_ROUT2V, reg | 0x0100); + reg = wm8750_read_reg_cache(codec, WM8750_LINVOL); + wm8750_write(codec, WM8750_LINVOL, reg | 0x0100); + reg = wm8750_read_reg_cache(codec, WM8750_RINVOL); + wm8750_write(codec, WM8750_RINVOL, reg | 0x0100); + + wm8750_add_controls(codec); + wm8750_add_widgets(codec); + ret = snd_soc_register_card(socdev); + if (ret < 0) { + printk(KERN_ERR "wm8750: failed to register card\n"); + goto card_err; + } + return ret; + +card_err: + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); +pcm_err: + kfree(codec->reg_cache); + return ret; +} + +/* If the i2c layer weren't so broken, we could pass this kind of data + around */ +static struct snd_soc_device *wm8750_socdev; + +#if defined (CONFIG_I2C) || defined (CONFIG_I2C_MODULE) + +/* + * WM8731 2 wire address is determined by GPIO5 + * state during powerup. + * low = 0x1a + * high = 0x1b + */ +static unsigned short normal_i2c[] = { 0, I2C_CLIENT_END }; + +/* Magic definition of all other variables and things */ +I2C_CLIENT_INSMOD; + +static struct i2c_driver wm8750_i2c_driver; +static struct i2c_client client_template; + +static int wm8750_codec_probe(struct i2c_adapter *adap, int addr, int kind) +{ + struct snd_soc_device *socdev = wm8750_socdev; + struct wm8750_setup_data *setup = socdev->codec_data; + struct snd_soc_codec *codec = socdev->codec; + struct i2c_client *i2c; + int ret; + + if (addr != setup->i2c_address) + return -ENODEV; + + client_template.adapter = adap; + client_template.addr = addr; + + i2c = kmemdup(&client_template, sizeof(client_template), GFP_KERNEL); + if (i2c == NULL) { + kfree(codec); + return -ENOMEM; + } + i2c_set_clientdata(i2c, codec); + codec->control_data = i2c; + + ret = i2c_attach_client(i2c); + if (ret < 0) { + err("failed to attach codec at addr %x\n", addr); + goto err; + } + + ret = wm8750_init(socdev); + if (ret < 0) { + err("failed to initialise WM8750\n"); + goto err; + } + return ret; + +err: + kfree(codec); + kfree(i2c); + return ret; +} + +static int wm8750_i2c_detach(struct i2c_client *client) +{ + struct snd_soc_codec *codec = i2c_get_clientdata(client); + i2c_detach_client(client); + kfree(codec->reg_cache); + kfree(client); + return 0; +} + +static int wm8750_i2c_attach(struct i2c_adapter *adap) +{ + return i2c_probe(adap, &addr_data, wm8750_codec_probe); +} + +/* corgi i2c codec control layer */ +static struct i2c_driver wm8750_i2c_driver = { + .driver = { + .name = "WM8750 I2C Codec", + .owner = THIS_MODULE, + }, + .id = I2C_DRIVERID_WM8750, + .attach_adapter = wm8750_i2c_attach, + .detach_client = wm8750_i2c_detach, + .command = NULL, +}; + +static struct i2c_client client_template = { + .name = "WM8750", + .driver = &wm8750_i2c_driver, +}; +#endif + +static int wm8750_probe(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct wm8750_setup_data *setup = socdev->codec_data; + struct snd_soc_codec *codec; + struct wm8750_priv *wm8750; + int ret = 0; + + info("WM8750 Audio Codec %s", WM8750_VERSION); + codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL); + if (codec == NULL) + return -ENOMEM; + + wm8750 = kzalloc(sizeof(struct wm8750_priv), GFP_KERNEL); + if (wm8750 == NULL) { + kfree(codec); + return -ENOMEM; + } + + codec->private_data = wm8750; + socdev->codec = codec; + mutex_init(&codec->mutex); + INIT_LIST_HEAD(&codec->dapm_widgets); + INIT_LIST_HEAD(&codec->dapm_paths); + wm8750_socdev = socdev; + INIT_DELAYED_WORK(&codec->delayed_work, wm8750_work); + +#if defined (CONFIG_I2C) || defined (CONFIG_I2C_MODULE) + if (setup->i2c_address) { + normal_i2c[0] = setup->i2c_address; + codec->hw_write = (hw_write_t)i2c_master_send; + ret = i2c_add_driver(&wm8750_i2c_driver); + if (ret != 0) + printk(KERN_ERR "can't add i2c driver"); + } +#else + /* Add other interfaces here */ +#endif + + return ret; +} + +/* + * This function forces any delayed work to be queued and run. + */ +static int run_delayed_work(struct delayed_work *dwork) +{ + int ret; + + /* cancel any work waiting to be queued. */ + ret = cancel_delayed_work(dwork); + + /* if there was any work waiting then we run it now and + * wait for it's completion */ + if (ret) { + schedule_delayed_work(dwork, 0); + flush_scheduled_work(); + } + return ret; +} + +/* power down chip */ +static int wm8750_remove(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + + if (codec->control_data) + wm8750_dapm_event(codec, SNDRV_CTL_POWER_D3cold); + run_delayed_work(&codec->delayed_work); + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); +#if defined (CONFIG_I2C) || defined (CONFIG_I2C_MODULE) + i2c_del_driver(&wm8750_i2c_driver); +#endif + kfree(codec->private_data); + kfree(codec); + + return 0; +} + +struct snd_soc_codec_device soc_codec_dev_wm8750 = { + .probe = wm8750_probe, + .remove = wm8750_remove, + .suspend = wm8750_suspend, + .resume = wm8750_resume, +}; + +EXPORT_SYMBOL_GPL(soc_codec_dev_wm8750); + +MODULE_DESCRIPTION("ASoC WM8750 driver"); +MODULE_AUTHOR("Liam Girdwood"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wm8750.h b/sound/soc/codecs/wm8750.h new file mode 100644 index 00000000000..a97a54a6348 --- /dev/null +++ b/sound/soc/codecs/wm8750.h @@ -0,0 +1,67 @@ +/* + * Copyright 2005 Openedhand Ltd. + * + * Author: Richard Purdie <richard@openedhand.com> + * + * Based on WM8753.h + * + * 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. + * + */ + +#ifndef _WM8750_H +#define _WM8750_H + +/* WM8750 register space */ + +#define WM8750_LINVOL 0x00 +#define WM8750_RINVOL 0x01 +#define WM8750_LOUT1V 0x02 +#define WM8750_ROUT1V 0x03 +#define WM8750_ADCDAC 0x05 +#define WM8750_IFACE 0x07 +#define WM8750_SRATE 0x08 +#define WM8750_LDAC 0x0a +#define WM8750_RDAC 0x0b +#define WM8750_BASS 0x0c +#define WM8750_TREBLE 0x0d +#define WM8750_RESET 0x0f +#define WM8750_3D 0x10 +#define WM8750_ALC1 0x11 +#define WM8750_ALC2 0x12 +#define WM8750_ALC3 0x13 +#define WM8750_NGATE 0x14 +#define WM8750_LADC 0x15 +#define WM8750_RADC 0x16 +#define WM8750_ADCTL1 0x17 +#define WM8750_ADCTL2 0x18 +#define WM8750_PWR1 0x19 +#define WM8750_PWR2 0x1a +#define WM8750_ADCTL3 0x1b +#define WM8750_ADCIN 0x1f +#define WM8750_LADCIN 0x20 +#define WM8750_RADCIN 0x21 +#define WM8750_LOUTM1 0x22 +#define WM8750_LOUTM2 0x23 +#define WM8750_ROUTM1 0x24 +#define WM8750_ROUTM2 0x25 +#define WM8750_MOUTM1 0x26 +#define WM8750_MOUTM2 0x27 +#define WM8750_LOUT2V 0x28 +#define WM8750_ROUT2V 0x29 +#define WM8750_MOUTV 0x2a + +#define WM8750_CACHE_REGNUM 0x2a + +#define WM8750_SYSCLK 0 + +struct wm8750_setup_data { + unsigned short i2c_address; +}; + +extern struct snd_soc_codec_dai wm8750_dai; +extern struct snd_soc_codec_device soc_codec_dev_wm8750; + +#endif diff --git a/sound/soc/codecs/wm9712.c b/sound/soc/codecs/wm9712.c new file mode 100644 index 00000000000..92a64871bcd --- /dev/null +++ b/sound/soc/codecs/wm9712.c @@ -0,0 +1,771 @@ +/* + * wm9712.c -- ALSA Soc WM9712 codec support + * + * Copyright 2006 Wolfson Microelectronics PLC. + * Author: Liam Girdwood + * liam.girdwood@wolfsonmicro.com or linux@wolfsonmicro.com + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Revision history + * 4th Feb 2006 Initial version. + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/version.h> +#include <linux/kernel.h> +#include <linux/device.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/ac97_codec.h> +#include <sound/initval.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> + +#define WM9712_VERSION "0.4" + +static unsigned int ac97_read(struct snd_soc_codec *codec, + unsigned int reg); +static int ac97_write(struct snd_soc_codec *codec, + unsigned int reg, unsigned int val); + +/* + * WM9712 register cache + */ +static const u16 wm9712_reg[] = { + 0x6174, 0x8000, 0x8000, 0x8000, // 6 + 0xf0f0, 0xaaa0, 0xc008, 0x6808, // e + 0xe808, 0xaaa0, 0xad00, 0x8000, // 16 + 0xe808, 0x3000, 0x8000, 0x0000, // 1e + 0x0000, 0x0000, 0x0000, 0x000f, // 26 + 0x0405, 0x0410, 0xbb80, 0xbb80, // 2e + 0x0000, 0xbb80, 0x0000, 0x0000, // 36 + 0x0000, 0x2000, 0x0000, 0x0000, // 3e + 0x0000, 0x0000, 0x0000, 0x0000, // 46 + 0x0000, 0x0000, 0xf83e, 0xffff, // 4e + 0x0000, 0x0000, 0x0000, 0xf83e, // 56 + 0x0008, 0x0000, 0x0000, 0x0000, // 5e + 0xb032, 0x3e00, 0x0000, 0x0000, // 66 + 0x0000, 0x0000, 0x0000, 0x0000, // 6e + 0x0000, 0x0000, 0x0000, 0x0006, // 76 + 0x0001, 0x0000, 0x574d, 0x4c12, // 7e + 0x0000, 0x0000 // virtual hp mixers +}; + +/* virtual HP mixers regs */ +#define HPL_MIXER 0x80 +#define HPR_MIXER 0x82 + +static const char *wm9712_alc_select[] = {"None", "Left", "Right", "Stereo"}; +static const char *wm9712_alc_mux[] = {"Stereo", "Left", "Right", "None"}; +static const char *wm9712_out3_src[] = {"Left", "VREF", "Left + Right", + "Mono"}; +static const char *wm9712_spk_src[] = {"Speaker Mix", "Headphone Mix"}; +static const char *wm9712_rec_adc[] = {"Stereo", "Left", "Right", "Mute"}; +static const char *wm9712_base[] = {"Linear Control", "Adaptive Boost"}; +static const char *wm9712_rec_gain[] = {"+1.5dB Steps", "+0.75dB Steps"}; +static const char *wm9712_mic[] = {"Mic 1", "Differential", "Mic 2", + "Stereo"}; +static const char *wm9712_rec_sel[] = {"Mic", "NC", "NC", "Speaker Mixer", + "Line", "Headphone Mixer", "Phone Mixer", "Phone"}; +static const char *wm9712_ng_type[] = {"Constant Gain", "Mute"}; +static const char *wm9712_diff_sel[] = {"Mic", "Line"}; + +static const struct soc_enum wm9712_enum[] = { +SOC_ENUM_SINGLE(AC97_PCI_SVID, 14, 4, wm9712_alc_select), +SOC_ENUM_SINGLE(AC97_VIDEO, 12, 4, wm9712_alc_mux), +SOC_ENUM_SINGLE(AC97_AUX, 9, 4, wm9712_out3_src), +SOC_ENUM_SINGLE(AC97_AUX, 8, 2, wm9712_spk_src), +SOC_ENUM_SINGLE(AC97_REC_SEL, 12, 4, wm9712_rec_adc), +SOC_ENUM_SINGLE(AC97_MASTER_TONE, 15, 2, wm9712_base), +SOC_ENUM_DOUBLE(AC97_REC_GAIN, 14, 6, 2, wm9712_rec_gain), +SOC_ENUM_SINGLE(AC97_MIC, 5, 4, wm9712_mic), +SOC_ENUM_SINGLE(AC97_REC_SEL, 8, 8, wm9712_rec_sel), +SOC_ENUM_SINGLE(AC97_REC_SEL, 0, 8, wm9712_rec_sel), +SOC_ENUM_SINGLE(AC97_PCI_SVID, 5, 2, wm9712_ng_type), +SOC_ENUM_SINGLE(0x5c, 8, 2, wm9712_diff_sel), +}; + +static const struct snd_kcontrol_new wm9712_snd_ac97_controls[] = { +SOC_DOUBLE("Speaker Playback Volume", AC97_MASTER, 8, 0, 31, 1), +SOC_SINGLE("Speaker Playback Switch", AC97_MASTER, 15, 1, 1), +SOC_DOUBLE("Headphone Playback Volume", AC97_HEADPHONE, 8, 0, 31, 1), +SOC_SINGLE("Headphone Playback Switch", AC97_HEADPHONE,15, 1, 1), + +SOC_SINGLE("Speaker Playback ZC Switch", AC97_MASTER, 7, 1, 0), +SOC_SINGLE("Speaker Playback Invert Switch", AC97_MASTER, 6, 1, 0), +SOC_SINGLE("Headphone Playback ZC Switch", AC97_HEADPHONE, 7, 1, 0), +SOC_SINGLE("Mono Playback ZC Switch", AC97_MASTER_MONO, 7, 1, 0), +SOC_SINGLE("Mono Playback Volume", AC97_MASTER_MONO, 0, 31, 0), + +SOC_SINGLE("ALC Target Volume", AC97_CODEC_CLASS_REV, 12, 15, 0), +SOC_SINGLE("ALC Hold Time", AC97_CODEC_CLASS_REV, 8, 15, 0), +SOC_SINGLE("ALC Decay Time", AC97_CODEC_CLASS_REV, 4, 15, 0), +SOC_SINGLE("ALC Attack Time", AC97_CODEC_CLASS_REV, 0, 15, 0), +SOC_ENUM("ALC Function", wm9712_enum[0]), +SOC_SINGLE("ALC Max Volume", AC97_PCI_SVID, 11, 7, 0), +SOC_SINGLE("ALC ZC Timeout", AC97_PCI_SVID, 9, 3, 1), +SOC_SINGLE("ALC ZC Switch", AC97_PCI_SVID, 8, 1, 0), +SOC_SINGLE("ALC NG Switch", AC97_PCI_SVID, 7, 1, 0), +SOC_ENUM("ALC NG Type", wm9712_enum[10]), +SOC_SINGLE("ALC NG Threshold", AC97_PCI_SVID, 0, 31, 1), + +SOC_SINGLE("Mic Headphone Volume", AC97_VIDEO, 12, 7, 1), +SOC_SINGLE("ALC Headphone Volume", AC97_VIDEO, 7, 7, 1), + +SOC_SINGLE("Out3 Switch", AC97_AUX, 15, 1, 1), +SOC_SINGLE("Out3 ZC Switch", AC97_AUX, 7, 1, 1), +SOC_SINGLE("Out3 Volume", AC97_AUX, 0, 31, 1), + +SOC_SINGLE("PCBeep Bypass Headphone Volume", AC97_PC_BEEP, 12, 7, 1), +SOC_SINGLE("PCBeep Bypass Speaker Volume", AC97_PC_BEEP, 8, 7, 1), +SOC_SINGLE("PCBeep Bypass Phone Volume", AC97_PC_BEEP, 4, 7, 1), + +SOC_SINGLE("Aux Playback Headphone Volume", AC97_CD, 12, 7, 1), +SOC_SINGLE("Aux Playback Speaker Volume", AC97_CD, 8, 7, 1), +SOC_SINGLE("Aux Playback Phone Volume", AC97_CD, 4, 7, 1), + +SOC_SINGLE("Phone Volume", AC97_PHONE, 0, 15, 0), +SOC_DOUBLE("Line Capture Volume", AC97_LINE, 8, 0, 31, 1), + +SOC_SINGLE("Capture 20dB Boost Switch", AC97_REC_SEL, 14, 1, 0), +SOC_SINGLE("Capture to Phone 20dB Boost Switch", AC97_REC_SEL, 11, 1, 1), + +SOC_SINGLE("3D Upper Cut-off Switch", AC97_3D_CONTROL, 5, 1, 1), +SOC_SINGLE("3D Lower Cut-off Switch", AC97_3D_CONTROL, 4, 1, 1), +SOC_SINGLE("3D Playback Volume", AC97_3D_CONTROL, 0, 15, 0), + +SOC_ENUM("Bass Control", wm9712_enum[5]), +SOC_SINGLE("Bass Cut-off Switch", AC97_MASTER_TONE, 12, 1, 1), +SOC_SINGLE("Tone Cut-off Switch", AC97_MASTER_TONE, 4, 1, 1), +SOC_SINGLE("Playback Attenuate (-6dB) Switch", AC97_MASTER_TONE, 6, 1, 0), +SOC_SINGLE("Bass Volume", AC97_MASTER_TONE, 8, 15, 0), +SOC_SINGLE("Treble Volume", AC97_MASTER_TONE, 0, 15, 0), + +SOC_SINGLE("Capture ADC Switch", AC97_REC_GAIN, 15, 1, 1), +SOC_ENUM("Capture Volume Steps", wm9712_enum[6]), +SOC_DOUBLE("Capture Volume", AC97_REC_GAIN, 8, 0, 63, 1), +SOC_SINGLE("Capture ZC Switch", AC97_REC_GAIN, 7, 1, 0), + +SOC_SINGLE("Mic 1 Volume", AC97_MIC, 8, 31, 1), +SOC_SINGLE("Mic 2 Volume", AC97_MIC, 0, 31, 1), +SOC_SINGLE("Mic 20dB Boost Switch", AC97_MIC, 7, 1, 0), +}; + +/* add non dapm controls */ +static int wm9712_add_controls(struct snd_soc_codec *codec) +{ + int err, i; + + for (i = 0; i < ARRAY_SIZE(wm9712_snd_ac97_controls); i++) { + err = snd_ctl_add(codec->card, + snd_soc_cnew(&wm9712_snd_ac97_controls[i],codec, NULL)); + if (err < 0) + return err; + } + return 0; +} + +/* We have to create a fake left and right HP mixers because + * the codec only has a single control that is shared by both channels. + * This makes it impossible to determine the audio path. + */ +static int mixer_event (struct snd_soc_dapm_widget *w, int event) +{ + u16 l, r, beep, line, phone, mic, pcm, aux; + + l = ac97_read(w->codec, HPL_MIXER); + r = ac97_read(w->codec, HPR_MIXER); + beep = ac97_read(w->codec, AC97_PC_BEEP); + mic = ac97_read(w->codec, AC97_VIDEO); + phone = ac97_read(w->codec, AC97_PHONE); + line = ac97_read(w->codec, AC97_LINE); + pcm = ac97_read(w->codec, AC97_PCM); + aux = ac97_read(w->codec, AC97_CD); + + if (l & 0x1 || r & 0x1) + ac97_write(w->codec, AC97_VIDEO, mic & 0x7fff); + else + ac97_write(w->codec, AC97_VIDEO, mic | 0x8000); + + if (l & 0x2 || r & 0x2) + ac97_write(w->codec, AC97_PCM, pcm & 0x7fff); + else + ac97_write(w->codec, AC97_PCM, pcm | 0x8000); + + if (l & 0x4 || r & 0x4) + ac97_write(w->codec, AC97_LINE, line & 0x7fff); + else + ac97_write(w->codec, AC97_LINE, line | 0x8000); + + if (l & 0x8 || r & 0x8) + ac97_write(w->codec, AC97_PHONE, phone & 0x7fff); + else + ac97_write(w->codec, AC97_PHONE, phone | 0x8000); + + if (l & 0x10 || r & 0x10) + ac97_write(w->codec, AC97_CD, aux & 0x7fff); + else + ac97_write(w->codec, AC97_CD, aux | 0x8000); + + if (l & 0x20 || r & 0x20) + ac97_write(w->codec, AC97_PC_BEEP, beep & 0x7fff); + else + ac97_write(w->codec, AC97_PC_BEEP, beep | 0x8000); + + return 0; +} + +/* Left Headphone Mixers */ +static const struct snd_kcontrol_new wm9712_hpl_mixer_controls[] = { + SOC_DAPM_SINGLE("PCBeep Bypass Switch", HPL_MIXER, 5, 1, 0), + SOC_DAPM_SINGLE("Aux Playback Switch", HPL_MIXER, 4, 1, 0), + SOC_DAPM_SINGLE("Phone Bypass Switch", HPL_MIXER, 3, 1, 0), + SOC_DAPM_SINGLE("Line Bypass Switch", HPL_MIXER, 2, 1, 0), + SOC_DAPM_SINGLE("PCM Playback Switch", HPL_MIXER, 1, 1, 0), + SOC_DAPM_SINGLE("Mic Sidetone Switch", HPL_MIXER, 0, 1, 0), +}; + +/* Right Headphone Mixers */ +static const struct snd_kcontrol_new wm9712_hpr_mixer_controls[] = { + SOC_DAPM_SINGLE("PCBeep Bypass Switch", HPR_MIXER, 5, 1, 0), + SOC_DAPM_SINGLE("Aux Playback Switch", HPR_MIXER, 4, 1, 0), + SOC_DAPM_SINGLE("Phone Bypass Switch", HPR_MIXER, 3, 1, 0), + SOC_DAPM_SINGLE("Line Bypass Switch", HPR_MIXER, 2, 1, 0), + SOC_DAPM_SINGLE("PCM Playback Switch", HPR_MIXER, 1, 1, 0), + SOC_DAPM_SINGLE("Mic Sidetone Switch", HPR_MIXER, 0, 1, 0), +}; + +/* Speaker Mixer */ +static const struct snd_kcontrol_new wm9712_speaker_mixer_controls[] = { + SOC_DAPM_SINGLE("PCBeep Bypass Switch", AC97_PC_BEEP, 11, 1, 1), + SOC_DAPM_SINGLE("Aux Playback Switch", AC97_CD, 11, 1, 1), + SOC_DAPM_SINGLE("Phone Bypass Switch", AC97_PHONE, 14, 1, 1), + SOC_DAPM_SINGLE("Line Bypass Switch", AC97_LINE, 14, 1, 1), + SOC_DAPM_SINGLE("PCM Playback Switch", AC97_PCM, 14, 1, 1), +}; + +/* Phone Mixer */ +static const struct snd_kcontrol_new wm9712_phone_mixer_controls[] = { + SOC_DAPM_SINGLE("PCBeep Bypass Switch", AC97_PC_BEEP, 7, 1, 1), + SOC_DAPM_SINGLE("Aux Playback Switch", AC97_CD, 7, 1, 1), + SOC_DAPM_SINGLE("Line Bypass Switch", AC97_LINE, 13, 1, 1), + SOC_DAPM_SINGLE("PCM Playback Switch", AC97_PCM, 13, 1, 1), + SOC_DAPM_SINGLE("Mic 1 Sidetone Switch", AC97_MIC, 14, 1, 1), + SOC_DAPM_SINGLE("Mic 2 Sidetone Switch", AC97_MIC, 13, 1, 1), +}; + +/* ALC headphone mux */ +static const struct snd_kcontrol_new wm9712_alc_mux_controls = +SOC_DAPM_ENUM("Route", wm9712_enum[1]); + +/* out 3 mux */ +static const struct snd_kcontrol_new wm9712_out3_mux_controls = +SOC_DAPM_ENUM("Route", wm9712_enum[2]); + +/* spk mux */ +static const struct snd_kcontrol_new wm9712_spk_mux_controls = +SOC_DAPM_ENUM("Route", wm9712_enum[3]); + +/* Capture to Phone mux */ +static const struct snd_kcontrol_new wm9712_capture_phone_mux_controls = +SOC_DAPM_ENUM("Route", wm9712_enum[4]); + +/* Capture left select */ +static const struct snd_kcontrol_new wm9712_capture_selectl_controls = +SOC_DAPM_ENUM("Route", wm9712_enum[8]); + +/* Capture right select */ +static const struct snd_kcontrol_new wm9712_capture_selectr_controls = +SOC_DAPM_ENUM("Route", wm9712_enum[9]); + +/* Mic select */ +static const struct snd_kcontrol_new wm9712_mic_src_controls = +SOC_DAPM_ENUM("Route", wm9712_enum[7]); + +/* diff select */ +static const struct snd_kcontrol_new wm9712_diff_sel_controls = +SOC_DAPM_ENUM("Route", wm9712_enum[11]); + +static const struct snd_soc_dapm_widget wm9712_dapm_widgets[] = { +SND_SOC_DAPM_MUX("ALC Sidetone Mux", SND_SOC_NOPM, 0, 0, + &wm9712_alc_mux_controls), +SND_SOC_DAPM_MUX("Out3 Mux", SND_SOC_NOPM, 0, 0, + &wm9712_out3_mux_controls), +SND_SOC_DAPM_MUX("Speaker Mux", SND_SOC_NOPM, 0, 0, + &wm9712_spk_mux_controls), +SND_SOC_DAPM_MUX("Capture Phone Mux", SND_SOC_NOPM, 0, 0, + &wm9712_capture_phone_mux_controls), +SND_SOC_DAPM_MUX("Left Capture Select", SND_SOC_NOPM, 0, 0, + &wm9712_capture_selectl_controls), +SND_SOC_DAPM_MUX("Right Capture Select", SND_SOC_NOPM, 0, 0, + &wm9712_capture_selectr_controls), +SND_SOC_DAPM_MUX("Mic Select Source", SND_SOC_NOPM, 0, 0, + &wm9712_mic_src_controls), +SND_SOC_DAPM_MUX("Differential Source", SND_SOC_NOPM, 0, 0, + &wm9712_diff_sel_controls), +SND_SOC_DAPM_MIXER("AC97 Mixer", SND_SOC_NOPM, 0, 0, NULL, 0), +SND_SOC_DAPM_MIXER_E("Left HP Mixer", AC97_INT_PAGING, 9, 1, + &wm9712_hpl_mixer_controls[0], ARRAY_SIZE(wm9712_hpl_mixer_controls), + mixer_event, SND_SOC_DAPM_POST_REG), +SND_SOC_DAPM_MIXER_E("Right HP Mixer", AC97_INT_PAGING, 8, 1, + &wm9712_hpr_mixer_controls[0], ARRAY_SIZE(wm9712_hpr_mixer_controls), + mixer_event, SND_SOC_DAPM_POST_REG), +SND_SOC_DAPM_MIXER("Phone Mixer", AC97_INT_PAGING, 6, 1, + &wm9712_phone_mixer_controls[0], ARRAY_SIZE(wm9712_phone_mixer_controls)), +SND_SOC_DAPM_MIXER("Speaker Mixer", AC97_INT_PAGING, 7, 1, + &wm9712_speaker_mixer_controls[0], + ARRAY_SIZE(wm9712_speaker_mixer_controls)), +SND_SOC_DAPM_MIXER("Mono Mixer", SND_SOC_NOPM, 0, 0, NULL, 0), +SND_SOC_DAPM_DAC("Left DAC", "Left HiFi Playback", AC97_INT_PAGING, 14, 1), +SND_SOC_DAPM_DAC("Right DAC", "Right HiFi Playback", AC97_INT_PAGING, 13, 1), +SND_SOC_DAPM_DAC("Aux DAC", "Aux Playback", SND_SOC_NOPM, 0, 0), +SND_SOC_DAPM_ADC("Left ADC", "Left HiFi Capture", AC97_INT_PAGING, 12, 1), +SND_SOC_DAPM_ADC("Right ADC", "Right HiFi Capture", AC97_INT_PAGING, 11, 1), +SND_SOC_DAPM_PGA("Headphone PGA", AC97_INT_PAGING, 4, 1, NULL, 0), +SND_SOC_DAPM_PGA("Speaker PGA", AC97_INT_PAGING, 3, 1, NULL, 0), +SND_SOC_DAPM_PGA("Out 3 PGA", AC97_INT_PAGING, 5, 1, NULL, 0), +SND_SOC_DAPM_PGA("Line PGA", AC97_INT_PAGING, 2, 1, NULL, 0), +SND_SOC_DAPM_PGA("Phone PGA", AC97_INT_PAGING, 1, 1, NULL, 0), +SND_SOC_DAPM_PGA("Mic PGA", AC97_INT_PAGING, 0, 1, NULL, 0), +SND_SOC_DAPM_MICBIAS("Mic Bias", AC97_INT_PAGING, 10, 1), +SND_SOC_DAPM_OUTPUT("MONOOUT"), +SND_SOC_DAPM_OUTPUT("HPOUTL"), +SND_SOC_DAPM_OUTPUT("HPOUTR"), +SND_SOC_DAPM_OUTPUT("LOUT2"), +SND_SOC_DAPM_OUTPUT("ROUT2"), +SND_SOC_DAPM_OUTPUT("OUT3"), +SND_SOC_DAPM_INPUT("LINEINL"), +SND_SOC_DAPM_INPUT("LINEINR"), +SND_SOC_DAPM_INPUT("PHONE"), +SND_SOC_DAPM_INPUT("PCBEEP"), +SND_SOC_DAPM_INPUT("MIC1"), +SND_SOC_DAPM_INPUT("MIC2"), +}; + +static const char *audio_map[][3] = { + /* virtual mixer - mixes left & right channels for spk and mono */ + {"AC97 Mixer", NULL, "Left DAC"}, + {"AC97 Mixer", NULL, "Right DAC"}, + + /* Left HP mixer */ + {"Left HP Mixer", "PCBeep Bypass Switch", "PCBEEP"}, + {"Left HP Mixer", "Aux Playback Switch", "Aux DAC"}, + {"Left HP Mixer", "Phone Bypass Switch", "Phone PGA"}, + {"Left HP Mixer", "Line Bypass Switch", "Line PGA"}, + {"Left HP Mixer", "PCM Playback Switch", "Left DAC"}, + {"Left HP Mixer", "Mic Sidetone Switch", "Mic PGA"}, + {"Left HP Mixer", NULL, "ALC Sidetone Mux"}, + //{"Right HP Mixer", NULL, "HP Mixer"}, + + /* Right HP mixer */ + {"Right HP Mixer", "PCBeep Bypass Switch", "PCBEEP"}, + {"Right HP Mixer", "Aux Playback Switch", "Aux DAC"}, + {"Right HP Mixer", "Phone Bypass Switch", "Phone PGA"}, + {"Right HP Mixer", "Line Bypass Switch", "Line PGA"}, + {"Right HP Mixer", "PCM Playback Switch", "Right DAC"}, + {"Right HP Mixer", "Mic Sidetone Switch", "Mic PGA"}, + {"Right HP Mixer", NULL, "ALC Sidetone Mux"}, + + /* speaker mixer */ + {"Speaker Mixer", "PCBeep Bypass Switch", "PCBEEP"}, + {"Speaker Mixer", "Line Bypass Switch", "Line PGA"}, + {"Speaker Mixer", "PCM Playback Switch", "AC97 Mixer"}, + {"Speaker Mixer", "Phone Bypass Switch", "Phone PGA"}, + {"Speaker Mixer", "Aux Playback Switch", "Aux DAC"}, + + /* Phone mixer */ + {"Phone Mixer", "PCBeep Bypass Switch", "PCBEEP"}, + {"Phone Mixer", "Line Bypass Switch", "Line PGA"}, + {"Phone Mixer", "Aux Playback Switch", "Aux DAC"}, + {"Phone Mixer", "PCM Playback Switch", "AC97 Mixer"}, + {"Phone Mixer", "Mic 1 Sidetone Switch", "Mic PGA"}, + {"Phone Mixer", "Mic 2 Sidetone Switch", "Mic PGA"}, + + /* inputs */ + {"Line PGA", NULL, "LINEINL"}, + {"Line PGA", NULL, "LINEINR"}, + {"Phone PGA", NULL, "PHONE"}, + {"Mic PGA", NULL, "MIC1"}, + {"Mic PGA", NULL, "MIC2"}, + + /* left capture selector */ + {"Left Capture Select", "Mic", "MIC1"}, + {"Left Capture Select", "Speaker Mixer", "Speaker Mixer"}, + {"Left Capture Select", "Line", "LINEINL"}, + {"Left Capture Select", "Headphone Mixer", "Left HP Mixer"}, + {"Left Capture Select", "Phone Mixer", "Phone Mixer"}, + {"Left Capture Select", "Phone", "PHONE"}, + + /* right capture selector */ + {"Right Capture Select", "Mic", "MIC2"}, + {"Right Capture Select", "Speaker Mixer", "Speaker Mixer"}, + {"Right Capture Select", "Line", "LINEINR"}, + {"Right Capture Select", "Headphone Mixer", "Right HP Mixer"}, + {"Right Capture Select", "Phone Mixer", "Phone Mixer"}, + {"Right Capture Select", "Phone", "PHONE"}, + + /* ALC Sidetone */ + {"ALC Sidetone Mux", "Stereo", "Left Capture Select"}, + {"ALC Sidetone Mux", "Stereo", "Right Capture Select"}, + {"ALC Sidetone Mux", "Left", "Left Capture Select"}, + {"ALC Sidetone Mux", "Right", "Right Capture Select"}, + + /* ADC's */ + {"Left ADC", NULL, "Left Capture Select"}, + {"Right ADC", NULL, "Right Capture Select"}, + + /* outputs */ + {"MONOOUT", NULL, "Phone Mixer"}, + {"HPOUTL", NULL, "Headphone PGA"}, + {"Headphone PGA", NULL, "Left HP Mixer"}, + {"HPOUTR", NULL, "Headphone PGA"}, + {"Headphone PGA", NULL, "Right HP Mixer"}, + + /* mono hp mixer */ + {"Mono HP Mixer", NULL, "Left HP Mixer"}, + {"Mono HP Mixer", NULL, "Right HP Mixer"}, + + /* Out3 Mux */ + {"Out3 Mux", "Left", "Left HP Mixer"}, + {"Out3 Mux", "Mono", "Phone Mixer"}, + {"Out3 Mux", "Left + Right", "Mono HP Mixer"}, + {"Out 3 PGA", NULL, "Out3 Mux"}, + {"OUT3", NULL, "Out 3 PGA"}, + + /* speaker Mux */ + {"Speaker Mux", "Speaker Mix", "Speaker Mixer"}, + {"Speaker Mux", "Headphone Mix", "Mono HP Mixer"}, + {"Speaker PGA", NULL, "Speaker Mux"}, + {"LOUT2", NULL, "Speaker PGA"}, + {"ROUT2", NULL, "Speaker PGA"}, + + {NULL, NULL, NULL}, +}; + +static int wm9712_add_widgets(struct snd_soc_codec *codec) +{ + int i; + + for(i = 0; i < ARRAY_SIZE(wm9712_dapm_widgets); i++) { + snd_soc_dapm_new_control(codec, &wm9712_dapm_widgets[i]); + } + + /* set up audio path audio_mapnects */ + for(i = 0; audio_map[i][0] != NULL; i++) { + snd_soc_dapm_connect_input(codec, audio_map[i][0], + audio_map[i][1], audio_map[i][2]); + } + + snd_soc_dapm_new_widgets(codec); + return 0; +} + +static unsigned int ac97_read(struct snd_soc_codec *codec, + unsigned int reg) +{ + u16 *cache = codec->reg_cache; + + if (reg == AC97_RESET || reg == AC97_GPIO_STATUS || + reg == AC97_VENDOR_ID1 || reg == AC97_VENDOR_ID2 || + reg == AC97_REC_GAIN) + return soc_ac97_ops.read(codec->ac97, reg); + else { + reg = reg >> 1; + + if (reg > (ARRAY_SIZE(wm9712_reg))) + return -EIO; + + return cache[reg]; + } +} + +static int ac97_write(struct snd_soc_codec *codec, unsigned int reg, + unsigned int val) +{ + u16 *cache = codec->reg_cache; + + soc_ac97_ops.write(codec->ac97, reg, val); + reg = reg >> 1; + if (reg <= (ARRAY_SIZE(wm9712_reg))) + cache[reg] = val; + + return 0; +} + +static int ac97_prepare(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_codec *codec = socdev->codec; + int reg; + u16 vra; + + vra = ac97_read(codec, AC97_EXTENDED_STATUS); + ac97_write(codec, AC97_EXTENDED_STATUS, vra | 0x1); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + reg = AC97_PCM_FRONT_DAC_RATE; + else + reg = AC97_PCM_LR_ADC_RATE; + + return ac97_write(codec, reg, runtime->rate); +} + +static int ac97_aux_prepare(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_codec *codec = socdev->codec; + u16 vra, xsle; + + vra = ac97_read(codec, AC97_EXTENDED_STATUS); + ac97_write(codec, AC97_EXTENDED_STATUS, vra | 0x1); + xsle = ac97_read(codec, AC97_PCI_SID); + ac97_write(codec, AC97_PCI_SID, xsle | 0x8000); + + if (substream->stream != SNDRV_PCM_STREAM_PLAYBACK) + return -ENODEV; + + return ac97_write(codec, AC97_PCM_SURR_DAC_RATE, runtime->rate); +} + +#define WM9712_AC97_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 |\ + SNDRV_PCM_RATE_22050 | SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) + +struct snd_soc_codec_dai wm9712_dai[] = { +{ + .name = "AC97 HiFi", + .playback = { + .stream_name = "HiFi Playback", + .channels_min = 1, + .channels_max = 2, + .rates = WM9712_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .capture = { + .stream_name = "HiFi Capture", + .channels_min = 1, + .channels_max = 2, + .rates = WM9712_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .prepare = ac97_prepare,}, +}, +{ + .name = "AC97 Aux", + .playback = { + .stream_name = "Aux Playback", + .channels_min = 1, + .channels_max = 1, + .rates = WM9712_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .prepare = ac97_aux_prepare,}, +} +}; +EXPORT_SYMBOL_GPL(wm9712_dai); + +static int wm9712_dapm_event(struct snd_soc_codec *codec, int event) +{ + u16 reg; + + switch (event) { + case SNDRV_CTL_POWER_D0: /* full On */ + /* liam - maybe enable thermal shutdown */ + reg = ac97_read(codec, AC97_EXTENDED_MID) & 0xdfff; + ac97_write(codec, AC97_EXTENDED_MID, reg); + break; + case SNDRV_CTL_POWER_D1: /* partial On */ + case SNDRV_CTL_POWER_D2: /* partial On */ + break; + case SNDRV_CTL_POWER_D3hot: /* Off, with power */ + /* enable master bias and vmid */ + reg = ac97_read(codec, AC97_EXTENDED_MID) & 0xbbff; + ac97_write(codec, AC97_EXTENDED_MID, reg); + ac97_write(codec, AC97_POWERDOWN, 0x0000); + break; + case SNDRV_CTL_POWER_D3cold: /* Off, without power */ + /* disable everything including AC link */ + ac97_write(codec, AC97_EXTENDED_MID, 0xffff); + ac97_write(codec, AC97_EXTENDED_MSTATUS, 0xffff); + ac97_write(codec, AC97_POWERDOWN, 0xffff); + break; + } + codec->dapm_state = event; + return 0; +} + +static int wm9712_reset(struct snd_soc_codec *codec, int try_warm) +{ + if (try_warm && soc_ac97_ops.warm_reset) { + soc_ac97_ops.warm_reset(codec->ac97); + if (!(ac97_read(codec, 0) & 0x8000)) + return 1; + } + + soc_ac97_ops.reset(codec->ac97); + if (ac97_read(codec, 0) & 0x8000) + goto err; + return 0; + +err: + printk(KERN_ERR "WM9712 AC97 reset failed\n"); + return -EIO; +} + +static int wm9712_soc_suspend(struct platform_device *pdev, + pm_message_t state) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + + wm9712_dapm_event(codec, SNDRV_CTL_POWER_D3cold); + return 0; +} + +static int wm9712_soc_resume(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + int i, ret; + u16 *cache = codec->reg_cache; + + ret = wm9712_reset(codec, 1); + if (ret < 0){ + printk(KERN_ERR "could not reset AC97 codec\n"); + return ret; + } + + wm9712_dapm_event(codec, SNDRV_CTL_POWER_D3hot); + + if (ret == 0) { + /* Sync reg_cache with the hardware after cold reset */ + for (i = 2; i < ARRAY_SIZE(wm9712_reg) << 1; i+=2) { + if (i == AC97_INT_PAGING || i == AC97_POWERDOWN || + (i > 0x58 && i != 0x5c)) + continue; + soc_ac97_ops.write(codec->ac97, i, cache[i>>1]); + } + } + + if (codec->suspend_dapm_state == SNDRV_CTL_POWER_D0) + wm9712_dapm_event(codec, SNDRV_CTL_POWER_D0); + + return ret; +} + +static int wm9712_soc_probe(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec; + int ret = 0; + + printk(KERN_INFO "WM9711/WM9712 SoC Audio Codec %s\n", WM9712_VERSION); + + socdev->codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL); + if (socdev->codec == NULL) + return -ENOMEM; + codec = socdev->codec; + mutex_init(&codec->mutex); + + codec->reg_cache = + kzalloc(sizeof(u16) * ARRAY_SIZE(wm9712_reg), GFP_KERNEL); + if (codec->reg_cache == NULL) { + ret = -ENOMEM; + goto cache_err; + } + memcpy(codec->reg_cache, wm9712_reg, sizeof(u16) * ARRAY_SIZE(wm9712_reg)); + codec->reg_cache_size = sizeof(u16) * ARRAY_SIZE(wm9712_reg); + codec->reg_cache_step = 2; + + codec->name = "WM9712"; + codec->owner = THIS_MODULE; + codec->dai = wm9712_dai; + codec->num_dai = ARRAY_SIZE(wm9712_dai); + codec->write = ac97_write; + codec->read = ac97_read; + codec->dapm_event = wm9712_dapm_event; + INIT_LIST_HEAD(&codec->dapm_widgets); + INIT_LIST_HEAD(&codec->dapm_paths); + + ret = snd_soc_new_ac97_codec(codec, &soc_ac97_ops, 0); + if (ret < 0) { + printk(KERN_ERR "wm9712: failed to register AC97 codec\n"); + goto codec_err; + } + + /* register pcms */ + ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1); + if (ret < 0) + goto pcm_err; + + ret = wm9712_reset(codec, 0); + if (ret < 0) { + printk(KERN_ERR "AC97 link error\n"); + goto reset_err; + } + + /* set alc mux to none */ + ac97_write(codec, AC97_VIDEO, ac97_read(codec, AC97_VIDEO) | 0x3000); + + wm9712_dapm_event(codec, SNDRV_CTL_POWER_D3hot); + wm9712_add_controls(codec); + wm9712_add_widgets(codec); + ret = snd_soc_register_card(socdev); + if (ret < 0) { + printk(KERN_ERR "wm9712: failed to register card\n"); + goto reset_err; + } + + return 0; + +reset_err: + snd_soc_free_pcms(socdev); + +pcm_err: + snd_soc_free_ac97_codec(codec); + +codec_err: + kfree(codec->reg_cache); + +cache_err: + kfree(socdev->codec); + socdev->codec = NULL; + return ret; +} + +static int wm9712_soc_remove(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + + if (codec == NULL) + return 0; + + snd_soc_dapm_free(socdev); + snd_soc_free_pcms(socdev); + snd_soc_free_ac97_codec(codec); + kfree(codec->reg_cache); + kfree(codec); + return 0; +} + +struct snd_soc_codec_device soc_codec_dev_wm9712 = { + .probe = wm9712_soc_probe, + .remove = wm9712_soc_remove, + .suspend = wm9712_soc_suspend, + .resume = wm9712_soc_resume, +}; + +EXPORT_SYMBOL_GPL(soc_codec_dev_wm9712); + +MODULE_DESCRIPTION("ASoC WM9711/WM9712 driver"); +MODULE_AUTHOR("Liam Girdwood"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wm9712.h b/sound/soc/codecs/wm9712.h new file mode 100644 index 00000000000..719105d61e6 --- /dev/null +++ b/sound/soc/codecs/wm9712.h @@ -0,0 +1,14 @@ +/* + * wm9712.h -- WM9712 Soc Audio driver + */ + +#ifndef _WM9712_H +#define _WM9712_H + +#define WM9712_DAI_AC97_HIFI 0 +#define WM9712_DAI_AC97_AUX 1 + +extern struct snd_soc_codec_dai wm9712_dai[2]; +extern struct snd_soc_codec_device soc_codec_dev_wm9712; + +#endif diff --git a/sound/soc/pxa/Kconfig b/sound/soc/pxa/Kconfig new file mode 100644 index 00000000000..579e1c8d2b2 --- /dev/null +++ b/sound/soc/pxa/Kconfig @@ -0,0 +1,60 @@ +menu "SoC Audio for the Intel PXA2xx" + +config SND_PXA2XX_SOC + tristate "SoC Audio for the Intel PXA2xx chip" + depends on ARCH_PXA && SND + select SND_PCM + help + Say Y or M if you want to add support for codecs attached to + the PXA2xx AC97, I2S or SSP interface. You will also need + to select the audio interfaces to support below. + +config SND_PXA2XX_AC97 + tristate + select SND_AC97_CODEC + +config SND_PXA2XX_SOC_AC97 + tristate + select AC97_BUS + select SND_SOC_AC97_BUS + +config SND_PXA2XX_SOC_I2S + tristate + +config SND_PXA2XX_SOC_CORGI + tristate "SoC Audio support for Sharp Zaurus SL-C7x0" + depends on SND_PXA2XX_SOC && PXA_SHARP_C7xx + select SND_PXA2XX_SOC_I2S + select SND_SOC_WM8731 + help + Say Y if you want to add support for SoC audio on Sharp + Zaurus SL-C7x0 models (Corgi, Shepherd, Husky). + +config SND_PXA2XX_SOC_SPITZ + tristate "SoC Audio support for Sharp Zaurus SL-Cxx00" + depends on SND_PXA2XX_SOC && PXA_SHARP_Cxx00 + select SND_PXA2XX_SOC_I2S + select SND_SOC_WM8750 + help + Say Y if you want to add support for SoC audio on Sharp + Zaurus SL-Cxx00 models (Spitz, Borzoi and Akita). + +config SND_PXA2XX_SOC_POODLE + tristate "SoC Audio support for Poodle" + depends on SND_PXA2XX_SOC && MACH_POODLE + select SND_PXA2XX_SOC_I2S + select SND_SOC_WM8731 + help + Say Y if you want to add support for SoC audio on Sharp + Zaurus SL-5600 model (Poodle). + +config SND_PXA2XX_SOC_TOSA + tristate "SoC AC97 Audio support for Tosa" + depends on SND_PXA2XX_SOC && MACH_TOSA + select SND_PXA2XX_SOC_AC97 + select SND_SOC_WM9712 + help + Say Y if you want to add support for SoC audio on Sharp + Zaurus SL-C6000x models (Tosa). + +endmenu diff --git a/sound/soc/pxa/Makefile b/sound/soc/pxa/Makefile new file mode 100644 index 00000000000..78e0d6b07d1 --- /dev/null +++ b/sound/soc/pxa/Makefile @@ -0,0 +1,20 @@ +# PXA Platform Support +snd-soc-pxa2xx-objs := pxa2xx-pcm.o +snd-soc-pxa2xx-ac97-objs := pxa2xx-ac97.o +snd-soc-pxa2xx-i2s-objs := pxa2xx-i2s.o + +obj-$(CONFIG_SND_PXA2XX_SOC) += snd-soc-pxa2xx.o +obj-$(CONFIG_SND_PXA2XX_SOC_AC97) += snd-soc-pxa2xx-ac97.o +obj-$(CONFIG_SND_PXA2XX_SOC_I2S) += snd-soc-pxa2xx-i2s.o + +# PXA Machine Support +snd-soc-corgi-objs := corgi.o +snd-soc-poodle-objs := poodle.o +snd-soc-tosa-objs := tosa.o +snd-soc-spitz-objs := spitz.o + +obj-$(CONFIG_SND_PXA2XX_SOC_CORGI) += snd-soc-corgi.o +obj-$(CONFIG_SND_PXA2XX_SOC_POODLE) += snd-soc-poodle.o +obj-$(CONFIG_SND_PXA2XX_SOC_TOSA) += snd-soc-tosa.o +obj-$(CONFIG_SND_PXA2XX_SOC_SPITZ) += snd-soc-spitz.o + diff --git a/sound/soc/pxa/corgi.c b/sound/soc/pxa/corgi.c new file mode 100644 index 00000000000..5ee51a994ac --- /dev/null +++ b/sound/soc/pxa/corgi.c @@ -0,0 +1,383 @@ +/* + * corgi.c -- SoC audio for Corgi + * + * Copyright 2005 Wolfson Microelectronics PLC. + * Copyright 2005 Openedhand Ltd. + * + * Authors: Liam Girdwood <liam.girdwood@wolfsonmicro.com> + * Richard Purdie <richard@openedhand.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Revision history + * 30th Nov 2005 Initial version. + * + */ + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/timer.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> + +#include <asm/mach-types.h> +#include <asm/hardware/scoop.h> +#include <asm/arch/pxa-regs.h> +#include <asm/arch/hardware.h> +#include <asm/arch/corgi.h> +#include <asm/arch/audio.h> + +#include "../codecs/wm8731.h" +#include "pxa2xx-pcm.h" +#include "pxa2xx-i2s.h" + +#define CORGI_HP 0 +#define CORGI_MIC 1 +#define CORGI_LINE 2 +#define CORGI_HEADSET 3 +#define CORGI_HP_OFF 4 +#define CORGI_SPK_ON 0 +#define CORGI_SPK_OFF 1 + + /* audio clock in Hz - rounded from 12.235MHz */ +#define CORGI_AUDIO_CLOCK 12288000 + +static int corgi_jack_func; +static int corgi_spk_func; + +static void corgi_ext_control(struct snd_soc_codec *codec) +{ + int spk = 0, mic = 0, line = 0, hp = 0, hs = 0; + + /* set up jack connection */ + switch (corgi_jack_func) { + case CORGI_HP: + hp = 1; + /* set = unmute headphone */ + set_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_L); + set_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_R); + break; + case CORGI_MIC: + mic = 1; + /* reset = mute headphone */ + reset_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_L); + reset_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_R); + break; + case CORGI_LINE: + line = 1; + reset_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_L); + reset_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_R); + break; + case CORGI_HEADSET: + hs = 1; + mic = 1; + reset_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_L); + set_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_R); + break; + } + + if (corgi_spk_func == CORGI_SPK_ON) + spk = 1; + + /* set the enpoints to their new connetion states */ + snd_soc_dapm_set_endpoint(codec, "Ext Spk", spk); + snd_soc_dapm_set_endpoint(codec, "Mic Jack", mic); + snd_soc_dapm_set_endpoint(codec, "Line Jack", line); + snd_soc_dapm_set_endpoint(codec, "Headphone Jack", hp); + snd_soc_dapm_set_endpoint(codec, "Headset Jack", hs); + + /* signal a DAPM event */ + snd_soc_dapm_sync_endpoints(codec); +} + +static int corgi_startup(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec *codec = rtd->socdev->codec; + + /* check the jack status at stream startup */ + corgi_ext_control(codec); + return 0; +} + +/* we need to unmute the HP at shutdown as the mute burns power on corgi */ +static int corgi_shutdown(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec *codec = rtd->socdev->codec; + + /* set = unmute headphone */ + set_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_L); + set_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MUTE_R); + return 0; +} + +static int corgi_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec_dai *codec_dai = rtd->dai->codec_dai; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + unsigned int clk = 0; + int ret = 0; + + switch (params_rate(params)) { + case 8000: + case 16000: + case 48000: + case 96000: + clk = 12288000; + break; + case 11025: + case 22050: + case 44100: + clk = 11289600; + break; + } + + /* set codec DAI configuration */ + ret = codec_dai->dai_ops.set_fmt(codec_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS); + if (ret < 0) + return ret; + + /* set cpu DAI configuration */ + ret = cpu_dai->dai_ops.set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS); + if (ret < 0) + return ret; + + /* set the codec system clock for DAC and ADC */ + ret = codec_dai->dai_ops.set_sysclk(codec_dai, WM8731_SYSCLK, clk, + SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + /* set the I2S system clock as input (unused) */ + ret = cpu_dai->dai_ops.set_sysclk(cpu_dai, PXA2XX_I2S_SYSCLK, 0, + SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + return 0; +} + +static struct snd_soc_ops corgi_ops = { + .startup = corgi_startup, + .hw_params = corgi_hw_params, + .shutdown = corgi_shutdown, +}; + +static int corgi_get_jack(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + ucontrol->value.integer.value[0] = corgi_jack_func; + return 0; +} + +static int corgi_set_jack(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + + if (corgi_jack_func == ucontrol->value.integer.value[0]) + return 0; + + corgi_jack_func = ucontrol->value.integer.value[0]; + corgi_ext_control(codec); + return 1; +} + +static int corgi_get_spk(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + ucontrol->value.integer.value[0] = corgi_spk_func; + return 0; +} + +static int corgi_set_spk(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + + if (corgi_spk_func == ucontrol->value.integer.value[0]) + return 0; + + corgi_spk_func = ucontrol->value.integer.value[0]; + corgi_ext_control(codec); + return 1; +} + +static int corgi_amp_event(struct snd_soc_dapm_widget *w, int event) +{ + if (SND_SOC_DAPM_EVENT_ON(event)) + set_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_APM_ON); + else + reset_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_APM_ON); + + return 0; +} + +static int corgi_mic_event(struct snd_soc_dapm_widget *w, int event) +{ + if (SND_SOC_DAPM_EVENT_ON(event)) + set_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MIC_BIAS); + else + reset_scoop_gpio(&corgiscoop_device.dev, CORGI_SCP_MIC_BIAS); + + return 0; +} + +/* corgi machine dapm widgets */ +static const struct snd_soc_dapm_widget wm8731_dapm_widgets[] = { +SND_SOC_DAPM_HP("Headphone Jack", NULL), +SND_SOC_DAPM_MIC("Mic Jack", corgi_mic_event), +SND_SOC_DAPM_SPK("Ext Spk", corgi_amp_event), +SND_SOC_DAPM_LINE("Line Jack", NULL), +SND_SOC_DAPM_HP("Headset Jack", NULL), +}; + +/* Corgi machine audio map (connections to the codec pins) */ +static const char *audio_map[][3] = { + + /* headset Jack - in = micin, out = LHPOUT*/ + {"Headset Jack", NULL, "LHPOUT"}, + + /* headphone connected to LHPOUT1, RHPOUT1 */ + {"Headphone Jack", NULL, "LHPOUT"}, + {"Headphone Jack", NULL, "RHPOUT"}, + + /* speaker connected to LOUT, ROUT */ + {"Ext Spk", NULL, "ROUT"}, + {"Ext Spk", NULL, "LOUT"}, + + /* mic is connected to MICIN (via right channel of headphone jack) */ + {"MICIN", NULL, "Mic Jack"}, + + /* Same as the above but no mic bias for line signals */ + {"MICIN", NULL, "Line Jack"}, + + {NULL, NULL, NULL}, +}; + +static const char *jack_function[] = {"Headphone", "Mic", "Line", "Headset", + "Off"}; +static const char *spk_function[] = {"On", "Off"}; +static const struct soc_enum corgi_enum[] = { + SOC_ENUM_SINGLE_EXT(5, jack_function), + SOC_ENUM_SINGLE_EXT(2, spk_function), +}; + +static const struct snd_kcontrol_new wm8731_corgi_controls[] = { + SOC_ENUM_EXT("Jack Function", corgi_enum[0], corgi_get_jack, + corgi_set_jack), + SOC_ENUM_EXT("Speaker Function", corgi_enum[1], corgi_get_spk, + corgi_set_spk), +}; + +/* + * Logic for a wm8731 as connected on a Sharp SL-C7x0 Device + */ +static int corgi_wm8731_init(struct snd_soc_codec *codec) +{ + int i, err; + + snd_soc_dapm_set_endpoint(codec, "LLINEIN", 0); + snd_soc_dapm_set_endpoint(codec, "RLINEIN", 0); + + /* Add corgi specific controls */ + for (i = 0; i < ARRAY_SIZE(wm8731_corgi_controls); i++) { + err = snd_ctl_add(codec->card, + snd_soc_cnew(&wm8731_corgi_controls[i],codec, NULL)); + if (err < 0) + return err; + } + + /* Add corgi specific widgets */ + for(i = 0; i < ARRAY_SIZE(wm8731_dapm_widgets); i++) { + snd_soc_dapm_new_control(codec, &wm8731_dapm_widgets[i]); + } + + /* Set up corgi specific audio path audio_map */ + for(i = 0; audio_map[i][0] != NULL; i++) { + snd_soc_dapm_connect_input(codec, audio_map[i][0], + audio_map[i][1], audio_map[i][2]); + } + + snd_soc_dapm_sync_endpoints(codec); + return 0; +} + +/* corgi digital audio interface glue - connects codec <--> CPU */ +static struct snd_soc_dai_link corgi_dai = { + .name = "WM8731", + .stream_name = "WM8731", + .cpu_dai = &pxa_i2s_dai, + .codec_dai = &wm8731_dai, + .init = corgi_wm8731_init, + .ops = &corgi_ops, +}; + +/* corgi audio machine driver */ +static struct snd_soc_machine snd_soc_machine_corgi = { + .name = "Corgi", + .dai_link = &corgi_dai, + .num_links = 1, +}; + +/* corgi audio private data */ +static struct wm8731_setup_data corgi_wm8731_setup = { + .i2c_address = 0x1b, +}; + +/* corgi audio subsystem */ +static struct snd_soc_device corgi_snd_devdata = { + .machine = &snd_soc_machine_corgi, + .platform = &pxa2xx_soc_platform, + .codec_dev = &soc_codec_dev_wm8731, + .codec_data = &corgi_wm8731_setup, +}; + +static struct platform_device *corgi_snd_device; + +static int __init corgi_init(void) +{ + int ret; + + if (!(machine_is_corgi() || machine_is_shepherd() || machine_is_husky())) + return -ENODEV; + + corgi_snd_device = platform_device_alloc("soc-audio", -1); + if (!corgi_snd_device) + return -ENOMEM; + + platform_set_drvdata(corgi_snd_device, &corgi_snd_devdata); + corgi_snd_devdata.dev = &corgi_snd_device->dev; + ret = platform_device_add(corgi_snd_device); + + if (ret) + platform_device_put(corgi_snd_device); + + return ret; +} + +static void __exit corgi_exit(void) +{ + platform_device_unregister(corgi_snd_device); +} + +module_init(corgi_init); +module_exit(corgi_exit); + +/* Module information */ +MODULE_AUTHOR("Richard Purdie"); +MODULE_DESCRIPTION("ALSA SoC Corgi"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/pxa/poodle.c b/sound/soc/pxa/poodle.c new file mode 100644 index 00000000000..0915cf74042 --- /dev/null +++ b/sound/soc/pxa/poodle.c @@ -0,0 +1,352 @@ +/* + * poodle.c -- SoC audio for Poodle + * + * Copyright 2005 Wolfson Microelectronics PLC. + * Copyright 2005 Openedhand Ltd. + * + * Authors: Liam Girdwood <liam.girdwood@wolfsonmicro.com> + * Richard Purdie <richard@openedhand.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + */ + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/timer.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> + +#include <asm/mach-types.h> +#include <asm/hardware/locomo.h> +#include <asm/arch/pxa-regs.h> +#include <asm/arch/hardware.h> +#include <asm/arch/poodle.h> +#include <asm/arch/audio.h> + +#include "../codecs/wm8731.h" +#include "pxa2xx-pcm.h" +#include "pxa2xx-i2s.h" + +#define POODLE_HP 1 +#define POODLE_HP_OFF 0 +#define POODLE_SPK_ON 1 +#define POODLE_SPK_OFF 0 + + /* audio clock in Hz - rounded from 12.235MHz */ +#define POODLE_AUDIO_CLOCK 12288000 + +static int poodle_jack_func; +static int poodle_spk_func; + +static void poodle_ext_control(struct snd_soc_codec *codec) +{ + int spk = 0; + + /* set up jack connection */ + if (poodle_jack_func == POODLE_HP) { + /* set = unmute headphone */ + locomo_gpio_write(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_MUTE_L, 1); + locomo_gpio_write(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_MUTE_R, 1); + snd_soc_dapm_set_endpoint(codec, "Headphone Jack", 1); + } else { + locomo_gpio_write(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_MUTE_L, 0); + locomo_gpio_write(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_MUTE_R, 0); + snd_soc_dapm_set_endpoint(codec, "Headphone Jack", 0); + } + + if (poodle_spk_func == POODLE_SPK_ON) + spk = 1; + + /* set the enpoints to their new connetion states */ + snd_soc_dapm_set_endpoint(codec, "Ext Spk", spk); + + /* signal a DAPM event */ + snd_soc_dapm_sync_endpoints(codec); +} + +static int poodle_startup(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec *codec = rtd->socdev->codec; + + /* check the jack status at stream startup */ + poodle_ext_control(codec); + return 0; +} + +/* we need to unmute the HP at shutdown as the mute burns power on poodle */ +static int poodle_shutdown(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec *codec = rtd->socdev->codec; + + /* set = unmute headphone */ + locomo_gpio_write(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_MUTE_L, 1); + locomo_gpio_write(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_MUTE_R, 1); + return 0; +} + +static int poodle_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec_dai *codec_dai = rtd->dai->codec_dai; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + unsigned int clk = 0; + int ret = 0; + + switch (params_rate(params)) { + case 8000: + case 16000: + case 48000: + case 96000: + clk = 12288000; + break; + case 11025: + case 22050: + case 44100: + clk = 11289600; + break; + } + + /* set codec DAI configuration */ + ret = codec_dai->dai_ops.set_fmt(codec_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS); + if (ret < 0) + return ret; + + /* set cpu DAI configuration */ + ret = cpu_dai->dai_ops.set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS); + if (ret < 0) + return ret; + + /* set the codec system clock for DAC and ADC */ + ret = codec_dai->dai_ops.set_sysclk(codec_dai, WM8731_SYSCLK, clk, + SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + /* set the I2S system clock as input (unused) */ + ret = cpu_dai->dai_ops.set_sysclk(cpu_dai, PXA2XX_I2S_SYSCLK, 0, + SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + return 0; +} + +static struct snd_soc_ops poodle_ops = { + .startup = poodle_startup, + .hw_params = poodle_hw_params, + .shutdown = poodle_shutdown, +}; + +static int poodle_get_jack(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + ucontrol->value.integer.value[0] = poodle_jack_func; + return 0; +} + +static int poodle_set_jack(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + + if (poodle_jack_func == ucontrol->value.integer.value[0]) + return 0; + + poodle_jack_func = ucontrol->value.integer.value[0]; + poodle_ext_control(codec); + return 1; +} + +static int poodle_get_spk(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + ucontrol->value.integer.value[0] = poodle_spk_func; + return 0; +} + +static int poodle_set_spk(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + + if (poodle_spk_func == ucontrol->value.integer.value[0]) + return 0; + + poodle_spk_func = ucontrol->value.integer.value[0]; + poodle_ext_control(codec); + return 1; +} + +static int poodle_amp_event(struct snd_soc_dapm_widget *w, int event) +{ + if (SND_SOC_DAPM_EVENT_ON(event)) + locomo_gpio_write(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_AMP_ON, 0); + else + locomo_gpio_write(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_AMP_ON, 1); + + return 0; +} + +/* poodle machine dapm widgets */ +static const struct snd_soc_dapm_widget wm8731_dapm_widgets[] = { +SND_SOC_DAPM_HP("Headphone Jack", NULL), +SND_SOC_DAPM_SPK("Ext Spk", poodle_amp_event), +}; + +/* Corgi machine audio_mapnections to the codec pins */ +static const char *audio_map[][3] = { + + /* headphone connected to LHPOUT1, RHPOUT1 */ + {"Headphone Jack", NULL, "LHPOUT"}, + {"Headphone Jack", NULL, "RHPOUT"}, + + /* speaker connected to LOUT, ROUT */ + {"Ext Spk", NULL, "ROUT"}, + {"Ext Spk", NULL, "LOUT"}, + + {NULL, NULL, NULL}, +}; + +static const char *jack_function[] = {"Off", "Headphone"}; +static const char *spk_function[] = {"Off", "On"}; +static const struct soc_enum poodle_enum[] = { + SOC_ENUM_SINGLE_EXT(2, jack_function), + SOC_ENUM_SINGLE_EXT(2, spk_function), +}; + +static const snd_kcontrol_new_t wm8731_poodle_controls[] = { + SOC_ENUM_EXT("Jack Function", poodle_enum[0], poodle_get_jack, + poodle_set_jack), + SOC_ENUM_EXT("Speaker Function", poodle_enum[1], poodle_get_spk, + poodle_set_spk), +}; + +/* + * Logic for a wm8731 as connected on a Sharp SL-C7x0 Device + */ +static int poodle_wm8731_init(struct snd_soc_codec *codec) +{ + int i, err; + + snd_soc_dapm_set_endpoint(codec, "LLINEIN", 0); + snd_soc_dapm_set_endpoint(codec, "RLINEIN", 0); + snd_soc_dapm_set_endpoint(codec, "MICIN", 1); + + /* Add poodle specific controls */ + for (i = 0; i < ARRAY_SIZE(wm8731_poodle_controls); i++) { + err = snd_ctl_add(codec->card, + snd_soc_cnew(&wm8731_poodle_controls[i],codec, NULL)); + if (err < 0) + return err; + } + + /* Add poodle specific widgets */ + for (i = 0; i < ARRAY_SIZE(wm8731_dapm_widgets); i++) { + snd_soc_dapm_new_control(codec, &wm8731_dapm_widgets[i]); + } + + /* Set up poodle specific audio path audio_map */ + for (i = 0; audio_map[i][0] != NULL; i++) { + snd_soc_dapm_connect_input(codec, audio_map[i][0], + audio_map[i][1], audio_map[i][2]); + } + + snd_soc_dapm_sync_endpoints(codec); + return 0; +} + +/* poodle digital audio interface glue - connects codec <--> CPU */ +static struct snd_soc_dai_link poodle_dai = { + .name = "WM8731", + .stream_name = "WM8731", + .cpu_dai = &pxa_i2s_dai, + .codec_dai = &wm8731_dai, + .init = poodle_wm8731_init, + .ops = &poodle_ops, +}; + +/* poodle audio machine driver */ +static struct snd_soc_machine snd_soc_machine_poodle = { + .name = "Poodle", + .dai_link = &poodle_dai, + .num_links = 1, +}; + +/* poodle audio private data */ +static struct wm8731_setup_data poodle_wm8731_setup = { + .i2c_address = 0x1b, +}; + +/* poodle audio subsystem */ +static struct snd_soc_device poodle_snd_devdata = { + .machine = &snd_soc_machine_poodle, + .platform = &pxa2xx_soc_platform, + .codec_dev = &soc_codec_dev_wm8731, + .codec_data = &poodle_wm8731_setup, +}; + +static struct platform_device *poodle_snd_device; + +static int __init poodle_init(void) +{ + int ret; + + if (!machine_is_poodle()) + return -ENODEV; + + locomo_gpio_set_dir(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_AMP_ON, 0); + /* should we mute HP at startup - burning power ?*/ + locomo_gpio_set_dir(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_MUTE_L, 0); + locomo_gpio_set_dir(&poodle_locomo_device.dev, + POODLE_LOCOMO_GPIO_MUTE_R, 0); + + poodle_snd_device = platform_device_alloc("soc-audio", -1); + if (!poodle_snd_device) + return -ENOMEM; + + platform_set_drvdata(poodle_snd_device, &poodle_snd_devdata); + poodle_snd_devdata.dev = &poodle_snd_device->dev; + ret = platform_device_add(poodle_snd_device); + + if (ret) + platform_device_put(poodle_snd_device); + + return ret; +} + +static void __exit poodle_exit(void) +{ + platform_device_unregister(poodle_snd_device); +} + +module_init(poodle_init); +module_exit(poodle_exit); + +/* Module information */ +MODULE_AUTHOR("Richard Purdie"); +MODULE_DESCRIPTION("ALSA SoC Poodle"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/pxa/pxa2xx-ac97.c b/sound/soc/pxa/pxa2xx-ac97.c new file mode 100644 index 00000000000..1bbbeff84ef --- /dev/null +++ b/sound/soc/pxa/pxa2xx-ac97.c @@ -0,0 +1,431 @@ +/* + * linux/sound/pxa2xx-ac97.c -- AC97 support for the Intel PXA2xx chip. + * + * Author: Nicolas Pitre + * Created: Dec 02, 2004 + * Copyright: MontaVista Software 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. + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/interrupt.h> +#include <linux/wait.h> +#include <linux/delay.h> + +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/ac97_codec.h> +#include <sound/initval.h> +#include <sound/soc.h> + +#include <asm/irq.h> +#include <linux/mutex.h> +#include <asm/hardware.h> +#include <asm/arch/pxa-regs.h> +#include <asm/arch/audio.h> + +#include "pxa2xx-pcm.h" +#include "pxa2xx-ac97.h" + +static DEFINE_MUTEX(car_mutex); +static DECLARE_WAIT_QUEUE_HEAD(gsr_wq); +static volatile long gsr_bits; + +/* + * Beware PXA27x bugs: + * + * o Slot 12 read from modem space will hang controller. + * o CDONE, SDONE interrupt fails after any slot 12 IO. + * + * We therefore have an hybrid approach for waiting on SDONE (interrupt or + * 1 jiffy timeout if interrupt never comes). + */ + +static unsigned short pxa2xx_ac97_read(struct snd_ac97 *ac97, + unsigned short reg) +{ + unsigned short val = -1; + volatile u32 *reg_addr; + + mutex_lock(&car_mutex); + + /* set up primary or secondary codec/modem space */ +#ifdef CONFIG_PXA27x + reg_addr = ac97->num ? &SAC_REG_BASE : &PAC_REG_BASE; +#else + if (reg == AC97_GPIO_STATUS) + reg_addr = ac97->num ? &SMC_REG_BASE : &PMC_REG_BASE; + else + reg_addr = ac97->num ? &SAC_REG_BASE : &PAC_REG_BASE; +#endif + reg_addr += (reg >> 1); + +#ifndef CONFIG_PXA27x + if (reg == AC97_GPIO_STATUS) { + /* read from controller cache */ + val = *reg_addr; + goto out; + } +#endif + + /* start read access across the ac97 link */ + GSR = GSR_CDONE | GSR_SDONE; + gsr_bits = 0; + val = *reg_addr; + + wait_event_timeout(gsr_wq, (GSR | gsr_bits) & GSR_SDONE, 1); + if (!((GSR | gsr_bits) & GSR_SDONE)) { + printk(KERN_ERR "%s: read error (ac97_reg=%x GSR=%#lx)\n", + __FUNCTION__, reg, GSR | gsr_bits); + val = -1; + goto out; + } + + /* valid data now */ + GSR = GSR_CDONE | GSR_SDONE; + gsr_bits = 0; + val = *reg_addr; + /* but we've just started another cycle... */ + wait_event_timeout(gsr_wq, (GSR | gsr_bits) & GSR_SDONE, 1); + +out: mutex_unlock(&car_mutex); + return val; +} + +static void pxa2xx_ac97_write(struct snd_ac97 *ac97, unsigned short reg, + unsigned short val) +{ + volatile u32 *reg_addr; + + mutex_lock(&car_mutex); + + /* set up primary or secondary codec/modem space */ +#ifdef CONFIG_PXA27x + reg_addr = ac97->num ? &SAC_REG_BASE : &PAC_REG_BASE; +#else + if (reg == AC97_GPIO_STATUS) + reg_addr = ac97->num ? &SMC_REG_BASE : &PMC_REG_BASE; + else + reg_addr = ac97->num ? &SAC_REG_BASE : &PAC_REG_BASE; +#endif + reg_addr += (reg >> 1); + + GSR = GSR_CDONE | GSR_SDONE; + gsr_bits = 0; + *reg_addr = val; + wait_event_timeout(gsr_wq, (GSR | gsr_bits) & GSR_CDONE, 1); + if (!((GSR | gsr_bits) & GSR_CDONE)) + printk(KERN_ERR "%s: write error (ac97_reg=%x GSR=%#lx)\n", + __FUNCTION__, reg, GSR | gsr_bits); + + mutex_unlock(&car_mutex); +} + +static void pxa2xx_ac97_warm_reset(struct snd_ac97 *ac97) +{ + gsr_bits = 0; + +#ifdef CONFIG_PXA27x + /* warm reset broken on Bulverde, + so manually keep AC97 reset high */ + pxa_gpio_mode(113 | GPIO_OUT | GPIO_DFLT_HIGH); + udelay(10); + GCR |= GCR_WARM_RST; + pxa_gpio_mode(113 | GPIO_ALT_FN_2_OUT); + udelay(500); +#else + GCR |= GCR_WARM_RST | GCR_PRIRDY_IEN | GCR_SECRDY_IEN; + wait_event_timeout(gsr_wq, gsr_bits & (GSR_PCR | GSR_SCR), 1); +#endif + + if (!((GSR | gsr_bits) & (GSR_PCR | GSR_SCR))) + printk(KERN_INFO "%s: warm reset timeout (GSR=%#lx)\n", + __FUNCTION__, gsr_bits); + + GCR &= ~(GCR_PRIRDY_IEN|GCR_SECRDY_IEN); + GCR |= GCR_SDONE_IE|GCR_CDONE_IE; +} + +static void pxa2xx_ac97_cold_reset(struct snd_ac97 *ac97) +{ + GCR &= GCR_COLD_RST; /* clear everything but nCRST */ + GCR &= ~GCR_COLD_RST; /* then assert nCRST */ + + gsr_bits = 0; +#ifdef CONFIG_PXA27x + /* PXA27x Developers Manual section 13.5.2.2.1 */ + pxa_set_cken(1 << 31, 1); + udelay(5); + pxa_set_cken(1 << 31, 0); + GCR = GCR_COLD_RST; + udelay(50); +#else + GCR = GCR_COLD_RST; + GCR |= GCR_CDONE_IE|GCR_SDONE_IE; + wait_event_timeout(gsr_wq, gsr_bits & (GSR_PCR | GSR_SCR), 1); +#endif + + if (!((GSR | gsr_bits) & (GSR_PCR | GSR_SCR))) + printk(KERN_INFO "%s: cold reset timeout (GSR=%#lx)\n", + __FUNCTION__, gsr_bits); + + GCR &= ~(GCR_PRIRDY_IEN|GCR_SECRDY_IEN); + GCR |= GCR_SDONE_IE|GCR_CDONE_IE; +} + +static irqreturn_t pxa2xx_ac97_irq(int irq, void *dev_id) +{ + long status; + + status = GSR; + if (status) { + GSR = status; + gsr_bits |= status; + wake_up(&gsr_wq); + +#ifdef CONFIG_PXA27x + /* Although we don't use those we still need to clear them + since they tend to spuriously trigger when MMC is used + (hardware bug? go figure)... */ + MISR = MISR_EOC; + PISR = PISR_EOC; + MCSR = MCSR_EOC; +#endif + + return IRQ_HANDLED; + } + + return IRQ_NONE; +} + +struct snd_ac97_bus_ops soc_ac97_ops = { + .read = pxa2xx_ac97_read, + .write = pxa2xx_ac97_write, + .warm_reset = pxa2xx_ac97_warm_reset, + .reset = pxa2xx_ac97_cold_reset, +}; + +static struct pxa2xx_pcm_dma_params pxa2xx_ac97_pcm_stereo_out = { + .name = "AC97 PCM Stereo out", + .dev_addr = __PREG(PCDR), + .drcmr = &DRCMRTXPCDR, + .dcmd = DCMD_INCSRCADDR | DCMD_FLOWTRG | + DCMD_BURST32 | DCMD_WIDTH4, +}; + +static struct pxa2xx_pcm_dma_params pxa2xx_ac97_pcm_stereo_in = { + .name = "AC97 PCM Stereo in", + .dev_addr = __PREG(PCDR), + .drcmr = &DRCMRRXPCDR, + .dcmd = DCMD_INCTRGADDR | DCMD_FLOWSRC | + DCMD_BURST32 | DCMD_WIDTH4, +}; + +static struct pxa2xx_pcm_dma_params pxa2xx_ac97_pcm_aux_mono_out = { + .name = "AC97 Aux PCM (Slot 5) Mono out", + .dev_addr = __PREG(MODR), + .drcmr = &DRCMRTXMODR, + .dcmd = DCMD_INCSRCADDR | DCMD_FLOWTRG | + DCMD_BURST16 | DCMD_WIDTH2, +}; + +static struct pxa2xx_pcm_dma_params pxa2xx_ac97_pcm_aux_mono_in = { + .name = "AC97 Aux PCM (Slot 5) Mono in", + .dev_addr = __PREG(MODR), + .drcmr = &DRCMRRXMODR, + .dcmd = DCMD_INCTRGADDR | DCMD_FLOWSRC | + DCMD_BURST16 | DCMD_WIDTH2, +}; + +static struct pxa2xx_pcm_dma_params pxa2xx_ac97_pcm_mic_mono_in = { + .name = "AC97 Mic PCM (Slot 6) Mono in", + .dev_addr = __PREG(MCDR), + .drcmr = &DRCMRRXMCDR, + .dcmd = DCMD_INCTRGADDR | DCMD_FLOWSRC | + DCMD_BURST16 | DCMD_WIDTH2, +}; + +#ifdef CONFIG_PM +static int pxa2xx_ac97_suspend(struct platform_device *pdev, + struct snd_soc_cpu_dai *dai) +{ + GCR |= GCR_ACLINK_OFF; + pxa_set_cken(CKEN2_AC97, 0); + return 0; +} + +static int pxa2xx_ac97_resume(struct platform_device *pdev, + struct snd_soc_cpu_dai *dai) +{ + pxa_gpio_mode(GPIO31_SYNC_AC97_MD); + pxa_gpio_mode(GPIO30_SDATA_OUT_AC97_MD); + pxa_gpio_mode(GPIO28_BITCLK_AC97_MD); + pxa_gpio_mode(GPIO29_SDATA_IN_AC97_MD); +#ifdef CONFIG_PXA27x + /* Use GPIO 113 as AC97 Reset on Bulverde */ + pxa_gpio_mode(113 | GPIO_ALT_FN_2_OUT); +#endif + pxa_set_cken(CKEN2_AC97, 1); + return 0; +} + +#else +#define pxa2xx_ac97_suspend NULL +#define pxa2xx_ac97_resume NULL +#endif + +static int pxa2xx_ac97_probe(struct platform_device *pdev) +{ + int ret; + + ret = request_irq(IRQ_AC97, pxa2xx_ac97_irq, IRQF_DISABLED, "AC97", NULL); + if (ret < 0) + goto err; + + pxa_gpio_mode(GPIO31_SYNC_AC97_MD); + pxa_gpio_mode(GPIO30_SDATA_OUT_AC97_MD); + pxa_gpio_mode(GPIO28_BITCLK_AC97_MD); + pxa_gpio_mode(GPIO29_SDATA_IN_AC97_MD); +#ifdef CONFIG_PXA27x + /* Use GPIO 113 as AC97 Reset on Bulverde */ + pxa_gpio_mode(113 | GPIO_ALT_FN_2_OUT); +#endif + pxa_set_cken(CKEN2_AC97, 1); + return 0; + + err: + if (CKEN & CKEN2_AC97) { + GCR |= GCR_ACLINK_OFF; + free_irq(IRQ_AC97, NULL); + pxa_set_cken(CKEN2_AC97, 0); + } + return ret; +} + +static void pxa2xx_ac97_remove(struct platform_device *pdev) +{ + GCR |= GCR_ACLINK_OFF; + free_irq(IRQ_AC97, NULL); + pxa_set_cken(CKEN2_AC97, 0); +} + +static int pxa2xx_ac97_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + cpu_dai->dma_data = &pxa2xx_ac97_pcm_stereo_out; + else + cpu_dai->dma_data = &pxa2xx_ac97_pcm_stereo_in; + + return 0; +} + +static int pxa2xx_ac97_hw_aux_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + cpu_dai->dma_data = &pxa2xx_ac97_pcm_aux_mono_out; + else + cpu_dai->dma_data = &pxa2xx_ac97_pcm_aux_mono_in; + + return 0; +} + +static int pxa2xx_ac97_hw_mic_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + return -ENODEV; + else + cpu_dai->dma_data = &pxa2xx_ac97_pcm_mic_mono_in; + + return 0; +} + +#define PXA2XX_AC97_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 |\ + SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 | SNDRV_PCM_RATE_44100 | \ + SNDRV_PCM_RATE_48000) + +/* + * There is only 1 physical AC97 interface for pxa2xx, but it + * has extra fifo's that can be used for aux DACs and ADCs. + */ +struct snd_soc_cpu_dai pxa_ac97_dai[] = { +{ + .name = "pxa2xx-ac97", + .id = 0, + .type = SND_SOC_DAI_AC97, + .probe = pxa2xx_ac97_probe, + .remove = pxa2xx_ac97_remove, + .suspend = pxa2xx_ac97_suspend, + .resume = pxa2xx_ac97_resume, + .playback = { + .stream_name = "AC97 Playback", + .channels_min = 2, + .channels_max = 2, + .rates = PXA2XX_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .capture = { + .stream_name = "AC97 Capture", + .channels_min = 2, + .channels_max = 2, + .rates = PXA2XX_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .hw_params = pxa2xx_ac97_hw_params,}, +}, +{ + .name = "pxa2xx-ac97-aux", + .id = 1, + .type = SND_SOC_DAI_AC97, + .playback = { + .stream_name = "AC97 Aux Playback", + .channels_min = 1, + .channels_max = 1, + .rates = PXA2XX_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .capture = { + .stream_name = "AC97 Aux Capture", + .channels_min = 1, + .channels_max = 1, + .rates = PXA2XX_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .hw_params = pxa2xx_ac97_hw_aux_params,}, +}, +{ + .name = "pxa2xx-ac97-mic", + .id = 2, + .type = SND_SOC_DAI_AC97, + .capture = { + .stream_name = "AC97 Mic Capture", + .channels_min = 1, + .channels_max = 1, + .rates = PXA2XX_AC97_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .hw_params = pxa2xx_ac97_hw_mic_params,}, +}, +}; + +EXPORT_SYMBOL_GPL(pxa_ac97_dai); +EXPORT_SYMBOL_GPL(soc_ac97_ops); + +MODULE_AUTHOR("Nicolas Pitre"); +MODULE_DESCRIPTION("AC97 driver for the Intel PXA2xx chip"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/pxa/pxa2xx-ac97.h b/sound/soc/pxa/pxa2xx-ac97.h new file mode 100644 index 00000000000..4c4b882316a --- /dev/null +++ b/sound/soc/pxa/pxa2xx-ac97.h @@ -0,0 +1,22 @@ +/* + * linux/sound/arm/pxa2xx-ac97.h + * + * 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. + */ + +#ifndef _PXA2XX_AC97_H +#define _PXA2XX_AC97_H + +/* pxa2xx DAI ID's */ +#define PXA2XX_DAI_AC97_HIFI 0 +#define PXA2XX_DAI_AC97_AUX 1 +#define PXA2XX_DAI_AC97_MIC 2 + +extern struct snd_soc_cpu_dai pxa_ac97_dai[3]; + +/* platform data */ +extern struct snd_ac97_bus_ops pxa2xx_ac97_ops; + +#endif diff --git a/sound/soc/pxa/pxa2xx-i2s.c b/sound/soc/pxa/pxa2xx-i2s.c new file mode 100644 index 00000000000..575a6137c04 --- /dev/null +++ b/sound/soc/pxa/pxa2xx-i2s.c @@ -0,0 +1,318 @@ +/* + * pxa2xx-i2s.c -- ALSA Soc Audio Layer + * + * Copyright 2005 Wolfson Microelectronics PLC. + * Author: Liam Girdwood + * liam.girdwood@wolfsonmicro.com or linux@wolfsonmicro.com + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Revision history + * 12th Aug 2005 Initial version. + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/delay.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/initval.h> +#include <sound/soc.h> + +#include <asm/hardware.h> +#include <asm/arch/pxa-regs.h> +#include <asm/arch/audio.h> + +#include "pxa2xx-pcm.h" +#include "pxa2xx-i2s.h" + +struct pxa_i2s_port { + u32 sadiv; + u32 sacr0; + u32 sacr1; + u32 saimr; + int master; + u32 fmt; +}; +static struct pxa_i2s_port pxa_i2s; + +static struct pxa2xx_pcm_dma_params pxa2xx_i2s_pcm_stereo_out = { + .name = "I2S PCM Stereo out", + .dev_addr = __PREG(SADR), + .drcmr = &DRCMRTXSADR, + .dcmd = DCMD_INCSRCADDR | DCMD_FLOWTRG | + DCMD_BURST32 | DCMD_WIDTH4, +}; + +static struct pxa2xx_pcm_dma_params pxa2xx_i2s_pcm_stereo_in = { + .name = "I2S PCM Stereo in", + .dev_addr = __PREG(SADR), + .drcmr = &DRCMRRXSADR, + .dcmd = DCMD_INCTRGADDR | DCMD_FLOWSRC | + DCMD_BURST32 | DCMD_WIDTH4, +}; + +static struct pxa2xx_gpio gpio_bus[] = { + { /* I2S SoC Slave */ + .rx = GPIO29_SDATA_IN_I2S_MD, + .tx = GPIO30_SDATA_OUT_I2S_MD, + .clk = GPIO28_BITCLK_IN_I2S_MD, + .frm = GPIO31_SYNC_I2S_MD, + }, + { /* I2S SoC Master */ +#ifdef CONFIG_PXA27x + .sys = GPIO113_I2S_SYSCLK_MD, +#else + .sys = GPIO32_SYSCLK_I2S_MD, +#endif + .rx = GPIO29_SDATA_IN_I2S_MD, + .tx = GPIO30_SDATA_OUT_I2S_MD, + .clk = GPIO28_BITCLK_OUT_I2S_MD, + .frm = GPIO31_SYNC_I2S_MD, + }, +}; + +static int pxa2xx_i2s_startup(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + + if (!cpu_dai->active) { + SACR0 |= SACR0_RST; + SACR0 = 0; + } + + return 0; +} + +/* wait for I2S controller to be ready */ +static int pxa_i2s_wait(void) +{ + int i; + + /* flush the Rx FIFO */ + for(i = 0; i < 16; i++) + SADR; + return 0; +} + +static int pxa2xx_i2s_set_dai_fmt(struct snd_soc_cpu_dai *cpu_dai, + unsigned int fmt) +{ + /* interface format */ + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + pxa_i2s.fmt = 0; + break; + case SND_SOC_DAIFMT_LEFT_J: + pxa_i2s.fmt = SACR1_AMSL; + break; + } + + switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBS_CFS: + pxa_i2s.master = 1; + break; + case SND_SOC_DAIFMT_CBM_CFS: + pxa_i2s.master = 0; + break; + default: + break; + } + return 0; +} + +static int pxa2xx_i2s_set_dai_sysclk(struct snd_soc_cpu_dai *cpu_dai, + int clk_id, unsigned int freq, int dir) +{ + if (clk_id != PXA2XX_I2S_SYSCLK) + return -ENODEV; + + if (pxa_i2s.master && dir == SND_SOC_CLOCK_OUT) + pxa_gpio_mode(gpio_bus[pxa_i2s.master].sys); + + return 0; +} + +static int pxa2xx_i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + + pxa_gpio_mode(gpio_bus[pxa_i2s.master].rx); + pxa_gpio_mode(gpio_bus[pxa_i2s.master].tx); + pxa_gpio_mode(gpio_bus[pxa_i2s.master].frm); + pxa_gpio_mode(gpio_bus[pxa_i2s.master].clk); + pxa_set_cken(CKEN8_I2S, 1); + pxa_i2s_wait(); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + cpu_dai->dma_data = &pxa2xx_i2s_pcm_stereo_out; + else + cpu_dai->dma_data = &pxa2xx_i2s_pcm_stereo_in; + + /* is port used by another stream */ + if (!(SACR0 & SACR0_ENB)) { + + SACR0 = 0; + SACR1 = 0; + if (pxa_i2s.master) + SACR0 |= SACR0_BCKD; + + SACR0 |= SACR0_RFTH(14) | SACR0_TFTH(1); + SACR1 |= pxa_i2s.fmt; + } + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + SAIMR |= SAIMR_TFS; + else + SAIMR |= SAIMR_RFS; + + switch (params_rate(params)) { + case 8000: + SADIV = 0x48; + break; + case 11025: + SADIV = 0x34; + break; + case 16000: + SADIV = 0x24; + break; + case 22050: + SADIV = 0x1a; + break; + case 44100: + SADIV = 0xd; + break; + case 48000: + SADIV = 0xc; + break; + case 96000: /* not in manual and possibly slightly inaccurate */ + SADIV = 0x6; + break; + } + + return 0; +} + +static int pxa2xx_i2s_trigger(struct snd_pcm_substream *substream, int cmd) +{ + int ret = 0; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + SACR0 |= SACR0_ENB; + break; + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + break; + default: + ret = -EINVAL; + } + + return ret; +} + +static void pxa2xx_i2s_shutdown(struct snd_pcm_substream *substream) +{ + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + SACR1 |= SACR1_DRPL; + SAIMR &= ~SAIMR_TFS; + } else { + SACR1 |= SACR1_DREC; + SAIMR &= ~SAIMR_RFS; + } + + if (SACR1 & (SACR1_DREC | SACR1_DRPL)) { + SACR0 &= ~SACR0_ENB; + pxa_i2s_wait(); + pxa_set_cken(CKEN8_I2S, 0); + } +} + +#ifdef CONFIG_PM +static int pxa2xx_i2s_suspend(struct platform_device *dev, + struct snd_soc_cpu_dai *dai) +{ + if (!dai->active) + return 0; + + /* store registers */ + pxa_i2s.sacr0 = SACR0; + pxa_i2s.sacr1 = SACR1; + pxa_i2s.saimr = SAIMR; + pxa_i2s.sadiv = SADIV; + + /* deactivate link */ + SACR0 &= ~SACR0_ENB; + pxa_i2s_wait(); + return 0; +} + +static int pxa2xx_i2s_resume(struct platform_device *pdev, + struct snd_soc_cpu_dai *dai) +{ + if (!dai->active) + return 0; + + pxa_i2s_wait(); + + SACR0 = pxa_i2s.sacr0 &= ~SACR0_ENB; + SACR1 = pxa_i2s.sacr1; + SAIMR = pxa_i2s.saimr; + SADIV = pxa_i2s.sadiv; + SACR0 |= SACR0_ENB; + + return 0; +} + +#else +#define pxa2xx_i2s_suspend NULL +#define pxa2xx_i2s_resume NULL +#endif + +#define PXA2XX_I2S_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 |\ + SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 | SNDRV_PCM_RATE_44100 | \ + SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_96000) + +struct snd_soc_cpu_dai pxa_i2s_dai = { + .name = "pxa2xx-i2s", + .id = 0, + .type = SND_SOC_DAI_I2S, + .suspend = pxa2xx_i2s_suspend, + .resume = pxa2xx_i2s_resume, + .playback = { + .channels_min = 2, + .channels_max = 2, + .rates = PXA2XX_I2S_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .capture = { + .channels_min = 2, + .channels_max = 2, + .rates = PXA2XX_I2S_RATES, + .formats = SNDRV_PCM_FMTBIT_S16_LE,}, + .ops = { + .startup = pxa2xx_i2s_startup, + .shutdown = pxa2xx_i2s_shutdown, + .trigger = pxa2xx_i2s_trigger, + .hw_params = pxa2xx_i2s_hw_params,}, + .dai_ops = { + .set_fmt = pxa2xx_i2s_set_dai_fmt, + .set_sysclk = pxa2xx_i2s_set_dai_sysclk, + }, +}; + +EXPORT_SYMBOL_GPL(pxa_i2s_dai); + +/* Module information */ +MODULE_AUTHOR("Liam Girdwood, liam.girdwood@wolfsonmicro.com, www.wolfsonmicro.com"); +MODULE_DESCRIPTION("pxa2xx I2S SoC Interface"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/pxa/pxa2xx-i2s.h b/sound/soc/pxa/pxa2xx-i2s.h new file mode 100644 index 00000000000..a2484f0881f --- /dev/null +++ b/sound/soc/pxa/pxa2xx-i2s.h @@ -0,0 +1,20 @@ +/* + * linux/sound/arm/pxa2xx-i2s.h + * + * 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. + */ + +#ifndef _PXA2XX_I2S_H +#define _PXA2XX_I2S_H + +/* pxa2xx DAI ID's */ +#define PXA2XX_DAI_I2S 0 + +/* I2S clock */ +#define PXA2XX_I2S_SYSCLK 0 + +extern struct snd_soc_cpu_dai pxa_i2s_dai; + +#endif diff --git a/sound/soc/pxa/pxa2xx-pcm.c b/sound/soc/pxa/pxa2xx-pcm.c new file mode 100644 index 00000000000..35e8fa3a469 --- /dev/null +++ b/sound/soc/pxa/pxa2xx-pcm.c @@ -0,0 +1,372 @@ +/* + * linux/sound/arm/pxa2xx-pcm.c -- ALSA PCM interface for the Intel PXA2xx chip + * + * Author: Nicolas Pitre + * Created: Nov 30, 2004 + * Copyright: (C) 2004 MontaVista Software, 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. + */ + +#include <linux/module.h> +#include <linux/init.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/dma-mapping.h> + +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> + +#include <asm/dma.h> +#include <asm/hardware.h> +#include <asm/arch/pxa-regs.h> +#include <asm/arch/audio.h> + +#include "pxa2xx-pcm.h" + +static const struct snd_pcm_hardware pxa2xx_pcm_hardware = { + .info = SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_PAUSE | + SNDRV_PCM_INFO_RESUME, + .formats = SNDRV_PCM_FMTBIT_S16_LE | + SNDRV_PCM_FMTBIT_S24_LE | + SNDRV_PCM_FMTBIT_S32_LE, + .period_bytes_min = 32, + .period_bytes_max = 8192 - 32, + .periods_min = 1, + .periods_max = PAGE_SIZE/sizeof(pxa_dma_desc), + .buffer_bytes_max = 128 * 1024, + .fifo_size = 32, +}; + +struct pxa2xx_runtime_data { + int dma_ch; + struct pxa2xx_pcm_dma_params *params; + pxa_dma_desc *dma_desc_array; + dma_addr_t dma_desc_array_phys; +}; + +static void pxa2xx_pcm_dma_irq(int dma_ch, void *dev_id) +{ + struct snd_pcm_substream *substream = dev_id; + struct pxa2xx_runtime_data *prtd = substream->runtime->private_data; + int dcsr; + + dcsr = DCSR(dma_ch); + DCSR(dma_ch) = dcsr & ~DCSR_STOPIRQEN; + + if (dcsr & DCSR_ENDINTR) { + snd_pcm_period_elapsed(substream); + } else { + printk( KERN_ERR "%s: DMA error on channel %d (DCSR=%#x)\n", + prtd->params->name, dma_ch, dcsr ); + } +} + +static int pxa2xx_pcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct pxa2xx_runtime_data *prtd = runtime->private_data; + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct pxa2xx_pcm_dma_params *dma = rtd->dai->cpu_dai->dma_data; + size_t totsize = params_buffer_bytes(params); + size_t period = params_period_bytes(params); + pxa_dma_desc *dma_desc; + dma_addr_t dma_buff_phys, next_desc_phys; + int ret; + + /* return if this is a bufferless transfer e.g. + * codec <--> BT codec or GSM modem -- lg FIXME */ + if (!dma) + return 0; + + /* this may get called several times by oss emulation + * with different params */ + if (prtd->params == NULL) { + prtd->params = dma; + ret = pxa_request_dma(prtd->params->name, DMA_PRIO_LOW, + pxa2xx_pcm_dma_irq, substream); + if (ret < 0) + return ret; + prtd->dma_ch = ret; + } else if (prtd->params != dma) { + pxa_free_dma(prtd->dma_ch); + prtd->params = dma; + ret = pxa_request_dma(prtd->params->name, DMA_PRIO_LOW, + pxa2xx_pcm_dma_irq, substream); + if (ret < 0) + return ret; + prtd->dma_ch = ret; + } + + snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); + runtime->dma_bytes = totsize; + + dma_desc = prtd->dma_desc_array; + next_desc_phys = prtd->dma_desc_array_phys; + dma_buff_phys = runtime->dma_addr; + do { + next_desc_phys += sizeof(pxa_dma_desc); + dma_desc->ddadr = next_desc_phys; + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + dma_desc->dsadr = dma_buff_phys; + dma_desc->dtadr = prtd->params->dev_addr; + } else { + dma_desc->dsadr = prtd->params->dev_addr; + dma_desc->dtadr = dma_buff_phys; + } + if (period > totsize) + period = totsize; + dma_desc->dcmd = prtd->params->dcmd | period | DCMD_ENDIRQEN; + dma_desc++; + dma_buff_phys += period; + } while (totsize -= period); + dma_desc[-1].ddadr = prtd->dma_desc_array_phys; + + return 0; +} + +static int pxa2xx_pcm_hw_free(struct snd_pcm_substream *substream) +{ + struct pxa2xx_runtime_data *prtd = substream->runtime->private_data; + + if (prtd && prtd->params) + *prtd->params->drcmr = 0; + + if (prtd->dma_ch) { + snd_pcm_set_runtime_buffer(substream, NULL); + pxa_free_dma(prtd->dma_ch); + prtd->dma_ch = 0; + } + + return 0; +} + +static int pxa2xx_pcm_prepare(struct snd_pcm_substream *substream) +{ + struct pxa2xx_runtime_data *prtd = substream->runtime->private_data; + + DCSR(prtd->dma_ch) &= ~DCSR_RUN; + DCSR(prtd->dma_ch) = 0; + DCMD(prtd->dma_ch) = 0; + *prtd->params->drcmr = prtd->dma_ch | DRCMR_MAPVLD; + + return 0; +} + +static int pxa2xx_pcm_trigger(struct snd_pcm_substream *substream, int cmd) +{ + struct pxa2xx_runtime_data *prtd = substream->runtime->private_data; + int ret = 0; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + DDADR(prtd->dma_ch) = prtd->dma_desc_array_phys; + DCSR(prtd->dma_ch) = DCSR_RUN; + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + DCSR(prtd->dma_ch) &= ~DCSR_RUN; + break; + + case SNDRV_PCM_TRIGGER_RESUME: + DCSR(prtd->dma_ch) |= DCSR_RUN; + break; + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + DDADR(prtd->dma_ch) = prtd->dma_desc_array_phys; + DCSR(prtd->dma_ch) |= DCSR_RUN; + break; + + default: + ret = -EINVAL; + } + + return ret; +} + +static snd_pcm_uframes_t +pxa2xx_pcm_pointer(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct pxa2xx_runtime_data *prtd = runtime->private_data; + + dma_addr_t ptr = (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) ? + DSADR(prtd->dma_ch) : DTADR(prtd->dma_ch); + snd_pcm_uframes_t x = bytes_to_frames(runtime, ptr - runtime->dma_addr); + + if (x == runtime->buffer_size) + x = 0; + return x; +} + +static int pxa2xx_pcm_open(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct pxa2xx_runtime_data *prtd; + int ret; + + snd_soc_set_runtime_hwparams(substream, &pxa2xx_pcm_hardware); + + /* + * For mysterious reasons (and despite what the manual says) + * playback samples are lost if the DMA count is not a multiple + * of the DMA burst size. Let's add a rule to enforce that. + */ + ret = snd_pcm_hw_constraint_step(runtime, 0, + SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 32); + if (ret) + goto out; + + ret = snd_pcm_hw_constraint_step(runtime, 0, + SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 32); + if (ret) + goto out; + + ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS); + if (ret < 0) + goto out; + + prtd = kzalloc(sizeof(struct pxa2xx_runtime_data), GFP_KERNEL); + if (prtd == NULL) { + ret = -ENOMEM; + goto out; + } + + prtd->dma_desc_array = + dma_alloc_writecombine(substream->pcm->card->dev, PAGE_SIZE, + &prtd->dma_desc_array_phys, GFP_KERNEL); + if (!prtd->dma_desc_array) { + ret = -ENOMEM; + goto err1; + } + + runtime->private_data = prtd; + return 0; + + err1: + kfree(prtd); + out: + return ret; +} + +static int pxa2xx_pcm_close(struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct pxa2xx_runtime_data *prtd = runtime->private_data; + + dma_free_writecombine(substream->pcm->card->dev, PAGE_SIZE, + prtd->dma_desc_array, prtd->dma_desc_array_phys); + kfree(prtd); + return 0; +} + +static int pxa2xx_pcm_mmap(struct snd_pcm_substream *substream, + struct vm_area_struct *vma) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + return dma_mmap_writecombine(substream->pcm->card->dev, vma, + runtime->dma_area, + runtime->dma_addr, + runtime->dma_bytes); +} + +struct snd_pcm_ops pxa2xx_pcm_ops = { + .open = pxa2xx_pcm_open, + .close = pxa2xx_pcm_close, + .ioctl = snd_pcm_lib_ioctl, + .hw_params = pxa2xx_pcm_hw_params, + .hw_free = pxa2xx_pcm_hw_free, + .prepare = pxa2xx_pcm_prepare, + .trigger = pxa2xx_pcm_trigger, + .pointer = pxa2xx_pcm_pointer, + .mmap = pxa2xx_pcm_mmap, +}; + +static int pxa2xx_pcm_preallocate_dma_buffer(struct snd_pcm *pcm, int stream) +{ + struct snd_pcm_substream *substream = pcm->streams[stream].substream; + struct snd_dma_buffer *buf = &substream->dma_buffer; + size_t size = pxa2xx_pcm_hardware.buffer_bytes_max; + buf->dev.type = SNDRV_DMA_TYPE_DEV; + buf->dev.dev = pcm->card->dev; + buf->private_data = NULL; + buf->area = dma_alloc_writecombine(pcm->card->dev, size, + &buf->addr, GFP_KERNEL); + if (!buf->area) + return -ENOMEM; + buf->bytes = size; + return 0; +} + +static void pxa2xx_pcm_free_dma_buffers(struct snd_pcm *pcm) +{ + struct snd_pcm_substream *substream; + struct snd_dma_buffer *buf; + int stream; + + for (stream = 0; stream < 2; stream++) { + substream = pcm->streams[stream].substream; + if (!substream) + continue; + + buf = &substream->dma_buffer; + if (!buf->area) + continue; + + dma_free_writecombine(pcm->card->dev, buf->bytes, + buf->area, buf->addr); + buf->area = NULL; + } +} + +static u64 pxa2xx_pcm_dmamask = DMA_32BIT_MASK; + +int pxa2xx_pcm_new(struct snd_card *card, struct snd_soc_codec_dai *dai, + struct snd_pcm *pcm) +{ + int ret = 0; + + if (!card->dev->dma_mask) + card->dev->dma_mask = &pxa2xx_pcm_dmamask; + if (!card->dev->coherent_dma_mask) + card->dev->coherent_dma_mask = DMA_32BIT_MASK; + + if (dai->playback.channels_min) { + ret = pxa2xx_pcm_preallocate_dma_buffer(pcm, + SNDRV_PCM_STREAM_PLAYBACK); + if (ret) + goto out; + } + + if (dai->capture.channels_min) { + ret = pxa2xx_pcm_preallocate_dma_buffer(pcm, + SNDRV_PCM_STREAM_CAPTURE); + if (ret) + goto out; + } + out: + return ret; +} + +struct snd_soc_platform pxa2xx_soc_platform = { + .name = "pxa2xx-audio", + .pcm_ops = &pxa2xx_pcm_ops, + .pcm_new = pxa2xx_pcm_new, + .pcm_free = pxa2xx_pcm_free_dma_buffers, +}; + +EXPORT_SYMBOL_GPL(pxa2xx_soc_platform); + +MODULE_AUTHOR("Nicolas Pitre"); +MODULE_DESCRIPTION("Intel PXA2xx PCM DMA module"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/pxa/pxa2xx-pcm.h b/sound/soc/pxa/pxa2xx-pcm.h new file mode 100644 index 00000000000..54c9c755e50 --- /dev/null +++ b/sound/soc/pxa/pxa2xx-pcm.h @@ -0,0 +1,34 @@ +/* + * linux/sound/arm/pxa2xx-pcm.h -- ALSA PCM interface for the Intel PXA2xx chip + * + * Author: Nicolas Pitre + * Created: Nov 30, 2004 + * Copyright: MontaVista Software, 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. + */ + +#ifndef _PXA2XX_PCM_H +#define _PXA2XX_PCM_H + +struct pxa2xx_pcm_dma_params { + char *name; /* stream identifier */ + u32 dcmd; /* DMA descriptor dcmd field */ + volatile u32 *drcmr; /* the DMA request channel to use */ + u32 dev_addr; /* device physical address for DMA */ +}; + +struct pxa2xx_gpio { + u32 sys; + u32 rx; + u32 tx; + u32 clk; + u32 frm; +}; + +/* platform data */ +extern struct snd_soc_platform pxa2xx_soc_platform; + +#endif diff --git a/sound/soc/pxa/spitz.c b/sound/soc/pxa/spitz.c new file mode 100644 index 00000000000..80e82109fef --- /dev/null +++ b/sound/soc/pxa/spitz.c @@ -0,0 +1,394 @@ +/* + * spitz.c -- SoC audio for Sharp SL-Cxx00 models Spitz, Borzoi and Akita + * + * Copyright 2005 Wolfson Microelectronics PLC. + * Copyright 2005 Openedhand Ltd. + * + * Authors: Liam Girdwood <liam.girdwood@wolfsonmicro.com> + * Richard Purdie <richard@openedhand.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Revision history + * 30th Nov 2005 Initial version. + * + */ + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/timer.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> + +#include <asm/mach-types.h> +#include <asm/hardware/scoop.h> +#include <asm/arch/pxa-regs.h> +#include <asm/arch/hardware.h> +#include <asm/arch/akita.h> +#include <asm/arch/spitz.h> +#include <asm/mach-types.h> +#include "../codecs/wm8750.h" +#include "pxa2xx-pcm.h" +#include "pxa2xx-i2s.h" + +#define SPITZ_HP 0 +#define SPITZ_MIC 1 +#define SPITZ_LINE 2 +#define SPITZ_HEADSET 3 +#define SPITZ_HP_OFF 4 +#define SPITZ_SPK_ON 0 +#define SPITZ_SPK_OFF 1 + + /* audio clock in Hz - rounded from 12.235MHz */ +#define SPITZ_AUDIO_CLOCK 12288000 + +static int spitz_jack_func; +static int spitz_spk_func; + +static void spitz_ext_control(struct snd_soc_codec *codec) +{ + if (spitz_spk_func == SPITZ_SPK_ON) + snd_soc_dapm_set_endpoint(codec, "Ext Spk", 1); + else + snd_soc_dapm_set_endpoint(codec, "Ext Spk", 0); + + /* set up jack connection */ + switch (spitz_jack_func) { + case SPITZ_HP: + /* enable and unmute hp jack, disable mic bias */ + snd_soc_dapm_set_endpoint(codec, "Headset Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Mic Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Line Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Headphone Jack", 1); + set_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_L); + set_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_R); + break; + case SPITZ_MIC: + /* enable mic jack and bias, mute hp */ + snd_soc_dapm_set_endpoint(codec, "Headphone Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Headset Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Line Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Mic Jack", 1); + reset_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_L); + reset_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_R); + break; + case SPITZ_LINE: + /* enable line jack, disable mic bias and mute hp */ + snd_soc_dapm_set_endpoint(codec, "Headphone Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Headset Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Mic Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Line Jack", 1); + reset_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_L); + reset_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_R); + break; + case SPITZ_HEADSET: + /* enable and unmute headset jack enable mic bias, mute L hp */ + snd_soc_dapm_set_endpoint(codec, "Headphone Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Mic Jack", 1); + snd_soc_dapm_set_endpoint(codec, "Line Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Headset Jack", 1); + reset_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_L); + set_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_R); + break; + case SPITZ_HP_OFF: + + /* jack removed, everything off */ + snd_soc_dapm_set_endpoint(codec, "Headphone Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Headset Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Mic Jack", 0); + snd_soc_dapm_set_endpoint(codec, "Line Jack", 0); + reset_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_L); + reset_scoop_gpio(&spitzscoop_device.dev, SPITZ_SCP_MUTE_R); + break; + } + snd_soc_dapm_sync_endpoints(codec); +} + +static int spitz_startup(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec *codec = rtd->socdev->codec; + + /* check the jack status at stream startup */ + spitz_ext_control(codec); + return 0; +} + +static int spitz_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec_dai *codec_dai = rtd->dai->codec_dai; + struct snd_soc_cpu_dai *cpu_dai = rtd->dai->cpu_dai; + unsigned int clk = 0; + int ret = 0; + + switch (params_rate(params)) { + case 8000: + case 16000: + case 48000: + case 96000: + clk = 12288000; + break; + case 11025: + case 22050: + case 44100: + clk = 11289600; + break; + } + + /* set codec DAI configuration */ + ret = codec_dai->dai_ops.set_fmt(codec_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS); + if (ret < 0) + return ret; + + /* set cpu DAI configuration */ + ret = cpu_dai->dai_ops.set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S | + SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS); + if (ret < 0) + return ret; + + /* set the codec system clock for DAC and ADC */ + ret = codec_dai->dai_ops.set_sysclk(codec_dai, WM8750_SYSCLK, clk, + SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + /* set the I2S system clock as input (unused) */ + ret = cpu_dai->dai_ops.set_sysclk(cpu_dai, PXA2XX_I2S_SYSCLK, 0, + SND_SOC_CLOCK_IN); + if (ret < 0) + return ret; + + return 0; +} + +static struct snd_soc_ops spitz_ops = { + .startup = spitz_startup, + .hw_params = spitz_hw_params, +}; + +static int spitz_get_jack(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + ucontrol->value.integer.value[0] = spitz_jack_func; + return 0; +} + +static int spitz_set_jack(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + + if (spitz_jack_func == ucontrol->value.integer.value[0]) + return 0; + + spitz_jack_func = ucontrol->value.integer.value[0]; + spitz_ext_control(codec); + return 1; +} + +static int spitz_get_spk(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + ucontrol->value.integer.value[0] = spitz_spk_func; + return 0; +} + +static int spitz_set_spk(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + + if (spitz_spk_func == ucontrol->value.integer.value[0]) + return 0; + + spitz_spk_func = ucontrol->value.integer.value[0]; + spitz_ext_control(codec); + return 1; +} + +static int spitz_mic_bias(struct snd_soc_dapm_widget *w, int event) +{ + if (machine_is_borzoi() || machine_is_spitz()) { + if (SND_SOC_DAPM_EVENT_ON(event)) + set_scoop_gpio(&spitzscoop2_device.dev, + SPITZ_SCP2_MIC_BIAS); + else + reset_scoop_gpio(&spitzscoop2_device.dev, + SPITZ_SCP2_MIC_BIAS); + } + + if (machine_is_akita()) { + if (SND_SOC_DAPM_EVENT_ON(event)) + akita_set_ioexp(&akitaioexp_device.dev, + AKITA_IOEXP_MIC_BIAS); + else + akita_reset_ioexp(&akitaioexp_device.dev, + AKITA_IOEXP_MIC_BIAS); + } + return 0; +} + +/* spitz machine dapm widgets */ +static const struct snd_soc_dapm_widget wm8750_dapm_widgets[] = { + SND_SOC_DAPM_HP("Headphone Jack", NULL), + SND_SOC_DAPM_MIC("Mic Jack", spitz_mic_bias), + SND_SOC_DAPM_SPK("Ext Spk", NULL), + SND_SOC_DAPM_LINE("Line Jack", NULL), + + /* headset is a mic and mono headphone */ + SND_SOC_DAPM_HP("Headset Jack", NULL), +}; + +/* Spitz machine audio_map */ +static const char *audio_map[][3] = { + + /* headphone connected to LOUT1, ROUT1 */ + {"Headphone Jack", NULL, "LOUT1"}, + {"Headphone Jack", NULL, "ROUT1"}, + + /* headset connected to ROUT1 and LINPUT1 with bias (def below) */ + {"Headset Jack", NULL, "ROUT1"}, + + /* ext speaker connected to LOUT2, ROUT2 */ + {"Ext Spk", NULL , "ROUT2"}, + {"Ext Spk", NULL , "LOUT2"}, + + /* mic is connected to input 1 - with bias */ + {"LINPUT1", NULL, "Mic Bias"}, + {"Mic Bias", NULL, "Mic Jack"}, + + /* line is connected to input 1 - no bias */ + {"LINPUT1", NULL, "Line Jack"}, + + {NULL, NULL, NULL}, +}; + +static const char *jack_function[] = {"Headphone", "Mic", "Line", "Headset", + "Off"}; +static const char *spk_function[] = {"On", "Off"}; +static const struct soc_enum spitz_enum[] = { + SOC_ENUM_SINGLE_EXT(5, jack_function), + SOC_ENUM_SINGLE_EXT(2, spk_function), +}; + +static const struct snd_kcontrol_new wm8750_spitz_controls[] = { + SOC_ENUM_EXT("Jack Function", spitz_enum[0], spitz_get_jack, + spitz_set_jack), + SOC_ENUM_EXT("Speaker Function", spitz_enum[1], spitz_get_spk, + spitz_set_spk), +}; + +/* + * Logic for a wm8750 as connected on a Sharp SL-Cxx00 Device + */ +static int spitz_wm8750_init(struct snd_soc_codec *codec) +{ + int i, err; + + /* NC codec pins */ + snd_soc_dapm_set_endpoint(codec, "RINPUT1", 0); + snd_soc_dapm_set_endpoint(codec, "LINPUT2", 0); + snd_soc_dapm_set_endpoint(codec, "RINPUT2", 0); + snd_soc_dapm_set_endpoint(codec, "LINPUT3", 0); + snd_soc_dapm_set_endpoint(codec, "RINPUT3", 0); + snd_soc_dapm_set_endpoint(codec, "OUT3", 0); + snd_soc_dapm_set_endpoint(codec, "MONO", 0); + + /* Add spitz specific controls */ + for (i = 0; i < ARRAY_SIZE(wm8750_spitz_controls); i++) { + err = snd_ctl_add(codec->card, + snd_soc_cnew(&wm8750_spitz_controls[i], codec, NULL)); + if (err < 0) + return err; + } + + /* Add spitz specific widgets */ + for (i = 0; i < ARRAY_SIZE(wm8750_dapm_widgets); i++) { + snd_soc_dapm_new_control(codec, &wm8750_dapm_widgets[i]); + } + + /* Set up spitz specific audio path audio_map */ + for (i = 0; audio_map[i][0] != NULL; i++) { + snd_soc_dapm_connect_input(codec, audio_map[i][0], + audio_map[i][1], audio_map[i][2]); + } + + snd_soc_dapm_sync_endpoints(codec); + return 0; +} + +/* spitz digital audio interface glue - connects codec <--> CPU */ +static struct snd_soc_dai_link spitz_dai = { + .name = "wm8750", + .stream_name = "WM8750", + .cpu_dai = &pxa_i2s_dai, + .codec_dai = &wm8750_dai, + .init = spitz_wm8750_init, + .ops = &spitz_ops, +}; + +/* spitz audio machine driver */ +static struct snd_soc_machine snd_soc_machine_spitz = { + .name = "Spitz", + .dai_link = &spitz_dai, + .num_links = 1, +}; + +/* spitz audio private data */ +static struct wm8750_setup_data spitz_wm8750_setup = { + .i2c_address = 0x1b, +}; + +/* spitz audio subsystem */ +static struct snd_soc_device spitz_snd_devdata = { + .machine = &snd_soc_machine_spitz, + .platform = &pxa2xx_soc_platform, + .codec_dev = &soc_codec_dev_wm8750, + .codec_data = &spitz_wm8750_setup, +}; + +static struct platform_device *spitz_snd_device; + +static int __init spitz_init(void) +{ + int ret; + + if (!(machine_is_spitz() || machine_is_borzoi() || machine_is_akita())) + return -ENODEV; + + spitz_snd_device = platform_device_alloc("soc-audio", -1); + if (!spitz_snd_device) + return -ENOMEM; + + platform_set_drvdata(spitz_snd_device, &spitz_snd_devdata); + spitz_snd_devdata.dev = &spitz_snd_device->dev; + ret = platform_device_add(spitz_snd_device); + + if (ret) + platform_device_put(spitz_snd_device); + + return ret; +} + +static void __exit spitz_exit(void) +{ + platform_device_unregister(spitz_snd_device); +} + +module_init(spitz_init); +module_exit(spitz_exit); + +MODULE_AUTHOR("Richard Purdie"); +MODULE_DESCRIPTION("ALSA SoC Spitz"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/pxa/tosa.c b/sound/soc/pxa/tosa.c new file mode 100644 index 00000000000..5504e30acf1 --- /dev/null +++ b/sound/soc/pxa/tosa.c @@ -0,0 +1,289 @@ +/* + * tosa.c -- SoC audio for Tosa + * + * Copyright 2005 Wolfson Microelectronics PLC. + * Copyright 2005 Openedhand Ltd. + * + * Authors: Liam Girdwood <liam.girdwood@wolfsonmicro.com> + * Richard Purdie <richard@openedhand.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Revision history + * 30th Nov 2005 Initial version. + * + * GPIO's + * 1 - Jack Insertion + * 5 - Hookswitch (headset answer/hang up switch) + * + */ + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/device.h> + +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> + +#include <asm/mach-types.h> +#include <asm/hardware/tmio.h> +#include <asm/arch/pxa-regs.h> +#include <asm/arch/hardware.h> +#include <asm/arch/audio.h> +#include <asm/arch/tosa.h> + +#include "../codecs/wm9712.h" +#include "pxa2xx-pcm.h" +#include "pxa2xx-ac97.h" + +static struct snd_soc_machine tosa; + +#define TOSA_HP 0 +#define TOSA_MIC_INT 1 +#define TOSA_HEADSET 2 +#define TOSA_HP_OFF 3 +#define TOSA_SPK_ON 0 +#define TOSA_SPK_OFF 1 + +static int tosa_jack_func; +static int tosa_spk_func; + +static void tosa_ext_control(struct snd_soc_codec *codec) +{ + int spk = 0, mic_int = 0, hp = 0, hs = 0; + + /* set up jack connection */ + switch (tosa_jack_func) { + case TOSA_HP: + hp = 1; + break; + case TOSA_MIC_INT: + mic_int = 1; + break; + case TOSA_HEADSET: + hs = 1; + break; + } + + if (tosa_spk_func == TOSA_SPK_ON) + spk = 1; + + snd_soc_dapm_set_endpoint(codec, "Speaker", spk); + snd_soc_dapm_set_endpoint(codec, "Mic (Internal)", mic_int); + snd_soc_dapm_set_endpoint(codec, "Headphone Jack", hp); + snd_soc_dapm_set_endpoint(codec, "Headset Jack", hs); + snd_soc_dapm_sync_endpoints(codec); +} + +static int tosa_startup(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_codec *codec = rtd->socdev->codec; + + /* check the jack status at stream startup */ + tosa_ext_control(codec); + return 0; +} + +static struct snd_soc_ops tosa_ops = { + .startup = tosa_startup, +}; + +static int tosa_get_jack(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + ucontrol->value.integer.value[0] = tosa_jack_func; + return 0; +} + +static int tosa_set_jack(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + + if (tosa_jack_func == ucontrol->value.integer.value[0]) + return 0; + + tosa_jack_func = ucontrol->value.integer.value[0]; + tosa_ext_control(codec); + return 1; +} + +static int tosa_get_spk(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + ucontrol->value.integer.value[0] = tosa_spk_func; + return 0; +} + +static int tosa_set_spk(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + + if (tosa_spk_func == ucontrol->value.integer.value[0]) + return 0; + + tosa_spk_func = ucontrol->value.integer.value[0]; + tosa_ext_control(codec); + return 1; +} + +/* tosa dapm event handlers */ +static int tosa_hp_event(struct snd_soc_dapm_widget *w, int event) +{ + if (SND_SOC_DAPM_EVENT_ON(event)) + set_tc6393_gpio(&tc6393_device.dev,TOSA_TC6393_L_MUTE); + else + reset_tc6393_gpio(&tc6393_device.dev,TOSA_TC6393_L_MUTE); + return 0; +} + +/* tosa machine dapm widgets */ +static const struct snd_soc_dapm_widget tosa_dapm_widgets[] = { +SND_SOC_DAPM_HP("Headphone Jack", tosa_hp_event), +SND_SOC_DAPM_HP("Headset Jack", NULL), +SND_SOC_DAPM_MIC("Mic (Internal)", NULL), +SND_SOC_DAPM_SPK("Speaker", NULL), +}; + +/* tosa audio map */ +static const char *audio_map[][3] = { + + /* headphone connected to HPOUTL, HPOUTR */ + {"Headphone Jack", NULL, "HPOUTL"}, + {"Headphone Jack", NULL, "HPOUTR"}, + + /* ext speaker connected to LOUT2, ROUT2 */ + {"Speaker", NULL, "LOUT2"}, + {"Speaker", NULL, "ROUT2"}, + + /* internal mic is connected to mic1, mic2 differential - with bias */ + {"MIC1", NULL, "Mic Bias"}, + {"MIC2", NULL, "Mic Bias"}, + {"Mic Bias", NULL, "Mic (Internal)"}, + + /* headset is connected to HPOUTR, and LINEINR with bias */ + {"Headset Jack", NULL, "HPOUTR"}, + {"LINEINR", NULL, "Mic Bias"}, + {"Mic Bias", NULL, "Headset Jack"}, + + {NULL, NULL, NULL}, +}; + +static const char *jack_function[] = {"Headphone", "Mic", "Line", "Headset", + "Off"}; +static const char *spk_function[] = {"On", "Off"}; +static const struct soc_enum tosa_enum[] = { + SOC_ENUM_SINGLE_EXT(5, jack_function), + SOC_ENUM_SINGLE_EXT(2, spk_function), +}; + +static const struct snd_kcontrol_new tosa_controls[] = { + SOC_ENUM_EXT("Jack Function", tosa_enum[0], tosa_get_jack, + tosa_set_jack), + SOC_ENUM_EXT("Speaker Function", tosa_enum[1], tosa_get_spk, + tosa_set_spk), +}; + +static int tosa_ac97_init(struct snd_soc_codec *codec) +{ + int i, err; + + snd_soc_dapm_set_endpoint(codec, "OUT3", 0); + snd_soc_dapm_set_endpoint(codec, "MONOOUT", 0); + + /* add tosa specific controls */ + for (i = 0; i < ARRAY_SIZE(tosa_controls); i++) { + err = snd_ctl_add(codec->card, + snd_soc_cnew(&tosa_controls[i],codec, NULL)); + if (err < 0) + return err; + } + + /* add tosa specific widgets */ + for (i = 0; i < ARRAY_SIZE(tosa_dapm_widgets); i++) { + snd_soc_dapm_new_control(codec, &tosa_dapm_widgets[i]); + } + + /* set up tosa specific audio path audio_map */ + for (i = 0; audio_map[i][0] != NULL; i++) { + snd_soc_dapm_connect_input(codec, audio_map[i][0], + audio_map[i][1], audio_map[i][2]); + } + + snd_soc_dapm_sync_endpoints(codec); + return 0; +} + +static struct snd_soc_dai_link tosa_dai[] = { +{ + .name = "AC97", + .stream_name = "AC97 HiFi", + .cpu_dai = &pxa_ac97_dai[PXA2XX_DAI_AC97_HIFI], + .codec_dai = &wm9712_dai[WM9712_DAI_AC97_HIFI], + .init = tosa_ac97_init, + .ops = &tosa_ops, +}, +{ + .name = "AC97 Aux", + .stream_name = "AC97 Aux", + .cpu_dai = &pxa_ac97_dai[PXA2XX_DAI_AC97_AUX], + .codec_dai = &wm9712_dai[WM9712_DAI_AC97_AUX], + .ops = &tosa_ops, +}, +}; + +static struct snd_soc_machine tosa = { + .name = "Tosa", + .dai_link = tosa_dai, + .num_links = ARRAY_SIZE(tosa_dai), +}; + +static struct snd_soc_device tosa_snd_devdata = { + .machine = &tosa, + .platform = &pxa2xx_soc_platform, + .codec_dev = &soc_codec_dev_wm9712, +}; + +static struct platform_device *tosa_snd_device; + +static int __init tosa_init(void) +{ + int ret; + + if (!machine_is_tosa()) + return -ENODEV; + + tosa_snd_device = platform_device_alloc("soc-audio", -1); + if (!tosa_snd_device) + return -ENOMEM; + + platform_set_drvdata(tosa_snd_device, &tosa_snd_devdata); + tosa_snd_devdata.dev = &tosa_snd_device->dev; + ret = platform_device_add(tosa_snd_device); + + if (ret) + platform_device_put(tosa_snd_device); + + return ret; +} + +static void __exit tosa_exit(void) +{ + platform_device_unregister(tosa_snd_device); +} + +module_init(tosa_init); +module_exit(tosa_exit); + +/* Module information */ +MODULE_AUTHOR("Richard Purdie"); +MODULE_DESCRIPTION("ALSA SoC Tosa"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/soc-core.c b/sound/soc/soc-core.c new file mode 100644 index 00000000000..36519aef55d --- /dev/null +++ b/sound/soc/soc-core.c @@ -0,0 +1,1587 @@ +/* + * soc-core.c -- ALSA SoC Audio Layer + * + * Copyright 2005 Wolfson Microelectronics PLC. + * Copyright 2005 Openedhand Ltd. + * + * Author: Liam Girdwood + * liam.girdwood@wolfsonmicro.com or linux@wolfsonmicro.com + * with code, comments and ideas from :- + * Richard Purdie <richard@openedhand.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Revision history + * 12th Aug 2005 Initial version. + * 25th Oct 2005 Working Codec, Interface and Platform registration. + * + * TODO: + * o Add hw rules to enforce rates, etc. + * o More testing with other codecs/machines. + * o Add more codecs and platforms to ensure good API coverage. + * o Support TDM on PCM and I2S + */ + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/pm.h> +#include <linux/bitops.h> +#include <linux/platform_device.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/initval.h> + +/* debug */ +#define SOC_DEBUG 0 +#if SOC_DEBUG +#define dbg(format, arg...) printk(format, ## arg) +#else +#define dbg(format, arg...) +#endif + +static DEFINE_MUTEX(pcm_mutex); +static DEFINE_MUTEX(io_mutex); +static DECLARE_WAIT_QUEUE_HEAD(soc_pm_waitq); + +/* + * This is a timeout to do a DAPM powerdown after a stream is closed(). + * It can be used to eliminate pops between different playback streams, e.g. + * between two audio tracks. + */ +static int pmdown_time = 5000; +module_param(pmdown_time, int, 0); +MODULE_PARM_DESC(pmdown_time, "DAPM stream powerdown time (msecs)"); + +/* + * This function forces any delayed work to be queued and run. + */ +static int run_delayed_work(struct delayed_work *dwork) +{ + int ret; + + /* cancel any work waiting to be queued. */ + ret = cancel_delayed_work(dwork); + + /* if there was any work waiting then we run it now and + * wait for it's completion */ + if (ret) { + schedule_delayed_work(dwork, 0); + flush_scheduled_work(); + } + return ret; +} + +#ifdef CONFIG_SND_SOC_AC97_BUS +/* unregister ac97 codec */ +static int soc_ac97_dev_unregister(struct snd_soc_codec *codec) +{ + if (codec->ac97->dev.bus) + device_unregister(&codec->ac97->dev); + return 0; +} + +/* stop no dev release warning */ +static void soc_ac97_device_release(struct device *dev){} + +/* register ac97 codec to bus */ +static int soc_ac97_dev_register(struct snd_soc_codec *codec) +{ + int err; + + codec->ac97->dev.bus = &ac97_bus_type; + codec->ac97->dev.parent = NULL; + codec->ac97->dev.release = soc_ac97_device_release; + + snprintf(codec->ac97->dev.bus_id, BUS_ID_SIZE, "%d-%d:%s", + codec->card->number, 0, codec->name); + err = device_register(&codec->ac97->dev); + if (err < 0) { + snd_printk(KERN_ERR "Can't register ac97 bus\n"); + codec->ac97->dev.bus = NULL; + return err; + } + return 0; +} +#endif + +static inline const char* get_dai_name(int type) +{ + switch(type) { + case SND_SOC_DAI_AC97: + return "AC97"; + case SND_SOC_DAI_I2S: + return "I2S"; + case SND_SOC_DAI_PCM: + return "PCM"; + } + return NULL; +} + +/* + * Called by ALSA when a PCM substream is opened, the runtime->hw record is + * then initialized and any private data can be allocated. This also calls + * startup for the cpu DAI, platform, machine and codec DAI. + */ +static int soc_pcm_open(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_pcm_runtime *runtime = substream->runtime; + struct snd_soc_dai_link *machine = rtd->dai; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_cpu_dai *cpu_dai = machine->cpu_dai; + struct snd_soc_codec_dai *codec_dai = machine->codec_dai; + int ret = 0; + + mutex_lock(&pcm_mutex); + + /* startup the audio subsystem */ + if (cpu_dai->ops.startup) { + ret = cpu_dai->ops.startup(substream); + if (ret < 0) { + printk(KERN_ERR "asoc: can't open interface %s\n", + cpu_dai->name); + goto out; + } + } + + if (platform->pcm_ops->open) { + ret = platform->pcm_ops->open(substream); + if (ret < 0) { + printk(KERN_ERR "asoc: can't open platform %s\n", platform->name); + goto platform_err; + } + } + + if (codec_dai->ops.startup) { + ret = codec_dai->ops.startup(substream); + if (ret < 0) { + printk(KERN_ERR "asoc: can't open codec %s\n", + codec_dai->name); + goto codec_dai_err; + } + } + + if (machine->ops && machine->ops->startup) { + ret = machine->ops->startup(substream); + if (ret < 0) { + printk(KERN_ERR "asoc: %s startup failed\n", machine->name); + goto machine_err; + } + } + + /* Check that the codec and cpu DAI's are compatible */ + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + runtime->hw.rate_min = + max(codec_dai->playback.rate_min, cpu_dai->playback.rate_min); + runtime->hw.rate_max = + min(codec_dai->playback.rate_max, cpu_dai->playback.rate_max); + runtime->hw.channels_min = + max(codec_dai->playback.channels_min, + cpu_dai->playback.channels_min); + runtime->hw.channels_max = + min(codec_dai->playback.channels_max, + cpu_dai->playback.channels_max); + runtime->hw.formats = + codec_dai->playback.formats & cpu_dai->playback.formats; + runtime->hw.rates = + codec_dai->playback.rates & cpu_dai->playback.rates; + } else { + runtime->hw.rate_min = + max(codec_dai->capture.rate_min, cpu_dai->capture.rate_min); + runtime->hw.rate_max = + min(codec_dai->capture.rate_max, cpu_dai->capture.rate_max); + runtime->hw.channels_min = + max(codec_dai->capture.channels_min, + cpu_dai->capture.channels_min); + runtime->hw.channels_max = + min(codec_dai->capture.channels_max, + cpu_dai->capture.channels_max); + runtime->hw.formats = + codec_dai->capture.formats & cpu_dai->capture.formats; + runtime->hw.rates = + codec_dai->capture.rates & cpu_dai->capture.rates; + } + + snd_pcm_limit_hw_rates(runtime); + if (!runtime->hw.rates) { + printk(KERN_ERR "asoc: %s <-> %s No matching rates\n", + codec_dai->name, cpu_dai->name); + goto machine_err; + } + if (!runtime->hw.formats) { + printk(KERN_ERR "asoc: %s <-> %s No matching formats\n", + codec_dai->name, cpu_dai->name); + goto machine_err; + } + if (!runtime->hw.channels_min || !runtime->hw.channels_max) { + printk(KERN_ERR "asoc: %s <-> %s No matching channels\n", + codec_dai->name, cpu_dai->name); + goto machine_err; + } + + dbg("asoc: %s <-> %s info:\n",codec_dai->name, cpu_dai->name); + dbg("asoc: rate mask 0x%x\n", runtime->hw.rates); + dbg("asoc: min ch %d max ch %d\n", runtime->hw.channels_min, + runtime->hw.channels_max); + dbg("asoc: min rate %d max rate %d\n", runtime->hw.rate_min, + runtime->hw.rate_max); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + cpu_dai->playback.active = codec_dai->playback.active = 1; + else + cpu_dai->capture.active = codec_dai->capture.active = 1; + cpu_dai->active = codec_dai->active = 1; + cpu_dai->runtime = runtime; + socdev->codec->active++; + mutex_unlock(&pcm_mutex); + return 0; + +machine_err: + if (machine->ops && machine->ops->shutdown) + machine->ops->shutdown(substream); + +codec_dai_err: + if (platform->pcm_ops->close) + platform->pcm_ops->close(substream); + +platform_err: + if (cpu_dai->ops.shutdown) + cpu_dai->ops.shutdown(substream); +out: + mutex_unlock(&pcm_mutex); + return ret; +} + +/* + * Power down the audio subsytem pmdown_time msecs after close is called. + * This is to ensure there are no pops or clicks in between any music tracks + * due to DAPM power cycling. + */ +static void close_delayed_work(struct work_struct *work) +{ + struct snd_soc_device *socdev = + container_of(work, struct snd_soc_device, delayed_work.work); + struct snd_soc_codec *codec = socdev->codec; + struct snd_soc_codec_dai *codec_dai; + int i; + + mutex_lock(&pcm_mutex); + for(i = 0; i < codec->num_dai; i++) { + codec_dai = &codec->dai[i]; + + dbg("pop wq checking: %s status: %s waiting: %s\n", + codec_dai->playback.stream_name, + codec_dai->playback.active ? "active" : "inactive", + codec_dai->pop_wait ? "yes" : "no"); + + /* are we waiting on this codec DAI stream */ + if (codec_dai->pop_wait == 1) { + + codec_dai->pop_wait = 0; + snd_soc_dapm_stream_event(codec, codec_dai->playback.stream_name, + SND_SOC_DAPM_STREAM_STOP); + + /* power down the codec power domain if no longer active */ + if (codec->active == 0) { + dbg("pop wq D3 %s %s\n", codec->name, + codec_dai->playback.stream_name); + if (codec->dapm_event) + codec->dapm_event(codec, SNDRV_CTL_POWER_D3hot); + } + } + } + mutex_unlock(&pcm_mutex); +} + +/* + * Called by ALSA when a PCM substream is closed. Private data can be + * freed here. The cpu DAI, codec DAI, machine and platform are also + * shutdown. + */ +static int soc_codec_close(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_dai_link *machine = rtd->dai; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_cpu_dai *cpu_dai = machine->cpu_dai; + struct snd_soc_codec_dai *codec_dai = machine->codec_dai; + struct snd_soc_codec *codec = socdev->codec; + + mutex_lock(&pcm_mutex); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + cpu_dai->playback.active = codec_dai->playback.active = 0; + else + cpu_dai->capture.active = codec_dai->capture.active = 0; + + if (codec_dai->playback.active == 0 && + codec_dai->capture.active == 0) { + cpu_dai->active = codec_dai->active = 0; + } + codec->active--; + + if (cpu_dai->ops.shutdown) + cpu_dai->ops.shutdown(substream); + + if (codec_dai->ops.shutdown) + codec_dai->ops.shutdown(substream); + + if (machine->ops && machine->ops->shutdown) + machine->ops->shutdown(substream); + + if (platform->pcm_ops->close) + platform->pcm_ops->close(substream); + cpu_dai->runtime = NULL; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + /* start delayed pop wq here for playback streams */ + codec_dai->pop_wait = 1; + schedule_delayed_work(&socdev->delayed_work, + msecs_to_jiffies(pmdown_time)); + } else { + /* capture streams can be powered down now */ + snd_soc_dapm_stream_event(codec, + codec_dai->capture.stream_name, SND_SOC_DAPM_STREAM_STOP); + + if (codec->active == 0 && codec_dai->pop_wait == 0){ + if (codec->dapm_event) + codec->dapm_event(codec, SNDRV_CTL_POWER_D3hot); + } + } + + mutex_unlock(&pcm_mutex); + return 0; +} + +/* + * Called by ALSA when the PCM substream is prepared, can set format, sample + * rate, etc. This function is non atomic and can be called multiple times, + * it can refer to the runtime info. + */ +static int soc_pcm_prepare(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_dai_link *machine = rtd->dai; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_cpu_dai *cpu_dai = machine->cpu_dai; + struct snd_soc_codec_dai *codec_dai = machine->codec_dai; + struct snd_soc_codec *codec = socdev->codec; + int ret = 0; + + mutex_lock(&pcm_mutex); + + if (machine->ops && machine->ops->prepare) { + ret = machine->ops->prepare(substream); + if (ret < 0) { + printk(KERN_ERR "asoc: machine prepare error\n"); + goto out; + } + } + + if (platform->pcm_ops->prepare) { + ret = platform->pcm_ops->prepare(substream); + if (ret < 0) { + printk(KERN_ERR "asoc: platform prepare error\n"); + goto out; + } + } + + if (codec_dai->ops.prepare) { + ret = codec_dai->ops.prepare(substream); + if (ret < 0) { + printk(KERN_ERR "asoc: codec DAI prepare error\n"); + goto out; + } + } + + if (cpu_dai->ops.prepare) { + ret = cpu_dai->ops.prepare(substream); + if (ret < 0) { + printk(KERN_ERR "asoc: cpu DAI prepare error\n"); + goto out; + } + } + + /* we only want to start a DAPM playback stream if we are not waiting + * on an existing one stopping */ + if (codec_dai->pop_wait) { + /* we are waiting for the delayed work to start */ + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) + snd_soc_dapm_stream_event(socdev->codec, + codec_dai->capture.stream_name, + SND_SOC_DAPM_STREAM_START); + else { + codec_dai->pop_wait = 0; + cancel_delayed_work(&socdev->delayed_work); + if (codec_dai->dai_ops.digital_mute) + codec_dai->dai_ops.digital_mute(codec_dai, 0); + } + } else { + /* no delayed work - do we need to power up codec */ + if (codec->dapm_state != SNDRV_CTL_POWER_D0) { + + if (codec->dapm_event) + codec->dapm_event(codec, SNDRV_CTL_POWER_D1); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + snd_soc_dapm_stream_event(codec, + codec_dai->playback.stream_name, + SND_SOC_DAPM_STREAM_START); + else + snd_soc_dapm_stream_event(codec, + codec_dai->capture.stream_name, + SND_SOC_DAPM_STREAM_START); + + if (codec->dapm_event) + codec->dapm_event(codec, SNDRV_CTL_POWER_D0); + if (codec_dai->dai_ops.digital_mute) + codec_dai->dai_ops.digital_mute(codec_dai, 0); + + } else { + /* codec already powered - power on widgets */ + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + snd_soc_dapm_stream_event(codec, + codec_dai->playback.stream_name, + SND_SOC_DAPM_STREAM_START); + else + snd_soc_dapm_stream_event(codec, + codec_dai->capture.stream_name, + SND_SOC_DAPM_STREAM_START); + if (codec_dai->dai_ops.digital_mute) + codec_dai->dai_ops.digital_mute(codec_dai, 0); + } + } + +out: + mutex_unlock(&pcm_mutex); + return ret; +} + +/* + * Called by ALSA when the hardware params are set by application. This + * function can also be called multiple times and can allocate buffers + * (using snd_pcm_lib_* ). It's non-atomic. + */ +static int soc_pcm_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_dai_link *machine = rtd->dai; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_cpu_dai *cpu_dai = machine->cpu_dai; + struct snd_soc_codec_dai *codec_dai = machine->codec_dai; + int ret = 0; + + mutex_lock(&pcm_mutex); + + if (machine->ops && machine->ops->hw_params) { + ret = machine->ops->hw_params(substream, params); + if (ret < 0) { + printk(KERN_ERR "asoc: machine hw_params failed\n"); + goto out; + } + } + + if (codec_dai->ops.hw_params) { + ret = codec_dai->ops.hw_params(substream, params); + if (ret < 0) { + printk(KERN_ERR "asoc: can't set codec %s hw params\n", + codec_dai->name); + goto codec_err; + } + } + + if (cpu_dai->ops.hw_params) { + ret = cpu_dai->ops.hw_params(substream, params); + if (ret < 0) { + printk(KERN_ERR "asoc: can't set interface %s hw params\n", + cpu_dai->name); + goto interface_err; + } + } + + if (platform->pcm_ops->hw_params) { + ret = platform->pcm_ops->hw_params(substream, params); + if (ret < 0) { + printk(KERN_ERR "asoc: can't set platform %s hw params\n", + platform->name); + goto platform_err; + } + } + +out: + mutex_unlock(&pcm_mutex); + return ret; + +platform_err: + if (cpu_dai->ops.hw_free) + cpu_dai->ops.hw_free(substream); + +interface_err: + if (codec_dai->ops.hw_free) + codec_dai->ops.hw_free(substream); + +codec_err: + if(machine->ops && machine->ops->hw_free) + machine->ops->hw_free(substream); + + mutex_unlock(&pcm_mutex); + return ret; +} + +/* + * Free's resources allocated by hw_params, can be called multiple times + */ +static int soc_pcm_hw_free(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_dai_link *machine = rtd->dai; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_cpu_dai *cpu_dai = machine->cpu_dai; + struct snd_soc_codec_dai *codec_dai = machine->codec_dai; + struct snd_soc_codec *codec = socdev->codec; + + mutex_lock(&pcm_mutex); + + /* apply codec digital mute */ + if (!codec->active && codec_dai->dai_ops.digital_mute) + codec_dai->dai_ops.digital_mute(codec_dai, 1); + + /* free any machine hw params */ + if (machine->ops && machine->ops->hw_free) + machine->ops->hw_free(substream); + + /* free any DMA resources */ + if (platform->pcm_ops->hw_free) + platform->pcm_ops->hw_free(substream); + + /* now free hw params for the DAI's */ + if (codec_dai->ops.hw_free) + codec_dai->ops.hw_free(substream); + + if (cpu_dai->ops.hw_free) + cpu_dai->ops.hw_free(substream); + + mutex_unlock(&pcm_mutex); + return 0; +} + +static int soc_pcm_trigger(struct snd_pcm_substream *substream, int cmd) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_dai_link *machine = rtd->dai; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_cpu_dai *cpu_dai = machine->cpu_dai; + struct snd_soc_codec_dai *codec_dai = machine->codec_dai; + int ret; + + if (codec_dai->ops.trigger) { + ret = codec_dai->ops.trigger(substream, cmd); + if (ret < 0) + return ret; + } + + if (platform->pcm_ops->trigger) { + ret = platform->pcm_ops->trigger(substream, cmd); + if (ret < 0) + return ret; + } + + if (cpu_dai->ops.trigger) { + ret = cpu_dai->ops.trigger(substream, cmd); + if (ret < 0) + return ret; + } + return 0; +} + +/* ASoC PCM operations */ +static struct snd_pcm_ops soc_pcm_ops = { + .open = soc_pcm_open, + .close = soc_codec_close, + .hw_params = soc_pcm_hw_params, + .hw_free = soc_pcm_hw_free, + .prepare = soc_pcm_prepare, + .trigger = soc_pcm_trigger, +}; + +#ifdef CONFIG_PM +/* powers down audio subsystem for suspend */ +static int soc_suspend(struct platform_device *pdev, pm_message_t state) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_machine *machine = socdev->machine; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_codec_device *codec_dev = socdev->codec_dev; + struct snd_soc_codec *codec = socdev->codec; + int i; + + /* mute any active DAC's */ + for(i = 0; i < machine->num_links; i++) { + struct snd_soc_codec_dai *dai = machine->dai_link[i].codec_dai; + if (dai->dai_ops.digital_mute && dai->playback.active) + dai->dai_ops.digital_mute(dai, 1); + } + + if (machine->suspend_pre) + machine->suspend_pre(pdev, state); + + for(i = 0; i < machine->num_links; i++) { + struct snd_soc_cpu_dai *cpu_dai = machine->dai_link[i].cpu_dai; + if (cpu_dai->suspend && cpu_dai->type != SND_SOC_DAI_AC97) + cpu_dai->suspend(pdev, cpu_dai); + if (platform->suspend) + platform->suspend(pdev, cpu_dai); + } + + /* close any waiting streams and save state */ + run_delayed_work(&socdev->delayed_work); + codec->suspend_dapm_state = codec->dapm_state; + + for(i = 0; i < codec->num_dai; i++) { + char *stream = codec->dai[i].playback.stream_name; + if (stream != NULL) + snd_soc_dapm_stream_event(codec, stream, + SND_SOC_DAPM_STREAM_SUSPEND); + stream = codec->dai[i].capture.stream_name; + if (stream != NULL) + snd_soc_dapm_stream_event(codec, stream, + SND_SOC_DAPM_STREAM_SUSPEND); + } + + if (codec_dev->suspend) + codec_dev->suspend(pdev, state); + + for(i = 0; i < machine->num_links; i++) { + struct snd_soc_cpu_dai *cpu_dai = machine->dai_link[i].cpu_dai; + if (cpu_dai->suspend && cpu_dai->type == SND_SOC_DAI_AC97) + cpu_dai->suspend(pdev, cpu_dai); + } + + if (machine->suspend_post) + machine->suspend_post(pdev, state); + + return 0; +} + +/* powers up audio subsystem after a suspend */ +static int soc_resume(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_machine *machine = socdev->machine; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_codec_device *codec_dev = socdev->codec_dev; + struct snd_soc_codec *codec = socdev->codec; + int i; + + if (machine->resume_pre) + machine->resume_pre(pdev); + + for(i = 0; i < machine->num_links; i++) { + struct snd_soc_cpu_dai *cpu_dai = machine->dai_link[i].cpu_dai; + if (cpu_dai->resume && cpu_dai->type == SND_SOC_DAI_AC97) + cpu_dai->resume(pdev, cpu_dai); + } + + if (codec_dev->resume) + codec_dev->resume(pdev); + + for(i = 0; i < codec->num_dai; i++) { + char* stream = codec->dai[i].playback.stream_name; + if (stream != NULL) + snd_soc_dapm_stream_event(codec, stream, + SND_SOC_DAPM_STREAM_RESUME); + stream = codec->dai[i].capture.stream_name; + if (stream != NULL) + snd_soc_dapm_stream_event(codec, stream, + SND_SOC_DAPM_STREAM_RESUME); + } + + /* unmute any active DAC's */ + for(i = 0; i < machine->num_links; i++) { + struct snd_soc_codec_dai *dai = machine->dai_link[i].codec_dai; + if (dai->dai_ops.digital_mute && dai->playback.active) + dai->dai_ops.digital_mute(dai, 0); + } + + for(i = 0; i < machine->num_links; i++) { + struct snd_soc_cpu_dai *cpu_dai = machine->dai_link[i].cpu_dai; + if (cpu_dai->resume && cpu_dai->type != SND_SOC_DAI_AC97) + cpu_dai->resume(pdev, cpu_dai); + if (platform->resume) + platform->resume(pdev, cpu_dai); + } + + if (machine->resume_post) + machine->resume_post(pdev); + + return 0; +} + +#else +#define soc_suspend NULL +#define soc_resume NULL +#endif + +/* probes a new socdev */ +static int soc_probe(struct platform_device *pdev) +{ + int ret = 0, i; + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_machine *machine = socdev->machine; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_codec_device *codec_dev = socdev->codec_dev; + + if (machine->probe) { + ret = machine->probe(pdev); + if(ret < 0) + return ret; + } + + for (i = 0; i < machine->num_links; i++) { + struct snd_soc_cpu_dai *cpu_dai = machine->dai_link[i].cpu_dai; + if (cpu_dai->probe) { + ret = cpu_dai->probe(pdev); + if(ret < 0) + goto cpu_dai_err; + } + } + + if (codec_dev->probe) { + ret = codec_dev->probe(pdev); + if(ret < 0) + goto cpu_dai_err; + } + + if (platform->probe) { + ret = platform->probe(pdev); + if(ret < 0) + goto platform_err; + } + + /* DAPM stream work */ + INIT_DELAYED_WORK(&socdev->delayed_work, close_delayed_work); + return 0; + +platform_err: + if (codec_dev->remove) + codec_dev->remove(pdev); + +cpu_dai_err: + for (i--; i >= 0; i--) { + struct snd_soc_cpu_dai *cpu_dai = machine->dai_link[i].cpu_dai; + if (cpu_dai->remove) + cpu_dai->remove(pdev); + } + + if (machine->remove) + machine->remove(pdev); + + return ret; +} + +/* removes a socdev */ +static int soc_remove(struct platform_device *pdev) +{ + int i; + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_machine *machine = socdev->machine; + struct snd_soc_platform *platform = socdev->platform; + struct snd_soc_codec_device *codec_dev = socdev->codec_dev; + + run_delayed_work(&socdev->delayed_work); + + if (platform->remove) + platform->remove(pdev); + + if (codec_dev->remove) + codec_dev->remove(pdev); + + for (i = 0; i < machine->num_links; i++) { + struct snd_soc_cpu_dai *cpu_dai = machine->dai_link[i].cpu_dai; + if (cpu_dai->remove) + cpu_dai->remove(pdev); + } + + if (machine->remove) + machine->remove(pdev); + + return 0; +} + +/* ASoC platform driver */ +static struct platform_driver soc_driver = { + .driver = { + .name = "soc-audio", + }, + .probe = soc_probe, + .remove = soc_remove, + .suspend = soc_suspend, + .resume = soc_resume, +}; + +/* create a new pcm */ +static int soc_new_pcm(struct snd_soc_device *socdev, + struct snd_soc_dai_link *dai_link, int num) +{ + struct snd_soc_codec *codec = socdev->codec; + struct snd_soc_codec_dai *codec_dai = dai_link->codec_dai; + struct snd_soc_cpu_dai *cpu_dai = dai_link->cpu_dai; + struct snd_soc_pcm_runtime *rtd; + struct snd_pcm *pcm; + char new_name[64]; + int ret = 0, playback = 0, capture = 0; + + rtd = kzalloc(sizeof(struct snd_soc_pcm_runtime), GFP_KERNEL); + if (rtd == NULL) + return -ENOMEM; + + rtd->dai = dai_link; + rtd->socdev = socdev; + codec_dai->codec = socdev->codec; + + /* check client and interface hw capabilities */ + sprintf(new_name, "%s %s-%s-%d",dai_link->stream_name, codec_dai->name, + get_dai_name(cpu_dai->type), num); + + if (codec_dai->playback.channels_min) + playback = 1; + if (codec_dai->capture.channels_min) + capture = 1; + + ret = snd_pcm_new(codec->card, new_name, codec->pcm_devs++, playback, + capture, &pcm); + if (ret < 0) { + printk(KERN_ERR "asoc: can't create pcm for codec %s\n", codec->name); + kfree(rtd); + return ret; + } + + pcm->private_data = rtd; + soc_pcm_ops.mmap = socdev->platform->pcm_ops->mmap; + soc_pcm_ops.pointer = socdev->platform->pcm_ops->pointer; + soc_pcm_ops.ioctl = socdev->platform->pcm_ops->ioctl; + soc_pcm_ops.copy = socdev->platform->pcm_ops->copy; + soc_pcm_ops.silence = socdev->platform->pcm_ops->silence; + soc_pcm_ops.ack = socdev->platform->pcm_ops->ack; + soc_pcm_ops.page = socdev->platform->pcm_ops->page; + + if (playback) + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &soc_pcm_ops); + + if (capture) + snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &soc_pcm_ops); + + ret = socdev->platform->pcm_new(codec->card, codec_dai, pcm); + if (ret < 0) { + printk(KERN_ERR "asoc: platform pcm constructor failed\n"); + kfree(rtd); + return ret; + } + + pcm->private_free = socdev->platform->pcm_free; + printk(KERN_INFO "asoc: %s <-> %s mapping ok\n", codec_dai->name, + cpu_dai->name); + return ret; +} + +/* codec register dump */ +static ssize_t codec_reg_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct snd_soc_device *devdata = dev_get_drvdata(dev); + struct snd_soc_codec *codec = devdata->codec; + int i, step = 1, count = 0; + + if (!codec->reg_cache_size) + return 0; + + if (codec->reg_cache_step) + step = codec->reg_cache_step; + + count += sprintf(buf, "%s registers\n", codec->name); + for(i = 0; i < codec->reg_cache_size; i += step) + count += sprintf(buf + count, "%2x: %4x\n", i, codec->read(codec, i)); + + return count; +} +static DEVICE_ATTR(codec_reg, 0444, codec_reg_show, NULL); + +/** + * snd_soc_new_ac97_codec - initailise AC97 device + * @codec: audio codec + * @ops: AC97 bus operations + * @num: AC97 codec number + * + * Initialises AC97 codec resources for use by ad-hoc devices only. + */ +int snd_soc_new_ac97_codec(struct snd_soc_codec *codec, + struct snd_ac97_bus_ops *ops, int num) +{ + mutex_lock(&codec->mutex); + + codec->ac97 = kzalloc(sizeof(struct snd_ac97), GFP_KERNEL); + if (codec->ac97 == NULL) { + mutex_unlock(&codec->mutex); + return -ENOMEM; + } + + codec->ac97->bus = kzalloc(sizeof(struct snd_ac97_bus), GFP_KERNEL); + if (codec->ac97->bus == NULL) { + kfree(codec->ac97); + codec->ac97 = NULL; + mutex_unlock(&codec->mutex); + return -ENOMEM; + } + + codec->ac97->bus->ops = ops; + codec->ac97->num = num; + mutex_unlock(&codec->mutex); + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_new_ac97_codec); + +/** + * snd_soc_free_ac97_codec - free AC97 codec device + * @codec: audio codec + * + * Frees AC97 codec device resources. + */ +void snd_soc_free_ac97_codec(struct snd_soc_codec *codec) +{ + mutex_lock(&codec->mutex); + kfree(codec->ac97->bus); + kfree(codec->ac97); + codec->ac97 = NULL; + mutex_unlock(&codec->mutex); +} +EXPORT_SYMBOL_GPL(snd_soc_free_ac97_codec); + +/** + * snd_soc_update_bits - update codec register bits + * @codec: audio codec + * @reg: codec register + * @mask: register mask + * @value: new value + * + * Writes new register value. + * + * Returns 1 for change else 0. + */ +int snd_soc_update_bits(struct snd_soc_codec *codec, unsigned short reg, + unsigned short mask, unsigned short value) +{ + int change; + unsigned short old, new; + + mutex_lock(&io_mutex); + old = snd_soc_read(codec, reg); + new = (old & ~mask) | value; + change = old != new; + if (change) + snd_soc_write(codec, reg, new); + + mutex_unlock(&io_mutex); + return change; +} +EXPORT_SYMBOL_GPL(snd_soc_update_bits); + +/** + * snd_soc_test_bits - test register for change + * @codec: audio codec + * @reg: codec register + * @mask: register mask + * @value: new value + * + * Tests a register with a new value and checks if the new value is + * different from the old value. + * + * Returns 1 for change else 0. + */ +int snd_soc_test_bits(struct snd_soc_codec *codec, unsigned short reg, + unsigned short mask, unsigned short value) +{ + int change; + unsigned short old, new; + + mutex_lock(&io_mutex); + old = snd_soc_read(codec, reg); + new = (old & ~mask) | value; + change = old != new; + mutex_unlock(&io_mutex); + + return change; +} +EXPORT_SYMBOL_GPL(snd_soc_test_bits); + +/** + * snd_soc_new_pcms - create new sound card and pcms + * @socdev: the SoC audio device + * + * Create a new sound card based upon the codec and interface pcms. + * + * Returns 0 for success, else error. + */ +int snd_soc_new_pcms(struct snd_soc_device *socdev, int idx, const char *xid) +{ + struct snd_soc_codec *codec = socdev->codec; + struct snd_soc_machine *machine = socdev->machine; + int ret = 0, i; + + mutex_lock(&codec->mutex); + + /* register a sound card */ + codec->card = snd_card_new(idx, xid, codec->owner, 0); + if (!codec->card) { + printk(KERN_ERR "asoc: can't create sound card for codec %s\n", + codec->name); + mutex_unlock(&codec->mutex); + return -ENODEV; + } + + codec->card->dev = socdev->dev; + codec->card->private_data = codec; + strncpy(codec->card->driver, codec->name, sizeof(codec->card->driver)); + + /* create the pcms */ + for(i = 0; i < machine->num_links; i++) { + ret = soc_new_pcm(socdev, &machine->dai_link[i], i); + if (ret < 0) { + printk(KERN_ERR "asoc: can't create pcm %s\n", + machine->dai_link[i].stream_name); + mutex_unlock(&codec->mutex); + return ret; + } + } + + mutex_unlock(&codec->mutex); + return ret; +} +EXPORT_SYMBOL_GPL(snd_soc_new_pcms); + +/** + * snd_soc_register_card - register sound card + * @socdev: the SoC audio device + * + * Register a SoC sound card. Also registers an AC97 device if the + * codec is AC97 for ad hoc devices. + * + * Returns 0 for success, else error. + */ +int snd_soc_register_card(struct snd_soc_device *socdev) +{ + struct snd_soc_codec *codec = socdev->codec; + struct snd_soc_machine *machine = socdev->machine; + int ret = 0, i, ac97 = 0, err = 0; + + mutex_lock(&codec->mutex); + for(i = 0; i < machine->num_links; i++) { + if (socdev->machine->dai_link[i].init) { + err = socdev->machine->dai_link[i].init(codec); + if (err < 0) { + printk(KERN_ERR "asoc: failed to init %s\n", + socdev->machine->dai_link[i].stream_name); + continue; + } + } + if (socdev->machine->dai_link[i].cpu_dai->type == SND_SOC_DAI_AC97) + ac97 = 1; + } + snprintf(codec->card->shortname, sizeof(codec->card->shortname), + "%s", machine->name); + snprintf(codec->card->longname, sizeof(codec->card->longname), + "%s (%s)", machine->name, codec->name); + + ret = snd_card_register(codec->card); + if (ret < 0) { + printk(KERN_ERR "asoc: failed to register soundcard for codec %s\n", + codec->name); + goto out; + } + +#ifdef CONFIG_SND_SOC_AC97_BUS + if (ac97) { + ret = soc_ac97_dev_register(codec); + if (ret < 0) { + printk(KERN_ERR "asoc: AC97 device register failed\n"); + snd_card_free(codec->card); + goto out; + } + } +#endif + + err = snd_soc_dapm_sys_add(socdev->dev); + if (err < 0) + printk(KERN_WARNING "asoc: failed to add dapm sysfs entries\n"); + + err = device_create_file(socdev->dev, &dev_attr_codec_reg); + if (err < 0) + printk(KERN_WARNING "asoc: failed to add codec sysfs entries\n"); +out: + mutex_unlock(&codec->mutex); + return ret; +} +EXPORT_SYMBOL_GPL(snd_soc_register_card); + +/** + * snd_soc_free_pcms - free sound card and pcms + * @socdev: the SoC audio device + * + * Frees sound card and pcms associated with the socdev. + * Also unregister the codec if it is an AC97 device. + */ +void snd_soc_free_pcms(struct snd_soc_device *socdev) +{ + struct snd_soc_codec *codec = socdev->codec; + + mutex_lock(&codec->mutex); +#ifdef CONFIG_SND_SOC_AC97_BUS + if (codec->ac97) + soc_ac97_dev_unregister(codec); +#endif + + if (codec->card) + snd_card_free(codec->card); + device_remove_file(socdev->dev, &dev_attr_codec_reg); + mutex_unlock(&codec->mutex); +} +EXPORT_SYMBOL_GPL(snd_soc_free_pcms); + +/** + * snd_soc_set_runtime_hwparams - set the runtime hardware parameters + * @substream: the pcm substream + * @hw: the hardware parameters + * + * Sets the substream runtime hardware parameters. + */ +int snd_soc_set_runtime_hwparams(struct snd_pcm_substream *substream, + const struct snd_pcm_hardware *hw) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + runtime->hw.info = hw->info; + runtime->hw.formats = hw->formats; + runtime->hw.period_bytes_min = hw->period_bytes_min; + runtime->hw.period_bytes_max = hw->period_bytes_max; + runtime->hw.periods_min = hw->periods_min; + runtime->hw.periods_max = hw->periods_max; + runtime->hw.buffer_bytes_max = hw->buffer_bytes_max; + runtime->hw.fifo_size = hw->fifo_size; + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_set_runtime_hwparams); + +/** + * snd_soc_cnew - create new control + * @_template: control template + * @data: control private data + * @lnng_name: control long name + * + * Create a new mixer control from a template control. + * + * Returns 0 for success, else error. + */ +struct snd_kcontrol *snd_soc_cnew(const struct snd_kcontrol_new *_template, + void *data, char *long_name) +{ + struct snd_kcontrol_new template; + + memcpy(&template, _template, sizeof(template)); + if (long_name) + template.name = long_name; + template.access = SNDRV_CTL_ELEM_ACCESS_READWRITE; + template.index = 0; + + return snd_ctl_new1(&template, data); +} +EXPORT_SYMBOL_GPL(snd_soc_cnew); + +/** + * snd_soc_info_enum_double - enumerated double mixer info callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to provide information about a double enumerated + * mixer control. + * + * Returns 0 for success. + */ +int snd_soc_info_enum_double(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; + uinfo->count = e->shift_l == e->shift_r ? 1 : 2; + uinfo->value.enumerated.items = e->mask; + + if (uinfo->value.enumerated.item > e->mask - 1) + uinfo->value.enumerated.item = e->mask - 1; + strcpy(uinfo->value.enumerated.name, + e->texts[uinfo->value.enumerated.item]); + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_info_enum_double); + +/** + * snd_soc_get_enum_double - enumerated double mixer get callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to get the value of a double enumerated mixer. + * + * Returns 0 for success. + */ +int snd_soc_get_enum_double(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + unsigned short val, bitmask; + + for (bitmask = 1; bitmask < e->mask; bitmask <<= 1) + ; + val = snd_soc_read(codec, e->reg); + ucontrol->value.enumerated.item[0] = (val >> e->shift_l) & (bitmask - 1); + if (e->shift_l != e->shift_r) + ucontrol->value.enumerated.item[1] = + (val >> e->shift_r) & (bitmask - 1); + + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_get_enum_double); + +/** + * snd_soc_put_enum_double - enumerated double mixer put callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to set the value of a double enumerated mixer. + * + * Returns 0 for success. + */ +int snd_soc_put_enum_double(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + unsigned short val; + unsigned short mask, bitmask; + + for (bitmask = 1; bitmask < e->mask; bitmask <<= 1) + ; + if (ucontrol->value.enumerated.item[0] > e->mask - 1) + return -EINVAL; + val = ucontrol->value.enumerated.item[0] << e->shift_l; + mask = (bitmask - 1) << e->shift_l; + if (e->shift_l != e->shift_r) { + if (ucontrol->value.enumerated.item[1] > e->mask - 1) + return -EINVAL; + val |= ucontrol->value.enumerated.item[1] << e->shift_r; + mask |= (bitmask - 1) << e->shift_r; + } + + return snd_soc_update_bits(codec, e->reg, mask, val); +} +EXPORT_SYMBOL_GPL(snd_soc_put_enum_double); + +/** + * snd_soc_info_enum_ext - external enumerated single mixer info callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to provide information about an external enumerated + * single mixer. + * + * Returns 0 for success. + */ +int snd_soc_info_enum_ext(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + + uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; + uinfo->count = 1; + uinfo->value.enumerated.items = e->mask; + + if (uinfo->value.enumerated.item > e->mask - 1) + uinfo->value.enumerated.item = e->mask - 1; + strcpy(uinfo->value.enumerated.name, + e->texts[uinfo->value.enumerated.item]); + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_info_enum_ext); + +/** + * snd_soc_info_volsw_ext - external single mixer info callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to provide information about a single external mixer control. + * + * Returns 0 for success. + */ +int snd_soc_info_volsw_ext(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + int mask = kcontrol->private_value; + + uinfo->type = + mask == 1 ? SNDRV_CTL_ELEM_TYPE_BOOLEAN : SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = mask; + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_info_volsw_ext); + +/** + * snd_soc_info_bool_ext - external single boolean mixer info callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to provide information about a single boolean external mixer control. + * + * Returns 0 for success. + */ +int snd_soc_info_bool_ext(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = 1; + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_info_bool_ext); + +/** + * snd_soc_info_volsw - single mixer info callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to provide information about a single mixer control. + * + * Returns 0 for success. + */ +int snd_soc_info_volsw(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + int mask = (kcontrol->private_value >> 16) & 0xff; + int shift = (kcontrol->private_value >> 8) & 0x0f; + int rshift = (kcontrol->private_value >> 12) & 0x0f; + + uinfo->type = + mask == 1 ? SNDRV_CTL_ELEM_TYPE_BOOLEAN : SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = shift == rshift ? 1 : 2; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = mask; + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_info_volsw); + +/** + * snd_soc_get_volsw - single mixer get callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to get the value of a single mixer control. + * + * Returns 0 for success. + */ +int snd_soc_get_volsw(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + int reg = kcontrol->private_value & 0xff; + int shift = (kcontrol->private_value >> 8) & 0x0f; + int rshift = (kcontrol->private_value >> 12) & 0x0f; + int mask = (kcontrol->private_value >> 16) & 0xff; + int invert = (kcontrol->private_value >> 24) & 0x01; + + ucontrol->value.integer.value[0] = + (snd_soc_read(codec, reg) >> shift) & mask; + if (shift != rshift) + ucontrol->value.integer.value[1] = + (snd_soc_read(codec, reg) >> rshift) & mask; + if (invert) { + ucontrol->value.integer.value[0] = + mask - ucontrol->value.integer.value[0]; + if (shift != rshift) + ucontrol->value.integer.value[1] = + mask - ucontrol->value.integer.value[1]; + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_get_volsw); + +/** + * snd_soc_put_volsw - single mixer put callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to set the value of a single mixer control. + * + * Returns 0 for success. + */ +int snd_soc_put_volsw(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + int reg = kcontrol->private_value & 0xff; + int shift = (kcontrol->private_value >> 8) & 0x0f; + int rshift = (kcontrol->private_value >> 12) & 0x0f; + int mask = (kcontrol->private_value >> 16) & 0xff; + int invert = (kcontrol->private_value >> 24) & 0x01; + int err; + unsigned short val, val2, val_mask; + + val = (ucontrol->value.integer.value[0] & mask); + if (invert) + val = mask - val; + val_mask = mask << shift; + val = val << shift; + if (shift != rshift) { + val2 = (ucontrol->value.integer.value[1] & mask); + if (invert) + val2 = mask - val2; + val_mask |= mask << rshift; + val |= val2 << rshift; + } + err = snd_soc_update_bits(codec, reg, val_mask, val); + return err; +} +EXPORT_SYMBOL_GPL(snd_soc_put_volsw); + +/** + * snd_soc_info_volsw_2r - double mixer info callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to provide information about a double mixer control that + * spans 2 codec registers. + * + * Returns 0 for success. + */ +int snd_soc_info_volsw_2r(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + int mask = (kcontrol->private_value >> 12) & 0xff; + + uinfo->type = + mask == 1 ? SNDRV_CTL_ELEM_TYPE_BOOLEAN : SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 2; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = mask; + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_info_volsw_2r); + +/** + * snd_soc_get_volsw_2r - double mixer get callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to get the value of a double mixer control that spans 2 registers. + * + * Returns 0 for success. + */ +int snd_soc_get_volsw_2r(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + int reg = kcontrol->private_value & 0xff; + int reg2 = (kcontrol->private_value >> 24) & 0xff; + int shift = (kcontrol->private_value >> 8) & 0x0f; + int mask = (kcontrol->private_value >> 12) & 0xff; + int invert = (kcontrol->private_value >> 20) & 0x01; + + ucontrol->value.integer.value[0] = + (snd_soc_read(codec, reg) >> shift) & mask; + ucontrol->value.integer.value[1] = + (snd_soc_read(codec, reg2) >> shift) & mask; + if (invert) { + ucontrol->value.integer.value[0] = + mask - ucontrol->value.integer.value[0]; + ucontrol->value.integer.value[1] = + mask - ucontrol->value.integer.value[1]; + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_get_volsw_2r); + +/** + * snd_soc_put_volsw_2r - double mixer set callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to set the value of a double mixer control that spans 2 registers. + * + * Returns 0 for success. + */ +int snd_soc_put_volsw_2r(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol); + int reg = kcontrol->private_value & 0xff; + int reg2 = (kcontrol->private_value >> 24) & 0xff; + int shift = (kcontrol->private_value >> 8) & 0x0f; + int mask = (kcontrol->private_value >> 12) & 0xff; + int invert = (kcontrol->private_value >> 20) & 0x01; + int err; + unsigned short val, val2, val_mask; + + val_mask = mask << shift; + val = (ucontrol->value.integer.value[0] & mask); + val2 = (ucontrol->value.integer.value[1] & mask); + + if (invert) { + val = mask - val; + val2 = mask - val2; + } + + val = val << shift; + val2 = val2 << shift; + + if ((err = snd_soc_update_bits(codec, reg, val_mask, val)) < 0) + return err; + + err = snd_soc_update_bits(codec, reg2, val_mask, val2); + return err; +} +EXPORT_SYMBOL_GPL(snd_soc_put_volsw_2r); + +static int __devinit snd_soc_init(void) +{ + printk(KERN_INFO "ASoC version %s\n", SND_SOC_VERSION); + return platform_driver_register(&soc_driver); +} + +static void snd_soc_exit(void) +{ + platform_driver_unregister(&soc_driver); +} + +module_init(snd_soc_init); +module_exit(snd_soc_exit); + +/* Module information */ +MODULE_AUTHOR("Liam Girdwood, liam.girdwood@wolfsonmicro.com, www.wolfsonmicro.com"); +MODULE_DESCRIPTION("ALSA SoC Core"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/soc-dapm.c b/sound/soc/soc-dapm.c new file mode 100644 index 00000000000..7caf8c7b0ac --- /dev/null +++ b/sound/soc/soc-dapm.c @@ -0,0 +1,1323 @@ +/* + * soc-dapm.c -- ALSA SoC Dynamic Audio Power Management + * + * Copyright 2005 Wolfson Microelectronics PLC. + * Author: Liam Girdwood + * liam.girdwood@wolfsonmicro.com or linux@wolfsonmicro.com + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Revision history + * 12th Aug 2005 Initial version. + * 25th Oct 2005 Implemented path power domain. + * 18th Dec 2005 Implemented machine and stream level power domain. + * + * Features: + * o Changes power status of internal codec blocks depending on the + * dynamic configuration of codec internal audio paths and active + * DAC's/ADC's. + * o Platform power domain - can support external components i.e. amps and + * mic/meadphone insertion events. + * o Automatic Mic Bias support + * o Jack insertion power event initiation - e.g. hp insertion will enable + * sinks, dacs, etc + * o Delayed powerdown of audio susbsytem to reduce pops between a quick + * device reopen. + * + * Todo: + * o DAPM power change sequencing - allow for configurable per + * codec sequences. + * o Support for analogue bias optimisation. + * o Support for reduced codec oversampling rates. + * o Support for reduced codec bias currents. + */ + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/pm.h> +#include <linux/bitops.h> +#include <linux/platform_device.h> +#include <linux/jiffies.h> +#include <sound/driver.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc-dapm.h> +#include <sound/initval.h> + +/* debug */ +#define DAPM_DEBUG 0 +#if DAPM_DEBUG +#define dump_dapm(codec, action) dbg_dump_dapm(codec, action) +#define dbg(format, arg...) printk(format, ## arg) +#else +#define dump_dapm(codec, action) +#define dbg(format, arg...) +#endif + +#define POP_DEBUG 0 +#if POP_DEBUG +#define POP_TIME 500 /* 500 msecs - change if pop debug is too fast */ +#define pop_wait(time) schedule_timeout_interruptible(msecs_to_jiffies(time)) +#define pop_dbg(format, arg...) printk(format, ## arg); pop_wait(POP_TIME) +#else +#define pop_dbg(format, arg...) +#define pop_wait(time) +#endif + +/* dapm power sequences - make this per codec in the future */ +static int dapm_up_seq[] = { + snd_soc_dapm_pre, snd_soc_dapm_micbias, snd_soc_dapm_mic, + snd_soc_dapm_mux, snd_soc_dapm_dac, snd_soc_dapm_mixer, snd_soc_dapm_pga, + snd_soc_dapm_adc, snd_soc_dapm_hp, snd_soc_dapm_spk, snd_soc_dapm_post +}; +static int dapm_down_seq[] = { + snd_soc_dapm_pre, snd_soc_dapm_adc, snd_soc_dapm_hp, snd_soc_dapm_spk, + snd_soc_dapm_pga, snd_soc_dapm_mixer, snd_soc_dapm_dac, snd_soc_dapm_mic, + snd_soc_dapm_micbias, snd_soc_dapm_mux, snd_soc_dapm_post +}; + +static int dapm_status = 1; +module_param(dapm_status, int, 0); +MODULE_PARM_DESC(dapm_status, "enable DPM sysfs entries"); + +/* create a new dapm widget */ +static inline struct snd_soc_dapm_widget *dapm_cnew_widget( + const struct snd_soc_dapm_widget *_widget) +{ + return kmemdup(_widget, sizeof(*_widget), GFP_KERNEL); +} + +/* set up initial codec paths */ +static void dapm_set_path_status(struct snd_soc_dapm_widget *w, + struct snd_soc_dapm_path *p, int i) +{ + switch (w->id) { + case snd_soc_dapm_switch: + case snd_soc_dapm_mixer: { + int val; + int reg = w->kcontrols[i].private_value & 0xff; + int shift = (w->kcontrols[i].private_value >> 8) & 0x0f; + int mask = (w->kcontrols[i].private_value >> 16) & 0xff; + int invert = (w->kcontrols[i].private_value >> 24) & 0x01; + + val = snd_soc_read(w->codec, reg); + val = (val >> shift) & mask; + + if ((invert && !val) || (!invert && val)) + p->connect = 1; + else + p->connect = 0; + } + break; + case snd_soc_dapm_mux: { + struct soc_enum *e = (struct soc_enum *)w->kcontrols[i].private_value; + int val, item, bitmask; + + for (bitmask = 1; bitmask < e->mask; bitmask <<= 1) + ; + val = snd_soc_read(w->codec, e->reg); + item = (val >> e->shift_l) & (bitmask - 1); + + p->connect = 0; + for (i = 0; i < e->mask; i++) { + if (!(strcmp(p->name, e->texts[i])) && item == i) + p->connect = 1; + } + } + break; + /* does not effect routing - always connected */ + case snd_soc_dapm_pga: + case snd_soc_dapm_output: + case snd_soc_dapm_adc: + case snd_soc_dapm_input: + case snd_soc_dapm_dac: + case snd_soc_dapm_micbias: + case snd_soc_dapm_vmid: + p->connect = 1; + break; + /* does effect routing - dynamically connected */ + case snd_soc_dapm_hp: + case snd_soc_dapm_mic: + case snd_soc_dapm_spk: + case snd_soc_dapm_line: + case snd_soc_dapm_pre: + case snd_soc_dapm_post: + p->connect = 0; + break; + } +} + +/* connect mux widget to it's interconnecting audio paths */ +static int dapm_connect_mux(struct snd_soc_codec *codec, + struct snd_soc_dapm_widget *src, struct snd_soc_dapm_widget *dest, + struct snd_soc_dapm_path *path, const char *control_name, + const struct snd_kcontrol_new *kcontrol) +{ + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + int i; + + for (i = 0; i < e->mask; i++) { + if (!(strcmp(control_name, e->texts[i]))) { + list_add(&path->list, &codec->dapm_paths); + list_add(&path->list_sink, &dest->sources); + list_add(&path->list_source, &src->sinks); + path->name = (char*)e->texts[i]; + dapm_set_path_status(dest, path, 0); + return 0; + } + } + + return -ENODEV; +} + +/* connect mixer widget to it's interconnecting audio paths */ +static int dapm_connect_mixer(struct snd_soc_codec *codec, + struct snd_soc_dapm_widget *src, struct snd_soc_dapm_widget *dest, + struct snd_soc_dapm_path *path, const char *control_name) +{ + int i; + + /* search for mixer kcontrol */ + for (i = 0; i < dest->num_kcontrols; i++) { + if (!strcmp(control_name, dest->kcontrols[i].name)) { + list_add(&path->list, &codec->dapm_paths); + list_add(&path->list_sink, &dest->sources); + list_add(&path->list_source, &src->sinks); + path->name = dest->kcontrols[i].name; + dapm_set_path_status(dest, path, i); + return 0; + } + } + return -ENODEV; +} + +/* update dapm codec register bits */ +static int dapm_update_bits(struct snd_soc_dapm_widget *widget) +{ + int change, power; + unsigned short old, new; + struct snd_soc_codec *codec = widget->codec; + + /* check for valid widgets */ + if (widget->reg < 0 || widget->id == snd_soc_dapm_input || + widget->id == snd_soc_dapm_output || + widget->id == snd_soc_dapm_hp || + widget->id == snd_soc_dapm_mic || + widget->id == snd_soc_dapm_line || + widget->id == snd_soc_dapm_spk) + return 0; + + power = widget->power; + if (widget->invert) + power = (power ? 0:1); + + old = snd_soc_read(codec, widget->reg); + new = (old & ~(0x1 << widget->shift)) | (power << widget->shift); + + change = old != new; + if (change) { + pop_dbg("pop test %s : %s in %d ms\n", widget->name, + widget->power ? "on" : "off", POP_TIME); + snd_soc_write(codec, widget->reg, new); + pop_wait(POP_TIME); + } + dbg("reg old %x new %x change %d\n", old, new, change); + return change; +} + +/* ramps the volume up or down to minimise pops before or after a + * DAPM power event */ +static int dapm_set_pga(struct snd_soc_dapm_widget *widget, int power) +{ + const struct snd_kcontrol_new *k = widget->kcontrols; + + if (widget->muted && !power) + return 0; + if (!widget->muted && power) + return 0; + + if (widget->num_kcontrols && k) { + int reg = k->private_value & 0xff; + int shift = (k->private_value >> 8) & 0x0f; + int mask = (k->private_value >> 16) & 0xff; + int invert = (k->private_value >> 24) & 0x01; + + if (power) { + int i; + /* power up has happended, increase volume to last level */ + if (invert) { + for (i = mask; i > widget->saved_value; i--) + snd_soc_update_bits(widget->codec, reg, mask, i); + } else { + for (i = 0; i < widget->saved_value; i++) + snd_soc_update_bits(widget->codec, reg, mask, i); + } + widget->muted = 0; + } else { + /* power down is about to occur, decrease volume to mute */ + int val = snd_soc_read(widget->codec, reg); + int i = widget->saved_value = (val >> shift) & mask; + if (invert) { + for (; i < mask; i++) + snd_soc_update_bits(widget->codec, reg, mask, i); + } else { + for (; i > 0; i--) + snd_soc_update_bits(widget->codec, reg, mask, i); + } + widget->muted = 1; + } + } + return 0; +} + +/* create new dapm mixer control */ +static int dapm_new_mixer(struct snd_soc_codec *codec, + struct snd_soc_dapm_widget *w) +{ + int i, ret = 0; + char name[32]; + struct snd_soc_dapm_path *path; + + /* add kcontrol */ + for (i = 0; i < w->num_kcontrols; i++) { + + /* match name */ + list_for_each_entry(path, &w->sources, list_sink) { + + /* mixer/mux paths name must match control name */ + if (path->name != (char*)w->kcontrols[i].name) + continue; + + /* add dapm control with long name */ + snprintf(name, 32, "%s %s", w->name, w->kcontrols[i].name); + path->long_name = kstrdup (name, GFP_KERNEL); + if (path->long_name == NULL) + return -ENOMEM; + + path->kcontrol = snd_soc_cnew(&w->kcontrols[i], w, + path->long_name); + ret = snd_ctl_add(codec->card, path->kcontrol); + if (ret < 0) { + printk(KERN_ERR "asoc: failed to add dapm kcontrol %s\n", + path->long_name); + kfree(path->long_name); + path->long_name = NULL; + return ret; + } + } + } + return ret; +} + +/* create new dapm mux control */ +static int dapm_new_mux(struct snd_soc_codec *codec, + struct snd_soc_dapm_widget *w) +{ + struct snd_soc_dapm_path *path = NULL; + struct snd_kcontrol *kcontrol; + int ret = 0; + + if (!w->num_kcontrols) { + printk(KERN_ERR "asoc: mux %s has no controls\n", w->name); + return -EINVAL; + } + + kcontrol = snd_soc_cnew(&w->kcontrols[0], w, w->name); + ret = snd_ctl_add(codec->card, kcontrol); + if (ret < 0) + goto err; + + list_for_each_entry(path, &w->sources, list_sink) + path->kcontrol = kcontrol; + + return ret; + +err: + printk(KERN_ERR "asoc: failed to add kcontrol %s\n", w->name); + return ret; +} + +/* create new dapm volume control */ +static int dapm_new_pga(struct snd_soc_codec *codec, + struct snd_soc_dapm_widget *w) +{ + struct snd_kcontrol *kcontrol; + int ret = 0; + + if (!w->num_kcontrols) + return -EINVAL; + + kcontrol = snd_soc_cnew(&w->kcontrols[0], w, w->name); + ret = snd_ctl_add(codec->card, kcontrol); + if (ret < 0) { + printk(KERN_ERR "asoc: failed to add kcontrol %s\n", w->name); + return ret; + } + + return ret; +} + +/* reset 'walked' bit for each dapm path */ +static inline void dapm_clear_walk(struct snd_soc_codec *codec) +{ + struct snd_soc_dapm_path *p; + + list_for_each_entry(p, &codec->dapm_paths, list) + p->walked = 0; +} + +/* + * Recursively check for a completed path to an active or physically connected + * output widget. Returns number of complete paths. + */ +static int is_connected_output_ep(struct snd_soc_dapm_widget *widget) +{ + struct snd_soc_dapm_path *path; + int con = 0; + + if (widget->id == snd_soc_dapm_adc && widget->active) + return 1; + + if (widget->connected) { + /* connected pin ? */ + if (widget->id == snd_soc_dapm_output && !widget->ext) + return 1; + + /* connected jack or spk ? */ + if (widget->id == snd_soc_dapm_hp || widget->id == snd_soc_dapm_spk || + widget->id == snd_soc_dapm_line) + return 1; + } + + list_for_each_entry(path, &widget->sinks, list_source) { + if (path->walked) + continue; + + if (path->sink && path->connect) { + path->walked = 1; + con += is_connected_output_ep(path->sink); + } + } + + return con; +} + +/* + * Recursively check for a completed path to an active or physically connected + * input widget. Returns number of complete paths. + */ +static int is_connected_input_ep(struct snd_soc_dapm_widget *widget) +{ + struct snd_soc_dapm_path *path; + int con = 0; + + /* active stream ? */ + if (widget->id == snd_soc_dapm_dac && widget->active) + return 1; + + if (widget->connected) { + /* connected pin ? */ + if (widget->id == snd_soc_dapm_input && !widget->ext) + return 1; + + /* connected VMID/Bias for lower pops */ + if (widget->id == snd_soc_dapm_vmid) + return 1; + + /* connected jack ? */ + if (widget->id == snd_soc_dapm_mic || widget->id == snd_soc_dapm_line) + return 1; + } + + list_for_each_entry(path, &widget->sources, list_sink) { + if (path->walked) + continue; + + if (path->source && path->connect) { + path->walked = 1; + con += is_connected_input_ep(path->source); + } + } + + return con; +} + +/* + * Scan each dapm widget for complete audio path. + * A complete path is a route that has valid endpoints i.e.:- + * + * o DAC to output pin. + * o Input Pin to ADC. + * o Input pin to Output pin (bypass, sidetone) + * o DAC to ADC (loopback). + */ +static int dapm_power_widgets(struct snd_soc_codec *codec, int event) +{ + struct snd_soc_dapm_widget *w; + int in, out, i, c = 1, *seq = NULL, ret = 0, power_change, power; + + /* do we have a sequenced stream event */ + if (event == SND_SOC_DAPM_STREAM_START) { + c = ARRAY_SIZE(dapm_up_seq); + seq = dapm_up_seq; + } else if (event == SND_SOC_DAPM_STREAM_STOP) { + c = ARRAY_SIZE(dapm_down_seq); + seq = dapm_down_seq; + } + + for(i = 0; i < c; i++) { + list_for_each_entry(w, &codec->dapm_widgets, list) { + + /* is widget in stream order */ + if (seq && seq[i] && w->id != seq[i]) + continue; + + /* vmid - no action */ + if (w->id == snd_soc_dapm_vmid) + continue; + + /* active ADC */ + if (w->id == snd_soc_dapm_adc && w->active) { + in = is_connected_input_ep(w); + dapm_clear_walk(w->codec); + w->power = (in != 0) ? 1 : 0; + dapm_update_bits(w); + continue; + } + + /* active DAC */ + if (w->id == snd_soc_dapm_dac && w->active) { + out = is_connected_output_ep(w); + dapm_clear_walk(w->codec); + w->power = (out != 0) ? 1 : 0; + dapm_update_bits(w); + continue; + } + + /* programmable gain/attenuation */ + if (w->id == snd_soc_dapm_pga) { + int on; + in = is_connected_input_ep(w); + dapm_clear_walk(w->codec); + out = is_connected_output_ep(w); + dapm_clear_walk(w->codec); + w->power = on = (out != 0 && in != 0) ? 1 : 0; + + if (!on) + dapm_set_pga(w, on); /* lower volume to reduce pops */ + dapm_update_bits(w); + if (on) + dapm_set_pga(w, on); /* restore volume from zero */ + + continue; + } + + /* pre and post event widgets */ + if (w->id == snd_soc_dapm_pre) { + if (!w->event) + continue; + + if (event == SND_SOC_DAPM_STREAM_START) { + ret = w->event(w, SND_SOC_DAPM_PRE_PMU); + if (ret < 0) + return ret; + } else if (event == SND_SOC_DAPM_STREAM_STOP) { + ret = w->event(w, SND_SOC_DAPM_PRE_PMD); + if (ret < 0) + return ret; + } + continue; + } + if (w->id == snd_soc_dapm_post) { + if (!w->event) + continue; + + if (event == SND_SOC_DAPM_STREAM_START) { + ret = w->event(w, SND_SOC_DAPM_POST_PMU); + if (ret < 0) + return ret; + } else if (event == SND_SOC_DAPM_STREAM_STOP) { + ret = w->event(w, SND_SOC_DAPM_POST_PMD); + if (ret < 0) + return ret; + } + continue; + } + + /* all other widgets */ + in = is_connected_input_ep(w); + dapm_clear_walk(w->codec); + out = is_connected_output_ep(w); + dapm_clear_walk(w->codec); + power = (out != 0 && in != 0) ? 1 : 0; + power_change = (w->power == power) ? 0: 1; + w->power = power; + + /* call any power change event handlers */ + if (power_change) { + if (w->event) { + dbg("power %s event for %s flags %x\n", + w->power ? "on" : "off", w->name, w->event_flags); + if (power) { + /* power up event */ + if (w->event_flags & SND_SOC_DAPM_PRE_PMU) { + ret = w->event(w, SND_SOC_DAPM_PRE_PMU); + if (ret < 0) + return ret; + } + dapm_update_bits(w); + if (w->event_flags & SND_SOC_DAPM_POST_PMU){ + ret = w->event(w, SND_SOC_DAPM_POST_PMU); + if (ret < 0) + return ret; + } + } else { + /* power down event */ + if (w->event_flags & SND_SOC_DAPM_PRE_PMD) { + ret = w->event(w, SND_SOC_DAPM_PRE_PMD); + if (ret < 0) + return ret; + } + dapm_update_bits(w); + if (w->event_flags & SND_SOC_DAPM_POST_PMD) { + ret = w->event(w, SND_SOC_DAPM_POST_PMD); + if (ret < 0) + return ret; + } + } + } else + /* no event handler */ + dapm_update_bits(w); + } + } + } + + return ret; +} + +#if DAPM_DEBUG +static void dbg_dump_dapm(struct snd_soc_codec* codec, const char *action) +{ + struct snd_soc_dapm_widget *w; + struct snd_soc_dapm_path *p = NULL; + int in, out; + + printk("DAPM %s %s\n", codec->name, action); + + list_for_each_entry(w, &codec->dapm_widgets, list) { + + /* only display widgets that effect routing */ + switch (w->id) { + case snd_soc_dapm_pre: + case snd_soc_dapm_post: + case snd_soc_dapm_vmid: + continue; + case snd_soc_dapm_mux: + case snd_soc_dapm_output: + case snd_soc_dapm_input: + case snd_soc_dapm_switch: + case snd_soc_dapm_hp: + case snd_soc_dapm_mic: + case snd_soc_dapm_spk: + case snd_soc_dapm_line: + case snd_soc_dapm_micbias: + case snd_soc_dapm_dac: + case snd_soc_dapm_adc: + case snd_soc_dapm_pga: + case snd_soc_dapm_mixer: + if (w->name) { + in = is_connected_input_ep(w); + dapm_clear_walk(w->codec); + out = is_connected_output_ep(w); + dapm_clear_walk(w->codec); + printk("%s: %s in %d out %d\n", w->name, + w->power ? "On":"Off",in, out); + + list_for_each_entry(p, &w->sources, list_sink) { + if (p->connect) + printk(" in %s %s\n", p->name ? p->name : "static", + p->source->name); + } + list_for_each_entry(p, &w->sinks, list_source) { + if (p->connect) + printk(" out %s %s\n", p->name ? p->name : "static", + p->sink->name); + } + } + break; + } + } +} +#endif + +/* test and update the power status of a mux widget */ +static int dapm_mux_update_power(struct snd_soc_dapm_widget *widget, + struct snd_kcontrol *kcontrol, int mask, + int val, struct soc_enum* e) +{ + struct snd_soc_dapm_path *path; + int found = 0; + + if (widget->id != snd_soc_dapm_mux) + return -ENODEV; + + if (!snd_soc_test_bits(widget->codec, e->reg, mask, val)) + return 0; + + /* find dapm widget path assoc with kcontrol */ + list_for_each_entry(path, &widget->codec->dapm_paths, list) { + if (path->kcontrol != kcontrol) + continue; + + if (!path->name || ! e->texts[val]) + continue; + + found = 1; + /* we now need to match the string in the enum to the path */ + if (!(strcmp(path->name, e->texts[val]))) + path->connect = 1; /* new connection */ + else + path->connect = 0; /* old connection must be powered down */ + } + + if (found) + dapm_power_widgets(widget->codec, SND_SOC_DAPM_STREAM_NOP); + + return 0; +} + +/* test and update the power status of a mixer widget */ +static int dapm_mixer_update_power(struct snd_soc_dapm_widget *widget, + struct snd_kcontrol *kcontrol, int reg, + int val_mask, int val, int invert) +{ + struct snd_soc_dapm_path *path; + int found = 0; + + if (widget->id != snd_soc_dapm_mixer) + return -ENODEV; + + if (!snd_soc_test_bits(widget->codec, reg, val_mask, val)) + return 0; + + /* find dapm widget path assoc with kcontrol */ + list_for_each_entry(path, &widget->codec->dapm_paths, list) { + if (path->kcontrol != kcontrol) + continue; + + /* found, now check type */ + found = 1; + if (val) + /* new connection */ + path->connect = invert ? 0:1; + else + /* old connection must be powered down */ + path->connect = invert ? 1:0; + break; + } + + if (found) + dapm_power_widgets(widget->codec, SND_SOC_DAPM_STREAM_NOP); + + return 0; +} + +/* show dapm widget status in sys fs */ +static ssize_t dapm_widget_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct snd_soc_device *devdata = dev_get_drvdata(dev); + struct snd_soc_codec *codec = devdata->codec; + struct snd_soc_dapm_widget *w; + int count = 0; + char *state = "not set"; + + list_for_each_entry(w, &codec->dapm_widgets, list) { + + /* only display widgets that burnm power */ + switch (w->id) { + case snd_soc_dapm_hp: + case snd_soc_dapm_mic: + case snd_soc_dapm_spk: + case snd_soc_dapm_line: + case snd_soc_dapm_micbias: + case snd_soc_dapm_dac: + case snd_soc_dapm_adc: + case snd_soc_dapm_pga: + case snd_soc_dapm_mixer: + if (w->name) + count += sprintf(buf + count, "%s: %s\n", + w->name, w->power ? "On":"Off"); + break; + default: + break; + } + } + + switch(codec->dapm_state){ + case SNDRV_CTL_POWER_D0: + state = "D0"; + break; + case SNDRV_CTL_POWER_D1: + state = "D1"; + break; + case SNDRV_CTL_POWER_D2: + state = "D2"; + break; + case SNDRV_CTL_POWER_D3hot: + state = "D3hot"; + break; + case SNDRV_CTL_POWER_D3cold: + state = "D3cold"; + break; + } + count += sprintf(buf + count, "PM State: %s\n", state); + + return count; +} + +static DEVICE_ATTR(dapm_widget, 0444, dapm_widget_show, NULL); + +int snd_soc_dapm_sys_add(struct device *dev) +{ + int ret = 0; + + if (dapm_status) + ret = device_create_file(dev, &dev_attr_dapm_widget); + + return ret; +} + +static void snd_soc_dapm_sys_remove(struct device *dev) +{ + if (dapm_status) + device_remove_file(dev, &dev_attr_dapm_widget); +} + +/* free all dapm widgets and resources */ +static void dapm_free_widgets(struct snd_soc_codec *codec) +{ + struct snd_soc_dapm_widget *w, *next_w; + struct snd_soc_dapm_path *p, *next_p; + + list_for_each_entry_safe(w, next_w, &codec->dapm_widgets, list) { + list_del(&w->list); + kfree(w); + } + + list_for_each_entry_safe(p, next_p, &codec->dapm_paths, list) { + list_del(&p->list); + kfree(p->long_name); + kfree(p); + } +} + +/** + * snd_soc_dapm_sync_endpoints - scan and power dapm paths + * @codec: audio codec + * + * Walks all dapm audio paths and powers widgets according to their + * stream or path usage. + * + * Returns 0 for success. + */ +int snd_soc_dapm_sync_endpoints(struct snd_soc_codec *codec) +{ + return dapm_power_widgets(codec, SND_SOC_DAPM_STREAM_NOP); +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_sync_endpoints); + +/** + * snd_soc_dapm_connect_input - connect dapm widgets + * @codec: audio codec + * @sink: name of target widget + * @control: mixer control name + * @source: name of source name + * + * Connects 2 dapm widgets together via a named audio path. The sink is + * the widget receiving the audio signal, whilst the source is the sender + * of the audio signal. + * + * Returns 0 for success else error. + */ +int snd_soc_dapm_connect_input(struct snd_soc_codec *codec, const char *sink, + const char * control, const char *source) +{ + struct snd_soc_dapm_path *path; + struct snd_soc_dapm_widget *wsource = NULL, *wsink = NULL, *w; + int ret = 0; + + /* find src and dest widgets */ + list_for_each_entry(w, &codec->dapm_widgets, list) { + + if (!wsink && !(strcmp(w->name, sink))) { + wsink = w; + continue; + } + if (!wsource && !(strcmp(w->name, source))) { + wsource = w; + } + } + + if (wsource == NULL || wsink == NULL) + return -ENODEV; + + path = kzalloc(sizeof(struct snd_soc_dapm_path), GFP_KERNEL); + if (!path) + return -ENOMEM; + + path->source = wsource; + path->sink = wsink; + INIT_LIST_HEAD(&path->list); + INIT_LIST_HEAD(&path->list_source); + INIT_LIST_HEAD(&path->list_sink); + + /* check for external widgets */ + if (wsink->id == snd_soc_dapm_input) { + if (wsource->id == snd_soc_dapm_micbias || + wsource->id == snd_soc_dapm_mic || + wsink->id == snd_soc_dapm_line) + wsink->ext = 1; + } + if (wsource->id == snd_soc_dapm_output) { + if (wsink->id == snd_soc_dapm_spk || + wsink->id == snd_soc_dapm_hp || + wsink->id == snd_soc_dapm_line) + wsource->ext = 1; + } + + /* connect static paths */ + if (control == NULL) { + list_add(&path->list, &codec->dapm_paths); + list_add(&path->list_sink, &wsink->sources); + list_add(&path->list_source, &wsource->sinks); + path->connect = 1; + return 0; + } + + /* connect dynamic paths */ + switch(wsink->id) { + case snd_soc_dapm_adc: + case snd_soc_dapm_dac: + case snd_soc_dapm_pga: + case snd_soc_dapm_input: + case snd_soc_dapm_output: + case snd_soc_dapm_micbias: + case snd_soc_dapm_vmid: + case snd_soc_dapm_pre: + case snd_soc_dapm_post: + list_add(&path->list, &codec->dapm_paths); + list_add(&path->list_sink, &wsink->sources); + list_add(&path->list_source, &wsource->sinks); + path->connect = 1; + return 0; + case snd_soc_dapm_mux: + ret = dapm_connect_mux(codec, wsource, wsink, path, control, + &wsink->kcontrols[0]); + if (ret != 0) + goto err; + break; + case snd_soc_dapm_switch: + case snd_soc_dapm_mixer: + ret = dapm_connect_mixer(codec, wsource, wsink, path, control); + if (ret != 0) + goto err; + break; + case snd_soc_dapm_hp: + case snd_soc_dapm_mic: + case snd_soc_dapm_line: + case snd_soc_dapm_spk: + list_add(&path->list, &codec->dapm_paths); + list_add(&path->list_sink, &wsink->sources); + list_add(&path->list_source, &wsource->sinks); + path->connect = 0; + return 0; + } + return 0; + +err: + printk(KERN_WARNING "asoc: no dapm match for %s --> %s --> %s\n", source, + control, sink); + kfree(path); + return ret; +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_connect_input); + +/** + * snd_soc_dapm_new_widgets - add new dapm widgets + * @codec: audio codec + * + * Checks the codec for any new dapm widgets and creates them if found. + * + * Returns 0 for success. + */ +int snd_soc_dapm_new_widgets(struct snd_soc_codec *codec) +{ + struct snd_soc_dapm_widget *w; + + mutex_lock(&codec->mutex); + list_for_each_entry(w, &codec->dapm_widgets, list) + { + if (w->new) + continue; + + switch(w->id) { + case snd_soc_dapm_switch: + case snd_soc_dapm_mixer: + dapm_new_mixer(codec, w); + break; + case snd_soc_dapm_mux: + dapm_new_mux(codec, w); + break; + case snd_soc_dapm_adc: + case snd_soc_dapm_dac: + case snd_soc_dapm_pga: + dapm_new_pga(codec, w); + break; + case snd_soc_dapm_input: + case snd_soc_dapm_output: + case snd_soc_dapm_micbias: + case snd_soc_dapm_spk: + case snd_soc_dapm_hp: + case snd_soc_dapm_mic: + case snd_soc_dapm_line: + case snd_soc_dapm_vmid: + case snd_soc_dapm_pre: + case snd_soc_dapm_post: + break; + } + w->new = 1; + } + + dapm_power_widgets(codec, SND_SOC_DAPM_STREAM_NOP); + mutex_unlock(&codec->mutex); + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_new_widgets); + +/** + * snd_soc_dapm_get_volsw - dapm mixer get callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to get the value of a dapm mixer control. + * + * Returns 0 for success. + */ +int snd_soc_dapm_get_volsw(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_dapm_widget *widget = snd_kcontrol_chip(kcontrol); + int reg = kcontrol->private_value & 0xff; + int shift = (kcontrol->private_value >> 8) & 0x0f; + int rshift = (kcontrol->private_value >> 12) & 0x0f; + int mask = (kcontrol->private_value >> 16) & 0xff; + int invert = (kcontrol->private_value >> 24) & 0x01; + + /* return the saved value if we are powered down */ + if (widget->id == snd_soc_dapm_pga && !widget->power) { + ucontrol->value.integer.value[0] = widget->saved_value; + return 0; + } + + ucontrol->value.integer.value[0] = + (snd_soc_read(widget->codec, reg) >> shift) & mask; + if (shift != rshift) + ucontrol->value.integer.value[1] = + (snd_soc_read(widget->codec, reg) >> rshift) & mask; + if (invert) { + ucontrol->value.integer.value[0] = + mask - ucontrol->value.integer.value[0]; + if (shift != rshift) + ucontrol->value.integer.value[1] = + mask - ucontrol->value.integer.value[1]; + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_get_volsw); + +/** + * snd_soc_dapm_put_volsw - dapm mixer set callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to set the value of a dapm mixer control. + * + * Returns 0 for success. + */ +int snd_soc_dapm_put_volsw(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_dapm_widget *widget = snd_kcontrol_chip(kcontrol); + int reg = kcontrol->private_value & 0xff; + int shift = (kcontrol->private_value >> 8) & 0x0f; + int rshift = (kcontrol->private_value >> 12) & 0x0f; + int mask = (kcontrol->private_value >> 16) & 0xff; + int invert = (kcontrol->private_value >> 24) & 0x01; + unsigned short val, val2, val_mask; + int ret; + + val = (ucontrol->value.integer.value[0] & mask); + + if (invert) + val = mask - val; + val_mask = mask << shift; + val = val << shift; + if (shift != rshift) { + val2 = (ucontrol->value.integer.value[1] & mask); + if (invert) + val2 = mask - val2; + val_mask |= mask << rshift; + val |= val2 << rshift; + } + + mutex_lock(&widget->codec->mutex); + widget->value = val; + + /* save volume value if the widget is powered down */ + if (widget->id == snd_soc_dapm_pga && !widget->power) { + widget->saved_value = val; + mutex_unlock(&widget->codec->mutex); + return 1; + } + + dapm_mixer_update_power(widget, kcontrol, reg, val_mask, val, invert); + if (widget->event) { + if (widget->event_flags & SND_SOC_DAPM_PRE_REG) { + ret = widget->event(widget, SND_SOC_DAPM_PRE_REG); + if (ret < 0) + goto out; + } + ret = snd_soc_update_bits(widget->codec, reg, val_mask, val); + if (widget->event_flags & SND_SOC_DAPM_POST_REG) + ret = widget->event(widget, SND_SOC_DAPM_POST_REG); + } else + ret = snd_soc_update_bits(widget->codec, reg, val_mask, val); + +out: + mutex_unlock(&widget->codec->mutex); + return ret; +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_put_volsw); + +/** + * snd_soc_dapm_get_enum_double - dapm enumerated double mixer get callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to get the value of a dapm enumerated double mixer control. + * + * Returns 0 for success. + */ +int snd_soc_dapm_get_enum_double(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_dapm_widget *widget = snd_kcontrol_chip(kcontrol); + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + unsigned short val, bitmask; + + for (bitmask = 1; bitmask < e->mask; bitmask <<= 1) + ; + val = snd_soc_read(widget->codec, e->reg); + ucontrol->value.enumerated.item[0] = (val >> e->shift_l) & (bitmask - 1); + if (e->shift_l != e->shift_r) + ucontrol->value.enumerated.item[1] = + (val >> e->shift_r) & (bitmask - 1); + + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_get_enum_double); + +/** + * snd_soc_dapm_put_enum_double - dapm enumerated double mixer set callback + * @kcontrol: mixer control + * @uinfo: control element information + * + * Callback to set the value of a dapm enumerated double mixer control. + * + * Returns 0 for success. + */ +int snd_soc_dapm_put_enum_double(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_dapm_widget *widget = snd_kcontrol_chip(kcontrol); + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + unsigned short val, mux; + unsigned short mask, bitmask; + int ret = 0; + + for (bitmask = 1; bitmask < e->mask; bitmask <<= 1) + ; + if (ucontrol->value.enumerated.item[0] > e->mask - 1) + return -EINVAL; + mux = ucontrol->value.enumerated.item[0]; + val = mux << e->shift_l; + mask = (bitmask - 1) << e->shift_l; + if (e->shift_l != e->shift_r) { + if (ucontrol->value.enumerated.item[1] > e->mask - 1) + return -EINVAL; + val |= ucontrol->value.enumerated.item[1] << e->shift_r; + mask |= (bitmask - 1) << e->shift_r; + } + + mutex_lock(&widget->codec->mutex); + widget->value = val; + dapm_mux_update_power(widget, kcontrol, mask, mux, e); + if (widget->event) { + if (widget->event_flags & SND_SOC_DAPM_PRE_REG) { + ret = widget->event(widget, SND_SOC_DAPM_PRE_REG); + if (ret < 0) + goto out; + } + ret = snd_soc_update_bits(widget->codec, e->reg, mask, val); + if (widget->event_flags & SND_SOC_DAPM_POST_REG) + ret = widget->event(widget, SND_SOC_DAPM_POST_REG); + } else + ret = snd_soc_update_bits(widget->codec, e->reg, mask, val); + +out: + mutex_unlock(&widget->codec->mutex); + return ret; +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_put_enum_double); + +/** + * snd_soc_dapm_new_control - create new dapm control + * @codec: audio codec + * @widget: widget template + * + * Creates a new dapm control based upon the template. + * + * Returns 0 for success else error. + */ +int snd_soc_dapm_new_control(struct snd_soc_codec *codec, + const struct snd_soc_dapm_widget *widget) +{ + struct snd_soc_dapm_widget *w; + + if ((w = dapm_cnew_widget(widget)) == NULL) + return -ENOMEM; + + w->codec = codec; + INIT_LIST_HEAD(&w->sources); + INIT_LIST_HEAD(&w->sinks); + INIT_LIST_HEAD(&w->list); + list_add(&w->list, &codec->dapm_widgets); + + /* machine layer set ups unconnected pins and insertions */ + w->connected = 1; + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_new_control); + +/** + * snd_soc_dapm_stream_event - send a stream event to the dapm core + * @codec: audio codec + * @stream: stream name + * @event: stream event + * + * Sends a stream event to the dapm core. The core then makes any + * necessary widget power changes. + * + * Returns 0 for success else error. + */ +int snd_soc_dapm_stream_event(struct snd_soc_codec *codec, + char *stream, int event) +{ + struct snd_soc_dapm_widget *w; + + if (stream == NULL) + return 0; + + mutex_lock(&codec->mutex); + list_for_each_entry(w, &codec->dapm_widgets, list) + { + if (!w->sname) + continue; + dbg("widget %s\n %s stream %s event %d\n", w->name, w->sname, + stream, event); + if (strstr(w->sname, stream)) { + switch(event) { + case SND_SOC_DAPM_STREAM_START: + w->active = 1; + break; + case SND_SOC_DAPM_STREAM_STOP: + w->active = 0; + break; + case SND_SOC_DAPM_STREAM_SUSPEND: + if (w->active) + w->suspend = 1; + w->active = 0; + break; + case SND_SOC_DAPM_STREAM_RESUME: + if (w->suspend) { + w->active = 1; + w->suspend = 0; + } + break; + case SND_SOC_DAPM_STREAM_PAUSE_PUSH: + break; + case SND_SOC_DAPM_STREAM_PAUSE_RELEASE: + break; + } + } + } + mutex_unlock(&codec->mutex); + + dapm_power_widgets(codec, event); + dump_dapm(codec, __FUNCTION__); + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_stream_event); + +/** + * snd_soc_dapm_set_endpoint - set audio endpoint status + * @codec: audio codec + * @endpoint: audio signal endpoint (or start point) + * @status: point status + * + * Set audio endpoint status - connected or disconnected. + * + * Returns 0 for success else error. + */ +int snd_soc_dapm_set_endpoint(struct snd_soc_codec *codec, + char *endpoint, int status) +{ + struct snd_soc_dapm_widget *w; + + list_for_each_entry(w, &codec->dapm_widgets, list) { + if (!strcmp(w->name, endpoint)) { + w->connected = status; + } + } + + return 0; +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_set_endpoint); + +/** + * snd_soc_dapm_free - free dapm resources + * @socdev: SoC device + * + * Free all dapm widgets and resources. + */ +void snd_soc_dapm_free(struct snd_soc_device *socdev) +{ + struct snd_soc_codec *codec = socdev->codec; + + snd_soc_dapm_sys_remove(socdev->dev); + dapm_free_widgets(codec); +} +EXPORT_SYMBOL_GPL(snd_soc_dapm_free); + +/* Module information */ +MODULE_AUTHOR("Liam Girdwood, liam.girdwood@wolfsonmicro.com, www.wolfsonmicro.com"); +MODULE_DESCRIPTION("Dynamic Audio Power Management core for ALSA SoC"); +MODULE_LICENSE("GPL"); |