/*
   TI Davinci driver - C64X+ DSP Kernel Module

   (c) Copyright 2007  Telio AG

   Written by Olaf Dreesen <dreesen@qarx.de>.

   All rights reserved.

   This module 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 library 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 library; if not, write to the
   Free Software Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.
*/

#include <linux/version.h>
#include <linux/init.h>
#include <linux/module.h>

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/fcntl.h>
#include <linux/firmware.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/page-flags.h>
#include <linux/poll.h>
#include <linux/proc_fs.h>
#include <linux/sched.h>
#include <linux/slab.h>

#include <asm/cacheflush.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <asm/system.h>
#include <asm/uaccess.h>

#include <linux/c64x.h>

#define C64X_IRQ

MODULE_LICENSE("GPL v2");
//MODULE_LICENSE("Propietary");
MODULE_AUTHOR("Olaf Dreesen <dreesen@qarx.de>");
MODULE_DESCRIPTION("A little c64+ handling module.");

#define C_MOD_MAJOR	400
#define C_MOD_NUM_DEV	1
#define C_MOD_NAME	"c64x"
#define F_NAME		"c64x_drv.bin"

#define CODE_BASE	0x00800000

/* DDR2:
 *
 * transfer buffer
 */
#define R_BASE		DAVINCI_C64X_MEM
#define R_LEN		0x02000000

/* L2RAM:
 *
 * 0x00800000 - 0x0080FFFF C64x+
 * 0x11800000 - 0x1180FFFF ARM
 */
#define D_BASE		0x11800000
#define D_LEN		0x00010000

/* L1DRAM:
 *
 * 0x00F04000 - 0x00F0FFFF C64x+
 * 0x11F04000 - 0x11F0FFFF ARM
 *
 * Queue controls	@ 0x00F04000	(4096 Bytes)
 */
#define Q_BASE		0x11F04000
#define Q_LEN		0x00001000

#define HQueueDSP	(l1dram[0x00>>2])
#define HQueueARM	(l1dram[0x04>>2])
#define LQueueDSP	(l1dram[0x08>>2])
#define LQueueARM	(l1dram[0x0C>>2])
#define DSPidle		(l1dram[0x10>>2])

/* IO Register needed:
 *
 *	0x01C40008	DSPBOOTADDR		DSP Boot Address
 *	0x01C40010	INTGEN			Interrupt Generator
 *	0x01C40038	CHP_SHRTSW		DSP Power
 *	0x01C4169C	MDCFG39			DSP Module config
 *	0x01C41A9C	MDCTL39			DSP Module control
 */
#define IO_BASE		0x01c40000
#define IO_LEN		0x00010000

#define DSPBOOTADDR	(mmr[0x0008>>2])
#define INTGEN		(mmr[0x0010>>2])
#define CHP_SHRTSW	(mmr[0x0038>>2])
#define MDCFG39		(mmr[0x169C>>2])
#define MDCTL39		(mmr[0x1A9C>>2])

MODULE_FIRMWARE(F_NAME);

static dev_t dev_major;
static struct cdev*dev_cdev;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 19)
static struct class*dev_class;
#else
static struct class_simple*dev_class;
#endif

static volatile unsigned int*mmr=0;
static unsigned char*l2ram=0;
static volatile unsigned int*l1dram=0;
static volatile void*dram=0;
static volatile c64xTaskControl*c64xctl=0;
static volatile c64xTask*queue_l=0;

#ifdef C64X_IRQ
static int dev_irq=46;
static DECLARE_WAIT_QUEUE_HEAD( wait_irq );

/* IRQ */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 21)
static irqreturn_t dev_irq_handler(int irq,void*dev_id) {
#else
static irqreturn_t dev_irq_handler(int irq,void*dev_id,struct pt_regs*regs) {
#endif
  wake_up_all( &wait_irq );
  return IRQ_HANDLED;
}
#endif

static u32 opencnt=0;

/* char-dev */
static int dev_open(struct inode*inode,struct file*filp) {
  if (opencnt++==0) {
    DSPidle=0;
    MDCTL39=0x00000103;		/* Go! Go, go Go! */
    while(DSPidle==0);
  }
  return 0;
}
static int dev_release(struct inode*inode,struct file*filp) {
  if (--opencnt==0) {
    MDCTL39=0x00000000;		/* local reset */
  }
  return 0;
}

static ssize_t dev_write(struct file*filp,const char __user*buffer,size_t len,loff_t*off) {
  long ret=0;
  unsigned long offset=*off;
  if (offset<D_LEN) {
    if ((offset+len)>=D_LEN) { len=D_LEN-offset; }
//    printk(KERN_INFO "c64x+ : read got offset %08lx %08lx\n",offset,(long)len);
    ret=len;
    *off+=len;
  }
  return ret;
}
static ssize_t dev_read(struct file*filp,char __user*buffer,size_t len,loff_t*off) {
  long ret=0;
  unsigned long offset=*off;
  if (offset<D_LEN) {
    if ((offset+len)>=D_LEN) { len=D_LEN-offset; }
//    printk(KERN_INFO "c64x+ : read got offset %08lx %08lx\n",offset,(long)len);
    ret=len;
    ret-=copy_to_user(buffer,(l2ram+offset),len);
    *off+=len;
  }
  return ret;
}

static int dev_mmap(struct file * file, struct vm_area_struct * vma) {
  size_t size=vma->vm_end-vma->vm_start;
  if (vma->vm_pgoff) {
    if (size!=R_LEN) return -EINVAL;
#if defined(pgprot_writecombine)
    vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
#else
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
#endif
    if (remap_pfn_range(vma,
			  vma->vm_start,
			  R_BASE>>PAGE_SHIFT,
			  size,
			  vma->vm_page_prot))
      return -EAGAIN;
  }
  else {
    if (size!=Q_LEN) return -EINVAL;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    if (remap_pfn_range(vma,
			  vma->vm_start,
			  Q_BASE>>PAGE_SHIFT,
			  size,
			  vma->vm_page_prot))
      return -EAGAIN;
  }
  return 0;
}

static void
c64x_dump( const char *condition )
{
  static const char *state_names[] = { "DONE", "ERROR", "TODO", "RUNNING" };

  uint32_t  ql_dsp = c64xctl->QL_dsp;
  uint32_t  ql_arm = c64xctl->QL_arm;
  uint32_t  tl_dsp = queue_l[ql_dsp & C64X_QUEUE_MASK].c64x_function;
  uint32_t  tl_arm = queue_l[ql_arm & C64X_QUEUE_MASK].c64x_function;
  int       dl;

  dl = ql_arm - ql_dsp;
  if (dl < 0)
    dl += C64X_QUEUE_LENGTH;

  printk( "C64X+ Queue: %s\n"
          "   [DSP %d / %d (%s), ARM %d / %d (%s)] <- %d pending\n",
          condition,
          ql_dsp, (tl_dsp >> 2) & 0x3fff, state_names[tl_dsp & 3],
          ql_arm, (tl_arm >> 2) & 0x3fff, state_names[tl_arm & 3],
          dl );
}

static int
c64x_wait_low( void )
{
  int ret;
  int num  = 0;
  /* Keep reference values for comparison. */
  u32 idle = c64xctl->idlecounter;
  u32 dsp  = c64xctl->QL_dsp;

  /* Wait for equal pointers... */
  while (dsp != c64xctl->QL_arm) {
    /* ...each time for a 1/50 second... */
    ret = wait_event_interruptible_timeout( wait_irq, c64xctl->QL_dsp == c64xctl->QL_arm, HZ/50 );
    if (ret < 0)
      return ret;

    /* ...if after that 1/50 second still the same command is running... */
    if (!ret && c64xctl->QL_dsp == dsp) {
      /* ...and almost one second elapsed in total, or the DSP felt idle... */
      if (++num > 42 || c64xctl->idlecounter != idle) {
        /* ...timeout! */
        printk( KERN_ERR "c64x+ : timeout waiting for idle queue\n" );
        c64x_dump( "TIMEOUT!!!" );
        return -ETIMEDOUT;
      }
    }
    else {
      /* Different command running, reset total elapsed time. */
      num = 0;
    }

    /* Update reference values. */
    idle = c64xctl->idlecounter;
    dsp  = c64xctl->QL_dsp;
  }

  return 0;
}

static int dev_ioctl(struct inode *i, struct file *f, unsigned int cmd, unsigned long arg) {
  switch (cmd) {
  case C64X_IOCTL_RESET:
    MDCTL39=0x00000000;		/* local reset */
    mdelay(10);
    DSPidle=0;
    MDCTL39=0x00000103;
    break;
  case C64X_IOCTL_WAIT_LOW:
    return c64x_wait_low();
  default:
    printk(KERN_INFO "c64x+ : unknown ioctl : cmd=%08x\n",cmd);
    return -EAGAIN;
    break;
  }
  return 0;
}
static struct file_operations dev_file_ops={
  .owner	= THIS_MODULE,
  .open 	= dev_open,
  .release	= dev_release,
  .read 	= dev_read,
  .write	= dev_write,
  .mmap		= dev_mmap,
  .ioctl	= dev_ioctl,
};

/* INIT */
static __initdata struct device dev_device = {
  .bus_id = "c64x0",
};
static int __init dev_init(void) {
  int ret=-EIO;
  u8 *at;
  const struct firmware*fw = NULL;

  printk(KERN_INFO "c64x+ : module load\n");

  if ((dram=ioremap(R_BASE,R_LEN))==0) {
    printk(KERN_ERR "c64x+ : module couldn't get memory\n");
    goto err0;
  }
  printk(KERN_INFO "c64x+ : module got memory @ %p\n",dram);
  queue_l = dram + 0x01e00000;

  /* get the 'device' memory */
  if ((mmr=ioremap(IO_BASE,IO_LEN))==0) {
    printk(KERN_ERR "c64x+ : module couldn't get IO-MMR\n");
    goto err0;
  }
  printk(KERN_INFO "c64x+ : DSP bootaddr: %08x\n",DSPBOOTADDR);
  printk(KERN_INFO "c64x+ : got mmr %p %08x %08x\n",mmr,MDCTL39,MDCFG39);

  printk(KERN_INFO "c64x+ : switch state: %08x\n",CHP_SHRTSW);

  MDCTL39=0x00000000;		/* local reset */
  mdelay(10);
  DSPBOOTADDR=CODE_BASE;	/* set DSP base address */

//  printk(KERN_INFO "c64x+ : check0: %p %08x %08x\n",mmr,MDCTL39,MDCFG39);

  /* get the 'device' memory */
  if ((l1dram=ioremap(Q_BASE,Q_LEN))==0) {
    printk(KERN_ERR "c64x+ : module couldn't get L1 dsp-memory\n");
    goto err1;
  }
  printk(KERN_INFO "c64x+ : module got L1D @ %p\n",l1dram);
  c64xctl = (volatile void*)l1dram;

  if ((l2ram=ioremap(D_BASE,D_LEN))==0) {
    printk(KERN_ERR "c64x+ : module couldn't get L2 dsp-memory\n");
    goto err2;
  }
  printk(KERN_INFO "c64x+ : module got L2 @ %p\n",l2ram);

  /* request firmware */
  device_initialize(&dev_device);
  ret=device_add(&dev_device);
  if (ret) {
       printk(KERN_ERR "c64x+ : device_add failed\n");
       goto err3;
  }
  printk(KERN_INFO "c64x+ : module requesting firmware '%s'\n",F_NAME);
  ret=request_firmware(&fw,F_NAME,&dev_device);
  printk(KERN_INFO "c64x+ : module got fw %p\n",fw);
  if (ret) {
    printk(KERN_ERR "c64x+ : no firmware upload (timeout or file not found?)\n");
    device_del(&dev_device);
    goto err3;
  }
  printk(KERN_INFO "c64x+ : firmware upload %p %zd\n",fw->data,fw->size);
  if (fw->size>32767) {
    printk(KERN_ERR "c64x+ : firmware too big! 32767 is maximum (for now)\n");
    release_firmware(fw);
    device_del(&dev_device);
    goto err3;
  }
  if (memcmp(fw->data+8,"C64x+DV",8)) {
    printk(KERN_ERR "c64x+ : firmware signature missing\n");
    release_firmware(fw);
    device_del(&dev_device);
    goto err3;
  }
  at = fw->data + fw->size;
  while ((ulong)--at > (ulong)fw->data) {
       if (*at == '@')
            break;
  }
  if (at == fw->data) {
    printk(KERN_ERR "c64x+ : firmware tag missing\n");
    release_firmware(fw);
    device_del(&dev_device);
    goto err3;
  }
  printk(KERN_NOTICE "c64x+ : got firmware of length %d at %p with tag '%*s' of length %d at %p+1\n",
         fw->size, fw->data,
         (int)((ulong)(fw->data + fw->size) - (ulong)at - 1), at + 1,
         (int)((ulong)(fw->data + fw->size) - (ulong)at - 1), at );
  /* move firmware into the hardware buffer here. */
  memcpy(l2ram,fw->data,fw->size);
  release_firmware(fw);
  device_del(&dev_device);

#if 0
  /* release DSP */
  printk(KERN_INFO "c64x+ : check1: %p %08x %08x\n",mmr,MDCTL39,MDCFG39);
  MDCTL39=0x00000103;		/* Hopefully run... */
  printk(KERN_INFO "c64x+ : check2: %p %08x %08x\n",mmr,MDCTL39,MDCFG39);
  printk(KERN_INFO "c64x+ : check3: %08x\n",DSPBOOTADDR);
#endif

  /* register char-dev */
  dev_major=MKDEV(C_MOD_MAJOR,0);
  ret=register_chrdev_region(dev_major,C_MOD_NUM_DEV,C_MOD_NAME);
  if (ret) {
    printk(KERN_ERR "c64x+ : can't get chrdev %d\n",C_MOD_MAJOR);
    goto err3;
  }

  /* allocate cdev */
  dev_cdev=cdev_alloc();
  dev_cdev->ops=&dev_file_ops;
  	/* cdev_init(&dev_data.cdev,&dev_file_ops); */
  ret=cdev_add(dev_cdev,dev_major,1);
  if (ret) {
    printk(KERN_ERR "c64x+ : can't allocate cdev\n");
    goto err4;
  }

#ifdef C64X_IRQ
  /* allocate interrupt slot */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 21)
  ret=request_irq(dev_irq,dev_irq_handler,IRQF_DISABLED,C_MOD_NAME,NULL);
#else
  ret=request_irq(dev_irq,dev_irq_handler,SA_INTERRUPT ,C_MOD_NAME,NULL);
#endif
  if (ret) {
    printk(KERN_ERR "c64x+ : can't get IRQ %d\n",dev_irq);
    goto err5;
  }
#endif

  /* tell sysfs/udev */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 19)
  dev_class=class_create(THIS_MODULE,C_MOD_NAME);
#else
  dev_class=class_simple_create(THIS_MODULE,C_MOD_NAME);
#endif
  if (IS_ERR(dev_class)) {
    ret=PTR_ERR(dev_class);
    printk(KERN_ERR "c64x+ : can't allocate class\n");
    goto err6;
  }
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 19)
  class_device_create(dev_class,NULL,dev_major,NULL,C_MOD_NAME"%d",0);
#else
  class_simple_device_add(dev_class,dev_major,NULL,C_MOD_NAME"%d",0);
#endif

  printk(KERN_INFO "c64x+ : module load finished\n");
  return 0;
  /* error out */
err6:
#ifdef C64X_IRQ
  free_irq(dev_irq,0);
err5:
#endif
  cdev_del(dev_cdev);
err4:
  unregister_chrdev_region(dev_major,1);
err3:
  iounmap(l2ram);
err2:
  iounmap((void*)l1dram);
err1:
  iounmap((void*)mmr);
err0:
  if (dram)
    iounmap((void*)dram);
  return ret;
}
module_init(dev_init);

/* EXIT */
static void __exit dev_exit(void) {
  /* Put the DSP into Reset */
  MDCTL39=0x00000000;
  /* release the DSP memory */
  iounmap((void*)mmr);
  iounmap((void*)l1dram);
  iounmap(l2ram);
  /* release all the other resources */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 19)
  class_device_destroy(dev_class,dev_major);
  class_destroy(dev_class);
#else
  class_simple_device_remove(dev_major);
  class_simple_destroy(dev_class);
#endif
#ifdef C64X_IRQ
  free_irq(dev_irq,0);
#endif
  cdev_del(dev_cdev);
  unregister_chrdev_region(dev_major,C_MOD_NUM_DEV);
}
module_exit(dev_exit);

