#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/delay.h>
#include <bspchip.h>
#include "rtk_serdes.h"

#define CHECK_INTERVAL	1000	/*1000 msecs*/
unsigned int DEBUG_LEVEL = 0;

struct timer_list timer_serdes_link;
static void check_serdes_status(void);


static int rtk_serdes_nway_status(void)
{
	unsigned int isNwayOn = 0;

	DBG_PRK("Check SERDES NWAY status\n");
	
	REG32(SYSREG_SDS_TOP1) = 0x08020000; mdelay(10);
	REG32(SYSREG_SDS_TOP0) = 0xc0020000; mdelay(10);
	mb();
	REG32(SYSREG_SDS_TOP0) = 0xd0020000; mdelay(10);
	mb();
	REG32(SYSREG_SDS_TOP0) = 0xc0020000; mdelay(10);
	isNwayOn = (REG32(SYSREG_SDS_TOP1) >> 8)& 0xf;	

	return isNwayOn;
}
static void rtk_serdes_disable_nway(void)	
{
	DBG_PRK("Disable SERDES NWAY\n");

	REG32(SYSREG_SDS_TOP1) = 0x080271e0; mdelay(10);
	REG32(SYSREG_SDS_TOP0) = 0xc0020001; mdelay(10);
	mb();	
	REG32(SYSREG_SDS_TOP0) = 0xd0020001; mdelay(10);
	mb();	
	REG32(SYSREG_SDS_TOP0) = 0xc0020001; mdelay(10);

	return;
}

static void rtk_serdes_enable_nway(void)	
{
	DBG_PRK("enable SERDES NWAY\n");

	REG32(SYSREG_SDS_TOP1) = 0x080270e0; mdelay(10);
	REG32(SYSREG_SDS_TOP0) = 0xc0020001; mdelay(10);
	mb();	
	REG32(SYSREG_SDS_TOP0) = 0xd0020001; mdelay(10);
	mb();	
	REG32(SYSREG_SDS_TOP0) = 0xc0020001; mdelay(10);

	return;
}

static int rtk_serdes_check_link(void)
{
	unsigned int status=0;

	REG32(SYSREG_SDS_TOP1) = 0x083d0000; mdelay(10);
	REG32(SYSREG_SDS_TOP0) = 0xc0020000; mdelay(10);
	mb();
	REG32(SYSREG_SDS_TOP0) = 0xd0020000; mdelay(10);
	mb();
	REG32(SYSREG_SDS_TOP0) = 0xc0020000; mdelay(10);

	status = REG32(SYSREG_SDS_TOP1) & 0xfff;

	return status;
}


static int serdes_write( struct file *filp, const char *buff,unsigned long len, void *data )
{
	char 			tmpbuf[512];
	char			*strptr;
	char			*cmdptr;
	char			*sg;
	unsigned int 	status = 0;

	if (buff && !copy_from_user(tmpbuf, buff, len))
	{
		tmpbuf[len] = '\0';
		
		strptr=tmpbuf;

		if(strlen(strptr)==0)
		{
			goto errout;
		}
		
		cmdptr = strsep(&strptr," ");
		if (cmdptr==NULL)
		{
			goto errout;
		}
		
		/*parse command*/
		if(strncmp(cmdptr, "check",5) == 0)
		{
			sg = strsep(&strptr," ");
			if(strncmp(sg, "link",4)==0){
				printk("Check SERDES link status !!\n");
				status = rtk_serdes_check_link();
				if(status == 0x111){
					printk("SERDES Link!\n");
				}
				else
				{
					printk("SERDES Link failed, status = 0x%X\n",status);
				}
			}
			else if(strncmp(sg, "pinmux",5)==0){
				printk("Check SERDES registers!!\n");
				printk("REG(0xb8000100) = 0x%08X\n",REG32(0xb8000100));
				printk("bit[31] = %x. Serdes %s\nbit[30] = %x. Select port %s\n"
					, (REG32(0xb8000100)&(1<<31))>>31, (REG32(0xb8000100)&(1<<31))? "enable":"disable"
					, (REG32(0xb8000100)&(1<<30))>>30, (REG32(0xb8000100)&(1<<30))? "4":"5");
			}
			else if(strncmp(sg, "nway",4)==0){
				printk("Check SERDES NWAY status\n");
				status = rtk_serdes_nway_status(); 
				if( status == 0)
					printk("SERDES NWAY is enabled, auto mode\n");
				else
					printk("SERDES NWAY is disabled, force mode\n");
			}
			
		}		
		if(strncmp(cmdptr, "dbg_level",9) == 0)
		{
			sg = strsep(&strptr," ");
			if(strncmp(sg, "1",1)==0){
				DEBUG_LEVEL = 1;
			}
			else if(strncmp(sg, "0",1)==0){
				DEBUG_LEVEL = 0;
			}
			
		}	

		if(strncmp(cmdptr, "auto",4) == 0)
		{
			sg = strsep(&strptr," ");
			if(strncmp(sg, "1",1)==0){/*enable auto mode*/
				DBG_PRK("Enable SERDES NWAY auto detection !!\n");
				setup_timer(&timer_serdes_link, check_serdes_status, 0);
				mod_timer(&timer_serdes_link, jiffies + msecs_to_jiffies(CHECK_INTERVAL));								
			}
			else if(strncmp(sg, "0",1)==0){/*disable auto mode*/
				if(timer_pending(&timer_serdes_link))
				{	/*Delete timer*/			
					DBG_PRK("Disable SERDES auto detection, set SERDES in force mode\n");
					del_timer(&timer_serdes_link); 
					rtk_serdes_disable_nway();
				}

			}
		}
	}

	return len;

errout:

	printk("error input\n");
	return len;

}


static int serdes_read(struct seq_file *seq, void *offset)
{
	seq_printf(seq, "Usage:\n");
	seq_printf(seq, "Disable/Enable auto detect:	#echo auto 0/1 > /proc/realtek/serdes\n");	
	seq_printf(seq, "Check link status:		#echo check link > /proc/realtek/serdes\n");
	seq_printf(seq, "Check SERDES port:		#echo check pinmux > /proc/realtek/serdes\n");
	seq_printf(seq, "Check SERDES NWAY:		#echo check nway > /proc/realtek/serdes\n");	
	seq_printf(seq, "Disable/Enable debug:	#echo dbg_level 0/1 > /proc/realtek/serdes\n");
	return 0;
}

static ssize_t write_proc_serdes(struct file *file, const char __user * userbuf, size_t count, loff_t * off) {	
	return serdes_write(file, userbuf, count, NULL);
}
	
static int read_proc_open_serdes(struct inode *inode, struct file *file) {
	return(single_open(file, serdes_read, NULL));
}	

static const struct file_operations serdes_ops = {
	.open  		= read_proc_open_serdes,
	.read		= seq_read,
	.write 		= write_proc_serdes,
	.llseek		= seq_lseek,
	.release		= single_release,
};

extern struct proc_dir_entry *realtek_proc;
static struct proc_dir_entry *serdes_status;

static unsigned int try_times = 0;
static void check_serdes_status(void)
{
	unsigned int status = 0;
	status = rtk_serdes_check_link();
	if(status == 0x111)
	{
		try_times = 0;
		DBG_PRK("SERDES successfully link-up, reset try_times\n"); 
	}
	else if(status == 0x101)
	{
		if(try_times >= 3)
		{		
			DBG_PRK("SERDES NWAY may not sync with link-partner in auto mode, switch to force mode!\n");
			if(rtk_serdes_nway_status() == 0)
			{
				rtk_serdes_disable_nway();
				try_times = 0;
			}
		}
		try_times++;
	}
	else if(status == 0x100 || status == 0x001 || status == 0x011)
	{
		DBG_PRK("SERDES may not correctly configure, status:0x%03x\n", status);
		try_times = 0;
	}
	else if(status == 0x000)
	{
		rtk_serdes_enable_nway();
		DBG_PRK("SERDES is out of connection, reset NWAY to auto mode\n");
		try_times = 0;		
	}
	mod_timer(&timer_serdes_link, jiffies + msecs_to_jiffies(CHECK_INTERVAL));	
	return;
}

static int __init rtk_serdes_init(void)
{
	int ret = -1;

	serdes_status = proc_create_data("serdes", 0644, realtek_proc, &serdes_ops, NULL);
	if(serdes_status == NULL) {
		printk("can't create proc entry for serdes_status\n");
		goto ERROR;
	}	
	if((REG32(0xb8000100)>>31)&0x1){/*Set switch port4 in force mode*/
		REG32(0xbb804114) = 0x52f7003f;
	}
	return 0;
ERROR:
	return ret;
}

static void __exit rtk_serdes_exit(void)
{
	remove_proc_entry("SERDES",realtek_proc);

	return;
}

module_init(rtk_serdes_init);
module_exit(rtk_serdes_exit);

MODULE_DESCRIPTION("Realtek SERDES Module");
MODULE_AUTHOR("Memphis <memphis.chung@realtek.com>");
MODULE_LICENSE("GPL");