// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2019 MediaTek Inc.
 */

#include <linux/arm-smccc.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#include <mt-plat/aee.h>
#include <linux/soc/mediatek/mtk_sip_svc.h>
#include "mmsram.h"

#define MMSYSRAM_INTEN0		(0x000)
#define MMSYSRAM_INTEN1		(0x004)
#define MMSYSRAM_INSTA0		(0x010)
#define MMSYSRAM_INSTA1		(0x014)
#define MMSYSRAM_SEC_ADDR0	(0x040)
#define MMSYSRAM_SEC_CTRL0	(0x060)

#define BIT_INSTA0		(0x6FF0E0E0)
#define BIT_INSTA1		(0x000037F8)
#define BIT_SECURE_ON		(0x11111111)

/* First Violation Latch Debug */
#define FVLD_APC0_LATCH_EN	(0x090)
#define FVLD_APC1_LATCH_EN	(0x0C0)
#define FVLD_MPU0_LATCH_EN	(0x110)
#define FVLD_MPU1_LATCH_EN	(0x140)
#define FVLD_APC0_VIO_ADDR	(0x0A4)
#define FVLD_APC0_VIO_ID	(0x0A8)
#define FVLD_APC0_VIO_WR_RD	(0x0AC)
#define FVLD_APC1_VIO_ADDR	(0x0D4)
#define FVLD_APC1_VIO_ID	(0x0D8)
#define FVLD_APC1_VIO_WR_RD	(0x0DC)
#define FVLD_MPU0_VIO_ADDR	(0x124)
#define FVLD_MPU0_VIO_ID	(0x128)
#define FVLD_MPU0_VIO_WR_RD	(0x12C)
#define FVLD_MPU1_VIO_ADDR	(0x154)
#define FVLD_MPU1_VIO_ID	(0x158)
#define FVLD_MPU1_VIO_WR_RD	(0x15C)

#define BIT_APC_LATCH_EN	(0x00000020)
#define BIT_MPU_LATCH_EN	(0x00000FF0)

#define DEC_APC0_VIO_DECERR(x)	(((x) >> 5) & 0x1)
#define DEC_APC0_VIO_WR_RD(x)	(((x) >> 6) & 0x3)
#define DEC_APC1_VIO_DECERR(x)	(((x) >> 13) & 0x1)
#define DEC_APC1_VIO_WR_RD(x)	(((x) >> 14) & 0x3)
#define DEC_MPU0_VIO_DEVAPC(x)	(((x) >> 20) & 0xFF)
#define DEC_MPU0_VIO_WR_RD(x)	(((x) >> 29) & 0x3)
#define DEC_MPU1_VIO_DEVAPC(x)	(((x) >> 3) & 0xFF)
#define DEC_MPU1_VIO_WR_RD(x)	(((x) >> 12) & 0x3)

#define DEC_APC_LATCH_EN(x)	(((x) >> 5) & 0x1)
#define DEC_APC_VIO_ADDR(x)	(x)
#define DEC_APC_VIO_ID(x)	((x) & 0xFFFFF)
#define DEC_APC_VIO_WR_RD(x)	(((x) >> 17) & 0x3)
#define DEC_MPU_LATCH_EN(x)	(((x) >> 4) & 0xFF)
#define DEC_MPU_VIO_ADDR(x)	((x) & 0x1FFFFF)
#define DEC_MPU_VIO_ID(x)	((x) & 0xFFFFF)
#define DEC_MPU_VIO_WR_RD(x)	(((x) >> 17) & 0x3)

#define MAX_CLK_NUM	(8)

struct mmsram_dev {
	struct device *dev;
	void __iomem *ctrl_base;
	void __iomem *sram_paddr;
	void __iomem *sram_vaddr;
	ssize_t sram_size;
	struct clk *clk[MAX_CLK_NUM];
	const char *clk_name[MAX_CLK_NUM];
};

enum smc_mmsram_request {
	MMSRAM_ENABLE_SECURE,
};

static struct work_struct dump_reg_work;

static struct mmsram_dev *mmsram;
#if !IS_ENABLED(CONFIG_FPGA_EARLY_PORTING)
static atomic_t clk_ref = ATOMIC_INIT(0);
#endif
static bool is_secure_on;
static bool debug_enable;

/* MTCMOS clocks should be defined before CG clocks in DTS */
static int set_clk_enable(bool is_enable)
{
	int ret = 0;

#if !IS_ENABLED(CONFIG_FPGA_EARLY_PORTING)
	int i, j;

	if (is_enable) {
		pm_runtime_get_sync(mmsram->dev);
		for (i = 0; i < MAX_CLK_NUM; i++) {
			if (mmsram->clk[i])
				ret = clk_prepare_enable(mmsram->clk[i]);

			if (ret) {
				pr_notice("mmsram clk(%s) enable fail:%d\n",
					mmsram->clk_name[i], ret);
				for (j = i - 1; j >= 0; j--)
					clk_disable_unprepare(mmsram->clk[j]);
				return ret;
			}
		}

		atomic_inc(&clk_ref);
	} else {
		for (i = MAX_CLK_NUM - 1; i >= 0; i--)
			if (mmsram->clk[i])
				clk_disable_unprepare(mmsram->clk[i]);
		pm_runtime_put_sync(mmsram->dev);
		atomic_dec(&clk_ref);
	}
#endif
	if (debug_enable)
		pr_notice("%s:%d\n", __func__, is_enable);
	return ret;
}

static void set_reg_secure(bool is_on)
{
	struct arm_smccc_res res;

	arm_smccc_smc(MTK_SIP_MMSRAM_CONTROL, MMSRAM_ENABLE_SECURE,
			is_on ? 1 : 0, 0, 0, 0, 0, 0, &res);
}

static s32 before_reg_rw(void)
{
	if (set_clk_enable(true)) {
		pr_notice("%s: enable clk fail\n", __func__);
		return -EINVAL;
	}
	set_reg_secure(false);
	return 0;
}

static void after_reg_rw(void)
{
	set_reg_secure(true);
	set_clk_enable(false);
}


void mmsram_set_secure(bool secure_on)
{
	if (secure_on == is_secure_on)
		return;
	is_secure_on = secure_on;
	if (before_reg_rw()) {
		pr_notice("%s: error before reg rw\n", __func__);
		return;
	}
	if (secure_on)
		writel(BIT_SECURE_ON, mmsram->ctrl_base + MMSYSRAM_SEC_CTRL0);
	else
		writel(0, mmsram->ctrl_base + MMSYSRAM_SEC_CTRL0);
	after_reg_rw();
}
EXPORT_SYMBOL_GPL(mmsram_set_secure);

static void init_mmsram_reg(void)
{
	if (before_reg_rw()) {
		pr_notice("%s: error before reg rw\n", __func__);
		return;
	}
	writel((0x1 << 24) | 0x160000,
		mmsram->ctrl_base + MMSYSRAM_SEC_ADDR0);

	writel(0x0, mmsram->ctrl_base + MMSYSRAM_INSTA0);
	writel(0x0, mmsram->ctrl_base + MMSYSRAM_INSTA1);

	writel(BIT_APC_LATCH_EN, mmsram->ctrl_base + FVLD_APC0_LATCH_EN);
	writel(BIT_APC_LATCH_EN, mmsram->ctrl_base + FVLD_APC1_LATCH_EN);
	writel(BIT_MPU_LATCH_EN, mmsram->ctrl_base + FVLD_MPU0_LATCH_EN);
	writel(BIT_MPU_LATCH_EN, mmsram->ctrl_base + FVLD_MPU1_LATCH_EN);
	writel(BIT_INSTA0, mmsram->ctrl_base + MMSYSRAM_INTEN0);
	writel(BIT_INSTA1, mmsram->ctrl_base + MMSYSRAM_INTEN1);

	after_reg_rw();
}

int mmsram_power_on(void)
{
	int ret = 0;

	set_clk_enable(true);
	init_mmsram_reg();
	if (debug_enable)
		pr_notice("mmsram power on\n");
	return ret;
}
EXPORT_SYMBOL_GPL(mmsram_power_on);

void mmsram_power_off(void)
{
	set_clk_enable(false);
	if (debug_enable)
		pr_notice("mmsram power off\n");
}
EXPORT_SYMBOL_GPL(mmsram_power_off);

int enable_mmsram(void)
{
	pr_notice("enable mmsram\n");

	return 0;
}
EXPORT_SYMBOL_GPL(enable_mmsram);

void disable_mmsram(void)
{
	pr_notice("disable mmsram\n");
}
EXPORT_SYMBOL_GPL(disable_mmsram);

void mmsram_get_info(struct mmsram_data *data)
{
	data->paddr = mmsram->sram_paddr;
	data->vaddr = mmsram->sram_vaddr;
	data->size = mmsram->sram_size;
	pr_notice("%s: pa:%#lx va:%#lx size:%#lx\n",
		__func__, (unsigned long)data->paddr,
		(unsigned long)data->vaddr, data->size);
}
EXPORT_SYMBOL_GPL(mmsram_get_info);

static void dump_reg_func(struct work_struct *work)
{
	u32 interrupt_monitor0, interrupt_monitor1;
	void __iomem *ctrl_base = mmsram->ctrl_base;

	if (before_reg_rw()) {
		pr_notice("%s: error before reg rw\n", __func__);
		return;
	}

	/* Print debug log */
	interrupt_monitor0 = readl(ctrl_base + MMSYSRAM_INSTA0);
	interrupt_monitor1 = readl(ctrl_base + MMSYSRAM_INSTA1);
	pr_notice("apc0_vio_decerr:%#x apc0_vio_wr_rd:%#x\n",
		DEC_APC0_VIO_DECERR(interrupt_monitor0),
		DEC_APC0_VIO_WR_RD(interrupt_monitor0));
	pr_notice("apc1_vio_decerr:%#x apc1_vio_wr_rd:%#x\n",
		DEC_APC1_VIO_DECERR(interrupt_monitor0),
		DEC_APC1_VIO_WR_RD(interrupt_monitor0));
	pr_notice("mpu0_vio_devapc:%#x mpu0_vio_wr_rd:%#x\n",
		DEC_MPU0_VIO_DEVAPC(interrupt_monitor0),
		DEC_MPU0_VIO_WR_RD(interrupt_monitor0));
	pr_notice("mpu1_vio_devapc:%#x mpu1_vio_wr_rd:%#x\n",
		DEC_MPU1_VIO_DEVAPC(interrupt_monitor1),
		DEC_MPU1_VIO_WR_RD(interrupt_monitor1));
	pr_notice("apc0_latch_en:%#x apc0_vio_addr:%#x\n",
		DEC_APC_LATCH_EN(readl(ctrl_base + FVLD_APC0_LATCH_EN)),
		DEC_APC_VIO_ADDR(readl(ctrl_base + FVLD_APC0_VIO_ADDR)));
	pr_notice("apc0_vio_id:%#x apc0_vio_wr_rd:%#x\n",
		DEC_APC_VIO_ID(readl(ctrl_base + FVLD_APC0_VIO_ID)),
		DEC_APC_VIO_WR_RD(readl(ctrl_base + FVLD_APC0_VIO_WR_RD)));

	pr_notice("apc1_latch_en:%#x apc1_vio_addr:%#x\n",
		DEC_APC_LATCH_EN(readl(ctrl_base + FVLD_APC1_LATCH_EN)),
		DEC_APC_VIO_ADDR(readl(ctrl_base + FVLD_APC1_VIO_ADDR)));
	pr_notice("apc1_vio_id:%#x apc1_vio_wr_rd:%#x\n",
		DEC_APC_VIO_ID(readl(ctrl_base + FVLD_APC1_VIO_ID)),
		DEC_APC_VIO_WR_RD(readl(ctrl_base + FVLD_APC1_VIO_WR_RD)));

	pr_notice("mpu0_latch_en:%#x mpu0_vio_addr:%#x\n",
		DEC_MPU_LATCH_EN(readl(ctrl_base + FVLD_MPU0_LATCH_EN)),
		DEC_MPU_VIO_ADDR(readl(ctrl_base + FVLD_MPU0_VIO_ADDR)));

	pr_notice("mpu0_vio_id:%#x mpu0_vio_wr_rd:%#x\n",
		DEC_MPU_VIO_ID(readl(ctrl_base + FVLD_MPU0_VIO_ID)),
		DEC_MPU_VIO_WR_RD(readl(ctrl_base + FVLD_MPU0_VIO_WR_RD)));
	pr_notice("mpu1_latch_en:%#x mpu1_vio_addr:%#x\n",
		DEC_MPU_LATCH_EN(readl(ctrl_base + FVLD_MPU1_LATCH_EN)),
		DEC_MPU_VIO_ADDR(readl(ctrl_base + FVLD_MPU1_VIO_ADDR)));
	pr_notice("mpu1_vio_id:%#x mpu1_vio_wr_rd:%#x\n",
		DEC_MPU_VIO_ID(readl(ctrl_base + FVLD_MPU1_VIO_ID)),
		DEC_MPU_VIO_WR_RD(readl(ctrl_base + FVLD_MPU1_VIO_WR_RD)));

	writel(0x0, ctrl_base + MMSYSRAM_INSTA0);
	writel(0x0, ctrl_base + MMSYSRAM_INSTA1);

	after_reg_rw();
#if IS_ENABLED(CONFIG_MTK_AEE_FEATURE)
	aee_kernel_warning("MMSRAM", "MMSRAM Violation.");
#endif
}

static irqreturn_t mmsram_irq_handler(int irq, void *data)
{
	pr_notice("handle mmsram irq!\n");
	schedule_work(&dump_reg_work);
	return IRQ_HANDLED;
}

static int mmsram_probe(struct platform_device *pdev)
{
	struct resource *res;
	int irq, err, clk_num, i;

	mmsram = devm_kzalloc(&pdev->dev, sizeof(*mmsram), GFP_KERNEL);
	if (!mmsram)
		return -ENOMEM;

	mmsram->dev = &pdev->dev;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		dev_notice(&pdev->dev, "could not get resource for ctrl\n");
		return -EINVAL;
	}

	mmsram->ctrl_base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(mmsram->ctrl_base)) {
		dev_notice(&pdev->dev,
			"could not ioremap resource for ctrl\n");
		return PTR_ERR(mmsram->ctrl_base);
	}

	res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
	if (!res) {
		dev_notice(&pdev->dev, "could not get resource for memory\n");
		return -EINVAL;
	}
	mmsram->sram_paddr = (void *)res->start;
	mmsram->sram_size = resource_size(res);
	mmsram->sram_vaddr =  (void __iomem *) devm_memremap(&pdev->dev,
				res->start, mmsram->sram_size, MEMREMAP_WT);
	if (IS_ERR(mmsram->sram_vaddr)) {
		dev_notice(&pdev->dev,
			"could not ioremap resource for memory\n");
		return PTR_ERR(mmsram->sram_vaddr);
	}

	dev_notice(&pdev->dev, "probe va=%p pa=%p size=%#lx\n",
		mmsram->sram_vaddr, mmsram->sram_paddr,
		mmsram->sram_size);

	if (!IS_ENABLED(CONFIG_FPGA_EARLY_PORTING)) {
		clk_num = of_property_read_string_array(pdev->dev.of_node,
			"clock-names", &mmsram->clk_name[0], MAX_CLK_NUM);
		for (i = 0; i < clk_num; i++) {
			mmsram->clk[i] = devm_clk_get(&pdev->dev,
						mmsram->clk_name[i]);
			if (IS_ERR(mmsram->clk[i])) {
				dev_notice(&pdev->dev,
					"could not get mmsram clk(%s) info\n",
					mmsram->clk_name[i]);
				return PTR_ERR(mmsram->clk[i]);
			}
		}
	}

	irq = platform_get_irq(pdev, 0);
	if (irq < 0) {
		dev_notice(&pdev->dev, "failed to get irq (%d)\n", irq);
		return -EINVAL;
	}
	err = devm_request_irq(&pdev->dev, irq, mmsram_irq_handler, IRQF_SHARED,
			       "mtk_mmsram", mmsram);
	if (err) {
		dev_notice(&pdev->dev,
			"failed to register ISR %d (%d)", irq, err);
		return err;
	}
	pm_runtime_enable(&pdev->dev);

	INIT_WORK(&dump_reg_work, dump_reg_func);

	return 0;
}

#define RESULT_STR_LEN 8
int test_mmsram;
struct mmsram_data *data;
int set_test_mmsram(const char *val, const struct kernel_param *kp)
{
	int result;
	u32 test_case, offset, value;
	const char *test_str = "12345678";
	char result_str[RESULT_STR_LEN + 1] = {0};

	result = sscanf(val, "%d %i %i", &test_case, &offset, &value);
	if (result != 3) {
		pr_notice("invalid input: %s, result(%d)\n", val, result);
		return -EINVAL;
	}
	pr_notice("%s (test_case, offset, value): (%d,%#x,%#x)\n",
		__func__, test_case, offset, value);

	switch (test_case) {
	case 0: /* Initialize */
		data = kzalloc(sizeof(*data), GFP_KERNEL);
		mmsram_get_info(data);
		enable_mmsram();
		mmsram_power_on();
		break;
	case 1: /* Uninitialize */
		mmsram_power_off();
		disable_mmsram();
		kfree(data);
		break;
	case 2: /* Write value to offset */
		writel(value, data->vaddr + offset);
		value = readl(data->vaddr + offset);
		pr_notice("write %#x success\n", value);
		break;
	case 3: /* Read value from offset */
		value = readl(data->vaddr + offset);
		pr_notice("read %#x success\n", value);
		break;
	case 4: /* Write test string to offset */
		memcpy_toio(data->vaddr + offset, test_str, RESULT_STR_LEN);
		pr_notice("write str:%s success\n", test_str);
		break;
	case 5: /* Write test string from offset */
		memcpy_fromio(result_str, data->vaddr, RESULT_STR_LEN);
		result_str[RESULT_STR_LEN] = '\0';
		pr_notice("read str:%s success\n", result_str);
		break;
	default:
		pr_notice("wrong input test_case:%d\n", test_case);
	}

	return 0;
}
static struct kernel_param_ops test_mmsram_ops = {
	.set = set_test_mmsram,
	.get = param_get_int,
};
module_param_cb(test_mmsram, &test_mmsram_ops, &test_mmsram, 0644);
MODULE_PARM_DESC(test_mmsram, "test mmsram");

static struct kernel_param_ops debug_enable_ops = {
	.set = param_set_bool,
	.get = param_get_bool,
};
module_param_cb(
	debug_enable, &debug_enable_ops, &debug_enable, 0644);
MODULE_PARM_DESC(debug_enable, "enable or disable mmsram debug log");

static const struct of_device_id of_mmsram_match_tbl[] = {
	{
		.compatible = "mediatek,mmsram",
	},
	{}
};

static struct platform_driver mmsram_drv = {
	.probe = mmsram_probe,
	.driver = {
		.name = "mtk-mmsram",
		.owner = THIS_MODULE,
		.of_match_table = of_mmsram_match_tbl,
	},
};
static int __init mtk_mmsram_init(void)
{
	s32 status;

	status = platform_driver_register(&mmsram_drv);
	if (status) {
		pr_notice("Failed to register mmsram driver(%d)\n", status);
		return -ENODEV;
	}
	return 0;
}
static void __exit mtk_mmsram_exit(void)
{
	platform_driver_unregister(&mmsram_drv);
}
module_init(mtk_mmsram_init);
module_exit(mtk_mmsram_exit);
MODULE_DESCRIPTION("MTK MMSRAM driver");
MODULE_AUTHOR("Anthony Huang<anthony.huang@mediatek.com>");
MODULE_LICENSE("GPL");
