/*
 * Revision 1.2: By Yacine (yacine@eyetap.org) Summer 2001
 *              * The driver now compiles under the Linux kernel 2.4.X
 *
 * Revision 1.1: By Taneem (taneem@eyetap.org) Tue Sep  5 20:36:44 EDT 2000
 *    	      	* Fixed the bug so that there won't be a mix up in minor numbers.
 *    	      	* Changed the code so the ibus port number can be changed through
 *                insmod.
 *    	      	       e.g.   insmod i_bus_base=0x278 ibus.o
 *    	      	* Took out the delay while writing (but the code is still there).
 *    	      	* Added some error checking
 *    	      	* Organized the code a bit
 *
 * i-bus2.x.c -- modified simple device driver for compatibility to 2.0 or 2.2
 * original implementation (Erlich, Mann, Fung, etc.) written for 2.0 kernels
 * 2000Apr20 Modified by Behr Raz (g7behr@cdf.utoronto.ca ) to compile under
 * pre and post 2.2.0 KERNEL versions
 *
 * i-bus2.x.c is source code for 2.x (e.g. 2.0 or 2.2 kernels)
 * ibus.o is the object
 * /dev/ibus0 and dev/ibus1 are the devices loaded by  load_i-bus.sh
 * echo -n -e "\377" > /dev/ibus0
 * is non inverting (turns all on)
 * echo -n -e "\377" > /dev/ibus1
 * is inverting (turns all off)
 */

#include <linux/module.h>
#include <linux/param.h>
#include <linux/delay.h>
#include <linux/sched.h>
#include <linux/kernel.h> /* printk() */
#include <linux/fs.h>     /* everything... */
#include <linux/errno.h>  /* error codes */
#include <linux/malloc.h>
#include <linux/ioport.h>

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

/* In 2.2.3 /usr/include/linux/versibusion.h includes 
 * a macro for this, but 2.0.35 doesn't - so I add 
 * it here if necessary. */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif

/* Conditional compilation. LINUX_VERSION_CODE is 
 * the code (as per KERNEL_VERSION) of this version. */
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
#include <asm/uaccess.h>  /* for put_user */
#include <linux/poll.h>
#endif

static int i_bus_major = 0;
static int i_bus_base  = 0;  /* Check module_init */
#define   DEFAULT_IBUS_BASE         0x240

#if LINUX_VERSION_CODE < KERNEL_VERSION(2,2,0)
#define   copy_to_user  	memcpy_tofs
#define   copy_from_user        memcpy_fromfs
#endif

enum i_bus_modes {I_BUS_DEFAULT=0, I_BUS_INVERT};

static int i_bus_open(struct inode *inode, struct file *filp){
  int minor = (MINOR(inode->i_rdev));

  if (minor > 1 && minor < 0) {
    printk("Error IBus: The minor number should be either 0 or 1\n");
    return -EINVAL;
  }
  MOD_INC_USE_COUNT;
  return 0;
}

/* This function is called when a process closes the 
 * device file. It doesn't have a return value in 
 * version 2.0.x because it can't fail (you must ALWAYS
 * be able to close a device). In version 2.2.x it is 
 * allowed to fail - but we won't let it. 
 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static int i_bus_release(struct inode *inode, struct file *filp){ 
#else 
     void i_bus_release(struct inode *inode, struct file *filp){
#endif

  MOD_DEC_USE_COUNT;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
  return 0;
#endif
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t i_bus_read(
    struct file *filp,
    char *buf,        /* The buffer to fill with data */
    size_t count,     /* The length of the buffer */
    loff_t *offset){  /* Our offset in the file */
#else
static int i_bus_read(
    struct inode *inode, 
    struct file *filp,
    char *buf,        /* The buffer to fill with the data */ 
    int count         /* The length of the buffer,mustn't write beyond that! */
){
#endif

  int retval = 1;
  unsigned port = i_bus_base;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
    int mode = (MINOR(filp->f_dentry->d_inode->i_rdev));
#else
    int mode = (MINOR(inode->i_rdev));
#endif
  unsigned char portdata;

  switch(mode) {
  case I_BUS_DEFAULT:
    portdata=inb(port);
    break;
  case I_BUS_INVERT:
    portdata=~inb(port);
    break;
  default:
    retval = -EINVAL;
        break;
  }
  if (retval < 0)
    return retval;
  else {
    copy_to_user(buf, &portdata,retval);
    return retval;
  }
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t i_bus_write(
    struct file *filp,
    const char *buf,
    size_t count,
    loff_t *offset
){
#else
int i_bus_write(
    struct inode *inode,
    struct file *filp,
    const char *buf,
    int count
){
#endif

  int retval = count;
  unsigned port = i_bus_base;
  unsigned char outval;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
    int mode = (MINOR(filp->f_dentry->d_inode->i_rdev));
#else
    int mode = (MINOR(inode->i_rdev));
#endif
  unsigned char *kbuf, *ptr;
  unsigned long j;
  
  kbuf = (unsigned char *) kmalloc(count, GFP_KERNEL);
  if (!kbuf)
    return -ENOMEM;

  copy_from_user(kbuf, buf, count);
  ptr=kbuf;
  switch(mode) {
  case I_BUS_DEFAULT:
    while (count--) {
      outval = *(ptr++);
      outb(outval,port);
#if 0
/* this adds a delay of 1 sec, but we don't need it for the first few labs.*/
      j = jiffies + 1 * HZ;
      while(jiffies<j) {
	schedule();
	outb(outval,port0);
      };
#endif
    };
    break;
  case I_BUS_INVERT:
    while (count--) {
      outval = ~ *(ptr++);
      outb(outval,port);
#if 0 
/* this adds a delay of 1 sec, but we don't need it for the first few labs.*/
      j = jiffies + 1 * HZ;
      while(jiffies<j) {
	schedule();
	outb(outval,port0);
      };
#endif
    };
    break;
  default:
    retval = -EINVAL;
    break;
  };
  kfree(kbuf);
  return retval;

}

struct file_operations i_bus_fops = {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
  NULL,
#endif
  NULL,          /* i_bus_lseek   */
  i_bus_read,
  i_bus_write,
  NULL,          /* i_bus_readdir */
  NULL,       	 /* i_bus_select  */
  NULL,          /* i_bus_ioctl   */
  NULL,          /* i_bus_mmap    */
  i_bus_open,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
  NULL,   /* flush */
#endif
  i_bus_release
};

#if LINUX_VERSION_CODE > 0x20118
MODULE_PARM(i_bus_base, "i");
#endif

int init_module(void){

  int result;

  if (!i_bus_base)    	        /* Check if i_bus_base was set through insmod */
    i_bus_base = DEFAULT_IBUS_BASE;  /* otherwise set it to DEFAULT_IBUS_BASE */

  result = check_region(i_bus_base,32);
  if (result){
    printk("i-bus2.x: can't get I/O address 0x%x\n",i_bus_base);
    return result;
  }
  request_region(i_bus_base,32,"ibus");

  result = register_chrdev(i_bus_major, "ibus", &i_bus_fops);
  if (result < 0){
      printk("i-bus2.x: can't get major number\n");
      release_region(i_bus_base,32);
      return result;
  }
  if (i_bus_major == 0) i_bus_major = result; /* dynamic */ 

  return 0;
}

void cleanup_module(void){

  unregister_chrdev(i_bus_major, "ibus");
  release_region(i_bus_base,32);
}

