/* Copyright (c) 2014-2016, The Linux Foundation. All rights reserved.
 * Copyright (c) 2013 ARM Ltd.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only version 2 as published by the Free Software Foundation.
 *
 * 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.
 */

/* MSM ARMv8 CPU Operations
 * Based on arch/arm64/kernel/smp_spin_table.c
 */

#include <linux/bitops.h>
#include <linux/cpu.h>
#include <linux/cpumask.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/smp.h>

#include <soc/qcom/cpu_pwr_ctl.h>
#include <soc/qcom/scm-boot.h>
#include <soc/qcom/socinfo.h>
#include <soc/qcom/pm.h>
#include <soc/qcom/spm.h>
#include <soc/qcom/jtag.h>

#include <asm/barrier.h>
#include <asm/cacheflush.h>
#include <asm/cpu_ops.h>
#include <asm/cputype.h>
#include <asm/smp_plat.h>

static DEFINE_RAW_SPINLOCK(boot_lock);

DEFINE_PER_CPU(int, cold_boot_done);

static int cold_boot_flags[] = {
	0,
	SCM_FLAG_COLDBOOT_CPU1,
	SCM_FLAG_COLDBOOT_CPU2,
	SCM_FLAG_COLDBOOT_CPU3,
};

static void write_pen_release(u64 val)
{
	void *start = (void *)&secondary_holding_pen_release;
	unsigned long size = sizeof(secondary_holding_pen_release);

	secondary_holding_pen_release = val;
	smp_wmb();
	__flush_dcache_area(start, size);
}

static int secondary_pen_release(unsigned int cpu)
{
	unsigned long timeout;

	/*
	 * Set synchronisation state between this boot processor
	 * and the secondary one
	 */
	raw_spin_lock(&boot_lock);
	write_pen_release(cpu_logical_map(cpu));

	/*
	 * Wake-up cpu with am IPI
	 */
	arch_send_wakeup_ipi_mask(cpumask_of(cpu));

	timeout = jiffies + (1 * HZ);
	while (time_before(jiffies, timeout)) {
		if (secondary_holding_pen_release == INVALID_HWID)
			break;
		udelay(10);
	}
	raw_spin_unlock(&boot_lock);

	return secondary_holding_pen_release != INVALID_HWID ? -ENOSYS : 0;
}

static int __init msm_cpu_init(struct device_node *dn, unsigned int cpu)
{
	return 0;
}

static int __init msm_cpu_prepare(unsigned int cpu)
{
	u64 mpidr_el1 = cpu_logical_map(cpu);

	if (scm_is_mc_boot_available()) {

		if (mpidr_el1 & ~MPIDR_HWID_BITMASK) {
			pr_err("CPU%d:Failed to set boot address\n", cpu);
			return -ENOSYS;
		}

		if (scm_set_boot_addr_mc(virt_to_phys(secondary_holding_pen),
				BIT(MPIDR_AFFINITY_LEVEL(mpidr_el1, 0)),
				BIT(MPIDR_AFFINITY_LEVEL(mpidr_el1, 1)),
				BIT(MPIDR_AFFINITY_LEVEL(mpidr_el1, 2)),
				SCM_FLAG_COLDBOOT_MC)) {
			pr_warn("CPU%d:Failed to set boot address\n", cpu);
			return -ENOSYS;
		}

	} else {
		if (scm_set_boot_addr(virt_to_phys(secondary_holding_pen),
			cold_boot_flags[cpu])) {
			pr_warn("Failed to set CPU %u boot address\n", cpu);
			return -ENOSYS;
		}
	}

	/* Mark CPU0 cold boot flag as done */
	if (per_cpu(cold_boot_done, 0) == false)
		per_cpu(cold_boot_done, 0) = true;

	return 0;
}

static int msm8953_cpu_boot(unsigned int cpu)
{
	int ret = 0;

	if (per_cpu(cold_boot_done, cpu) == false) {
		ret = msm8953_unclamp_secondary_arm_cpu(cpu);
		if (ret)
			return ret;

		per_cpu(cold_boot_done, cpu) = true;
	}
	return secondary_pen_release(cpu);
}

#ifdef CONFIG_HOTPLUG_CPU
static void msm8953_wfi_cpu_die(unsigned int cpu)
{
	if (unlikely(cpu != smp_processor_id())) {
		pr_crit("%s: running on %u, should be %u\n",
			__func__, smp_processor_id(), cpu);
		BUG();
	}
	for (;;) {
		wfi();
		if (secondary_holding_pen_release == cpu_logical_map(cpu))
			break;	/*Proper wake up */

		pr_debug("CPU%u: spurious wakeup call\n", cpu);
		BUG();
	}
}
#endif

static int msm8937_cpu_boot(unsigned int cpu)
{
	int ret = 0;

	if (per_cpu(cold_boot_done, cpu) == false) {
		ret = msm8937_unclamp_secondary_arm_cpu(cpu);
		if (ret)
			return ret;

		per_cpu(cold_boot_done, cpu) = true;
	}
	return secondary_pen_release(cpu);
}

#ifdef CONFIG_HOTPLUG_CPU
static void msm8937_wfi_cpu_die(unsigned int cpu)
{
	if (unlikely(cpu != smp_processor_id())) {
		pr_crit("%s: running on %u, should be %u\n",
			__func__, smp_processor_id(), cpu);
		BUG();
	}
	for (;;) {
		wfi();
		if (secondary_holding_pen_release == cpu_logical_map(cpu)) {
			/*Proper wake up */
			break;
		}
		pr_debug("CPU%u: spurious wakeup call\n", cpu);
		BUG();
	}
}
#endif

static int msm_cpu_boot(unsigned int cpu)
{
	int ret = 0;

	if (per_cpu(cold_boot_done, cpu) == false) {
		if (of_board_is_sim()) {
			ret = msm_unclamp_secondary_arm_cpu_sim(cpu);
			if (ret)
				return ret;
		} else {
			ret = msm_unclamp_secondary_arm_cpu(cpu);
			if (ret)
				return ret;
		}
		per_cpu(cold_boot_done, cpu) = true;
	}
	return secondary_pen_release(cpu);
}

void msm_cpu_postboot(void)
{
	msm_jtag_restore_state();
	/*
	 * Let the primary processor know we're out of the pen.
	 */
	write_pen_release(INVALID_HWID);

	msm_spm_set_low_power_mode(MSM_SPM_MODE_CLOCK_GATING, false);

	/*
	 * Synchronise with the boot thread.
	 */
	raw_spin_lock(&boot_lock);
	raw_spin_unlock(&boot_lock);
}

#ifdef CONFIG_HOTPLUG_CPU
static void msm_wfi_cpu_die(unsigned int cpu)
{
	if (unlikely(cpu != smp_processor_id())) {
		pr_crit("%s: running on %u, should be %u\n",
			__func__, smp_processor_id(), cpu);
		BUG();
	}
	for (;;) {
		lpm_cpu_hotplug_enter(cpu);
		if (secondary_holding_pen_release == cpu_logical_map(cpu)) {
			/*Proper wake up */
			break;
		}
		pr_debug("CPU%u: spurious wakeup call\n", cpu);
		BUG();
	}
}
#endif

static struct cpu_operations msm_cortex_a_ops = {
	.name		= "qcom,arm-cortex-acc",
	.cpu_init	= msm_cpu_init,
	.cpu_prepare	= msm_cpu_prepare,
	.cpu_boot	= msm_cpu_boot,
	.cpu_postboot	= msm_cpu_postboot,
#ifdef CONFIG_HOTPLUG_CPU
	.cpu_die        = msm_wfi_cpu_die,
#endif
	.cpu_suspend       = msm_pm_collapse,
};
CPU_METHOD_OF_DECLARE(msm_cortex_a_ops,
		"qcom,arm-cortex-acc", &msm_cortex_a_ops);

static struct cpu_operations msm8953_cortex_a_ops = {
	.name		= "qcom,8953-arm-cortex-acc",
	.cpu_init	= msm_cpu_init,
	.cpu_prepare	= msm_cpu_prepare,
	.cpu_boot	= msm8953_cpu_boot,
	.cpu_postboot	= msm_cpu_postboot,
#ifdef CONFIG_HOTPLUG_CPU
	.cpu_die        = msm8953_wfi_cpu_die,
#endif
#ifdef CONFIG_ARM64_CPU_SUSPEND
	.cpu_suspend       = msm_pm_collapse,
#endif
};
CPU_METHOD_OF_DECLARE(msm8953_cortex_a_ops,
	"qcom,8953-arm-cortex-acc", &msm8953_cortex_a_ops);

static struct cpu_operations msm8937_cortex_a_ops = {
	.name		= "qcom,8937-arm-cortex-acc",
	.cpu_init	= msm_cpu_init,
	.cpu_prepare	= msm_cpu_prepare,
	.cpu_boot	= msm8937_cpu_boot,
	.cpu_postboot	= msm_cpu_postboot,
#ifdef CONFIG_HOTPLUG_CPU
	.cpu_die        = msm8937_wfi_cpu_die,
#endif
#ifdef CONFIG_ARM64_CPU_SUSPEND
	.cpu_suspend       = msm_pm_collapse,
#endif
};
CPU_METHOD_OF_DECLARE(msm8937_cortex_a_ops,
	"qcom,8937-arm-cortex-acc", &msm8937_cortex_a_ops);
