/*
    comedi/drivers/conrad_190226.c

    COMEDI - Linux Control and Measurement Device Interface
    Copyright (C) 2002 Anders Blomdell <anders.blomdell@control.lth.se>

    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.

*/

/*
Driver: conrad_190226
Description: Driver for serial connected Conrad LTC1090/LTC1290 based 8 channel 10/12 bit ADC
Devices:
Author: Sven Killig, based on http://www.netzmafia.de/skripten/hardware/da-wandler/da-wandler.html
Updated: Sat, 27 Sep 2008 23:34:00 +0200
Status: in development

*/

/*
sync; cd /home/sonic/download/comedi/comedi-0.7.76/; sudo comedi_config -r /dev/comedi0; sudo rmmod conrad_190226; sudo make install; sudo rm /lib/modules/2.6.24-19-generic/comedi/drivers/s626.ko; sudo modprobe conrad_190226; sudo comedi_config /dev/comedi0 conrad_190226
*/
#include <linux/comedidev.h>

#include <linux/delay.h>
#include <linux/ioport.h>

#include <asm/termios.h>
#include <asm/ioctls.h>
#include <linux/serial.h>
#include <linux/poll.h>


/*#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/ioctl.h>*/
#include <linux/workqueue.h>


/*
 * Board descriptions for two imaginary boards.  Describing the
 * boards in this way is optional, and completely driver-dependent.
 * Some drivers use arrays such as this, other do not.
 */
typedef struct conrad_190226_board_struct {
	const char *name;
} conrad_190226_board;

static const conrad_190226_board conrad_190226_boards[] = {
	{
      name:	"conrad_190226"}
};

/*
 * Useful for shorthand access to the particular board structure
 */
#define thisboard ((const conrad_190226_board *)dev->board_ptr)

typedef struct {
	unsigned int port;		// /dev/ttyS<port>
	struct file *tty;
	//double Ref;
	//int SerialFd;	// file descriptor
	//int16_t *inBuffer;
atomic_t nicht_mehr_einhaengen /*= 0*/;
//struct timer_list mytimer;
struct work_struct mywork;
//struct delayed_work mywork;
int i,j;
} conrad_190226_private;

/*
 * most drivers define the following macro to make it easy to
 * access the private structure.
 */
#define devpriv ((conrad_190226_private *)dev->private)





/*
 Conrad 8Channel 10/12-Bit ADC Board:
 Current: 2.2 mA

	Serial <-> LTC1090
 -------------------
	8(CTS) <-> Dout
	7(RTS) <-> Din
	4(DTR) <-> SClk
	FF <-> CS

	Serial Port : 0x3f8 (default)
	all signals are active low (refer to 8250 (or 16550) data-sheet for details)!
	Modem Control Register: 0x3fc (bit 0:DTR bit 1:RTS)
	Modem Status Register:  0x3fe (bit 4:CTS)

	SClk lo -> CS Low (DFF Reset)
	SClk hi and rising edge on Din -> CS High
*/

/*// Number of channels
#define NUMCHANNELS       8

// Size of one A/D value
#define SIZEADIN          ((sizeof(int16_t)))

// Size of the input-buffer IN BYTES
// Always multiple of 8 for 8 microframes which is needed in the highspeed mode
#define SIZEINBUF         ((8*SIZEADIN))*/



#define Reference_Voltage 5.000
#define AClk 336000

// single differential
#define LTC1090_DIFF   (0 << 0)
#define LTC1090_SINGLE (1 << 0)

// Uni
#define LTC1090_UNI    (1 << 4)
// MSB First
#define LTC1090_MSBF   (1 << 5)
// Word len (warning ! Bit reversal)
#define LTC1090_WL8    (0 << 6)
#define LTC1090_WL10   (2 << 6)
#define LTC1090_WL12   (1 << 6)
#define LTC1090_WL16   (3 << 6)

// in : ABC A:MSB ->  C->1 A->2 B->3 
#define LTC1090_CHANNEL(x) ( ((x & 1) << 1) | ((x & 2) << 2) | ((x & 4)))

/*static const comedi_lrange range_conrad_190226_ai_range = { 1, {
			UNI_RANGE(5),
	}
};*/





static int conrad_190226_attach(comedi_device * dev, comedi_devconfig * it);
static int conrad_190226_detach(comedi_device * dev);
comedi_driver driver_conrad_190226 = {
      driver_name:"conrad_190226",
      module:THIS_MODULE,
      attach:conrad_190226_attach,
      detach:conrad_190226_detach,
      board_name:&conrad_190226_boards[0].name,
      offset:sizeof(conrad_190226_board),
      num_names:sizeof(conrad_190226_boards) / sizeof(conrad_190226_board),
};

static int conrad_190226_ai_rinsn(comedi_device * dev, comedi_subdevice * s,
	comedi_insn * insn, lsampl_t * data);

static void Ltc1090(comedi_device * dev, double r /*= 5.000*/);
static void destroy_Ltc1090(comedi_device * dev);
static void init(comedi_device * dev, unsigned int port /*= 0*/);
static unsigned int readVol(comedi_device * dev, unsigned int next_ch);
static void setSclk(comedi_device * dev, unsigned int set /*= 1*/);
static void setDin(comedi_device * dev, unsigned int set /*= 1*/);
static unsigned int getDout(comedi_device * dev);
static void closeSerial(comedi_device * dev);
static void initSerial(comedi_device * dev, unsigned int p);
static unsigned int setDtr(comedi_device * dev, unsigned int s);
static unsigned int setRts(comedi_device * dev, unsigned int s);
static unsigned int setLine(comedi_device * dev, unsigned int set, unsigned int line);
static unsigned int readCts(comedi_device * dev);

static void inc_count(unsigned long arg);
static int ktimer_init(comedi_device * dev);
static void ktimer_exit(comedi_device * dev);

static void Ltc1090(comedi_device * dev, double r /*= 5.000*/) {
	printk("Ltc1090()\n");
	//devpriv->Ref = r;	// Reference Voltage / V
	setSclk(dev, 1);
}

static void destroy_Ltc1090(comedi_device * dev) {
	printk("destroy_Ltc1090()\n");
	closeSerial(dev);
}

/**
	\brief call this to setup the serial port
	\param port the port number: 0 means first port
*/
static void init(comedi_device * dev, unsigned int port /*= 0*/) {
	printk("init()\n");
	initSerial(dev, port);
}

/**
	\brief read previously selected channel from ltc1090.
	\param next_ch Channel which should be read by next call
	\return Voltage from previously selected input
*/
static unsigned int readVol(comedi_device * dev, unsigned int next_ch) {
	unsigned int i, v = 0;
	unsigned int Cfg = LTC1090_SINGLE | LTC1090_UNI | LTC1090_MSBF | /*LTC1090_WL10*/LTC1090_WL12 | LTC1090_CHANNEL(next_ch);
	//printk("readVol(%i)\n", next_ch);
	for (i = 0; i < /*10*/12; i++) {
		setSclk(dev, 0);	// set SClk to low (also CS low)
comedi_udelay(1);
		setDin(dev, Cfg & (1 << i)); // prepare data input bit
comedi_udelay(1);
		setSclk(dev, 1); // rise SClk
comedi_udelay(1);
		v |= (1-getDout(dev)); // read data output bit
comedi_udelay(1);
		v <<= 1; // shift
	}
	// SClk still hi -> reset FF with rising edge on Din
	setDin(dev, 0);
comedi_udelay(1);
	setDin(dev, 1);
	//comedi_udelay(175 /*(100*1000*1000)/AClk*/ );  // conversion time
	//return devpriv->Ref*((v>>1)/ /*1024.0*/ /*4096.0*/ 4.096); // return 
	return v>>1;
}

/**
	\brief control SClk line. Setting to low will also set CS to low (enable).
	To disable CS set SClk to high and send rising edge on Din
*/
static void setSclk(comedi_device * dev, unsigned int set /*= 1*/) {
	setDtr(dev, set);
}

/**
	\brief Control Din line. Also Clock for Flipflop to rise CS (disable)
*/
static void setDin(comedi_device * dev, unsigned int set /*= 1*/) {
	setRts(dev, set);
}

/**
	\brief Dout line. This line has no special behaviour.
*/
static unsigned int getDout(comedi_device * dev) {
	return readCts(dev);
}

static void closeSerial(comedi_device * dev) {
	printk("closeSerial()\n");
	//if (devpriv->tty > 0) close(devpriv->tty);
	if (!IS_ERR(devpriv->tty) && (devpriv->tty != 0)) {
		filp_close(devpriv->tty, 0);
	}
}

static void initSerial(comedi_device * dev, unsigned int p) {
	/*char devname[16];
	sprintf(devname, "/dev/ttyS%d", p);
	devpriv->SerialFd = open(devname, O_RDWR);
	if (devpriv->SerialFd < 0) return devpriv->SerialFd;
	return 0;*/
	char port[20];
	printk("initSerial(%i)\n", p);

	sprintf(port, "/dev/ttyS%d", p);
	devpriv->tty = filp_open(port, 0, O_RDWR);
}

static unsigned int setDtr(comedi_device * dev, unsigned int s) {
	return setLine(dev, s, TIOCM_DTR);
}

static unsigned int setRts(comedi_device * dev, unsigned int s) {
	return setLine(dev, s, TIOCM_RTS);
}


static unsigned int setLine(comedi_device * dev, unsigned int set, unsigned int line) {
	int r;
	/*int*/unsigned long lines;
	//long result = 0;
	mm_segment_t oldfs;

	/*if (!IS_ERR(devpriv->tty) && (devpriv->tty != 0)) {
		printk("setLine.IS_ERR.1\n");
		return -100;
	}*/

	//r = ioctl(devpriv->SerialFd, TIOCMGET, &lines);
	oldfs = get_fs();
	set_fs(KERNEL_DS);
	r=devpriv->tty->f_op->ioctl(devpriv->tty->f_dentry->d_inode, devpriv->tty, TIOCMGET, (unsigned long)&lines);
	if (r != 0) {
		printk("!setLine.1.r\n");
		set_fs(oldfs);
		return r;
	}
	lines &= ~line;
	if (set) lines |= line;

	//r = ioctl(devpriv->SerialFd, TIOCMSET, &lines);
	oldfs = get_fs();
	set_fs(KERNEL_DS);
	r=devpriv->tty->f_op->ioctl(devpriv->tty->f_dentry->d_inode, devpriv->tty, TIOCMSET, (unsigned long)&lines);
	if (r != 0) {
		printk("!setLine.2.r\n");
	}

	set_fs(oldfs);
	return 0;
}

static unsigned int readCts(comedi_device * dev) {
	int r;
	/*int*/ long lines;
	//long result = 0;
	mm_segment_t oldfs;

	/*if (!IS_ERR(devpriv->tty) && (devpriv->tty != 0)) {
		printk("readCts.IS_ERR.1\n");
		return -100;
	}*/

	//r = ioctl(devpriv->SerialFd, TIOCMGET, &lines);
	oldfs = get_fs();
	set_fs(KERNEL_DS);
	r=devpriv->tty->f_op->ioctl(devpriv->tty->f_dentry->d_inode, devpriv->tty, TIOCMGET, (unsigned long)&lines);
	set_fs(oldfs);

	if (r != 0) {
		printk("!readCts.r\n");
		return r;
	}
	if((lines & TIOCM_CTS) != 0) return 1;
	else return 0;
}





// This will cancel a running acquisition operation.
// This is called by comedi but never from inside the
// driver.
static int conrad_190226_ai_cancel(comedi_device * dev, comedi_subdevice * s)
{
	int res = 0;
	printk("conrad_190226_ai_cancel()\n");
	atomic_set(&(devpriv->nicht_mehr_einhaengen),1);
	//ktimer_exit(dev);
	//cancel_delayed_work(&(devpriv->mywork));
	flush_scheduled_work();
	return res;
}


static int conrad_190226_ai_cmdtest(comedi_device * dev,
	comedi_subdevice * s, comedi_cmd * cmd)
{
	int err = 0, tmp, i;
	unsigned int tmpTimer;
	/*usbduxsub_t *this_usbduxsub = dev->private;
	if (!(this_usbduxsub->probed)) {
		return -ENODEV;
	}
#ifdef NOISY_DUX_DEBUGBUG*/
	printk("comedi%d: conrad_190226_ai_cmdtest\n", dev->minor);
//#endif
	/* make sure triggers are valid */
	// Only immediate triggers are allowed
	tmp = cmd->start_src;
	cmd->start_src &= TRIG_NOW | TRIG_INT;
	if (!cmd->start_src || tmp != cmd->start_src)
		err++;

	// trigger should happen timed
	tmp = cmd->scan_begin_src;
	// start a new _scan_ with a timer
	cmd->scan_begin_src &= TRIG_TIMER;
	if (!cmd->scan_begin_src || tmp != cmd->scan_begin_src)
		err++;

	// scanning is continous
	tmp = cmd->convert_src;
	cmd->convert_src &= TRIG_NOW;
	if (!cmd->convert_src || tmp != cmd->convert_src)
		err++;

	// issue a trigger when scan is finished and start a new scan
	tmp = cmd->scan_end_src;
	cmd->scan_end_src &= TRIG_COUNT;
	if (!cmd->scan_end_src || tmp != cmd->scan_end_src)
		err++;

	// trigger at the end of count events or not, stop condition or not
	tmp = cmd->stop_src;
	cmd->stop_src &= TRIG_COUNT | TRIG_NONE;
	if (!cmd->stop_src || tmp != cmd->stop_src)
		err++;

	if (err)
		return 1;

	/* step 2: make sure trigger sources are unique and mutually compatible */
	/* note that mutual compatiblity is not an issue here */
	if (cmd->scan_begin_src != TRIG_FOLLOW &&
		cmd->scan_begin_src != TRIG_EXT &&
		cmd->scan_begin_src != TRIG_TIMER)
		err++;
	if (cmd->stop_src != TRIG_COUNT && cmd->stop_src != TRIG_NONE)
		err++;

	if (err)
		return 2;

	/* step 3: make sure arguments are trivially compatible */

	if (cmd->start_arg != 0) {
		cmd->start_arg = 0;
		err++;
	}

	if (cmd->scan_begin_src == TRIG_FOLLOW) {
		/* internal trigger */
		if (cmd->scan_begin_arg != 0) {
			cmd->scan_begin_arg = 0;
			err++;
		}
	}

	if (cmd->scan_begin_src == TRIG_TIMER) {
		//if (this_usbduxsub->high_speed) {
			// In high speed mode microframes are possible.
			// However, during one microframe we can roughly
			// sample one channel. Thus, the more channels
			// are in the channel list the more time we need.
			i = 1;
			// find a power of 2 for the number of channels
			/*while (i < (cmd->chanlist_len)) {
				i = i * 2;
			}*/
			if (cmd->scan_begin_arg < (1400000 / 8 * i)) {
				cmd->scan_begin_arg = 1400000 / 8 * i;
				err++;
			}
			// now calc the real sampling rate with all the rounding errors
			tmpTimer =
				((unsigned int)(cmd->scan_begin_arg / 175000)) *
				175000;
			if (cmd->scan_begin_arg != tmpTimer) {
				cmd->scan_begin_arg = tmpTimer;
				err++;
			}
		/*} else {	// full speed
			// 1kHz scans every USB frame
			if (cmd->scan_begin_arg < 1000000) {
				cmd->scan_begin_arg = 1000000;
				err++;
			}
			// calc the real sampling rate with the rounding errors
			tmpTimer =
				((unsigned int)(cmd->scan_begin_arg /
					1000000)) * 1000000;
			if (cmd->scan_begin_arg != tmpTimer) {
				cmd->scan_begin_arg = tmpTimer;
				err++;
			}
		}*/
	}
	// the same argument
	if (cmd->scan_end_arg != cmd->chanlist_len) {
		cmd->scan_end_arg = cmd->chanlist_len;
		err++;
	}

	if (cmd->stop_src == TRIG_COUNT) {
		/* any count is allowed */
	} else {
		/* TRIG_NONE */
		if (cmd->stop_arg != 0) {
			cmd->stop_arg = 0;
			err++;
		}
	}

	if (err)
		return 3;

	return 0;
}




static void inc_count(unsigned long arg)
{
int pk=0;
printk("inc_count(%i)\n",pk++);
	comedi_device *dev = (comedi_device *) arg;
	comedi_async *async = dev->read_subdev->async;
	comedi_cmd *cmd = &async->cmd;
    //printk("inc_count called (%ld)...\n", devpriv->mytimer.expires );
printk("inc_count(%i)\n",pk++);
	int i=0, n, j;
printk("inc_count(%i)\n",pk++);
	unsigned int chan;
printk("inc_count(%i)\n",pk++);
	n = cmd->chanlist_len;
printk("inc_count(%i)\n",pk++);
	// dummy first read
	chan = CR_CHAN(cmd->chanlist[0]);
printk("inc_count(%i)\n",pk++);

	//for(j=1; j <= cmd->stop_arg; j++) {
		//for (i = 0; i < n; i++) {
			chan = CR_CHAN(cmd->chanlist[(i+1)&7]);
printk("inc_count(%i)\n",pk++);
			//devpriv->inBuffer[(i-1)&7]=readVol(dev, chan);
			// transfer data
			//comedi_buf_put(s->async,devpriv->inBuffer[(i-1)&7]);
			comedi_buf_put(async,readVol(dev, chan));
//			comedi_buf_put(s->async,0);
//			readVol(dev, chan);
printk("inc_count(%i)\n",pk++);
		//}
	//}




   //devpriv->mytimer.expires = jiffies + (2*HZ); // 2 second
printk("inc_count(%i)\n",pk++);
    if( atomic_read( &devpriv->nicht_mehr_einhaengen ) ) {
        //complete( &on_exit );
	async->events |= COMEDI_CB_EOA;
	async->events |= COMEDI_CB_EOS;
	comedi_event(dev, dev->read_subdev);
    } else {
        //add_timer( &devpriv->mytimer );
	schedule_delayed_work(&devpriv->mywork, jiffies + (2*HZ));
    }
printk("inc_count(%i)\n",pk++);
}

/*static int ktimer_init(comedi_device * dev)
{
	printk("ktimer_init()\n");
	init_timer( &devpriv->mytimer );
	devpriv->mytimer.function = inc_count;
	devpriv->mytimer.data = (unsigned long)dev;
	devpriv->mytimer.expires = jiffies + (2*HZ); // 2 second
	add_timer( &devpriv->mytimer );
	return 0;
}

static void ktimer_exit(comedi_device * dev)
{
	printk("ktimer_exit()\n");
    if( timer_pending( &devpriv->mytimer ) )
        printk("Timer ist aktiviert ...\n");
    if( del_timer_sync( &devpriv->mytimer ) )
        printk("Aktiver Timer deaktiviert\n");
    else
        printk("Kein Timer aktiv\n");
}*/





static int conrad_190226_ai_cmd(comedi_device * dev, comedi_subdevice * s)
{
	comedi_cmd *cmd = &s->async->cmd;
	int i, n, j;
	unsigned int chan;
	printk("conrad_190226_ai_cmd()\n");
	n = s->async->cmd.chanlist_len;
	// dummy first read
	chan = CR_CHAN(cmd->chanlist[0]);
	readVol(dev, chan);

	/*for(j=1; j <= cmd->stop_arg; j++) {
		for (i = 0; i < n; i++) {
			chan = CR_CHAN(cmd->chanlist[(i+1)&7]);
			//devpriv->inBuffer[(i-1)&7]=readVol(dev, chan);
			// transfer data
			//comedi_buf_put(s->async,devpriv->inBuffer[(i-1)&7]);
			comedi_buf_put(s->async,readVol(dev, chan));
		}
	}

	// tell comedi that data is there
	s->async->events |= COMEDI_CB_EOA;
	s->async->events |= COMEDI_CB_EOS;
	comedi_event(dev, s);*/
	ktimer_init(dev);
	//INIT_WORK(&devpriv->mywork, inc_count, &dev);
	DECLARE_WORK(&devpriv->mywork, inc_count, &dev);
	return 0;
}


static int conrad_190226_ai_rinsn(comedi_device * dev, comedi_subdevice * s,
	comedi_insn * insn, lsampl_t * data)
{
	int n;
	int chan;
	printk("conrad_190226_ai_rinsn()\n");

	// sample one channel
	chan = CR_CHAN(insn->chanspec);
	
	// dummy first read
	readVol(dev, chan);
	for (n = 0; n < insn->n; n++) {
		data[n] = readVol(dev, chan);
	}
	return n;
}




static int conrad_190226_attach(comedi_device * dev, comedi_devconfig * it)
{
	comedi_subdevice *s;

	printk("comedi%d: conrad_190226: ", dev->minor);
	dev->board_name = thisboard->name;
	/* set number of subdevices */
	dev->n_subdevices = 1;
	if (alloc_private(dev, sizeof(conrad_190226_private)) < 0) {
		return -ENOMEM;
	}

	devpriv->port = it->options[0];
	atomic_set(&(devpriv->nicht_mehr_einhaengen),0);
	printk("/dev/ttyS%d\n", devpriv->port);

	init(dev, it->options[0]);	// open Serial port
	//init(dev, it->options[0]);	// open Serial port
	/*if (i != 0) {
		printk("Cannot init serial port\n");
		return -1;
	}*/
	if (IS_ERR(devpriv->tty)) {
		printk("conrad_190226: file open error = %ld\n", PTR_ERR(devpriv->tty));
		return -1; // TODO
	}
	Ltc1090(dev, Reference_Voltage);	// set to correct Refernce Voltage

	if (alloc_subdevices(dev, 1) < 0)
		return -ENOMEM;

	/* analog input subdevice */
	s = dev->subdevices + 0;
	dev->read_subdev = s;
	s->type = COMEDI_SUBD_AI;
	s->subdev_flags = SDF_READABLE | SDF_GROUND | SDF_CMD_READ;
	// 8 channels
	s->n_chan = 8;
	// length of the channellist
	s->len_chanlist = 8;
	// max value from the A/D converter (12bit)
	s->maxdata = 0xfff;
	// range table to convert to physical units
	//s->range_table = &range_conrad_190226_ai_range;
	s->range_table = &range_unipolar5;
	// callback functions
	s->insn_read = conrad_190226_ai_rinsn;
	s->do_cmdtest = conrad_190226_ai_cmdtest;
	s->do_cmd = conrad_190226_ai_cmd;
	s->cancel = conrad_190226_ai_cancel;

	/*// create space for the in buffer and set it to zero
	devpriv->inBuffer = kzalloc(SIZEINBUF, GFP_KERNEL);
	if (!(devpriv->inBuffer)) {
		printk("comedi_: conrad_190226: could not alloc space for inBuffer\n");
		return -ENOMEM;
	}*/

	printk("attached\n");

	return 1;
}

static int conrad_190226_detach(comedi_device * dev)
{
	printk("comedi%d: conrad_190226: remove\n", dev->minor);

	destroy_Ltc1090(dev);

	return 0;
}

COMEDI_INITCLEANUP(driver_conrad_190226);

