Maemo patchset 20101501+0m5
[h-e-n] / drivers / misc / ssi / ssi_driver_dma.c
diff --git a/drivers/misc/ssi/ssi_driver_dma.c b/drivers/misc/ssi/ssi_driver_dma.c
new file mode 100644 (file)
index 0000000..ba1a655
--- /dev/null
@@ -0,0 +1,424 @@
+/*
+ * ssi_driver_dma.c
+ *
+ * Implements SSI low level interface driver functionality with DMA support.
+ *
+ * Copyright (C) 2007-2008 Nokia Corporation. All rights reserved.
+ *
+ * Contact: Carlos Chinea <carlos.chinea@nokia.com>
+ *
+ * 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ */
+#include <linux/dma-mapping.h>
+#include "ssi_driver.h"
+#include <mach/omap-pm.h>
+
+#define SSI_SYNC_WRITE 0
+#define SSI_SYNC_READ  1
+#define SSI_L3_TPUT    13428 /* 13428 KiB/s => ~110 Mbit/s*/
+
+static unsigned char ssi_sync_table[2][2][8] = {
+       {
+               {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
+               {0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00}
+       }, {
+               {0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17},
+               {0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f}
+       }
+};
+
+/**
+ * ssi_get_sync_port - Get the port number associate to a GDD sync.
+ * @sync - The sync mask from where to deduce the port.
+ *
+ * There is not masking scheme that can retrieve easily the port number
+ * from the sync value. TI spec made our live harder by for example
+ * associating 0x18 and 0x08 values to different ports :(
+ *
+ * Return port number (1 or 2) associated to the sync mask.
+ */
+static inline unsigned int ssi_get_sync_port(unsigned int sync)
+{
+       return (((sync > 0x00) && (sync < 0x09)) ||
+               ((sync > 0x0f) && (sync < 0x18))) ? 1 : 2;
+}
+
+/**
+ * ssi_get_free_lch - Get a free GDD(DMA)logical channel
+ * @ssi_ctrl- SSI controller of the GDD.
+ *
+ * Needs to be called holding the ssi_controller lock
+ *
+ * Return a free logical channel number. If there is no free lch
+ * then returns an out of range value
+ */
+static unsigned int ssi_get_free_lch(struct ssi_dev *ssi_ctrl)
+{
+       unsigned int enable_reg;
+       unsigned int i;
+       unsigned int lch = ssi_ctrl->last_gdd_lch;
+
+       enable_reg = ssi_inl(ssi_ctrl->base, SSI_SYS_GDD_MPU_IRQ_ENABLE_REG);
+       for (i = 1; i <= SSI_NUM_LCH; i++) {
+               lch = (lch + i) & (SSI_NUM_LCH - 1);
+               if (!(enable_reg & SSI_GDD_LCH(lch))) {
+                       ssi_ctrl->last_gdd_lch = lch;
+                       return lch;
+               }
+       }
+
+       return lch;
+}
+
+/**
+ * ssi_driver_write_dma - Program GDD [DMA] to write data from memory to
+ * the ssi channel buffer.
+ * @ssi_channel - pointer to the ssi_channel to write data to.
+ * @data - 32-bit word pointer to the data.
+ * @size - Number of 32bit words to be transfered.
+ *
+ * ssi_controller lock must be held before calling this function.
+ *
+ * Return 0 on success and < 0 on error.
+ */
+int ssi_driver_write_dma(struct ssi_channel *ssi_channel, u32 *data,
+                        unsigned int size)
+{
+       struct ssi_dev *ssi_ctrl = ssi_channel->ssi_port->ssi_controller;
+       void __iomem *base = ssi_ctrl->base;
+       struct ssi_platform_data *pdata = ssi_ctrl->dev->platform_data;
+       unsigned int port = ssi_channel->ssi_port->port_number;
+       unsigned int channel = ssi_channel->channel_number;
+       unsigned int sync;
+       int lch;
+       dma_addr_t dma_data;
+       dma_addr_t s_addr;
+       u16 tmp;
+
+       if ((size < 1) || (data == NULL))
+               return -EINVAL;
+
+       clk_enable(ssi_ctrl->ssi_clk);
+
+       lch = ssi_get_free_lch(ssi_ctrl);
+       if (lch >= SSI_NUM_LCH) {
+               dev_err(ssi_ctrl->dev, "No free GDD logical "
+                                                               "channels.\n");
+               clk_disable(ssi_ctrl->ssi_clk);
+               return -EBUSY;  /* No free GDD logical channels. */
+       }
+
+       if ((pdata->set_min_bus_tput) && (ssi_ctrl->gdd_usecount++ == 0))
+               pdata->set_min_bus_tput(ssi_ctrl->dev, OCP_INITIATOR_AGENT,
+                                       SSI_L3_TPUT);
+       /* NOTE: Gettting a free gdd logical channel and
+        * reserve it must be done atomicaly. */
+       ssi_channel->write_data.lch = lch;
+
+       sync = ssi_sync_table[SSI_SYNC_WRITE][port - 1][channel];
+       dma_data = dma_map_single(ssi_ctrl->dev, data, size * 4,
+                                       DMA_TO_DEVICE);
+
+       tmp = SSI_SRC_SINGLE_ACCESS0 |
+               SSI_SRC_MEMORY_PORT |
+               SSI_DST_SINGLE_ACCESS0 |
+               SSI_DST_PERIPHERAL_PORT |
+               SSI_DATA_TYPE_S32;
+       ssi_outw(tmp, base, SSI_GDD_CSDP_REG(lch));
+
+       tmp = SSI_SRC_AMODE_POSTINC | SSI_DST_AMODE_CONST | sync;
+       ssi_outw(tmp, base, SSI_GDD_CCR_REG(lch));
+
+       ssi_outw((SSI_BLOCK_IE | SSI_TOUT_IE), base, SSI_GDD_CICR_REG(lch));
+
+       s_addr = (dma_addr_t)io_v2p(base +
+                                       SSI_SST_BUFFER_CH_REG(port, channel));
+       ssi_outl(s_addr, base, SSI_GDD_CDSA_REG(lch));
+
+       ssi_outl(dma_data, base, SSI_GDD_CSSA_REG(lch));
+       ssi_outw(size, base, SSI_GDD_CEN_REG(lch));
+
+       ssi_outl_or(SSI_GDD_LCH(lch), base, SSI_SYS_GDD_MPU_IRQ_ENABLE_REG);
+       ssi_outw_or(SSI_CCR_ENABLE, base, SSI_GDD_CCR_REG(lch));
+
+       return 0;
+}
+
+/**
+ * ssi_driver_read_dma - Program GDD [DMA] to write data to memory from
+ * the ssi channel buffer.
+ * @ssi_channel - pointer to the ssi_channel to read data from.
+ * @data - 32-bit word pointer where to store the incoming data.
+ * @size - Number of 32bit words to be transfered to the buffer.
+ *
+ * ssi_controller lock must be held before calling this function.
+ *
+ * Return 0 on success and < 0 on error.
+ */
+int ssi_driver_read_dma(struct ssi_channel *ssi_channel, u32 *data,
+                       unsigned int count)
+{
+       struct ssi_dev *ssi_ctrl = ssi_channel->ssi_port->ssi_controller;
+       void __iomem *base = ssi_ctrl->base;
+       struct ssi_platform_data *pdata = ssi_ctrl->dev->platform_data;
+       unsigned int port = ssi_channel->ssi_port->port_number;
+       unsigned int channel = ssi_channel->channel_number;
+       unsigned int sync;
+       unsigned int lch;
+       dma_addr_t dma_data;
+       dma_addr_t d_addr;
+       u16 tmp;
+
+       clk_enable(ssi_ctrl->ssi_clk);
+       lch = ssi_get_free_lch(ssi_ctrl);
+       if (lch >= SSI_NUM_LCH) {
+               dev_err(ssi_ctrl->dev, "No free GDD logical channels.\n");
+               clk_disable(ssi_ctrl->ssi_clk);
+               return -EBUSY;  /* No free GDD logical channels. */
+       }
+       if ((pdata->set_min_bus_tput) && (ssi_ctrl->gdd_usecount++ == 0))
+               pdata->set_min_bus_tput(ssi_ctrl->dev, OCP_INITIATOR_AGENT,
+                                       SSI_L3_TPUT);
+       /*
+        * NOTE: Gettting a free gdd logical channel and
+        * reserve it must be done atomicaly.
+        */
+       ssi_channel->read_data.lch = lch;
+
+       sync = ssi_sync_table[SSI_SYNC_READ][port - 1][channel];
+
+       dma_data = dma_map_single(ssi_ctrl->dev, data, count * 4,
+                                       DMA_FROM_DEVICE);
+
+       tmp = SSI_DST_SINGLE_ACCESS0 |
+               SSI_DST_MEMORY_PORT |
+               SSI_SRC_SINGLE_ACCESS0 |
+               SSI_SRC_PERIPHERAL_PORT |
+               SSI_DATA_TYPE_S32;
+       ssi_outw(tmp, base, SSI_GDD_CSDP_REG(lch));
+
+       tmp = SSI_DST_AMODE_POSTINC | SSI_SRC_AMODE_CONST | sync;
+       ssi_outw(tmp, base, SSI_GDD_CCR_REG(lch));
+
+       ssi_outw((SSI_BLOCK_IE | SSI_TOUT_IE), base, SSI_GDD_CICR_REG(lch));
+
+       d_addr = (dma_addr_t)io_v2p(base +
+                                       SSI_SSR_BUFFER_CH_REG(port, channel));
+       ssi_outl(d_addr, base, SSI_GDD_CSSA_REG(lch));
+
+       ssi_outl(dma_data, base, SSI_GDD_CDSA_REG(lch));
+       ssi_outw(count, base, SSI_GDD_CEN_REG(lch));
+
+       ssi_outl_or(SSI_GDD_LCH(lch), base, SSI_SYS_GDD_MPU_IRQ_ENABLE_REG);
+       ssi_outw_or(SSI_CCR_ENABLE, base, SSI_GDD_CCR_REG(lch));
+
+       return 0;
+}
+
+void ssi_driver_cancel_write_dma(struct ssi_channel *ssi_ch)
+{
+       int lch = ssi_ch->write_data.lch;
+       unsigned int port = ssi_ch->ssi_port->port_number;
+       unsigned int channel = ssi_ch->channel_number;
+       struct ssi_dev *ssi_ctrl = ssi_ch->ssi_port->ssi_controller;
+       struct ssi_platform_data *pdata = ssi_ctrl->dev->platform_data;
+       u32 ccr;
+
+       if (lch < 0)
+               return;
+
+       clk_enable(ssi_ctrl->ssi_clk);
+       ccr = ssi_inw(ssi_ctrl->base, SSI_GDD_CCR_REG(lch));
+       if (!(ccr & SSI_CCR_ENABLE)) {
+               dev_dbg(&ssi_ch->dev->device, LOG_NAME "Write cancel on not "
+               "enabled logical channel %d CCR REG 0x%08X\n", lch, ccr);
+               clk_disable(ssi_ctrl->ssi_clk);
+               return;
+       }
+
+       if ((pdata->set_min_bus_tput) && (--ssi_ctrl->gdd_usecount == 0))
+               pdata->set_min_bus_tput(ssi_ctrl->dev, OCP_INITIATOR_AGENT, 0);
+
+       ssi_outw_and(~SSI_CCR_ENABLE, ssi_ctrl->base, SSI_GDD_CCR_REG(lch));
+       ssi_outl_and(~SSI_GDD_LCH(lch), ssi_ctrl->base,
+                                               SSI_SYS_GDD_MPU_IRQ_ENABLE_REG);
+       ssi_outl(SSI_GDD_LCH(lch), ssi_ctrl->base,
+                                               SSI_SYS_GDD_MPU_IRQ_STATUS_REG);
+
+       ssi_outl_and(~NOTFULL(channel), ssi_ctrl->base,
+                                               SSI_SST_BUFSTATE_REG(port));
+
+       ssi_reset_ch_write(ssi_ch);
+       clk_disable(ssi_ctrl->ssi_clk);
+       clk_disable(ssi_ctrl->ssi_clk);
+}
+
+void ssi_driver_cancel_read_dma(struct ssi_channel *ssi_ch)
+{
+       int lch = ssi_ch->read_data.lch;
+       struct ssi_dev *ssi_ctrl = ssi_ch->ssi_port->ssi_controller;
+       struct ssi_platform_data *pdata = ssi_ctrl->dev->platform_data;
+       unsigned int port = ssi_ch->ssi_port->port_number;
+       unsigned int channel = ssi_ch->channel_number;
+       u32 reg;
+
+       if (lch < 0)
+               return;
+
+       clk_enable(ssi_ctrl->ssi_clk);
+       reg = ssi_inw(ssi_ctrl->base, SSI_GDD_CCR_REG(lch));
+       if (!(reg & SSI_CCR_ENABLE)) {
+               dev_dbg(&ssi_ch->dev->device, LOG_NAME "Read cancel on not "
+               "enable logical channel %d CCR REG 0x%08X\n", lch, reg);
+               clk_disable(ssi_ctrl->ssi_clk);
+               return;
+       }
+
+       if ((pdata->set_min_bus_tput) && (--ssi_ctrl->gdd_usecount == 0))
+               pdata->set_min_bus_tput(ssi_ctrl->dev, OCP_INITIATOR_AGENT, 0);
+
+       ssi_outw_and(~SSI_CCR_ENABLE, ssi_ctrl->base, SSI_GDD_CCR_REG(lch));
+       ssi_outl_and(~SSI_GDD_LCH(lch), ssi_ctrl->base,
+                                               SSI_SYS_GDD_MPU_IRQ_ENABLE_REG);
+       ssi_outl(SSI_GDD_LCH(lch), ssi_ctrl->base,
+                                               SSI_SYS_GDD_MPU_IRQ_STATUS_REG);
+
+       ssi_outl_and(~NOTEMPTY(channel), ssi_ctrl->base,
+                                               SSI_SSR_BUFSTATE_REG(port));
+
+       ssi_reset_ch_read(ssi_ch);
+       clk_disable(ssi_ctrl->ssi_clk);
+       clk_disable(ssi_ctrl->ssi_clk);
+}
+
+static void do_ssi_gdd_lch(struct ssi_dev *ssi_ctrl, unsigned int gdd_lch)
+{
+       struct ssi_platform_data *pdata = ssi_ctrl->dev->platform_data;
+       void __iomem *base = ssi_ctrl->base;
+       struct ssi_channel *ch;
+       unsigned int port;
+       unsigned int channel;
+       u32 sync;
+       u32 gdd_csr;
+       dma_addr_t dma_h;
+       size_t size;
+
+       sync = ssi_inw(base, SSI_GDD_CCR_REG(gdd_lch)) & SSI_CCR_SYNC_MASK;
+       port = ssi_get_sync_port(sync);
+
+       spin_lock(&ssi_ctrl->lock);
+
+       ssi_outl_and(~SSI_GDD_LCH(gdd_lch), base,
+                                               SSI_SYS_GDD_MPU_IRQ_ENABLE_REG);
+       gdd_csr = ssi_inw(base, SSI_GDD_CSR_REG(gdd_lch));
+
+       if (!(gdd_csr & SSI_CSR_TOUR)) {
+               if (sync & 0x10) { /* Read path */
+                       channel = sync & 0x7;
+                       dma_h = ssi_inl(base, SSI_GDD_CDSA_REG(gdd_lch));
+                       size = ssi_inw(base, SSI_GDD_CEN_REG(gdd_lch)) * 4;
+                       dma_sync_single(ssi_ctrl->dev, dma_h, size,
+                                       DMA_FROM_DEVICE);
+                       dma_unmap_single(ssi_ctrl->dev, dma_h, size,
+                                               DMA_FROM_DEVICE);
+                       ch = ctrl_get_ch(ssi_ctrl, port, channel);
+                       ssi_reset_ch_read(ch);
+                       spin_unlock(&ssi_ctrl->lock);
+                       ch->read_done(ch->dev);
+               } else {
+                       channel = (sync - 1) & 0x7;
+                       dma_h = ssi_inl(base, SSI_GDD_CSSA_REG(gdd_lch));
+                       size = ssi_inw(base, SSI_GDD_CEN_REG(gdd_lch)) * 4;
+                       dma_unmap_single(ssi_ctrl->dev, dma_h, size,
+                                               DMA_TO_DEVICE);
+                       ch = ctrl_get_ch(ssi_ctrl, port, channel);
+                       ssi_reset_ch_write(ch);
+                       spin_unlock(&ssi_ctrl->lock);
+                       ch->write_done(ch->dev);
+               }
+       } else {
+               dev_err(ssi_ctrl->dev, "Error  on GDD transfer "
+                               "on gdd channel %d sync %d\n", gdd_lch, sync);
+               spin_unlock(&ssi_ctrl->lock);
+               ssi_port_event_handler(&ssi_ctrl->ssi_port[port - 1],
+                                                       SSI_EVENT_ERROR, NULL);
+       }
+
+       if ((pdata->set_min_bus_tput) && (--ssi_ctrl->gdd_usecount == 0))
+               pdata->set_min_bus_tput(ssi_ctrl->dev, OCP_INITIATOR_AGENT, 0);
+       /* Decrease clk usecount which was increased in
+        * ssi_driver_{read,write}_dma() */
+       clk_disable(ssi_ctrl->ssi_clk);
+}
+
+static void do_ssi_gdd_tasklet(unsigned long device)
+{
+       struct ssi_dev *ssi_ctrl = (struct ssi_dev *)device;
+       void __iomem *base = ssi_ctrl->base;
+       unsigned int gdd_lch = 0;
+       u32 status_reg = 0;
+       u32 lch_served = 0;
+
+       clk_enable(ssi_ctrl->ssi_clk);
+
+       status_reg = ssi_inl(base, SSI_SYS_GDD_MPU_IRQ_STATUS_REG);
+
+       for (gdd_lch = 0; gdd_lch < SSI_NUM_LCH; gdd_lch++) {
+               if (status_reg & SSI_GDD_LCH(gdd_lch)) {
+                       do_ssi_gdd_lch(ssi_ctrl, gdd_lch);
+                       lch_served |= SSI_GDD_LCH(gdd_lch);
+               }
+       }
+
+       ssi_outl(lch_served, base, SSI_SYS_GDD_MPU_IRQ_STATUS_REG);
+
+       status_reg = ssi_inl(base, SSI_SYS_GDD_MPU_IRQ_STATUS_REG);
+       clk_disable(ssi_ctrl->ssi_clk);
+
+       if (status_reg)
+               tasklet_hi_schedule(&ssi_ctrl->ssi_gdd_tasklet);
+       else
+               enable_irq(ssi_ctrl->gdd_irq);
+}
+
+static irqreturn_t ssi_gdd_mpu_handler(int irq, void *ssi_controller)
+{
+       struct ssi_dev *ssi_ctrl = ssi_controller;
+
+       tasklet_hi_schedule(&ssi_ctrl->ssi_gdd_tasklet);
+       disable_irq_nosync(ssi_ctrl->gdd_irq);
+
+       return IRQ_HANDLED;
+}
+
+int __init ssi_gdd_init(struct ssi_dev *ssi_ctrl, const char *irq_name)
+{
+       tasklet_init(&ssi_ctrl->ssi_gdd_tasklet, do_ssi_gdd_tasklet,
+                                               (unsigned long)ssi_ctrl);
+       if (request_irq(ssi_ctrl->gdd_irq, ssi_gdd_mpu_handler, IRQF_DISABLED,
+                                               irq_name, ssi_ctrl) < 0) {
+               dev_err(ssi_ctrl->dev, "FAILED to request GDD IRQ %d",
+                                                       ssi_ctrl->gdd_irq);
+               return -EBUSY;
+       }
+
+       return 0;
+}
+
+void ssi_gdd_exit(struct ssi_dev *ssi_ctrl)
+{
+       tasklet_disable(&ssi_ctrl->ssi_gdd_tasklet);
+       free_irq(ssi_ctrl->gdd_irq, ssi_ctrl);
+}