/*
 * (C) Copyright 2005 2N TELEKOMUNIKACE, Ladislav Michl
 *
 * See file CREDITS for list of people who contributed to this
 * project.
 *
 * 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.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 * MA 02111-1307 USA
 */

#include <common.h>
#include <stb225_gpio.h>
#include <asm/arch/regaccess.h>

#if (CONFIG_COMMANDS & CFG_CMD_NAND)

#include <nand.h>

#if ! defined (NAND_ALE_ADDRESS_LINE)
#define NAND_ALE_ADDRESS_LINE		16	/* A16 is the default on STB225 board */
#endif

#if ! defined (NAND_CLE_ADDRESS_LINE)
#define NAND_CLE_ADDRESS_LINE		20	/* A20 is the default on STB225 board */
#endif

extern int stb225_bootmode (void);
extern void nand_data_read (unsigned long base, unsigned char *dst, unsigned int bytecount);
extern void nand_data_write (unsigned long base, const unsigned char *src, unsigned int bytecount);

/*
   PNX8335_CONFIG_NAND_CS_EN allows individual nCS lines to be either
   forced low or left to toggle under the control of the MIU. Set bit
   0,1,2 or 3 in PNX8335_CONFIG_NAND_CS_EN to force the corresponding
   nCS line low, clear to release control back to the MIU.

   - NAND_CTL_SETNCE means drive nCS low, which means bit in
     PNX8335_CONFIG_NAND_CS_EN should be set high.
   - NAND_CTL_CLRNCE means drive nCS high, which means bit in
     PNX8335_CONFIG_NAND_CS_EN should be cleared (and MIU accesses stopped).

   PNX8335_CONFIG_NAND_CS_EN is not supported on M0...
*/

static int miu_address_to_cs (unsigned long base)
{
	return (base >> 25) & 0x03;
}

static void miu_ncs_assert (unsigned long base)
{
	udelay (1);
	PNX8335_CONFIG_NAND_CS_EN |= (1 << miu_address_to_cs (base));
	udelay (1);
}

static void miu_ncs_release (unsigned long base)
{
	udelay (1);
	PNX8335_CONFIG_NAND_CS_EN &= ~(1 << miu_address_to_cs (base));
	udelay (1);
}

static void stb225_nand_hwcontrol_ncs_control (struct mtd_info *mtd, int cmd)
{
	struct nand_chip *this = mtd->priv;
	unsigned long IO_ADDR_W = (unsigned long) this->IO_ADDR_W;

	if (cmd == NAND_CTL_SETNCE) {
		miu_ncs_assert (IO_ADDR_W);
		return;
	}
	if (cmd == NAND_CTL_CLRNCE) {
		miu_ncs_release (IO_ADDR_W);
		return;
	}

	IO_ADDR_W &= ~((1 << NAND_CLE_ADDRESS_LINE) | (1 << NAND_ALE_ADDRESS_LINE));

	switch (cmd) {
		case NAND_CTL_SETCLE: IO_ADDR_W |= (1 << NAND_CLE_ADDRESS_LINE); break;
		case NAND_CTL_SETALE: IO_ADDR_W |= (1 << NAND_ALE_ADDRESS_LINE); break;
	}

	this->IO_ADDR_W = (void *) IO_ADDR_W;
}

static void stb225_nand_hwcontrol_ncs_ignore (struct mtd_info *mtd, int cmd)
{
	struct nand_chip *this = mtd->priv;
	unsigned long IO_ADDR_W;

	if ((cmd == NAND_CTL_SETNCE) || (cmd == NAND_CTL_CLRNCE))
		return;

	IO_ADDR_W = (unsigned long) this->IO_ADDR_W;
	IO_ADDR_W &= ~((1 << NAND_CLE_ADDRESS_LINE) | (1 << NAND_ALE_ADDRESS_LINE));

	switch (cmd) {
		case NAND_CTL_SETCLE: IO_ADDR_W |= (1 << NAND_CLE_ADDRESS_LINE); break;
		case NAND_CTL_SETALE: IO_ADDR_W |= (1 << NAND_ALE_ADDRESS_LINE); break;
	}

	this->IO_ADDR_W = (void *) IO_ADDR_W;
}

static void stb225_nand_read_buf (struct mtd_info *mtd, u_char *buf, int len)
{
	struct nand_chip *this = mtd->priv;
	unsigned int pcp_ctl_orig = *((unsigned int *) 0xB7E09204);
	unsigned int pcp_ctl_new = (pcp_ctl_orig & ~(0x03 << 2));	/* clear bits 2..3, ie force pibus clock to (480 / 6) MHz */

	*((unsigned int *) 0xB7E09204) = pcp_ctl_new;
	nand_data_read ((unsigned long) this->IO_ADDR_R, buf, len);
	*((unsigned int *) 0xB7E09204) = pcp_ctl_orig;
}

static void stb225_nand_write_buf (struct mtd_info *mtd, const u_char *buf, int len)
{
	struct nand_chip *this = mtd->priv;
	unsigned int pcp_ctl_orig = *((unsigned int *) 0xB7E09204);
	unsigned int pcp_ctl_new = (pcp_ctl_orig & ~(0x03 << 2));	/* clear bits 2..3, ie force pibus clock to (480 / 6) MHz */

	*((unsigned int *) 0xB7E09204) = pcp_ctl_new;
	nand_data_write ((unsigned long) this->IO_ADDR_W, buf, len);
	*((unsigned int *) 0xB7E09204) = pcp_ctl_orig;
}

static int stb225_nand_wait (struct mtd_info *mtd, struct nand_chip *this, int state)
{
	unsigned long timeo, timeo_start;
	unsigned long hz = get_tbclk();

	timeo = (hz / 1000) * 20;	/* 20 msec */
	if (state == FL_ERASING)
		timeo *= 20;		/* 400 msec */

	this->cmdfunc (mtd, NAND_CMD_STATUS, -1, -1);

	timeo_start = get_timer (0);

	while (1) {
		if (get_timer (timeo_start) > timeo) {
			printf ("%s: Timeout (timeo ticks: %d)!", __FUNCTION__, timeo);
			return 0x01;
		}

		if (this->dev_ready) {
			if (this->dev_ready (mtd))
				break;
		} else {
			if (this->read_byte (mtd) & NAND_STATUS_READY)
				break;
		}
	}

	return this->read_byte (mtd);
}

#ifdef STB225_NAND_SUPPORTS_READY_SIGNAL
static int stb225_nand_ready (struct mtd_info *mtd)
{
	return gpio_get_value (GPIO_NAND_RDY);		/* return raw pin state, ie low == busy, high == ready */
}
#endif

int board_nand_init (struct nand_chip *nand)
{
	unsigned long base_address;
	char *ncs_mode_string = "?";
	int bootmode = stb225_bootmode();

	if ((bootmode == BOOTMODE_NAND_8) || (bootmode == BOOTMODE_NAND_16))
		base_address = MIU_CS0_BASE_UNCACHED;
	else
		base_address = MIU_CS2_BASE_UNCACHED;

	nand->IO_ADDR_R = nand->IO_ADDR_W = (void __iomem *) base_address;
	nand->eccmode = NAND_ECC_SOFT;
	nand->read_buf = stb225_nand_read_buf;
	nand->write_buf = stb225_nand_write_buf;
	nand->waitfunc = stb225_nand_wait;
	nand->chip_delay = 60;				/* typical values: SLC 25 usec, MLC 60 usec */
#ifdef STB225_NAND_SUPPORTS_READY_SIGNAL
	nand->dev_ready = stb225_nand_ready;
#endif

	if (CPU_IS_PNX8335_M0) {
		/*
		   We can't control nCS0 on M0 silicon... therefore nCS from MIU will
		   toggle. If NAND chip doesn't support CS toggling, then the NAND CS
		   line should be hardwired asserted (and MIU should not be used to
		   access any other devices !).
		*/
		nand->hwcontrol = stb225_nand_hwcontrol_ncs_ignore;
		ncs_mode_string = "toggle or external assert";
	}
	else {
#if defined (NAND_NCS_ALWAYS_TOGGLE)
		/* force for debug purposes only... */
		nand->hwcontrol = stb225_nand_hwcontrol_ncs_ignore;
		ncs_mode_string = "toggle";
#elif defined (NAND_NCS_ALWAYS_MTD_CONTROL)
		/* force for debug purposes only... */
		nand->hwcontrol = stb225_nand_hwcontrol_ncs_control;
		ncs_mode_string = "MTD control";
#elif defined (NAND_NCS_ALWAYS_FORCE)
		/* force for debug purposes only... */
		miu_ncs_assert (base_address);
		nand->hwcontrol = stb225_nand_hwcontrol_ncs_ignore;
		ncs_mode_string = "force asserted";
#else
		/*
		   On M1, we can either force nCS low all the time or allow NAND MTD
		   driver to control it. Force low has been used historically, but has
		   two disadvantages: the MIU can not be shared with other devices and
		   the NAND device does not enter low power state when idle. For now,
		   force low unless we booted from NOR (in which case we know another
		   device must be be present on MIU because we booted from it...).
		   Eventually we should probably just use MTD control in all cases if
		   M1 silicon is detected.
		*/
		if ((bootmode == BOOTMODE_NOR_8) || (bootmode == BOOTMODE_NOR_16)) {
			nand->hwcontrol = stb225_nand_hwcontrol_ncs_control;
			ncs_mode_string = "MTD control";
		}
		else {
			miu_ncs_assert (base_address);
			nand->hwcontrol = stb225_nand_hwcontrol_ncs_ignore;
			ncs_mode_string = "force asserted legacy mode";
		}
#endif
	}

	printf ("NAND: nCS%d (%s)\n", miu_address_to_cs (base_address), ncs_mode_string);

	return 0;
}

/*******************************************************************************
*******************************************************************************/

#if 1

#include <command.h>
#include <linux/mtd/nand_ecc.h>

static unsigned int ticks_delta_to_usec (unsigned int delta)
{
	return (unsigned int) ((((unsigned long long) delta) * 1000000) / get_tbclk());
}

static unsigned int ticks_delta_to_kbps (unsigned int delta, unsigned int bytes)
{
	return (unsigned int) ((((unsigned long long) bytes) * get_tbclk()) /
			       (((unsigned long long) delta) * 1024));
}

/*
   32 bit LFSR, maximal length with taps at bit positions 30, 26, 25 and 0.
   Depending on where you start counting bit positions from, the same LFSR
   can also be described as having taps at 1,5,6 and 31. That is the form
   given in the document at:

   http://archives.e-insite.net/archives/ednmag/reg/1996/010496/01df4.htm
*/
static unsigned int prbs_iterate32 (unsigned int state)
{
	int i;

	for (i = 0; i < 32; i++) {
		unsigned int newbit = (((state >> 30) ^ (state >> 26) ^ (state >> 25) ^ (state >> 0)) & 1);
		state = ((state >> 1) | (newbit << 31));
	}

	return state;
}

static int selftest_ecc (void)
{
	unsigned char buf_data[256 * 1024];
	unsigned char buf_ecc[3 * 1024];
	unsigned char ecc_calc[3 + 1];		/* + 1 to maintain stack alignment... */

	int i;
	unsigned int prbs_state;
	unsigned int start, stop;

	prbs_state = 0xFFFFFFFF;
	for (i = 0; i < sizeof(buf_data); i += 4) {
		prbs_state = prbs_iterate32 (prbs_state);
		buf_data[i+0] = (prbs_state >> 0) & 0xFF;
		buf_data[i+1] = (prbs_state >> 8) & 0xFF;
		buf_data[i+2] = (prbs_state >> 16) & 0xFF;
		buf_data[i+3] = (prbs_state >> 24) & 0xFF;
	}

	start = get_ticks();
	for (i = 0; i < sizeof(buf_data); i += 256) {
		nand_calculate_ecc (NULL, (const unsigned char *) &buf_data[i], &buf_ecc[(i / 256) * 3]);
	}
	stop = get_ticks();
	printf ("NAND ECC: Hamming ECC %s: %d kBytes in %5d usec (%5d kBytes/sec)\n", "generate",
		sizeof(buf_data) / 1024, ticks_delta_to_usec (stop - start),
		ticks_delta_to_kbps ((stop - start), sizeof(buf_data)));

	for (i = 0; i < sizeof(buf_data); i += 256) {
		unsigned int bit_offset = ((i / 256) % 8);
		unsigned int byte_offset = i + (((i / 256) / 8) % 256);
//		printf ("%6d: %6d (%4d * 256 + %3d), %d\n", i, byte_offset, i / 256, byte_offset - i, bit_offset);
		buf_data[byte_offset] ^= (1 << bit_offset);
	}

	start = get_ticks();
	for (i = 0; i < sizeof(buf_data); i += 256) {
		nand_calculate_ecc (NULL, (const unsigned char *) &buf_data[i], ecc_calc);
		if (nand_correct_data (NULL, (unsigned char *) &buf_data[i], &buf_ecc[(i / 256) * 3], ecc_calc) != 1) {
			printf ("NAND ECC: Expecting correctable error at buf block offset %d, aborting\n", i);
			break;
		}
	}
	stop = get_ticks();
	printf ("NAND ECC: Hamming ECC %s: %d kBytes in %5d usec (%5d kBytes/sec)\n", "correct ",
		sizeof(buf_data) / 1024, ticks_delta_to_usec (stop - start),
		ticks_delta_to_kbps ((stop - start), sizeof(buf_data)));

	prbs_state = 0xFFFFFFFF;
	for (i = 0; i < sizeof(buf_data); i += 4) {
		prbs_state = prbs_iterate32 (prbs_state);
		if ((buf_data[i+0] != ((prbs_state >> 0) & 0xFF)) ||
		    (buf_data[i+1] != ((prbs_state >> 8) & 0xFF)) ||
		    (buf_data[i+2] != ((prbs_state >> 16) & 0xFF)) ||
		    (buf_data[i+3] != ((prbs_state >> 24) & 0xFF)))
		{
			printf ("NAND ECC: Uncorrected error (i == %d), aborting\n", i);
			break;
		}
	}

	return 0;
}

U_BOOT_CMD(
	st_ecc, 1, 1, (int (*)(struct cmd_tbl_s *, int, int, char *[])) selftest_ecc,
	"st_ecc  - run NAND ECC selftest benchmark\n",
	" - run NAND ECC selftest benchmark\n"
);

#endif

/*******************************************************************************
*******************************************************************************/

#endif

