/* $XConsortium: CirrusClk.c,v 1.4 95/01/27 14:46:39 kaleb Exp $ */
/* $XFree86: xc/programs/Xserver/hw/xfree86/common_hw/CirrusClk.c,v 3.4 1995/01/15 10:33:34 dawes Exp $ */

/*
 * Programming of the built-in Cirrus clock generator.
 * Harm Hanemaayer <hhanemaa@cs.ruu.nl>
 */
 
#include "compiler.h"
#include "misc.h"
#define NO_OSLIB_PROTOTYPES
#include "xf86_OSlib.h"

/* CLOCK_FACTOR is double the osc freq in kHz (osc = 14.31818 MHz) */
#define CLOCK_FACTOR 28636

/* clock in kHz is (numer * CLOCK_FACTOR / (denom & 0x3E)) >> (denom & 1) */
#define CLOCKVAL(n, d) \
     ((((n) & 0x7F) * CLOCK_FACTOR / ((d) & 0x3E)) >> ((d) & 1))

#define NU_FIXED_CLOCKS 19

typedef struct {
  unsigned char numer;
  unsigned char denom;
} cirrusClockRec;

static cirrusClockRec cirrusClockTab[] = {
  { 0x2C, 0x33 },		/* 12.599 */
  { 0x4A, 0x2B },		/* 25.227 */
  { 0x5B, 0x2F },		/* 28.325 */
  { 0x45, 0x30 }, 		/* 41.164 */
  { 0x7E, 0x33 },		/* 36.082 */
  { 0x42, 0x1F },		/* 31.500 */
  { 0x51, 0x3A },		/* 39.992 */
  { 0x55, 0x36 },		/* 45.076 */
  { 0x65, 0x3A },		/* 49.867 */
  { 0x76, 0x34 },		/* 64.983 */
  { 0x7E, 0x32 },		/* 72.163 */
  { 0x6E, 0x2A },		/* 75.000 */
  { 0x5F, 0x22 },		/* 80.013 */
  { 0x7D, 0x2A },		/* 85.226 */
  { 0x58, 0x1C },		/* 89.998 */
  { 0x49, 0x16 },		/* 95.019 */
  { 0x46, 0x14 },		/* 100.226 */
  { 0x53, 0x16 },		/* 108.035 */
  { 0x5C, 0x18 },		/* 110.248 */
};


/*
 * This function returns the 7-bit numerator and 6-bit denominator/post-scalar
 * value that corresponds to the closest clock found. If the MCLK is very close
 * to the requested frequency, it sets a flag so that the MCLK can be used
 * as VCLK on chips that support it.
 * If a frequency close to one of the tested clock values is found,
 * use the tested clock since others can be unstable.
 */

int CirrusFindClock(freq, num_out, den_out, usemclk_out)
	int freq;
	int *num_out;
	int *den_out;
	int *usemclk_out;
{
	int n, i;
	int num, den;
	int ffreq, mindiff;
	int mclk;

	/* Prefer a tested value if it matches within 0.1%. */
	for (i = 0; i < NU_FIXED_CLOCKS; i++) {
		int diff;
		diff = abs(CLOCKVAL(cirrusClockTab[i].numer, 
		          cirrusClockTab[i].denom) - freq);
		if (diff < freq / 1000) {
			num = cirrusClockTab[i].numer;
			den = cirrusClockTab[i].denom;
			goto foundclock;
		}
	}

	mindiff = freq; 
	for (n = 0x10; n < 0x7f; n++) {
		int d;
		for (d = 0x14; d < 0x3f; d++) {
			int c, diff;
			/* Avoid combinations that can be unstable. */
			if (n < d && (d & 1) == 0)
				continue;
			c = CLOCKVAL(n, d);
			diff = abs(c - freq);
			if (diff < mindiff) {
				mindiff = diff;
				num = n;
				den = d;
				ffreq = c;
			}
		}
	}

foundclock:
	*num_out = num;
	*den_out = den;

	/* Calculate the MCLK. */
	outb(0x3c4, 0x0f);
	mclk = 14318 * (inb(0x3c5) & 0x3f) / 8;
	/*
	 * Favour MCLK as VLCK if it matches as good as the found clock,
	 * or if it is within 0.2 MHz of the request clock. A VCLK close
	 * to MCLK can cause instability.
	 */
	if (abs(mclk - freq) <= mindiff + 10 || abs(mclk - freq) <= 200)
		*usemclk_out = TRUE;
	else
		*usemclk_out = FALSE;

	return 0;
}


int CirrusSetClock(freq)
	int freq;
{
	int num, den, usemclk;
	unsigned char tmp;

	CirrusFindClock(freq, &num, &den, &usemclk);

	/*
	 * The 'Use MCLK as VCLK' flag is ignored.
	 * This function is only called during start-up if the
	 * "probe_clocks" option is specified.
	 */

	/* Set VLCK3. */
	outb(0x3c4, 0x0e);
	tmp = inb(0x3c5);
	outb(0x3c5, (tmp & 0x80) | num);
	outb(0x3c4, 0x1e);
	tmp = inb(0x3c5);
	outb(0x3c5, (tmp & 0xc0) | den);
	return 0;
}
