#include "u.h"
#include "../port/lib.h"
#include "mem.h"
#include "dat.h"
#include "fns.h"
#include "io.h"

/*
 *  8253 timer
 */
enum
{
	T0cntr=	0x40,		/* counter ports */
	T1cntr=	0x41,		/* ... */
	T2cntr=	0x42,		/* ... */
	Tmode=	0x43,		/* mode port (control word register) */
	T2ctl=	0x61,		/* counter 2 control port */

	/* commands */
	Latch0=	0x00,		/* latch counter 0's value */
	Load0l=	0x10,		/* load counter 0's lsb */
	Load0m=	0x20,		/* load counter 0's msb */
	Load0=	0x30,		/* load counter 0 with 2 bytes */

	Latch1=	0x40,		/* latch counter 1's value */
	Load1l=	0x50,		/* load counter 1's lsb */
	Load1m=	0x60,		/* load counter 1's msb */
	Load1=	0x70,		/* load counter 1 with 2 bytes */

	Latch2=	0x80,		/* latch counter 2's value */
	Load2l=	0x90,		/* load counter 2's lsb */
	Load2m=	0xa0,		/* load counter 2's msb */
	Load2=	0xb0,		/* load counter 2 with 2 bytes */

	/* 8254 read-back command: everything > pc-at has an 8254 */
	Rdback=	0xc0,		/* readback counters & status */
	Rdnstat=0x10,		/* don't read status */
	Rdncnt=	0x20,		/* don't read counter value */
	Rd0cntr=0x02,		/* read back for which counter */
	Rd1cntr=0x04,
	Rd2cntr=0x08,

	/* modes */
	ModeMsk=0xe,
	Square=	0x6,		/* periodic square wave */
	Trigger=0x0,		/* interrupt on terminal count */
	Sstrobe=0x8,		/* software triggered strobe */

	/* T2ctl bits */
	T2gate=	(1<<0),		/* enable T2 counting */
	T2spkr=	(1<<1),		/* connect T2 out to speaker */
	T2out=	(1<<5),		/* output of T2 */

	Freq=	1193182,	/* Real clock frequency */
	Tickshift=8,		/* extra accuracy */
	MaxPeriod=Freq/HZ,
	MinPeriod=Freq/(100*HZ),

	Wdogms	= 200,		/* ms between strokes */
};

typedef struct I8253 I8253;
struct I8253
{
	Lock;
	ulong	period;		/* current clock period */
	int	enabled;
	uvlong	hz;

	ushort	last;		/* last value of clock 1 */
	uvlong	ticks;		/* cumulative ticks of counter 1 */

	ulong	periodset;
};
I8253 i8253;

void
i8253init(void)
{
	int loops, x;

	ioalloc(T0cntr, 4, 0, "i8253");
	ioalloc(T2ctl, 1, 0, "i8253.cntr2ctl");

	i8253.period = Freq/HZ;

	/*
	 *  enable a 1/HZ interrupt for providing scheduling interrupts
	 */
	outb(Tmode, Load0|Square);
	outb(T0cntr, (Freq/HZ));	/* low byte */
	outb(T0cntr, (Freq/HZ)>>8);	/* high byte */

	/*
	 *  enable a longer period counter to use as a clock
	 */
	outb(Tmode, Load2|Square);
	outb(T2cntr, 0);		/* low byte */
	outb(T2cntr, 0);		/* high byte */
	x = inb(T2ctl);
	x |= T2gate;
	outb(T2ctl, x);
	
	/*
	 * Introduce a little delay to make sure the count is
	 * latched and the timer is counting down; with a fast
	 * enough processor this may not be the case.
	 * The i8254 (which this probably is) has a read-back
	 * command which can be used to make sure the counting
	 * register has been written into the counting element.
	 */
	x = (Freq/HZ);
	for(loops = 0; loops < 100000 && x >= (Freq/HZ); loops++){
		outb(Tmode, Latch0);
		x = inb(T0cntr);
		x |= inb(T0cntr)<<8;
	}
}

/*
 * if the watchdog is running and we're on cpu 0 and ignoring (clock)
 * interrupts, disable the watchdog temporarily so that the (presumed)
 * long-running loop to follow will not trigger an NMI.
 * wdogresume restarts the watchdog if wdogpause stopped it.
 */
static int
wdogpause(void)
{
	int turndogoff;

	turndogoff = watchdogon && m->machno == 0 && !islo();
	if (turndogoff) {
		watchdog->disable();
		watchdogon = 0;
	}
	return turndogoff;
}

static void
wdogresume(int resume)
{
	if (resume) {
		watchdog->enable();
		watchdogon = 1;
	}
}

void
guesscpuhz(int aalcycles)
{
	int loops, incr, x, y, dogwason;
	uvlong a, b, cpufreq;

	dogwason = wdogpause();		/* don't get NMI while busy looping */

	/* find biggest loop that doesn't wrap */
	incr = 16000000/(aalcycles*HZ*2);
	x = 2000;
	for(loops = incr; loops < 64*1024; loops += incr) {
	
		/*
		 *  measure time for the loop
		 *
		 *			MOVL	loops,CX
		 *	aaml1:	 	AAM
		 *			LOOP	aaml1
		 *
		 *  the time for the loop should be independent of external
		 *  cache and memory system since it fits in the execution
		 *  prefetch buffer.
		 *
		 */
		outb(Tmode, Latch0);
		cycles(&a);
		x = inb(T0cntr);
		x |= inb(T0cntr)<<8;
		aamloop(loops);
		outb(Tmode, Latch0);
		cycles(&b);
		y = inb(T0cntr);
		y |= inb(T0cntr)<<8;
		x -= y;
	
		if(x < 0)
			x += Freq/HZ;

		if(x > Freq/(3*HZ))
			break;
	}
	wdogresume(dogwason);

	/*
 	 *  figure out clock frequency and a loop multiplier for delay().
	 *  n.b. counter goes up by 2*Freq
	 */
	if(x == 0)
		x = 1;			/* avoid division by zero on vmware 7 */
	cpufreq = (vlong)loops*((aalcycles*2*Freq)/x);
	m->loopconst = (cpufreq/1000)/aalcycles;	/* AAM+LOOP's for 1 ms */

	if(m->havetsc && a != b){  /* a == b means virtualbox has confused us */
		/* counter goes up by 2*Freq */
		b = (b-a)<<1;
		b *= Freq;
		b /= x;

		/*
		 *  round to the nearest megahz
		 */
		m->cpumhz = (b+500000)/1000000L;
		m->cpuhz = b;
		m->cyclefreq = b;
	} else {
		/*
		 *  add in possible 0.5% error and convert to MHz
		 */
		m->cpumhz = (cpufreq + cpufreq/200)/1000000;
		m->cpuhz = cpufreq;
	}

	/* don't divide by zero in trap.c */
	if (m->cpumhz == 0)
		panic("guesscpuhz: zero m->cpumhz");
	i8253.hz = Freq<<Tickshift;
}

void
i8253timerset(uvlong next)
{
	long period;
	ulong want;
	ulong now;

	period = MaxPeriod;
	if(next != 0){
		want = next>>Tickshift;
		now = i8253.ticks;	/* assuming whomever called us just did fastticks() */

		period = want - now;
		if(period < MinPeriod)
			period = MinPeriod;
		else if(period > MaxPeriod)
			period = MaxPeriod;
	}

	/* hysteresis */
	if(i8253.period != period){
		ilock(&i8253);
		/* load new value */
		outb(Tmode, Load0|Square);
		outb(T0cntr, period);		/* low byte */
		outb(T0cntr, period >> 8);		/* high byte */

		/* remember period */
		i8253.period = period;
		i8253.periodset++;
		iunlock(&i8253);
	}
}

static void
i8253clock(Ureg* ureg, void*)
{
	timerintr(ureg, 0);
}

void
i8253enable(void)
{
	i8253.enabled = 1;
	i8253.period = Freq/HZ;
	intrenable(IrqCLOCK, i8253clock, 0, BUSUNKNOWN, "clock");
}

void
i8253link(void)
{
}

/*
 *  return the total ticks of counter 2.  We shift by
 *  8 to give timesync more wriggle room for interpretation
 *  of the frequency
 */
uvlong
i8253read(uvlong *hz)
{
	ushort y, x;
	uvlong ticks;

	if(hz)
		*hz = i8253.hz;

	ilock(&i8253);
	outb(Tmode, Latch2);
	y = inb(T2cntr);
	y |= inb(T2cntr)<<8;

	if(y < i8253.last)
		x = i8253.last - y;
	else {
		x = i8253.last + (0x10000 - y);
		if (x > 3*MaxPeriod) {
			outb(Tmode, Load2|Square);
			outb(T2cntr, 0);		/* low byte */
			outb(T2cntr, 0);		/* high byte */
			y = 0xFFFF;
			x = i8253.period;
		}
	}
	i8253.last = y;
	i8253.ticks += x>>1;
	ticks = i8253.ticks;
	iunlock(&i8253);

	return ticks<<Tickshift;
}

void
delay(int millisecs)
{
	if (millisecs > 10*1000)
		iprint("delay(%d) from %#p\n", millisecs,
			getcallerpc(&millisecs));
	if (watchdogon && m->machno == 0 && !islo())
		for (; millisecs > Wdogms; millisecs -= Wdogms) {
			delay(Wdogms);
			watchdog->restart();
		}
	millisecs *= m->loopconst;
	if(millisecs <= 0)
		millisecs = 1;
	aamloop(millisecs);
}

void
microdelay(int microsecs)
{
	if (watchdogon && m->machno == 0 && !islo())
		for (; microsecs > Wdogms*1000; microsecs -= Wdogms*1000) {
			delay(Wdogms);
			watchdog->restart();
		}
	microsecs *= m->loopconst;
	microsecs /= 1000;
	if(microsecs <= 0)
		microsecs = 1;
	aamloop(microsecs);
}

/*  
 *  performance measurement ticks.  must be low overhead.
 *  doesn't have to count over a second.
 */
ulong
perfticks(void)
{
	uvlong x;

	if(m->havetsc)
		cycles(&x);
	else
		x = 0;
	return x;
}
