/*
 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.
 
 2006-10-21: Created

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

 $Id: usbio.c,v 1.2 2013/09/05 03:35:32 dim Exp $
 $Date: 2013/09/05 03:35:32 $

 $Author: dim $
 $Revision: 1.2 $
 $Log: usbio.c,v $
 Revision 1.2  2013/09/05 03:35:32  dim
 Converted to the latest libusb-1.0 interface

 Revision 1.1  2012/11/11 05:25:19  dim
 Moved from iGp12/src/usbio, updated to handle LLRF4 and iGp12

 Revision 1.2  2012/08/29 06:45:59  dim
 Added usbio_write_eeprom(), write_offsets command

 Revision 1.1.1.1  2010/10/05 16:42:44  dim
 Initial import of iGp12

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

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

 Revision 1.1.1.1  2006/11/16 19:38:15  dim
 Initial import

*/

#include <stdlib.h>
#include <stdio.h>
#include <error.h>
#include <errno.h>
#include <assert.h>
#include <pthread.h>

#include "usbio.h"
#include "../include/eeprom.h"
#include "../include/usb_defs.h"
#include "../include/vendor_commands.h"
#include "../include/board_ids.h"

char *exnm = NULL;

#if 0
const char *libusb_error_name(int errcode)
{
  static char buf[20];
  
  sprintf(buf, "%d", errcode);
  
  return buf;
}
#endif

libusb_device_handle *compare_device(libusb_device *device, 
      struct libusb_device_descriptor *desc, u_int16_t vid,
      u_int16_t pid, char *serial, u_int16_t *bcdDevice)
{
  char ser_buf[1024];
  libusb_device_handle *handle;
  
  /* Get the descriptor */
  libusb_get_device_descriptor(device, desc);
  
  if (desc->idVendor != vid)
    return NULL;
  
  if (desc->idProduct != pid)
    return NULL;
  
  if (serial)
  {
    if (libusb_open(device, &handle) == 0)
    {
      libusb_get_string_descriptor_ascii(handle, desc->iSerialNumber,
                                         (unsigned char *)ser_buf, 1024);
      if (strncmp(ser_buf, serial, 1024) == 0)
        return handle;
      else
      {
        libusb_close(handle);
        return NULL;
      }
    }
    else
      return NULL;
  }
  else if ((bcdDevice == NULL) || (desc->bcdDevice == *bcdDevice))
  {
    if (libusb_open(device, &handle) == 0)
      return handle;
    else
      return NULL;
  }

  /* Should never reach this */
  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;
  pthread_mutexattr_t attr;

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

  /* Set up the mutex */
  pthread_mutexattr_init(&attr);
  pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
  pthread_mutex_init(&ctxt->mutex, &attr);

  /* Get USB going - create default context */
  libusb_init(NULL);
  
  /* discover devices */
  libusb_device **list;
  ssize_t cnt = libusb_get_device_list(NULL, &list);
  ssize_t i = 0;
  if (cnt < 0)
  {
#ifdef USBIO_DEBUG
    fprintf(stderr, 
    "%s: usbio_open(): libusb_get_device_list() returned %d", exnm, (int)cnt);
#endif
    free(ctxt);
    return NULL;
  }
  
  for (i = 0; i < cnt; i++)
  {
    libusb_device *device = list[i];
    ctxt->handle = compare_device(device, &ctxt->desc,
                                  FX2_DEFAULT_VID, FX2_DEFAULT_PID, serial, bcd);
    if (ctxt->handle)
      break;

    ctxt->handle = compare_device(device, &ctxt->desc,
                                  IGP12_VID, IGP12_PID, serial, bcd);
    if (ctxt->handle)
      break;

    ctxt->handle = compare_device(device, &ctxt->desc,
                                  LLRF4_VID, LLRF4_PID, serial, bcd);
    if (ctxt->handle)
      break;

    ctxt->handle = compare_device(device, &ctxt->desc,
                                  LLRF46_VID, LLRF46_PID, serial, bcd);
    if (ctxt->handle)
      break;
  }
                                
  libusb_free_device_list(list, 1);

  if (ctxt->handle == NULL)
  {
#ifdef USBIO_DEBUG
    fprintf(stderr, 
    "%s: usbio_open(): devices 0x%04x:0x%04x, 0x%04x:0x%04x, 0x%04x:0x%04x, and 0x%04x:0x%04x not found\n",
            exnm, FX2_DEFAULT_VID, FX2_DEFAULT_PID, IGP12_VID, IGP12_PID,
            LLRF4_VID, LLRF4_PID, LLRF46_VID, LLRF46_PID);
#endif
    free(ctxt);
    return NULL;
  }

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

void usbio_close(usbio_context *ctxt, int flag)
{
  if (!flag)
    libusb_release_interface(ctxt->handle, 0);
    
  libusb_close(ctxt->handle);
  libusb_exit(NULL);
  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)
{
  unsigned int *buf;
  unsigned int sz = (count + 2) * sizeof(int);
  unsigned int rc;
  int sent;

  if (count > USBIO_MAXLEN)
  {
    error_at_line(0, 0, __FILE__, __LINE__,
                  "write request size %d exceeds USBIO_MAXLEN", count);
    return 0;
  }
  
  if (count == 0)
  {
    error_at_line(0, 0, __FILE__, __LINE__,
                  "usbio_block_write(0x%x) called with zero count", addr);
    return 0; /* Do nothing, just return */
  }
  
  if ((rc = pthread_mutex_lock(&ctxt->mutex)))
  {
    error_at_line(0, errno, __FILE__, __LINE__,
                  "pthread_mutex_lock() returned %d", rc);
    return 0;
  }
  
  buf = (unsigned int *)malloc(sz);
  if (buf == NULL)
  {
    pthread_mutex_unlock(&ctxt->mutex);
#ifdef USBIO_DEBUG
    error_at_line(0, errno, __FILE__, __LINE__,
                 "malloc(%d) failed", sz);
#endif
    return 0;
  }
  
  /* Set up the header first */
  buf[0] = count & ~USBIO_M_RW;
  buf[1] = addr;
  memcpy(buf + 2, data, count*sizeof(unsigned int));
    
  /* Send it out */
  rc = libusb_bulk_transfer(ctxt->handle, USBIO_EP_WRITE, 
                            (unsigned char *)buf, 
                            sz, &sent, USBIO_TIMEOUT);
  free(buf);
  
  if (sent != sz || (rc && (rc != LIBUSB_ERROR_TIMEOUT)))
  {
    pthread_mutex_unlock(&ctxt->mutex);
#ifdef USBIO_DEBUG
    fprintf(stderr, "usbio_block_write: data write (0x%x) failed (%d/%d): %s\n",
            addr, sent, sz, libusb_error_name(rc));
#endif
    return sent/sizeof(int);
  }
  if (rc == LIBUSB_ERROR_TIMEOUT)
    fprintf(stderr, "usbio_block_write: bulk write (0x%x, %d/%d) returned timeout\n",
            addr, sent, sz);
  
  pthread_mutex_unlock(&ctxt->mutex);
  return count;
}

unsigned int usbio_block_read(usbio_context *ctxt, unsigned int addr,
                     unsigned int *data, unsigned int count)
{
  unsigned int buf[2];
  unsigned int sz, rc;
  int xfrd;

  if (count > USBIO_MAXLEN)
  {
    error_at_line(0, 0, __FILE__, __LINE__,
                  "Read request size %d exceeds USBIO_MAXLEN", count);
    return 0;
  }
  
  if (count == 0)
  {
    error_at_line(0, 0, __FILE__, __LINE__,
                  "usbio_block_read(0x%x) called with zero count", addr);
    return 0; /* Do nothing, just return */
  }
  
  
  if ((rc = pthread_mutex_lock(&ctxt->mutex)))
  {
    fprintf(stderr, "usbio_block_read(): pthread_mutex_lock() returned %d\n",
            rc);
    perror("usbio_block_read(): ");
  }

  buf[0] = count | USBIO_M_RW;
  buf[1] = addr;
 
  /* Send it out */
  sz = sizeof(buf);
  rc = libusb_bulk_transfer(ctxt->handle, USBIO_EP_WRITE, (unsigned char *)buf, 
                        sz, &xfrd, USBIO_TIMEOUT);
  if (xfrd != sz || (rc && (rc != LIBUSB_ERROR_TIMEOUT)))
  {
    pthread_mutex_unlock(&ctxt->mutex);
#ifdef USBIO_DEBUG
    fprintf(stderr, "usbio_block_read: header write (0x%x) failed %d/8: %s\n",
            buf[1], xfrd, libusb_error_name(rc));
#endif
    return 0;
  }
  if (rc == LIBUSB_ERROR_TIMEOUT)
    fprintf(stderr, "usbio_block_read: bulk write (0x%x, %d/%d) returned timeout\n",
            buf[1], xfrd, sz);
    
  sz = count * sizeof(int);
  rc = libusb_bulk_transfer(ctxt->handle, USBIO_EP_RDATA, (unsigned char *)data, 
                            sz, &xfrd, USBIO_TIMEOUT);
  if (xfrd != sz || (rc && (rc != LIBUSB_ERROR_TIMEOUT)))
  {
    pthread_mutex_unlock(&ctxt->mutex);
#ifdef USBIO_DEBUG
    fprintf(stderr, "usbio_block_read: bulk read (0x%x) failed %d/%d: %s\n",
            buf[1], xfrd, sz, libusb_error_name(rc));
#endif
    return xfrd/sizeof(int);
  }
  if (rc == LIBUSB_ERROR_TIMEOUT)
    fprintf(stderr, "usbio_block_read: bulk read (0x%x, %d/%d) returned timeout\n",
            buf[1], xfrd, sz);
  
  pthread_mutex_unlock(&ctxt->mutex);
  return count;
}

unsigned int usbio_mult_write(usbio_context *ctxt, unsigned int *addr, 
                              unsigned int *data, int count)
{
  int k, rc, xfrd;
  unsigned int *buf, *ptr, sz;
  
  if (count > USBIO_MULT_MAXLEN)
  {
    error_at_line(0, 0, __FILE__, __LINE__,
                  "write request size %d exceeds USBIO_MULT_MAXLEN", count);
    return 0;
  }

  if (ctxt == NULL)
    return -1;
  
  if (count == 0)
    return 0;
    
  sz  = 3*count*sizeof(unsigned int);
  buf = (unsigned int *)malloc(sz);
  if (buf == NULL)
  {
#ifdef USBIO_DEBUG
    error_at_line(0, errno, __FILE__, __LINE__,
                 "malloc(%d) failed", sz);
#endif
    return 0;
  }
  
  for (k = 0, ptr = buf; k < count; k++)
  {
    *ptr++ = 1; /* Count */
    *ptr++ = addr[k];
    *ptr++ = data[k];
  }
  
  pthread_mutex_lock(&ctxt->mutex);
  rc = libusb_bulk_transfer(ctxt->handle, USBIO_EP_WRITE, 
       (unsigned char *)buf, sz, &xfrd, USBIO_TIMEOUT);
  free(buf);
  if (xfrd != sz || (rc && (rc != LIBUSB_ERROR_TIMEOUT)))
  {
    pthread_mutex_unlock(&ctxt->mutex);
#ifdef USBIO_DEBUG
    fprintf(stderr, "usbio_mult_write: write (0x%x) failed %d/%d: %s\n",
            addr[0], xfrd, sz, libusb_error_name(rc));
#endif
    return xfrd/3/sizeof(unsigned int);
  }
  if (rc == LIBUSB_ERROR_TIMEOUT)
    fprintf(stderr, "usbio_mult_write: write (0x%x, %d/%d) returned timeout\n",
            addr[0], xfrd, sz);

  pthread_mutex_unlock(&ctxt->mutex);
  
  return xfrd/3/sizeof(unsigned int);
}

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 = libusb_control_transfer(ctxt->handle, bmRequestType, bmRequest,
                       Value, Index, (unsigned char *)buf, cnt,
                       USBIO_VENDOR_TIMEOUT);
  return rc;
}

int usbio_if_reset(usbio_context *ctxt)
{
  int rc;

  pthread_mutex_lock(&ctxt->mutex);
  /* Pulse FPGA RESET & reset FX2 FIFOs (handled by the firmware) */
  rc = usbio_vendor_cmd(ctxt, USB_REQ_VENDOR_FPGA_RESET, 0, 0, 
                          NULL, 0);
  pthread_mutex_unlock(&ctxt->mutex);
  return rc;
}

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;
}

int usbio_write_eeprom(usbio_context *ctxt, int addr, char *data, int length)
{
  char *buf;
  int rc;
  
  if (length > 32)
    return -1;
  
  buf = (char *)malloc(length+2);
  
  if (buf == NULL)
    return -1;
  
  *buf     = (addr >> 8) & 0xff;
  *(buf+1) = addr & 0xff;
  memcpy(buf+2, data, length);
  
  rc = usbio_i2c_write(ctxt, I2C_EEPROM_ADDR, buf, length+2);
  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)
{
  unsigned char reg_value = value & 0xff;

  /* Cannot use usbio_vendor_cmd - single request for reading and writing */
  return libusb_control_transfer(ctxt->handle, USB_TYPE_VENDOR, USB_REQ_VENDOR_FW_LOAD,
                       0xe600, 0, &reg_value,  1u, USBIO_VENDOR_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 = libusb_control_transfer(ctxt->handle, USB_DIR_IN | USB_TYPE_VENDOR,
                       USB_REQ_VENDOR_FW_LOAD, addr, 0, p, 
                       MIN(USBIO_FW_CHUNK, length), USBIO_VENDOR_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 = libusb_control_transfer(ctxt->handle, USB_TYPE_VENDOR,
                       USB_REQ_VENDOR_FW_LOAD, addr, 0, p, 
                       MIN(USBIO_FW_CHUNK, length), USBIO_VENDOR_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, (int)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;
}

char *usbio_read_serial(usbio_context *ctxt)
{
  char string[1024];

  libusb_get_string_descriptor_ascii(ctxt->handle, ctxt->desc.iSerialNumber,
                                     (unsigned char *)string, 1024);
  return strdup(string);
}
