/*
 * Copyright (c) 2012 Samsung Electronics Co., Ltd.
 *      http://www.samsung.com
 *
 * EXYNOS - Thermal Management support
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/power_supply.h>
#include <linux/interrupt.h>
#include <linux/err.h>
#include <linux/io.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/debugfs.h>

#include <plat/cpu.h>

#include <mach/regs-tmu.h>
#include <mach/cpufreq.h>
#include <mach/tmu.h>
#include <mach/map.h>
#include <mach/regs-mem.h>
#include <mach/smc.h>

static DEFINE_MUTEX(tmu_lock);

static struct workqueue_struct *tmu_monitor_wq;

static unsigned int get_refresh_period(unsigned int freq_ref)
{
	unsigned int rclk, tmp, refresh_nsec;

	rclk = freq_ref / 1000000;

#if defined(CONFIG_ARM_TRUSTZONE)
	exynos_smc_read_sfr(SMC_CMD_REG,
		SMC_REG_ID_SFR_R(EXYNOS5_PA_DREXII + EXYNOS_DMC_TIMINGAREF_OFFSET),
		&tmp, 0);
#else
	tmp = __raw_readl(S5P_VA_DREXII + EXYNOS_DMC_TIMINGAREF_OFFSET);
#endif
	refresh_nsec = ((tmp & 0xff) * 1000) / rclk;

	return refresh_nsec;
}

static void set_refresh_period(unsigned int freq_ref,
				unsigned int refresh_nsec)
{
	unsigned int rclk, auto_refresh;

	rclk = freq_ref / 1000000;
	auto_refresh = ((unsigned int)(rclk * refresh_nsec / 1000));

	/* change auto refresh period in TIMING_AREF register of DMC */
#if defined(CONFIG_ARM_TRUSTZONE)
	exynos_smc(SMC_CMD_REG,
		SMC_REG_ID_SFR_W(EXYNOS5_PA_DREXII + EXYNOS_DMC_TIMINGAREF_OFFSET),
		auto_refresh, 0);
#else
	__raw_writel(auto_refresh, S5P_VA_DREXII +
			EXYNOS_DMC_TIMING_AREF_OFFSET);
#endif
}

static int get_cur_temp(struct tmu_info *info)
{
	int curr_temp;
	int temperature;

	curr_temp = __raw_readl(info->tmu_base + CURRENT_TEMP) & 0xff;

	/* compensate and calculate current temperature */
	temperature = curr_temp - info->te1 + TMU_DC_VALUE;

	return temperature;
}

static void tmu_monitor(struct work_struct *work)
{
	struct delayed_work *delayed_work = to_delayed_work(work);
	struct tmu_info *info =
		container_of(delayed_work, struct tmu_info, polling);
	struct tmu_data *data = info->dev->platform_data;
	int cur_temp;

	cur_temp = get_cur_temp(info);

	dev_dbg(info->dev, "Current: %dc, FLAG=%d\n", cur_temp, info->tmu_state);

	mutex_lock(&tmu_lock);
	switch (info->tmu_state) {
	case TMU_STATUS_NORMAL:
		exynos_thermal_unthrottle();
		enable_irq(info->irq);
		goto out;
	case TMU_STATUS_THROTTLED:
		if (cur_temp >= data->ts.start_tripping)
			info->tmu_state = TMU_STATUS_TRIPPED;
		else if (cur_temp > data->ts.stop_throttle)
			exynos_thermal_throttle();
		else
			info->tmu_state = TMU_STATUS_NORMAL;
		break;
	case TMU_STATUS_TRIPPED:
		if (cur_temp >= data->ts.start_emergency)
			panic("Emergency thermal shutdown: temp=%d\n",
			      cur_temp);
		if (cur_temp >= data->ts.start_tripping) {
			pr_err("thermal tripped: temp=%d\n", cur_temp);
			/* Throttle twice while tripping */
			exynos_thermal_throttle();
		} else {
			info->tmu_state = TMU_STATUS_THROTTLED;
		}
		/* Throttle when tripped */
		exynos_thermal_throttle();
		break;
	default:
		break;
	}

	/* Memory throttling */
	if (cur_temp >= data->ts.start_mem_throttle &&
		!info->mem_throttled) {
		set_refresh_period(FREQ_IN_PLL, info->auto_refresh_mem_throttle);
		info->mem_throttled = true;
		dev_dbg(info->dev, "set auto refresh period %dns\n",
				info->auto_refresh_mem_throttle);
	} else if (cur_temp <= data->ts.stop_mem_throttle &&
		info->mem_throttled) {
		set_refresh_period(FREQ_IN_PLL, info->auto_refresh_normal);
		info->mem_throttled = false;
		dev_dbg(info->dev, "set auto refresh period %dns\n",
				info->auto_refresh_normal);
	}

	queue_delayed_work_on(0, tmu_monitor_wq,
			      &info->polling, info->sampling_rate);
out:
	mutex_unlock(&tmu_lock);
}

static void pm_tmu_save(struct tmu_info *info)
{
	info->reg_save[0] = __raw_readl(info->tmu_base + TMU_CON);
	info->reg_save[1] = __raw_readl(info->tmu_base + SAMPLING_INTERNAL);
	info->reg_save[2] = __raw_readl(info->tmu_base + CNT_VALUE0);
	info->reg_save[3] = __raw_readl(info->tmu_base + CNT_VALUE1);
	info->reg_save[4] = __raw_readl(info->tmu_base + INTEN);
	info->reg_save[5] = __raw_readl(info->tmu_base + THD_TEMP_RISE);
}

static void pm_tmu_restore(struct tmu_info *info)
{
	__raw_writel(info->reg_save[5], info->tmu_base + THD_TEMP_RISE);
	__raw_writel(info->reg_save[4], info->tmu_base + INTEN);
	__raw_writel(info->reg_save[3], info->tmu_base + CNT_VALUE1);
	__raw_writel(info->reg_save[2], info->tmu_base + CNT_VALUE0);
	__raw_writel(info->reg_save[1], info->tmu_base + SAMPLING_INTERNAL);
	__raw_writel(info->reg_save[0], info->tmu_base + TMU_CON);
}

static int exynos_tmu_init(struct tmu_info *info)
{
	struct tmu_data *data = info->dev->platform_data;
	unsigned int te_temp, con;
	unsigned int temp_throttle, temp_trip;
	unsigned int rising_thr;

	/* must reload for using efuse value at EXYNOS4212 */
	__raw_writel(TRIMINFO_RELOAD, info->tmu_base + TRIMINFO_CON);

	/* get the compensation parameter */
	te_temp = __raw_readl(info->tmu_base + TRIMINFO);
	info->te1 = te_temp & TRIM_INFO_MASK;
	info->te2 = ((te_temp >> 8) & TRIM_INFO_MASK);

	if ((EFUSE_MIN_VALUE > info->te1)
	    || (info->te1 > EFUSE_MAX_VALUE)
	    || (info->te2 != 0))
		info->te1 = data->efuse_value;

	/* Map auto refresh period of normal & memory throttle mode */
	info->auto_refresh_normal = get_refresh_period(FREQ_IN_PLL);
	info->auto_refresh_mem_throttle = info->auto_refresh_normal / 2;

	dev_info(info->dev, "Current auto refresh interval(%d nsec),"
			" Normal auto refresh interval(%d nsec),"
			" memory throttle auto refresh internal(%d nsec)\n",
			get_refresh_period(FREQ_IN_PLL),
			info->auto_refresh_normal, info->auto_refresh_mem_throttle);

	/*Get rising Threshold and Set interrupt level*/
	temp_throttle = data->ts.start_throttle
			+ info->te1 - TMU_DC_VALUE;
	temp_trip = data->ts.start_tripping
			+ info->te1 - TMU_DC_VALUE;

	rising_thr = (temp_throttle | (temp_trip << 8) |
		     (UNUSED_THRESHOLD << 16) | (UNUSED_THRESHOLD << 24));

	__raw_writel(rising_thr, info->tmu_base + THD_TEMP_RISE);

	/* Set TMU status */
	info->tmu_state = TMU_STATUS_INIT;

	/* To poll current temp, set sampling rate */
	info->sampling_rate = msecs_to_jiffies(1000);

	/* Need to initail regsiter setting after getting parameter info */
	/* [28:23] vref [11:8] slope - Tunning parameter */
	__raw_writel(data->slope, info->tmu_base + TMU_CON);

	__raw_writel((CLEAR_RISE_INT | CLEAR_FALL_INT), info->tmu_base + INTCLEAR);

	/* TMU core enable */
	con = __raw_readl(info->tmu_base + TMU_CON);
	con |= (MUX_ADDR_VALUE << 20 | CORE_EN);

	__raw_writel(con, info->tmu_base + TMU_CON);

	/* Because temperature sensing time is appro 940us,
	* tmu is enabled and 1st valid sample can get 1ms after.
	*/
	mdelay(1);

	/*LEV0 LEV1 interrupt enable */
	__raw_writel(INTEN_RISE0 | INTEN_RISE1, info->tmu_base + INTEN);

	return 0;
}

static irqreturn_t tmu_irq(int irq, void *id)
{
	struct tmu_info *info = id;
	unsigned int status;

	disable_irq_nosync(irq);

	status = __raw_readl(info->tmu_base + INTSTAT);
	/* To handle multiple interrupt pending,
	* interrupt by high temperature are serviced with priority.
	*/
	if (status & INTSTAT_RISE1) {
		dev_info(info->dev, "Tripping interrupt\n");
		info->tmu_state = TMU_STATUS_TRIPPED;
		__raw_writel(INTCLEAR_RISE1, info->tmu_base + INTCLEAR);
	} else if (status & INTSTAT_RISE0) {
		dev_info(info->dev, "Throttling interrupt\n");
		__raw_writel(INTCLEAR_RISE0, info->tmu_base + INTCLEAR);
		info->tmu_state = TMU_STATUS_THROTTLED;
	} else {
		dev_err(info->dev, "%s: TMU interrupt error. INTSTAT : %x\n", __func__, status);
		__raw_writel(status, info->tmu_base + INTCLEAR);
		return IRQ_HANDLED;
	}

	queue_delayed_work_on(0, tmu_monitor_wq, &info->polling, usecs_to_jiffies(0));

	return IRQ_HANDLED;
}

static struct dentry *tmu_debugfs;

static int tmu_debug_show(struct seq_file *s, void *unused)
{
	struct tmu_info *info = s->private;
	char *cur_tmu_state;

	seq_printf(s, "Current Temperature : %d\n", get_cur_temp(info));
	switch (info->tmu_state) {
	case TMU_STATUS_INIT:
	case TMU_STATUS_NORMAL:
		cur_tmu_state = "TMU_STATUS_NORMAL";
		break;
	case TMU_STATUS_THROTTLED:
		cur_tmu_state = "TMU_STATUS_THROTTLED";
		break;
	case TMU_STATUS_TRIPPED:
		cur_tmu_state = "TMU_STATUS_TRIPPED";
		break;
	default:
		cur_tmu_state = "INVALID STATUS";
		break;
	}
	seq_printf(s, "Current TMU State : %s\n", cur_tmu_state);
	seq_printf(s, "Memory Throttling : %s\n",
			info->mem_throttled ? "throttled" : "unthrottled");
	seq_printf(s, "Memory throttle auto refresh time : %d ns\n",
			info->auto_refresh_mem_throttle);
	seq_printf(s, "Normal auto refresh time : %d ns\n", info->auto_refresh_normal);
	seq_printf(s, "TMU monitoring sample rate : %d ms\n",
			info->sampling_rate);

	return 0;
}

static int tmu_debug_open(struct inode *inode, struct file *file)
{
	return single_open(file, tmu_debug_show, inode->i_private);
}

const static struct file_operations tmu_dev_status_fops = {
	.open		= tmu_debug_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= single_release,
};

static int __devinit tmu_probe(struct platform_device *pdev)
{
	struct tmu_info *info;
	struct resource *res;
	int ret;

	if (dev_get_platdata(&pdev->dev) == NULL) {
		dev_err(&pdev->dev, "No platform data\n");
		ret = -ENODEV;
		goto err_out;
	}

	info = devm_kzalloc(&pdev->dev, sizeof(struct tmu_info), GFP_KERNEL);
	if (info == NULL) {
		dev_err(&pdev->dev, "failed to alloc memory!\n");
		ret = -ENOMEM;
		goto err_out;
	}

	info->dev = &pdev->dev;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (res == NULL) {
		dev_err(&pdev->dev, "failed to get memory region resource\n");
		ret = -ENODEV;
		goto err_out;
	}

	info->tmu_base = devm_request_and_ioremap(&pdev->dev, res);
	if (info->tmu_base == NULL) {
		dev_err(&pdev->dev, "failed ioremap()\n");
		ret = -ENOMEM;
		goto err_out;
	}

	INIT_DELAYED_WORK_DEFERRABLE(&info->polling, tmu_monitor);

	tmu_monitor_wq = create_freezable_workqueue("tmu");
	if (!tmu_monitor_wq) {
		dev_err(&pdev->dev, "Creation of tmu_monitor_wq failed\n");
		ret = -EFAULT;
		goto err_out;
	}

	info->irq = platform_get_irq(pdev, 0);
	if (info->irq < 0) {
		dev_err(&pdev->dev, "no irq for thermal\n");
		ret = info->irq;
		goto err_wq;
	}

	platform_set_drvdata(pdev, info);

	ret = exynos_tmu_init(info);
	if (ret < 0)
		goto err_noinit;

	ret = request_irq(info->irq, tmu_irq,
			IRQF_DISABLED, "tmu", info);
	if (ret) {
		dev_err(&pdev->dev, "IRQ%d error %d\n", info->irq, ret);
		ret = -EBUSY;
		goto err_noinit;
	}

	tmu_debugfs =
		debugfs_create_file("tmu_dev_status",
				S_IRUGO, NULL, info, &tmu_dev_status_fops);
	if (IS_ERR_OR_NULL(tmu_debugfs)) {
		tmu_debugfs = NULL;
		dev_err(&pdev->dev, "%s: debugfs_create_file() failed\n", __func__);
		goto err_nodbgfs;
	}

	dev_info(&pdev->dev, "Tmu Initialization is sucessful...!\n");
	return 0;

err_nodbgfs:
	free_irq(info->irq, NULL);
err_noinit:
	platform_set_drvdata(pdev, NULL);
err_wq:
	destroy_workqueue(tmu_monitor_wq);
err_out:
	dev_err(&pdev->dev, "initialization failed.\n");
	return ret;
}

static int __devexit tmu_remove(struct platform_device *pdev)
{
	struct tmu_info *info = platform_get_drvdata(pdev);

	cancel_delayed_work_sync(&info->polling);
	disable_irq(info->irq);
	free_irq(info->irq, info);
	debugfs_remove(tmu_debugfs);
	destroy_workqueue(tmu_monitor_wq);

	platform_set_drvdata(pdev, NULL);

	return 0;
}

#ifdef CONFIG_PM
static int tmu_suspend(struct platform_device *pdev, pm_message_t state)
{
	struct tmu_info *info = platform_get_drvdata(pdev);

	pm_tmu_save(info);
	return 0;
}

static int tmu_resume(struct platform_device *pdev)
{
	struct tmu_info *info = platform_get_drvdata(pdev);

	pm_tmu_restore(info);
	return 0;
}

#else
#define tmu_suspend	NULL
#define tmu_resume	NULL
#endif

static struct platform_driver tmu_driver = {
	.probe		= tmu_probe,
	.remove		= __devexit_p(tmu_remove),
	.suspend	= tmu_suspend,
	.resume		= tmu_resume,
	.driver		= {
		.name	=	"exynos_tmu",
		.owner	=	THIS_MODULE,
	},
};

module_platform_driver(tmu_driver);

