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