/*******************************************************************************
    OpenAirInterface
    Copyright(c) 1999 - 2014 Eurecom

    OpenAirInterface 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 3 of the License, or
    (at your option) any later version.


    OpenAirInterface 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 OpenAirInterface.The full GNU General Public License is
    included in this distribution in the file called "COPYING". If not,
    see <http://www.gnu.org/licenses/>.

   Contact Information
   OpenAirInterface Admin: openair_admin@eurecom.fr
   OpenAirInterface Tech : openair_tech@eurecom.fr
   OpenAirInterface Dev  : openair4g-devel@eurecom.fr

   Address      : Eurecom, Campus SophiaTech, 450 Route des Chappes, CS 50193 - 06904 Biot Sophia Antipolis cedex, FRANCE

 *******************************************************************************/

#include "em1_drv.h"

void em1_unlock_user_page(struct em1_page_s *ep)
{
  if (ep->addr != 0) {
    //    printk(KERN_DEBUG "unlocking user read page");
    page_cache_release(ep->page);
    ep->addr = 0;
  }
}

int em1_lock_user_page(struct em1_page_s *ep, uintptr_t addr, int direction)
{
  int res;

  if (ep->addr == addr)
    return 0;

  em1_unlock_user_page(ep);

  //  printk(KERN_DEBUG "locking user page for addr %p", addr);

  down_read(&current->mm->mmap_sem);
  res = get_user_pages(current, current->mm, addr & ~(PAGE_SIZE-1),
                       1, direction == DMA_FROM_DEVICE,
                       0, /* don't force */ &ep->page, NULL);
  up_read(&current->mm->mmap_sem);

  if (res != 1) {
    printk(KERN_ERR "exmimo1: get_user_pages error");
    return -EFAULT;
  }

  ep->addr = addr;

  return 0;
}

static ssize_t em1_dma_access(struct em1_file_s *fpv, char __user *buf,
                              size_t size, int direction, int opcode)
{
  DECLARE_WAITQUEUE(wait, current);
  struct em1_private_s *pv = fpv->pv;
  uint32_t cmd[4];
  uintptr_t addr = (uintptr_t)buf;
  uintptr_t end = (uintptr_t)buf + size - 1;
  struct em1_page_s *page;
  int fres = -ERESTARTSYS;
  uint16_t dma_this;

  /* check single page span */
  if ((addr & ~(PAGE_SIZE-1)) != (end & ~(PAGE_SIZE-1)))
    return -EINVAL;

  if (!access_ok(direction == DMA_FROM_DEVICE ? VERIFY_WRITE : VERIFY_READ, buf, size))
    return -EFAULT;

  page = direction == DMA_FROM_DEVICE ? &fpv->read : &fpv->write;

  if (em1_lock_user_page(page, addr, direction))
    return -EFAULT;

  addr = dma_map_page(pv->dev, page->page, 0, PAGE_SIZE, direction);

  /* prepare command for em1 */
  cmd[0] = opcode | 3;
  cmd[1] = sizeof(addr) > 4 ? (uint32_t)(addr >> 32) : 0;
  cmd[2] = (uint32_t)addr;
  cmd[3] = size;

  if (!em1_user_op_enter(pv, &wait, &pv->rq_wait_fifo_w,
                         EM1_BUSY_FIFO_W, TASK_INTERRUPTIBLE)) {
    /* send command */
    fres = em1_fifo_write(pv, cmd, 4);
    pv->busy &= ~EM1_BUSY_FIFO_W;
    dma_this = ++pv->dma_started;
  }

  em1_user_op_leave(pv, &wait, &pv->rq_wait_fifo_w);

  //  printk(KERN_DEBUG "exmimo1: dma fifo write %i, addr %x", fres, addr);

  if (!fres) {
    if (!em1_user_op_enter(pv, &wait, &pv->rq_wait_dma,
                           EM1_BUSY_DMA, TASK_UNINTERRUPTIBLE)) {
      //      printk(KERN_DEBUG "wait dma");

      /* wait for DMA end irq */
      while ((int16_t)(dma_this - pv->dma_done) > 0) {
        set_current_state(TASK_UNINTERRUPTIBLE);
        spin_unlock_irq(&pv->lock);
        schedule();
        spin_lock_irq(&pv->lock);
      }

      pv->busy &= ~EM1_BUSY_DMA;
      fres = size;
    }

    em1_user_op_leave(pv, &wait, &pv->rq_wait_dma);
  }

  dma_unmap_page(pv->dev, addr, PAGE_SIZE, direction);

  if (direction == DMA_FROM_DEVICE)
    SetPageDirty(page->page);

  return fres;
}

ssize_t em1_write(struct file *file, const char __user *buf, size_t size, loff_t *ppos)
{
  return em1_dma_access(file->private_data, (char*)buf, size,
                        DMA_TO_DEVICE, EM1_CMD_OP_DMA_PC2EM1);
}

ssize_t em1_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
  return em1_dma_access(file->private_data, buf, size,
                        DMA_FROM_DEVICE, EM1_CMD_OP_DMA_EM12PC);
}

/*
 * Local Variables:
 * c-file-style: "linux"
 * indent-tabs-mode: t
 * tab-width: 8
 * End:
 */

