/*
 * PXA2xx i2Sound: support for Intel PXA2xx I2S audio
 *
 * Copyright (c) 2004,2005 Giorgio Padrin <giorgio@mandarinlogiq.org>
 *
 * 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.
 *
 * History:
 *
 * 2005-03	Initial release; Audio engine ((c) 2004) derived from pxa-uda1380.c
 *		-- Giorgio Padrin
 * 2005-06	Power management -- Giorgio Padrin
 * 2005-10	Some finishing -- Giorgio Padrin
 */

#include "adriver.h"

#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/string.h>
#include <linux/delay.h>
#include <linux/pm.h>
#include <linux/errno.h>
#include <linux/kmod.h>
#include <linux/dma-mapping.h>
#include <linux/platform_device.h>

#include <asm/semaphore.h>
#include <asm/hardware.h>
#include <asm/dma.h>
#include <asm/system.h>

#include <asm/arch/pxa-regs.h>

#include "pxa2xx-i2sound.h"
#include <sound/pcm_params.h>

struct snd_pxa2xx_i2sound_engine snd_pxa2xx_i2sound_engine;

struct snd_pxa2xx_i2sound_stream {
	int id;				/* SNDRV_PCM_STREAM_(PLAYBACK|CAPTURE) */

	struct snd_pxa2xx_i2sound_engine *engine;

	int dma_ch;			/* DMA channel */

	dma_addr_t buffer;		/* buffer (physical address) */
	size_t buffer_size;

	size_t period_size;

	pxa_dma_desc *dma_dring;	/* DMA descriptors ring */
	char *dma_dring_store;		/* storage space for DMA descriptors ring */
	dma_addr_t dma_dring_p;		/* (physical address) */
	dma_addr_t dma_dring_store_p;
	size_t dma_dring_size;

	volatile int dma_running;	/* DMA running? */

  	struct snd_pcm_substream *asubs;	/* attached ALSA substream */

	struct snd_pcm_hardware ahw;		/* ALSA hw description */
};

struct snd_pxa2xx_i2sound_engine {
	unsigned int rate;			/* sample rate */
	unsigned int rate_in_use;
	spinlock_t rate_lock;

	struct snd_pxa2xx_i2sound_stream streams[2];
};

struct snd_pxa2xx_i2sound_i2slink {
	unsigned int usage_count;
	u32 sadiv;
	spinlock_t lock;
	int suspended;
};

struct snd_pxa2xx_i2sound_card {
	const char *name;

	struct snd_pxa2xx_i2sound_engine engine;

	struct snd_pxa2xx_i2sound_i2slink i2slink;

	struct snd_pxa2xx_i2sound_board *board;

	struct snd_card *acard;		/* ALSA card */
	struct snd_pcm *apcm;		/* ALSA pcm */

	struct platform_driver pdriver;
	struct platform_device pdev;
};

static struct snd_pxa2xx_i2sound_card card;

#define flag_stream(s)		(1 << (s)->id)

#define info_clock_from_pxa	((card.board->info & SND_PXA2xx_I2SOUND_INFO_CLOCK_FROM_PXA) ? 1 : 0)
#define info_can_capture	((card.board->info & SND_PXA2xx_I2SOUND_INFO_CAN_CAPTURE) ? 1 : 0)
#define info_half_duplex	((card.board->info & SND_PXA2xx_I2SOUND_INFO_HALF_DUPLEX) ? 1 : 0)

/* PXA 25x or 27x kind detection
   PXA21x and PXA26x belong to the PXA 25x kind  */
static inline int pxa_25x_kind(void)
{
	return ((read_cpuid(CPUID_ID) & 0x3f0) != 0x110);
}


/* begin {{ Audio Engine }} */

#define MAX_DMA_XFER_SIZE	4096
#define MAX_BUFFER_SIZE		65536

static int snd_pxa2xx_i2sound_ngn_init(void)
{
	spin_lock_init(&card.engine.rate_lock);

	/* preallocate buffer */
	return snd_pcm_lib_preallocate_pages_for_all(card.apcm, SNDRV_DMA_TYPE_DEV, NULL,
						     MAX_BUFFER_SIZE,
						     info_can_capture ? MAX_BUFFER_SIZE : 0);
}

static void snd_pxa2xx_i2sound_ngn_free(void)
{
	/* ALSA takes care of deallocate preallocated buffer */
}

/* Constraint: buffer_size integer multiple of period_size
* A better way to assure it?
*/
static unsigned int period_sizes[]
		= { 512, 1024, 2048, 4096, 8192, 16384, 32768 };
static unsigned int buffer_sizes[]
		= { 1024, 2048, 4096, 8192, 16384, 32768, 65536 };
static struct snd_pcm_hw_constraint_list cnstr_period_sizes = {
        	.count = ARRAY_SIZE(period_sizes),
        	.list = period_sizes,
	};
static struct snd_pcm_hw_constraint_list cnstr_buffer_sizes = {
        	.count = ARRAY_SIZE(buffer_sizes),
        	.list = buffer_sizes,
	};

static struct snd_pcm_hw_constraint_list cnstr_rates;

static void snd_pxa2xx_i2sound_ngn_free_buffer(struct snd_pxa2xx_i2sound_stream *s);

static int snd_pxa2xx_i2sound_ngn_init_buffer(struct snd_pxa2xx_i2sound_stream *s,
					      size_t buffer_size, size_t period_size)
{
	int ret;

	unsigned int dma_dpp; /* dma descriptors per period */
	unsigned int periods;
	unsigned int i, j, di;
	dma_addr_t p, d_buf;
	size_t d_size = sizeof(pxa_dma_desc);

	size_t dma_xfer_size = (period_size >= MAX_DMA_XFER_SIZE) ?
			       MAX_DMA_XFER_SIZE : period_size;
	dma_dpp = (period_size - 1) / dma_xfer_size + 1;
	periods = buffer_size / period_size;

	/* free old buffer in case */
	if (s->buffer)
		snd_pxa2xx_i2sound_ngn_free_buffer(s);

	/* allocate buffer */
	ret = snd_pcm_lib_malloc_pages(s->asubs, buffer_size);
	if (ret < 0) return ret;

	s->buffer = s->asubs->runtime->dma_addr;

	/* ---- allocate and setup DMA descriptors ring ---- */

	/* allocate storage space for descriptors ring (alignment issue) */
	s->dma_dring_size = periods * dma_dpp * d_size;
	s->dma_dring_store = dma_alloc_coherent(NULL, s->dma_dring_size + 15,
		 				&s->dma_dring_store_p, GFP_KERNEL);
	if (!s->dma_dring_store) {
		s->dma_dring_size = 0;
		snd_pxa2xx_i2sound_ngn_free_buffer(s);
		return -ENOMEM;
	}

	/* setup the descriptors ring pointer, aligned on a 16 bytes boundary */
	s->dma_dring = ((dma_addr_t) s->dma_dring_store & 0xf) ?
			 (pxa_dma_desc *) (((dma_addr_t) s->dma_dring_store & ~0xf) + 16) :
			 (pxa_dma_desc *) s->dma_dring_store;
	s->dma_dring_p = s->dma_dring_store_p +
			  (dma_addr_t) s->dma_dring - (dma_addr_t) s->dma_dring_store;

	/* fill the descriptors ring */
	di = 0; /* current descriptor index */
	p = s->buffer; /* p: current period (address in buffer) */
	 /* iterate over periods */
	for (i = 0; i < periods; i++, p += period_size) {
		d_buf = p; /* d_buf: address in buffer for current dma descriptor */
		/* iterate over dma descriptors in a period */
		for (j = 0; j < dma_dpp; j++, di++, d_buf += dma_xfer_size) {
			/* link to next descriptor */
			s->dma_dring[di].ddadr = s->dma_dring_p + (di + 1) * d_size;
			if (s->id == SNDRV_PCM_STREAM_PLAYBACK) {
				s->dma_dring[di].dsadr = d_buf;
				s->dma_dring[di].dtadr = __PREG(SADR);
				s->dma_dring[di].dcmd = DCMD_INCSRCADDR | DCMD_FLOWTRG |
							DCMD_BURST32 | DCMD_WIDTH4;
			}
			else {
				s->dma_dring[di].dsadr = __PREG(SADR);
				s->dma_dring[di].dtadr = d_buf;
				s->dma_dring[di].dcmd =	DCMD_INCTRGADDR | DCMD_FLOWSRC |
							DCMD_BURST32 | DCMD_WIDTH4;
			}
			s->dma_dring[di].dcmd |=
			  ((p + period_size - d_buf) >= dma_xfer_size) ?
			  dma_xfer_size : p + period_size - d_buf; /* transfer length */
		}
		s->dma_dring[di - 1].dcmd |= DCMD_ENDIRQEN; /* period irq */
	}
	s->dma_dring[di - 1].ddadr = s->dma_dring_p; /* close the ring */

	return 0;
}

static void snd_pxa2xx_i2sound_ngn_free_buffer(struct snd_pxa2xx_i2sound_stream *s)
{
	if (s->dma_dring_store) {
		dma_free_coherent(NULL, s->dma_dring_size + 15,
				  s->dma_dring_store, s->dma_dring_store_p);
		s->dma_dring_store = NULL;
		s->dma_dring = NULL;
		s->dma_dring_store_p = 0;
		s->dma_dring_p = 0;
		s->dma_dring_size = 0;
	}
	if (s->buffer) {
		snd_pcm_lib_free_pages(s->asubs);
		s->buffer = 0;
	}
}

static int snd_pxa2xx_i2sound_ngn_set_rate(unsigned int rate);

static int snd_pxa2xx_i2sound_ngn_request_rate(struct snd_pxa2xx_i2sound_stream *s,
					       unsigned int rate)
{
	spin_lock(&card.engine.rate_lock);

	card.engine.rate_in_use &= ~flag_stream(s);
	if (rate != card.engine.rate) {
		if (!card.engine.rate_in_use) {
			if (snd_pxa2xx_i2sound_ngn_set_rate(rate) == 0)
				card.engine.rate = rate;
			else goto error;
		}
		else goto error;
	}
	card.engine.rate_in_use |= flag_stream(s);
	spin_unlock(&card.engine.rate_lock);
	return 0;

 error:	spin_unlock(&card.engine.rate_lock);
	return -EINVAL;
}

static void snd_pxa2xx_i2sound_ngn_free_rate(struct snd_pxa2xx_i2sound_stream *s)
{
	spin_lock(&card.engine.rate_lock);
	card.engine.rate_in_use &= ~flag_stream(s);
	spin_unlock(&card.engine.rate_lock);
}

static int snd_pxa2xx_i2sound_i2slink_set_rate_pxa(unsigned int rate);

static int snd_pxa2xx_i2sound_ngn_set_rate(unsigned int rate)
{
	if (info_clock_from_pxa)
		if (snd_pxa2xx_i2sound_i2slink_set_rate_pxa(rate) != 0)
			return -EINVAL;
	if (card.board->set_rate != NULL)
		if (card.board->set_rate(rate) != 0)
			return -EINVAL;
	return 0;
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 19)
static void snd_pxa2xx_i2sound_ngn_dma_irq(int ch, void *data)
#else
static void snd_pxa2xx_i2sound_ngn_dma_irq(int ch, void *data, struct pt_regs *regs)
#endif
{
	struct snd_pxa2xx_i2sound_stream *s = (struct snd_pxa2xx_i2sound_stream*) data;
	u32 dcsr;

	dcsr = DCSR(ch);
	DCSR(ch) = dcsr & ~DCSR_STOPIRQEN;

	if (dcsr & DCSR_BUSERR) {
		snd_printk(KERN_ERR "%s: bus error\n", __FUNCTION__);
		return;
	}

	if (dcsr & DCSR_ENDINTR)
		return snd_pcm_period_elapsed(s->asubs);
}

static inline void snd_pxa2xx_i2sound_ngn_map_dma_reqs(struct snd_pxa2xx_i2sound_stream *s)
{
	if (s->id == SNDRV_PCM_STREAM_PLAYBACK)
		DRCMRTXSADR = s->dma_ch | DRCMR_MAPVLD;
	else
		DRCMRRXSADR = s->dma_ch | DRCMR_MAPVLD;
}

static inline void snd_pxa2xx_i2sound_ngn_unmap_dma_reqs(struct snd_pxa2xx_i2sound_stream *s)
{
	if (s->id == SNDRV_PCM_STREAM_PLAYBACK)
		DRCMRTXSADR = 0;
	else
		DRCMRRXSADR = 0;
}

static int snd_pxa2xx_i2sound_ngn_request_dma_channel(struct snd_pxa2xx_i2sound_stream *s)
{
	int ret;
	char *name = (s->id == SNDRV_PCM_STREAM_PLAYBACK) ?
		     "i2s out" :
		     "i2s in";

	ret = pxa_request_dma(name, DMA_PRIO_LOW, snd_pxa2xx_i2sound_ngn_dma_irq, s);
	if (ret < 0) return ret;
	s->dma_ch = ret;
	snd_pxa2xx_i2sound_ngn_map_dma_reqs(s);
	return 0;
}

static void snd_pxa2xx_i2sound_ngn_free_dma_channel(struct snd_pxa2xx_i2sound_stream *s)
{
	snd_pxa2xx_i2sound_ngn_unmap_dma_reqs(s);
	pxa_free_dma(s->dma_ch);
}

static int snd_pxa2xx_i2sound_ngn_attach_asubs(struct snd_pcm_substream *asubs)
{
	struct snd_pxa2xx_i2sound_stream *s = &card.engine.streams[asubs->stream];
	int ret;

	asubs->private_data = (void*) s;
	s->asubs = asubs;

	if ((ret = snd_pxa2xx_i2sound_ngn_request_dma_channel(s)) < 0) return ret;

	/* ALSA hw description */
	asubs->runtime->hw = s->ahw;

	/* ---- constraints on hw params space ---- */

	snd_pcm_hw_constraint_list(asubs->runtime, 0, SNDRV_PCM_HW_PARAM_PERIOD_BYTES,
				   		      &cnstr_period_sizes);
	snd_pcm_hw_constraint_list(asubs->runtime, 0, SNDRV_PCM_HW_PARAM_BUFFER_BYTES,
						      &cnstr_buffer_sizes);
	if (!info_clock_from_pxa && card.board->streams_hw[s->id].rates_table != NULL) {
		/* rates constraint */
		cnstr_rates.count = card.board->streams_hw[s->id].rates_num;
        	cnstr_rates.list = card.board->streams_hw[s->id].rates_table;
		snd_pcm_hw_constraint_list(asubs->runtime, 0, SNDRV_PCM_HW_PARAM_RATE,
				   		      &cnstr_rates);
	}

	/* more board specific constraints? */
	if (card.board->add_hw_constraints)
		card.board->add_hw_constraints(asubs);

	return 0;
}

static void snd_pxa2xx_i2sound_ngn_detach_asubs(struct snd_pcm_substream *asubs)
{
	struct snd_pxa2xx_i2sound_stream *s = &card.engine.streams[asubs->stream];

	snd_pxa2xx_i2sound_ngn_free_dma_channel(s);

	asubs->private_data = NULL;
	s->asubs = NULL;
}

static int snd_pxa2xx_i2sound_ngn_on(struct snd_pxa2xx_i2sound_stream *s, unsigned int rate,
				  size_t buffer_size, size_t period_size)
{
	int ret;

	/* request sample rate */
	ret = snd_pxa2xx_i2sound_ngn_request_rate(s, rate);
	if (ret) return ret;

	/* init buffer */
	ret = snd_pxa2xx_i2sound_ngn_init_buffer(s, buffer_size, period_size);
	if (ret) {
		snd_pxa2xx_i2sound_ngn_free_rate(s);
		return ret;
	}

	s->buffer_size = buffer_size;
	s->period_size = period_size;

	return 0;
}

static void snd_pxa2xx_i2sound_ngn_off(struct snd_pxa2xx_i2sound_stream *s)
{
	snd_pxa2xx_i2sound_ngn_free_rate(s);
	snd_pxa2xx_i2sound_ngn_free_buffer(s);
	s->buffer_size = 0;
	s->period_size = 0;
}

static int snd_pxa2xx_i2sound_ngn_start(struct snd_pxa2xx_i2sound_stream *s)
{
	DDADR(s->dma_ch) = s->dma_dring_p;
	DCSR(s->dma_ch) = DCSR_RUN;
	s->dma_running = 1;
	return 0;
}

static int snd_pxa2xx_i2sound_ngn_stop(struct snd_pxa2xx_i2sound_stream *s)
{
	unsigned int timeout = 100; /* safety timeout */

	DCSR(s->dma_ch) = DCSR_STOPIRQEN;

	while (!(DCSR(s->dma_ch) & DCSR_STOPSTATE) && timeout > 0) {
		 udelay(10);
		 timeout--;
	}

	s->dma_running = 0;
	return 0;
}

static u32 snd_pxa2xx_i2sound_ngn_pointer(struct snd_pxa2xx_i2sound_stream *s)
{
	return ((s->id == SNDRV_PCM_STREAM_PLAYBACK) ?
	      DSADR(s->dma_ch) : DTADR(s->dma_ch))
	      - s->buffer;
}

#ifdef CONFIG_PM
static int snd_pxa2xx_i2sound_ngn_suspend(struct snd_pxa2xx_i2sound_stream *s)
{
	if (s->asubs) snd_pxa2xx_i2sound_ngn_unmap_dma_reqs(s);
	return 0;
}

static int snd_pxa2xx_i2sound_ngn_resume(struct snd_pxa2xx_i2sound_stream *s)
{
	if (s->asubs) snd_pxa2xx_i2sound_ngn_map_dma_reqs(s);
	return 0;
}
#endif

/* end {{ Audio Engine }} */


/* begin {{ I2SLink }} */

static void snd_pxa2xx_i2sound_i2slink_init(void)
{
	spin_lock_init(&card.i2slink.lock);
	card.i2slink.sadiv = 0xd; /* rate 44.1 kHz */;
}

static void snd_pxa2xx_i2sound_i2slink_on(void)
{
	if (info_clock_from_pxa) {
		pxa_gpio_mode(GPIO28_BITCLK_OUT_I2S_MD);
		if (pxa_25x_kind()) pxa_gpio_mode(GPIO32_SYSCLK_I2S_MD);
		else pxa_gpio_mode(GPIO113_I2S_SYSCLK_MD);
	}
	else {
		pxa_gpio_mode(GPIO28_BITCLK_IN_I2S_MD);
	}
	pxa_gpio_mode(GPIO30_SDATA_OUT_I2S_MD);
	if (info_can_capture) pxa_gpio_mode(GPIO29_SDATA_IN_I2S_MD);
	pxa_gpio_mode(GPIO31_SYNC_I2S_MD);

	pxa_set_cken(CKEN8_I2S, 1);
	SACR0 = SACR0_RST;
	if (info_clock_from_pxa) {
		SADIV = card.i2slink.sadiv; /* rate 44.1 kHz */
		SACR0 = SACR0_ENB | SACR0_BCKD | SACR0_RFTH(8) | SACR0_TFTH(8);
	}
	else {
		SACR0 = SACR0_ENB | SACR0_RFTH(8) | SACR0_TFTH(8);
	}
}

static void snd_pxa2xx_i2sound_i2slink_off(void)
{
	SACR0 = SACR0_RST;
	pxa_set_cken(CKEN8_I2S, 0);

	if (info_clock_from_pxa) {
		pxa_gpio_mode(GPIO28_BITCLK | GPIO_OUT | GPIO_DFLT_LOW);
		if (pxa_25x_kind()) pxa_gpio_mode(GPIO32_SYSCLK | GPIO_OUT | GPIO_DFLT_LOW);
		else pxa_gpio_mode(GPIO113_I2S_SYSCLK | GPIO_OUT | GPIO_DFLT_LOW);
	}
	else {
		pxa_gpio_mode(GPIO28_BITCLK | GPIO_IN);
	}
	pxa_gpio_mode(GPIO30_SDATA_OUT | GPIO_OUT | GPIO_DFLT_LOW);
	if (info_can_capture) pxa_gpio_mode(GPIO29_SDATA_IN | GPIO_IN);
	pxa_gpio_mode(GPIO31_SYNC | GPIO_OUT | GPIO_DFLT_LOW);
}

void snd_pxa2xx_i2sound_i2slink_get(void)
{
	spin_lock(&card.i2slink.lock);
	if (card.i2slink.usage_count++ == 0 &&
	    !card.i2slink.suspended)
		snd_pxa2xx_i2sound_i2slink_on();
	spin_unlock(&card.i2slink.lock);
}

void snd_pxa2xx_i2sound_i2slink_free(void)
{
	spin_lock(&card.i2slink.lock);
	if (--card.i2slink.usage_count == 0 &&
	    !card.i2slink.suspended)
		snd_pxa2xx_i2sound_i2slink_off();
	spin_unlock(&card.i2slink.lock);
}

#ifdef CONFIG_PM
void snd_pxa2xx_i2sound_i2slink_suspend(void)
{
	spin_lock(&card.i2slink.lock);
	card.i2slink.suspended = 1;
	if (card.i2slink.usage_count == 0)
		snd_pxa2xx_i2sound_i2slink_off();
	spin_unlock(&card.i2slink.lock);
}

void snd_pxa2xx_i2sound_i2slink_resume(void)
{
	spin_lock(&card.i2slink.lock);
	if (card.i2slink.usage_count > 0)
		snd_pxa2xx_i2sound_i2slink_on();
	card.i2slink.suspended = 0;
	spin_unlock(&card.i2slink.lock);
}
#endif

static int snd_pxa2xx_i2sound_i2slink_set_rate_pxa(unsigned int rate)
{
	switch (rate) {
	case 8000: case 11025: case 16000: case 22050: case 44100: case 48000:
		spin_lock(&card.i2slink.lock);
		SADIV = card.i2slink.sadiv = 576000 / rate;
		spin_unlock(&card.i2slink.lock);
		return 0;
	default: return -EINVAL;
	}
}

/* end {{ I2SLink }} */


/* begin {{ ALSA PCM Ops }} */

static int snd_pxa2xx_i2sound_pcm_hw_params(struct snd_pcm_substream *asubs,
					    struct snd_pcm_hw_params * ahw_params)
{
	return snd_pxa2xx_i2sound_ngn_on((struct snd_pxa2xx_i2sound_stream*) asubs->private_data,
					 params_rate(ahw_params),
					 params_buffer_bytes(ahw_params),
					 params_period_bytes(ahw_params));
}

static int snd_pxa2xx_i2sound_pcm_hw_free(struct snd_pcm_substream *asubs)
{
	snd_pxa2xx_i2sound_ngn_off((struct snd_pxa2xx_i2sound_stream*) asubs->private_data);
	return 0;
}

static int snd_pxa2xx_i2sound_pcm_prepare(struct snd_pcm_substream *asubs)
{
	return 0;
}

static int snd_pxa2xx_i2sound_pcm_trigger(struct snd_pcm_substream *asubs, int cmd)
{
	struct snd_pxa2xx_i2sound_stream* s =
		(struct snd_pxa2xx_i2sound_stream*) asubs->private_data;
	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
		snd_pxa2xx_i2sound_ngn_start(s);
		break;
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_SUSPEND:
		snd_pxa2xx_i2sound_ngn_stop(s);
		break;
	default:
		return -EINVAL;
		break;
	}
	return 0;
}

static snd_pcm_uframes_t snd_pxa2xx_i2sound_pcm_pointer(struct snd_pcm_substream *asubs)
{
	struct snd_pxa2xx_i2sound_stream* s = 
		(struct snd_pxa2xx_i2sound_stream*) asubs->private_data;
	return bytes_to_frames(asubs->runtime,
			       snd_pxa2xx_i2sound_ngn_pointer(s));
}

static int snd_pxa2xx_i2sound_pcm_open(struct snd_pcm_substream *asubs)
{
	int ret;
	ret = snd_pxa2xx_i2sound_ngn_attach_asubs(asubs);
	if (ret == 0) {
		snd_pxa2xx_i2sound_i2slink_get();
		card.board->open_stream(asubs->stream);
	}
	return ret;
}

static int snd_pxa2xx_i2sound_pcm_close(struct snd_pcm_substream *asubs)
{
	card.board->close_stream(asubs->stream);
	snd_pxa2xx_i2sound_i2slink_free();
	snd_pxa2xx_i2sound_ngn_detach_asubs(asubs);
	return 0;
}

static struct snd_pcm_ops snd_pxa2xx_i2sound_pcm_ops = {
	.open		= snd_pxa2xx_i2sound_pcm_open,
	.close		= snd_pxa2xx_i2sound_pcm_close,
	.ioctl		= snd_pcm_lib_ioctl,
	.hw_params	= snd_pxa2xx_i2sound_pcm_hw_params,
	.hw_free	= snd_pxa2xx_i2sound_pcm_hw_free,
	.prepare	= snd_pxa2xx_i2sound_pcm_prepare,
	.trigger	= snd_pxa2xx_i2sound_pcm_trigger,
	.pointer	= snd_pxa2xx_i2sound_pcm_pointer
};

/* end {{ ALSA PCM Ops }} */


/* begin {{ Platform Device }} */

#ifdef CONFIG_PM
static int snd_pxa2xx_i2sound_card_suspend(struct snd_card *acard, pm_message_t state);
static int snd_pxa2xx_i2sound_card_resume(struct snd_card *acard);

static int snd_pxa2xx_i2sound_pdev_suspend(struct platform_device *pdev, pm_message_t state)
{
	return snd_pxa2xx_i2sound_card_suspend(card.acard, state);
}

static int snd_pxa2xx_i2sound_pdev_resume(struct platform_device *pdev)
{
	return snd_pxa2xx_i2sound_card_resume(card.acard);
}
#endif

static void snd_pxa2xx_i2sound_pdev_device_release(struct device *dev) {}

static int snd_pxa2xx_i2sound_pdev_register(void)
{
	int ret = 0;

	/* platform driver */
	card.pdriver.driver.name = card.name;
#ifdef CONFIG_PM
	card.pdriver.suspend = snd_pxa2xx_i2sound_pdev_suspend;
	card.pdriver.resume = snd_pxa2xx_i2sound_pdev_resume;
#endif
	if ((ret = platform_driver_register(&card.pdriver))) return ret;

	/* platform device */
	card.pdev.name = card.name;
	card.pdev.id = -1;
	card.pdev.dev.release = snd_pxa2xx_i2sound_pdev_device_release;
	if ((ret = platform_device_register(&card.pdev)))
		platform_driver_unregister(&card.pdriver);
	return ret;
}

static void snd_pxa2xx_i2sound_pdev_unregister(void)
{
	platform_device_unregister(&card.pdev);
	platform_driver_unregister(&card.pdriver);
}

/* end {{ Platform Device }} */


/* begin {{ Soundcard }} */

static void snd_pxa2xx_i2sound_card_free_pcm(struct snd_pcm * apcm);

static int snd_pxa2xx_i2sound_card_create_pcm(void)
{
	int ret;
	int s_id;
	struct snd_pxa2xx_i2sound_stream* s;
	struct snd_pxa2xx_i2sound_board_stream_hw* board_s_hw;

	ret = snd_pcm_new(card.acard, card.board->acard_id, 0,
			  1,
			  (info_can_capture) ? 1 : 0,
			  &card.apcm);
	if (ret < 0) return ret;

	ret = snd_pxa2xx_i2sound_ngn_init();
	if (ret < 0)	return ret;

	card.apcm->info_flags = info_half_duplex ?
				       SNDRV_PCM_INFO_HALF_DUPLEX :
				       SNDRV_PCM_INFO_JOINT_DUPLEX;
	strlcpy(card.apcm->name, card.name, sizeof(card.apcm->name));

	for (s_id = 0; s_id < 2; s_id++) {
		if (s_id == SNDRV_PCM_STREAM_CAPTURE && !info_can_capture)
			continue;
		s = &card.engine.streams[s_id];
		s->id = s_id;
		s->ahw.info		= SNDRV_PCM_INFO_INTERLEAVED |
					  SNDRV_PCM_INFO_BLOCK_TRANSFER;
		s->ahw.formats		= SNDRV_PCM_FMTBIT_S16_LE;
		s->ahw.channels_min		= 2;
		s->ahw.channels_max		= 2;
		s->ahw.buffer_bytes_max	= MAX_BUFFER_SIZE;
		s->ahw.period_bytes_min	= 512;
		s->ahw.period_bytes_max	= MAX_BUFFER_SIZE/2;
		s->ahw.periods_min	= 2;
		s->ahw.periods_max	= MAX_BUFFER_SIZE/512;
		if (!info_clock_from_pxa){
			board_s_hw = &card.board->streams_hw[s->id];
			s->ahw.rates 	= board_s_hw->rates;
			s->ahw.rate_min	= board_s_hw->rate_min;
			s->ahw.rate_max	= board_s_hw->rate_max;
		}
		else {
			s->ahw.rates = SNDRV_PCM_RATE_8000  | SNDRV_PCM_RATE_16000 |
				       SNDRV_PCM_RATE_22050 | SNDRV_PCM_RATE_44100 |
				       SNDRV_PCM_RATE_48000;
			s->ahw.rate_min	=  8000;
			s->ahw.rate_max = 48000;
		}
		snd_pcm_set_ops(card.apcm, s->id, &snd_pxa2xx_i2sound_pcm_ops);
	}

	card.apcm->private_free = snd_pxa2xx_i2sound_card_free_pcm;

	return 0;
}

static void snd_pxa2xx_i2sound_card_free_pcm(struct snd_pcm * apcm)
{
	int s_id;
	struct snd_pxa2xx_i2sound_stream* s;

	for (s_id = 0; s_id < 2; s_id++) {
		if (s_id == SNDRV_PCM_STREAM_CAPTURE && !info_can_capture)
			continue;
		s = &card.engine.streams[s_id];
		memset(s, 0, sizeof(struct snd_pxa2xx_i2sound_stream));
	}
	snd_pxa2xx_i2sound_ngn_free();
	card.apcm = NULL;
}

static int snd_pxa2xx_i2sound_board_activate(void)
{
	int ret = 0;

	if (card.board->activate) {
		if (info_clock_from_pxa) snd_pxa2xx_i2sound_i2slink_get();
		ret = card.board->activate();
		if (info_clock_from_pxa) snd_pxa2xx_i2sound_i2slink_free();
	}
	return ret;
}

static void snd_pxa2xx_i2sound_board_deactivate(void)
{
	if (card.board->deactivate) {
		if (info_clock_from_pxa) snd_pxa2xx_i2sound_i2slink_get();
		card.board->deactivate();
		if (info_clock_from_pxa) snd_pxa2xx_i2sound_i2slink_free();
	}
}

#ifdef CONFIG_PM
static int snd_pxa2xx_i2sound_card_suspend(struct snd_card *acard, pm_message_t state)
{
	int s_id;
	
	snd_power_change_state(card.acard, SNDRV_CTL_POWER_D3hot);

	snd_pcm_suspend_all(card.apcm);

	for (s_id = 0; s_id < 2; s_id++) {
		if (s_id == SNDRV_PCM_STREAM_CAPTURE && !info_can_capture)
			continue;
		snd_pxa2xx_i2sound_ngn_suspend(&card.engine.streams[s_id]);
	}

	if (info_clock_from_pxa) snd_pxa2xx_i2sound_i2slink_get();
	card.board->suspend(state);
	if (info_clock_from_pxa) snd_pxa2xx_i2sound_i2slink_free();

	snd_pxa2xx_i2sound_i2slink_suspend();

	return 0;
}

static int snd_pxa2xx_i2sound_card_resume(struct snd_card *acard)
{
	int s_id;

	snd_pxa2xx_i2sound_i2slink_resume();

	if (info_clock_from_pxa) snd_pxa2xx_i2sound_i2slink_get();
	card.board->resume();
	if (info_clock_from_pxa) snd_pxa2xx_i2sound_i2slink_free();

	for (s_id = 0; s_id < 2; s_id++) {
		if (s_id == SNDRV_PCM_STREAM_CAPTURE && !info_can_capture)
			continue;
		snd_pxa2xx_i2sound_ngn_resume(&card.engine.streams[s_id]);
	}

	snd_power_change_state(card.acard, SNDRV_CTL_POWER_D0);
	
	return 0;
}
#endif

int snd_pxa2xx_i2sound_card_activate(struct snd_pxa2xx_i2sound_board *board)
{
	int ret = 0;

	if (board == NULL) return -EINVAL;
	card.board = board;
	card.name = card.board->name;

	snd_pxa2xx_i2sound_i2slink_init();

	if ((ret = snd_pxa2xx_i2sound_board_activate()) < 0) goto failed_activate_board;

	/* new ALSA card */
	ret = snd_card_create(-1, card.board->acard_id, THIS_MODULE, 0,
			      &card.acard);
	if (ret < 0)
		goto failed_new_acard;

	strlcpy(card.acard->driver, card.board->acard_id, sizeof(card.acard->driver));
	strlcpy(card.acard->shortname, card.name, sizeof(card.acard->shortname));
	strlcpy(card.acard->longname, card.board->desc, sizeof(card.acard->longname));

	/* mixer */
	if ((ret = board->add_mixer_controls(card.acard)) < 0)
		goto failed_create_acard;

	/* PCM */
	if ((ret = snd_pxa2xx_i2sound_card_create_pcm()) < 0) goto failed_create_acard;

	/* register the card */
        if ((ret = snd_card_register(card.acard)) < 0) goto failed_create_acard;

	if ((ret = snd_pxa2xx_i2sound_pdev_register()) < 0)
		goto failed_register_pdev;

	snd_printk(KERN_INFO "PXA2xx i2Sound: %s activated\n", card.name);

	return 0;

 failed_register_pdev:
 failed_create_acard:
	snd_card_free(card.acard);
	card.acard = NULL;
 failed_new_acard:
	snd_pxa2xx_i2sound_board_deactivate();
 failed_activate_board:
	snd_pxa2xx_i2sound_i2slink_free();
	card.board = NULL;
	return ret;
}

void snd_pxa2xx_i2sound_card_deactivate(void)
{
	snd_pxa2xx_i2sound_pdev_unregister();
	snd_card_free(card.acard);
	card.acard = NULL;
	snd_pxa2xx_i2sound_board_deactivate();
	snd_pxa2xx_i2sound_i2slink_free();
	card.board = NULL;
	snd_printk(KERN_INFO "PXA2xx i2Sound: %s deactivated\n", card.name);
}

/* end {{ Soundcard }} */


/* begin {{ Module }} */

EXPORT_SYMBOL(snd_pxa2xx_i2sound_card_activate);
EXPORT_SYMBOL(snd_pxa2xx_i2sound_card_deactivate);
EXPORT_SYMBOL(snd_pxa2xx_i2sound_i2slink_get);
EXPORT_SYMBOL(snd_pxa2xx_i2sound_i2slink_free);

static int __init snd_pxa_i2Sound_module_on_load(void) { return 0; }
static void __exit snd_pxa_i2Sound_module_on_unload(void) {}
module_init(snd_pxa_i2Sound_module_on_load);
module_exit(snd_pxa_i2Sound_module_on_unload);

MODULE_AUTHOR("Giorgio Padrin");
MODULE_DESCRIPTION("PXA2xx i2Sound: support for Intel PXA2xx I2S audio");
MODULE_LICENSE("GPL");

/* end {{ Module }} */
