/* sound/arm/s3c24xx-iis.c
 *
 * (c) 2004-2005 Simtec Electronics
 *	http://armlinux.simtec.co.uk/
 *	Ben Dooks <ben@simtec.co.uk>
 *
 * S3C24XX ALSA IIS audio driver core
 *
 * 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.
*/

/* Notes:
 *   The I2S pins that are needed (SCLK,LRCLK,SDO,SDI) are always
 *   configured when the I2S system is active. SDO and SDI are
 *   always configured even if the system is not open for both
 *   capture and playback. This ensures that if the DAC is
 *   not muted, then the output line is held in place, reducing
 *   the chance of any random data being picked up. CDCLK is
 *   only selected when the system is in master mode.
 *
 *   When the I2S unit is not being used, all pins are set to
 *   input, so either the internal resistors need to be configured
 *   or the lines need to have external pull-up or pull-downs.
*/

#include <linux/config.h>
#include <sound/driver.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/ioctl.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/pm.h>
#include <linux/dma-mapping.h>

#include <asm/hardware.h>
#include <asm/dma.h>
#include <asm/io.h>

#include <asm/hardware/clock.h>

#include <sound/driver.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/initval.h>

#include <asm/arch/regs-iis.h>
#include <asm/arch/regs-gpio.h>
#include <asm/arch/audio.h>

#include "s3c24xx-iis.h"


/* debugging macros */

#if 0
#define DBG(x...)
#else
#define DBG(x...) do { printk(KERN_DEBUG x); } while(0)
#endif

#if 1
#define DBG_DMA(x...)
#else
#define DBG_DMA(x...) do { printk(KERN_DEBUG x); } while(0)
#endif

static s3c24xx_card_t *our_card;

/* s3c24xx_snd_capture_hw
 *
 * the default initialiser for the capture hardware stream,
 * not to be altered
*/

static snd_pcm_hardware_t s3c24xx_snd_hw = {
	.info			= ( SNDRV_PCM_INFO_INTERLEAVED |
				    SNDRV_PCM_INFO_MMAP | 
				    SNDRV_PCM_INFO_MMAP_VALID |
				    SNDRV_PCM_INFO_PAUSE |
				    SNDRV_PCM_INFO_RESUME ),
	.formats		= ( SNDRV_PCM_FMTBIT_S16_LE |
				    SNDRV_PCM_FMTBIT_U16_LE |
				    SNDRV_PCM_FMTBIT_U8 |
				    SNDRV_PCM_FMTBIT_S8 ),
	.rates			= SNDRV_PCM_RATE_44100,
	.rate_min		= 8000,
	.rate_max		= 48000,
	.channels_min		= 2,
	.channels_max		= 2,
	.buffer_bytes_max	= 64*1024,
	.period_bytes_min	= 4,
	.period_bytes_max	= PAGE_SIZE,
	.periods_min		= 2,
	.periods_max		= 128,
	.fifo_size		= 32,
};



static s3c2410_dma_client_t s3c24xx_snd_playback_client = {
	.name		= "S3C24XX Audio Playback",
};

static s3c2410_dma_client_t s3c24xx_snd_capture_client = {
	.name		= "S3C24XX Audio Capture",
};

/* debug helpers */

static void s3c24xx_snd_showregs(s3c24xx_card_t *card)
{
	printk(KERN_DEBUG "IIS: CON=%08x, MOD=%08x, PSR=%08x, FCON=%08x\n",
	       readl(card->regs + S3C2410_IISCON),
	       readl(card->regs + S3C2410_IISMOD),
	       readl(card->regs + S3C2410_IISPSR),
	       readl(card->regs + S3C2410_IISFCON));
}

static void s3c24xx_snd_showdma(s3c24xx_runtime_t *or)
{
	dma_addr_t src, dst;

	s3c2410_dma_getposition(or->dma_ch, &src, &dst);
	DBG("dma position: src=0x%lx, dst=0x%lx\n", (long)src, (long)dst);
}

/* conversion */

static struct s3c24xx_platdata_iis *to_platdata(s3c24xx_card_t *card)
{
	return (card->device == NULL) ? NULL : card->device->platform_data;
}

/* helpers */

static inline int s3c24xx_snd_is_clkmaster(s3c24xx_card_t *chip)
{
	return (readl(chip->regs + S3C2410_IISMOD) & S3C2410_IISMOD_SLAVE) ? 0:1;
}

/* audio hardware control */

static void s3c24xx_snd_txctrl(s3c24xx_card_t *card, int on)
{
	unsigned long iisfcon;
	unsigned long iiscon;
	unsigned long iismod;

	iisfcon = readl(card->regs + S3C2410_IISFCON);
	iiscon  = readl(card->regs + S3C2410_IISCON);
	iismod  = readl(card->regs + S3C2410_IISMOD);

	if (on) {
		iisfcon |= S3C2410_IISFCON_TXDMA | S3C2410_IISFCON_TXENABLE;
		iiscon  |= S3C2410_IISCON_TXDMAEN | S3C2410_IISCON_IISEN;
		iiscon  &= ~S3C2410_IISCON_TXIDLE;
		iismod  |= S3C2410_IISMOD_TXMODE;
		iismod  |= S3C2410_IISMOD_RXMODE;

		writel(iismod,  card->regs + S3C2410_IISMOD);
		writel(iisfcon, card->regs + S3C2410_IISFCON);
		writel(iiscon,  card->regs + S3C2410_IISCON);
	} else {
		/* note, we have to disable the FIFOs otherwise bad things
		 * seem to happen when the DMA stops. According to the 
		 * Samsung supplied kernel, this should allow the DMA 
		 * engine and FIFOs to reset. If this isn't allowed, the
		 * DMA engine will simply freeze randomly.
                 */

		iisfcon &= ~S3C2410_IISFCON_TXENABLE;
		iisfcon &= ~S3C2410_IISFCON_TXDMA;
		iiscon  |=  S3C2410_IISCON_TXIDLE;
		iiscon  &= ~S3C2410_IISCON_TXDMAEN;
		//iismod  &= ~S3C2410_IISMOD_TXMODE;

		writel(iiscon,  card->regs + S3C2410_IISCON);
		writel(iisfcon, card->regs + S3C2410_IISFCON);
		//writel(iismod,  card->regs + S3C2410_IISMOD);
	}

	DBG("%s: iismod=0x%08lx, iisfcon=0x%08lx, iiscon=0x%08lx\n",
	    __FUNCTION__, iismod, iisfcon, iiscon);
}

static void s3c24xx_snd_rxctrl(s3c24xx_card_t *card, int on)
{
	unsigned long iisfcon;
	unsigned long iiscon;
	unsigned long iismod;

	iisfcon = readl(card->regs + S3C2410_IISFCON);
	iiscon  = readl(card->regs + S3C2410_IISCON);
	iismod  = readl(card->regs + S3C2410_IISMOD);

	if (on) {
		iisfcon |= S3C2410_IISFCON_RXDMA | S3C2410_IISFCON_RXENABLE;
		iiscon  |= S3C2410_IISCON_RXDMAEN | S3C2410_IISCON_IISEN;
		iiscon  &= ~S3C2410_IISCON_RXIDLE;
		iismod  |= S3C2410_IISMOD_RXMODE;
		iismod  |= S3C2410_IISMOD_TXMODE;

		writel(iismod,  card->regs + S3C2410_IISMOD);
		writel(iisfcon, card->regs + S3C2410_IISFCON);
		writel(iiscon,  card->regs + S3C2410_IISCON);
	} else {
                /* note, we have to disable the FIFOs otherwise bad things
                 * seem to happen when the DMA stops. According to the 
                 * Samsung supplied kernel, this should allow the DMA 
                 * engine and FIFOs to reset. If this isn't allowed, the
                 * DMA engine will simply freeze randomly.
                 */

                iisfcon &= ~S3C2410_IISFCON_RXENABLE;
                iisfcon &= ~S3C2410_IISFCON_RXDMA;
                iiscon  |= S3C2410_IISCON_RXIDLE;
                iiscon  &= ~S3C2410_IISCON_RXDMAEN;
		//iismod  &= ~S3C2410_IISMOD_RXMODE;

		writel(iisfcon, card->regs + S3C2410_IISFCON);
		writel(iiscon,  card->regs + S3C2410_IISCON);
		//writel(iismod,  card->regs + S3C2410_IISMOD);
	}

	DBG("%s: iismod=0x%08lx, iisfcon=0x%08lx, iiscon=0x%08lx\n",
	    __FUNCTION__, iismod, iisfcon, iiscon);
}

int s3c24xx_iis_cfgclksrc(s3c24xx_card_t *card, const char *name)
{
	struct s3c24xx_platdata_iis *pdata = to_platdata(card);
	unsigned long iismod;

	if (name == NULL && pdata != NULL)
		name = pdata->codec_clk;

	if (name == NULL)
		return -EINVAL;

	DBG("%s: clock source %s\n", __FUNCTION__, name);

	/* alter clock source (master/slave) appropriately */

	iismod = readl(card->regs + S3C2410_IISMOD);

	if (strcmp(name, "pclk") == 0 || strcmp(name, "cdclk") == 0) {
		iismod &= ~S3C2410_IISMOD_SLAVE;		
	} else {
		iismod |= S3C2410_IISMOD_SLAVE;
	}

	writel(iismod, card->regs + S3C2410_IISMOD);
	return 0;
}


/* s3c24xx_pcm_enqueue
 *
 * place a dma buffer onto the queue for the dma system
 * to handle.
*/

static void s3c24xx_pcm_enqueue(s3c24xx_runtime_t *or)
{
	dma_addr_t pos = or->dma_pos;
	int ret;

	DBG_DMA("%s: pos=%lx, period=%x, end=%lx, loaded=%d\n", __FUNCTION__,
		(long)or->dma_start, or->dma_period,
		(long)or->dma_end, or->dma_loaded);

	do {
		DBG_DMA("%s: load buffer %lx\n", __FUNCTION__, (long)pos);

		ret = s3c2410_dma_enqueue(or->dma_ch, or, pos, or->dma_period);

		if (ret == 0) {
			or->dma_loaded++;
			pos += or->dma_period;
			if (pos >= or->dma_end)
				pos = or->dma_start;
		}
	} while (ret == 0 && or->dma_loaded < or->dma_limit);

	or->dma_pos = pos;
}


static void s3c24xx_snd_playback_buffdone(s3c2410_dma_chan_t *ch,
					  void *bufid, int size,
					  s3c2410_dma_buffresult_t result)
{
	s3c24xx_runtime_t *or = bufid;

	DBG_DMA("%s: %p,%p, or %p, sz %d, res %d\n", 
		__FUNCTION__, ch, bufid, or, size, result);

	if (or->stream)
		snd_pcm_period_elapsed(or->stream);

	spin_lock(&or->lock);
	if (or->state & ST_RUNNING) {
		or->dma_loaded--;
		s3c24xx_pcm_enqueue(or);
	}
	spin_unlock(&or->lock);
}

static void s3c24xx_snd_capture_buffdone(s3c2410_dma_chan_t *ch,
					 void *bufid, int size,
					 s3c2410_dma_buffresult_t result)
{
	s3c24xx_runtime_t *or = bufid;

	DBG_DMA("%s: %p,%p, or %p, sz %d, res %d\n", 
		__FUNCTION__, ch, bufid, or, size, result);

	if (or->stream)
		snd_pcm_period_elapsed(or->stream);

	spin_lock(&or->lock);
	if (or->state & ST_RUNNING) {
		or->dma_loaded--;
		s3c24xx_pcm_enqueue(or);
	}
	spin_unlock(&or->lock);
}



static int call_startup(struct s3c24xx_iis_ops *ops)
{
	if (ops && ops->startup)
		return (ops->startup)(ops);

	return 0;
}

static int call_open(struct s3c24xx_iis_ops *ops,
		     snd_pcm_substream_t *substream)
{
	if (ops && ops->open)
		return (ops->open)(ops, substream);

	return 0;
}

static int s3c24xx_snd_open(snd_pcm_substream_t *substream)
{
	s3c24xx_card_t *chip = snd_pcm_substream_chip(substream);
	s3c24xx_runtime_t *or;
	snd_pcm_runtime_t *runtime = substream->runtime;
	int ret = 0;

	DBG("%s: substream=%p, chip=%p\n", __FUNCTION__, substream, chip);

	runtime->private_data = &chip->playback;
	runtime->hw	      = s3c24xx_snd_hw;

	down(&chip->sem);

	/* todo - request dma channel nicely */

	DBG("%s: state: playback 0x%x, capture 0x%x\n",
	    __FUNCTION__, chip->playback.state, chip->capture.state);

	if (chip->playback.state == 0 && chip->capture.state == 0) {

		/* ensure we hold all the modules we need */

		if (chip->base_ops) {
			if (try_module_get(chip->base_ops->owner))
				chip->base_ops_claimed = 1;
			else {
				dev_err(chip->device, "cannot claim module\n");
				ret = -EINVAL;
				goto exit_err;
			}
		}

		if (chip->chip_ops) {
			if (try_module_get(chip->chip_ops->owner))
				chip->chip_ops_claimed = 1;
			else {
				dev_err(chip->device, "cannot claim module\n");
				ret = -EINVAL;
				goto exit_err;
			}
		}

		/* ensure the chip is started */

		ret = call_startup(chip->base_ops);
		if (ret)
			goto exit_err;

		ret = call_startup(chip->chip_ops);
		if (ret)
			goto exit_err;

		/* configure the pins for audio (iis) control */

		s3c2410_gpio_cfgpin(S3C2410_GPE0, S3C2410_GPE0_I2SLRCK);
		s3c2410_gpio_cfgpin(S3C2410_GPE1, S3C2410_GPE1_I2SSCLK);
		s3c2410_gpio_cfgpin(S3C2410_GPE3, S3C2410_GPE3_I2SSDI);
		s3c2410_gpio_cfgpin(S3C2410_GPE4, S3C2410_GPE4_I2SSDO);
		
		if (s3c24xx_snd_is_clkmaster(chip))
			s3c2410_gpio_cfgpin(S3C2410_GPE2, S3C2410_GPE2_CDCLK);
	}

	/* initialise the stream */

	runtime->hw = s3c24xx_snd_hw;

	if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
		or = &chip->capture;
	else
		or = &chip->playback;

	/* call all the registered helpers */

	ret = call_open(chip->base_ops, substream);
	if (ret < 0)
		goto exit_err;

	ret = call_open(chip->chip_ops, substream);
	if (ret < 0)
		goto exit_err;

	or->state |= ST_OPENED;

	runtime->private_data = or;
	or->stream = substream;
	      
	up(&chip->sem);

	return ret;

 exit_err:
	up(&chip->sem);
	return ret;
}


static void call_shutdown(struct s3c24xx_iis_ops *ops)
{
	if (ops && ops->shutdown)
		(ops->shutdown)(ops);
}

static int call_close(struct s3c24xx_iis_ops *ops, 
		      snd_pcm_substream_t *substream)
{
	if (ops && ops->close)
		return (ops->close)(ops, substream);
	
	return 0;
}

/* close callback */
static int s3c24xx_snd_close(snd_pcm_substream_t *substream)
{
	s3c24xx_card_t *chip = snd_pcm_substream_chip(substream);
	snd_pcm_runtime_t *runtime = substream->runtime;
	s3c24xx_runtime_t *or = runtime->private_data;

	DBG("%s: substream=%p, chip=%p\n", __FUNCTION__, substream, chip);
		
	down(&chip->sem); 

	snd_assert(or->state != 0, chip = chip);

	/* mark stream as closed */
	or->state &= ~ST_OPENED;

	/* close all the devices associated with this */
	call_close(chip->chip_ops, substream);
	call_close(chip->base_ops, substream);

	/* are both shut-down? */
	if (chip->playback.state == 0 && chip->capture.state == 0) {
		/* call shut-down methods for all */
				
		call_shutdown(chip->chip_ops);
		call_shutdown(chip->base_ops);

		/* de-configure the IIS pins, go to input */

		s3c2410_gpio_cfgpin(S3C2410_GPE0, S3C2410_GPE0_INP);
		s3c2410_gpio_cfgpin(S3C2410_GPE1, S3C2410_GPE1_INP);
		
		if (s3c24xx_snd_is_clkmaster(chip))
			s3c2410_gpio_cfgpin(S3C2410_GPE2, S3C2410_GPE2_INP);
		
		s3c2410_gpio_cfgpin(S3C2410_GPE3, S3C2410_GPE3_INP);
		s3c2410_gpio_cfgpin(S3C2410_GPE4, S3C2410_GPE4_INP);

		/* free any hold we had on the modules */

		if (chip->base_ops_claimed) {
			chip->base_ops_claimed = 0;
			module_put(chip->base_ops->owner);
		}

		if (chip->chip_ops_claimed) {
			chip->chip_ops_claimed = 0;
			module_put(chip->chip_ops->owner);
		}
	}
	
	up(&chip->sem);
	return 0;
}


/* hw_params callback */
static int s3c24xx_snd_pcm_hw_params(snd_pcm_substream_t *substream,
				     snd_pcm_hw_params_t *hw_params)
{
	DBG("%s: sub=%p, hwp=%p\n", __FUNCTION__, substream, hw_params);

	snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer);
	return 0;
}

/* hw_free callback */
static int s3c24xx_snd_pcm_hw_free(snd_pcm_substream_t *substream)
{
	DBG("%s: substream %p\n", __FUNCTION__, substream);

	snd_pcm_set_runtime_buffer(substream, NULL);
	return 0;
}

/* frequency control */

static long s3c24xx_snd_getdiv(s3c24xx_card_t *card, unsigned int rate,
			       unsigned long *iismod, unsigned long iisdiv)
{
	unsigned long clkrate = clk_get_rate(card->clock);
	long div;
	long diff;

	if (iisdiv & S3C2410_IISMOD_384FS)
		clkrate /= 384;
	else
		clkrate /= 256;

	div = clkrate / rate;

	/* is divisor in range */

	if (div > 32 || div < 1)
		return -1L;

	/* is the rate generated near enough our clock rate? */

	diff = (clkrate / div) - rate;

	if (diff > 100 || diff < 100)
		return -1L;
	       
	/* update info and return */

	*iismod &= ~S3C2410_IISMOD_384FS;
	*iismod |= iisdiv;

	return (div-1) << S3C2410_IISPSR_INTSHIFT;
}

/* s3c24xx_snd_setrate
 *
 * configure the clock rate for when the s3c24xx is in clock master
 * mode.
*/

int s3c24xx_snd_setrate(s3c24xx_card_t *chip, snd_pcm_runtime_t *run)
{
	unsigned long iismod = readl(chip->regs + S3C2410_IISMOD);
	unsigned long rate;
	unsigned long tmp;
	long prescaler;

	/* if this is set to slave mode, then our clocks are all
	 * inputs anyway, so we cannot alter the frequency here */

	if (iismod & S3C2410_IISMOD_SLAVE) {
		printk(KERN_ERR "%s: called with IISMOD as slave\n",
		       __FUNCTION__);
		return -EINVAL;
	}

	if (iismod & S3C2410_IISMOD_16BIT)
		rate = run->rate * 32;
	else
		rate = run->rate * 16;


	DBG("%s: clock rate %ld\n", __FUNCTION__, rate);

	prescaler = s3c24xx_snd_getdiv(chip, rate, &iismod,
				       S3C2410_IISMOD_256FS);
	if (prescaler < 0)
		prescaler = s3c24xx_snd_getdiv(chip, rate, &iismod,
					       S3C2410_IISMOD_384FS);

	if (prescaler < 0) {
		printk(KERN_ERR "cannot get rate %d Hz\n", run->rate);
		return -ERANGE;
	}

	/* update timing registers */

	writel(iismod, chip->regs + S3C2410_IISMOD);

	tmp = readl(chip->regs + S3C2410_IISPSR) & ~S3C2410_IISPSR_INTMASK;
	tmp |= prescaler;
	writel(tmp, chip->regs + S3C2410_IISPSR);

	return 0;
}

EXPORT_SYMBOL(s3c24xx_snd_setrate);

/* s3c24xx_iismod_cfg
 *
 * Update the s3c24xx IISMOD register
*/

void s3c24xx_iismod_cfg(s3c24xx_card_t *chip,
			unsigned long set,
			unsigned long mask)
{
	unsigned long flags;
	unsigned long tmp;

	local_irq_save(flags);

	tmp = readl(chip->regs + S3C2410_IISMOD);
	tmp &= ~mask;
	tmp |= set;

	DBG("%s: new iismod 0x%08lx\n", __FUNCTION__, tmp);

	writel(tmp, chip->regs + S3C2410_IISMOD);
	local_irq_restore(flags);
}

EXPORT_SYMBOL(s3c24xx_iismod_cfg);

static int call_prepare(struct s3c24xx_iis_ops *ops,
			snd_pcm_substream_t *substream, snd_pcm_runtime_t *rt)
{
	if (ops && ops->prepare)
		return (ops->prepare)(ops, substream, rt);

	return 0;
}

/* prepare callback */

static int s3c24xx_snd_pcm_prepare(snd_pcm_substream_t *substream)
{
	s3c24xx_card_t *chip = snd_pcm_substream_chip(substream);
	s3c24xx_runtime_t *or = substream->runtime->private_data;
	snd_pcm_runtime_t *runtime = substream->runtime;
	unsigned long flags;
	int ret = 0;

	DBG("%s: substream=%p, chip=%p\n", __FUNCTION__, substream, chip);
	DBG("%s: r->dma_addr=%lx, r->dma_area=%p\n", __FUNCTION__,
	    (long)runtime->dma_addr, runtime->dma_area);

	/* prepare DMA */

	spin_lock_irqsave(&or->lock, flags);
	or->dma_loaded	= 0;
	or->dma_limit	= runtime->hw.periods_min;
	or->dma_period	= snd_pcm_lib_period_bytes(substream);
	or->dma_start	= runtime->dma_addr;
	or->dma_pos	= or->dma_start;
	or->dma_end	= or->dma_start + snd_pcm_lib_buffer_bytes(substream);
	spin_unlock_irqrestore(&or->lock, flags);

	DBG("%s: dma: period=%d, start=%lx, pos=%lx, end=%lx\n", __FUNCTION__,
	    or->dma_period, (long)or->dma_start,
	    (long)or->dma_pos, (long)or->dma_end);
	
	/* flush the DMA channel */

	s3c2410_dma_ctrl(or->dma_ch, S3C2410_DMAOP_FLUSH);

	/* call the register ops for prepare, to allow things like
	 * sample rate and format to be set
	 */

	ret = call_prepare(chip->base_ops, substream, runtime);
	if (ret < 0)
		goto exit_err;

	ret = call_prepare(chip->chip_ops, substream, runtime);

 exit_err:
	return ret;
}

/* s3c24xx_snd_lrsync
 *
 * Wait for the LR signal to allow synchronisation to the L/R clock
 * from the DAC. May only be needed for slave mode.
*/

static int s3c24xx_snd_lrsync(s3c24xx_card_t *chip)
{
	unsigned long iiscon;
	int timeout = 10000;

	while (1) {
		iiscon = readl(chip->regs + S3C2410_IISCON);
		if (iiscon & S3C2410_IISCON_LRINDEX)
			break;

		if (--timeout < 0)
			return -ETIMEDOUT;
	}

	return 0;
}

/* trigger callback */
static int s3c24xx_snd_pcm_trigger(snd_pcm_substream_t *substream, int cmd)
{
	s3c24xx_runtime_t *or = substream->runtime->private_data;
	s3c24xx_card_t *chip = snd_pcm_substream_chip(substream);
	unsigned long flags;
	int ret = -EINVAL;

	DBG("%s: substream=%p, cmd=%d\n", __FUNCTION__, substream, cmd);

	spin_lock_irqsave(&or->lock, flags);

	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
		or->state |= ST_RUNNING;

		/* enqueue dma buffers and start the IIS engine */

		s3c24xx_pcm_enqueue(or);
		s3c24xx_snd_showregs(chip);
		s3c2410_dma_ctrl(or->dma_ch, S3C2410_DMAOP_START);

		if (!s3c24xx_snd_is_clkmaster(chip)) {			
			ret = s3c24xx_snd_lrsync(chip);
			if (ret)
				goto exit_err;
		}

		if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
			s3c24xx_snd_rxctrl(chip, 1);
		else
			s3c24xx_snd_txctrl(chip, 1);

		ret = 0;
		break;

	case SNDRV_PCM_TRIGGER_STOP:
		or->state &= ~ST_RUNNING;

		if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
			s3c24xx_snd_rxctrl(chip, 0);
		else
			s3c24xx_snd_txctrl(chip, 0);
		
		s3c2410_dma_ctrl(or->dma_ch, S3C2410_DMAOP_STOP);
		s3c24xx_snd_showdma(or);	
		s3c24xx_snd_showregs(chip);

		ret = 0;
		break;

	default:
		ret = -EINVAL;
	}

 exit_err:
	spin_unlock_irqrestore(&or->lock, flags);
	return ret;
}


/* pointer callback */
static snd_pcm_uframes_t s3c24xx_snd_pcm_pointer(snd_pcm_substream_t *substream)
{
	s3c24xx_runtime_t *or = substream->runtime->private_data;
	unsigned long flags;
	unsigned long res;
	dma_addr_t src, dst;

	spin_lock_irqsave(&or->lock, flags);
	s3c2410_dma_getposition(or->dma_ch, &src, &dst);

	if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
		res = dst - or->dma_start;
	else
		res = src - or->dma_start;

	spin_unlock_irqrestore(&or->lock, flags);

	/* we seem to be getting the odd error from the pcm library due
	 * to out-of-bounds pointers. this is maybe due to the dma engine
	 * not having loaded the new values for the channel before being
	 * callled... (todo - fix )
	 */

	if (res > (or->dma_end - or->dma_start))
		DBG("%s: %lx,%lx (%lx) from %lx,%lx\n", __FUNCTION__,
		    (long)src, (long)dst, res,
		    (long)or->dma_start, (long)or->dma_end);

	if (res >= snd_pcm_lib_buffer_bytes(substream)) {
		if (res > snd_pcm_lib_buffer_bytes(substream))
			DBG("%s: res %lx >= %lx\n", __FUNCTION__,
			    res, (long)snd_pcm_lib_buffer_bytes(substream));
		
		if (res == snd_pcm_lib_buffer_bytes(substream))
			res = 0;
	}

	return bytes_to_frames(substream->runtime, res);
}

static int s3c24xx_snd_pcm_mmap(snd_pcm_substream_t *substream,
				struct vm_area_struct *vma)
{
	snd_pcm_runtime_t *runtime = substream->runtime;

	return dma_mmap_writecombine(substream->pcm->card->dev, vma,
				     runtime->dma_area,
				     runtime->dma_addr,
				     runtime->dma_bytes);
}

static int sc24xx_snd_dma_prealloc(snd_pcm_t *pcm, int stream)
{
	snd_pcm_substream_t *substream = pcm->streams[stream].substream;
	struct snd_dma_buffer *buf = &substream->dma_buffer;
	size_t size = s3c24xx_snd_hw.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 == NULL) {
		dev_err(pcm->card->dev, "failed to allocate dma buff\n");
		return -ENOMEM;
	}

	DBG("%s: stream %d: dma area %p, 0x%lx\n", __FUNCTION__,
	    stream, buf->area, (long)buf->addr);

	return 0;
}

static void sc24xx_snd_dma_free(snd_pcm_t *pcm, int stream)
{
	snd_pcm_substream_t *substream = pcm->streams[stream].substream;
	struct snd_dma_buffer *buf = &substream->dma_buffer;
	size_t size = s3c24xx_snd_hw.buffer_bytes_max;

	dma_free_writecombine(pcm->card->dev, size, buf->area, buf->addr);

	buf->area = NULL;
	buf->addr = (dma_addr_t)NULL;
}

/* operators */
static snd_pcm_ops_t s3c24xx_snd_pcm_ops = {
	.open		= s3c24xx_snd_open,
	.close		= s3c24xx_snd_close,
	.ioctl		= snd_pcm_lib_ioctl,
	.hw_params	= s3c24xx_snd_pcm_hw_params,
	.hw_free	= s3c24xx_snd_pcm_hw_free,
	.prepare	= s3c24xx_snd_pcm_prepare,
	.trigger	= s3c24xx_snd_pcm_trigger,
	.pointer	= s3c24xx_snd_pcm_pointer,
	.mmap		= s3c24xx_snd_pcm_mmap,
};

static int snd_card_s3c24xx_pcm(s3c24xx_card_t *card)
{
	snd_pcm_t *pcm;
	int err;

	DBG("snd_card_s3c24xx_pcm: card=%p\n", card);

	err = snd_pcm_new(card->card, "S3C24XX-IIS", 0, 1, 1, &pcm);
	if (err < 0) {
		printk(KERN_ERR "cannot register new pcm");
		return err;
	}

	DBG("snd_card_s3c24xx_pcm: new pcm %p\n", pcm);

	card->pcm	  = pcm;
	pcm->private_data = card;

	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &s3c24xx_snd_pcm_ops);
	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &s3c24xx_snd_pcm_ops);

	/* allocate dma resources */

	err = sc24xx_snd_dma_prealloc(pcm, SNDRV_PCM_STREAM_PLAYBACK);
	if (err)
		goto exit_err;

	err = sc24xx_snd_dma_prealloc(pcm, SNDRV_PCM_STREAM_CAPTURE);
	if (err)
		goto exit_err;

	strcpy(pcm->name, "s3c24xx");

	DBG("snd_card_s3c24xx_pcm: ok\n");

	return 0;

 exit_err:
	return err;
}

static int s3c24xx_iis_free(s3c24xx_card_t *card)
{
	sc24xx_snd_dma_free(card->pcm, SNDRV_PCM_STREAM_PLAYBACK);
	sc24xx_snd_dma_free(card->pcm, SNDRV_PCM_STREAM_CAPTURE);
}

static int s3c24xx_iis_dma_init(s3c24xx_card_t *card)
{
	int err = 0;

	our_card->playback.dma_ch = 2;
	our_card->capture.dma_ch = 1;

	err = s3c2410_dma_request(our_card->playback.dma_ch,
				  &s3c24xx_snd_playback_client, NULL);
	if (err) {
		printk(KERN_ERR "failed to get dma channel\n");
		return err;
	}

	s3c2410_dma_devconfig(our_card->playback.dma_ch,
			      S3C2410_DMASRC_MEM, 0x03,
			      S3C2410_PA_IIS + S3C2410_IISFIFO);

	s3c2410_dma_config(our_card->playback.dma_ch,
			   2, S3C2410_DCON_CH2_I2SSDO|S3C2410_DCON_SYNC_PCLK|S3C2410_DCON_HANDSHAKE);

	s3c2410_dma_set_buffdone_fn(our_card->playback.dma_ch,
				    s3c24xx_snd_playback_buffdone);

	/* attach capture channel */

	err = s3c2410_dma_request(our_card->capture.dma_ch,
				  &s3c24xx_snd_capture_client, NULL);
	if (err) {
		printk(KERN_ERR "failed to get dma channel\n");
		return err;
	}

	s3c2410_dma_config(our_card->capture.dma_ch,
			   2, S3C2410_DCON_HANDSHAKE |
			   S3C2410_DCON_SYNC_PCLK |
			   (2 << S3C2410_DCON_SRCSHIFT));

	s3c2410_dma_devconfig(our_card->capture.dma_ch,
			      S3C2410_DMASRC_HW, 0x3,
			      S3C2410_PA_IIS + S3C2410_IISFIFO);

	s3c2410_dma_set_buffdone_fn(our_card->capture.dma_ch,
				    s3c24xx_snd_capture_buffdone);

	return 0;
}

static int s3c24xx_iis_dma_free(s3c24xx_card_t *card)
{
	s3c2410_dma_free(card->capture.dma_ch, &s3c24xx_snd_capture_client);
	s3c2410_dma_free(card->playback.dma_ch, &s3c24xx_snd_playback_client);
}

int s3c24xx_iis_remove(struct device *dev)
{
	s3c24xx_card_t *card = dev_get_drvdata(dev);

	card = dev_get_drvdata(dev);
	if (card == NULL)
		return 0;

	s3c24xx_iis_dma_free(card);
	s3c24xx_iis_free(card);

	kfree(card);
	dev_set_drvdata(dev, NULL);

	return 0;
}

#if 1

static ssize_t s3c24xx_iis_show_regs(struct device *dev, char *buf)
{
	s3c24xx_card_t *our_card = dev_get_drvdata(dev);

	return snprintf(buf, PAGE_SIZE,
			"IISMOD = %08x\n"
			"IISCON = %08x\n"
			"IISPSR = %08x\n"
			"IISFCON= %08x\n",
			readl(our_card->regs + S3C2410_IISCON),
			readl(our_card->regs + S3C2410_IISMOD),
			readl(our_card->regs + S3C2410_IISPSR),
			readl(our_card->regs + S3C2410_IISFCON));
}

static DEVICE_ATTR(regs, S_IRUGO, s3c24xx_iis_show_regs, NULL);
#endif

s3c24xx_card_t *s3c24xx_iis_probe(struct device *dev)
{
	struct s3c24xx_platdata_iis *pdata = dev->platform_data;
	snd_card_t *card;
	int err;

	dev_info(dev, "probe called\n");

	card = snd_card_new(-1, 0, THIS_MODULE, sizeof(s3c24xx_card_t));

	our_card = (s3c24xx_card_t *)card->private_data;

	if (card == NULL) {
		printk(KERN_ERR "snd_card_new() failed\n");
		err = -EINVAL;
		goto exit_err;
	}
	
	our_card->card     = card;
	our_card->device   = dev;
	our_card->base_ops = (pdata) ? pdata->ops : NULL;

	/* get resources such as clocks and IO */

	our_card->clock = clk_get(dev, "iis");
	DBG("our_card.clock = %p\n", our_card->clock);

	if (IS_ERR(our_card->clock)) {
		err = PTR_ERR(our_card->clock);
		goto exit_err;
	}

	our_card->regs = ioremap(S3C2410_PA_IIS, 0x100);
	if (our_card->regs == NULL) {
		err = -ENXIO;
		goto exit_err;
	}

	DBG("our_card.regs at %p\n", our_card->regs);
	
	clk_enable(our_card->clock);

	init_MUTEX(&our_card->sem);
	spin_lock_init(&our_card->capture.lock);
	spin_lock_init(&our_card->playback.lock);

	/* ensure the channels are both shutdown */

	s3c24xx_snd_txctrl(our_card, 0);
	s3c24xx_snd_rxctrl(our_card, 0);

	/* static dma channel setup for the moment */

	err = s3c24xx_iis_dma_init(our_card);
	if (err)
		goto exit_err;

	/* default names for s3c2410 audio driver */

	strcpy(card->driver,    "S3C24XX");
	strcpy(card->shortname, "S3C24XX");
	strcpy(card->longname,  "Samsung S3C24XX");

	err = snd_card_s3c24xx_pcm(our_card);
	if (err) {
		printk(KERN_ERR "failed to init pcm devices\n");
		goto exit_err;
	}

	dev_info(dev, "soundcard attached ok (%p)\n", our_card);

	dev_set_drvdata(dev, our_card);
	device_create_file(dev, &dev_attr_regs);
	return our_card;

 exit_err:
	dev_err(dev, "error initialised card (%d)\n", err);
	snd_card_free(our_card->card);
	return ERR_PTR(err);
}

EXPORT_SYMBOL(s3c24xx_iis_probe);

#ifdef CONFIG_PM

static int call_suspend(struct s3c24xx_iis_ops *ops)
{
	if (ops && ops->suspend)
		return (ops->suspend)(ops);

	return 0;
}

int s3c24xx_iis_suspend(struct device *dev, u32 state, u32 level)
{
	s3c24xx_card_t *card = dev_get_drvdata(dev);

	if (card && level == SUSPEND_DISABLE) {
		if (card->card->power_state == SNDRV_CTL_POWER_D3cold)
			goto exit;

		/* inform sound core that we wish to suspend */

		snd_pcm_suspend_all(card->pcm);
		snd_power_change_state(card->card, SNDRV_CTL_POWER_D3cold);

		/* inform any of our clients */

		call_suspend(card->chip_ops);
		call_suspend(card->base_ops);

		clk_disable(card->clock);
	}

 exit:
	return 0;
}

static int call_resume(struct s3c24xx_iis_ops *ops)
{
	if (ops && ops->resume)
		return (ops->resume)(ops);

	return 0;
}

int s3c24xx_iis_resume(struct device *dev, u32 level)
{
	s3c24xx_card_t *card = dev_get_drvdata(dev);

	if (card && level == RESUME_ENABLE) {
		if (card->card->power_state == SNDRV_CTL_POWER_D0)
			goto exit;

		clk_enable(card->clock);

		/* inform any of our clients */

		call_resume(card->base_ops);
		call_resume(card->chip_ops);

		/* inform sound core that we wish to resume */

		snd_power_change_state(card->card, SNDRV_CTL_POWER_D0);
	}

 exit:
	return 0;
}

EXPORT_SYMBOL(s3c24xx_iis_suspend);
EXPORT_SYMBOL(s3c24xx_iis_resume);

#endif /* CONFIG_PM */
