/* ----------------------------------------------------------------------------
 *         SAM Software Package License
 * ----------------------------------------------------------------------------
 * Copyright (c) 2015, Atmel Corporation
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the disclaimer below.
 *
 * Atmel's name may not be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * DISCLAIMER: THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE
 * DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ----------------------------------------------------------------------------
 */

#include "chip.h"
#include "peripherals/mpddrc.h"
#include "peripherals/sfrbu.h"
#include "peripherals/pmc.h"
#include "trace.h"
#include "timer.h"
#include "compiler.h"
#include <stdlib.h>

static void _set_ddr_timings(struct _mpddrc_desc* desc)
{
	MPDDRC->MPDDRC_TPR0 = desc->tpr0;
	MPDDRC->MPDDRC_TPR1 = desc->tpr1;
	MPDDRC->MPDDRC_TPR2 = desc->tpr2;
}

static uint32_t _compute_ba_offset(void)
{
	/* Compute BA[] offset according to CR configuration */
	uint32_t offset = (MPDDRC->MPDDRC_CR & MPDDRC_CR_NC_Msk) + 9;
	if (!(MPDDRC->MPDDRC_CR & MPDDRC_CR_DECOD_INTERLEAVED))
		offset += ((MPDDRC->MPDDRC_CR & MPDDRC_CR_NR_Msk) >> 2) + 11;

	offset += (MPDDRC->MPDDRC_MD & MPDDRC_MD_DBW) ? 1 : 2;

	return offset;
}

/**
 * \brief Send a NOP command
 */
static void _send_nop_cmd(void)
{
	MPDDRC->MPDDRC_MR = MPDDRC_MR_MODE_NOP_CMD;
	/* Perform a write to a DDR memory access to acknoledge the command */
	*((uint32_t *)DDR_CS_ADDR) = 0;
}

static void _send_lmr_cmd(void)
{
	MPDDRC->MPDDRC_MR = MPDDRC_MR_MODE_LMR_CMD;
	/* Perform a write to a DDR memory access to acknoledge the command */
	*((uint32_t *)DDR_CS_ADDR) = 0u;
}

static void _send_ext_lmr_cmd(uint32_t opcode, uint32_t ba_offset)
{
	MPDDRC->MPDDRC_MR = MPDDRC_MR_MODE_EXT_LMR_CMD;
	/* Perform a write to a DDR memory access to acknoledge the command */
	*((uint32_t *)(DDR_CS_ADDR + (opcode << ba_offset))) = 0u;
}

static void _send_normal_cmd(void)
{
	MPDDRC->MPDDRC_MR = MPDDRC_MR_MODE_NORMAL_CMD;
	/* Perform a write to a DDR memory access to acknoledge the command */
	*((uint32_t *)DDR_CS_ADDR) = 0;
}

static void _send_precharge_cmd(void)
{
	MPDDRC->MPDDRC_MR = MPDDRC_MR_MODE_PRCGALL_CMD;
	/* Perform a write to a DDR memory access to acknoledge the command */
	*((uint32_t *)DDR_CS_ADDR) = 0;
}

static void _send_refresh_cmd(void)
{
	MPDDRC->MPDDRC_MR = MPDDRC_MR_MODE_RFSH_CMD;
	/* Perform a write to a DDR memory access to acknoledge the command */
	*((uint32_t *)DDR_CS_ADDR) = 0;
}

#ifdef CONFIG_HAVE_DDR3

static void _send_calib_cmd(void)
{
	MPDDRC->MPDDRC_MR = MPDDRC_MR_MODE_DEEP_CALIB_MD;
	/* Perform a write to a DDR memory access to acknoledge the command */
	*((uint32_t *)DDR_CS_ADDR) = 0;
}

static void _configure_ddr3(struct _mpddrc_desc* desc)
{
	/* Timings */
	_set_ddr_timings(desc);
	uint32_t ba_offset = _compute_ba_offset();

	/*
	 * Step 3: Issue a NOP command to the memory controller using
	 * its mode register (MPDDRC_MR).
	 */
	_send_nop_cmd();

	/*
	 * Step 4: A pause of at least 500us must be observed before a
	 * single toggle.
	 */
	timer_sleep(50);
	/*
	 * Step 5: Issue a NOP command to the memory controller using
	 * its mode register (MPDDRC_MR). CKE is now driven high.
	 */
	_send_nop_cmd();
	timer_sleep(1);
	/*
	 * Step 6: Issue Extended Mode Register Set 2 (EMRS2) cycle to
	 * choose between commercial or high temperature
	 * operations.
	 */
	_send_ext_lmr_cmd(0x2, ba_offset);
	timer_sleep(1);
	/*
	 * Step 7: Issue Extended Mode Register Set 3 (EMRS3) cycle to set
	 * the Extended Mode Register to 0.
	 */
	_send_ext_lmr_cmd(0x3, ba_offset);
	timer_sleep(1);
	/*
	 * Step 8: Issue Extended Mode Register Set 1 (EMRS1) cycle to
	 * disable and to program O.D.S. (Output Driver Strength).
	 */
	_send_ext_lmr_cmd(0x1, ba_offset);
	timer_sleep(1);
	/*
	 * Step 9: Write a one to the DLL bit (enable DLL reset) in the MPDDRC
	 * Configuration Register (MPDDRC_CR)
	 */
	/* Not done for DDR3 */

	/*
	 * Step 10: Issue a Mode Register Set (MRS) cycle to reset DLL.
	 */
	_send_lmr_cmd();
	timer_sleep(5);
	/*
	 * Step 11: Issue a Calibration command (MRS) cycle to calibrate RTT and
	 * RON values for the Process Voltage Temperature (PVT).
	 */
	_send_calib_cmd();
	timer_sleep(1);

	/*
	 * Step 12: A Normal Mode command is provided.
	 * Program the Normal mode in the MPDDRC_MR and perform a write access
	 * to any DDR3-SDRAM address to acknowledge this command.
	 */
	_send_normal_cmd();
	/*
	 * Step 13: Perform a write access to any DDR3-SDRAM address.
	 */
	*((uint32_t *)(DDR_CS_ADDR)) = 0;
}

#endif

static void _configure_ddr2(struct _mpddrc_desc* desc)
{
	/* Timings */
	_set_ddr_timings(desc);
	uint32_t ba_offset = _compute_ba_offset();

	/* Step 3: An NOP command is issued to the DDR2-SDRAM. Program
	 * the NOP command into the Mode Register and wait minimum 200
	 * us */
	_send_nop_cmd();
	timer_sleep(20);

	/* Step 4:  Issue a NOP command. */
	_send_nop_cmd();
	timer_sleep(1);

	/* Step 5: Issue all banks precharge command. */
	_send_precharge_cmd();
	timer_sleep(1);

	/* Step 6: Issue an Extended Mode Register set (EMRS2) cycle
	 * to chose between commercialor high  temperature
	 * operations. */
	_send_ext_lmr_cmd(0x2, ba_offset);
	timer_sleep(1);

	/* Step 7: Issue an Extended Mode Register set (EMRS3) cycle
	 * to set all registers to 0. */
	_send_ext_lmr_cmd(0x3, ba_offset);
	timer_sleep(1);

	/* Step 8:  Issue an Extended Mode Register set (EMRS1) cycle
	 * to enable DLL. */
	_send_ext_lmr_cmd(0x1, ba_offset);
	timer_sleep(1);

	/* Step 9:  Program DLL field into the Configuration Register. */
	MPDDRC->MPDDRC_CR |= MPDDRC_CR_DLL_RESET_ENABLED;

	/* Step 10: A Mode Register set (MRS) cycle is issued to reset DLL. */
	MPDDRC->MPDDRC_MR = MPDDRC_MR_MODE_LMR_CMD;
	/* Perform a write to a DDR memory access to acknoledge the command */
	*((uint32_t *)DDR_CS_ADDR) = 0;

	timer_sleep(1);

	/* Step 11: Issue all banks precharge command to the DDR2-SDRAM. */
	_send_precharge_cmd();
	timer_sleep(1);

	/* Step 12: Two auto-refresh (CBR) cycles are
	 * provided. Program the auto refresh command (CBR) into the
	 * Mode Register. */
	_send_refresh_cmd();
	timer_sleep(1);
	_send_refresh_cmd();
	timer_sleep(1);

	/* Step 13: Program DLL field into the Configuration Register
	 * to low(Disable DLL reset). */
	MPDDRC->MPDDRC_CR &= ~MPDDRC_CR_DLL_RESET_ENABLED;

	/* Step 14: Issue a Mode Register set (MRS) cycle to program
	 * the parameters of the DDR2-SDRAM devices. */
	_send_lmr_cmd();
	timer_sleep(1);

	/* Step 15: Program OCD field into the Configuration Register
	 * to high (OCD calibration default). */
	MPDDRC->MPDDRC_CR |= MPDDRC_CR_OCD_DDR2_DEFAULT_CALIB;

	/* Step 16: An Extended Mode Register set (EMRS1) cycle is
	 * issued to OCD default value. */
	_send_ext_lmr_cmd(0x1, ba_offset);
	timer_sleep(1);

	/* Step 19,20: A mode Normal command is provided. Program the
	 * Normal mode into Mode Register. */
	_send_normal_cmd();
	timer_sleep(1);
}

extern void mpddrc_configure(struct _mpddrc_desc* desc)
{
	/* Retrieve the current resolution to put it back later */
	uint32_t resolution = timer_get_resolution();
	/* Configure time to have 10 microseconds resolution */
	timer_configure(10);

	/* controller and DDR clock */
	pmc_enable_peripheral(ID_MPDDRC);
	pmc_enable_system_clock(PMC_SYSTEM_CLOCK_DDR);

	/* Step1: Program memory device type */
	MPDDRC->MPDDRC_MD = desc->mode;

	/* set driver impedance */
	uint32_t value = MPDDRC->MPDDRC_IO_CALIBR;
	value &= ~MPDDRC_IO_CALIBR_RDIV_Msk;
	value &= ~MPDDRC_IO_CALIBR_TZQIO_Msk;
	value &= ~MPDDRC_IO_CALIBR_CALCODEP_Msk;
	value &= ~MPDDRC_IO_CALIBR_CALCODEN_Msk;
	value |= desc->io_calibr;
	MPDDRC->MPDDRC_IO_CALIBR = value;

	MPDDRC->MPDDRC_RD_DATA_PATH = desc->data_path;

	/* Step 2: Program features of the DDR3-SDRAM device in the
	 * configuration register and timing parameter registers (TPR0
	 * ans TPR1) */

	/* Configurations */
	MPDDRC->MPDDRC_CR = desc->control;

#ifdef CONFIG_HAVE_DDR3_SELFREFRESH
	if (sfrbu_is_ddr_backup_enabled())
		/* DDR memory had been initilized and in backup mode */
		MPDDRC->MPDDRC_LPR =
			MPDDRC_LPR_LPCB_SELFREFRESH |
			MPDDRC_LPR_CLK_FR_ENABLED |
			MPDDRC_LPR_PASR(0) |
			MPDDRC_LPR_DS(2) |
			MPDDRC_LPR_TIMEOUT_NONE |
			MPDDRC_LPR_APDE_DDR2_FAST_EXIT |
			MPDDRC_LPR_UPD_MR(0);
	else
		/* DDR memory is not in backup mode */
		MPDDRC->MPDDRC_LPR =
			MPDDRC_LPR_LPCB_SELFREFRESH |
			MPDDRC_LPR_CLK_FR_ENABLED |
			MPDDRC_LPR_PASR(0) |
			MPDDRC_LPR_DS(2) |
			MPDDRC_LPR_TIMEOUT_DELAY_128_CLK |
			MPDDRC_LPR_APDE_DDR2_SLOW_EXIT |
			MPDDRC_LPR_UPD_MR(0);
#endif

	switch(desc->type) {
#ifdef CONFIG_HAVE_DDR3
	case MPDDRC_TYPE_DDR3:
#ifdef CONFIG_HAVE_DDR3_SELFREFRESH
		_set_ddr_timings(desc);
		/* Initialize DDR chip when needed */
		if (!sfrbu_is_ddr_backup_enabled())
#endif
		_configure_ddr3(desc);
		break;
#endif
	case MPDDRC_TYPE_DDR2:
		_configure_ddr2(desc);
		break;
	default:
		trace_error("Device not handled\r\n");
		abort();
	}

	/* Last step: Write the refresh rate */
	/* Refresh Timer is (64ms / (bank_size)) * master_clock */
	uint32_t master_clock = pmc_get_master_clock()/1000000;
	MPDDRC->MPDDRC_RTR = MPDDRC_RTR_COUNT(64000*master_clock/desc->bank);

	/* wait for end of calibration */
	timer_sleep(1);

	/* Restore resolution or put the default one if not already set */
	timer_configure(resolution);

#ifdef CONFIG_HAVE_DDR3_SELFREFRESH
	if (sfrbu_is_ddr_backup_enabled()) {
		MPDDRC->MPDDRC_MR = MPDDRC_MR_MODE_NORMAL_CMD;
		sfrbu_disable_ddr_backup();
	}
#endif
}
