https://github.com/torvalds/linux
Raw File
Tip revision: ebf53826e105f488f4f628703a108e98940d1dc5 authored by Linus Torvalds on 01 February 2011, 03:05:49 UTC
Linux 2.6.38-rc3
Tip revision: ebf5382
s3c-pl330.c
/* linux/arch/arm/plat-samsung/s3c-pl330.c
 *
 * Copyright (C) 2010 Samsung Electronics Co. Ltd.
 *	Jaswinder Singh <jassi.brar@samsung.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/io.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <linux/err.h>

#include <asm/hardware/pl330.h>

#include <plat/s3c-pl330-pdata.h>

/**
 * struct s3c_pl330_dmac - Logical representation of a PL330 DMAC.
 * @busy_chan: Number of channels currently busy.
 * @peri: List of IDs of peripherals this DMAC can work with.
 * @node: To attach to the global list of DMACs.
 * @pi: PL330 configuration info for the DMAC.
 * @kmcache: Pool to quickly allocate xfers for all channels in the dmac.
 * @clk: Pointer of DMAC operation clock.
 */
struct s3c_pl330_dmac {
	unsigned		busy_chan;
	enum dma_ch		*peri;
	struct list_head	node;
	struct pl330_info	*pi;
	struct kmem_cache	*kmcache;
	struct clk		*clk;
};

/**
 * struct s3c_pl330_xfer - A request submitted by S3C DMA clients.
 * @token: Xfer ID provided by the client.
 * @node: To attach to the list of xfers on a channel.
 * @px: Xfer for PL330 core.
 * @chan: Owner channel of this xfer.
 */
struct s3c_pl330_xfer {
	void			*token;
	struct list_head	node;
	struct pl330_xfer	px;
	struct s3c_pl330_chan	*chan;
};

/**
 * struct s3c_pl330_chan - Logical channel to communicate with
 * 	a Physical peripheral.
 * @pl330_chan_id: Token of a hardware channel thread of PL330 DMAC.
 * 	NULL if the channel is available to be acquired.
 * @id: ID of the peripheral that this channel can communicate with.
 * @options: Options specified by the client.
 * @sdaddr: Address provided via s3c2410_dma_devconfig.
 * @node: To attach to the global list of channels.
 * @lrq: Pointer to the last submitted pl330_req to PL330 core.
 * @xfer_list: To manage list of xfers enqueued.
 * @req: Two requests to communicate with the PL330 engine.
 * @callback_fn: Callback function to the client.
 * @rqcfg: Channel configuration for the xfers.
 * @xfer_head: Pointer to the xfer to be next excecuted.
 * @dmac: Pointer to the DMAC that manages this channel, NULL if the
 * 	channel is available to be acquired.
 * @client: Client of this channel. NULL if the
 * 	channel is available to be acquired.
 */
struct s3c_pl330_chan {
	void				*pl330_chan_id;
	enum dma_ch			id;
	unsigned int			options;
	unsigned long			sdaddr;
	struct list_head		node;
	struct pl330_req		*lrq;
	struct list_head		xfer_list;
	struct pl330_req		req[2];
	s3c2410_dma_cbfn_t		callback_fn;
	struct pl330_reqcfg		rqcfg;
	struct s3c_pl330_xfer		*xfer_head;
	struct s3c_pl330_dmac		*dmac;
	struct s3c2410_dma_client	*client;
};

/* All DMACs in the platform */
static LIST_HEAD(dmac_list);

/* All channels to peripherals in the platform */
static LIST_HEAD(chan_list);

/*
 * Since we add resources(DMACs and Channels) to the global pool,
 * we need to guard access to the resources using a global lock
 */
static DEFINE_SPINLOCK(res_lock);

/* Returns the channel with ID 'id' in the chan_list */
static struct s3c_pl330_chan *id_to_chan(const enum dma_ch id)
{
	struct s3c_pl330_chan *ch;

	list_for_each_entry(ch, &chan_list, node)
		if (ch->id == id)
			return ch;

	return NULL;
}

/* Allocate a new channel with ID 'id' and add to chan_list */
static void chan_add(const enum dma_ch id)
{
	struct s3c_pl330_chan *ch = id_to_chan(id);

	/* Return if the channel already exists */
	if (ch)
		return;

	ch = kmalloc(sizeof(*ch), GFP_KERNEL);
	/* Return silently to work with other channels */
	if (!ch)
		return;

	ch->id = id;
	ch->dmac = NULL;

	list_add_tail(&ch->node, &chan_list);
}

/* If the channel is not yet acquired by any client */
static bool chan_free(struct s3c_pl330_chan *ch)
{
	if (!ch)
		return false;

	/* Channel points to some DMAC only when it's acquired */
	return ch->dmac ? false : true;
}

/*
 * Returns 0 is peripheral i/f is invalid or not present on the dmac.
 * Index + 1, otherwise.
 */
static unsigned iface_of_dmac(struct s3c_pl330_dmac *dmac, enum dma_ch ch_id)
{
	enum dma_ch *id = dmac->peri;
	int i;

	/* Discount invalid markers */
	if (ch_id == DMACH_MAX)
		return 0;

	for (i = 0; i < PL330_MAX_PERI; i++)
		if (id[i] == ch_id)
			return i + 1;

	return 0;
}

/* If all channel threads of the DMAC are busy */
static inline bool dmac_busy(struct s3c_pl330_dmac *dmac)
{
	struct pl330_info *pi = dmac->pi;

	return (dmac->busy_chan < pi->pcfg.num_chan) ? false : true;
}

/*
 * Returns the number of free channels that
 * can be handled by this dmac only.
 */
static unsigned ch_onlyby_dmac(struct s3c_pl330_dmac *dmac)
{
	enum dma_ch *id = dmac->peri;
	struct s3c_pl330_dmac *d;
	struct s3c_pl330_chan *ch;
	unsigned found, count = 0;
	enum dma_ch p;
	int i;

	for (i = 0; i < PL330_MAX_PERI; i++) {
		p = id[i];
		ch = id_to_chan(p);

		if (p == DMACH_MAX || !chan_free(ch))
			continue;

		found = 0;
		list_for_each_entry(d, &dmac_list, node) {
			if (d != dmac && iface_of_dmac(d, ch->id)) {
				found = 1;
				break;
			}
		}
		if (!found)
			count++;
	}

	return count;
}

/*
 * Measure of suitability of 'dmac' handling 'ch'
 *
 * 0 indicates 'dmac' can not handle 'ch' either
 * because it is not supported by the hardware or
 * because all dmac channels are currently busy.
 *
 * >0 vlaue indicates 'dmac' has the capability.
 * The bigger the value the more suitable the dmac.
 */
#define MAX_SUIT	UINT_MAX
#define MIN_SUIT	0

static unsigned suitablility(struct s3c_pl330_dmac *dmac,
		struct s3c_pl330_chan *ch)
{
	struct pl330_info *pi = dmac->pi;
	enum dma_ch *id = dmac->peri;
	struct s3c_pl330_dmac *d;
	unsigned s;
	int i;

	s = MIN_SUIT;
	/* If all the DMAC channel threads are busy */
	if (dmac_busy(dmac))
		return s;

	for (i = 0; i < PL330_MAX_PERI; i++)
		if (id[i] == ch->id)
			break;

	/* If the 'dmac' can't talk to 'ch' */
	if (i == PL330_MAX_PERI)
		return s;

	s = MAX_SUIT;
	list_for_each_entry(d, &dmac_list, node) {
		/*
		 * If some other dmac can talk to this
		 * peri and has some channel free.
		 */
		if (d != dmac && iface_of_dmac(d, ch->id) && !dmac_busy(d)) {
			s = 0;
			break;
		}
	}
	if (s)
		return s;

	s = 100;

	/* Good if free chans are more, bad otherwise */
	s += (pi->pcfg.num_chan - dmac->busy_chan) - ch_onlyby_dmac(dmac);

	return s;
}

/* More than one DMAC may have capability to transfer data with the
 * peripheral. This function assigns most suitable DMAC to manage the
 * channel and hence communicate with the peripheral.
 */
static struct s3c_pl330_dmac *map_chan_to_dmac(struct s3c_pl330_chan *ch)
{
	struct s3c_pl330_dmac *d, *dmac = NULL;
	unsigned sn, sl = MIN_SUIT;

	list_for_each_entry(d, &dmac_list, node) {
		sn = suitablility(d, ch);

		if (sn == MAX_SUIT)
			return d;

		if (sn > sl)
			dmac = d;
	}

	return dmac;
}

/* Acquire the channel for peripheral 'id' */
static struct s3c_pl330_chan *chan_acquire(const enum dma_ch id)
{
	struct s3c_pl330_chan *ch = id_to_chan(id);
	struct s3c_pl330_dmac *dmac;

	/* If the channel doesn't exist or is already acquired */
	if (!ch || !chan_free(ch)) {
		ch = NULL;
		goto acq_exit;
	}

	dmac = map_chan_to_dmac(ch);
	/* If couldn't map */
	if (!dmac) {
		ch = NULL;
		goto acq_exit;
	}

	dmac->busy_chan++;
	ch->dmac = dmac;

acq_exit:
	return ch;
}

/* Delete xfer from the queue */
static inline void del_from_queue(struct s3c_pl330_xfer *xfer)
{
	struct s3c_pl330_xfer *t;
	struct s3c_pl330_chan *ch;
	int found;

	if (!xfer)
		return;

	ch = xfer->chan;

	/* Make sure xfer is in the queue */
	found = 0;
	list_for_each_entry(t, &ch->xfer_list, node)
		if (t == xfer) {
			found = 1;
			break;
		}

	if (!found)
		return;

	/* If xfer is last entry in the queue */
	if (xfer->node.next == &ch->xfer_list)
		t = list_entry(ch->xfer_list.next,
				struct s3c_pl330_xfer, node);
	else
		t = list_entry(xfer->node.next,
				struct s3c_pl330_xfer, node);

	/* If there was only one node left */
	if (t == xfer)
		ch->xfer_head = NULL;
	else if (ch->xfer_head == xfer)
		ch->xfer_head = t;

	list_del(&xfer->node);
}

/* Provides pointer to the next xfer in the queue.
 * If CIRCULAR option is set, the list is left intact,
 * otherwise the xfer is removed from the list.
 * Forced delete 'pluck' can be set to override the CIRCULAR option.
 */
static struct s3c_pl330_xfer *get_from_queue(struct s3c_pl330_chan *ch,
		int pluck)
{
	struct s3c_pl330_xfer *xfer = ch->xfer_head;

	if (!xfer)
		return NULL;

	/* If xfer is last entry in the queue */
	if (xfer->node.next == &ch->xfer_list)
		ch->xfer_head = list_entry(ch->xfer_list.next,
					struct s3c_pl330_xfer, node);
	else
		ch->xfer_head = list_entry(xfer->node.next,
					struct s3c_pl330_xfer, node);

	if (pluck || !(ch->options & S3C2410_DMAF_CIRCULAR))
		del_from_queue(xfer);

	return xfer;
}

static inline void add_to_queue(struct s3c_pl330_chan *ch,
		struct s3c_pl330_xfer *xfer, int front)
{
	struct pl330_xfer *xt;

	/* If queue empty */
	if (ch->xfer_head == NULL)
		ch->xfer_head = xfer;

	xt = &ch->xfer_head->px;
	/* If the head already submitted (CIRCULAR head) */
	if (ch->options & S3C2410_DMAF_CIRCULAR &&
		(xt == ch->req[0].x || xt == ch->req[1].x))
		ch->xfer_head = xfer;

	/* If this is a resubmission, it should go at the head */
	if (front) {
		ch->xfer_head = xfer;
		list_add(&xfer->node, &ch->xfer_list);
	} else {
		list_add_tail(&xfer->node, &ch->xfer_list);
	}
}

static inline void _finish_off(struct s3c_pl330_xfer *xfer,
		enum s3c2410_dma_buffresult res, int ffree)
{
	struct s3c_pl330_chan *ch;

	if (!xfer)
		return;

	ch = xfer->chan;

	/* Do callback */
	if (ch->callback_fn)
		ch->callback_fn(NULL, xfer->token, xfer->px.bytes, res);

	/* Force Free or if buffer is not needed anymore */
	if (ffree || !(ch->options & S3C2410_DMAF_CIRCULAR))
		kmem_cache_free(ch->dmac->kmcache, xfer);
}

static inline int s3c_pl330_submit(struct s3c_pl330_chan *ch,
		struct pl330_req *r)
{
	struct s3c_pl330_xfer *xfer;
	int ret = 0;

	/* If already submitted */
	if (r->x)
		return 0;

	xfer = get_from_queue(ch, 0);
	if (xfer) {
		r->x = &xfer->px;

		/* Use max bandwidth for M<->M xfers */
		if (r->rqtype == MEMTOMEM) {
			struct pl330_info *pi = xfer->chan->dmac->pi;
			int burst = 1 << ch->rqcfg.brst_size;
			u32 bytes = r->x->bytes;
			int bl;

			bl = pi->pcfg.data_bus_width / 8;
			bl *= pi->pcfg.data_buf_dep;
			bl /= burst;

			/* src/dst_burst_len can't be more than 16 */
			if (bl > 16)
				bl = 16;

			while (bl > 1) {
				if (!(bytes % (bl * burst)))
					break;
				bl--;
			}

			ch->rqcfg.brst_len = bl;
		} else {
			ch->rqcfg.brst_len = 1;
		}

		ret = pl330_submit_req(ch->pl330_chan_id, r);

		/* If submission was successful */
		if (!ret) {
			ch->lrq = r; /* latest submitted req */
			return 0;
		}

		r->x = NULL;

		/* If both of the PL330 ping-pong buffers filled */
		if (ret == -EAGAIN) {
			dev_err(ch->dmac->pi->dev, "%s:%d!\n",
				__func__, __LINE__);
			/* Queue back again */
			add_to_queue(ch, xfer, 1);
			ret = 0;
		} else {
			dev_err(ch->dmac->pi->dev, "%s:%d!\n",
				__func__, __LINE__);
			_finish_off(xfer, S3C2410_RES_ERR, 0);
		}
	}

	return ret;
}

static void s3c_pl330_rq(struct s3c_pl330_chan *ch,
	struct pl330_req *r, enum pl330_op_err err)
{
	unsigned long flags;
	struct s3c_pl330_xfer *xfer;
	struct pl330_xfer *xl = r->x;
	enum s3c2410_dma_buffresult res;

	spin_lock_irqsave(&res_lock, flags);

	r->x = NULL;

	s3c_pl330_submit(ch, r);

	spin_unlock_irqrestore(&res_lock, flags);

	/* Map result to S3C DMA API */
	if (err == PL330_ERR_NONE)
		res = S3C2410_RES_OK;
	else if (err == PL330_ERR_ABORT)
		res = S3C2410_RES_ABORT;
	else
		res = S3C2410_RES_ERR;

	/* If last request had some xfer */
	if (xl) {
		xfer = container_of(xl, struct s3c_pl330_xfer, px);
		_finish_off(xfer, res, 0);
	} else {
		dev_info(ch->dmac->pi->dev, "%s:%d No Xfer?!\n",
			__func__, __LINE__);
	}
}

static void s3c_pl330_rq0(void *token, enum pl330_op_err err)
{
	struct pl330_req *r = token;
	struct s3c_pl330_chan *ch = container_of(r,
					struct s3c_pl330_chan, req[0]);
	s3c_pl330_rq(ch, r, err);
}

static void s3c_pl330_rq1(void *token, enum pl330_op_err err)
{
	struct pl330_req *r = token;
	struct s3c_pl330_chan *ch = container_of(r,
					struct s3c_pl330_chan, req[1]);
	s3c_pl330_rq(ch, r, err);
}

/* Release an acquired channel */
static void chan_release(struct s3c_pl330_chan *ch)
{
	struct s3c_pl330_dmac *dmac;

	if (chan_free(ch))
		return;

	dmac = ch->dmac;
	ch->dmac = NULL;
	dmac->busy_chan--;
}

int s3c2410_dma_ctrl(enum dma_ch id, enum s3c2410_chan_op op)
{
	struct s3c_pl330_xfer *xfer;
	enum pl330_chan_op pl330op;
	struct s3c_pl330_chan *ch;
	unsigned long flags;
	int idx, ret;

	spin_lock_irqsave(&res_lock, flags);

	ch = id_to_chan(id);

	if (!ch || chan_free(ch)) {
		ret = -EINVAL;
		goto ctrl_exit;
	}

	switch (op) {
	case S3C2410_DMAOP_START:
		/* Make sure both reqs are enqueued */
		idx = (ch->lrq == &ch->req[0]) ? 1 : 0;
		s3c_pl330_submit(ch, &ch->req[idx]);
		s3c_pl330_submit(ch, &ch->req[1 - idx]);
		pl330op = PL330_OP_START;
		break;

	case S3C2410_DMAOP_STOP:
		pl330op = PL330_OP_ABORT;
		break;

	case S3C2410_DMAOP_FLUSH:
		pl330op = PL330_OP_FLUSH;
		break;

	case S3C2410_DMAOP_PAUSE:
	case S3C2410_DMAOP_RESUME:
	case S3C2410_DMAOP_TIMEOUT:
	case S3C2410_DMAOP_STARTED:
		spin_unlock_irqrestore(&res_lock, flags);
		return 0;

	default:
		spin_unlock_irqrestore(&res_lock, flags);
		return -EINVAL;
	}

	ret = pl330_chan_ctrl(ch->pl330_chan_id, pl330op);

	if (pl330op == PL330_OP_START) {
		spin_unlock_irqrestore(&res_lock, flags);
		return ret;
	}

	idx = (ch->lrq == &ch->req[0]) ? 1 : 0;

	/* Abort the current xfer */
	if (ch->req[idx].x) {
		xfer = container_of(ch->req[idx].x,
				struct s3c_pl330_xfer, px);

		/* Drop xfer during FLUSH */
		if (pl330op == PL330_OP_FLUSH)
			del_from_queue(xfer);

		ch->req[idx].x = NULL;

		spin_unlock_irqrestore(&res_lock, flags);
		_finish_off(xfer, S3C2410_RES_ABORT,
				pl330op == PL330_OP_FLUSH ? 1 : 0);
		spin_lock_irqsave(&res_lock, flags);
	}

	/* Flush the whole queue */
	if (pl330op == PL330_OP_FLUSH) {

		if (ch->req[1 - idx].x) {
			xfer = container_of(ch->req[1 - idx].x,
					struct s3c_pl330_xfer, px);

			del_from_queue(xfer);

			ch->req[1 - idx].x = NULL;

			spin_unlock_irqrestore(&res_lock, flags);
			_finish_off(xfer, S3C2410_RES_ABORT, 1);
			spin_lock_irqsave(&res_lock, flags);
		}

		/* Finish off the remaining in the queue */
		xfer = ch->xfer_head;
		while (xfer) {

			del_from_queue(xfer);

			spin_unlock_irqrestore(&res_lock, flags);
			_finish_off(xfer, S3C2410_RES_ABORT, 1);
			spin_lock_irqsave(&res_lock, flags);

			xfer = ch->xfer_head;
		}
	}

ctrl_exit:
	spin_unlock_irqrestore(&res_lock, flags);

	return ret;
}
EXPORT_SYMBOL(s3c2410_dma_ctrl);

int s3c2410_dma_enqueue(enum dma_ch id, void *token,
			dma_addr_t addr, int size)
{
	struct s3c_pl330_chan *ch;
	struct s3c_pl330_xfer *xfer;
	unsigned long flags;
	int idx, ret = 0;

	spin_lock_irqsave(&res_lock, flags);

	ch = id_to_chan(id);

	/* Error if invalid or free channel */
	if (!ch || chan_free(ch)) {
		ret = -EINVAL;
		goto enq_exit;
	}

	/* Error if size is unaligned */
	if (ch->rqcfg.brst_size && size % (1 << ch->rqcfg.brst_size)) {
		ret = -EINVAL;
		goto enq_exit;
	}

	xfer = kmem_cache_alloc(ch->dmac->kmcache, GFP_ATOMIC);
	if (!xfer) {
		ret = -ENOMEM;
		goto enq_exit;
	}

	xfer->token = token;
	xfer->chan = ch;
	xfer->px.bytes = size;
	xfer->px.next = NULL; /* Single request */

	/* For S3C DMA API, direction is always fixed for all xfers */
	if (ch->req[0].rqtype == MEMTODEV) {
		xfer->px.src_addr = addr;
		xfer->px.dst_addr = ch->sdaddr;
	} else {
		xfer->px.src_addr = ch->sdaddr;
		xfer->px.dst_addr = addr;
	}

	add_to_queue(ch, xfer, 0);

	/* Try submitting on either request */
	idx = (ch->lrq == &ch->req[0]) ? 1 : 0;

	if (!ch->req[idx].x)
		s3c_pl330_submit(ch, &ch->req[idx]);
	else
		s3c_pl330_submit(ch, &ch->req[1 - idx]);

	spin_unlock_irqrestore(&res_lock, flags);

	if (ch->options & S3C2410_DMAF_AUTOSTART)
		s3c2410_dma_ctrl(id, S3C2410_DMAOP_START);

	return 0;

enq_exit:
	spin_unlock_irqrestore(&res_lock, flags);

	return ret;
}
EXPORT_SYMBOL(s3c2410_dma_enqueue);

int s3c2410_dma_request(enum dma_ch id,
			struct s3c2410_dma_client *client,
			void *dev)
{
	struct s3c_pl330_dmac *dmac;
	struct s3c_pl330_chan *ch;
	unsigned long flags;
	int ret = 0;

	spin_lock_irqsave(&res_lock, flags);

	ch = chan_acquire(id);
	if (!ch) {
		ret = -EBUSY;
		goto req_exit;
	}

	dmac = ch->dmac;

	ch->pl330_chan_id = pl330_request_channel(dmac->pi);
	if (!ch->pl330_chan_id) {
		chan_release(ch);
		ret = -EBUSY;
		goto req_exit;
	}

	ch->client = client;
	ch->options = 0; /* Clear any option */
	ch->callback_fn = NULL; /* Clear any callback */
	ch->lrq = NULL;

	ch->rqcfg.brst_size = 2; /* Default word size */
	ch->rqcfg.swap = SWAP_NO;
	ch->rqcfg.scctl = SCCTRL0; /* Noncacheable and nonbufferable */
	ch->rqcfg.dcctl = DCCTRL0; /* Noncacheable and nonbufferable */
	ch->rqcfg.privileged = 0;
	ch->rqcfg.insnaccess = 0;

	/* Set invalid direction */
	ch->req[0].rqtype = DEVTODEV;
	ch->req[1].rqtype = ch->req[0].rqtype;

	ch->req[0].cfg = &ch->rqcfg;
	ch->req[1].cfg = ch->req[0].cfg;

	ch->req[0].peri = iface_of_dmac(dmac, id) - 1; /* Original index */
	ch->req[1].peri = ch->req[0].peri;

	ch->req[0].token = &ch->req[0];
	ch->req[0].xfer_cb = s3c_pl330_rq0;
	ch->req[1].token = &ch->req[1];
	ch->req[1].xfer_cb = s3c_pl330_rq1;

	ch->req[0].x = NULL;
	ch->req[1].x = NULL;

	/* Reset xfer list */
	INIT_LIST_HEAD(&ch->xfer_list);
	ch->xfer_head = NULL;

req_exit:
	spin_unlock_irqrestore(&res_lock, flags);

	return ret;
}
EXPORT_SYMBOL(s3c2410_dma_request);

int s3c2410_dma_free(enum dma_ch id, struct s3c2410_dma_client *client)
{
	struct s3c_pl330_chan *ch;
	struct s3c_pl330_xfer *xfer;
	unsigned long flags;
	int ret = 0;
	unsigned idx;

	spin_lock_irqsave(&res_lock, flags);

	ch = id_to_chan(id);

	if (!ch || chan_free(ch))
		goto free_exit;

	/* Refuse if someone else wanted to free the channel */
	if (ch->client != client) {
		ret = -EBUSY;
		goto free_exit;
	}

	/* Stop any active xfer, Flushe the queue and do callbacks */
	pl330_chan_ctrl(ch->pl330_chan_id, PL330_OP_FLUSH);

	/* Abort the submitted requests */
	idx = (ch->lrq == &ch->req[0]) ? 1 : 0;

	if (ch->req[idx].x) {
		xfer = container_of(ch->req[idx].x,
				struct s3c_pl330_xfer, px);

		ch->req[idx].x = NULL;
		del_from_queue(xfer);

		spin_unlock_irqrestore(&res_lock, flags);
		_finish_off(xfer, S3C2410_RES_ABORT, 1);
		spin_lock_irqsave(&res_lock, flags);
	}

	if (ch->req[1 - idx].x) {
		xfer = container_of(ch->req[1 - idx].x,
				struct s3c_pl330_xfer, px);

		ch->req[1 - idx].x = NULL;
		del_from_queue(xfer);

		spin_unlock_irqrestore(&res_lock, flags);
		_finish_off(xfer, S3C2410_RES_ABORT, 1);
		spin_lock_irqsave(&res_lock, flags);
	}

	/* Pluck and Abort the queued requests in order */
	do {
		xfer = get_from_queue(ch, 1);

		spin_unlock_irqrestore(&res_lock, flags);
		_finish_off(xfer, S3C2410_RES_ABORT, 1);
		spin_lock_irqsave(&res_lock, flags);
	} while (xfer);

	ch->client = NULL;

	pl330_release_channel(ch->pl330_chan_id);

	ch->pl330_chan_id = NULL;

	chan_release(ch);

free_exit:
	spin_unlock_irqrestore(&res_lock, flags);

	return ret;
}
EXPORT_SYMBOL(s3c2410_dma_free);

int s3c2410_dma_config(enum dma_ch id, int xferunit)
{
	struct s3c_pl330_chan *ch;
	struct pl330_info *pi;
	unsigned long flags;
	int i, dbwidth, ret = 0;

	spin_lock_irqsave(&res_lock, flags);

	ch = id_to_chan(id);

	if (!ch || chan_free(ch)) {
		ret = -EINVAL;
		goto cfg_exit;
	}

	pi = ch->dmac->pi;
	dbwidth = pi->pcfg.data_bus_width / 8;

	/* Max size of xfer can be pcfg.data_bus_width */
	if (xferunit > dbwidth) {
		ret = -EINVAL;
		goto cfg_exit;
	}

	i = 0;
	while (xferunit != (1 << i))
		i++;

	/* If valid value */
	if (xferunit == (1 << i))
		ch->rqcfg.brst_size = i;
	else
		ret = -EINVAL;

cfg_exit:
	spin_unlock_irqrestore(&res_lock, flags);

	return ret;
}
EXPORT_SYMBOL(s3c2410_dma_config);

/* Options that are supported by this driver */
#define S3C_PL330_FLAGS (S3C2410_DMAF_CIRCULAR | S3C2410_DMAF_AUTOSTART)

int s3c2410_dma_setflags(enum dma_ch id, unsigned int options)
{
	struct s3c_pl330_chan *ch;
	unsigned long flags;
	int ret = 0;

	spin_lock_irqsave(&res_lock, flags);

	ch = id_to_chan(id);

	if (!ch || chan_free(ch) || options & ~(S3C_PL330_FLAGS))
		ret = -EINVAL;
	else
		ch->options = options;

	spin_unlock_irqrestore(&res_lock, flags);

	return 0;
}
EXPORT_SYMBOL(s3c2410_dma_setflags);

int s3c2410_dma_set_buffdone_fn(enum dma_ch id, s3c2410_dma_cbfn_t rtn)
{
	struct s3c_pl330_chan *ch;
	unsigned long flags;
	int ret = 0;

	spin_lock_irqsave(&res_lock, flags);

	ch = id_to_chan(id);

	if (!ch || chan_free(ch))
		ret = -EINVAL;
	else
		ch->callback_fn = rtn;

	spin_unlock_irqrestore(&res_lock, flags);

	return ret;
}
EXPORT_SYMBOL(s3c2410_dma_set_buffdone_fn);

int s3c2410_dma_devconfig(enum dma_ch id, enum s3c2410_dmasrc source,
			  unsigned long address)
{
	struct s3c_pl330_chan *ch;
	unsigned long flags;
	int ret = 0;

	spin_lock_irqsave(&res_lock, flags);

	ch = id_to_chan(id);

	if (!ch || chan_free(ch)) {
		ret = -EINVAL;
		goto devcfg_exit;
	}

	switch (source) {
	case S3C2410_DMASRC_HW: /* P->M */
		ch->req[0].rqtype = DEVTOMEM;
		ch->req[1].rqtype = DEVTOMEM;
		ch->rqcfg.src_inc = 0;
		ch->rqcfg.dst_inc = 1;
		break;
	case S3C2410_DMASRC_MEM: /* M->P */
		ch->req[0].rqtype = MEMTODEV;
		ch->req[1].rqtype = MEMTODEV;
		ch->rqcfg.src_inc = 1;
		ch->rqcfg.dst_inc = 0;
		break;
	default:
		ret = -EINVAL;
		goto devcfg_exit;
	}

	ch->sdaddr = address;

devcfg_exit:
	spin_unlock_irqrestore(&res_lock, flags);

	return ret;
}
EXPORT_SYMBOL(s3c2410_dma_devconfig);

int s3c2410_dma_getposition(enum dma_ch id, dma_addr_t *src, dma_addr_t *dst)
{
	struct s3c_pl330_chan *ch = id_to_chan(id);
	struct pl330_chanstatus status;
	int ret;

	if (!ch || chan_free(ch))
		return -EINVAL;

	ret = pl330_chan_status(ch->pl330_chan_id, &status);
	if (ret < 0)
		return ret;

	*src = status.src_addr;
	*dst = status.dst_addr;

	return 0;
}
EXPORT_SYMBOL(s3c2410_dma_getposition);

static irqreturn_t pl330_irq_handler(int irq, void *data)
{
	if (pl330_update(data))
		return IRQ_HANDLED;
	else
		return IRQ_NONE;
}

static int pl330_probe(struct platform_device *pdev)
{
	struct s3c_pl330_dmac *s3c_pl330_dmac;
	struct s3c_pl330_platdata *pl330pd;
	struct pl330_info *pl330_info;
	struct resource *res;
	int i, ret, irq;

	pl330pd = pdev->dev.platform_data;

	/* Can't do without the list of _32_ peripherals */
	if (!pl330pd || !pl330pd->peri) {
		dev_err(&pdev->dev, "platform data missing!\n");
		return -ENODEV;
	}

	pl330_info = kzalloc(sizeof(*pl330_info), GFP_KERNEL);
	if (!pl330_info)
		return -ENOMEM;

	pl330_info->pl330_data = NULL;
	pl330_info->dev = &pdev->dev;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		ret = -ENODEV;
		goto probe_err1;
	}

	request_mem_region(res->start, resource_size(res), pdev->name);

	pl330_info->base = ioremap(res->start, resource_size(res));
	if (!pl330_info->base) {
		ret = -ENXIO;
		goto probe_err2;
	}

	irq = platform_get_irq(pdev, 0);
	if (irq < 0) {
		ret = irq;
		goto probe_err3;
	}

	ret = request_irq(irq, pl330_irq_handler, 0,
			dev_name(&pdev->dev), pl330_info);
	if (ret)
		goto probe_err4;

	/* Allocate a new DMAC */
	s3c_pl330_dmac = kmalloc(sizeof(*s3c_pl330_dmac), GFP_KERNEL);
	if (!s3c_pl330_dmac) {
		ret = -ENOMEM;
		goto probe_err5;
	}

	/* Get operation clock and enable it */
	s3c_pl330_dmac->clk = clk_get(&pdev->dev, "pdma");
	if (IS_ERR(s3c_pl330_dmac->clk)) {
		dev_err(&pdev->dev, "Cannot get operation clock.\n");
		ret = -EINVAL;
		goto probe_err6;
	}
	clk_enable(s3c_pl330_dmac->clk);

	ret = pl330_add(pl330_info);
	if (ret)
		goto probe_err7;

	/* Hook the info */
	s3c_pl330_dmac->pi = pl330_info;

	/* No busy channels */
	s3c_pl330_dmac->busy_chan = 0;

	s3c_pl330_dmac->kmcache = kmem_cache_create(dev_name(&pdev->dev),
				sizeof(struct s3c_pl330_xfer), 0, 0, NULL);

	if (!s3c_pl330_dmac->kmcache) {
		ret = -ENOMEM;
		goto probe_err8;
	}

	/* Get the list of peripherals */
	s3c_pl330_dmac->peri = pl330pd->peri;

	/* Attach to the list of DMACs */
	list_add_tail(&s3c_pl330_dmac->node, &dmac_list);

	/* Create a channel for each peripheral in the DMAC
	 * that is, if it doesn't already exist
	 */
	for (i = 0; i < PL330_MAX_PERI; i++)
		if (s3c_pl330_dmac->peri[i] != DMACH_MAX)
			chan_add(s3c_pl330_dmac->peri[i]);

	printk(KERN_INFO
		"Loaded driver for PL330 DMAC-%d %s\n",	pdev->id, pdev->name);
	printk(KERN_INFO
		"\tDBUFF-%ux%ubytes Num_Chans-%u Num_Peri-%u Num_Events-%u\n",
		pl330_info->pcfg.data_buf_dep,
		pl330_info->pcfg.data_bus_width / 8, pl330_info->pcfg.num_chan,
		pl330_info->pcfg.num_peri, pl330_info->pcfg.num_events);

	return 0;

probe_err8:
	pl330_del(pl330_info);
probe_err7:
	clk_disable(s3c_pl330_dmac->clk);
	clk_put(s3c_pl330_dmac->clk);
probe_err6:
	kfree(s3c_pl330_dmac);
probe_err5:
	free_irq(irq, pl330_info);
probe_err4:
probe_err3:
	iounmap(pl330_info->base);
probe_err2:
	release_mem_region(res->start, resource_size(res));
probe_err1:
	kfree(pl330_info);

	return ret;
}

static int pl330_remove(struct platform_device *pdev)
{
	struct s3c_pl330_dmac *dmac, *d;
	struct s3c_pl330_chan *ch;
	unsigned long flags;
	int del, found;

	if (!pdev->dev.platform_data)
		return -EINVAL;

	spin_lock_irqsave(&res_lock, flags);

	found = 0;
	list_for_each_entry(d, &dmac_list, node)
		if (d->pi->dev == &pdev->dev) {
			found = 1;
			break;
		}

	if (!found) {
		spin_unlock_irqrestore(&res_lock, flags);
		return 0;
	}

	dmac = d;

	/* Remove all Channels that are managed only by this DMAC */
	list_for_each_entry(ch, &chan_list, node) {

		/* Only channels that are handled by this DMAC */
		if (iface_of_dmac(dmac, ch->id))
			del = 1;
		else
			continue;

		/* Don't remove if some other DMAC has it too */
		list_for_each_entry(d, &dmac_list, node)
			if (d != dmac && iface_of_dmac(d, ch->id)) {
				del = 0;
				break;
			}

		if (del) {
			spin_unlock_irqrestore(&res_lock, flags);
			s3c2410_dma_free(ch->id, ch->client);
			spin_lock_irqsave(&res_lock, flags);
			list_del(&ch->node);
			kfree(ch);
		}
	}

	/* Disable operation clock */
	clk_disable(dmac->clk);
	clk_put(dmac->clk);

	/* Remove the DMAC */
	list_del(&dmac->node);
	kfree(dmac);

	spin_unlock_irqrestore(&res_lock, flags);

	return 0;
}

static struct platform_driver pl330_driver = {
	.driver		= {
		.owner	= THIS_MODULE,
		.name	= "s3c-pl330",
	},
	.probe		= pl330_probe,
	.remove		= pl330_remove,
};

static int __init pl330_init(void)
{
	return platform_driver_register(&pl330_driver);
}
module_init(pl330_init);

static void __exit pl330_exit(void)
{
	platform_driver_unregister(&pl330_driver);
	return;
}
module_exit(pl330_exit);

MODULE_AUTHOR("Jaswinder Singh <jassi.brar@samsung.com>");
MODULE_DESCRIPTION("Driver for PL330 DMA Controller");
MODULE_LICENSE("GPL");
back to top