/*
 * This file is part of the coreboot project.
 *
 * Copyright (C) 2008 VIA Technologies, Inc.
 * (Written by Aaron Lwe <aaron.lwe@gmail.com> for VIA)
 * Copyright (C) 2007 Corey Osgood <corey.osgood@gmail.com>
 *
 * 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.
 */

#include <stdint.h>
#include <arch/io.h>
#include <northbridge/via/cn700/raminit.h>
#include <console/console.h>
#include <spd.h>
#include <delay.h>
#include "cn700.h"

#if IS_ENABLED(CONFIG_DEBUG_RAM_SETUP)
#define PRINT_DEBUG_MEM(x)		printk(BIOS_DEBUG, x)
#define PRINT_DEBUG_MEM_HEX8(x)		printk(BIOS_DEBUG, "%02x", x)
#define PRINT_DEBUG_MEM_HEX16(x)	printk(BIOS_DEBUG, "%04x", x)
#define PRINT_DEBUG_MEM_HEX32(x)	printk(BIOS_DEBUG, "%08x", x)
#define DUMPNORTH()			dump_pci_device(PCI_DEV(0, 0, 0))
#else
#define PRINT_DEBUG_MEM(x)
#define PRINT_DEBUG_MEM_HEX8(x)
#define PRINT_DEBUG_MEM_HEX16(x)
#define PRINT_DEBUG_MEM_HEX32(x)
#define DUMPNORTH()
#endif

static void do_ram_command(pci_devfn_t dev, u8 command)
{
	u8 reg;

	/* TODO: Support for multiple DIMMs. */

	reg = pci_read_config8(dev, DRAM_MISC_CTL);
	reg &= 0xf8;		/* Clear bits 2-0. */
	reg |= command;
	pci_write_config8(dev, DRAM_MISC_CTL, reg);
}

static void c7_pci_write_config8(pci_devfn_t dev, u8 reg, u8 val)
{
	udelay(200);
	pci_write_config8(dev, reg, val);
}

/**
 * Configure the bus between the CPU and the northbridge. This might be able to
 * be moved to post-ram code in the future. For the most part, these registers
 * should not be messed around with. These are too complex to explain short of
 * copying the datasheets into the comments, but most of these values are from
 * the BIOS Porting Guide, so they should work on any board. If they don't,
 * try the values from your factory BIOS.
 *
 * TODO: Changing the DRAM frequency doesn't work (hard lockup).
 *
 * @param dev The northbridge's CPU Host Interface (D0F2).
 */
static void c7_cpu_setup(pci_devfn_t dev)
{
	/* Host bus interface registers (D0F2 0x50-0x67) */
	/* Request phase control */
	c7_pci_write_config8(dev, 0x50, 0x88);
	/* CPU Interface Control */
	c7_pci_write_config8(dev, 0x51, 0x7a);
	c7_pci_write_config8(dev, 0x52, 0x6f);
	/* Arbitration */
	c7_pci_write_config8(dev, 0x53, 0x88);
	/* Miscellaneous Control */
	c7_pci_write_config8(dev, 0x54, 0x1e);
	c7_pci_write_config8(dev, 0x55, 0x16);
	/* Write Policy */
	c7_pci_write_config8(dev, 0x56, 0x01);
	/* Miscellaneous Control */
	/*
	 * DRAM Operating Frequency (bits 7:5)
	 *      000 : 100MHz    001 : 133MHz
	 *      010 : 166MHz    011 : 200MHz
	 *      100 : 266MHz    101 : 333MHz
	 *      110/111 : Reserved
	 */
	/* CPU Miscellaneous Control */
	c7_pci_write_config8(dev, 0x59, 0x44);

	/* Write Policy */
	c7_pci_write_config8(dev, 0x5d, 0xb2);
	/* Bandwidth Timer */
	c7_pci_write_config8(dev, 0x5e, 0x88);
	/* CPU Miscellaneous Control */
	c7_pci_write_config8(dev, 0x5f, 0xc7);

	/* Line DRDY# Timing Control */
	c7_pci_write_config8(dev, 0x60, 0xff);
	c7_pci_write_config8(dev, 0x61, 0xff);
	c7_pci_write_config8(dev, 0x62, 0x0f);
	/* QW DRDY# Timing Control */
	c7_pci_write_config8(dev, 0x63, 0xff);
	c7_pci_write_config8(dev, 0x64, 0xff);
	c7_pci_write_config8(dev, 0x65, 0x0f);
	/* Read Line Burst DRDY# Timing Control */
	c7_pci_write_config8(dev, 0x66, 0xff);
	c7_pci_write_config8(dev, 0x67, 0x30);

	/* Host Bus I/O Circuit (see datasheet) */
	/* Host Address Pullup/down Driving */
	c7_pci_write_config8(dev, 0x70, 0x11);
	c7_pci_write_config8(dev, 0x71, 0x11);
	c7_pci_write_config8(dev, 0x72, 0x11);
	c7_pci_write_config8(dev, 0x73, 0x11);
	/* Miscellaneous Control */
	c7_pci_write_config8(dev, 0x74, 0x35);
	/* AGTL+ I/O Circuit */
	c7_pci_write_config8(dev, 0x75, 0x28);
	/* AGTL+ Compensation Status */
	c7_pci_write_config8(dev, 0x76, 0x74);
	/* AGTL+ Auto Compensation Offest */
	c7_pci_write_config8(dev, 0x77, 0x00);
	/* Host FSB CKG Control */
	c7_pci_write_config8(dev, 0x78, 0x0a);
	/* Address/Address Clock Output Delay Control */
	c7_pci_write_config8(dev, 0x79, 0xaa);
	/* Address Strobe Input Delay Control */
	c7_pci_write_config8(dev, 0x7a, 0x24);
	/* Address CKG Rising/Falling Time Control */
	c7_pci_write_config8(dev, 0x7b, 0xaa);
	/* Address CKG Clock Rising/Falling Time Control */
	c7_pci_write_config8(dev, 0x7c, 0x00);
	/* Undefined (can't remember why I did this) */
	c7_pci_write_config8(dev, 0x7d, 0x6d);
	c7_pci_write_config8(dev, 0x7e, 0x00);
	c7_pci_write_config8(dev, 0x7f, 0x00);
	c7_pci_write_config8(dev, 0x80, 0x1b);
	c7_pci_write_config8(dev, 0x81, 0x0a);
	c7_pci_write_config8(dev, 0x82, 0x0a);
	c7_pci_write_config8(dev, 0x83, 0x0a);
}

/**
 * Set up DRAM size according to SPD data. Eventually, DRAM timings should be
 * done in a similar manner.
 *
 * @param ctrl The northbridge devices and SPD addresses.
 */
static void sdram_set_size(const struct mem_controller *ctrl)
{
	u8 density, ranks, result, col;

	ranks = spd_read_byte(ctrl->channel0[0], SPD_NUM_DIMM_BANKS);
	ranks = (ranks & 0x07) + 1;
	density = spd_read_byte(ctrl->channel0[0],
				SPD_DENSITY_OF_EACH_ROW_ON_MODULE);
	switch (density) {
	case 0x80:
		result = 0x08;	/* 512MB / 64MB = 0x08 */
		break;
	case 0x40:
		result = 0x04;
		break;
	case 0x20:
		result = 0x02;
		break;
	case 0x10:
		result = 0xff;	/* 16GB */
		break;
	case 0x08:
		result = 0xff;	/* 8GB */
		break;
	case 0x04:
		result = 0xff;	/* 4GB */
		break;
	case 0x02:
		result = 0x20;	/* 2GB */
		break;
	case 0x01:
		result = 0x10;	/* 1GB */
		break;
	default:
		result = 0;
	}

	switch (result) {
	case 0xff:
		die("DRAM module size too big, not supported by CN700\n");
		break;
	case 0:
		die("DRAM module has unknown density\n");
		break;
	default:
		printk(BIOS_DEBUG, "Found %iMB of ram\n", result * ranks * 64);
	}

	pci_write_config8(ctrl->d0f3, 0x40, result);
	pci_write_config8(ctrl->d0f3, 0x48, 0x00);
	if (ranks == 2) {
		pci_write_config8(ctrl->d0f3, 0x41, result * ranks);
		pci_write_config8(ctrl->d0f3, 0x49, result);
	}
	/* Size mirror */
	pci_write_config8(ctrl->d0f7, 0xe5, (result * ranks) << 2);
	pci_write_config8(ctrl->d0f7, 0x57, (result * ranks) << 2);
	/* Low Top Address */
	pci_write_config8(ctrl->d0f3, 0x84, 0x00);
	pci_write_config8(ctrl->d0f3, 0x85, (result * ranks) << 2);
	pci_write_config8(ctrl->d0f3, 0x88, (result * ranks) << 2);

	/* Physical-Virtual Mapping */
	if (ranks == 2)
		pci_write_config8(ctrl->d0f3, 0x54,
				  1 << 7 | 0 << 4 | 1 << 3 | 1 << 0);
	if (ranks == 1)
		pci_write_config8(ctrl->d0f3, 0x54, 1 << 7 | 0 << 4);
	pci_write_config8(ctrl->d0f3, 0x55, 0x00);
	/* Virtual rank interleave, disable */
	pci_write_config32(ctrl->d0f3, 0x58, 0x00);

	/* MA Map Type */
	result = spd_read_byte(ctrl->channel0[0], SPD_NUM_BANKS_PER_SDRAM);
	if (result == 8) {
		col = spd_read_byte(ctrl->channel0[0], SPD_NUM_COLUMNS);
		switch (col) {
		case 10:
			pci_write_config8(ctrl->d0f3, 0x50, 0xa0);
			break;
		case 11:
			pci_write_config8(ctrl->d0f3, 0x50, 0xc0);
			break;
		case 12:
			pci_write_config8(ctrl->d0f3, 0x50, 0xe0);
			break;
		}
	} else if (result == 4) {
		col = spd_read_byte(ctrl->channel0[0], SPD_NUM_COLUMNS);
		switch (col) {
		case 9:
			pci_write_config8(ctrl->d0f3, 0x50, 0x00);
			break;
		case 10:
			pci_write_config8(ctrl->d0f3, 0x50, 0x20);
			break;
		case 11:
			pci_write_config8(ctrl->d0f3, 0x50, 0x40);
			break;
		case 12:
			pci_write_config8(ctrl->d0f3, 0x50, 0x60);
			break;
		}
	}
	pci_write_config8(ctrl->d0f3, 0x51, 0x00);
}

/**
 * Set up various RAM and other control registers statically. Some of these may
 * not be needed, other should be done with SPD info, but that's a project for
 * the future.
 */
static void sdram_set_registers(const struct mem_controller *ctrl)
{
	u8 reg;

	/* Set WR = 5 */
	pci_write_config8(ctrl->d0f3, 0x61, 0xe0);
	/* Set CAS = 4 */
	pci_write_config8(ctrl->d0f3, 0x62, 0xfa);
	/* DRAM timing-3 */
	pci_write_config8(ctrl->d0f3, 0x63, 0xca);
	/* DRAM timing-4 */
	pci_write_config8(ctrl->d0f3, 0x64, 0xcc);
	/* DIMM command / Address Selection */
	pci_write_config8(ctrl->d0f3, 0x67, 0x00);
	/* Disable cross bank/multi page mode */
	pci_write_config8(ctrl->d0f3, 0x69, 0x00);
	/* Disable refresh now */
	pci_write_config8(ctrl->d0f3, 0x6a, 0x00);

	/* Frequency 100 MHz */
	pci_write_config8(ctrl->d0f3, 0x90, 0x00);
	pci_write_config8(ctrl->d0f2, 0x57, 0x18);
	/* Allow manual DLL reset */
	pci_write_config8(ctrl->d0f3, 0x6b, 0x10);

	/* Bank/Rank Interleave Address Select */
	pci_write_config8(ctrl->d0f3, 0x52, 0x33);
	pci_write_config8(ctrl->d0f3, 0x53, 0x3f);

	/* Set to DDR2 SDRAM, BL = 8 (0xc8, 0xc0 for bl = 4) */
	pci_write_config8(ctrl->d0f3, 0x6c, 0xc8);

	/* DRAM Bus Turn-Around Setting */
	pci_write_config8(ctrl->d0f3, 0x60, 0x03);
	/* DRAM Arbitration Control */
	pci_write_config8(ctrl->d0f3, 0x66, 0x80);
	/*
	 * DQS Tuning: testing on a couple different boards has shown this is
	 * static, or close enough that it can be. Which is good, because the
	 * tuning function used too many registers.
	 */
	/* DQS Output Delay for Channel A */
	pci_write_config8(ctrl->d0f3, 0x70, 0x00);
	/* MD Output Delay for Channel A */
	pci_write_config8(ctrl->d0f3, 0x71, 0x01);
	pci_write_config8(ctrl->d0f3, 0x73, 0x01);

	/* DRAM arbitration timer */
	pci_write_config8(ctrl->d0f3, 0x65, 0xd9);

	/* DRAM signal timing control */
	pci_write_config8(ctrl->d0f3, 0x74, 0x01);
	pci_write_config8(ctrl->d0f3, 0x75, 0x01);
	pci_write_config8(ctrl->d0f3, 0x76, 0x06);
	pci_write_config8(ctrl->d0f3, 0x77, 0x92);
	pci_write_config8(ctrl->d0f3, 0x78, 0x83);
	pci_write_config8(ctrl->d0f3, 0x79, 0x83);
	pci_write_config8(ctrl->d0f3, 0x7a, 0x00);
	pci_write_config8(ctrl->d0f3, 0x7b, 0x10);

	/* DRAM clocking control */
	pci_write_config8(ctrl->d0f3, 0x91, 0x01);
	/* CS/CKE Clock Phase Control */
	pci_write_config8(ctrl->d0f3, 0x92, 0x02);
	/* SCMD/MA Clock Phase Control */
	pci_write_config8(ctrl->d0f3, 0x93, 0x02);
	/* DCLKO Feedback Mode Output Control */
	pci_write_config8(ctrl->d0f3, 0x94, 0x00);
	pci_write_config8(ctrl->d0f3, 0x9d, 0x0f);

	/* SDRAM ODT Control */
	pci_write_config8(ctrl->d0f3, 0xda, 0x80);
	/* Channel A DQ/DQS CKG Output Delay Control */
	pci_write_config8(ctrl->d0f3, 0xdc, 0x54);
	/* Channel A DQ/DQS CKG Output Delay Control */
	pci_write_config8(ctrl->d0f3, 0xdd, 0x55);
	/* ODT lookup table */
	pci_write_config8(ctrl->d0f3, 0xd8, 0x01);
	pci_write_config8(ctrl->d0f3, 0xd9, 0x0a);

	/* DDR SDRAM control */
	pci_write_config8(ctrl->d0f3, 0x6d, 0xc0);
	pci_write_config8(ctrl->d0f3, 0x6f, 0x41);

	/* DQ/DQS Strength Control */
	pci_write_config8(ctrl->d0f3, 0xd0, 0xaa);

	/* Compensation Control */
	pci_write_config8(ctrl->d0f3, 0xd3, 0x01); /* Enable autocompensation */
	/* ODT (some are set with driving select above) */
	pci_write_config8(ctrl->d0f3, 0xd4, 0x80);
	pci_write_config8(ctrl->d0f3, 0xd5, 0x8a);
	/* Memory Pads Driving and Range Select */
	pci_write_config8(ctrl->d0f3, 0xd6, 0xaa);

	pci_write_config8(ctrl->d0f3, 0xe0, 0xee);
	pci_write_config8(ctrl->d0f3, 0xe2, 0xac);
	pci_write_config8(ctrl->d0f3, 0xe4, 0x66);
	pci_write_config8(ctrl->d0f3, 0xe6, 0x33);
	pci_write_config8(ctrl->d0f3, 0xe8, 0x86);
	/* DQS / DQ CKG Duty Cycle Control */
	pci_write_config8(ctrl->d0f3, 0xec, 0x00);
	/* MCLK Output Duty Control */
	pci_write_config8(ctrl->d0f3, 0xee, 0x00);
	/* DQS CKG Input Delay Control */
	pci_write_config8(ctrl->d0f3, 0xef, 0x10);

	/* DRAM duty control */
	pci_write_config8(ctrl->d0f3, 0xed, 0x10);

	/* SMM and APIC decoding, we do not use SMM */
	reg = 0x29;
	pci_write_config8(ctrl->d0f3, 0x86, reg);
	/* SMM and APIC decoding mirror */
	pci_write_config8(ctrl->d0f7, 0xe6, reg);

	/* DRAM module configuration */
	pci_write_config8(ctrl->d0f3, 0x6e, 0x89);
}

static void sdram_set_post(const struct mem_controller *ctrl)
{
	pci_devfn_t dev = ctrl->d0f3;

	/* Enable multipage mode. */
	pci_write_config8(dev, 0x69, 0x03);

	/* Enable refresh. */
	pci_write_config8(dev, 0x6a, 0x32);

	/* VGA device. */
	pci_write_config16(dev, 0xa0, (1 << 15));
	pci_write_config16(dev, 0xa4, 0x0010);
}

static void sdram_enable(pci_devfn_t dev, u8 *rank_address)
{
	u8 i;

	/* 1. Apply NOP. */
	PRINT_DEBUG_MEM("RAM Enable 1: Apply NOP\n");
	do_ram_command(dev, RAM_COMMAND_NOP);
	udelay(100);
	read32(rank_address + 0x10);

	/* 2. Precharge all. */
	udelay(400);
	PRINT_DEBUG_MEM("RAM Enable 2: Precharge all\n");
	do_ram_command(dev, RAM_COMMAND_PRECHARGE);
	read32(rank_address + 0x10);

	/* 3. Mode register set. */
	PRINT_DEBUG_MEM("RAM Enable 3: Mode register set\n");
	do_ram_command(dev, RAM_COMMAND_MRS);
	read32(rank_address + 0x120000);	/* EMRS DLL Enable */
	read32(rank_address + 0x800);	/* MRS DLL Reset */

	/* 4. Precharge all again. */
	PRINT_DEBUG_MEM("RAM Enable 4: Precharge all\n");
	do_ram_command(dev, RAM_COMMAND_PRECHARGE);
	read32(rank_address + 0x0);

	/* 5. Perform 8 refresh cycles. Wait tRC each time. */
	PRINT_DEBUG_MEM("RAM Enable 5: CBR\n");
	do_ram_command(dev, RAM_COMMAND_CBR);
	for (i = 0; i < 8; i++) {
		read32(rank_address + 0x20);
		udelay(100);
	}

	/* 6. Mode register set. */
	PRINT_DEBUG_MEM("RAM Enable 6: Mode register set\n");
	/* Safe value for now, BL = 8, WR = 5, CAS = 4 */
	/*
	 * (E)MRS values are from the BPG. No direct explanation is given, but
	 * they should somehow conform to the JEDEC DDR2 SDRAM Specification
	 * (JESD79-2C).
	 */
	do_ram_command(dev, RAM_COMMAND_MRS);
	read32(rank_address + 0x002258); /* MRS command */
	read32(rank_address + 0x121c20); /* EMRS OCD Default */
	read32(rank_address + 0x120020); /* EMRS OCD Calibration Mode Exit */

	/* 8. Normal operation */
	PRINT_DEBUG_MEM("RAM Enable 7: Normal operation\n");
	do_ram_command(dev, RAM_COMMAND_NORMAL);
	read32(rank_address + 0x30);
}

/*
 * Support one DIMM with up to 2 ranks.
 */
void ddr_ram_setup(const struct mem_controller *ctrl)
{
	u8 reg;

	c7_cpu_setup(ctrl->d0f2);
	sdram_set_registers(ctrl);
	sdram_set_size(ctrl);
	sdram_enable(ctrl->d0f3, (u8 *)0);
	reg = pci_read_config8(ctrl->d0f3, 0x41);
	if (reg != 0)
		sdram_enable(ctrl->d0f3,
			     (u8 *)(pci_read_config8(ctrl->d0f3, 0x40) << 26));
	sdram_set_post(ctrl);
}
