/******************************************************************************
**
** FILE NAME    : ifxmips_eeprom.c
** PROJECT      : IFX UEIP
** MODULES      : EEPROM
**
** DESCRIPTION  : X25040 EEPROM Driver
** COPYRIGHT    :       Copyright (c) 2009
**                      Infineon Technologies AG
**                      Am Campeon 1-12, 85579 Neubiberg, Germany
**
**    This program is free software; you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation; either version 2 of the License, or
**    (at your option) any later version.
**
** HISTORY
** $Version $Date          $Author        $Comment
** 0.0.1    06 July,2009  Lei Chuanhua    First UEIP release
*******************************************************************************/

#ifndef EXPORT_SYMTAB
#define EXPORT_SYMTAB
#endif
#ifndef AUTOCONF_INCLUDED
#include <linux/config.h>
#endif /* AUTOCONF_INCLUDED */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/types.h>
#include <linux/major.h>
#include <linux/proc_fs.h>
#include <linux/fcntl.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <asm/uaccess.h>

/* Project header */
#include <common_routines.h>
#include <ifx_gpio.h>
#include <ifx_ssc.h>

#define IFX_EEPROM_VER_MAJOR        1
#define IFX_EEPROM_VER_MID          1
#define IFX_EEPROM_VER_MINOR        1

#define IFX_EEPROM_EMSG(fmt,args...) printk("%s:" fmt, __func__, ##args)
//#define IFX_EEPROM_DBG
#ifdef IFX_EEPROM_DBG
#define IFX_EEPROM_DMSG(fmt,args...) printk("%s:" fmt, __func__, ##args)
#define INLINE 
#else
#define IFX_EEPROM_DMSG(fmt,args...)
#define INLINE inline 
#endif

static IFX_SSC_HANDLE *eeprom_handler;

/* allow the user to set the major device number 60 ~ 63 for experimental */
static int maj = 60;

/* commands for EEPROM, x25160, x25140 */
#define IFX_EEPROM_WREN         ((u8)0x06)
#define IFX_EEPROM_WRDI         ((u8)0x04)
#define IFX_EEPROM_RDSR         ((u8)0x05)
#define IFX_EEPROM_WRSR         ((u8)0x01)
#define IFX_EEPROM_READ         ((u8)0x03)
#define IFX_EEPROM_WRITE        ((u8)0x02)
#define IFX_EEPROM_PAGE_SIZE          4
#define IFX_EEPROM_SIZE               512
#define IFX_EEPROM_DETECT_COUNTER     100000

#if (LINUX_VERSION_CODE < KERNEL_VERSION(2,5,52))
MODULE_PARM (maj, "i");
#else
#include <linux/moduleparam.h>
module_param(maj, int, 0600);
#endif 
MODULE_PARM_DESC (maj, "EEPROM major device number");

static int
ifx_eeprom_rdsr (char *status)
{
    int ret = 0;
    u8 cmd = IFX_EEPROM_RDSR;

    if ((ret = ifx_sscTxRx(eeprom_handler,(char *)&cmd, sizeof(u8), status, sizeof(u8))) != 2){
        IFX_EEPROM_EMSG("line %d ifx_sscTxRx fails %d\n", __LINE__, ret);
        return -1;
    }
    return 0;
}

static INLINE int
ifx_eeprom_wip_over(void)
{
    int ret = 0;
    u8 status;
    int count = 0;

    while (1) {
        ret = ifx_eeprom_rdsr(&status);
        IFX_EEPROM_DMSG("status %x \n", status);
        if (ret) {
            IFX_EEPROM_EMSG("read back status fails %d\n", ret);
            break;
        }
        else if ((status & 1) == 0) {
            break;
        }
        if (++count > IFX_EEPROM_DETECT_COUNTER) {
            IFX_EEPROM_EMSG("Detect counter out of range\n");
            ret = -1;
            break;
        }
    }
    return ret;
}

static int
ifx_eeprom_wren(void)
{
    int ret = 0;
    u8 cmd = IFX_EEPROM_WREN;

    if ((ret = ifx_sscTx(eeprom_handler,(char *)&cmd, sizeof(u8))) != 1){
        IFX_EEPROM_EMSG("line %d ifx_sscTx fails %d\n", __LINE__, ret);
        return ret;
    }
    ifx_eeprom_wip_over();
    return 0; 
}

static int
ifx_eeprom_wrsr(void)
{
    int ret = 0;
    u8 cmd[2];

    cmd[0] = IFX_EEPROM_WRSR;
    cmd[1] = 0;
    
    if ((ret = ifx_eeprom_wren())) {
        IFX_EEPROM_EMSG ("ifx_eeprom_wren fails\n");
        return ret;
    }
    if ((ret = ifx_sscTx(eeprom_handler,(char *)&cmd, sizeof(cmd))) != 2){
        IFX_EEPROM_EMSG("line %d ifx_sscTx fails %d\n", __LINE__, ret);
        return ret;
    }
    ifx_eeprom_wip_over();
    return 0;
}

static int
eeprom_read(u32 addr, unsigned char *buf, u32 len)
{
    int ret = 0;
    u8 write_buf[2];
    u32 eff = 0;
    int total = 0;

    while (1) {
        ifx_eeprom_wip_over ();
        eff = IFX_EEPROM_PAGE_SIZE - (addr % IFX_EEPROM_PAGE_SIZE);
        eff = (eff < len) ? eff : len;
        write_buf[0] = IFX_EEPROM_READ | ((u8) ((addr & 0x100) >> 5));
        write_buf[1] = (addr & 0xff);
        if ((ret = ifx_sscTxRx(eeprom_handler, write_buf, 2, buf, eff)) != (eff + 2)) {
            IFX_EEPROM_EMSG("ifx_sscTxRx fails %d\n", ret);
            goto eeprom_read_err_out;
        }
        buf += eff;
        len -= eff;
        addr += eff;
        total += eff;
        if (len <= 0) {
            break;
        }
    }
eeprom_read_err_out:
    return total;
}

static int
eeprom_write(u32 addr, unsigned char *buf, u32 len)
{
    int ret = 0;
    u32 eff = 0;
    int start = 0;
    int total = 0;
    static u8 write_buf[IFX_EEPROM_SIZE] = {0}; /* XXX */

    while (1) {
        ifx_eeprom_wip_over();
        if ((ret = ifx_eeprom_wren())) {
            IFX_EEPROM_EMSG("ifx_eeprom_wren fails\n");
            return ret;
        }
        write_buf[0] = IFX_EEPROM_WRITE | ((u8) ((addr & 0x100) >> 5));
        write_buf[1] = (addr & 0xff);
        start = 2;
        
        eff = IFX_EEPROM_PAGE_SIZE - (addr % IFX_EEPROM_PAGE_SIZE);
        eff = (eff < len) ? eff : len;
        memcpy(write_buf + start, buf, eff);
        
        total = start + eff;
        if ((ret = ifx_sscTx(eeprom_handler, write_buf, total)) != total) {
            IFX_EEPROM_EMSG("ifx_sscTx fails %d\n", ret);
            return ret;
        }
        buf += eff;
        len -= eff;
        addr += eff;
        if (len <= 0)
            break;
    }
    return 0;
}

static INLINE int
ifx_eeprom_open (struct inode *inode, struct file *filp)
{
    filp->f_pos = 0;
    return 0;
}

static INLINE int
ifx_eeprom_close (struct inode *inode, struct file *filp)
{
    return 0;
}

static INLINE int
ifx_eeprom_ioctl (struct inode *inode, struct file *filp, unsigned int cmd,
             unsigned long data)
{
    return 0;
}

ssize_t
ifx_eeprom_kread (char *buf, size_t len, u32 addr)
{
    int ret = 0;

    if ((addr + len) > IFX_EEPROM_SIZE) {
        IFX_EEPROM_EMSG("invalid len\n");
        addr = 0;
        len = IFX_EEPROM_SIZE / 2;
    }

    if ((ret = ifx_eeprom_wrsr())) {
        IFX_EEPROM_EMSG("EEPROM reset fails\n");
        goto read_err_out;
    }

    if ((ret = eeprom_read (addr, buf, len)) != len) {
        IFX_EEPROM_EMSG("eeprom read fails\n");
        goto read_err_out;
    } 
read_err_out:
    return ret;
}
EXPORT_SYMBOL(ifx_eeprom_kread);

static ssize_t
ifx_eeprom_read (struct file *filp, char *ubuf, size_t len, loff_t * off)
{
    int ret = 0;
    u8 ssc_rx_buf[IFX_EEPROM_SIZE];
    
    if (*off >= IFX_EEPROM_SIZE)
        return 0;
    if (*off + len > IFX_EEPROM_SIZE)
        len = IFX_EEPROM_SIZE - *off;
    if (len == 0)
       return 0;
    if ((ret = ifx_eeprom_kread (ssc_rx_buf, len, *off)) < 0) {
        IFX_EEPROM_EMSG("read fails, err=%x\n", ret);
        return ret;
    }
    if (copy_to_user ((void *) ubuf, ssc_rx_buf, ret) != 0) {
        ret = -EFAULT;
    }
    *off += len;
    return len;
}

ssize_t
ifx_eeprom_kwrite (char *buf, size_t len, u32 addr)
{
    int ret = 0;

    if ((ret = ifx_eeprom_wrsr())) {
        IFX_EEPROM_EMSG("EEPROM reset fails\n");
        goto write_err_out;
    }
    if ((ret = eeprom_write(addr, buf, len))) {
        IFX_EEPROM_EMSG("eeprom write fails\n");
        goto write_err_out;
    }
write_err_out:
    return ret;
}
EXPORT_SYMBOL(ifx_eeprom_kwrite);

static ssize_t
ifx_eeprom_write (struct file *filp, const char *ubuf, size_t len,
             loff_t * off)
{
    int ret = 0;
    unsigned char ssc_tx_buf[IFX_EEPROM_SIZE];
    
    if (*off >= IFX_EEPROM_SIZE)
        return 0;
    if ((len + *off) > IFX_EEPROM_SIZE)
        len = IFX_EEPROM_SIZE - *off;

    if ((ret = copy_from_user (ssc_tx_buf, ubuf, len))) {
        return EFAULT;
    }
    ret = ifx_eeprom_kwrite (ssc_tx_buf, len, *off);
    if (ret > 0) {
        *off = ret;
    }
    return ret;
}

static loff_t
ifx_eeprom_llseek (struct file * filp, loff_t off, int whence)
{
    loff_t newpos;
    
    switch (whence) {
        case 0:     /*SEEK_SET */
            newpos = off;
            break;
        case 1:     /*SEEK_CUR */
            newpos = filp->f_pos + off;
            break;
        default:
            return -EINVAL;
    }
    if (newpos < 0)
        return -EINVAL;
    filp->f_pos = newpos;
    return newpos;
}


static struct 
file_operations ifx_eeprom_fops = {
    .owner   = THIS_MODULE,
    .llseek  = ifx_eeprom_llseek,
    .read    = ifx_eeprom_read,
    .write   = ifx_eeprom_write,
    .ioctl   = ifx_eeprom_ioctl,
    .open    = ifx_eeprom_open, 
    .release = ifx_eeprom_close,
};

#define IFX_EEPROM_MODE                IFX_SSC_MODE_0
#define IFX_EEPROM_PRIORITY            IFX_SSC_PRIO_LOW
#define IFX_EEPROM_FRAGSIZE            512
#define IFX_EEPROM_MAXFIFOSIZE         32
#define IFX_EEPROM_CS                  IFX_SSC_WHBGPOSTAT_OUT0_POS
#define IFX_EEPROM_NAME                "ifx_eeprom"
#ifdef  CONFIG_USE_EMULATOR
#define IFX_EEPROM_BAUDRATE            10000                       /*  10KHz */
#else
#define IFX_EEPROM_BAUDRATE            1000000                      /*  1 MHz */
#endif

static INLINE int 
ifx_eeprom_cs_handler(u32 csq, IFX_CS_DATA cs_data)
{
    if (csq == IFX_SSC_CS_ON) { /* Low active */
        return ifx_ssc_cs_low(cs_data);
    }
    else {
        return ifx_ssc_cs_high(cs_data);
    }
}

static INLINE IFX_SSC_HANDLE
ifx_eeprom_register(char *dev_name) 
{
    IFX_SSC_CONFIGURE_t ssc_cfg = {0};
    
    ssc_cfg.baudrate     = IFX_EEPROM_BAUDRATE;
    ssc_cfg.csset_cb     = ifx_eeprom_cs_handler;
    ssc_cfg.cs_data      = IFX_EEPROM_CS;
    ssc_cfg.fragSize     = IFX_EEPROM_FRAGSIZE;
    ssc_cfg.maxFIFOSize  = IFX_EEPROM_MAXFIFOSIZE;
    ssc_cfg.ssc_mode     = IFX_EEPROM_MODE;
    ssc_cfg.ssc_prio     = IFX_EEPROM_PRIORITY;
    return ifx_sscAllocConnection(dev_name, &ssc_cfg);
}

static INLINE void 
ifx_eeprom_gpio_init(void)
{
    ifx_gpio_register(IFX_GPIO_MODULE_SPI_EEPROM);
}

static INLINE void 
ifx_eeprom_gpio_release(void)
{   
    ifx_gpio_deregister(IFX_GPIO_MODULE_SPI_EEPROM);
}

static int __init
ifx_eeprom_init (void)
{
    int ret = 0;
    char ver_str[128] = {0};
    
    if ((ret = register_chrdev (maj, "eeprom", &ifx_eeprom_fops)) < 0) {
        printk ("Unable to register major %d for the Infineon Amazon EEPROM\n", maj);
        if (maj == 0) {
           goto errout;
        }
        else {
            maj = 0;
            if ((ret = register_chrdev (maj, "ssc",  &ifx_eeprom_fops)) < 0) {
                printk ("Unable to register major 0 for the Infineon Amazon EEPROM\n");
                goto errout;
            }
       }
    }
    if (maj == 0)
        maj = ret;
    eeprom_handler = ifx_eeprom_register(IFX_EEPROM_NAME);
    if (eeprom_handler == NULL) {
        printk("%s failed to register eeprom\n", __func__);
        unregister_chrdev (maj, "eeprom");
        return -ENOMEM;
    }
    ifx_eeprom_gpio_init();

    ifx_drv_ver(ver_str, "SPI EERPOM", IFX_EEPROM_VER_MAJOR, IFX_EEPROM_VER_MID, IFX_EEPROM_VER_MINOR);
    printk(KERN_INFO "%s", ver_str);
errout:
    return ret;
}

static void __exit
ifx_eeprom_exit (void)
{
    /* New kernel has no return value */
    unregister_chrdev(maj, "eeprom");

    ifx_sscFreeConnection(eeprom_handler); 
    ifx_eeprom_gpio_release();
}
module_init(ifx_eeprom_init);
module_exit(ifx_eeprom_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Lei Chuanhua, Chuanhua.lei@infineon.com");
MODULE_DESCRIPTION("IFX EEPROM driver");
MODULE_SUPPORTED_DEVICE("ifx_eeprom");

#ifndef MODULE
static int __init
ifx_eeprom_set_maj (char *str)
{
    maj = simple_strtol (str, NULL, 0);
    return 1;
}
__setup ("eeprom_maj=", ifx_eeprom_set_maj);
#endif /* !MODULE */