/* linux/arch/arm/plat-s3c64xx/dma.c
 *
 * Copyright 2009 Openmoko, Inc.
 * Copyright 2009 Simtec Electronics
 *	Ben Dooks <ben@simtec.co.uk>
 *	http://armlinux.simtec.co.uk/
 *
 * S3C64XX DMA 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.
*/

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/dmapool.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/clk.h>
#include <linux/err.h>
#include <linux/io.h>

#include <mach/dma.h>
#include <mach/map.h>
#include <mach/irqs.h>

#include <mach/regs-sys.h>

#include <asm/hardware/pl080.h>

/* dma channel state information */

struct s3c64xx_dmac {
	struct device		dev;
	struct clk		*clk;
	void __iomem		*regs;
	struct s3c2410_dma_chan *channels;
	enum dma_ch		 chanbase;
};

/* pool to provide LLI buffers */
static struct dma_pool *dma_pool;

/* Debug configuration and code */

static unsigned char debug_show_buffs = 0;

static void dbg_showchan(struct s3c2410_dma_chan *chan)
{
	pr_debug("DMA%d: %08x->%08x L %08x C %08x,%08x S %08x\n",
		 chan->number,
		 readl(chan->regs + PL080_CH_SRC_ADDR),
		 readl(chan->regs + PL080_CH_DST_ADDR),
		 readl(chan->regs + PL080_CH_LLI),
		 readl(chan->regs + PL080_CH_CONTROL),
		 readl(chan->regs + PL080S_CH_CONTROL2),
		 readl(chan->regs + PL080S_CH_CONFIG));
}

static void show_lli(struct pl080s_lli *lli)
{
	pr_debug("LLI[%p] %08x->%08x, NL %08x C %08x,%08x\n",
		 lli, lli->src_addr, lli->dst_addr, lli->next_lli,
		 lli->control0, lli->control1);
}

static void dbg_showbuffs(struct s3c2410_dma_chan *chan)
{
	struct s3c64xx_dma_buff *ptr;
	struct s3c64xx_dma_buff *end;

	pr_debug("DMA%d: buffs next %p, curr %p, end %p\n",
		 chan->number, chan->next, chan->curr, chan->end);

	ptr = chan->next;
	end = chan->end;

	if (debug_show_buffs) {
		for (; ptr != NULL; ptr = ptr->next) {
			pr_debug("DMA%d: %08x ",
				 chan->number, ptr->lli_dma);
			show_lli(ptr->lli);
		}
	}
}

/* End of Debug */

static struct s3c2410_dma_chan *s3c64xx_dma_map_channel(unsigned int channel)
{
	struct s3c2410_dma_chan *chan;
	unsigned int start, offs;

	start = 0;

	if (channel >= DMACH_PCM1_TX)
		start = 8;

	for (offs = 0; offs < 8; offs++) {
		chan = &s3c2410_chans[start + offs];
		if (!chan->in_use)
			goto found;
	}

	return NULL;

found:
	s3c_dma_chan_map[channel] = chan;
	return chan;
}

int s3c2410_dma_config(enum dma_ch channel, int xferunit)
{
	struct s3c2410_dma_chan *chan = s3c_dma_lookup_channel(channel);

	if (chan == NULL)
		return -EINVAL;

	switch (xferunit) {
	case 1:
		chan->hw_width = 0;
		break;
	case 2:
		chan->hw_width = 1;
		break;
	case 4:
		chan->hw_width = 2;
		break;
	default:
		printk(KERN_ERR "%s: illegal width %d\n", __func__, xferunit);
		return -EINVAL;
	}

	return 0;
}
EXPORT_SYMBOL(s3c2410_dma_config);

static void s3c64xx_dma_fill_lli(struct s3c2410_dma_chan *chan,
				 struct pl080s_lli *lli,
				 dma_addr_t data, int size)
{
	dma_addr_t src, dst;
	u32 control0, control1;

	switch (chan->source) {
	case DMA_FROM_DEVICE:
		src = chan->dev_addr;
		dst = data;
		control0 = PL080_CONTROL_SRC_AHB2;
		control0 |= PL080_CONTROL_DST_INCR;
		break;

	case DMA_TO_DEVICE:
		src = data;
		dst = chan->dev_addr;
		control0 = PL080_CONTROL_DST_AHB2;
		control0 |= PL080_CONTROL_SRC_INCR;
		break;
	default:
		BUG();
	}

	/* note, we do not currently setup any of the burst controls */

	control1 = size >> chan->hw_width;	/* size in no of xfers */
	control0 |= PL080_CONTROL_PROT_SYS;	/* always in priv. mode */
	control0 |= PL080_CONTROL_TC_IRQ_EN;	/* always fire IRQ */
	control0 |= (u32)chan->hw_width << PL080_CONTROL_DWIDTH_SHIFT;
	control0 |= (u32)chan->hw_width << PL080_CONTROL_SWIDTH_SHIFT;

	lli->src_addr = src;
	lli->dst_addr = dst;
	lli->next_lli = 0;
	lli->control0 = control0;
	lli->control1 = control1;
}

static void s3c64xx_lli_to_regs(struct s3c2410_dma_chan *chan,
				struct pl080s_lli *lli)
{
	void __iomem *regs = chan->regs;

	pr_debug("%s: LLI %p => regs\n", __func__, lli);
	show_lli(lli);

	writel(lli->src_addr, regs + PL080_CH_SRC_ADDR);
	writel(lli->dst_addr, regs + PL080_CH_DST_ADDR);
	writel(lli->next_lli, regs + PL080_CH_LLI);
	writel(lli->control0, regs + PL080_CH_CONTROL);
	writel(lli->control1, regs + PL080S_CH_CONTROL2);
}

static int s3c64xx_dma_start(struct s3c2410_dma_chan *chan)
{
	struct s3c64xx_dmac *dmac = chan->dmac;
	u32 config;
	u32 bit = chan->bit;

	dbg_showchan(chan);

	pr_debug("%s: clearing interrupts\n", __func__);

	/* clear interrupts */
	writel(bit, dmac->regs + PL080_TC_CLEAR);
	writel(bit, dmac->regs + PL080_ERR_CLEAR);

	pr_debug("%s: starting channel\n", __func__);

	config = readl(chan->regs + PL080S_CH_CONFIG);
	config |= PL080_CONFIG_ENABLE;
	config &= ~PL080_CONFIG_HALT;

	pr_debug("%s: writing config %08x\n", __func__, config);
	writel(config, chan->regs + PL080S_CH_CONFIG);

	return 0;
}

static int s3c64xx_dma_stop(struct s3c2410_dma_chan *chan)
{
	u32 config;
	int timeout;

	pr_debug("%s: stopping channel\n", __func__);

	dbg_showchan(chan);

	config = readl(chan->regs + PL080S_CH_CONFIG);
	config |= PL080_CONFIG_HALT;
	writel(config, chan->regs + PL080S_CH_CONFIG);

	timeout = 1000;
	do {
		config = readl(chan->regs + PL080S_CH_CONFIG);
		pr_debug("%s: %d - config %08x\n", __func__, timeout, config);
		if (config & PL080_CONFIG_ACTIVE)
			udelay(10);
		else
			break;
		} while (--timeout > 0);

	if (config & PL080_CONFIG_ACTIVE) {
		printk(KERN_ERR "%s: channel still active\n", __func__);
		return -EFAULT;
	}

	config = readl(chan->regs + PL080S_CH_CONFIG);
	config &= ~PL080_CONFIG_ENABLE;
	writel(config, chan->regs + PL080S_CH_CONFIG);
