/*
 * SCOM support for A2 platforms
 *
 * Copyright 2007-2011 Benjamin Herrenschmidt, David Gibson,
 *		       Michael Ellerman, IBM Corp.
 *
 * 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.
 */

#include <linux/cpumask.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/spinlock.h>
#include <linux/types.h>

#include <asm/cputhreads.h>
#include <asm/reg_a2.h>
#include <asm/scom.h>
#include <asm/udbg.h>

#include "wsp.h"

#define SCOM_RAMC		0x2a		/* Ram Command */
#define SCOM_RAMC_TGT1_EXT	0x80000000
#define SCOM_RAMC_SRC1_EXT	0x40000000
#define SCOM_RAMC_SRC2_EXT	0x20000000
#define SCOM_RAMC_SRC3_EXT	0x10000000
#define SCOM_RAMC_ENABLE	0x00080000
#define SCOM_RAMC_THREADSEL	0x00060000
#define SCOM_RAMC_EXECUTE	0x00010000
#define SCOM_RAMC_MSR_OVERRIDE	0x00008000
#define SCOM_RAMC_MSR_PR	0x00004000
#define SCOM_RAMC_MSR_GS	0x00002000
#define SCOM_RAMC_FORCE		0x00001000
#define SCOM_RAMC_FLUSH		0x00000800
#define SCOM_RAMC_INTERRUPT	0x00000004
#define SCOM_RAMC_ERROR		0x00000002
#define SCOM_RAMC_DONE		0x00000001
#define SCOM_RAMI		0x29		/* Ram Instruction */
#define SCOM_RAMIC		0x28		/* Ram Instruction and Command */
#define SCOM_RAMIC_INSN		0xffffffff00000000
#define SCOM_RAMD		0x2d		/* Ram Data */
#define SCOM_RAMDH		0x2e		/* Ram Data High */
#define SCOM_RAMDL		0x2f		/* Ram Data Low */
#define SCOM_PCCR0		0x33		/* PC Configuration Register 0 */
#define SCOM_PCCR0_ENABLE_DEBUG	0x80000000
#define SCOM_PCCR0_ENABLE_RAM	0x40000000
#define SCOM_THRCTL		0x30		/* Thread Control and Status */
#define SCOM_THRCTL_T0_STOP	0x80000000
#define SCOM_THRCTL_T1_STOP	0x40000000
#define SCOM_THRCTL_T2_STOP	0x20000000
#define SCOM_THRCTL_T3_STOP	0x10000000
#define SCOM_THRCTL_T0_STEP	0x08000000
#define SCOM_THRCTL_T1_STEP	0x04000000
#define SCOM_THRCTL_T2_STEP	0x02000000
#define SCOM_THRCTL_T3_STEP	0x01000000
#define SCOM_THRCTL_T0_RUN	0x00800000
#define SCOM_THRCTL_T1_RUN	0x00400000
#define SCOM_THRCTL_T2_RUN	0x00200000
#define SCOM_THRCTL_T3_RUN	0x00100000
#define SCOM_THRCTL_T0_PM	0x00080000
#define SCOM_THRCTL_T1_PM	0x00040000
#define SCOM_THRCTL_T2_PM	0x00020000
#define SCOM_THRCTL_T3_PM	0x00010000
#define SCOM_THRCTL_T0_UDE	0x00008000
#define SCOM_THRCTL_T1_UDE	0x00004000
#define SCOM_THRCTL_T2_UDE	0x00002000
#define SCOM_THRCTL_T3_UDE	0x00001000
#define SCOM_THRCTL_ASYNC_DIS	0x00000800
#define SCOM_THRCTL_TB_DIS	0x00000400
#define SCOM_THRCTL_DEC_DIS	0x00000200
#define SCOM_THRCTL_AND		0x31		/* Thread Control and Status */
#define SCOM_THRCTL_OR		0x32		/* Thread Control and Status */


static DEFINE_PER_CPU(scom_map_t, scom_ptrs);

static scom_map_t get_scom(int cpu, struct device_node *np, int *first_thread)
{
	scom_map_t scom = per_cpu(scom_ptrs, cpu);
	int tcpu;

	if (scom_map_ok(scom)) {
		*first_thread = 0;
		return scom;
	}

	*first_thread = 1;

	scom = scom_map_device(np, 0);

	for (tcpu = cpu_first_thread_sibling(cpu);
	     tcpu <= cpu_last_thread_sibling(cpu); tcpu++)
		per_cpu(scom_ptrs, tcpu) = scom;

	/* Hack: for the boot core, this will actually get called on
	 * the second thread up, not the first so our test above will
	 * set first_thread incorrectly. */
	if (cpu_first_thread_sibling(cpu) == 0)
		*first_thread = 0;

	return scom;
}

static int a2_scom_ram(scom_map_t scom, int thread, u32 insn, int extmask)
{
	u64 cmd, mask, val;
	int n = 0;

	cmd = ((u64)insn << 32) | (((u64)extmask & 0xf) << 28)
		| ((u64)thread << 17) | SCOM_RAMC_ENABLE | SCOM_RAMC_EXECUTE;
	mask = SCOM_RAMC_DONE | SCOM_RAMC_INTERRUPT | SCOM_RAMC_ERROR;

	scom_write(scom, SCOM_RAMIC, cmd);

	while (!((val = scom_read(scom, SCOM_RAMC)) & mask)) {
		pr_devel("Waiting on RAMC = 0x%llx\n", val);
		if (++n == 3) {
			pr_err("RAMC timeout on instruction 0x%08x, thread %d\n",
			       insn, thread);
			return -1;
		}
	}

	if (val & SCOM_RAMC_INTERRUPT) {
		pr_err("RAMC interrupt on instruction 0x%08x, thread %d\n",
		       insn, thread);
		return -SCOM_RAMC_INTERRUPT;
	}

	if (val & SCOM_RAMC_ERROR) {
		pr_err("RAMC error on instruction 0x%08x, thread %d\n",
		       insn, thread);
		return -SCOM_RAMC_ERROR;
	}

	return 0;
}

static int a2_scom_getgpr(scom_map_t scom, int thread, int gpr, int alt,
			  u64 *out_gpr)
{
	int rc;

	/* or rN, rN, rN */
	u32 insn = 0x7c000378 | (gpr << 21) | (gpr << 16) | (gpr << 11);
	rc = a2_scom_ram(scom, thread, insn, alt ? 0xf : 0x0);
	if (rc)
		return rc;

	*out_gpr = scom_read(scom, SCOM_RAMD);

	return 0;
}

static int a2_scom_getspr(scom_map_t scom, int thread, int spr, u64 *out_spr)
{
	int rc, sprhi, sprlo;
	u32 insn;

	sprhi = spr >> 5;
	sprlo = spr & 0x1f;
	insn = 0x7c2002a6 | (sprlo << 16) | (sprhi << 11); /* mfspr r1,spr */

	if (spr == 0x0ff0)
		insn = 0x7c2000a6; /* mfmsr r1 */

	rc = a2_scom_ram(scom, thread, insn, 0xf);
	if (rc)
		return rc;
	return a2_scom_getgpr(scom, thread, 1, 1, out_spr);
}

static int a2_scom_setgpr(scom_map_t scom, int thread, int gpr,
			  int alt, u64 val)
{
	u32 lis = 0x3c000000 | (gpr << 21);
	u32 li = 0x38000000 | (gpr << 21);
	u32 oris = 0x64000000 | (gpr << 21) | (gpr << 16);
	u32 ori = 0x60000000 | (gpr << 21) | (gpr << 16);
	u32 rldicr32 = 0x780007c6 | (gpr << 21) | (gpr << 16);
	u32 highest = val >> 48;
	u32 higher = (val >> 32) & 0xffff;
	u32 high = (val >> 16) & 0xffff;
	u32 low = val & 0xffff;
	int lext = alt ? 0x8 : 0x0;
	int oext = alt ? 0xf : 0x0;
	int rc = 0;

	if (highest)
		rc |= a2_scom_ram(scom, thread, lis | highest, lext);

	if (higher) {
		if (highest)
			rc |= a2_scom_ram(scom, thread, oris | higher, oext);
		else
			rc |= a2_scom_ram(scom, thread, li | higher, lext);
	}

	if (highest || higher)
		rc |= a2_scom_ram(scom, thread, rldicr32, oext);

	if (high) {
		if (highest || higher)
			rc |= a2_scom_ram(scom, thread, oris | high, oext);
		else
			rc |= a2_scom_ram(scom, thread, lis | high, lext);
	}

	if (highest || higher || high)
		rc |= a2_scom_ram(scom, thread, ori | low, oext);
	else
		rc |= a2_scom_ram(scom, thread, li | low, lext);

	return rc;
}

static int a2_scom_setspr(scom_map_t scom, int thread, int spr, u64 val)
{
	int sprhi = spr >> 5;
	int sprlo = spr & 0x1f;
	/* mtspr spr, r1 */
	u32 insn = 0x7c2003a6 | (sprlo << 16) | (sprhi << 11);

	if (spr == 0x0ff0)
		insn = 0x7c200124; /* mtmsr r1 */

	if (a2_scom_setgpr(scom, thread, 1, 1, val))
		return -1;

	return a2_scom_ram(scom, thread, insn, 0xf);
}

static int a2_scom_initial_tlb(scom_map_t scom, int thread)
{
	extern u32 a2_tlbinit_code_start[], a2_tlbinit_code_end[];
	extern u32 a2_tlbinit_after_iprot_flush[];
	extern u32 a2_tlbinit_after_linear_map[];
	u32 assoc, entries, i;
	u64 epn, tlbcfg;
	u32 *p;
	int rc;

	/* Invalidate all entries (including iprot) */

	rc = a2_scom_getspr(scom, thread, SPRN_TLB0CFG, &tlbcfg);
	if (rc)
		goto scom_fail;
	entries = tlbcfg & TLBnCFG_N_ENTRY;
	assoc = (tlbcfg & TLBnCFG_ASSOC) >> 24;
	epn = 0;

	/* Set MMUCR2 to enable 4K, 64K, 1M, 16M and 1G pages */
	a2_scom_setspr(scom, thread, SPRN_MMUCR2, 0x000a7531);
	/* Set MMUCR3 to write all thids bit to the TLB */
	a2_scom_setspr(scom, thread, SPRN_MMUCR3, 0x0000000f);

	/* Set MAS1 for 1G page size, and MAS2 to our initial EPN */
	a2_scom_setspr(scom, thread, SPRN_MAS1, MAS1_TSIZE(BOOK3E_PAGESZ_1GB));
	a2_scom_setspr(scom, thread, SPRN_MAS2, epn);
	for (i = 0; i < entries; i++) {

		a2_scom_setspr(scom, thread, SPRN_MAS0, MAS0_ESEL(i % assoc));

		/* tlbwe */
		rc = a2_scom_ram(scom, thread, 0x7c0007a4, 0);
		if (rc)
			goto scom_fail;

		/* Next entry is new address? */
		if((i + 1) % assoc == 0) {
			epn += (1 << 30);
			a2_scom_setspr(scom, thread, SPRN_MAS2, epn);
		}
	}

	/* Setup args for linear mapping */
	rc = a2_scom_setgpr(scom, thread, 3, 0, MAS0_TLBSEL(0));
	if (rc)
		goto scom_fail;

	/* Linear mapping */
	for (p = a2_tlbinit_code_start; p < a2_tlbinit_after_linear_map; p++) {
		rc = a2_scom_ram(scom, thread, *p, 0);
		if (rc)
			goto scom_fail;
	}

	/*
	 * For the boot thread, between the linear mapping and the debug
	 * mappings there is a loop to flush iprot mappings. Ramming doesn't do
	 * branches, but the secondary threads don't need to be nearly as smart
	 * (i.e. we don't need to worry about invalidating the mapping we're
	 * standing on).
	 */

	/* Debug mappings. Expects r11 = MAS0 from linear map (set above) */
	for (p = a2_tlbinit_after_iprot_flush; p < a2_tlbinit_code_end; p++) {
		rc = a2_scom_ram(scom, thread, *p, 0);
		if (rc)
			goto scom_fail;
	}

scom_fail:
	if (rc)
		pr_err("Setting up initial TLB failed, err %d\n", rc);

	if (rc == -SCOM_RAMC_INTERRUPT) {
		/* Interrupt, dump some status */
		int rc[10];
		u64 iar, srr0, srr1, esr, mas0, mas1, mas2, mas7_3, mas8, ccr2;
		rc[0] = a2_scom_getspr(scom, thread, SPRN_IAR, &iar);
		rc[1] = a2_scom_getspr(scom, thread, SPRN_SRR0, &srr0);
		rc[2] = a2_scom_getspr(scom, thread, SPRN_SRR1, &srr1);
		rc[3] = a2_scom_getspr(scom, thread, SPRN_ESR, &esr);
		rc[4] = a2_scom_getspr(scom, thread, SPRN_MAS0, &mas0);
		rc[5] = a2_scom_getspr(scom, thread, SPRN_MAS1, &mas1);
		rc[6] = a2_scom_getspr(scom, thread, SPRN_MAS2, &mas2);
		rc[7] = a2_scom_getspr(scom, thread, SPRN_MAS7_MAS3, &mas7_3);
		rc[8] = a2_scom_getspr(scom, thread, SPRN_MAS8, &mas8);
		rc[9] = a2_scom_getspr(scom, thread, SPRN_A2_CCR2, &ccr2);
		pr_err(" -> retreived IAR =0x%llx (err %d)\n", iar, rc[0]);
		pr_err("    retreived SRR0=0x%llx (err %d)\n", srr0, rc[1]);
		pr_err("    retreived SRR1=0x%llx (err %d)\n", srr1, rc[2]);
		pr_err("    retreived ESR =0x%llx (err %d)\n", esr, rc[3]);
		pr_err("    retreived MAS0=0x%llx (err %d)\n", mas0, rc[4]);
		pr_err("    retreived MAS1=0x%llx (err %d)\n", mas1, rc[5]);
		pr_err("    retreived MAS2=0x%llx (err %d)\n", mas2, rc[6]);
		pr_err("    retreived MS73=0x%llx (err %d)\n", mas7_3, rc[7]);
		pr_err("    retreived MAS8=0x%llx (err %d)\n", mas8, rc[8]);
		pr_err("    retreived CCR2=0x%llx (err %d)\n", ccr2, rc[9]);
	}

	return rc;
}

int a2_scom_startup_cpu(unsigned int lcpu, int thr_idx, struct device_node *np)
{
	u64 init_iar, init_msr, init_ccr2;
	unsigned long start_here;
	int rc, core_setup;
	scom_map_t scom;
	u64 pccr0;

	scom = get_scom(lcpu, np, &core_setup);
	if (!scom) {
		printk(KERN_ERR "Couldn't map SCOM for CPU%d\n", lcpu);
		return -1;
	}

	pr_devel("Bringing up CPU%d using SCOM...\n", lcpu);

	pccr0 = scom_read(scom, SCOM_PCCR0);
	scom_write(scom, SCOM_PCCR0, pccr0 | SCOM_PCCR0_ENABLE_DEBUG |
				     SCOM_PCCR0_ENABLE_RAM);

	/* Stop the thead with THRCTL. If we are setting up the TLB we stop all
	 * threads. We also disable asynchronous interrupts while RAMing.
	 */
	if (core_setup)
		scom_write(scom, SCOM_THRCTL_OR,
			      SCOM_THRCTL_T0_STOP |
			      SCOM_THRCTL_T1_STOP |
			      SCOM_THRCTL_T2_STOP |
			      SCOM_THRCTL_T3_STOP |
			      SCOM_THRCTL_ASYNC_DIS);
	else
		scom_write(scom, SCOM_THRCTL_OR, SCOM_THRCTL_T0_STOP >> thr_idx);

	/* Flush its pipeline just in case */
	scom_write(scom, SCOM_RAMC, ((u64)thr_idx << 17) |
		      SCOM_RAMC_FLUSH | SCOM_RAMC_ENABLE);

	a2_scom_getspr(scom, thr_idx, SPRN_IAR, &init_iar);
	a2_scom_getspr(scom, thr_idx, 0x0ff0, &init_msr);
	a2_scom_getspr(scom, thr_idx, SPRN_A2_CCR2, &init_ccr2);

	/* Set MSR to MSR_CM (0x0ff0 is magic value for MSR_CM) */
	rc = a2_scom_setspr(scom, thr_idx, 0x0ff0, MSR_CM);
	if (rc) {
		pr_err("Failed to set MSR ! err %d\n", rc);
		return rc;
	}

	/* RAM in an sync/isync for the sake of it */
	a2_scom_ram(scom, thr_idx, 0x7c0004ac, 0);
	a2_scom_ram(scom, thr_idx, 0x4c00012c, 0);

	if (core_setup) {
		pr_devel("CPU%d is first thread in core, initializing TLB...\n",
			 lcpu);
		rc = a2_scom_initial_tlb(scom, thr_idx);
		if (rc)
			goto fail;
	}

	start_here = *(unsigned long *)(core_setup ? generic_secondary_smp_init
					: generic_secondary_thread_init);
	pr_devel("CPU%d entry point at 0x%lx...\n", lcpu, start_here);

	rc |= a2_scom_setspr(scom, thr_idx, SPRN_IAR, start_here);
	rc |= a2_scom_setgpr(scom, thr_idx, 3, 0,
			     get_hard_smp_processor_id(lcpu));
	/*
	 * Tell book3e_secondary_core_init not to set up the TLB, we've
	 * already done that.
	 */
	rc |= a2_scom_setgpr(scom, thr_idx, 4, 0, 1);

	rc |= a2_scom_setspr(scom, thr_idx, SPRN_TENS, 0x1 << thr_idx);

	scom_write(scom, SCOM_RAMC, 0);
	scom_write(scom, SCOM_THRCTL_AND, ~(SCOM_THRCTL_T0_STOP >> thr_idx));
	scom_write(scom, SCOM_PCCR0, pccr0);
fail:
	pr_devel("  SCOM initialization %s\n", rc ? "failed" : "succeeded");
	if (rc) {
		pr_err("Old IAR=0x%08llx MSR=0x%08llx CCR2=0x%08llx\n",
		       init_iar, init_msr, init_ccr2);
	}

	return rc;
}
