/* rs_timer.c 
 * Copyright (C) 2002 by Wojciech M. Zabolotny
 * Significantly based on examples given by Ori Pomerantz in
 * "Kernel Module Programming Guide"
 * and by Alessandro Rubini in "Linux Device Drivers"
 * 
 * Prosty sterownik, pozwalający używać portu RS232 w niestandardowy
 * sposób jako timera generującego przerwania o regulowanej
 * w szerokim zakresie częstotliwości
 */


/* Standardowe zbiory nagłówkowe */

#include <linux/kernel.h>   
#include <linux/module.h>

/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif        

//#define read_write_t int
//#define count_t int
/* Zbiory nagłówkowe dla urządzeń znakowych */
#include <linux/fs.h>
#include <linux/wrapper.h>
#include <linux/mm.h>
#include <linux/config.h>
#include <linux/ioport.h>
#include <linux/interrupt.h>
#include <linux/poll.h>
#include <asm/io.h>
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif

/* Tu dołączamy nasze kody IOCTL */
#include <linux/ioctl.h>
#include "rs_timer.h"

/* Kody błędów */
//#include <errno.h>

#if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
#include <asm/uaccess.h>  /* for put_user */
#endif                             

#define SUCCESS 0
#define DEVICE_NAME "rs_timer"

/* Zmienna globalna określająca adres bazowy portu RS232 */
int io_base=0;
MODULE_PARM(io_base,"i");
/* Zmienna globalna określająca numer przerwania, którego używa nasz port */
int irq=0;
MODULE_PARM(irq,"i");
/* Zmienna globalna określająca szybkość transmisji RS i w konsekwencji
   częstostliwość przerwań */
int div=65535;
MODULE_PARM(div,"i");

/* Zmienna globalna określająca, czy sterownik ma używać tablicy rezerwacji I/O */
int use_IO_table=1;
MODULE_PARM(use_IO_table,"i");

/* Zmienna będąca licznikiem przerwań */
unsigned char irq_count = 0;
/* Zmienna będąca znacznikiem informacji, że urządzenie jest już otwarte */
unsigned char dev_open = 0;

/* definicje adresów portów */
#define RS_TX (io_base)
#define RS_RX (io_base)
#define RS_DIV (io_base)
#define RS_DIVL (io_base)
#define RS_DIVH (io_base+1)
#define RS_IER (io_base+1)
#define RS_IIR (io_base+2)
#define RS_FCR (io_base+2)
#define RS_LCR (io_base+3)
#define RS_MCR (io_base+4)
#define RS_LSR (io_base+5)
#define RS_MSR (io_base+6)
#define RS_SCRATCH (io_base+7)

/* Kolejka, na której będą oczekiwać procesy */
DECLARE_WAIT_QUEUE_HEAD (readqueue);
/*
Implementacja procedury obsługi przerwania
*/


void ster1_irq_bh(unsigned long unused)
{
  wake_up_interruptible(&readqueue);
}

DECLARE_TASKLET(ster1_tasklet,ster1_irq_bh,0);

void ster1_irq(int irq, void * dev_id, struct pt_regs *regs)
{
  /* Niezbędna obsługa sprzętu - zapis kolejnego znaku do nadania */
  outb(255,RS_TX);
  irq_count++;
  /* Zlecamy wykonanie kodu budzącego proces oczekujący na kolejce */
  tasklet_schedule(&ster1_tasklet);
}


/* Implementacja procedury inicjalizacji portu RS */
void rs_init(unsigned short rate_div)
{
  //Ustawiamy prędkość transmisji
  outb_p(0x80,RS_LCR);
  outw_p(rate_div,RS_DIV);
  //Ustawiamy parametry transmisji: 8b, bez parzystości, 1 stop
  outb_p(0x03,RS_LCR);
  //Włączamy zgłaszanie przerwań
  outb_p(0x3+0x8,RS_MCR);
  //Uaktywniamy przerwanie gdy pusty jest bufor nadajnika
  outb_p(0x02,RS_IER);
  //Wysyłamy pierwszy znak
  outb_p(0x05,RS_TX); 
  outb_p(0x05,RS_TX);
}
/* Implementacja procedury zatrzymania przerwań RS */
void rs_stop()
{
  outb_p(0,RS_MCR);
  outb_p(0,RS_IER);
}

/* Procedura odczytu urządzenia - zawsze 1 bajt */
ssize_t ster1_read(struct file *filp,
	char *buf, size_t count,loff_t off)
{
  //Usypiamy proces na odpowiedniej kolejce
  int res=wait_event_interruptible(readqueue,irq_count != 0);
  if(res) return res; //Zadanie otrzymało sygnał
  __copy_to_user(buf,&irq_count,1);
  irq_count=0;
  return 1; //Odczytano jeden znak
}	

/*
Implementacja funkcji otwarcia urządzenia
 */
static int ster1_open(struct inode *inode, 
                       struct file *file)
{
  int res=0;
  if(dev_open) return -EBUSY; //Pozwalamy tylko na jednokrotne otwarcie
  /* Teraz instalujemy nasze przerwanie */
  res=request_irq(irq,ster1_irq,0,DEVICE_NAME,NULL);
  if(res) {
    printk (KERN_INFO "ster1_rs: nie można podłączyć irq %i\n", irq);
    irq = -1;
  }
  /* W poprzedniej wersji tu było wywołanie rs_init(div), które
     włączało urządzenie, w tej wersji - dopiero wywołanie IOCTL
     uruchomi urządzenie
  */
  MOD_INC_USE_COUNT;
  return SUCCESS;
}


/* Implementacja funkcji ioctl */
int ster1_ioctl(struct inode *inode,struct file *filp,
		unsigned int cmd,unsigned long arg)
{
  switch(cmd) {
  case IOCTL_SET_DIV:
    rs_init(arg);
    return 0;
  case IOCTL_STOP:
    rs_stop();
    return 0;
  }
  return -ENOTTY;
}

/* Implementacja metody poll */
unsigned int ster1_poll(struct file *filp,poll_table *wait)
{
  unsigned int mask =0;
  poll_wait(filp,&readqueue,wait);
  if(irq_count) mask |= POLLIN |POLLRDNORM;
  return mask;
}

//Funkcja zwalniania urządzenia

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static int ster1_release(struct inode *inode, 
                          struct file *file)
#else 
static void ster1_release(struct inode *inode, 
                           struct file *file)
#endif
{
#ifdef DEBUG
  printk ("<1>device_release(%p,%p)\n", inode, file);
#endif
  /* Najpierw blokujemy zgłaszanie przerwań przez urządzenie */
  rs_stop();
  /* Teraz zwalniamy przerwanie */
  if(irq>=0) free_irq(irq,NULL);
  dev_open=0;
  MOD_DEC_USE_COUNT;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
  return 0;
#endif
}


struct file_operations Fops = {
  .read=ster1_read, /* read */
  .open=ster1_open,
  .ioctl=ster1_ioctl,
  .poll=ster1_poll,
  .release=ster1_release,  /* a.k.a. close */
};

static int Major=0;

/* Inicjalizacja modułu */
int init_module()
{
  int err;
  /* Register the character device (at least try) */
  Major = register_chrdev(0, 
                          DEVICE_NAME,
                          &Fops);
  /* Negative values signify an error */
  if (Major < 0) {
    printk ("<1>%s device failed with %d\n",
            "Sorry, registering the character",
            Major);
    return Major;
  }
  /* Rezerwujemy obszar IO */
  if(use_IO_table) {
    err=check_region(io_base,8);
    if(err<0) {
      printk ("<1>%s device failed with %d\n",
	      "Sorry, registering the character",
	      Major);
      return err;
    }
    request_region(io_base,8,DEVICE_NAME);
  }
  printk ("<1>%s The major device number is %d.\n",
          "Registeration is a success.",
          Major);
  printk ("<1>If you want to talk to the device driver,\n");
  printk ("<1>you'll have to create a device file. \n");
  printk ("<1>We suggest you use:\n");
  printk ("<1>mknod <name> c %d <minor>\n", Major);
  printk ("<1>You can try different minor numbers %s",
          "and see what happens.\n");
  return 0;
}

/* Usuwanie modułu - sprzątamy po sobie */
void cleanup_module()
{
  int ret;
  /* Zwalniamy obszar IO */
  if(use_IO_table) release_region(io_base,8);
  /* Odrejestrowujemy urządzenie */
  ret = unregister_chrdev(Major, DEVICE_NAME);
  /* If there's an error, report it */ 
  if (ret < 0)
    printk("<1>Error in unregister_chrdev: %d\n", ret);
}
