/* 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 executed.
* @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");