/*
 * Driver interface to the NAND Flash controller on the Samsung HAMCOP
 * Companion chip.
 *
 * HAMCOP: iPAQ H22xx
 *	   Specifications: http://www.handhelds.org/platforms/hp/ipaq-h22xx/
 *
 * Copyright © 2004 Matt Reimer
 *
 * Use consistent with the GNU GPL is permitted,
 * provided that this copyright notice is
 * preserved in its entirety in all copies and derived works.
 *
 * Author:  Matt Reimer <mreimer@vpop.net>
 *          March 2004
 *
 */

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/version.h>
#include <linux/config.h>

#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/delay.h>
#include <linux/input.h>
#include <linux/device.h>
#include <linux/soc-device.h>
#include <linux/workqueue.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/nand.h>
#include <linux/mtd/partitions.h>

#include <asm/arch/hardware.h>
#include <asm/irq.h>
#include <asm/io.h>

#include <asm/hardware/ipaq-hamcop.h>

#include <asm/hardware/shamcop-clocks.h>

// Define this to see all suspend/resume init/cleanup messages
#define DEBUG_INIT()  \
        if (0) printk(KERN_NOTICE "enter %s\n", __FUNCTION__)
#define DEBUG_FINI() \
        if (0) printk(KERN_NOTICE "leave %s\n", __FUNCTION__)
#define DEBUG_OUT(f,a...) \
	if (0) printk(KERN_NOTICE "%s:" f, __FUNCTION__, ##a)
#define DEBUG_ISR_INIT()  \
        if (0) printk(KERN_NOTICE "enter %s\n", __FUNCTION__)
#define DEBUG_ISR_FINI() \
        if (0) printk(KERN_NOTICE "leave %s\n", __FUNCTION__)
#define DEBUG_ISR_OUT(f,a...) \
	if (0) printk(KERN_NOTICE "%s:" f, __FUNCTION__, ##a)

#define PDEBUG(format,arg...) printk(KERN_DEBUG __FILE__ ":%s - " format "\n", __FUNCTION__, ## arg)
#define PALERT(format,arg...) printk(KERN_ALERT __FILE__ ":%s - " format "\n", __FUNCTION__, ## arg)
#define PERROR(format,arg...) printk(KERN_ERR __FILE__ ":%s - " format "\n", __FUNCTION__, ## arg)

/****************************************************************************
 *   NAND Flash controller
 ****************************************************************************/

static int write_enable = 1;
module_param(write_enable, uint, 0);
MODULE_PARM_DESC(write_enable, "Enable writes to NAND flash");

static struct nand_oobinfo wince_eccoob = {
	.useecc		= MTD_NANDECC_AUTOPLACE,
	.eccbytes	= 3,
	.eccpos		= { 6, 7, 8 },
	.oobfree	= { {0, 5}, {9, 7} },
};

static struct nand_oobinfo nand_hw_eccoob = {
	.useecc = MTD_NANDECC_AUTOPLACE,
	.eccbytes = 3,
	.eccpos = {0, 1, 2 },
	.oobfree = { {8, 8} }
};


/*#ifdef CONFIG_MTD_PARTITIONS*/
/*
 * Define static partitions for flash device
 */
static struct mtd_partition partition_info[] = {
	{ name: "HAMCOP First-stage Bootstrap",
		offset: 0,
		size: 16*1024 },
	{ name: "HAMCOP Bootloader",
		offset: 16*1024,
		size: 512*1024 },
	{ name: "HAMCOP NAND Flash",
		offset: 528*1024,
		size: 1*1024*1024 }
		/* size: 32*1024*1024 - 528*1024 } */
};
#define NUM_PARTITIONS 3

/*#endif*/

struct hamcop_nand_data {
	struct semaphore   lock;      // Mutex for access from user-level
	wait_queue_head_t  waitq;     // Waitq for user-level access (waits for interrupt service)

	struct device      *parent;
	void               *map;

	struct mtd_info	   *mtd;

/*
	struct resource     res;
	struct work_struct  work;
*/
};

static inline void
hamcop_nand_write_register (struct hamcop_nand_data *nand, u32 reg, u16 val)
{
	__raw_writew (val, reg + nand->map);
}

static inline u16
hamcop_nand_read_register (struct hamcop_nand_data *nand, u32 reg)
{
	return __raw_readw (reg + nand->map);
}



/* hardware specific access to control-lines */
static void hamcop_nand_hwcontrol(struct mtd_info *mtd, int cmd) 
{
	struct nand_chip* this = (struct nand_chip *) (mtd->priv);
	struct hamcop_nand_data *nand = (struct hamcop_nand_data *) (this->priv);
	
	switch(cmd) {
		
	case NAND_CTL_SETCLE: 
		this->IO_ADDR_W = nand->map + _HAMCOP_NF_CMMD; break;
	case NAND_CTL_CLRCLE: 
		this->IO_ADDR_W = nand->map + _HAMCOP_NF_DATA; break;
	case NAND_CTL_SETALE:
		this->IO_ADDR_W = nand->map + _HAMCOP_NF_ADDR0; break;
	case NAND_CTL_CLRALE:
		this->IO_ADDR_W = nand->map + _HAMCOP_NF_DATA; break;
	}
}

static void hamcop_nand_cmdfunc(struct mtd_info *mtd, unsigned command, int column, int page_addr)
{
	register struct nand_chip *this = mtd->priv;
	struct hamcop_nand_data *nand = (struct hamcop_nand_data *) (this->priv);

	/*
	 * Write out the command to the device.
	 */
	if (command == NAND_CMD_SEQIN) {
		int readcmd;
		if (column >= mtd->oobblock) {
			/* OOB area */
			column -= mtd->oobblock;
			readcmd = NAND_CMD_READOOB;
		} else if (column < 256) {
			/* First 256 bytes --> READ0 */
			readcmd = NAND_CMD_READ0;
		} else {
			column -= 256;
			readcmd = NAND_CMD_READ1;
		}
		hamcop_nand_write_register (nand, _HAMCOP_NF_CMMD, readcmd);
	}
	hamcop_nand_write_register (nand, _HAMCOP_NF_CMMD, command & 0xff);

	if (column != -1 || page_addr != -1) {
		/* Serially input address */
		if (column != -1)
			hamcop_nand_write_register (nand, _HAMCOP_NF_ADDR0,
				column & 0xff);
		if (page_addr != -1) {
			hamcop_nand_write_register (nand, _HAMCOP_NF_ADDR0,
		       	    page_addr & 0xff);
			hamcop_nand_write_register (nand, _HAMCOP_NF_ADDR0,
		       	    (page_addr >> 8) & 0xff);
			/* One more address cycle for higher density devices */
			if (mtd->size & 0x0c000000) 
			hamcop_nand_write_register (nand, _HAMCOP_NF_ADDR0,
		       	    (page_addr >> 16) & 0x0f);
		}
		/* Latch in address */
	}
	
	/* 
	 * program and erase have their own busy handlers 
	 * status and sequential in needs no delay
	 */
	switch (command) {
			
	case NAND_CMD_PAGEPROG:
	case NAND_CMD_ERASE1:
	case NAND_CMD_ERASE2:
	case NAND_CMD_SEQIN:
	case NAND_CMD_STATUS:
		return;

	case NAND_CMD_RESET:
		break;

	/* This applies to read commands */	
	default:
		break;
	}

	/* Apply this short delay always to ensure that we do wait tWB in
	 * any case on any machine. */
	ndelay (100);

	/* wait until command is processed */
	if (this->dev_ready)
		while (!this->dev_ready(mtd));
}

static void hamcop_nand_select_chip(struct mtd_info *mtd, int chip)
{
	u16 cfg;
	register struct nand_chip *this = mtd->priv;
	struct hamcop_nand_data *nand = (struct hamcop_nand_data *) (this->priv);

	cfg = hamcop_nand_read_register (nand, _HAMCOP_NF_CONT0);

	if (chip == 0)
		hamcop_nand_write_register (nand, _HAMCOP_NF_CONT0,
			cfg & ~HAMCOP_NF_CONT0_NFNCE);
	else
		hamcop_nand_write_register (nand, _HAMCOP_NF_CONT0,
			cfg | HAMCOP_NF_CONT0_NFNCE);
}


/*
 *	read device ready pin
 */
static int hamcop_nand_device_ready(struct mtd_info *mtd)
{
	register struct nand_chip *this = mtd->priv;
	struct hamcop_nand_data *nand = (struct hamcop_nand_data *) (this->priv);

	return (hamcop_nand_read_register(nand, _HAMCOP_NF_STAT1) &
		HAMCOP_NF_STAT1_FLASH_RNB) ? 1 : 0;
}

/* over-ride the standard functions for a little more speed? */

static void hamcop_nand_read_buf(struct mtd_info *mtd, u_char *buf, int len)
{
	struct nand_chip *this = mtd->priv;
	readsb(this->IO_ADDR_R, buf, len);
}

static void hamcop_nand_write_buf(struct mtd_info *mtd,
                                   const u_char *buf, int len)
{
	struct nand_chip *this = mtd->priv;
	writesb(this->IO_ADDR_W, buf, len);
}


/*
 * Initialize hardware ECC.
 */

static void hamcop_nand_enable_hwecc(struct mtd_info *mtd, int mode)
{
	u16 cfg;
	register struct nand_chip *this = mtd->priv;
	struct hamcop_nand_data *nand = (struct hamcop_nand_data *) (this->priv);

	cfg = hamcop_nand_read_register(nand, _HAMCOP_NF_CONT0);
	hamcop_nand_write_register(nand, _HAMCOP_NF_CONT0,
		cfg | HAMCOP_NF_CONT0_INITECC);
}

static int hamcop_nand_correct_data(struct mtd_info *mtd, u_char *dat,
				    u_char *read_ecc, u_char *calc_ecc)
{
	if (read_ecc[0] == calc_ecc[0] &&
	    read_ecc[1] == calc_ecc[1] &&
	    read_ecc[2] == calc_ecc[2]) 
		return 0;

	/* we curently have no method for correcting the error */

	return -1;
}

static int hamcop_nand_calculate_ecc(struct mtd_info *mtd,
				     const u_char *dat, u_char *ecc_code)
{
	u16 ecc;
	register struct nand_chip *this = mtd->priv;
	struct hamcop_nand_data *nand = (struct hamcop_nand_data *) (this->priv);

	ecc = hamcop_nand_read_register(nand, _HAMCOP_NF_ECCL0);
	ecc_code[0] = ecc & 0xff;
	ecc_code[1] = (ecc >> 8) & 0xff;
	ecc_code[2] = hamcop_nand_read_register(nand, _HAMCOP_NF_ECCL1) & 0xff;

        return 0;
}


static void 
hamcop_nand_up (struct hamcop_nand_data *nand)
{
        DEBUG_INIT ();
        
        /* Enable the NAND clock */
        shamcop_clock_enable (nand->parent, SHAMCOP_NAND_CLKEN, 1);

        DEBUG_FINI ();
}

static void 
hamcop_nand_down (struct hamcop_nand_data *nand)
{
        DEBUG_INIT();
        
	/* XXX what about outstanding I/O? */

        /* Disable the NAND clock */
        shamcop_clock_enable (nand->parent, SHAMCOP_NAND_CLKEN, 0);

        DEBUG_FINI ();
}

/* ppc uses 0x1213 */
static void hamcop_nand_reset (struct hamcop_nand_data *nand)
{
	DEBUG_INIT ();

	/* Set flash memory timing */
	hamcop_nand_write_register(nand, _HAMCOP_NF_CONF0,
		HAMCOP_NF_CONF0_TACLS_1  |
		HAMCOP_NF_CONF0_TWRPH0_2 |
		HAMCOP_NF_CONF0_TWRPH1_2);

	/* No RnB interrupts. */
	hamcop_nand_write_register(nand, _HAMCOP_NF_CONT1, 0);

	/* NAND Flash controller enable */
	hamcop_nand_write_register(nand, _HAMCOP_NF_CONT0,
		HAMCOP_NF_CONT0_WP_EN |
		HAMCOP_NF_CONT0_MODE_NFMODE);

	/* Chip Enable -> RESET -> Wait for Ready -> Chip Disable */
	hamcop_nand_write_register(nand, _HAMCOP_NF_CMMD, NAND_CMD_RESET);

	hamcop_nand_read_register(nand, _HAMCOP_NF_STAT1);

	while (!(hamcop_nand_read_register(nand, _HAMCOP_NF_STAT1) &
		HAMCOP_NF_STAT1_FLASH_RNB));

	hamcop_nand_write_register(nand, _HAMCOP_NF_CONT0,
		HAMCOP_NF_CONT0_WP_EN |
		HAMCOP_NF_CONT0_NFNCE |
		HAMCOP_NF_CONT0_MODE_NFMODE);

	DEBUG_FINI ();
}


/* XXX What if I/O is in progress? */
static int 
hamcop_nand_suspend (struct device *dev, u32 state, u32 level)
{
	struct hamcop_nand_data *nand = dev->driver_data;

	DEBUG_INIT();
	//down (&nand->lock);  // No interruptions
	hamcop_nand_down (nand);
	DEBUG_FINI ();
	return 0;
}

/* XXX Should we set MODE etc.? */
static int
hamcop_nand_resume (struct device *dev, u32 level)
{
	struct hamcop_nand_data *nand = dev->driver_data;

	DEBUG_INIT();
	hamcop_nand_up (nand);
	hamcop_nand_reset (nand);
	//up (&nand->lock);
	DEBUG_FINI();

	return 0;
}

static int 
hamcop_nand_probe (struct device *dev)
{
	struct hamcop_nand_data *nand;
	struct soc_device *sdev = to_soc_device (dev);
	struct nand_chip *this;
	const char *part_type = 0;
	int mtd_parts_nb = 0;
	struct mtd_partition *mtd_parts = 0;

	DEBUG_INIT();

	nand = kmalloc (sizeof (*nand), GFP_KERNEL);
	if (!nand)
		return -ENOMEM;
	memset (nand, 0, sizeof (*nand));

	nand->parent = dev->parent;
	dev->driver_data = nand;
	nand->map = (void *)sdev->resource[1].start;

	//init_MUTEX (&nand->lock);
	//init_waitqueue_head (&nand->waitq);

	/* Allocate memory for MTD device structure and private data */
	nand->mtd = kmalloc(sizeof(struct mtd_info) + sizeof(struct nand_chip),
			     GFP_KERNEL);
	if (!nand->mtd) {
		printk("Unable to allocate HAMCOP NAND MTD device structure.\n");
		return -ENOMEM; /* XXX */
	}

	/* Get pointer to private data. */
	this = (struct nand_chip *) (&nand->mtd[1]);

	/* Initialize structures. */
	memset((char *) nand->mtd, 0, sizeof(struct mtd_info));
	memset((char *) this, 0, sizeof(struct nand_chip));

	/* Link the private data with the MTD structure. */
	nand->mtd->priv = this;

	/* Enable the NAND Flash controller. */
	hamcop_nand_up (nand);

	/* Reset the controller. */
	hamcop_nand_reset (nand);

	/* Set up callbacks. */
	this->priv	  = nand;
	this->IO_ADDR_R   = nand->map + _HAMCOP_NF_DATA;
	this->IO_ADDR_W   = nand->map + _HAMCOP_NF_DATA;
	this->select_chip = hamcop_nand_select_chip;
	this->hwcontrol   = hamcop_nand_hwcontrol;
//	this->cmdfunc     = hamcop_nand_cmdfunc;
//	this->dev_ready   = hamcop_nand_device_ready;
//	this->read_buf    = hamcop_nand_read_buf;
//	this->write_buf   = hamcop_nand_write_buf;
	this->chip_delay  = 50; 	/* 50 us command delay time */

	this->correct_data  = hamcop_nand_correct_data;
	this->enable_hwecc  = hamcop_nand_enable_hwecc;
	this->calculate_ecc = hamcop_nand_calculate_ecc;
	this->eccmode       = NAND_ECC_HW3_512;
	this->autooob       = &nand_hw_eccoob;

	/* Scan to find existence of the device. */
	if (nand_scan (nand->mtd, 1)) {
		printk(KERN_NOTICE "No NAND device - returning -ENXIO\n");
		kfree (nand->mtd);
		nand->mtd = NULL;
		return -ENXIO; /* XXX */
	}
this->options &= ~NAND_NO_AUTOINCR;

#ifdef CONFIG_MTD_CMDLINE_PARTS
	mtd_parts_nb = parse_cmdline_partitions(nand->mtd, &mtd_parts, 
						"hamcop-nand");
	if (mtd_parts_nb > 0)
	  part_type = "command line";
	else
	  mtd_parts_nb = 0;
#endif
	if (mtd_parts_nb == 0) {
		mtd_parts = partition_info;
		mtd_parts_nb = NUM_PARTITIONS;
		part_type = "static";
	}
	
	/* Register the partitions */
	printk(KERN_NOTICE "Using %s partition definition\n", part_type);
	add_mtd_partitions(nand->mtd, mtd_parts, mtd_parts_nb);
	
	DEBUG_FINI ();

	return 0;
}

static int
hamcop_nand_remove (struct device *dev)
{
	struct hamcop_nand_data *nand = dev->driver_data;

	DEBUG_INIT();

	hamcop_nand_down (nand);

	nand_release(nand->mtd);

	kfree (nand->mtd);
	kfree (nand);

	DEBUG_FINI ();

	return 0;
}

static soc_device_id hamcop_nand_device_ids[] = {
	IPAQ_HAMCOP_NAND_DEVICE_ID, 0
};

struct soc_device_driver hamcop_nand_soc_device_driver = {
	.device_ids = hamcop_nand_device_ids,
	.driver = {
		.name     = "hamcop nand",
		.probe    = hamcop_nand_probe,
		.remove   = hamcop_nand_remove,
		.suspend  = hamcop_nand_suspend,
		.resume   = hamcop_nand_resume
	}
};

static int
hamcop_nand_init (void)
{
	return soc_driver_register (&hamcop_nand_soc_device_driver);
}

static void
hamcop_nand_cleanup (void)
{
	soc_driver_unregister (&hamcop_nand_soc_device_driver);
}

module_init(hamcop_nand_init);
module_exit(hamcop_nand_cleanup);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Matt Reimer <mreimer at vpop dot net>");
MODULE_DESCRIPTION("NAND flash driver for Samsung HAMCOP Companion Chip (used in iPAQ h2200)");
