/* Driver for the XYCOM XVME 203/293 used as a real time
   clock for RCI */

#include "xyclk.h"
#if NXYCLK > 0

#include <sys/types.h>
#include <sys/param.h>
#include <sys/buf.h>
#include <sys/file.h>
#include <sys/dir.h>
#include <sys/systm.h>
#include <sys/time.h>
#include <sys/user.h>
#include <sys/errno.h>
#include <sys/kernel.h>
#include <machine/psl.h>
#include <sundev/mbvar.h>

#include <rciext/spl.h>
#include <rciext/rci.h>
#include <rciext/rciioctl.h>
#include <rciext/xyclkreg.h>
#ifdef RCI
#include <rciext/rciioctl.h>
#endif

struct xyclk_softc {
	int open;		/* device is open */
	int enabled;		/* device will run when open */
	int rate;		/* interrupt interval (msec) */
	int debug;		/* debug flag */
	unsigned short hardcount; /* last counter reading */
	unsigned long icount;	/* interrupt count */
} xyclk_softc[NXYCLK];

#define DEFAULT_RATE	5

union pack {
	unsigned char c[2];
	unsigned short s;
  };
	
/* Macros for sending commands. Assumes 'xyclk' and 'pack' defined */
#define COMMAND_A(c)	(xyclk->cntrA_com = (c))
#define WRITE_A(d)	(pack.s = (d), 			\
			 xyclk->cntrA_data = pack.c[1], \
			 xyclk->cntrA_data = pack.c[0])
#define READ_A(d)	(pack.c[1] = xyclk->cntrA_data,	\
			 pack.c[0] = xyclk->cntrA_data,	\
			 (d) = pack.s)
#define COMMAND_B(c)	(xyclk->cntrB_com = (c))
#define WRITE_B(d)	(pack.s = (d), 			\
			 xyclk->cntrB_data = pack.c[1], \
			 xyclk->cntrB_data = pack.c[0])
#define READ_B(d)	(pack.c[1] = xyclk->cntrB_data,	\
			 pack.c[0] = xyclk->cntrB_data,	\
			 (d) = pack.s)
#define COMMAND_I(c)	(xyclk->uic_com = (c))
#define WRITE_I(d)	(xyclk->uic_data = (d))
#define READ_I(d)	((d) = xyclk->uic_data)

int xyclkprobe(), xyclkattach();

struct  mb_device *xyclkdinfo[NXYCLK];
struct  mb_driver xyclkdriver = 
    { xyclkprobe, 
      0, 
      xyclkattach, 
      0, 0, 0, 
      sizeof(struct xyclkdevice), 
      "xyclk", 
      xyclkdinfo, 
      0, 0, 0, 0
    };
int xywallclk_debug = 0;
int max_xyclkintr = 10;
int num_xyclkintr = 0;
int xywallclk_retry = 1;
int xywallclk_hit = 0;

#include <sys/time.h>

static struct timeval readWallClock()
{
	register struct xyclk_softc *xysoft;
	register struct xyclkdevice *xyclk;
	union pack pack;
	unsigned short count2;
	unsigned short count3;
	struct timeval tv;
	unsigned short hardcount;
	int icount;
	int rate;
	int inc;
	int usec_500;
	int extra_usec;
	int s;

	/* hack for now: assume unit 0 */
	int unit = 0;

	xysoft = &xyclk_softc[unit];
	xyclk = (struct xyclkdevice *)xyclkdinfo[unit]->md_addr;

	s = splhigh();

	hardcount = xysoft->hardcount;
	rate = xysoft->rate;
	icount = xysoft->icount;
	COMMAND_A (SAVE | 0x6);
	COMMAND_A (LOAD_PTR | HOLD_REG | CNTR2);
	READ_A (count2);
	/* If counter2 equals rate*500, it may have just wrapped
	   and we should resave counter 3 */
	if (count2 == rate*500)
	 { xywallclk_hit++;
	   if (xywallclk_retry)
	    { COMMAND_A (SAVE | 0x4);
	    }
	 }
	COMMAND_A (LOAD_PTR | HOLD_REG | CNTR3);
	READ_A (count3);

	splx(s);

	extra_usec = rate*500 - count2;
	if ((inc = count3 - hardcount) < 0)
	 { inc += 0x10000;
	 }
	/* total time, in units if 500 usec, is

		   2 rate icount + rate inc + extra_usec / 500

	   with remainder of extra_usec % 500. This time is
	   then converted into seconds and useconds. */

	usec_500 = 2*rate*icount + rate*inc + extra_usec/500;
	tv.tv_sec = usec_500/2000;
	tv.tv_usec = extra_usec%500 + (usec_500%2000)*500;

	if (xywallclk_debug)
	 { printf ("count2=%d count3=%d rate=%d icount=%d hardcount=%d\n",
		    count2, count3, rate, icount, hardcount);
	   printf ("inc=%d extra_usec=%d usec_500=%d\n",
		    inc, extra_usec, usec_500);
	   printf ("tv = %d + %d\n", tv.tv_sec, tv.tv_usec);
	 }
	return (tv);	
}

xyclkprobe(reg, unit)
caddr_t reg;
int unit;
{
	struct xyclkdevice *xyclk;

	xyclk = (struct xyclkdevice *)reg;

	if (peekc (&xyclk->cntrA_data) == -1 ||
	    peekc (&xyclk->cntrA_com) == -1 ||
	    peekc (&xyclk->cntrB_data) == -1 ||
	    peekc (&xyclk->cntrB_com) == -1 ||
	    peekc (&xyclk->uic_data) == -1 ||
	    peekc (&xyclk->uic_com) == -1 ||
	    peekc (&xyclk->csr) == -1 ||
	    peekc (&xyclk->quadselA) == -1 ||
	    peekc (&xyclk->quadselB) == -1 )
	 { return (0);
	 }
	else
	 { return (sizeof (struct xyclkdevice));
	 }
}

xyclkattach(md)
struct mb_device *md;
{
	struct xyclkdevice *xyclk;
	struct xyclk_softc *xysoft;

	xyclk = (struct xyclkdevice*)(md->md_addr);
	xysoft = &xyclk_softc[md->md_unit];

	/* initialize the soft control structure */

	xysoft->open = 0;
	xysoft->enabled = 1;
	xysoft->rate = DEFAULT_RATE;
	xysoft->debug = 0;
	xysoft->icount = 0;
	xysoft->hardcount = 0;

	/* do a general rest on the device */

	COMMAND_A (RESET_CNTRS);
	COMMAND_A (LOAD & 0x1f);
	COMMAND_B (RESET_CNTRS);
	COMMAND_B (LOAD & 0x1f);
	COMMAND_I (RESET_ICTRL);

	RCI_setwallclock (readWallClock);

	return(0);
}

xyclkopen(dev,flag)
dev_t dev;
int flag;
{
	register struct xyclkdevice *xyclk;
	struct xyclk_softc *xysoft;
	struct mb_device *md;
	int unit;

	unit = minor(dev);
	md = xyclkdinfo[unit];
	if (unit < 0 || unit >= NXYCLK || !md || !md->md_alive)
	 { return (ENXIO);
	 }
	xysoft = &xyclk_softc[unit];
	xyclk = (struct xyclkdevice *)md->md_addr;

	if (!xysoft->open && xysoft->enabled)
	 { 
	   /* set up the device, and enable interrupts */

	   RCI_use_hardclock(0);
	   xysoft->open = 1;
	   xysoft->icount = 0;
	   xysoft->hardcount = 0;
	   num_xyclkintr = 0;
	   xyclkSetup (md, xysoft);
	   COMMAND_I (SELR_ARM);
	 }
	return (0);
}

xyclkclose(dev,flag)
dev_t dev;
int flag;
{
	register struct xyclkdevice *xyclk;
	struct xyclk_softc *xysoft;
	struct mb_device *md;
	int unit;

	unit = minor(dev);
	md = xyclkdinfo[unit];
	if (unit < 0 || unit >= NXYCLK || !md || !md->md_alive)
	 { return (ENXIO);
	 }
	xysoft = &xyclk_softc[unit];
	xyclk = (struct xyclkdevice *)md->md_addr;

	xysoft->open = 0;
	COMMAND_I (SELR_DISARM);

	RCI_use_hardclock(1);

	return (0);
}

xyclkioctl(dev,cmd,data,flag)
dev_t dev;
int cmd;
caddr_t data;
int flag;
{
	register struct xyclkdevice *xyclk;
	struct xyclk_softc *xysoft;
	struct mb_device *md;
	int unit;
	union pack pack;

	unit = minor(dev);
	md = xyclkdinfo[unit];
	if (unit < 0 || unit >= NXYCLK || !md || !md->md_alive)
	 { return (ENXIO);
	 }
	xysoft = &xyclk_softc[unit];
	xyclk = (struct xyclkdevice *)md->md_addr;

	switch (cmd)
	 { default:
	    { return (ENOTTY);
	      break;
	    }
	   case RCIIOC_GETCLKRATE:
	    { *(int*)data = xysoft->rate;
	      break;
	    }
	   case RCIIOC_CLKSTATUS:
	    { *(int*)data = xysoft->enabled;
	      break;
	    }
	   case RCIIOC_SETCLKRATE:
	    { int interval = *(int*)data;

	      if (interval <= 0 || (interval*500) > 0x10000)
	       { return (EINVAL);
	       }
	      xysoft->rate = interval;
	      COMMAND_A (LOAD_PTR | LOAD_REG | CNTR2);
	      WRITE_A (interval*500);
	      break;
	    }
	   case RCIIOC_CLKENABLE:
	    { if (!xysoft->enabled)
	       { xysoft->enabled = 1;
		 xysoft->open = 1;
		 xysoft->icount = 0;
		 xysoft->hardcount = 0;
		 xyclkSetup (md, xysoft);
		 COMMAND_I (SELR_ARM);
	       }
	      break;
	    }
	   case RCIIOC_CLKDISABLE:
	    { xysoft->enabled = 0;
	      COMMAND_I (SELR_DISARM);
	      break;
	    }
	   case RCIIOC_SETCLKDEBUG:
	    { xysoft->debug = *(int*)data;
	      break;
	    }
	 }
	return(0);
}

struct timeval last_xyclkintr;
struct timeval RCI_wallclock();

xyclkintr(unit)
int unit;
{
	register struct xyclk_softc *xysoft;
	register struct xyclkdevice *xyclk;
	union pack pack;
	int s;

	extern int inside_switch;

	last_xyclkintr = RCI_wallclock();

	xysoft = &xyclk_softc[unit];
	xyclk = (struct xyclkdevice *)xyclkdinfo[unit]->md_addr;

	s = splhigh();

	if (++num_xyclkintr > max_xyclkintr)
	 { xyclk->csr = 0x10;	/* disable interrupts */
	   printf ("xyclkintr interrupt saturation\n");
	   return;
	 }
	if (inside_switch)
	 { xyclk->csr = 0x10;	/* disable interrupts */
	   printf ("xyclkintr: inside RCI switch, old IPL = 0x%x\n",
		    (s & 0xf00));
	 }

	if (xysoft->open && xysoft->enabled)
	 { unsigned short count3;	
	   int inc;
	   int ticks;

	   COMMAND_A (LOAD_PTR | HOLD_REG | CNTR3);
	   COMMAND_A (SAVE | 0x4);
	   READ_A (count3);

	   if ((inc = count3 - xysoft->hardcount) < 0)
	    { inc += 0x10000;
	    }
	   ticks = (inc+1-(xysoft->hardcount%2))/2;
	   xysoft->hardcount = count3;
	   xysoft->icount += ticks;
	   RCI_devclock(ticks);
	 }
	if (xysoft->debug && ((xysoft->icount % 200) == 0))
	 { printf ("xyclk: %d\n", xysoft->icount);
	 }

	num_xyclkintr--;

	splx(s);
}

xyclkSetup(md, xysoft)
struct mb_device *md;
struct xyclk_softc *xysoft;
{
	register struct xyclkdevice *xyclk;
	union pack pack;

	xyclk = (struct xyclkdevice*)(md->md_addr);

	/* do a general rest on the device */

	COMMAND_A (RESET_CNTRS);
	COMMAND_A (LOAD & 0x1f);
	COMMAND_B (RESET_CNTRS);
	COMMAND_B (LOAD & 0x1f);
	COMMAND_I (RESET_ICTRL);

	/* Use counter group A. Set up the counters to run
	   at the initial default rate, and set them running */

	COMMAND_A (LOAD_PTR | MASTER_REG | CNTRL);
	WRITE_A (BCD_SCALER);

	/* Counter 1 is set to count down, divide by 4 */

	COMMAND_A (LOAD_PTR | MODE_REG | CNTR1);
	WRITE_A (SRC_F1 | REPEAT_CNT | OUT_TOGGLE);
	COMMAND_A (LOAD_PTR | LOAD_REG | CNTR1);
	WRITE_A (4);

	/* Counter 2 is set to count down, divide by rate*500
	   (interrupts are generated at half the counting rate) */

	COMMAND_A (LOAD_PTR | MODE_REG | CNTR2);
	WRITE_A (SRC_TCNM1 | REPEAT_CNT | OUT_TOGGLE);
	COMMAND_A (LOAD_PTR | LOAD_REG | CNTR2);
	WRITE_A (xysoft->rate*500);

	/* Counter 3 counts up, and keeps track of the number of
	   interrupts times 2 */

	COMMAND_A (LOAD_PTR | MODE_REG | CNTR3);
	WRITE_A (SRC_TCNM1 | REPEAT_CNT | UP_CNT | OUT_TOGGLE);
	COMMAND_A (LOAD_PTR | LOAD_REG | CNTR3);
	WRITE_A (0);

	COMMAND_A (LOAD_ARM | 0x7);

	/* now set up stuff for the interrupts */

	COMMAND_I (VECTOR | 0x1);
	WRITE_I (md->md_intr->v_vec);
		COMMAND_I (MODE | HIGHEDGE_INTR);
	COMMAND_I (SELW_AC);
	WRITE_I (0xff);
	COMMAND_I (CLEAR_MR | 0x1);
	xyclk->csr = VME_IPL;	/* enable IPL 4 interrupts */
}


#endif
