/*
 This usbio library is designed to communicate to an FPGA target via
 Cypress FX2 USB device. The library together with the FPGA local bus
 master (fx2.v) makes the FPGA local bus appear to the Linux host as a
 memory-mapped bus with 32-bit address and 32-bit data. Two USB
 endpoints are used:
 
 EP2: write transfers (control and data)
 EP6: read transfers (data)
 
 The library also implements functions for FX2 firmware loading, I2C
 communication (PROM), FIFO/FPGA interface reset control, and USB error
 status readout.
 
 2006-10-21: Created
 2006-11-08: Converted to usbdevfs bulk ioctl interface
 2008-08-08: Moved from XEM3001 development platform to LLRF4

 Copyright (c) 2006-2009 Dimtel, Inc., All Rights Reserved

 $Id: usbio_bulk.c,v 1.1.1.1 2010/10/05 16:42:44 dim Exp $
 $Date: 2010/10/05 16:42:44 $
 $Author: dim $
 $Revision: 1.1.1.1 $
 $Log: usbio_bulk.c,v $
 Revision 1.1.1.1  2010/10/05 16:42:44  dim
 Initial import of iGp12

 Revision 1.2  2010/03/12 19:04:59  dim
 Updates for multiple boards, bug fixes

 Revision 1.1.1.1  2009/06/02 20:26:02  dim
 Initial import

 Revision 1.3  2009/06/02 19:56:51  dim
 Full support for service functions (fw load, 8051 control, EEPROM) added


*/

#include <stdio.h>
#include <error.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <linux/usbdevice_fs.h>

#include "usbio_bulk.h"
#include "../include/usb_defs.h"
#include "../include/vendor_commands.h"
#include "../include/iGp12_usb.h"

char *exnm = NULL;

struct usb_device *find_device(u_int16_t vid, u_int16_t pid, char *serial,
                              u_int16_t *bcdDevice)
{
  char ser_buf[1024];
  struct usb_bus *p;
  struct usb_device *q;
  usb_dev_handle *handle;

  p=usb_busses;
  while(p!=NULL)
  {
    q=p->devices;
    while(q!=NULL)
    {
      if((q->descriptor.idVendor == vid) && (q->descriptor.idProduct == pid))
      {
        if (serial)
        {
          if ((handle = usb_open(q)))
          {
            if (usb_get_string_simple(handle, q->descriptor.iSerialNumber, ser_buf, 1024))
              if (strcmp(ser_buf, serial) == 0)
              {
                usb_close(handle);
                return q;
              }
            usb_close(handle);
          }
        }
        else if (bcdDevice)
        {
          if (q->descriptor.bcdDevice == *bcdDevice)
            return q;
        }
        else
          return q;
      }
      q=q->next;
    }
    p=p->next;
  }
  return NULL;
}

usbio_context *usbio_open(int flag)
{
  return usbio_open_full(flag, NULL, NULL);
}

usbio_context *usbio_open_serial(int flag, char *serial)
{
  return usbio_open_full(flag, serial, NULL);
}

usbio_context *usbio_open_full(int flag, char *serial, unsigned short *bcd)
{
  int rc;
  usbio_context *ctxt;

  ctxt = (usbio_context *)malloc(sizeof(usbio_context));
  if (ctxt == NULL)
    return NULL;

  /* Set up the mutex */
  pthread_mutex_init(&ctxt->mutex, NULL);

  /* Get USB going */
  usb_init();
  usb_find_busses();
  usb_find_devices();

  /* Find our device by the VID/PID. Needs fixing to accept,
     for example, a descriptor string. */
  ctxt->device=find_device(IGP12_VID, IGP12_PID, serial, bcd);
  if (ctxt->device == NULL)
    ctxt->device = find_device(USBIO_VENDOR, USBIO_PRODUCT, serial, bcd);

  if (ctxt->device==NULL)
  {
#ifdef USBIO_DEBUG
    fprintf(stderr, 
    "%s: usbio_open(): devices 0x%04x:0x%04x and 0x%04x:0x%04x not found\n",
            exnm, IGP12_VID, IGP12_PID, USBIO_VENDOR, USBIO_PRODUCT);
#endif
    free(ctxt);
    return NULL;
  }

  ctxt->handle = usb_open(ctxt->device);

  if (!flag)
  {
    if (usb_claim_interface(ctxt->handle, 0) < 0)
    {
#ifdef USBIO_DEBUG
      fprintf(stderr, "%s: usbio_open(): could not claim interface 0: %s\n", 
              exnm, usb_strerror());
#endif
      usb_close(ctxt->handle);
      free(ctxt);
      return NULL;
    }
  
    /* Reset the FPGA state machine and FIFOs */
    rc = usbio_if_reset(ctxt);
    if (rc < 0)
    {
#ifdef USBIO_DEBUG
      fprintf(stderr,"%s: usbio_open(): usb_if_reset() failed: %s\n",
              exnm, usb_strerror());
#endif
      usbio_close(ctxt, 0);
      return NULL;
    }
    
    usb_set_altinterface(ctxt->handle, 1);
  }

  /* Extract the file descriptor - this is not very portable,
     but to do this right need usbi.h */
  ctxt->fd = *(int *)(ctxt->handle);

  return ctxt;
}

void usbio_close(usbio_context *ctxt, int flag)
{
  if (!flag)
    usb_release_interface(ctxt->handle, 0);
  usb_close(ctxt->handle);
  free(ctxt);
}

unsigned int usbio_write(usbio_context *ctxt, unsigned int addr, 
                              unsigned int data)
{
  return usbio_block_write(ctxt, addr, &data, 1);
}

unsigned int usbio_block_write(usbio_context *ctxt, unsigned int addr, 
                      unsigned int *data, unsigned int count)
{
  char *ptr = (char *)data;
  unsigned int buf[USBIO_MAXLEN/sizeof(int)];
  unsigned int bytes, len, rc, first = 1;
  struct usbdevfs_bulktransfer btf;
  
  pthread_mutex_lock(&ctxt->mutex);

#if 0
  if ((addr <= 9) && (addr+count > 9))
    printf("./usbwblk 0x%08x 0x%x < /dev/zero\n", addr, count);
#endif
  
  btf.ep      = USBIO_EP_WRITE;
  btf.data    = (void *)buf;
  btf.timeout = USBIO_TIMEOUT;
  
  bytes = (count+2)*sizeof(int);
  while (bytes > 0)
  {
    len = MIN(bytes, USBIO_MAXLEN);
    bytes -= len;
    btf.len = len;

    /* Form a transaction */
    if (first)
    {
      buf[0] = count & ~USBIO_M_RW;
      buf[1] = addr;
      len   -= 2*sizeof(int);
      memcpy(buf+2, ptr, len);
    }
    else
      btf.data = (void *) ptr;

    ptr  += len;
    
    /* Send it out */
    rc = ioctl(ctxt->fd, USBDEVFS_BULK, &btf);
    if (rc != btf.len)
    {
#ifdef USBIO_DEBUG
      fprintf(stderr, 
      "%s: usbio_block_write(): request for bulk write failed, %d/%d\n",
      exnm, rc, btf.len);
#endif
      pthread_mutex_unlock(&ctxt->mutex);
      len = rc/sizeof(int) + (unsigned int)((unsigned int *)ptr - data);
      if (first)
        return len - 2;
      else
        return len;
    }
    first = 0;
  }
  
  pthread_mutex_unlock(&ctxt->mutex);
  return (unsigned int)((unsigned int *)ptr - data);
}

unsigned int usbio_block_read(usbio_context *ctxt, unsigned int addr,
                     unsigned int *data, unsigned int count)
{
  char *ptr = (char *)data;
  unsigned int buf[2];
  unsigned int rc, bytes;
  struct usbdevfs_bulktransfer btf_write, btf_read;
  
  pthread_mutex_lock(&ctxt->mutex);
  
  btf_write.ep      = USBIO_EP_WRITE;
  btf_write.len     = sizeof(buf);
  btf_write.timeout = USBIO_TIMEOUT;
  btf_write.data    = (void *)buf;
  
  btf_read.ep       = USBIO_EP_RDATA | USB_ENDPOINT_IN;
  btf_read.timeout  = USBIO_TIMEOUT;
  
  /* Form a transaction */    
  buf[0] = count | USBIO_M_RW;
  buf[1] = addr;
  /* Send it out */
  rc = ioctl(ctxt->fd, USBDEVFS_BULK, &btf_write);
  if (rc != btf_write.len)
  {
#ifdef USBIO_DEBUG
    fprintf(stderr, "%s: usbio_block_read(): request for bulk write failed\n",
            exnm);
#endif
    pthread_mutex_unlock(&ctxt->mutex);
    return 0;
  }

  /* Now read the data */
  bytes = count * sizeof(int);
  while (bytes > 0)
  {
    btf_read.len = MIN(bytes, USBIO_MAXLEN);
    bytes -= btf_read.len;

    btf_read.data = (void *)ptr;
    rc = ioctl(ctxt->fd, USBDEVFS_BULK, &btf_read);
    if (rc != btf_read.len)
    {
#ifdef USBIO_DEBUG
      fprintf(stderr, 
      "%s: usbio_block_read(): request for bulk read failed (%d/%d)\n",
      exnm, rc, btf_read.len);
#endif
      pthread_mutex_unlock(&ctxt->mutex);
      return (unsigned int)((unsigned int *)ptr - data + rc/sizeof(unsigned int));
    }
    ptr += rc;
  }
  
  pthread_mutex_unlock(&ctxt->mutex);
  return (unsigned int)((unsigned int *)ptr - data);
}

unsigned int usbio_mult_write(usbio_context *ctxt, unsigned int *addr, 
                              unsigned int *data, int count)
{
/*  unsigned int buf[3];
  unsigned int bytes, idx, rc;
  struct usbdevfs_bulktransfer btf; */
  unsigned int idx;
  
  if (count*3*sizeof(int) > USBIO_MAXLEN)
    return 0;

  if (ctxt == NULL)
    return -1;
    
  for (idx = 0; idx < count; idx++)
    usbio_block_write(ctxt, addr[idx], data+idx, 1);
 
  return idx;

/*
  pthread_mutex_lock(&ctxt->mutex);
  
  btf.ep      = USBIO_EP_WRITE;
  btf.data    = (void *)buf;
  btf.timeout = USBIO_TIMEOUT;
  btf.len     = 3*sizeof(unsigned int);
  
  for (idx = 0; idx < count; idx++)
  {
    buf[0] = 1;
    buf[1] = addr[idx];
    buf[2] = data[idx];
    rc = ioctl(ctxt->fd, USBDEVFS_BULK, &btf);
  }
  
  pthread_mutex_unlock(&ctxt->mutex);
  
  return rc/3;
*/
}

static int usbio_vendor_cmd(usbio_context *ctxt, int bmRequest,
                            int Value, int Index, char *buf, int cnt)
{
  int rc;
  int bmRequestType = (bmRequest & USB_DIR_IN) | USB_TYPE_VENDOR;

  rc = usb_control_msg(ctxt->handle, bmRequestType, bmRequest,
                       Value, Index, (char *)buf, cnt, USBIO_TIMEOUT);
  return rc;
}

int usbio_if_reset(usbio_context *ctxt)
{
  /* Pulse FPGA RESET & reset FX2 FIFOs (handled by the firmware) */
  return usbio_vendor_cmd(ctxt, USB_REQ_VENDOR_FPGA_RESET, 0, 0, 
                          NULL, 0);
}

int usbio_i2c_write(usbio_context *ctxt, char addr, const void *buf, int len)
{
  if (len < 1 || len > EP0_MAX_PKT_SIZE)
    return -1;
  
  return usbio_vendor_cmd(ctxt, USB_REQ_VENDOR_I2C_WRITE, (int)addr, 0,
                          (char *)buf, len);
}

int usbio_i2c_read(usbio_context *ctxt, char addr, void *buf, int len)
{
  if (len < 1 || len > EP0_MAX_PKT_SIZE)
    return -1;
  
  return usbio_vendor_cmd(ctxt, USB_REQ_VENDOR_I2C_READ, (int)addr, 0,
                          (char *)buf, len);
}

int usbio_usberr_read(usbio_context *ctxt)
{
  char buf;
  int rc;
  
  rc = usbio_vendor_cmd(ctxt, USB_REQ_VENDOR_GET_STATUS, 0, 0, &buf, 1);
  
  if (rc != 1)
    return -1;
  else
    return (int)buf;
}

int usbio_write_sernum(usbio_context *ctxt, char *sernum)
{
  char *buf;
  int length = strlen(sernum), rc;
  
  if (length > 255)
    return -1;
  
  buf = (char *)malloc(length+5);
  
  if (buf == NULL)
    return -1;
  
  *buf     = (EEPROM_SERIAL >> 8) & 0xff;
  *(buf+1) = EEPROM_SERIAL & 0xff;
  *(buf+2) = EEPROM_SERIAL_HDR1;
  *(buf+3) = EEPROM_SERIAL_HDR2;
  *(buf+4) = length;
  memcpy(buf+5, sernum, length);
  
  rc = usbio_i2c_write(ctxt, I2C_EEPROM_ADDR, buf, length+5);
  free(buf);
  return rc;
}

static int hex2nibble(char c)
{
  if (c >= '0' && c <= '9')
    return c - '0';
  if (c >= 'a' && c <= 'f')
    return c - 'a' + 10;
  if (c >= 'A' && c <= 'F')
    return c - 'A' + 10;
  
  /* Invalid */
  return -1;
}

int string2bytes(char *buf, char *string, int bufsize)
{
  int len = strlen(string), bptr = 0, sptr = 0, val;

  if (len % 2)
  {
    fprintf(stderr, "%s: data string must have even number of characters\n",
            exnm);
    return 0;
  }
  
  if (len/2 > bufsize)
  {
    fprintf(stderr, "%s: data length of %d exceeds maximum size of %d bytes\n",
            exnm, len>>1, bufsize);
    return 0;
  }
  
  while (*(string+sptr))
  {
    val = hex2nibble(*(string+sptr));
    if (val < 0)
    {
      fprintf(stderr, "%s: invalid character ""%c"" at location %d\n", 
              exnm, *(string+sptr), sptr);
      return 0;
    }
    *(buf+bptr) = (sptr & 1) ? *(buf+bptr) | val : val << 4;
    
    if (sptr++ & 1) bptr++;
  }
  return bptr;
}

int usbio_cpu_reset(usbio_context *ctxt, int value)
{
  char reg_value = value & 0xff;

  /* Cannot use usbio_vendor_cmd - single request for reading and writing */
  return usb_control_msg(ctxt->handle, USB_TYPE_VENDOR, USB_REQ_VENDOR_FW_LOAD,
                       0xe600, 0, &reg_value,  1, USBIO_TIMEOUT);
}

int usbio_readram(usbio_context *ctxt, int addr, int length, unsigned char **ram)
{
  unsigned char *p;
  int cnt;
  *ram = (unsigned char *)malloc(length);
  
  if (*ram == NULL)
    return -1;
  
  p = *ram;
  while (length)
  {
    cnt = usb_control_msg(ctxt->handle, USB_DIR_IN | USB_TYPE_VENDOR,
                       USB_REQ_VENDOR_FW_LOAD, addr, 0, (char *)p, 
                       MIN(USBIO_FW_CHUNK, length), USBIO_TIMEOUT);
    if (cnt < 0)
    {
      free(*ram);
      return -1;
    }
    else
    {
      length -= cnt;
      p      += cnt;
      addr   += cnt;
    }
  }
  return (p - *ram);
}

int usbio_writeram(usbio_context *ctxt, int addr, int length, unsigned char *data)
{
  unsigned char *p = data;
  int cnt;
  
  while (length)
  {
    cnt = usb_control_msg(ctxt->handle, USB_TYPE_VENDOR,
                       USB_REQ_VENDOR_FW_LOAD, addr, 0, (char *)p, 
                       MIN(USBIO_FW_CHUNK, length), USBIO_TIMEOUT);
    if (cnt < 0)
      return -1;
    else
    {
      length -= cnt;
      p      += cnt;
      addr   += cnt;
    }
  }
  return (p - data);
}

int usbio_fw_load(usbio_context *ctxt, char *file)
{
  /* Valid Intel HEX line is at most 521 character long. Add end of line
  symbols (<=2), and 0. Let there be some margin :) */
  char s[532], *p, eof = 0;
  unsigned char  byte, count, type, xsum, sum_val, data[255];
  unsigned short addr;
  int byte_count = 0, line = 0, rc;
  FILE *fid;

  fid = fopen(file, "r");
  if (fid == NULL)
  {
    error(0, errno, "usbio_fw_load(): file %s could not be opened for reading", file);
    return -1;
  }

  /* Start from halting 8051 */
  usbio_cpu_reset(ctxt, 1);

  while ((fgets(s, 532, fid) != NULL) && !eof)
  {
    p = s; line++;
    if (*p++ != ':')
    {
      error(0, 0, "usbio_fw_load(): invalid string in %s, start character %c, skipping",
            file, *s);
      continue;
    }
    /* Must have at least 10 characters for a valid record */
    if (strlen(p) < 10)
    {
      error(0, 0, "usbio_fw_load(): invalid record, line %d, skipping", line);
      continue;
    }
    count  = hex2nibble(*p++) << 4;
    count |= hex2nibble(*p++);
    addr   = hex2nibble(*p++) << 12;
    addr  |= hex2nibble(*p++) << 8;
    addr  |= hex2nibble(*p++) << 4;
    addr  |= hex2nibble(*p++);
    type   = hex2nibble(*p++) << 4;
    type  |= hex2nibble(*p++);
    
    /* Make sure we know what to do with this type */
    switch (type)
    {
      case 0: /* Read bytes, write to FX2 */
        /* Check length first */
        if (strlen(p) < (int)count * 2 + 2)
        {
          error(0, 0, "usbio_fw_load(): short data on line %d, %d bytes read, %d expected, aborting",
                line, strlen(p), (int)count * 2);
          return -1;
        }
        xsum = count + (addr & 0xff) + (addr>>8) + type;
        for (byte = 0; byte < count; byte++)
        {
          data[byte]  = hex2nibble(*p++) << 4;
          data[byte] |= hex2nibble(*p++);
          xsum += data[byte];
        }
        
        /* Read checksum */
        sum_val  = hex2nibble(*p++) << 4;
        sum_val |= hex2nibble(*p++);
        if (((xsum+sum_val) & 0xff) != 0)
        {
          error(0, 0, "usbio_fw_load(): checksum error on line %d, aborting", line);
          return -1;
        }
        
        /* Write the data */
        if ((rc = usbio_writeram(ctxt, addr, count, data)) != count)
        {
          error(0, 0, "usbio_fw_load(): error writing data to FX2 (%d/%d), aborting\n",
                rc, count);
          return -1;
        }
        byte_count += count;
        break;
      case 1: /* EOF record, return */
        eof = 1;
        break;
      default:
        error(0, 0, "usbio_fw_load(): invalid record type %d\n", type);
    }
  }

  /* Release 8051 */
  usbio_cpu_reset(ctxt, 0);
  
  return byte_count;
}
