/*
 * max6902.c
 *
 * Driver for MAX6902 RTC
 *
 * Copyright (C) 2004 Compulab Ltd.
 *
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 *
 */

#include <linux/config.h>
#include <linux/module.h>
#include <linux/version.h>
 
#include <linux/kernel.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/rtc.h>
#include <linux/string.h>
#include <linux/miscdevice.h>
#include <linux/proc_fs.h>

#include <asm/hardware.h>
#include <asm/delay.h>
#include <asm/arch/pxa-regs.h>

#include "max6902.h"

#define PROC_MAX6902_NAME	"driver/max6902"

#define BCD_TO_BIN(val) (((val)&15) + ((val)>>4)*10)
#define BIN_TO_BCD(val) ((((val)/10)<<4) + (val)%10)


#define DEBUG 0
#if DEBUG
static unsigned int rtc_debug = DEBUG;
#else
#define rtc_debug 0     /* gcc will remove all the debug code for us */
#endif


static int max6902_rtc_ioctl( struct inode *, struct file *, unsigned int, unsigned long);
static int max6902_rtc_open(struct inode *inode, struct file *file);
static int max6902_rtc_release(struct inode *inode, struct file *file);

static struct file_operations rtc_fops = {
	owner:		THIS_MODULE,
	ioctl:		max6902_rtc_ioctl,
	open:		max6902_rtc_open,
	release:	max6902_rtc_release,
};

static struct miscdevice max6902_rtc_miscdev = {
	RTC_MINOR,
	"rtc",
	&rtc_fops
};

static void
max6902_set_reg(unsigned char address, unsigned char data){
	SSDR = ((address << 8) | data);
	asm volatile ("mcr p15, 0, r0, c7, c10, 4":::"r0");
	if(!(SSSR & 0x4)) while((SSSR & 0xf00) == 0) ;
	while((SSSR & 0xf00) != 0) ;
	while(SSSR & 0x10);
	while(SSSR & 0x8) data=SSDR;
}

static unsigned char
max6902_get_reg(unsigned char address){
	unsigned int data;
	SSDR = ((0x80 | address) << 8);
	asm volatile ("mcr p15, 0, r0, c7, c10, 4":::"r0");
	if(!(SSSR & 0x4)) while((SSSR & 0xf00) == 0) ;
	while((SSSR & 0xf00) != 0) ;
	while(SSSR & 0x10);
	data = SSDR;
	return (data & 0xff);
}


 
static int
max6902_get_datetime(struct rtc_time *dt){
	int ret=0;

	dt->tm_sec	= BCD_TO_BIN(max6902_get_reg(0x01));
	dt->tm_min	= BCD_TO_BIN(max6902_get_reg(0x03));
	dt->tm_hour	= BCD_TO_BIN(max6902_get_reg(0x05));
	dt->tm_mday	= BCD_TO_BIN(max6902_get_reg(0x07));
	dt->tm_mon	= BCD_TO_BIN(max6902_get_reg(0x09)-1);
	dt->tm_wday	= BCD_TO_BIN(max6902_get_reg(0x0B));
	dt->tm_year = BCD_TO_BIN(max6902_get_reg(0x0D)) + BCD_TO_BIN(max6902_get_reg(0x13))*100;

	if(rtc_debug)printk("\n%s : Read RTC values\n",__FUNCTION__);
	if(rtc_debug)printk("tm_hour: %i\n",dt->tm_hour);
	if(rtc_debug)printk("tm_min : %i\n",dt->tm_min);
	if(rtc_debug)printk("tm_sec : %i\n",dt->tm_sec);
	if(rtc_debug)printk("tm_year: %i\n",dt->tm_year);
	if(rtc_debug)printk("tm_mon : %i\n",dt->tm_mon);
	if(rtc_debug)printk("tm_mday: %i\n",dt->tm_mday);
	if(rtc_debug)printk("tm_wday: %i\n",dt->tm_wday);

	dt->tm_year = dt->tm_year-1900;

	return ret;
}


static int
max6902_set_datetime(struct rtc_time *dt, int datetoo){

	dt->tm_year = dt->tm_year+1900;

	if(rtc_debug)printk("\n%s : Setting RTC values\n",__FUNCTION__);
	if(rtc_debug)printk("tm_sec : %i\n",dt->tm_sec);
	if(rtc_debug)printk("tm_min : %i\n",dt->tm_min);
	if(rtc_debug)printk("tm_hour: %i\n",dt->tm_hour);
	if(rtc_debug)printk("tm_mday: %i\n",dt->tm_mday);
	if(rtc_debug)printk("tm_wday: %i\n",dt->tm_wday);
	if(rtc_debug)printk("tm_year: %i\n",dt->tm_year);

	// Remove write protection
	max6902_set_reg(0xF, 0);

	max6902_set_reg(0x01, BIN_TO_BCD(dt->tm_sec));
	max6902_set_reg(0x03, BIN_TO_BCD(dt->tm_min));
	max6902_set_reg(0x05, BIN_TO_BCD(dt->tm_hour));

	if (datetoo) {
		max6902_set_reg(0x07, BIN_TO_BCD(dt->tm_mday));
		max6902_set_reg(0x09, BIN_TO_BCD(dt->tm_mon+1));
		max6902_set_reg(0x0B, BIN_TO_BCD(dt->tm_wday));
		max6902_set_reg(0x0D, BIN_TO_BCD(dt->tm_year%100));
		max6902_set_reg(0x13, BIN_TO_BCD(dt->tm_year/100));
	}

	udelay(5000);

	// Write protect
	max6902_set_reg(0xF, 0x80);

	return 0;
}

static int max6902_read_sram(char* buf) {
	int i;
	for(i=0;i<MAX6902_SRAM_LEN;i++) 
		buf[i]=max6902_get_reg(0x41+2*i);
	return 0;
}

static int max6902_write_sram(char* buf) {
	int i;
	// Remove write protection
	max6902_set_reg(0xF, 0);

	for(i=0;i<MAX6902_SRAM_LEN;i++) max6902_set_reg(0x41+2*i, buf[i]);
	udelay(1000);

	// Write protect
	max6902_set_reg(0xF, 0x80);

	return 0;
}



static int
max6902_rtc_open(struct inode *inode, struct file *file)
{
	return 0;
}

static int
max6902_rtc_release(struct inode *inode, struct file *file)
{
	return 0;
}

static int
max6902_rtc_ioctl( struct inode *inode, struct file *file,
		unsigned int cmd, unsigned long arg)
{
	struct rtc_time wtime;
	int status = 0;
	char buf[MAX6902_SRAM_LEN];

	switch (cmd) {
		default:
		case RTC_UIE_ON:
		case RTC_UIE_OFF:
		case RTC_PIE_ON:
		case RTC_PIE_OFF:
		case RTC_AIE_ON:
		case RTC_AIE_OFF:
		case RTC_ALM_SET: 
		case RTC_ALM_READ:
		case RTC_IRQP_READ:
		case RTC_IRQP_SET:
		case RTC_EPOCH_READ:
		case RTC_EPOCH_SET:
		case RTC_WKALM_SET:
		case RTC_WKALM_RD:
			status = -EINVAL;
			break;

		case RTC_RD_TIME:
			max6902_get_datetime(&wtime);
			if( copy_to_user((void *)arg, &wtime, sizeof (struct rtc_time)))
				status = -EFAULT;
			break;

		case RTC_SET_TIME:
			if (!capable(CAP_SYS_TIME))
			{
				status = -EACCES;
				break;
			}

			if (copy_from_user(&wtime, (struct rtc_time *)arg, sizeof(struct rtc_time)) )
			{
				status = -EFAULT;
				break;
			}

			max6902_set_datetime(&wtime, 1);
			break;
		case RTC_WRITE_SRAM:
			if (copy_from_user(buf, (char*)arg, MAX6902_SRAM_LEN) )
			{
				status = -EFAULT;
				break;
			}
			max6902_write_sram(buf);
			break;
		case RTC_READ_SRAM:
			max6902_read_sram(buf);
			if (copy_to_user((char*)arg, buf, MAX6902_SRAM_LEN) )
				status = -EFAULT;
			break;
	}

	return status;
}

static char *
max6902_mon2str( unsigned int mon)
{
        char *mon2str[12] = {
          "Jan", "Feb", "Mar", "Apr", "May", "Jun",
          "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
        };
        if( mon > 11) return "error";
        else return mon2str[ mon];
}

static int max6902_rtc_proc_output( char *buf)
{
	char *p = buf;
	struct rtc_time dt;
	char sbuf[MAX6902_SRAM_LEN];
	int i;

	max6902_get_datetime(&dt);
	max6902_read_sram(sbuf);


    p += sprintf(p, "\nMAX6902 (Real Time Clock)\n");
    p += sprintf(p, "Date/Time           : %02d-%s-%04d %02d:%02d:%02d\n", 
			dt.tm_mday,
			max6902_mon2str(dt.tm_mon),
			dt.tm_year+1900,
			dt.tm_hour,
			dt.tm_min,
			dt.tm_sec);


    p += sprintf(p, "SRAM dump:\n");
	for( i=0; i<MAX6902_SRAM_LEN; i++)
	{
		p += sprintf(p, "%02X ", sbuf[i]);
		if( (i%8) == 7) p += sprintf(p, "\n");
	}
	p += sprintf(p, "\n\n");

	return  p - buf;
}

static int max6902_rtc_read_proc(char *page, char **start, off_t off,
                int count, int *eof, void *data)
{
        int len = max6902_rtc_proc_output (page);
        if (len <= off+count) *eof = 1;
        *start = page + off;
        len -= off;
        if (len>count) len = count;
        if (len<0) len = 0;
        return len;
}

static __init int max6902_init(void){

	int retval = 0;
	unsigned char temp;

	//printk("SSCR0: %x\n", SSCR0);
	//printk("SSCR1: %x\n", SSCR1);

	asm volatile ("mcr p15, 0, r0, c7, c10, 4":::"r0");
	while(SSSR & 0x10);
	while(SSSR & 0x8) temp=SSDR;

	//printk("After emptying FIFO: %x\n", SSSR);

	// Remove write protection
	max6902_set_reg(0xF, 0x80);

	if(max6902_get_reg(0xF) == 0x80)
	{
		misc_register (&max6902_rtc_miscdev);
        create_proc_read_entry (PROC_MAX6902_NAME, 0, 0, max6902_rtc_read_proc, NULL);
		printk("MAX6902 RTC driver successfully loaded.\n");
	}
	else
	{
		printk("MAX6902 RTC not detected.\n");
		retval = -ENODEV;
	}

	return retval;
}

static __exit void max6902_exit(void)
{
    remove_proc_entry (PROC_MAX6902_NAME, NULL);
	misc_deregister(&max6902_rtc_miscdev);
}

module_init(max6902_init);
module_exit(max6902_exit);


MODULE_AUTHOR ("Compulab Ltd.");
MODULE_LICENSE ("GPL");
