// SPDX-License-Identifier: (GPL-2.0 OR BSD-3-Clause)
/*
 * Copyright (c) 2023 MediaTek Inc.
 *
 */

#include <linux/bitops.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/platform_device.h>
#include <linux/printk.h>
#include <linux/slab.h>
#include <soc/mediatek/emi.h>

struct emi_cen {
	/*
	 * EMI setting from device tree
	 */
	unsigned int emi_cen_cnt;
	unsigned int ch_cnt;
	unsigned int rk_cnt;
	unsigned long long *rk_size;
	void __iomem **emi_cen_base;
	void __iomem **emi_chn_base;

	/* address from the sysfs file for EMI addr2dram */
	unsigned long a2d_addr;

	/*
	 * EMI addr2dram settings from device tree
	 */
	unsigned int a2d_ver;
	unsigned int disph;
	unsigned int hash;

	/*
	 * EMI addr2dram settings calculated at run time
	 */
	unsigned long offset;
	unsigned long max;
	unsigned int magics[8];
	unsigned long cas;
	unsigned long chab_rk0_sz, chab_rk1_sz;
	unsigned long chcd_rk0_sz, chcd_rk1_sz;
	unsigned int channels;
	unsigned int dualrk_ch0, dualrk_ch1;
	unsigned int chn_hash_lsb, chnpos;
	unsigned int chab_row_mask[2], chcd_row_mask[2];
	unsigned int chab_col_mask[2], chcd_col_mask[2];
	unsigned int dw32;
	unsigned int chn_4bank_mode;
};

#define EMI_CONA_DW32_EN 1
#define EMI_CONA_CHN_POS_0 2
#define EMI_CONA_CHN_POS_1 3
#define EMI_CONA_COL 4
#define EMI_CONA_COL2ND 6
#define EMI_CONA_CHN_EN 8
#define EMI_CONA_ROW 12
#define EMI_CONA_ROW2ND 14
#define EMI_CONA_DUAL_RANK_EN_CHN1 16
#define EMI_CONA_DUAL_RANK_EN 17
#define EMI_CONA_CAS_SIZE 18
#define EMI_CONA_CHN1_COL 20
#define EMI_CONA_CHN1_COL2ND 22
#define EMI_CONA_ROW_EXT0 24
#define EMI_CONA_ROW2ND_EXT0 25
#define EMI_CONA_CAS_SIZE_BIT3 26
#define EMI_CONA_CHN1_ROW 28
#define EMI_CONA_CHN1_ROW2ND 30

#define EMI_CONH_CHN1_ROW_EXT0 4
#define EMI_CONH_CHN1_ROW2ND_EXT0 5
#define EMI_CONH_CHNAB_RANK0_SIZE 16
#define EMI_CONH_CHNAB_RANK1_SIZE 20
#define EMI_CONH_CHNCD_RANK0_SIZE 24
#define EMI_CONH_CHNCD_RANK1_SIZE 28

#define EMI_CONH_2ND_CHN_4BANK_MODE 6

#define EMI_CONK_CHNAB_RANK0_SIZE_EXT 16
#define EMI_CONK_CHNAB_RANK1_SIZE_EXT 20
#define EMI_CONK_CHNCD_RANK0_SIZE_EXT 24
#define EMI_CONK_CHNCD_RANK1_SIZE_EXT 28

#define MTK_EMI_DRAM_OFFSET 0x40000000
#define MTK_EMI_HASH 0x7
#define MTK_EMI_DISPATCH 0x0
#define MTK_EMI_A2D_VERSION 1

#define MAX_CEN_EMI_NAME_LEN		10
#define EMI_ARBA	(0x100)
#define EMI_ARBC	(0x110)
#define EMI_ARBD	(0x118)
#define EMI_ARBE	(0x120)
#define EMI_ARBF	(0x128)
#define EMI_ARBG	(0x130)
#define EMI_ARBH	(0x138)
#define EMI_ARB(p)		(EMI_ARBA + (((p) == 1) ? 0 : (p)) * 0x8)
#define EMI_BW_LMT_MASK		GENMASK(5, 0)
/* low 6 bit */
#define EMI_GET_BW_LMT(ori)	((ori) & EMI_BW_LMT_MASK)
#define EMI_BW_HFILTER_EN	(14)

static unsigned int emi_a2d_con_offset[] = {
	0x00, 0x28, 0x38, 0x3c, 0x50,
};

/* global pointer for exported functions */
static struct emi_cen *global_emi_cen;

/*
 * NoteXXX: emimpu_ap_region_init() will initialize the latest EMI MPU
 * region for AP CPU. It must be called after initialization of drivers
 * in which call mtk_emimpu_set_protection() to initialize other EMI
 * MPU regions.
 *
 * For guaranteeing the call sequence, emimpu_ap_region_init() is
 * either called via late_initcall_sync (when EMI drivers are not built
 * as kernel modules) or called in emicen_init() (When EMI drivers are
 * kernel modules).
 *
 * Scenario #1: EMI drivers are kernel modules:
 * Other drivers (which call mtk_emimpu_set_protection()) must be
 * loaded before emi-cen.ko is loaded.
 *
 * Scenario #2. EMI drivers are NOT kernel modules:
 * Other drivers must be initialized before kernel's initcall stage.
 */
#if !defined(MODULE)
static int __init emicen_init_mpu_ap_region(void)
{
	emimpu_ap_region_init();
}
late_initcall_sync(emicen_init_mpu_ap_region);
#endif

/*
 * mtk_cen_emi_bwl_get - get the bandwidth limiter value of emi port
 *
 * Returns the function status
 */
int mtk_cen_emi_bwl_get(u32 emi_id, u32 port, u32 *val)
{
	void __iomem *base;
	struct device_node *np;
	char dev_name[MAX_CEN_EMI_NAME_LEN];
	int num = 0;
	u32 emi_cnt;
	u32 emi_port_cnt;
	u32 value;

	np = of_find_node_by_name(NULL, "emi_arch");
	if (!np)
		return -1;

	if (of_property_read_u32(np, "emi_cnt", &emi_cnt) < 0) {
		pr_err("[MTK-SMI]emi_cen_bwl_get can not find node emi_cnt\n");
		return -1;
	}

	if (of_property_read_u32(np, "emi_port_cnt", &emi_port_cnt) < 0) {
		pr_err("[MTK-SMI]emi_cen_bwl_get can not find node emi_port_cnt\n");
		return -1;
	}

	if (port >= emi_port_cnt || emi_id >= emi_cnt) {
		pr_err("[MTK-SMI]emi_cen_bwl_get bad port: %u\n", port);
		return -ENODEV;
	}

	num = snprintf(dev_name, MAX_CEN_EMI_NAME_LEN, "emi_cen%d", emi_id);

	if (num >= MAX_CEN_EMI_NAME_LEN)
		return -1;

	np = of_find_node_by_name(NULL, dev_name);
	if (!np) {
		pr_err("[MTK-SMI]emi_cen_bwl_get can not find node %s\n", dev_name);
		return -1;
	}

	base = of_iomap(np, 0);
	if (!base) {
		pr_err("[MTK-SMI]emi_cen_bwl_get %pOF: of_iomap failed\n", np);
		return -1;
	}

	value = readl(base + EMI_ARB(port));
	*val = EMI_GET_BW_LMT(value);
	iounmap(base);
	return 0;
}
EXPORT_SYMBOL_GPL(mtk_cen_emi_bwl_get);

/*
 * mtk_emicen_get_ch_cnt - set the bandwidth limiter value of emi port
 *
 * Returns the function status
 */
int mtk_cen_emi_bwl_set(u32 emi_id, u32 port, u32 bw_lmt, bool hard_mode)
{
	void __iomem *base;
	struct device_node *np;
	char dev_name[MAX_CEN_EMI_NAME_LEN];
	int num = 0;
	u32 emi_cnt;
	u32 emi_port_cnt;
	u32 value;

	np = of_find_node_by_name(NULL, "emi_arch");
	if (!np)
		return -1;

	if (of_property_read_u32(np, "emi_cnt", &emi_cnt) < 0) {
		pr_err("[MTK-SMI]emi_cen_bwl_set can not find node emi_cnt\n");
		return -1;
	}

	if (of_property_read_u32(np, "emi_port_cnt", &emi_port_cnt) < 0) {
		pr_err("[MTK-SMI]emi_cen_bwl_set can not find node emi_port_cnt\n");
		return -1;
	}

	if (port >= emi_port_cnt || emi_id >= emi_cnt) {
		pr_err("[MTK-SMI]emi_cen_bwl_set bad emi: %u port: %u\n", emi_id, port);
		return -ENODEV;
	}

	num = snprintf(dev_name, MAX_CEN_EMI_NAME_LEN, "emi_cen%d", emi_id);

	if (num >= MAX_CEN_EMI_NAME_LEN)
		return -1;

	np = of_find_node_by_name(NULL, dev_name);
	if (!np) {
		pr_err("[MTK-SMI]emi_cen_bwl_set can not find node\n");
		return -1;
	}

	base = of_iomap(np, 0);
	if (!base) {
		pr_err("[MTK-SMI]emi_cen_bwl_set %pOF: of_iomap failed\n", np);
		return -1;
	}

	value = (readl(base + EMI_ARB(port)) & ~EMI_BW_LMT_MASK);
	value |= (bw_lmt & EMI_BW_LMT_MASK);
	if (hard_mode)
		value &= (~BIT(EMI_BW_HFILTER_EN));
	else
		value |= (BIT(EMI_BW_HFILTER_EN));

	writel(value, base + EMI_ARB(port));
	iounmap(base);
	return 0;
}
EXPORT_SYMBOL_GPL(mtk_cen_emi_bwl_set);

/*
 * mtk_emicen_get_ch_cnt - get the channel count
 *
 * Returns the channel count
 */
unsigned int mtk_emicen_get_ch_cnt(void)
{
	return (global_emi_cen) ? global_emi_cen->ch_cnt : 0;
}
EXPORT_SYMBOL(mtk_emicen_get_ch_cnt);

/*
 * mtk_emicen_get_rk_cnt - get the rank count
 *
 * Returns the rank count
 */
unsigned int mtk_emicen_get_rk_cnt(void)
{
	return (global_emi_cen) ? global_emi_cen->rk_cnt : 0;
}
EXPORT_SYMBOL(mtk_emicen_get_rk_cnt);

/*
 * mtk_emicen_get_rk_size - get the rank size of target rank
 * @rk_id:	the id of target rank
 *
 * Returns the rank size of target rank
 */
unsigned int mtk_emicen_get_rk_size(unsigned int rk_id)
{
	if (rk_id < mtk_emicen_get_rk_cnt())
		return (global_emi_cen) ? global_emi_cen->rk_size[rk_id] : 0;
	else
		return 0;
}
EXPORT_SYMBOL(mtk_emicen_get_rk_size);

/*
 * prepare_a2d: a helper function to initialize and calculate settings for the
 *		mtk_emicen_addr2dram() function
 *
 * There is no code comment for the translation. This is intended since
 * the fomular of translation is derived from the implementation of EMI.
 */
static inline void prepare_a2d(struct emi_cen *cen)
{
	const unsigned int mask_4b = 0xf, mask_2b = 0x3;
	void __iomem *emi_cen_base;
	unsigned long emi_cona;
	unsigned long emi_conf;
	unsigned long emi_conh;
	unsigned long emi_conh_2nd;
	unsigned long emi_conk;
	unsigned long tmp;

	if (!cen)
		return;

	emi_cen_base = cen->emi_cen_base[0];

	emi_cona = readl(emi_cen_base + emi_a2d_con_offset[0]);
	emi_conf = readl(emi_cen_base + emi_a2d_con_offset[1]);
	emi_conh = readl(emi_cen_base + emi_a2d_con_offset[2]);
	emi_conh_2nd = readl(emi_cen_base + emi_a2d_con_offset[3]);
	emi_conk = readl(emi_cen_base + emi_a2d_con_offset[4]);

	cen->offset = MTK_EMI_DRAM_OFFSET;

	cen->magics[0] = emi_conf & mask_4b;
	cen->magics[1] = (emi_conf >> 4) & mask_4b;
	cen->magics[2] = (emi_conf >> 8) & mask_4b;
	cen->magics[3] = (emi_conf >> 12) & mask_4b;
	cen->magics[4] = (emi_conf >> 16) & mask_4b;
	cen->magics[5] = (emi_conf >> 20) & mask_4b;
	cen->magics[6] = (emi_conf >> 24) & mask_4b;
	cen->magics[7] = (emi_conf >> 28) & mask_4b;

	cen->dw32 = test_bit(EMI_CONA_DW32_EN, &emi_cona) ? 1 : 0;

	cen->channels = (emi_cona >> EMI_CONA_CHN_EN) & mask_2b;

	cen->cas = (emi_cona >> EMI_CONA_CAS_SIZE) & mask_2b;
	cen->cas += cen->dw32 << 2;
	cen->cas += ((emi_cona >> EMI_CONA_CAS_SIZE_BIT3) & 1) << 3;
	cen->cas = cen->cas << 28;
	cen->cas = cen->cas << cen->channels;

	cen->dualrk_ch0 = test_bit(EMI_CONA_DUAL_RANK_EN, &emi_cona) ? 1 : 0;
	cen->dualrk_ch1 = test_bit(EMI_CONA_DUAL_RANK_EN_CHN1,
				&emi_cona) ? 1 : 0;

	cen->chn_hash_lsb = 7 + (cen->hash & (~(cen->hash) + 1));
	if (cen->hash)
		cen->chnpos = cen->chn_hash_lsb;
	else {
		cen->chnpos = test_bit(EMI_CONA_CHN_POS_1, &emi_cona) ? 2 : 0;
		cen->chnpos |= test_bit(EMI_CONA_CHN_POS_0, &emi_cona) ? 1 : 0;
	}

	tmp = (emi_conh >> EMI_CONH_CHNAB_RANK0_SIZE) & mask_4b;
	tmp += ((emi_conk >> EMI_CONK_CHNAB_RANK0_SIZE_EXT) & mask_4b) << 4;
	if (tmp)
		cen->chab_rk0_sz = tmp << 8;
	else {
		tmp = (emi_cona >> EMI_CONA_COL) & mask_2b;
		tmp += (emi_cona >> EMI_CONA_ROW) & mask_2b;
		tmp += test_bit(EMI_CONA_ROW_EXT0, &emi_cona) ? 4 : 0;
		tmp += cen->dw32;
		tmp += 7;
		cen->chab_rk0_sz = 1 << tmp;
	}

	tmp = (emi_conh >> EMI_CONH_CHNAB_RANK1_SIZE) & mask_4b;
	tmp += ((emi_conk >> EMI_CONK_CHNAB_RANK1_SIZE_EXT) & mask_4b) << 4;
	if (tmp)
		cen->chab_rk1_sz = tmp << 8;
	else if (!test_bit(EMI_CONA_DUAL_RANK_EN, &emi_cona))
		cen->chab_rk1_sz = 0;
	else {
		tmp = (emi_cona >> EMI_CONA_COL2ND) & mask_2b;
		tmp += (emi_cona >> EMI_CONA_ROW2ND) & mask_2b;
		tmp += test_bit(EMI_CONA_ROW2ND_EXT0, &emi_cona) ? 4 : 0;
		tmp += cen->dw32;
		tmp += 7;
		cen->chab_rk1_sz = 1 << tmp;
	}

	tmp = (emi_conh >> EMI_CONH_CHNCD_RANK0_SIZE) & mask_4b;
	tmp += ((emi_conk >> EMI_CONK_CHNCD_RANK0_SIZE_EXT) & mask_4b) << 4;
	if (tmp)
		cen->chcd_rk0_sz = tmp << 8;
	else {
		tmp = (emi_cona >> EMI_CONA_CHN1_COL) & mask_2b;
		tmp += (emi_cona >> EMI_CONA_CHN1_ROW) & mask_2b;
		tmp += test_bit(EMI_CONH_CHN1_ROW_EXT0, &emi_conh) ? 4 : 0;
		tmp += cen->dw32;
		tmp += 7;
		cen->chcd_rk0_sz = 1 << tmp;
	}

	tmp = (emi_conh >> EMI_CONH_CHNCD_RANK1_SIZE) & mask_4b;
	tmp += ((emi_conk >> EMI_CONK_CHNCD_RANK1_SIZE_EXT) & mask_4b) << 4;
	if (tmp)
		cen->chcd_rk1_sz = tmp << 8;
	else if (!test_bit(EMI_CONA_DUAL_RANK_EN_CHN1, &emi_cona))
		cen->chcd_rk1_sz = 0;
	else {
		tmp = (emi_cona >> EMI_CONA_CHN1_COL2ND) & mask_2b;
		tmp += (emi_cona >> EMI_CONA_CHN1_ROW2ND) & mask_2b;
		tmp += test_bit(EMI_CONH_CHN1_ROW2ND_EXT0, &emi_conh) ? 4 : 0;
		tmp += cen->dw32;
		tmp += 7;
		cen->chcd_rk1_sz = 1 << tmp;
	}

	cen->max = cen->chab_rk0_sz + cen->chab_rk1_sz;
	cen->max += cen->chcd_rk0_sz + cen->chcd_rk0_sz;
	if ((cen->channels > 1) || (cen->disph > 0))
		cen->max *= 2;
	cen->max = cen->max << 20;

	cen->chab_row_mask[0] = (emi_cona >> EMI_CONA_ROW) & mask_2b;
	cen->chab_row_mask[0] += test_bit(EMI_CONA_ROW_EXT0,
					&emi_cona) ? 4 : 0;
	cen->chab_row_mask[0] += 13;
	cen->chab_row_mask[1] = (emi_cona >> EMI_CONA_ROW2ND) & mask_2b;
	cen->chab_row_mask[1] += test_bit(EMI_CONA_ROW2ND_EXT0,
					&emi_cona) ? 4 : 0;
	cen->chab_row_mask[1] += 13;

	cen->chcd_row_mask[0] = (emi_cona >> EMI_CONA_CHN1_ROW) & mask_2b;
	cen->chcd_row_mask[0] += test_bit(EMI_CONH_CHN1_ROW_EXT0,
					&emi_conh) ? 4 : 0;
	cen->chcd_row_mask[0] += 13;
	cen->chcd_row_mask[1] = (emi_cona >> EMI_CONA_CHN1_ROW2ND) & mask_2b;
	cen->chcd_row_mask[1] += test_bit(EMI_CONH_CHN1_ROW2ND_EXT0,
					&emi_conh) ? 4 : 0;
	cen->chcd_row_mask[1] += 13;

	cen->chab_col_mask[0] = (emi_cona >> EMI_CONA_COL) & mask_2b;
	cen->chab_col_mask[0] += 9;
	cen->chab_col_mask[1] = (emi_cona >> EMI_CONA_COL2ND) & mask_2b;
	cen->chab_col_mask[1] += 9;

	cen->chcd_col_mask[0] = (emi_cona >> EMI_CONA_CHN1_COL) & mask_2b;
	cen->chcd_col_mask[0] += 9;
	cen->chcd_col_mask[1] = (emi_cona >> EMI_CONA_CHN1_COL2ND) & mask_2b;
	cen->chcd_col_mask[1] += 9;

	cen->chn_4bank_mode = test_bit(EMI_CONH_2ND_CHN_4BANK_MODE,
					&emi_conh_2nd) ? 1 : 0;
}

/*
 * use_a2d_magic: a helper function to calculate the input address
 *		for the mtk_emicen_addr2dram() function
 *
 * There is no code comment for the translation. This is intended since
 * the fomular of translation is derived from the implementation of EMI.
 */
static inline unsigned int use_a2d_magic(unsigned long addr, unsigned int bit)
{
	unsigned long magic;
	unsigned int ret;

	if (!global_emi_cen)
		return 0;

	magic = global_emi_cen->magics[((bit >= 9) & (bit <= 16)) ?
					(bit - 9) : 0];

	ret = test_bit(bit, &addr) ? 1 : 0;
	ret ^= (test_bit(16, &addr) && test_bit(0, &magic)) ? 1 : 0;
	ret ^= (test_bit(17, &addr) && test_bit(1, &magic)) ? 1 : 0;
	ret ^= (test_bit(18, &addr) && test_bit(2, &magic)) ? 1 : 0;
	ret ^= (test_bit(19, &addr) && test_bit(3, &magic)) ? 1 : 0;

	return ret;
}

/*
 * mtk_emicen_addr2dram_v1 - Translate a physical address to
			a DRAM-point-of-view map for EMI v1
 * @addr - input physical address
 * @map - output map stored in struct emi_addr_map
 *
 * Return 0 on success, -1 on failures.
 *
 * There is no code comment for the translation. This is intended since
 * the fomular of translation is derived from the implementation of EMI.
 */
static int mtk_emicen_addr2dram_v1(unsigned long addr,
					struct emi_addr_map *map)
{
	unsigned long disph, hash;
	unsigned long saddr, bfraddr, chnaddr;
	unsigned long max_rk0_sz;
	unsigned int tmp;
	unsigned int chn_hash_lsb, row_mask, col_mask;
	bool ch_ab_not_cd;

	if (!global_emi_cen)
		return -1;
	if (!map)
		return -1;
	if (addr < global_emi_cen->offset)
		return -1;
	if (addr > global_emi_cen->max)
		return -1;

	addr -= global_emi_cen->offset;

	map->emi = -1;
	map->channel = -1;
	map->rank = -1;
	map->bank = -1;
	map->row = -1;
	map->column = -1;

	disph = global_emi_cen->disph;
	hash = global_emi_cen->hash;
	chn_hash_lsb = global_emi_cen->chn_hash_lsb;

	tmp = (test_bit(8, &addr) & test_bit(0, &disph)) ? 1 : 0;
	tmp ^= (test_bit(9, &addr) & test_bit(1, &disph)) ? 1 : 0;
	tmp ^= (test_bit(10, &addr) & test_bit(2, &disph)) ? 1 : 0;
	tmp ^= (test_bit(11, &addr) & test_bit(3, &disph)) ? 1 : 0;
	map->emi = tmp;

	saddr = addr;
	clear_bit(9, &saddr);
	clear_bit(10, &saddr);
	clear_bit(11, &saddr);
	clear_bit(12, &saddr);
	clear_bit(13, &saddr);
	clear_bit(14, &saddr);
	clear_bit(15, &saddr);
	clear_bit(16, &saddr);
	saddr |= use_a2d_magic(addr, 9) << 9;
	saddr |= use_a2d_magic(addr, 10) << 10;
	saddr |= use_a2d_magic(addr, 11) << 11;
	saddr |= use_a2d_magic(addr, 12) << 12;
	saddr |= use_a2d_magic(addr, 13) << 13;
	saddr |= use_a2d_magic(addr, 14) << 14;
	saddr |= use_a2d_magic(addr, 15) << 15;
	saddr |= use_a2d_magic(addr, 16) << 16;

	if (global_emi_cen->disph <= 0)
		bfraddr = saddr;
	else {
		tmp = 7 + __ffs(disph);
		bfraddr = (saddr >> (tmp + 1)) << tmp;
		bfraddr += saddr & ((1 << tmp) - 1);
	}

	if (bfraddr < global_emi_cen->cas)
		return -1;

	if (!global_emi_cen->channels)
		map->channel = global_emi_cen->channels;
	else if (hash) {
		tmp = (test_bit(8, &addr) && test_bit(0, &hash)) ? 1 : 0;
		tmp ^= (test_bit(9, &addr) && test_bit(1, &hash)) ? 1 : 0;
		tmp ^= (test_bit(10, &addr) && test_bit(2, &hash)) ? 1 : 0;
		tmp ^= (test_bit(11, &addr) && test_bit(3, &hash)) ? 1 : 0;
		map->channel = tmp;
	} else {
		if (global_emi_cen->channels == 1) {
			tmp = 0;
			switch (global_emi_cen->chnpos) {
			case 0:
				tmp = 7;
				break;
			case 1:
				tmp = 8;
				break;
			case 2:
				tmp = 9;
				break;
			case 3:
				tmp = 12;
				break;
			default:
				return -1;
			}
			map->channel = (bfraddr >> tmp) % 2;
		} else if (global_emi_cen->channels == 2) {
			tmp = 0;
			switch (global_emi_cen->chnpos) {
			case 0:
				tmp = 7;
				break;
			case 1:
				tmp = 8;
				break;
			case 2:
				tmp = 9;
				break;
			case 3:
				tmp = 12;
				break;
			default:
				return -1;
			}
			map->channel = (bfraddr >> tmp) % 4;
		} else {
			return -1;
		}
	}

	if (map->channel > 1)
		ch_ab_not_cd = 0;
	else {
		if (map->channel == 1)
			ch_ab_not_cd = (global_emi_cen->channels > 1) ? 1 : 0;
		else
			ch_ab_not_cd = 1;
	}

	max_rk0_sz = (ch_ab_not_cd) ?
			global_emi_cen->chab_rk0_sz :
			global_emi_cen->chcd_rk0_sz;
	max_rk0_sz = max_rk0_sz << 20;

	if (!global_emi_cen->channels)
		chnaddr = bfraddr;
	else if (global_emi_cen->chnpos > 3) {
		tmp = chn_hash_lsb;
		chnaddr = bfraddr >> (tmp + 1);
		chnaddr = chnaddr << tmp;
		chnaddr += bfraddr & ((1 << tmp) - 1);
	} else if (global_emi_cen->channels == 1 ||
			global_emi_cen->channels == 2) {
		tmp = 0;
		switch (global_emi_cen->chnpos) {
		case 0:
			tmp = 7;
			break;
		case 1:
			tmp = 8;
			break;
		case 2:
			tmp = 9;
			break;
		case 3:
			tmp = 12;
			break;
		default:
			break;
		}
		chnaddr = bfraddr >> (tmp + (global_emi_cen->channels - 1));
		chnaddr = chnaddr << tmp;
		chnaddr += bfraddr & ((1 << tmp) - 1);
	} else {
		return -1;
	}

	if ((map->channel) ?
		!global_emi_cen->dualrk_ch1 :
		!global_emi_cen->dualrk_ch0)
		map->rank = 0;
	else {
		if (chnaddr > max_rk0_sz)
			map->rank = 1;
		else
			map->rank = 0;
	}

	row_mask = (ch_ab_not_cd) ?
			((map->rank) ?
				global_emi_cen->chab_row_mask[1] :
				global_emi_cen->chab_row_mask[0]) :
			((map->rank) ?
				global_emi_cen->chcd_row_mask[1] :
				global_emi_cen->chcd_row_mask[0]);
	col_mask = (ch_ab_not_cd) ?
			((map->rank) ?
				global_emi_cen->chab_col_mask[1] :
				global_emi_cen->chab_col_mask[0]) :
			((map->rank) ?
				global_emi_cen->chcd_col_mask[1] :
				global_emi_cen->chcd_col_mask[0]);

	tmp = chnaddr - (max_rk0_sz * map->rank);
	tmp /= 1 << (global_emi_cen->dw32 + 1 + col_mask + 3);
	tmp &= (1 << row_mask) - 1;
	map->row = tmp;

	tmp = chnaddr;
	tmp /= 1 << (global_emi_cen->dw32 + 1 + col_mask);
	tmp &= ((!global_emi_cen->chn_4bank_mode) ? 8 : 4) - 1;
	map->bank = tmp;

	tmp = chnaddr;
	tmp /= 1 << (global_emi_cen->dw32 + 1);
	tmp &= (1 << col_mask) - 1;
	map->column = tmp;

	return 0;
}

/*
 * mtk_emicen_addr2dram - Translate a physical address to
			a DRAM-point-of-view map
 * @addr - input physical address
 * @map - output map stored in struct emi_addr_map
 *
 * Return 0 on success, -1 on failures.
 *
 * There is no code comment for the translation. This is intended since
 * the fomular of translation is derived from the implementation of EMI.
 */
int mtk_emicen_addr2dram(unsigned long addr, struct emi_addr_map *map)
{
	if (!global_emi_cen)
		return -1;

	if (global_emi_cen->a2d_ver == 1)
		return mtk_emicen_addr2dram_v1(addr, map);
	else
		return -1;
}
EXPORT_SYMBOL(mtk_emicen_addr2dram);

static ssize_t emicen_addr2dram_show(struct device_driver *driver, char *buf)
{
	int ret;
	struct emi_addr_map map;
	unsigned long addr;

	if (!global_emi_cen)
		return 0;

	addr = global_emi_cen->a2d_addr;

	ret = mtk_emicen_addr2dram(addr, &map);
	if (!ret)
		return snprintf(buf, PAGE_SIZE,
		     "0x%lx\n->\nemi%d\nchn%d\nrank%d\nbank%d\nrow%d\ncol%d\n",
		     addr, map.emi, map.channel, map.rank,
		     map.bank, map.row, map.column);
	else
		return snprintf(buf, PAGE_SIZE, "0x%lx\n->failed\n", addr);
}

static ssize_t emicen_addr2dram_store
	(struct device_driver *driver, const char *buf, size_t count)
{
	u64 addr;
	int ret;

	if (!global_emi_cen)
		return count;

	ret = kstrtou64(buf, 16, &addr);
	if (ret)
		return ret;

	global_emi_cen->a2d_addr = (unsigned long)addr;

	return count;
}

static DRIVER_ATTR_RW(emicen_addr2dram);

static int emicen_probe(struct platform_device *pdev)
{
	struct device_node *emicen_node = pdev->dev.of_node;
	struct device_node *emichn_node =
		of_parse_phandle(emicen_node, "mediatek,emi-reg", 0);
	struct emi_cen *cen;
	unsigned int i;
	int ret;

	dev_info(&pdev->dev, "driver probed\n");

	cen = devm_kzalloc(&pdev->dev,
		sizeof(struct emi_cen), GFP_KERNEL);
	if (!cen)
		return -ENOMEM;

	ret = of_property_read_u32(emicen_node,
		"ch_cnt", &(cen->ch_cnt));
	if (ret) {
		dev_crit(&pdev->dev, "No ch_cnt\n");
		return -ENXIO;
	}


	ret = of_property_read_u32(emicen_node,
		"rk_cnt", &(cen->rk_cnt));
	if (ret) {
		dev_err(&pdev->dev, "No rk_cnt\n");
		return -ENXIO;
	}

	cen->rk_size = devm_kmalloc_array(&pdev->dev,
		cen->rk_cnt, sizeof(unsigned long long),
		GFP_KERNEL);
	if (!(cen->rk_size))
		return -ENOMEM;

	ret = of_property_read_u64_array(emicen_node,
		"rk_size", cen->rk_size, cen->rk_cnt);
	if (ret) {
		dev_err(&pdev->dev, "No rk_size\n");
		return -ENXIO;
	}

	ret = of_property_count_elems_of_size(emicen_node,
		"reg", sizeof(unsigned int) * 4);
	if (ret <= 0) {
		dev_err(&pdev->dev, "No reg\n");
		return -ENXIO;
	}

	cen->emi_cen_cnt = (unsigned int)ret;

	cen->emi_cen_base = devm_kmalloc_array(&pdev->dev,
		cen->emi_cen_cnt, sizeof(phys_addr_t), GFP_KERNEL);
	if (!(cen->emi_cen_base))
		return -ENOMEM;

	for (i = 0; i < cen->emi_cen_cnt; i++)
		cen->emi_cen_base[i] = of_iomap(emicen_node, i);

	cen->emi_chn_base = devm_kmalloc_array(&pdev->dev,
		cen->ch_cnt, sizeof(phys_addr_t), GFP_KERNEL);
	if (!(cen->emi_chn_base))
		return -ENOMEM;

	for (i = 0; i < cen->ch_cnt; i++)
		cen->emi_chn_base[i] = of_iomap(emichn_node, i);

	ret = of_property_read_u32(emicen_node,
		"a2d_ver", &(cen->a2d_ver));
	if (ret) {
		dev_info(&pdev->dev, "No a2d_ver\n");
		cen->a2d_ver = MTK_EMI_A2D_VERSION;
	}

	ret = of_property_read_u32(emicen_node,
		"a2d_disph", &(cen->disph));
	if (ret) {
		dev_info(&pdev->dev, "No a2d_disph\n");
		cen->disph = MTK_EMI_DISPATCH;
	}

	ret = of_property_read_u32(emicen_node,
		"a2d_hash", &(cen->hash));
	if (ret) {
		dev_info(&pdev->dev, "No a2d_hash\n");
		cen->hash = MTK_EMI_HASH;
	}

	ret = of_property_read_u32_array(emicen_node,
		"a2d_conf_offset", emi_a2d_con_offset,
		ARRAY_SIZE(emi_a2d_con_offset));
	if (ret)
		dev_info(&pdev->dev, "No a2d_conf_offset\n");

	prepare_a2d(cen);

	global_emi_cen = cen;

	dev_info(&pdev->dev, "%s(%d), %s(%d)\n",
		"ch_cnt", cen->ch_cnt,
		"rk_cnt", cen->rk_cnt);

	for (i = 0; i < cen->rk_cnt; i++)
		dev_info(&pdev->dev, "rk_size%d(0x%llx)\n",
			i, cen->rk_size[i]);

	dev_info(&pdev->dev, "a2d_ver %d\n", cen->a2d_ver);

	dev_info(&pdev->dev, "a2d_disph %d\n", cen->disph);

	dev_info(&pdev->dev, "a2d_hash %d\n", cen->hash);

	for (i = 0; i < ARRAY_SIZE(emi_a2d_con_offset); i++)
		dev_info(&pdev->dev, "emi_a2d_con_offset[%d] %d\n",
			i, emi_a2d_con_offset[i]);

	return 0;
}

static int emicen_remove(struct platform_device *pdev)
{
	global_emi_cen = NULL;

	return 0;
}

static const struct of_device_id emicen_of_ids[] = {
	{.compatible = "mediatek,common-emicen",},
	{}
};

static struct platform_driver emicen_drv = {
	.probe = emicen_probe,
	.remove = emicen_remove,
	.driver = {
		.name = "emicen_drv",
		.owner = THIS_MODULE,
		.of_match_table = emicen_of_ids,
	},
};

static __init int emicen_init(void)
{
	int ret;

	pr_info("emicen was loaded\n");

	ret = platform_driver_register(&emicen_drv);
	if (ret) {
		pr_err("emicen: failed to register driver\n");
		return ret;
	}

	ret = driver_create_file(&emicen_drv.driver,
		&driver_attr_emicen_addr2dram);
	if (ret) {
		pr_err("emicen: failed to create addr2dram file\n");
		return ret;
	}

/*
 * #if defined(MODULE)
 *		emimpu_ap_region_init();
 * #endif
 */
	return 0;
}

module_init(emicen_init);

MODULE_DESCRIPTION("MediaTek EMI Central Driver");
MODULE_LICENSE("GPL v2");
