/* a module to convert a US NTSC based TiVo to PAL input and output

   tridge@samba.org, 2000
   
   This version is for a US based Standalone Tivo (Philips HDR xxx) 
   running V3.0 SW
   modified by STS for 3.0 SW
   many clues and changes based on the work done by d18c7db

   Updated by Keith Wilkinson June 2003 to allow selection between the S-Video and Composite Inputs
   and adjustment of the colour saturation, brightness and contrast. Also included Warren Toomey's
   code to adjust the image width.
   
   Updated by Keith Wilkinson November 2003 to condense all the different tuner types/frequencies 
   into a single file. Tuner type is selectable via command line parameter "tunertype"
   
   Updated by Keith Wilkinson March 2004 to allow fine tuning of the tuner frequencies. Frequencies
   are modified by using the "finetune" parameter.

   Updated by Robert Lowery April 2004 to enable RF Output with Samsung/ALPS tuners
   
   Updated by Keith Wilkinson September 2004 to disable the adaptive comb filter when using the S-Video input.
   Added new parameter (luminanceDelay) to allow adjustment of the luminance delay.
   
   Updated by Keith Wilkinson November 2004 to renable the adaptive comb filter when using the S-Video input
   and enable the S-Video processing delay feature (bit 5) of the LUMINANCE CONTROL register.
*/

#define DEBUG	0		/* turn on debug prints/statements */

#include <linux/config.h>
#include <linux/module.h>
#include <linux/fs.h>
#include "tuner_chans.h"

#define VERSION					"3.0.02"
#define MAX_CHAN				69

// I2C Bus Addresses
#define TUNER_INPUT				0xC2
#define TUNER_OUTPUT			0xCA
#define VIDEO_DECODER			0x42
#define VIDEO_ENCODER			0x8C
#define MSP_CHIP				0x88

// SAA7114 Registers
#define ANALOG_INPUT_CONTROL_1			0x02
#define HORIZONTAL_SYNC_START			0x06
#define LUMINANCE_CONTROL				0x09
#define LUMINANCE_BRIGHTNESS_CONTROL	0x0A
#define LUMINANCE_CONTRAST_CONTROL		0x0B
#define CHROMINANCE_SATURATION_CONTROL	0x0C
#define CHROMINANCE_CONTROL_1			0x0E
#define CHROMINANCE_CONTROL_2			0x10
#define MODE_DELAY_CONTROL_1			0x11


// SAA7120 Registers
#define FIRST_ACTIVE_LINE	0x7A
#define LAST_ACTIVE_LINE	0x7B

// MSP Chip Registers
#define ACB_REGISTER				0x00130000
#define SRC_SELECT_OUTPUT_REGISTER	0x000B0000
#define SCART_PRESCALE_REGISTER		0x000D0000
#define MODUS_REGISTER				0x00300000

// MSP Chip Subaddresses
#define SUBADDRESS_10H		0x10
#define SUBADDRESS_12H		0x12

// MSP Chip SCART Input values
#define SCART_1		0x0000	
#define SCART_2		0x0200
#define SCART_3		0x0300
#define SCART_4		0x0001

/* offsets into modules
These offsets have to be reajusted for every new version of the Tivo SW
It took me a while to find out, what Tridge did, so here is a recipe: 
run readelf -r <module>.o 
search for the function name column "Symbol's Name" 
unfortunately V3.0 SW does not export most of these names any more,
so this is a comparison of addresses and guess, which .text could be the right one.
I use the syntax:
_call_	if this function is called in the module
_export_	if this function is exported by the module
*/

/* offsets into fpga7114.o */
#define offset_call_fpga_hw_ioctl		0x298c
#define offset_call_MSPInit				0x64B8
#define offset_call_fpga_get_channel	0x3b44
#define offset_export_fpga_hw_ioctl		0x4C
#define offset_BangRegs					0x3B50
//#define offset_standin_tune           0x0000  // not yet found in 3.0
//#define offset_getFreq_C2             0x0000  // not yet found in 3.0
//#define offset_getFreq_C6             0x0000  // not yet found in 3.0

static u32 base_fpga7114 = 0;

#define SVIDEO_INPUT		-2
#define COMPOSITE_INPUT		-1

#define AUS_PHILIPS	0
#define AUS_SAMSUNG	1
#define NZ			2
#define CCIR		3
#define CCIR_CATV	4

// reference to functions from the I2C module
extern int I2CRead(int module, int subaddress, unsigned char *data, int len);
extern int I2CReadNoSub(int module, unsigned char *data, int len);
extern int I2CWrite(int module, int subaddress, unsigned char *data, int len);
extern int I2CWriteNoSub(int module, unsigned char *data, int len);

// default values for the command line parameters
static u32 screenwidth = 544;			// width of the screen image in pixels
static u8 saturation = 0x65;			// tuner input colour saturation level
static u8 brightness = 0x7d;			// tuner brightness level
static u8 contrast = 0x43;				// tuner contrast level
static u8 avsaturation = 0x65;			// A/V input colour saturation level
static u8 avbrightness = 0x7d;			// A/V input brightness level
static u8 avcontrast = 0x43;			// A/V input contrast level
static u8 dualaudioinput = 0;			// use the SCART 2 input for the S-Video signal
static u32 tunertype = AUS_SAMSUNG;		// default tuner type is ALPS/Samsung
static u8 horizontalshift = 0xC3;		// sets the "synch start" value which controls the horizontal position
static char *finetune = "";				// fine tuning parameters
static u8 rfoutput = 60;				// default RF output channel
static signed char luminanceDelay = 0;	// luminance delay compensation value

MODULE_PARM(screenwidth, "i");
MODULE_PARM(saturation, "b");
MODULE_PARM(brightness, "b");
MODULE_PARM(contrast, "b");
MODULE_PARM(avsaturation, "b");
MODULE_PARM(avbrightness, "b");
MODULE_PARM(avcontrast, "b");
MODULE_PARM(dualaudioinput, "b");
MODULE_PARM(tunertype, "i");
MODULE_PARM(horizontalshift, "b");
MODULE_PARM(finetune, "s");
MODULE_PARM(rfoutput, "b");
MODULE_PARM(luminanceDelay, "i");

MODULE_PARM_DESC(screenwidth, "Width of the image in pixels; try 480, 544 or 720");
MODULE_PARM_DESC(saturation, "Colour saturation, from 0 to 127");
MODULE_PARM_DESC(brightness, "Brightness, from 0 to 255");
MODULE_PARM_DESC(contrast, "Contrast, from 0 to 127");
MODULE_PARM_DESC(avsaturation, "Colour saturation for A/V input, from 0 to 127");
MODULE_PARM_DESC(avbrightness, "A/V input brightness, from 0 to 255");
MODULE_PARM_DESC(avcontrast, "A/V input contrast, from 0 to 127");
MODULE_PARM_DESC(dualaudioinput, "Use the SCART 2 input for the S-Video signal");
MODULE_PARM_DESC(tunertype, "Tuner type: (AUS)Philips = 0, (AUS)Samsung = 1, NZ = 2, CCIR = 3, CCIR_CATV = 4");
MODULE_PARM_DESC(horizontalshift, "Horizontal Shift, 0 to 108 and 128 to 236. default = 195");
MODULE_PARM_DESC(finetune, "Tuner frequency fine tuning. channel:frequency,channel:frequency,...");
MODULE_PARM_DESC(rfoutput, "RF Output Channel (Samsung / ALPS), from 28 to 69");
MODULE_PARM_DESC(luminanceDelay, "Luminance Delay Control, from -4 to +3");

/* useful to know what channel we are on */
static int channel = COMPOSITE_INPUT;

static u32 bypass_offset (u32 addr);

/* we should be using frequencyAdjustmentG but it doesn't seem to work!? */
static int adjust;

// local functions
u32 atol(char *str);
void FineTuneFrequencies(void);
void SetRFOutput(void);

///////////////////////////////////////////////////////////////
static int iicw4(int module, int subaddress, u32 v)
{
	return I2CWrite(module, subaddress, (char *) &v, 4);
}

///////////////////////////////////////////////////////////////
static int iicw1(int module, int subaddress, unsigned char v)
{
	return I2CWrite(module, subaddress, (char *) &v, 1);
}

///////////////////////////////////////////////////////////////
static unsigned char iicr1(int module, int subaddress)
{
	unsigned char v;
	I2CRead(module, subaddress, &v, 1);
	return v;
}

///////////////////////////////////////////////////////////////
static void cache_flush(u32 addr)
{
__asm("dcbf 0,%0\n\t" "sync \n\t" "icbi 0,%0 \n\t" "isync \n\t": :"r" (addr));
}

///////////////////////////////////////////////////////////////
// a hook for the Tmdh2Tune fn.
// We need to tune to channel `chan' with fine-tune offset
// frequencyAdjustmentG.
//
static int Tmdh2Tune_x(int chan, int sigtype)
{
	int i, f;
	static int last_channel = -1;
  
#if	DEBUG
  printk ("PALMOD: Tmdh2Tune_x channel=%d\n", channel);
#endif
	if (last_channel != chan) {
		adjust = 0;
	}

	if (tunertype == AUS_PHILIPS) {
		for (i = 0; aus_philips_tv_chans[i].freq != 0; i++)
			if (chan == aus_philips_tv_chans[i].chan)
				break;
		if (aus_philips_tv_chans[i].freq == 0)
			return -1;
		f = (aus_philips_tv_chans[i].freq + 38900) + adjust * 125 / 4;
		f = ((f * 16 + 500) / 1000) << 16 | 0xce00;
	
		if (aus_philips_tv_chans[i].freq < 170000)
			f |= 0xa0;
		else if (aus_philips_tv_chans[i].freq < 450000)
			f |= 0x90;
		else
			f |= 0x30;
	}

	else if (tunertype == AUS_SAMSUNG) {
		for (i = 0; aus_samsung_tv_chans[i].freq != 0; i++)
			if (chan == aus_samsung_tv_chans[i].chan)
				break;
		if (aus_samsung_tv_chans[i].freq == 0)
			return -1;
		f = (aus_samsung_tv_chans[i].freq + 38900) + adjust * 125 / 4;
		f = ((f * 16 + 500) / 1000) << 16 | 0x8e00;

		if (aus_samsung_tv_chans[i].freq < 170000)
			f |= 0x01;
		else if (aus_samsung_tv_chans[i].freq < 450000)
			f |= 0x02;
		else
			f |= 0x08;
	}

	else if (tunertype == NZ) {
		for (i = 0; nz_tv_chans[i].freq != 0; i++)
			if (chan == nz_tv_chans[i].chan)
				break;
		if (nz_tv_chans[i].freq == 0)
			return -1;
		f = (nz_tv_chans[i].freq + 38900) + adjust * 125 / 4;
		f = ((f * 16 + 500) / 1000) << 16 | 0xce00;

		if (nz_tv_chans[i].freq < 170000)
			f |= 0xa0;
		else if (nz_tv_chans[i].freq < 450000)
			f |= 0x90;
		else
			f |= 0x30;
	}

	else if (tunertype == CCIR) {
		for (i = 0; ccir_tv_chans[i].freq != 0; i++)
			if (chan == ccir_tv_chans[i].chan)
				break;
		if (ccir_tv_chans[i].freq == 0)
			return -1;
		f = (ccir_tv_chans[i].freq + 38900) + adjust * 125 / 4;
		f = ((f * 16 + 500) / 1000) << 16 | 0xce00;

		if (ccir_tv_chans[i].freq < 170000)
			f |= 0xa0;
		else if (ccir_tv_chans[i].freq < 450000)
			f |= 0x90;
		else
			f |= 0x30;
	}

	else if (tunertype == CCIR_CATV) {
		for (i = 0; ccir_catv_tv_chans[i].freq != 0; i++)
		if (chan == ccir_catv_tv_chans[i].chan)
			break;
		if (ccir_catv_tv_chans[i].freq == 0)
			return -1;
		f = (ccir_catv_tv_chans[i].freq + 38900) + adjust * 125 / 4;
		f = ((f * 16 + 500) / 1000) << 16 | 0xce00;

		if (ccir_catv_tv_chans[i].freq < 170000)
			f |= 0xa0;
		else if (ccir_catv_tv_chans[i].freq < 450000)
			f |= 0x90;
		else
			f |= 0x30;
	}

	I2CWriteNoSub(TUNER_INPUT, (u8 *)&f, 4);							// set tuner frequency
	iicw1(VIDEO_DECODER, ANALOG_INPUT_CONTROL_1, 0x80);					// analog amplifier on
	iicw1(VIDEO_DECODER, LUMINANCE_BRIGHTNESS_CONTROL, brightness);		// brightness adjustment
	iicw1(VIDEO_DECODER, LUMINANCE_CONTRAST_CONTROL, contrast);			// contrast adjustment
	iicw1(VIDEO_DECODER, CHROMINANCE_SATURATION_CONTROL, saturation);	// saturation adjustment

#if DEBUG
	printk("PALMOD: tuned to 0x%08x for chan %d adj=%d\n",
		(unsigned) f, chan, adjust);
#endif
	last_channel = chan;
	return 0;
}

///////////////////////////////////////////////////////////////
static int fpga_hw_ioctl_x(u32 a1, u32 a2, u32 a3, u32 a4)
{
	int (*fpga_hw_ioctl)(u32 a1, u32 a2, u32 a3, u32 a4) =
			bypass_offset(offset_call_fpga_hw_ioctl);
	int ret;
	
#if DEBUG
	printk ("fpga_hw_ioctl: a1=%x a2=%x a3=%x a4=%x\n", a1, a2, a3, a4);
#endif

	if (a3 == 0x8000) {
		u32 *p = (u32 *)a4;
		channel = p[1];
		
#if DEBUG
	printk("PALMOD: 8000: %x %x %x %x\n", p[0], p[1], p[2], p[3]);
	printk("PALMOD: channel=%d\n", channel);
#endif

		// If the source is an A/V input, p[1] will contain either -1 (Composite) or -2 (S-Video).
		if (channel != SVIDEO_INPUT)
			p[1] = COMPOSITE_INPUT;
		p[3] = screenwidth;
    }
    
	ret = fpga_hw_ioctl(a1, a2, a3, a4);

	if (channel >= 0)
      Tmdh2Tune_x(channel, 0);
	
	if (channel < 0) {
		// set saturation, bightness and contrast adjustment on A/V inputs.
		iicw1(VIDEO_DECODER, LUMINANCE_BRIGHTNESS_CONTROL, avbrightness);
		iicw1(VIDEO_DECODER, LUMINANCE_CONTRAST_CONTROL, avcontrast);
		iicw1(VIDEO_DECODER, CHROMINANCE_SATURATION_CONTROL, avsaturation);

		// s-video input?
		if (channel == -2) {
			// set s-video processing delay (stops chrominance signal shift)
			iicw1(VIDEO_DECODER, LUMINANCE_CONTROL, 0xA0);
			
			// apply user adjustment to the luminance delay
			iicw1(VIDEO_DECODER, MODE_DELAY_CONTROL_1, (u8)luminanceDelay);
		}
	}
	
	if (screenwidth == 720)
		iicw1(VIDEO_DECODER, HORIZONTAL_SYNC_START, horizontalshift);	// adjust horizontal screen position
		
	return ret;
}

///////////////////////////////////////////////////////////////
// a hook for the MSPInit function
//
static void MSPInit_x(int x)
{
#if DEBUG
	printk("MSP channel=%d\n", channel);
#endif

	// This gets the vertical sizing right on the screen. Not
	// really part of the msp init, but this is an easy place to
	// put it
	iicw1(VIDEO_ENCODER, FIRST_ACTIVE_LINE, 0x05);
	iicw1(VIDEO_ENCODER, LAST_ACTIVE_LINE, 0x35);

	if ((channel == -1) || (channel == -2)) {	// Composite or S-Video source?
		void (*MSPInit)(int) = bypass_offset(offset_call_MSPInit);
		MSPInit(0);
		// this directs the correct SCART input to be used for audio input 
		// when not using the tuner.
		// channel == -1 is composite Video, channel == -2 is S-Video
		if ((channel == -1))
			// select SCART 3 for the audio input
			iicw4(MSP_CHIP, SUBADDRESS_12H, ACB_REGISTER | SCART_3);
		else if (dualaudioinput)
			// select SCART 2 for the audio input
			iicw4(MSP_CHIP, SUBADDRESS_12H, ACB_REGISTER | SCART_2);
		else
			// select SCART 3 for the audio input
			iicw4(MSP_CHIP, SUBADDRESS_12H, ACB_REGISTER | SCART_3);
		return;
	}
	// SETUP FOR TUNER INPUT
	iicw4(MSP_CHIP, SUBADDRESS_10H, MODUS_REGISTER | 0x0060);
	iicw4(MSP_CHIP, SUBADDRESS_12H, SRC_SELECT_OUTPUT_REGISTER | 0x0220);
	iicw4(MSP_CHIP, SUBADDRESS_12H, ACB_REGISTER | SCART_1);		// select SCART 1 for the audio input
	iicw4(MSP_CHIP, SUBADDRESS_12H, SCART_PRESCALE_REGISTER | 0x1900);
}

///////////////////////////////////////////////////////////////
static u32 bypass_offset(u32 addr)
{
	extern int fpga_get_channel(void);

#if DEBUG
	printk("&fpga_get_channel= %d\n", channel);
#endif

	return addr + (((u32) fpga_get_channel) - offset_call_fpga_get_channel);
}

///////////////////////////////////////////////////////////////
static void install_bypass_fn(u32 newfn, u32 call)
{
	u32 old;
	call = bypass_offset(call);
	old = *(u32 *) call;
	*(u32 *) call = (0x48000001) | ((newfn - call) & 0xFFFFFF);
	cache_flush(call);
}

///////////////////////////////////////////////////////////////
int init_module(void)
{
	int n;
	char *version = VERSION;
	install_bypass_fn((u32) fpga_hw_ioctl_x, offset_export_fpga_hw_ioctl);

	{
	u32 addrs[] = { 0x1be8, 0x1c00, 0x1c18, 0x1c34, 0x222c,
					0x2244, 0x225c, 0x2288, 0x712c, 0};
    int i;
	for (i = 0; addrs[i]; i++)
		install_bypass_fn((u32)MSPInit_x, addrs[i]);
	}

	// make sure the parameter values are within range
	if (saturation > 127)
		saturation = 127;
	if (contrast > 127)
		contrast = 127;
	if (brightness > 255)
		brightness = 255;
	if (avsaturation > 127)
		avsaturation = 127;
	if (avcontrast > 127)
		avcontrast = 127;
	if (avbrightness > 255)
		avbrightness = 255;
    if (screenwidth != 480 && screenwidth != 544 && screenwidth != 720)
    	screenwidth = 544;
    if (luminanceDelay > 3)
    	luminanceDelay = 3;
    if (luminanceDelay < -4)
    	luminanceDelay = -4;
    
    printk("PALMOD: Luminance delay=%d\n", luminanceDelay);
    	
    // luminance delay is a 3-bit value
    if (luminanceDelay < 0) {
   		n = 4;	// set high order bit
   		n += (4 + luminanceDelay);
   		luminanceDelay = n;
    }
    
	if (*finetune != 0)	// check for finetune parameter
		FineTuneFrequencies();
    
	if (dualaudioinput == 1)
		printk("PALMOD: Dual Audio Input Hack enabled\n");

	printk("PALMOD using tuner type %d\n", tunertype);
	printk("PALMOD: AV screenwidth=%d\n", screenwidth);
	printk("PALMOD: AV horizontalshift=%d\n", horizontalshift);
	printk("PALMOD: AV saturation=%d\n", avsaturation);
	printk("PALMOD: AV brightness=%d\n", avbrightness);
	printk("PALMOD: AV contrast=%d\n", avcontrast);
	printk("PALMOD: Tuner saturation=%d\n", saturation);
	printk("PALMOD: Tuner brightness=%d\n", brightness);
	printk("PALMOD: Tuner contrast=%d\n", contrast);
  
	SetRFOutput();
	printk("PALMOD Version %s loaded successfully\n", version);
	return 0;
}

///////////////////////////////////////////////////////////////
void cleanup_module(void)
{
	printk("palmod removed!\n");
}

///////////////////////////////////////////////////////////////
void SetRFOutput(void)
{
	int i;
	int rfo;
	
	if (tunertype == AUS_SAMSUNG) {
		if (rfoutput < 28 || rfoutput > 69)
			rfoutput = 60;

	    for (i = 0; aus_samsung_tv_chans[i].freq != 0; i++) {
			if (rfoutput == aus_samsung_tv_chans[i].chan) {
				rfo = 0x8a480004;
				rfo |= ((aus_samsung_tv_chans[i].freq - 250) / 1000) << 4;
				I2CWriteNoSub(TUNER_OUTPUT, (u8 *)&rfo, 4);
				printk("PALMOD: RF Ouput channel=%d\n", rfoutput);
				break;
			}
	    }
	}
}

///////////////////////////////////////////////////////////////
void FineTuneFrequencies(void)

{
	char *start_ptr;
	char *end_ptr;
	char temp[10];
	int chan;
	int i, count = 0;
	u32 freq;
	
	printk("finetune = %s\n", finetune);
	start_ptr = end_ptr = finetune;								// initialise pointers
	
	while (*start_ptr) {										// loop until entire string is processed
		
		// extract the channel number
		count = 0;												// reset character count
		while (*end_ptr != ':' && *end_ptr && count < 2) {		// extract up to 2 characters
			++end_ptr;											// increment pointer
			++count;											// increment count
		}
		
		if (*end_ptr == 0)										// make sure we found something
			break;
			
		memset(temp, 0, 10);									// initialise buffer
		memcpy(temp, start_ptr, end_ptr - start_ptr);			// copy the text
		chan = (int)atol(temp);									// convert it to a number
		
		if (chan < 0 || chan > MAX_CHAN)						// check for valid channel
			break;
		
		// extract the frequency
		++end_ptr;												// point to start of frequency text
		start_ptr = end_ptr;									// initialise start pointer
		count = 0;												// reset character count
		while (*end_ptr != ',' && *end_ptr && count < 6) {		// extract up to 6 characters
			++end_ptr;											// increment pointer
			++count;											// increment count
		}
		
		memset(temp, 0, 10);									// initialise buffer
		memcpy(temp, start_ptr, end_ptr - start_ptr);			// copy the text
		freq = atol(temp);										// convert it to a number
		
		if (freq == 0)											// check for valid frequency
			break;
			
		printk("Channel: %d, Frequency: %d\n", chan, freq);		// put an entry in the kernel log
		
		// modify the appropriate table with the new frequency
		if (tunertype == AUS_PHILIPS) {
			for (i = 0; aus_philips_tv_chans[i].freq != 0; i++) {
				if (chan == aus_philips_tv_chans[i].chan) {
					aus_philips_tv_chans[i].freq = freq;
  					break;
  				}
  			}
  		}
  	
  		else if (tunertype == AUS_SAMSUNG) {
			for (i = 0; aus_samsung_tv_chans[i].freq != 0; i++)
				if (chan == aus_samsung_tv_chans[i].chan) {
					aus_samsung_tv_chans[i].freq = freq;
  					break;
  				}
  		}
  	
		else if (tunertype == NZ) {
			for (i = 0; nz_tv_chans[i].freq != 0; i++)
				if (chan == nz_tv_chans[i].chan) {
					nz_tv_chans[i].freq = freq;
  					break;
  				}
  		}
		else if (tunertype == CCIR) {
			for (i = 0; ccir_tv_chans[i].freq != 0; i++)
				if (chan == ccir_tv_chans[i].chan) {
					ccir_tv_chans[i].freq = freq;
  					break;
  				}
  		}
		else if (tunertype == CCIR_CATV) {
			for (i = 0; ccir_catv_tv_chans[i].freq != 0; i++)
				if (chan == ccir_catv_tv_chans[i].chan) {
					ccir_catv_tv_chans[i].freq = freq;
  					break;
  				}
  		}
  	
		if (*end_ptr) {											// check for end of list
			++end_ptr;
			start_ptr = end_ptr;
		}
		else
			break;
	}
}

///////////////////////////////////////////////////////////////
// atol: convert ascii string to an unsigned 32-bit value
//
u32 atol(char *str)
{
	int i;
	u32 n;
 
	n = 0;
	for(i = 0; str[i] >= '0' && str[i] <= '9'; ++i)
		n = 10 * n + (str[i] - '0');
	return n;
}

