/*======================================================================

    Device driver for Intel 82365 PCMCIA controller

    Written by David Hinds, dhinds@allegro.stanford.edu
    
======================================================================*/

#include <linux/types.h>
#include <linux/termios.h>
#include <linux/errno.h>
#include <linux/sched.h>
#include <linux/config.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/tty.h>
#include <linux/fcntl.h>
#include <linux/string.h>
#include <linux/pcmcia.h>

#include <asm/io.h>
#include <asm/bitops.h>
#include <asm/segment.h>
#include <asm/system.h>

#include "i82365.h"

/*====================================================================*/

static unsigned short I365_GET_PAIR(unsigned short slot,
                                    unsigned short reg)
  {
  unsigned int a, b;
  a = I365_GET(slot, reg);
  b = I365_GET(slot, reg+1);
  return (a + (b<<8));
  }

/*====================================================================*/

void i365_csc_on(unsigned int slot, int irq);
void i365_csc_off(unsigned int slot);
int i365_csc_poll(unsigned int slot);
void i365_csc_ack(unsigned int slot, int event);

int i365_ioctl(unsigned int slot, unsigned int cmd, unsigned long arg);

int i365_probe(struct pcmcia_ctrl *ctrl)
  {
  int id, slot;

  printk("Intel 82365 PCMCIA probe: ");
  for (slot = 0; slot < 8; slot++) {
    id = I365_GET(slot, I365_IDENT);
    if ((id != 0x82) && (id != 0x83)) break;
    }
  if (slot > 0) {
    printk("%d sockets found\n", slot);
    ctrl->csc_on = i365_csc_on;
    ctrl->csc_off = i365_csc_off;
    ctrl->csc_poll = i365_csc_poll;
    ctrl->csc_ack = i365_csc_ack;
    ctrl->ioctl = i365_ioctl;
    return slot;
    }
  else {
    printk("not found.\n");
    return -ENODEV;
    }
  } /* i365_probe */
  
/*====================================================================*/

void i365_csc_on(unsigned int slot, int irq)
  {
  /* Turn on all card status change interrupts */
  unsigned char ctl;
  I365_SET(slot, I365_CSCINT, (irq << 4) | I365_CSC_ANY);
  ctl = I365_GET(slot, I365_INTCTL);
  ctl &= ~I365_INTR_ENA;
  I365_SET(slot, I365_INTCTL, ctl);
  ctl = I365_GET(slot, I365_GENCTL);
  ctl |= I365_CTL_GPI_ENA;
  I365_SET(slot, I365_GENCTL, ctl);
  /* Set explicit write back acknowledge */
  ctl = I365_GET(slot, I365_GBLCTL);
  ctl |= I365_GBL_WRBACK;
  I365_SET(slot, I365_GBLCTL, ctl);
  } /* i365_csc_on */

void i365_csc_off(unsigned int slot)
  {
  /* Turn off all card status change interrupts */
  I365_SET(slot, I365_CSCINT, 0x00);
  } /* i365_csc_off */

int i365_csc_poll(unsigned int slot)
  {
  int ret;
  /* Anything to report? */
  int csc = I365_GET(slot, I365_CSC);
  ret  = (csc & I365_CSC_DEAD) ? PC_BATDEAD : 0;
  ret |= (csc & I365_CSC_WARN) ? PC_BATWARN : 0;
  ret |= (csc & I365_CSC_READY) ? PC_READY : 0;
  ret |= (csc & I365_CSC_DETECT) ? PC_DETECT : 0;
  ret |= (csc & I365_CSC_GPI) ? PC_GPI : 0;
  return ret;
  } /* i365_csc_poll */

void i365_csc_ack(unsigned int slot, int event)
  {
  int csc;
  /* Acknowledge events that have been processed */
  csc  = (event & PC_BATDEAD) ? I365_CSC_DEAD : 0;
  csc |= (event & PC_BATWARN) ? I365_CSC_WARN : 0;
  csc |= (event & PC_READY) ? I365_CSC_READY : 0;
  csc |= (event & PC_DETECT) ? I365_CSC_DETECT : 0;
  csc |= (event & PC_GPI) ? I365_CSC_GPI : 0;
  I365_SET(slot, I365_CSC, csc);
  } /* i365_csc_ack */

/*====================================================================*/

static void delay(int len)
  {
  int start = jiffies;
  int boguscount = 150000;
  sti();
  while (jiffies - start < len)
    if (boguscount-- < 0) {
      printk("jiffy failure (t=%ld)...", jiffies);
      break;
      }
  } /* delay */
  
/*====================================================================*/

int i365_get_status(unsigned int slot, unsigned int *value)
  {
  unsigned char status;
  status = I365_GET(slot, I365_STATUS);
  *value = ((status & I365_CS_DETECT) == I365_CS_DETECT) ? PC_DETECT : 0;
  *value |= (status & I365_CS_BVD1) ? PC_BATDEAD : 0;
  *value |= (status & I365_CS_BVD2) ? PC_BATWARN : 0;
  *value |= (status & I365_CS_WRPROT) ? PC_WRPROT : 0;
  *value |= (status & I365_CS_READY) ? PC_READY : 0;
  *value |= (status & I365_CS_POWERON) ? PC_POWERON : 0;
  *value |= (status & I365_CS_GPI) ? PC_GPI : 0;
  return 0;
  } /* i365_get_status */
  
/*====================================================================*/

int i365_get_flags(unsigned int slot, unsigned int *value)
  {
  unsigned char reg, vcc, vpp;
  reg = I365_GET(slot, I365_POWER);
  *value  = (reg & I365_PWR_AUTO) ? PC_PWR_AUTO : 0;
  vcc = reg & I365_VCC_MASK; vpp = reg & I365_VPP_MASK;
  *value |= (vcc == I365_VCC_3V) ? PC_VCC_3V : 0;
  *value |= (vcc == I365_VCC_5V) ? PC_VCC_5V : 0;
  *value |= (vpp == I365_VPP_5V) ? PC_VPP_5V : 0;
  *value |= (vpp == I365_VPP_12V) ? PC_VPP_12V : 0;
  reg = I365_GET(slot, I365_INTCTL);
  *value |= (reg & I365_PC_IOCARD) ? PC_IOCARD : 0;
  *value |= (reg & I365_PC_RESET) ? 0 : PC_RESET;
  return 0;
  } /* i365_get_flags */
  
/*====================================================================*/

int i365_set_flags(unsigned int slot, unsigned int *value)
  {
  unsigned char reg = I365_GET(slot, I365_POWER);
  reg &= ~(I365_PWR_AUTO | I365_VCC_MASK | I365_VPP_MASK);
  reg |= (*value & PC_PWR_AUTO) ? I365_PWR_AUTO : 0;
  reg |= (*value & PC_VCC_3V) ? I365_VCC_3V|I365_PWR_OUT : 0;
  reg |= (*value & PC_VCC_5V) ? I365_VCC_5V|I365_PWR_OUT : 0;
  reg |= (*value & PC_VPP_5V) ? I365_VPP_5V : 0;
  reg |= (*value & PC_VPP_12V) ? I365_VPP_12V : 0;
  I365_SET(slot, I365_POWER, reg);
  delay(2);
  reg = I365_GET(slot, I365_INTCTL);
  reg &= ~(I365_PC_IOCARD | I365_PC_RESET);
  /* Note that the reset signal is inverted */
  reg |= (*value & PC_RESET) ? 0 : I365_PC_RESET;
  reg |= (*value & PC_IOCARD) ? I365_PC_IOCARD : 0;
  I365_SET(slot, I365_INTCTL, reg);
  return 0;
  } /* i365_set_flags */
  
/*====================================================================*/

int i365_get_irq(unsigned int slot, unsigned int *value)
  {
  unsigned char ctl = I365_GET(slot, I365_INTCTL);
  *value = ctl & 0x0F;
  return 0;
  } /* i365_get_irq */
  
/*====================================================================*/

int i365_set_irq(unsigned int slot, unsigned int *value)
  {
  unsigned char ctl = I365_GET(slot, I365_INTCTL);
  if (*value > 15) return -EINVAL;
  ctl |= *value;
  I365_SET(slot, I365_INTCTL, ctl);
  return 0;
  } /* i365_set_irq */
  
/*====================================================================*/

int i365_get_io_map(unsigned int slot, struct pcmcia_io_map *io)
  {
  unsigned char map, ioctl, addr;
  map = io->map;
  if (map > 2) return -EINVAL;
  io->start = I365_GET_PAIR(slot, I365_IO(map)+I365_W_START);
  io->stop = I365_GET_PAIR(slot, I365_IO(map)+I365_W_STOP);
  ioctl = I365_GET(slot, I365_IOCTL);
  addr = I365_GET(slot, I365_ADDRWIN);
  io->flags  = (addr & I365_ENA_IO(map)) ? MAP_ACTIVE : 0;
  io->flags |= (ioctl & I365_IOCTL_WAIT(map)) ? MAP_WAIT : 0; 
  io->flags |= (ioctl & I365_IOCTL_0WS(map)) ? MAP_0WS : 0;
  io->flags |= (ioctl & I365_IOCTL_CS16(map)) ? MAP_CS16 : 0;
  io->flags |= (ioctl & I365_IOCTL_16BIT(map)) ? MAP_16BIT : 0;
  return 0;
  } /* i365_get_io_map */

/*====================================================================*/

int i365_set_io_map(unsigned int slot, struct pcmcia_io_map *io)
  {
  unsigned char map, ioctl, addr;
  map = io->map;
  if ((map > 2) || (io->start > 0xFFFF) || (io->stop > 0xFFFF) ||
      (io->stop < io->start)) return -EINVAL;
  addr = I365_GET(slot, I365_ADDRWIN);
  /* Turn off the window before changing anything */
  if (addr & I365_ENA_IO(map)) {
    addr &= ~I365_ENA_IO(map);
    I365_SET(slot, I365_ADDRWIN, addr);
    }
  I365_SET_PAIR(slot, I365_IO(map)+I365_W_START, io->start);
  I365_SET_PAIR(slot, I365_IO(map)+I365_W_STOP, io->stop);
  ioctl = I365_GET(slot, I365_IOCTL) & ~I365_IOCTL_MASK(map);
  if (io->flags & MAP_WAIT) ioctl |= I365_IOCTL_WAIT(map);
  if (io->flags & MAP_0WS) ioctl |= I365_IOCTL_0WS(map);
  if (io->flags & MAP_CS16) ioctl |= I365_IOCTL_CS16(map);
  if (io->flags & MAP_16BIT) ioctl |= I365_IOCTL_16BIT(map);
  I365_SET(slot, I365_IOCTL, ioctl);
  /* Turn on the window if necessary */
  if (io->flags & MAP_ACTIVE) {
    addr |= I365_ENA_IO(map);
    I365_SET(slot, I365_ADDRWIN, addr);
    }
  return 0;
  } /* i365_set_io_map */

/*====================================================================*/

int i365_get_mem_map(unsigned int slot, struct pcmcia_mem_map *mem)
  {
  unsigned short base, i;
  unsigned char map, addr;

  map = mem->map;
  if (map > 4) return -EINVAL;
  addr = I365_GET(slot, I365_ADDRWIN);
  mem->flags = (addr & I365_ENA_MEM(map)) ? MAP_ACTIVE : 0;
  base = I365_MEM(map);
  
  i = I365_GET_PAIR(slot, base+I365_W_START);
  mem->flags |= (i & I365_MEM_16BIT) ? MAP_16BIT : 0;
  mem->flags |= (i & I365_MEM_0WS) ? MAP_0WS : 0;
  mem->sys_start = ((long)(i & 0x0FFF) << 12);
  
  i = I365_GET_PAIR(slot, base+I365_W_STOP);
  mem->extra_ws  = (i & I365_MEM_WS0) ? 1 : 0;
  mem->extra_ws += (i & I365_MEM_WS1) ? 2 : 0;
  mem->sys_stop = ((long)(i & 0x0FFF) << 12) + 0x0FFF;
  
  i = I365_GET_PAIR(slot, base+I365_W_OFF);
  mem->flags |= (i & I365_MEM_WRPROT) ? MAP_WRPROT : 0;
  mem->flags |= (i & I365_MEM_REG) ? MAP_ATTRIB : 0;
  mem->card_start = ((long)(i & 0x3FFF) << 12) + mem->sys_start;
  mem->card_start &= 0x3FFFFFF;
  return 0;
  } /* i365_get_mem_map */

/*====================================================================*/
  
int i365_set_mem_map(unsigned int slot, struct pcmcia_mem_map *mem)
  {
  unsigned short base, i;
  unsigned char map, addr;

  map = mem->map;
  if ((map > 4) || (mem->card_start > 0x3FFFFFF) ||
      (mem->sys_start > 0xFFFFFF) || (mem->sys_stop > 0xFFFFFF) ||
      (mem->sys_start > mem->sys_stop) || (mem->extra_ws > 3))
    return -EINVAL;
  addr = I365_GET(slot, I365_ADDRWIN);
  /* Turn off the window before changing anything */
  if (addr & I365_ENA_MEM(map)) {
    addr &= ~I365_ENA_MEM(map);
    I365_SET(slot, I365_ADDRWIN, addr);
    }
  base = I365_MEM(map);

  i = mem->sys_start >> 12;
  if (mem->flags & MAP_16BIT) i |= I365_MEM_16BIT;
  if (mem->flags & MAP_0WS) i |= I365_MEM_0WS;
  I365_SET_PAIR(slot, base+I365_W_START, i);
  
  i = mem->sys_stop >> 12;
  if (mem->extra_ws & 1) i |= I365_MEM_WS0;
  if (mem->extra_ws & 2) i |= I365_MEM_WS1;
  I365_SET_PAIR(slot, base+I365_W_STOP, i);
  
  i = ((mem->card_start - mem->sys_start) >> 12) & 0x3FFF;
  if (mem->flags & MAP_WRPROT) i |= I365_MEM_WRPROT;
  if (mem->flags & MAP_ATTRIB) i |= I365_MEM_REG;
  I365_SET_PAIR(slot, base+I365_W_OFF, i);
  
  /* Turn on the window if necessary */
  if (mem->flags & MAP_ACTIVE) {
    addr |= I365_ENA_MEM(map);
    I365_SET(slot, I365_ADDRWIN, addr);
    }
  return 0;
  } /* i365_set_mem_map */

/*====================================================================*/

int i365_ioctl(unsigned int slot, unsigned int cmd, unsigned long arg)
  {
  int err;
  unsigned int size, value;
  struct pcmcia_io_map iomap;
  struct pcmcia_mem_map memmap;

  err = 0;
  size = (cmd & IOCSIZE_MASK) >> IOCSIZE_SHIFT;
  if (cmd & IOC_IN) {
    err = verify_area(VERIFY_READ, (char *)arg, size);
    if (err) return err;
    }
  if (cmd & IOC_OUT) {
    err = verify_area(VERIFY_WRITE, (char *)arg, size);
    if (err) return err;
    }
  
  switch (cmd) {
    case PCMCIA_GET_STATUS:
      err = i365_get_status(slot, &value);
      memcpy_tofs((char *)arg, &value, sizeof(int));
      break;
    case PCMCIA_GET_FLAGS:
      err = i365_get_flags(slot, &value);
      memcpy_tofs((char *)arg, &value, sizeof(int));
      break;
    case PCMCIA_SET_FLAGS:
      if (!suser()) return -EPERM;
      memcpy_fromfs(&value, (char *)arg, sizeof(int));
      err = i365_set_flags(slot, &value);
      break;
    case PCMCIA_GET_IRQ:
      err = i365_get_irq(slot, &value);
      memcpy_tofs((char *)arg, &value, sizeof(int));
      break;
    case PCMCIA_SET_IRQ:
      if (!suser()) return -EPERM;
      memcpy_fromfs(&value, (char *)arg, sizeof(int));
      err = i365_set_irq(slot, &value);
      break;
    case PCMCIA_GET_IO_MAP:
      memcpy_fromfs(&iomap, (char *)arg, sizeof(struct pcmcia_io_map));
      err = i365_get_io_map(slot, &iomap);
      memcpy_tofs((char *)arg, &iomap, sizeof(struct pcmcia_io_map));
      break;
    case PCMCIA_SET_IO_MAP:
      if (!suser()) return -EPERM;
      memcpy_fromfs(&iomap, (char *)arg, sizeof(struct pcmcia_io_map));
      err = i365_set_io_map(slot, &iomap);
      break;
    case PCMCIA_GET_MEM_MAP:
      memcpy_fromfs(&memmap, (char *)arg, sizeof(struct pcmcia_mem_map));
      err = i365_get_mem_map(slot, &memmap);
      memcpy_tofs((char *)arg, &memmap, sizeof(struct pcmcia_mem_map));
      break;
    case PCMCIA_SET_MEM_MAP:
      if (!suser()) return -EPERM;
      memcpy_fromfs(&memmap, (char *)arg, sizeof(struct pcmcia_mem_map));
      err = i365_set_mem_map(slot, &memmap);
      break;
    default:
      err = -EINVAL;
      break;
    }
  return err;
  } /* i365_ioctl */
  

