/*                                                 -*-mode:C;tab-width:8-*-
 * 
 * cxdrv.c -- a driver for the cx100 frame grabber
 *
 * Copyright (C) 1995 Alessandro Rubini  -- rubini@ipvvis.unipv.it
 *
 *   This program 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 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * Public names are all called cxg_* (for cxgrabber). Upper or lowercase
 * is chosen according to the type of symbol. Static sysmbols and local
 * defines are unstructured
 */

/*
 * TODO:
 *      write
 *      check permissions in 
 *      luts
 *      overlay
 *      gain/offset
 *      drawing-lib (maybe I'll never do it)
 */

#define __KERNEL__

#define MODULE
#include <linux/module.h>
#include <linux/version.h>

#include <linux/types.h>
#include <linux/sched.h>
#include <linux/fcntl.h>
#include <linux/errno.h>
#include <linux/ioport.h>
#include <linux/malloc.h>
#include <linux/timer.h> /* actually, timer use is faulty, and disabled */

#if 0
#  include <linux/ctype.h>  /* Hmm... no ctype array is exported... */
#else
#  define isspace(c) ((c)==' ' || (c)=='\n' || (c)=='\t')
#  define isdigit(c) ((c)>='0' && (c)<='9')
#endif 

#include <asm/io.h>
#include <asm/irq.h>

#include "cxdrv.h"

/*
 * Don't use static symbols when debugging
 */
#ifdef DEBUG_CX100
#define static
#endif

	
/*
 * These variables are those which are supposed to be changed by insmod
 */
int cxg_major = 0;             /* major device number */
int cxg_fake  = 0;             /* 1 to avoid interrupt, 2 to avoid all */
int cxg_base  = 0;             /* force a base address */
int cxg_ram   = 0;             /* choose the ram page */
int cxg_irq   = 0;             /* specify the irq line for the device */

static  int cxg_boards = 0;
static  struct file_operations cx_fops;
static  struct file_operations cx_ol_fops;
static  struct file_operations cx_pgm_fops;

static Cxg_Board hwinfo[MAX_BOARDS];  /* may support multiple boards */

/* this depends on "filp" being defined ;-) */
#define CLD ((Cxg_Clientdata *)(filp->private_data))
  

char    kernel_version [] = UTS_RELEASE;
static int cx_getfieldcount(Cxg_Board *board);

/*-------------------------------------------------------------- insert */
/*
 * Insert the current filedescriptor in the waitqueue of the board
 */
static inline void cx_insert(Cxg_Board *board, ulong waitfor, int flg)
{
  ulong flags;
  static void cx_awakethem();
 
  if (board->irq>=0)
    {
    save_flags(flags);
    cli();
    PDEBUG(("insert irq: wfor %li\n",waitfor));
    if (waitfor < board->field_w) /* override */
      { board->field_w=waitfor; board->grabreq=flg; }
    restore_flags(flags);
    return;
    }

#if 0 /* buggy implementation, and thus disabled. Use interrupts instead. */
  /*
   * Else, we have the buggy boards, and must provide a workaround.
   * Try with interval timers. Assume 50Hz. If we're ntsc we should use
   * the interrupt.
   */
  save_flags(flags);
  cli();
  PDEBUG(("insert no irq: wfor %li\n",waitfor));
  if (waitfor < board->field_w) /* override */
    {
    del_timer(&board->timer);
    board->timer.expires=(waitfor - cx_getfieldcount(board))*2+jiffies-2;
    board->timer.data=(unsigned long)board;
    board->timer.function=cx_awakethem;
    PDEBUG(("adding\n"));
    add_timer(&board->timer);
    PDEBUG(("added\n"));
    board->grabreq=flg;
    }
    restore_flags(flags);
#endif

  return;
}


/*
 * the following is somehow a duplicate of the interrupt.
 * but this must do busy-waiting.... horrible!
 */

static void cx_awakethem(ulong data)
{
  Cxg_Board *board=(Cxg_Board *)data;
  int count=cx_getfieldcount(board);
  int frames=2;

  PDEBUG(("timer awoke for %03x at %i\n",board->base,count));
  if (board->field_w > count) /* noone waiting */
    {PDEBUG(("Can' happen\n")); return;}

  /* otherwise, awake processes */
  if (board->grabreq)
    {
    PDEBUG(("cx timer: start grabbing\n"));
    board->grabreq=0;
    if (board->lowres) {SETBIT(board,CXG_BIT_FLD_GRAB); frames=1;}
    else                SETBIT(board,CXG_BIT_FRM_GRAB);

    /* ok, now wait for frames more */
    board->timer.expires=2 + 2*!board->lowres + jiffies;
    PDEBUG(("cx timer: readded\n"));
    add_timer(&board->timer); /* fields are already right */
    return;
    }

  PDEBUG(("Awaking at frame %i\n",count));
  board->field_w=~0;
  wake_up_interruptible(&(board->waitq));
  return;
}


/*-------------------------------------------------------------- wait */
/*
 * This functions provides the basic wait utility.
 */
static void cx_wait(int millisec)
{
  ulong j=jiffies;
  int ticks=(millisec*HZ+500)/1000;

  PDEBUGG(("wait %i ticks\n",ticks));
  j+=ticks;

#if 1
  while(1)
    {
    ulong flags;

    PDEBUG(("wait till timeout...\n"));
    current->timeout=j;

    /* interruptible_sleep_on(&waitq);    Oops... */

    current->state=TASK_INTERRUPTIBLE;
    save_flags(flags);
    sti();
    schedule();
    restore_flags(flags);
    
    if (!current->timeout)
      return;
    }
#else
  while (j>jiffies)
    schedule();
#endif

}
 
/*-------------------------- busywait */
static int _timeout_;
static inline void busywait(int timeout) {
  _timeout_=jiffies+timeout;
  while (jiffies<_timeout_)
    /* nothing */;
}
 

/*-------------------------------------------------------------- reset */
static int cx_reset(Cxg_Board *board)
{

  inp(PORT6(board));    /* awake it */
  cx_wait(100);

  board->lowres=0;                  /* put it in high res */
  CLRBIT(board,CXG_BIT_LOW_RES);
  if (board->roi) kfree(board->roi); /* no roi's for pgm */
  board->roi=NULL;

  board->ram=0;
  CLRBIT(board,CXG_BIT_RAM_ENB);
  CLRBIT(board,CXG_BIT_ILUT_ENB);  /* fixme -- Whatelse? */
  CLRBIT(board,CXG_BIT_OLUT_ENB);
  CLRBIT(board,CXG_BIT_OL_ENB);

  /* don't clear counter, it lasts half a century.... */

  return 0; /* always succeed */
}

/*-------------------------------------------------------------- findboard */
/*
 * This procedure fakes a board only if one is not there. So I can
 * detect the first one and fake the second
 */
static int cx_findboard(Cxg_Board *board)
{
  int i;
  int fakenow=0;
  int found=0;

  if (cxg_fake) /* here faking is always available */
    {
    cxg_fake--;
    fakenow++;
    }
  do
    {
    inp(PORT6(board));    /* awake it */
    cx_wait(100);

    if (inp(PORT6(board))==0xff)
      break;

    COMMAND(board,CXG_CMD_CXMODE);        /* first, put it in cx mode */
    cx_wait(100);

    i=COMMAND(board,CXG_CMD_BOGUS);        /* should return FF */
    PDEBUGCMD(CXG_CMD_BOGUS,i);
    if(i!=0xff) break; 

    i=COMMAND(board,CXG_CMD_CXMODE);        /* should return 0 */
    PDEBUGCMD(CXG_CMD_CXMODE,i);
    if(i!=0) break;

    i=COMMAND(board,CXG_CMD_GETSTATUS);        /* should be on */
    PDEBUGCMD(CXG_CMD_GETSTATUS,i);
    if(!(i&CXG_SBIT_CX100)) break;
    
    /*
     * toggle all the pages and watch port 0
     */
    
    for(i=3; i>=0; --i)
      {
      SETPAGE(board,i);
      if(GETPAGE(board)!=i)
	break;
      }
    
    PDEBUG(("cx test: Found one at 0x%x\n",board->base));
    found++;

    SETBIT(board, CXG_BIT_OL_ENB);
    board->may_overlay=GETBIT(board, CXG_BIT_OL_ENB);
    PDEBUGG(("cx testboard: may_overlay = %i\n",board->may_overlay));
  
    CLRBIT(board,CXG_BIT_ILUT_ENB);
    CLRBIT(board,CXG_BIT_OLUT_ENB);
    CLRBIT(board,CXG_BIT_RAM_ENB);
    CLRBIT(board,CXG_BIT_OL_ENB);

    COMMAND(board,CXG_CMD_CLRCOUNT);
    board->status=COMMAND(board,CXG_CMD_GETSTATUS);
    board->fpga_rev=COMMAND(board,CXG_CMD_GETGAV);
    board->micro_rev=COMMAND(board,CXG_CMD_GETFWV);
    }
  while(0); /* just to use break */

  board->driver_rev1=_CXG_REV1;
  board->driver_rev2=_CXG_REV2;
  board->irq=-1;
  board->hwirq=-1; /* unfortunately, I can't know the irq address in advance */
  board->ram=0;  /* no ram */
  board->usecount=0;
  board->lowres=0;

  board->jiffies=jiffies;
  board->fields=0;
  board->field_w=~0; /* infinite ;) */
  board->fake=fakenow;
  if (found)
    PDEBUGG(("cx init: %lx jiffies -- 0 fields\n",jiffies));

  if (found)
    {
    cxg_fake+=fakenow; /* found now, fake the next */
#if 0
    COMMAND(board,CXG_CMD_SHUTDOWN); /* damn it! */
#endif
    return 0;
    }

  if (fakenow)
    {
    PDEBUG(("cx test: faking a board at 0x%x\n",board->base));
    return 0;
    }
  return -1;


}

static volatile ulong wrong_interrupts;

/*-------------------------------------------------------------- waitvb */
/*
 * This function performs busy-waiting. Anyways, one field only lasts
 * two jiffies, and wise software is interrupt-driven.
 */

static int waitvb(Cxg_Board *board)
{
int j=jiffies+5;
/*............................... */ FAKE_BEGIN

  while(jiffies<j)
    ;
/*............................... */ FAKE_ELSE

  while(GETBIT(board,CXG_BIT_VBLANK))
    if (jiffies>j)
      return 0;
  while (!GETBIT(board,CXG_BIT_VBLANK))
    if (jiffies>j)
      return 0;
/*............................... */ FAKE_END

  return 1;
}

/*-------------------------------------------------------------- fieldcount */
static int cx_getfieldcount(Cxg_Board *board)
{
  int res,tmp,guess;

  guess = ( (jiffies-board->jiffies) / 2 ) + board->fields; /* 50Hz */

/*............................... */ FAKE_BEGIN

  res=guess;  
/*............................... */ FAKE_ELSE

  if (board->irq >= 0)
    {
    board->jiffies=jiffies;
    return board->fields;
    }

  /*
   * Else, get the 16-bit number and upgrade it as needed
   */

  res=COMMAND(board,CXG_CMD_GETCOUNTL);
  tmp=COMMAND(board,CXG_CMD_GETCOUNTH);

  /*
   * The following should be used to avoid races, but it oopses
   */
#if 0
  if (res!=COMMAND(board,CXG_CMD_GETCOUNTL))
    res=(COMMAND(board,CXG_CMD_GETCOUNTH)<<8) 
      | COMMAND(board,CXG_CMD_GETCOUNTL);
  else
#endif
    res|=tmp<<8;
  
  /*
   * now extend it to 32 bits (not the first time)
   */
  if (board->fields!=0)
    {
    PDEBUG(("j 0x%lx b->j 0x%lx b->f 0x%lx\n",jiffies,board->jiffies,
	    board->fields));
    res |= (board->fields & ~0xffff);
    PDEBUG(("cx field: guess 0x%x, got 0x%x\n",guess,res));
    guess+=4096; /* exceed */
    if (guess-res > 0x10000)
      {
      res += (guess-res) & 0xf0000;
      PDEBUG(("cx field: real 0x%x\n",res));
      }
    }

/*............................... */ FAKE_END

  board->fields=res; board->jiffies=jiffies;
  return res;
  }

/*-------------------------------------------------------------- waitfield */
/*
 * This function uses the interrupt, if it is available, and
 * falls back on the 'silly' behaviour if no interrupt is there.
 * Needless to say that interrupt-driven operation is *far* better.
 */
static int cx_waitfieldcount(struct file *filp, Cxg_Board *board,
				    unsigned w, int reqflg)
{
  unsigned currcount;
  unsigned waitfor;

  waitfor= w ? w : CLD->cd_next;
  currcount=cx_getfieldcount(board); 
  PDEBUG(("wait %u to %u\n",currcount,waitfor));

  if (waitfor-currcount>1)
    {
    if (board->irq >= 0)
      {
      current->timeout=jiffies+4*(waitfor-currcount); /* safe... */

      /* 
       * Managing multiple processes is tricky. All of them will sleep
       * on the same queue. So the boards knows the next awake-time, and
       * the process will go to sleep again if incorrectly awakened.
       * Proper working is ensured by having field_w==~0 after awaking.
       */

      while (1)
	{
	cx_insert(board,waitfor,reqflg);
	interruptible_sleep_on (&board->waitq);
	PDEBUG(("%p awoke\n",CLD));
	if ((currcount=board->fields) >= waitfor)
	  break;
        if (current->signal & ~current->blocked)
	  return -ERESTARTSYS;
	if (!current->timeout)
	  return -ENODATA; 
	}

      }
    else  /* estimate missing millisec, assume worst case (60Hz) */
      {
      while(waitfor-currcount > 1) /* leave one for busy-wait */
	{
	cx_wait( (waitfor-currcount)*1000/60 /* avoid rounding */ -5 );
	currcount=cx_getfieldcount(board);
	}
      /*
       * If a grabbing is requested, we'll cry. We must loose some
       * fields while waiting for the grab
       */
      if (reqflg)
	{
	if (!board->lowres)
	  {
	  SETBIT(board,CXG_BIT_FRM_GRAB);
	  waitvb(board);
	  }
	else
	  SETBIT(board,CXG_BIT_FLD_GRAB);
	waitvb(board);
	}
      else
	if (waitfor-currcount!=1) /* last one, else, we're over */
	  waitvb(board);    
      }
    }
  if (!w)
    CLD->cd_next += CLD->cd_step;

  return currcount;
}

/*-------------------------------------------------------------- checkram */
static inline unsigned char cx_checkseg(int seg)
{
volatile unsigned char *ptr;
int i;

  if (seg<10) return CXG_MEM_RAM;
  PDEBUGG(("seg %i\n",seg));
  for (i=0; i<MAX_BOARDS; i++)
    if (hwinfo[i].ram==seg)
      {
      PDEBUGG(("used by board %i\n",i));
      return CXG_MEM_BUSY;
      }
  for (ptr=(unsigned char *)(seg<<16);
       ptr<(unsigned char *)((seg+1)<<16);
       ptr+=0x97) /* a silly number */
    {
    unsigned char testval;

    testval=*ptr;
    *ptr= testval^0xff;
    PDEBUGG(("ptr %p val %02x newval %02x\n",ptr,testval,*ptr));
    if ((*ptr^testval) == 0xff) {*ptr=testval; return CXG_MEM_RAM; }
    if (*ptr!=testval) continue; /* changed?? */
    if (testval 
	&& testval != 0xff 
	&& testval != ((int)ptr & 0xff))
      return CXG_MEM_ROM;
    }
  return CXG_MEM_FREE;
}

/*-------------------------------------------------------------- setram */
static int setram(Cxg_Board *board, int arg)
{
/*............................... */ FAKE_BEGIN
  return -EINVAL;
/*............................... */ FAKE_END


  if (arg==CXG_RAMPAGE_NONE)
    {
    CLRBIT(board,CXG_BIT_RAM_ENB);
    board->ram=0; return 0;
    PDEBUGG(("ram disabled\n"));
    return 0;
    }
  if (arg==CXG_AUTODETECT)
    {
    if (board->ram) return 0; /* already one */
    for (arg=0x0f;arg>0x09;arg--)
      if (cx_checkseg(arg)==CXG_MEM_FREE)
	break;
	}
  if (arg<0x0a || arg > 0x0f) return -EINVAL;
  PDEBUGG(("cx: setting ram to %x\n",arg));
  COMMAND(board,0xD0+arg); board->ram=arg;
  SETBIT(board,CXG_BIT_RAM_ENB);
  return 0;
}


/*-------------------------------------------------------------- interrupt */
/* Somehow tricky: when processes are awaiting (maybe selecting),
 * we must assure no additional delay. This means that grabbing is part
 * of the waiting process. Non-busy grab is accomplished using a static
 * variable to keep status information.
 */
static void cx_interrupt(int irq, struct pt_regs *regs)
{
  ulong i;
  Cxg_Board *board;
  static int grabbing=0;

  PDEBUGG(("cx irq\n"));

  for (i=0, board=hwinfo; i<cxg_boards; board++, i++)
    if (board->irq==irq)
      break;

  if (i==cxg_boards)
    { wrong_interrupts++; return; }

  /* now clear momentarily CXG_BIT_IRQ_ENB to ack the interrupt */
  CLRBIT(board,CXG_BIT_IRQ_ENB);
  SETBIT(board,CXG_BIT_IRQ_ENB);

  board->irq_count++;
  i= ++board->fields;

  if (grabbing)
    { grabbing--; return; }

  if (board->field_w > i) /* noone waiting */
    return;

  /* otherwise, awake processes */
  if (board->grabreq)
    {
    PDEBUG(("cx int: start grabbing\n"));
    board->grabreq=0;
    grabbing= 1 + !board->lowres; /* fields to wait for */
    if (board->lowres) SETBIT(board,CXG_BIT_FLD_GRAB);
    else               SETBIT(board,CXG_BIT_FRM_GRAB);
    return; /* don't awake, just grab */
    }

  PDEBUG(("Awaking at frame %li\n",i));
  board->field_w=~0;
  wake_up_interruptible(&(board->waitq)); 
  return;
}

/*-------------------------------------------------------------- setirq */
static int setirq(Cxg_Board *board, int irq)
{
  int err;
/*............................... */ FAKE_BEGIN
  return -EINVAL;
/*............................... */ FAKE_END

  if (irq==CXG_AUTODETECT && board->hwirq>=0)
    irq=board->hwirq; /* already detected */

  if (board->irq==irq)
    return 0;

  PDEBUG(("cx setirq %i\n",irq));

  CLRBIT(board,CXG_BIT_IRQ_ENB);
  if (board->irq>=0)
    {
    cx_getfieldcount(board); /* sync jiffies for next guess */
    PDEBUG(("cx setirq: freeing %d\n",board->irq));
    free_irq(board->irq);
    board->irq=-1;
    }
  if (irq==CXG_IRQ_NONE)
    return 0;

  if (irq!=CXG_AUTODETECT)
    {
    err=request_irq(irq,cx_interrupt,SA_INTERRUPT /* fast */,"cx100");
    PDEBUG(("cx irq %i: err is %i\n",irq,err));
    if (!err) 
      {
      board->irq=irq;
      board->irq_count=0;
      PDEBUG(("setting bit\n"));
      SETBIT(board,CXG_BIT_IRQ_ENB);
      enable_irq(irq);
      }
    return err;
    }

/*
 * Well, autodetection is tricky: we must wait some time just to check
 * whether the irq arrives. This code works, but current PAL-driven
 * boards are unable to generate interrupts. This will change, hopefully.
 */
  {
  static int try[]={3,4,5,7,0};
  int i,now;

  PDEBUG(("Entering autodetection\n"));
  for (i=0; (now=try[i]); i++)
    {
    PDEBUG(("cx getint: try %d\n",now));
    if (setirq(board,now)) continue;
    wrong_interrupts=0;
    cx_wait(200); /* 0.2 second delay: enough */
    PDEBUG(("cx getint: true %lu, fake %lu\n",
	    board->irq_count,wrong_interrupts));
    CLRBIT(board,CXG_BIT_IRQ_ENB);
    if (wrong_interrupts) continue;
    if (board->irq_count)
      {board->irq_count=0; break;}
    }

  if (!now)
    {
    PDEBUG(("cx: autodetect failed -- no interrupts\n"));
    setirq(board,CXG_IRQ_NONE);
    return -EADDRNOTAVAIL;
    }

  /* found */
  SETBIT(board,CXG_BIT_IRQ_ENB);
  board->hwirq=now;
  }
  return 0;
}

/*-------------------------------------------------------------- xfer region */
enum xferdir {XFER_IN=0, XFER_OUT};

static int cx_xfer(Cxg_Board *board, Cxg_Roi *roiptr, int dir)
{
  char *buf=roiptr->xtra.data;
  int ram=board->ram<<16;
  unsigned short row=0, page=0;
  int incr=256;

  PDEBUG(("region %dx%d+%d+%d\n",
      roiptr->wid,roiptr->hei,roiptr->x,roiptr->y));

  ram += incr*roiptr->y+roiptr->x;
  if (!board->lowres)
    {
    ram+= incr*(roiptr->y);
    incr=512;
    page=(roiptr->y)/128;
    PDEBUG(("page is %i\n",page));
    SETPAGE(board,page);
    ram -= 0x10000*page;
    }
  ram+=roiptr->x;

  for (row=0; row<roiptr->hei; row++, ram+=incr, buf+=roiptr->wid)
    {
    if (!board->lowres && ram>0x10000)
      { SETPAGE(board, ++page); ram-=0x10000; }
    if (dir==XFER_IN)
      memcpy_tofs(buf,(void *)ram,roiptr->wid);
    else
      memcpy_fromfs((void *)ram,buf,roiptr->wid);
    }
  return 0;
}


/*============================================================== lseek ===*/
static  int cx_lseek (struct inode *inode, struct file *filp,
		      off_t ofs, int origin)
{
  return  -ESPIPE;
}

/*============================================================== open ====*/
static  int cx_open (struct inode *inode, struct file *filp)
{
  Cxg_Board *board;
  int err;

  if (BOARDNO(inode->i_rdev) >= cxg_boards)  return -ENODEV;
  board=hwinfo+BOARDNO(inode->i_rdev);

#ifndef DEBUG_CX100

  if (board->fake)
    {
    printk("cx100 open: can't use a fake device\n");
    return -ENODEV;
    }

#endif

  if (board->usecount==0)
    { PDEBUG(("reset\n")); cx_reset(board); }


  switch (DEVTYPE(inode->i_rdev)) /* node selection */
    {
    case CX_FULL:
      if ( (err=setirq(board,CXG_AUTODETECT)) ) return err;
      if ( (err=setram(board,CXG_AUTODETECT)) )
	{ setirq(board,CXG_IRQ_NONE); return err; }
      break;
      
    case CX_PGM:
      if ( (err=setram(board,CXG_AUTODETECT)) ) return err;
      /* SETBIT(board,CXG_BIT_ACQ_REQ); */
      if( !waitvb(board)) return -ENODATA; /* no video */
      if (board->lowres) SETBIT(board,CXG_BIT_FLD_GRAB);
      else	         SETBIT(board,CXG_BIT_FRM_GRAB);
      waitvb(board); if (!board->lowres) waitvb(board);
      filp->f_op = &cx_pgm_fops;
      break;

    case CX_RAW:
      break;

    case CX_OVL:
      if (!board->may_overlay) return -ENODEV;
      filp->f_op = &cx_ol_fops;
      break;

    default:
      return -ENODEV;
    }


  filp->private_data=kmalloc(sizeof(Cxg_Clientdata),GFP_KERNEL);
  if (!(filp->private_data)) return -ENOMEM;

  /* reset file info */
  CLD->cd_board=board;
  CLD->cd_type=DEVTYPE(inode->i_rdev);
  CLD->cd_ispgm=(DEVTYPE(inode->i_rdev)==CX_PGM);
  CLD->cd_rois=CLD->cd_currRoi=NULL;
  CLD->cd_next=CLD->cd_step=0; /* not blocking */
  CLD->cd_fstart=0;
  
  MOD_INC_USE_COUNT;
  board->usecount++;
  PDEBUG(("opened %ith time\n",board->usecount));
  return 0;
}

/*============================================================== close ===*/
static  void cx_close(struct inode *inode, struct file *filp) 
{
  Cxg_Board *board=CLD->cd_board;

  board->usecount--;
  PDEBUG(("cx close: usecount remains %d\n",board->usecount));

  if (board->usecount==0)
    {
    CLRBIT(board,CXG_BIT_ILUT_ENB);
    CLRBIT(board,CXG_BIT_OLUT_ENB);
    CLRBIT(board,CXG_BIT_OL_ENB);
    board->ram=0;
    CLRBIT(board,CXG_BIT_RAM_ENB);
    if (board->roi)
      kfree(board->roi);
    board->roi=NULL;
    setirq(board,CXG_IRQ_NONE);
#if 0
    COMMAND(CLD->cd_board,CXG_CMD_SHUTDOWN); /* drives me mad.... */
#endif

    }

  if (CLD->cd_rois) kfree(CLD->cd_rois);
  kfree(CLD);
  filp->private_data=NULL;

  MOD_DEC_USE_COUNT;
}

/*============================================================== select ====*/
static  int cx_select (struct inode *inode, struct file *filp,
               int sel_type, select_table *wait)

{
  Cxg_Board *board=CLD->cd_board;

  PDEBUGG(("cx Select\n"));
  if (sel_type==SEL_IN)
    {
    if (!CLD->cd_next) return 1; /* next is 0 if always readable */
    if (cx_getfieldcount(board) > CLD->cd_next) 
      {PDEBUG(("late select\n")); return 1;}
    cx_insert(board,CLD->cd_next,1 /* grab */ );
    select_wait(&board->waitq,wait);
    PDEBUG(("select_wait\n"));
    return 0;
    }
  if (sel_type==SEL_OUT) 
    return 1; /* always writable */
  return 0; /* never exception-able */
}

/*============================================================== read ====*/
static  int cx_read(struct inode *inode, struct file *filp,
		      char *buf, int count)
{
  Cxg_Board *board=CLD->cd_board;
  int ram=board->ram<<16;
  ulong p = filp->f_pos;
  ulong l;
  int count0=count;
  unsigned short row, page=0;
  Cxg_Roi *roiptr;

  if (!(board->ram))
    return 0; /* nothing to read */

  PDEBUGG(("cx read: count=0x%x\n",count));

  /*
   * raw mode. For pgm use /dev/cxg0pgm
   */
  if (!CLD->cd_rois)
    {
    int imgsize=0x10000*(board->lowres ? 1 : 4);

    /* must block? */
    if (!filp->f_pos && CLD->cd_next) /* beginning of image */
      {
      PDEBUG(("read: block block\n"));
      if (CLD->cd_next > cx_getfieldcount(board))
	{
	if (filp->f_flags & O_NONBLOCK) /* Hmm.... */
	  return -EAGAIN;
	cx_waitfieldcount(filp,board,0 /* next */,1 /* grab */);
	}
      else /* data ready */
	{
	PDEBUG(("late read\n"));
	CLD->cd_next += CLD->cd_step;
	}
      PDEBUG(("rd: next is %li\n",CLD->cd_next));
      }

    if (board->lowres==0) /* set the page */
      SETPAGE(board,(p>>16)&3);

    p&=0xFFFF; /* low part */
    if (count+p > 0x10000) count=0x10000-p;

    memcpy_tofs(buf,(void *)((ram)+p),count);
    filp->f_pos+=count;
    PDEBUGG(("cx read: done 0x%x\n",count));
    if ((filp->f_pos&(imgsize-1)) == 0) /* Hmmm.... '%' called __moddi3 */
      filp->f_pos=0;
    PDEBUG(("read: pos is %li\n",filp->f_pos));
    return count;
    }

/*
 * roi mode -- FIXME -- use copy-region if enough buffer
 */

  roiptr=CLD->cd_currRoi;
  PDEBUGG(("roi %p: %dx%d+%d+%d (next is %p)\n",roiptr,
	  roiptr->wid,roiptr->hei,roiptr->x,roiptr->y,roiptr->next));
  if (roiptr->count==~0) roiptr->count=p; /* begin now */
  l=roiptr->count+CLD->cd_ispgm; /* real beginning */

  PDEBUGG(("p=%li,l=%li\n",p,l));
  if (p<l && count>=l-p)    /* header */
    {
    PDEBUGG(("header misses %li\n",l-p));
    memcpy_tofs(buf, roiptr->xtra.header + _CXG_PGM_OFFSET - (l-p), l-p);
    count-=l-p; buf+=l-p; p=l;
    }
  row=(p-l)/roiptr->wid;
  ram+= (board->lowres? 256 : 512)*(roiptr->y+row) + roiptr->x;
  PDEBUGG(("row %i (remainder %li)\n",row,(p-l)%roiptr->wid));
  if (!board->lowres)
    {
    page=(roiptr->y+row)/128;
    SETPAGE(board,page);
    ram -= 0x10000*page;
    }

  while (count >= roiptr->wid) /* body: only whole lines */
    {
    if (!board->lowres && (roiptr->y+row)/128 != page)
      { SETPAGE(board, ++page); ram-=0x10000; }
    memcpy_tofs(buf,(void *)ram,roiptr->wid);
    buf+=roiptr->wid; count-=roiptr->wid;
    ram += (board->lowres ? 256 : 512);
    if (++row == roiptr->hei)
      {
      CLD->cd_currRoi = roiptr->next;
      CLD->cd_currRoi->count = ~0;
      break;
      }
    }

  PDEBUGG(("read %i bytes\n",count0-count));
  filp->f_pos += count0-count;
  return count0-count; /* actually returned */
}

/*============================================================== write ===*/
static  int cx_write (struct inode *inode, struct file *filp,
		      const char *buf, int count)
{
  PDEBUG(("cx write\n"));
  return 0; /* still to go */
}

/*============================================================== ol_read ====*
 *
 * In order to act with overlay memory in a way consistent with the 
 * video memory, without loosing performance in video operations,
 * these functions toggle on and off overlay memory, and fall back to
 * the conventional read() and write()
 */
static  int cx_ol_read (struct inode *inode, struct file *filp,
		      char *buf, int count)
{
  int result;

  PDEBUG(("cx ol_read\n"));
  SETBIT(CLD->cd_board,CXG_BIT_OL_ENB);
  result=cx_read(inode,filp,buf,count);
  CLRBIT(CLD->cd_board,CXG_BIT_OL_ENB);
  return result;
}


/*============================================================== ol_write ===*/
static  int cx_ol_write (struct inode *inode, struct file *filp,
			 const char *buf, int count)
{
  int result;

  PDEBUG(("cx ol_write\n"));
  SETBIT(CLD->cd_board,CXG_BIT_OL_ENB);
  result=cx_write(inode,filp,buf,count);
  CLRBIT(CLD->cd_board,CXG_BIT_OL_ENB);
  return result;
}

/*============================================================== pgm_read ===*
 *
 * A pgm device returns a single pgm image on read(), and is cfg'ed by write()
 */
static  int cx_pgm_read (struct inode *inode, struct file *filp,
                      char *buf, int count)
{
  Cxg_Board *board=CLD->cd_board;
  Cxg_Roi *roiptr=board->roi;
  int ram=board->ram<<16;
  ulong p = filp->f_pos;
  int count0=count;
  unsigned short row, page=0;
  static char pgmh[_CXG_PGM_OFFSET+1];

  if (!roiptr)
    {
    roiptr=kmalloc(sizeof(Cxg_Roi),GFP_KERNEL);
    if (!roiptr) return -ENOMEM;
    if (board->lowres)
      roiptr->wid=roiptr->hei=256;
    else
      roiptr->wid=roiptr->hei=512;
    roiptr->x=roiptr->y=0;
    board->roi=roiptr;
    PDEBUG(("rd: malloc roi at %p\n",roiptr));
    }

  if (p>=_CXG_PGM_OFFSET+(roiptr->wid*roiptr->hei))
    return 0; /* end of pgm */

  if (p<_CXG_PGM_OFFSET)
    {
    sprintf(pgmh,"P5\n%3i %3i\n255\n",roiptr->wid,roiptr->hei);
    while (p<_CXG_PGM_OFFSET && count>0)
      {
      put_user(pgmh[p],buf);
      buf++; p++; count--;
      }
  
    }
    PDEBUGG(("pgm read: %ix%i+%i+%i\n",roiptr->wid,roiptr->hei,
	    roiptr->x, roiptr->y));

  /* now xfer the body, whole lines at a time... */
  row=(p-_CXG_PGM_OFFSET)/roiptr->wid;
  ram+= (board->lowres? 256 : 512)*(roiptr->y+row) + roiptr->x;
  PDEBUGG(("row %i (remainder %li)\n",row,(p-_CXG_PGM_OFFSET)%roiptr->wid));
  if (!board->lowres)
    {
    page=(roiptr->y+row)/128;
    SETPAGE(board,page);
    ram -= 0x10000*page;
    }

  while (count >= roiptr->wid) /* body: only whole lines */
    {
    PDEBUGG(("row %i\n",row));
    if (!board->lowres && (roiptr->y+row)/128 != page)
      { SETPAGE(board, ++page); ram-=0x10000; }
    memcpy_tofs(buf,(void *)ram,roiptr->wid);
    buf+=roiptr->wid; count-=roiptr->wid;
    ram += (board->lowres ? 256 : 512);
    if (++row == roiptr->hei)
      break;
    }
  filp->f_pos+=(count0-count);
  PDEBUGG(("read %i bytes\n",count0-count));
  return count0-count;
}
	    

/*============================================================== pgm_write ==*/
static  int cx_pgm_write (struct inode *inode, struct file *filp,
			  const char *buf, int count)
{
  Cxg_Board *board=CLD->cd_board;
  Cxg_Roi *roiptr=board->roi;
#define MYBUFLEN 32 /* enough... */
  static char mybuf[MYBUFLEN];
  char *p, *pe;
  int i, data[4];
  char datasep[4]="x++";
  int max=board->lowres ? 256 : 512;

  PDEBUGG(("cx pgm_write\n"));

/*
 * Writing to a pgm node means changing the resolution ('L'/'H') and
 * setting the region of interest. Any subsequent read will return that
 * very region. Note that one operation must come in one write.
 * Any settings are discarded on last close.
 */
  
  if (count>MYBUFLEN-1) count=MYBUFLEN-1;
  memcpy_fromfs(mybuf,buf,count); mybuf[count+1]='\0'; p=mybuf;
  
  
  while (count && isspace(*p))
    { count--; p++; }

  switch(*p)
    {
    case 'l': case 'L': /* FIXME -- remove duplicate */
      PDEBUG(("low res\n"));
      SETBIT(board,CXG_BIT_LOW_RES);
      board->lowres=1;
      if (board->roi)
	kfree(board->roi); 
      board->roi=NULL;
      count--; p++;
      break;
    case 'h': case 'H':
      PDEBUG(("high res\n"));
      CLRBIT(board,CXG_BIT_LOW_RES);
      board->lowres=0;
      if (board->roi)
	kfree(board->roi);
      board->roi=NULL;
      count--; p++;
      break;
    }
  if (p != mybuf) return p-mybuf;

  if (!isdigit(*p))
    return -EINVAL;

  /* ok, now read widxhei+x+y */

  for (i=0; i<4; i++)
    {
    data[i]=simple_strtoul(p,&pe,0);
    PDEBUGG(("i is %i: \"%s\", \"%s\", %i\n",i,p,pe,data[i]));
    if (pe==p) {i--; break;}
    p=pe;
    if (*p==datasep[i] || isspace(*p))
      p++;
    }
  if (i!=4 ) return -EINVAL;

  if (!roiptr)
    {
    board->roi=roiptr=kmalloc(sizeof(Cxg_Roi),GFP_KERNEL);
    if (!roiptr) return -ENOMEM;
    }
  if (data[2]>=max || data[3]>=max)
    {printk("CX100 pgm write: ul corner out of range\n"); return -EINVAL;}
  if (data[0]+data[2]>max || data[1]+data[3]>max)
    {printk("CX100 pgm write: br corner out of range\n"); return -EINVAL;}
  roiptr->wid=data[0]; roiptr->hei=data[1];
  roiptr->x = data[2]; roiptr->y = data[3];

  PDEBUGG(("wrote %i\n",p-mybuf));
  return p-mybuf;
}


/*============================================================== ioctl ===*/
static  int cx_ioctl(struct inode *inode, struct file * filp,
            unsigned int cmd, ulong arg)
{
  int err, opt;
  char *ptr;
  unsigned int *ptrarg=(unsigned int *)arg;
  Cxg_Board *board=CLD->cd_board;
  Cxg_Roi tmproi, *roiptr;

  /*
   * All nodes can get ioctls, tough some of them are out of context 
   */
  PDEBUGG(("cx ioctl: cmd is 0x%x -- arg is %p\n",cmd,ptrarg));

  switch(cmd)
    {
    /*============================== bit manipulation */

    case CXG_GETPORTS:
      err=verify_area(VERIFY_WRITE,(void *)arg,4*sizeof(char));
      if (err) return err;
      ptr=(char *)arg;      
      put_fs_byte(GETPORT(board,0),ptr++);
      put_fs_byte(GETPORT(board,1),ptr++);
      put_fs_byte(GETPORT(board,2),ptr++);
      put_fs_byte(GETPORT(board,6),ptr++);
      return 0;

    case CXG_SETBIT:
      if (arg > CXG_MAX_WR_BIT) return -EACCES;
      SETBIT(board,arg);
      return 0; 
      
    case CXG_CLRBIT:
      if (arg > CXG_MAX_WR_BIT) return -EACCES;
      CLRBIT(board,arg);
      return 0;

    case CXG_GETBIT:
      err=verify_area(VERIFY_WRITE,ptrarg,sizeof(unsigned int));
      if (err) return err;
      arg=get_fs_long(ptrarg);
      put_fs_long((unsigned int)(GETBIT(board,arg)&0xFF),ptrarg);
      return 0;

    case CXG_SETBITV:
      if (arg > 2*CXG_MAX_WR_BIT) return -EACCES;
      fs_outp(board,arg,PORT6(board));
      return 0; 

      /*============================== command */

    case CXG_COMMAND:
      err=verify_area(VERIFY_WRITE,ptrarg,sizeof(unsigned int));
      if (err) return err;
      arg=get_fs_long(ptrarg);
      put_fs_long((unsigned int)(COMMAND(board,arg)&0xFF),ptrarg);
      return 0;

      /*============================== information retrivial */

    case CXG_GETHWINFO:
      err=verify_area(VERIFY_WRITE,(void *)arg,sizeof(Cxg_Board));
      if (err) return err;
      cx_getfieldcount(board);
      board->has_video=GETBIT(board,CXG_BIT_HAVE_VID);
      board->status=COMMAND(board,CXG_CMD_GETSTATUS);
      memcpy_tofs((Cxg_Board *)arg,board,sizeof(Cxg_Board));
      return 0;

    case CXG_GETSWINFO:
      err=verify_area(VERIFY_WRITE,(void *)arg,sizeof(Cxg_Clientdata));
      if (err) return err;
      memcpy_tofs((Cxg_Clientdata *)arg,CLD,sizeof(Cxg_Clientdata));
      return 0;

    case CXG_GETRAMINFO:
      err=verify_area(VERIFY_WRITE,(void *)arg,16);
      if (err) return err;
      for (err=0;err<16;err++)
	put_fs_byte(cx_checkseg(err),(char *)arg+err);
      return 0;

      /*============================== grab control */

    case CXG_GRABCTL:
      if( !waitvb(board)) return -ENODATA; /* no video */
      opt=arg&0xff00;
      CLRBIT(board,CXG_BIT_ACQ_REQ);
      switch(arg &= 0xff)
	{
	case CXG_GRAB_FRAME:
	  if (board->lowres) SETBIT(board,CXG_BIT_FLD_GRAB);
	  else	             SETBIT(board,CXG_BIT_FRM_GRAB);
	  if (!(opt&CXG_GRAB_WAIT)) return 0;
	                      waitvb(board);
	  if (!board->lowres) waitvb(board);
	  return 0;

	case CXG_GRAB_FIELD0:
	case CXG_GRAB_FIELD1:
	  if (GETBIT(board,CXG_BIT_FIELD)&1 != arg-CXG_GRAB_FIELD0)
	    waitvb(board);
	  /* fall through */
	case CXG_GRAB_FIELD:
	  SETBIT(board,CXG_BIT_FLD_GRAB);
	  if (!(opt&CXG_GRAB_WAIT)) return 0;
	  waitvb(board);                 return 0;
 
	case CXG_GRAB_TRIGGERED: /* not implemented */
	default:
	  return -EINVAL;
	}
      case CXG_SCHEDULE:
        

      /*============================== delays */

    case CXG_WAITVB:
      arg&=3;
      if( !waitvb(board)) return -ENODATA; /* no video */
      if (arg>1) return 0;
      if (GETBIT(board,CXG_BIT_FIELD)&1 != arg) waitvb(board);
      return 0;

      /*============================== setting modes */

    case CXG_SETIRQ:
      return setirq(board,arg);

    case CXG_RAMPAGE:
      return setram(board,arg);

    case CXG_SETROI:
      PDEBUG(("ioctl: setroi\n"));
      if (CLD->cd_rois) kfree(CLD->cd_rois);
      err=verify_area(VERIFY_READ,ptrarg,sizeof(Cxg_Roi));
      if (err) return err;
      memcpy_fromfs(&tmproi,ptrarg,sizeof(Cxg_Roi));
      err=verify_area(VERIFY_READ,ptrarg,sizeof(Cxg_Roi)*tmproi.count);
      if (err) return err;

      if (!(err=tmproi.count)) return 0; /* done */
      PDEBUG(("Got %i Roi's\n",err));
      CLD->cd_rois=kmalloc(sizeof(Cxg_Roi)*err,GFP_KERNEL);
      if (!CLD->cd_rois) return -ENOMEM;
      memcpy_fromfs(CLD->cd_rois,ptrarg,sizeof(Cxg_Roi)*err);
      for (roiptr=CLD->cd_rois; err; roiptr++, err--)
	{
	roiptr->next=roiptr+1;
	sprintf(roiptr->xtra.header,"P5\n%3i\n%3i\n255\n",
		roiptr->wid,roiptr->hei);
	PDEBUG(("roi %p: %dx%d+%d+%d (next is %p)\n",roiptr,
		roiptr->wid,roiptr->hei,roiptr->x,roiptr->y,roiptr->next));
	PDEBUGG(("%p: header: \"%s\" next %p\n",
		roiptr,roiptr->header,roiptr->next));
	}
      roiptr--; /* back to the last */
      roiptr->next=CLD->cd_currRoi=CLD->cd_rois;
      CLD->cd_currRoi->count=~0; /* to be reset at first read */
      return 0;
      
    case CXG_SETPGMMODE:
      CLD->cd_ispgm= (arg ?_CXG_PGM_OFFSET : 0);
      return 0;

    case CXG_LOWRES:
      SETBITV(board,CXG_BIT_LOW_RES,!!arg);
      board->lowres=!!arg;
      if (board->roi) { kfree(board->roi); board->roi=NULL; }
      return 0;

    case CXG_INTERVAL:
      if ( (err=verify_area(VERIFY_READ,(void *)arg,2*sizeof(long))) )
	return err;
      if (board->irq>-0 || board->irq_count==0)
	return -EINVAL; /* can't block if no interrupts are there */
      CLD->cd_step=get_fs_long(ptrarg);
      CLD->cd_next=get_fs_long(ptrarg+1);
      if (CLD->cd_next==0)
	CLD->cd_next=cx_getfieldcount(board)+1;
      PDEBUG(("interval: %li %li\n",CLD->cd_step,CLD->cd_next));
      return 0;

      /*============================== field counter */

    case CXG_GETCOUNT:
      err=verify_area(VERIFY_WRITE,ptrarg,sizeof(unsigned int));
      if (err) return err;
      arg=cx_getfieldcount(board);
      memcpy_tofs(ptrarg,&arg,sizeof(unsigned int));
      return 0;
      
    case CXG_WAITCOUNT:
      err=verify_area(VERIFY_WRITE,ptrarg,sizeof(unsigned int));
      if (err) return err;
      arg=get_fs_long(ptrarg);
      err=cx_waitfieldcount(filp,board,arg,0);
      put_fs_long((unsigned int)err,ptrarg);
      return 0;
      
      /*============================== region xfer */

    case CXG_GETREGION:
    case CXG_PUTREGION:
      PDEBUG(("ioctl: getregion\n"));
      err=verify_area(VERIFY_READ,ptrarg,sizeof(Cxg_Roi));
      if (err) return err;
      memcpy_fromfs(&tmproi,ptrarg,sizeof(Cxg_Roi));
      arg=tmproi.wid*tmproi.hei;
      err=verify_area(cmd==CXG_GETREGION ? VERIFY_WRITE : VERIFY_READ,
		      tmproi.xtra.data,arg);
      if (err) return err;
      return cx_xfer(board,&tmproi,cmd==CXG_GETREGION ? XFER_IN : XFER_OUT);
  
      /*============================== lookup tables */

    case CXG_INLUT:
    case CXG_INLUT1:
    case CXG_GETINLUT:
      
    case CXG_OUTLUT:
    case CXG_OUTLUT1:
    case CXG_GETOUTLUT:

      /*============================== gain/offset */

      
    default:

    }
  return -ENOIOCTLCMD;
}

/*============================================================== fops ====*/
static struct file_operations cx_fops = {
    cx_lseek,
    cx_read,
    cx_write,
    NULL,       /* cx_readdir */  
    cx_select,
    cx_ioctl,
    NULL,       /* cx_mmap */
    cx_open,
    cx_close
};
    
/*
 * Overlays have different funcions.
 */

static struct file_operations cx_ol_fops = {
    cx_lseek,
    cx_ol_read,
    cx_ol_write,
    NULL,       /* cx_readdir */  
    NULL,       /* cx_select */
    cx_ioctl,
    NULL,       /* cx_mmap */
    cx_open,
    cx_close
};

static struct file_operations cx_pgm_fops = {
    NULL,       /* default */
    cx_pgm_read,
    cx_pgm_write,
    NULL,       /* cx_readdir */  
    cx_select,
    cx_ioctl,
    NULL,       /* cx_mmap */
    cx_open,
    cx_close
};


    
/*============================================================== INIT ===*/
/*=======================================================================*/

/*
 * This way to initialize the board won't work if the module would be
 * part of the kernel proper, because the function goes to sleep to wait
 * for board response. This is inacceptable at boot time, where only one
 * task is there.
 */

int init_module (void) 
{
  int base, err;

  PDEBUG(("cx init: base %x, fake %d\n",cxg_base,cxg_fake));

  if (cxg_base && cxg_base<PORT_MIN && cxg_base>PORT_MAX)
    {
    printk("cx init: supplied base 0x%x is out of range (0x%x-0x%x)\n",
	   cxg_base,PORT_MIN,PORT_MAX-PORT_STEP);
    return -EINVAL;
    }

/*
 * Look for a major, and allow auto-assignment
 */

  err = register_chrdev (cxg_major, "cx100", &cx_fops);
  if (err <0)
    {
    printk("cx init: failed with error %d\n",-err);
    return err;
    }
  if (cxg_major==0) cxg_major=err;
  PDEBUGG(("cx init: got major %d\n",cxg_major));

/* 
 * Look for ports, and allow auto_assignment
 */

  base = cxg_base ? cxg_base : PORT_MIN;
  do
    {
    if ((err=check_region(base,PORT_STEP))!=0)
      {
      PDEBUGG(("cx init: address 0x%x already in use",base));
      continue;
      }
    request_region(base,PORT_STEP,"cx100");

    hwinfo[cxg_boards].base=base;
    if ((err=cx_findboard(hwinfo+cxg_boards)) == 0)
      {
      /* found one */
      if (hwinfo[cxg_boards].fake)
	release_region(base,PORT_STEP); /* no region for fake boards */
      cxg_boards++;
      continue;
      }
    else
      PDEBUGG(("cx init: not found at 0x%x\n",base));

    release_region(base,PORT_STEP);
    }
  while (cxg_base==0 && (base+=PORT_STEP)<PORT_MAX);

  if (cxg_boards==0)
    {
    printk("cx init: no ports found\n");
    goto fail_port;
    }

/*
 * If such requested, get a page in memory (first device)
 */

  if (cxg_ram)
    {
    if (cxg_ram < 0x0A || cxg_ram > 0x0E)
      {
      PDEBUG(("cx init: ram out of range\n"));
      err=-EINVAL; goto fail_ram; 
      }
    
    if (COMMAND(hwinfo,0xD0+cxg_ram)!=0)
      {
      PDEBUG(("cx init: command failed\n"));
      err=-EINVAL; goto fail_ram;
      }
    printk("cx init: got ram page 0x%02x\n",cxg_ram);
    hwinfo->ram=cxg_ram;
    }

/*
 * If such requested, get the irq (first device)
 */

  if (cxg_irq)
    setirq(hwinfo,cxg_irq); /* ignore failure */

  printk(KERN_INFO "Cx100: driver loaded\n");
  return 0;

  fail_ram:  while (cxg_boards--) 
               release_region(hwinfo[cxg_boards].base,PORT_STEP);
  fail_port: unregister_chrdev(cxg_major,"cx100");

  PDEBUG(("cx init: Not loaded\n"));
  return err;
}

/*============================================================== CLEAN ===*/
void cleanup_module (void) 
{
  int b;

  if (MOD_IN_USE)
    {
    PDEBUG(("cx unload: busy\n"));
    return /* -EBUSY */;
    }
  printk((KERN_INFO "CX100: driver unloaded\n"));
  for (b=0; b<cxg_boards; b++)
    {
    if (hwinfo[b].fake)
      continue;
    COMMAND(hwinfo+b,CXG_CMD_SHUTDOWN);
    release_region(hwinfo[b].base,PORT_STEP);
    
    if (hwinfo[b].irq>=0)
      free_irq(hwinfo[b].irq);
    }

  unregister_chrdev(cxg_major,"cx100");
  return;
    
}

    
