/*
 * This file contains the implementation of a more precise time than that
 * provided by DOS.  Routines are provided to increase the clock rate to
 * around 1165 interrupts per second, for a granularity of close to 858
 * microseconds between clock pulses, rather than the 55 milliseconds between
 * normal PC clock pulses (18.2 times/second).
 *
 * Note that the timer_init() routine must be called before the timer_read()
 * routines will work, and that the timer_stop() routine MUST be called
 * before the program terminates, or the machine will be toasted. For this
 * reason, timer_init() installs the timer_stop() routine to be called at
 * program exit.
 */

#include <stdlib.h>
#include <dos.h>
#include <conio.h> /* outp() */
#include "timer.h" /* include self for control */

/* selects timer's resolution */
#define TIMER_1165_HZ
/*#define TIMER_582_HZ*/
/*#define TIMER_291_HZ*/

/* 1165.215 hz */
#ifdef TIMER_1165_HZ
#define DIV_MSB   0x04 /* a divisor of 1024 gives: */
#define DIV_LSB   0x00 /* 1193180 / 1024 = 1165 Hz */
#define ORIG_INTR_MASK 63 /* call the original clock INT every 64 calls */
#define USEC_INC  858 /* one cycle is 858.21us to be exact */
#define USEC_COMP 13  /* how many us to compensate for every ORIG_INTR_MASK+1 call */
#endif

/* 582.607 hz */
#ifdef TIMER_582_HZ
#define DIV_MSB   0x08
#define DIV_LSB   0x00
#define ORIG_INTR_MASK 31
#define USEC_INC  1716 /* 1716.42 to be exact */
#define USEC_COMP 13   /* how many us to compensate for every ORIG_INTR_MASK+1 call */
#endif

/* 291.304 hz */
#ifdef TIMER_291_HZ
#define DIV_MSB   0x10
#define DIV_LSB   0x00
#define ORIG_INTR_MASK 15
#define USEC_INC  3433 /* 3432.84 to be exact */
#define USEC_COMP -2
#endif


#define CLOCK_INT 0x08

/* redefine a few functions, as needed by OpenWatcom */
#define setvect _dos_setvect
#define getvect _dos_getvect
#define disable _disable
#define enable _enable

static unsigned long nowtime = 0;        /* current time counter */
static void interrupt (*oldfunc)(void);  /* interrupt function pointer */


/* This routine will handle the clock interrupt at its higher rate. It will
 * call the DOS handler every ORIG_INTR_MASK times it is called, to maintain
 * the 18.2 times per second that DOS needs to be called. Each time through,
 * it adds to the nowtime value.
 * When it is not calling the DOS handler, this routine must reset the 8259A
 * interrupt controller before returning. */
static void interrupt handle_clock(void) {
  static int callmod = 0;

  /* increment the time */
  nowtime += USEC_INC;

  /* increment the callmod */
  callmod++;
  callmod &= ORIG_INTR_MASK;

  /* if this is the 64th call, then call handler */
  if (callmod == 0) {
    nowtime += USEC_COMP; /* compensate for integer division inaccuracy */
    _chain_intr(oldfunc);
  } else {  /* otherwise, clear the interrupt controller */
    outp(0x20, 0x20);  /* end of interrupt */
  }
}


/* reset the timer value, this can be used by the application to make sure
 * no timer wrap occurs during critical parts of the code flow */
void timer_reset(void) {
  nowtime = 0;
}


/* This routine will stop the timer. It has void return value so that it
 * can be an exit procedure. */
void timer_stop(void) {
  /* Disable interrupts */
  disable();

  /* Reinstate the old interrupt handler */
  setvect(CLOCK_INT, oldfunc);

  /* Reinstate the clock rate to standard 18.2 Hz */
  outp(0x43, 0x36);       /* Set up for count to be sent          */
  outp(0x40, 0x00);       /* LSB = 00  \_together make 65536 (0)  */
  outp(0x40, 0x00);       /* MSB = 00  /                          */

  /* Enable interrupts */
  enable();
}


/* This routine will start the fast clock rate by installing the handle_clock
 * routine as the interrupt service routine for the clock interrupt and then
 * setting the interrupt rate up to its higher speed by programming the 8253
 * timer chip. */
void timer_init(void) {
  /* Store the old interrupt handler */
  oldfunc = getvect(CLOCK_INT);

  /* Set the nowtime to zero */
  nowtime = 0;

  /* Disable interrupts */
  disable();

  /* Install the new interrupt handler */
  setvect(CLOCK_INT, handle_clock);

  /* Increase the clock rate */
  outp(0x43, 0x36);     /* Set up for count to be sent            */
  outp(0x40, DIV_LSB);  /* LSB = 00  \_together make 2^10 = 1024  */
  outp(0x40, DIV_MSB);  /* MSB = 04  /                            */

  /* Enable interrupts */
  enable();

  /* Install the timer_stop() routine to be called at exit */
  atexit(timer_stop);

}


/* This routine will return the present value of the time, as a number of
 * microseconds.  Interrupts are disabled during this time to prevent the
 * clock from changing while it is being read. */
void timer_read(unsigned long *res) {
  /* Disable interrupts */
  disable();

  /* Read the time */
  *res = nowtime;

  /* Enable interrupts */
  enable();
}
