/*
 * Copyright (c) 2014-2015, 2017, The Linux Foundation. All rights reserved.
 *
 * 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.
 */

#include <linux/spinlock.h>
#include <linux/module.h>
#include <linux/msm_rtb.h>
#include <linux/debugfs.h>
#include <linux/seq_file.h>
#include <asm/cputype.h>

#include <soc/qcom/kryo-l2-accessors.h>

#define	L2CPUSRSELR_EL1	S3_3_c15_c0_6
#define	L2CPUSRDR_EL1	S3_3_c15_c0_7

#define arm64_sys_reg_read(reg) ({					\
	u64 __val;							\
	asm volatile("mrs %0, " __stringify(reg) : "=r" (__val));	\
	__val;								\
})

#define arm64_sys_reg_write(reg, val) {					\
	asm volatile("msr " __stringify(reg) ", %0" : : "r" (val));	\
}

static DEFINE_RAW_SPINLOCK(l2_access_lock);

/**
 * set_l2_indirect_reg: write value to an L2 register
 * @reg: Address of L2 register.
 * @value: Value to be written to register.
 *
 * Use architecturally required barriers for ordering between system register
 * accesses, and system registers with respect to device memory
 */
void set_l2_indirect_reg(u64 reg, u64 val)
{
	unsigned long flags;
	mb();
	raw_spin_lock_irqsave(&l2_access_lock, flags);
	uncached_logk(LOGK_L2CPWRITE, (void *)reg);
	arm64_sys_reg_write(L2CPUSRSELR_EL1, reg);
	isb();
	arm64_sys_reg_write(L2CPUSRDR_EL1, val);
	isb();
	raw_spin_unlock_irqrestore(&l2_access_lock, flags);
}
EXPORT_SYMBOL(set_l2_indirect_reg);

/**
 * get_l2_indirect_reg: read an L2 register value
 * @reg: Address of L2 register.
 *
 * Use architecturally required barriers for ordering between system register
 * accesses, and system registers with respect to device memory
 */
u64 get_l2_indirect_reg(u64 reg)
{
	u64 val;
	unsigned long flags;

	raw_spin_lock_irqsave(&l2_access_lock, flags);
	uncached_logk(LOGK_L2CPREAD, (void *)reg);
	arm64_sys_reg_write(L2CPUSRSELR_EL1, reg);
	isb();
	val = arm64_sys_reg_read(L2CPUSRDR_EL1);
	raw_spin_unlock_irqrestore(&l2_access_lock, flags);

	return val;
}
EXPORT_SYMBOL(get_l2_indirect_reg);

#if defined(CONFIG_MSM_L2_IA_DEBUG)

static u32 debug_addr;
static int debug_target_cpu;

static void remote_l2_ia_read(void *data)
{
	u64 *val = data;
	*val = get_l2_indirect_reg(debug_addr);
}

static void remote_l2_ia_write(void *data)
{
	u64 *val = data;

	set_l2_indirect_reg(debug_addr, *val);
}

static int l2_indirect_target_cpu_set(void *data, u64 val)
{
	if (val > num_possible_cpus())
		return -EINVAL;

	debug_target_cpu = val;
	return 0;
}

static int l2_indirect_target_cpu_get(void *data, u64 *val)
{
	*val = debug_target_cpu;
	return 0;
}

DEFINE_SIMPLE_ATTRIBUTE(l2_ia_cpu_fops, l2_indirect_target_cpu_get,
			l2_indirect_target_cpu_set, "%llu\n");

static int l2_indirect_addr_set(void *data, u64 val)
{
	debug_addr = val;
	return 0;
}

static int l2_indirect_addr_get(void *data, u64 *val)
{
	*val = debug_addr;
	return 0;
}

DEFINE_SIMPLE_ATTRIBUTE(l2_ia_addr_fops, l2_indirect_addr_get,
			l2_indirect_addr_set, "%llu\n");

static int l2_indirect_val_set(void *data, u64 val)
{
	return smp_call_function_single(debug_target_cpu, remote_l2_ia_write,
					&val, 1);
}

DEFINE_SIMPLE_ATTRIBUTE(l2_ia_set_fops, NULL,
			l2_indirect_val_set, "%llu\n");

static int l2_indirect_get(void *data, u64 *val)
{
	return smp_call_function_single(debug_target_cpu, remote_l2_ia_read,
					val, 1);
}

DEFINE_SIMPLE_ATTRIBUTE(l2_ia_get_fops, l2_indirect_get,
			NULL, "%llu\n");

/**
 * l2_ia_debug_init() - Initialize L2 indirect access register debugfs
 */
static int l2_ia_debug_init(void)
{
	static struct dentry *debugfs_base;

	debugfs_base = debugfs_create_dir("l2_indirect", NULL);
	if (!debugfs_base)
		return -ENOMEM;

	if (!debugfs_create_file("get", S_IRUGO, debugfs_base, NULL,
				&l2_ia_get_fops))
		return -ENOMEM;

	if (!debugfs_create_file("set", S_IRUGO | S_IWUSR, debugfs_base, NULL,
				&l2_ia_set_fops))
		return -ENOMEM;

	if (!debugfs_create_file("address", S_IRUGO | S_IWUSR, debugfs_base,
				 NULL, &l2_ia_addr_fops))
		return -ENOMEM;

	if (!debugfs_create_file("target_cpu", S_IRUGO | S_IWUSR, debugfs_base,
				 NULL, &l2_ia_cpu_fops))
		return -ENOMEM;

	return 0;
}
late_initcall(l2_ia_debug_init);

#endif /* CONFIG_MSM_L2_IA_DEBUG */
