/*
 *  PNX833x Hardware Watchdog Driver
 *  (c) Copyright 2008 NXP
 *
 *  Heavily based upon - IndyDog 0.3 A Hardware Watchdog Device for SGI IP22
 *    (c) Copyright 2002 Guido Guenther <agx@sigxcpu.org>, 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
 *  as published by the Free Software Foundation; either version
 *  2 of the License, or (at your option) any later version.
 *
 *  based on softdog.c by Alan Cox <alan@redhat.com>
 */

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/miscdevice.h>
#include <linux/watchdog.h>
#include <linux/notifier.h>
#include <linux/reboot.h>
#include <linux/spinlock.h>
#include <linux/sysrq.h>
#include <linux/init.h>
#include <pnx833x.h>

#define WATCHDOG_DEFAULT_TIMEOUT_SECONDS	30

#if ! defined (WATCHDOG_START_ENABLED)
#define WATCHDOG_START_ENABLED			0
#endif

#define PFX "PNX833x watchdog: "

#if 0
#define DPRINTK(format, args...) printk(KERN_INFO PFX format, ## args)
#else
#define DPRINTK(format, args...) do { } while (0)
#endif

/** CONFIG block */
#define PNX833X_CONFIG				(0x07000U)
#define PNX833X_CONFIG_CPU_COUNTERS_CONTROL	(0x1C)
#define PNX833X_CONFIG_CPU_WATCHDOG		(0x54)
#define PNX833X_CONFIG_CPU_WATCHDOG_COMPARE	(0x58)

/** RESET block */
#define PNX833X_RESET				(0x08000U)
#define PNX833X_RESET_CONFIG			(0x08)

/** CLOCK block */
#define PNX833X_CLOCK				(0x09000U)
#define PNX833X_CLOCK_PCP_CTL			(0x204)


static spinlock_t pnx833x_wdt_lock;
static unsigned int pnx833x_wdt_timeout_ticks;
static int pnx833x_wdt_opened;
static int expect_close;

static unsigned int pnx833x_wdt_timeout_seconds = WATCHDOG_DEFAULT_TIMEOUT_SECONDS;
module_param(pnx833x_wdt_timeout_seconds, uint, 0);
MODULE_PARM_DESC(timeout_seconds, "Watchdog timeout in seconds (default=" __MODULE_STRING(WATCHDOG_DEFAULT_TIMEOUT_SECONDS) ")");

static int nowayout = WATCHDOG_NOWAYOUT;
module_param(nowayout, int, 0);
MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default=" __MODULE_STRING(WATCHDOG_NOWAYOUT) ")");

static int start_enabled = WATCHDOG_START_ENABLED;
module_param(start_enabled, int, 0);
MODULE_PARM_DESC(start_enabled, "Watchdog is started on module insertion (default=" __MODULE_STRING(WATCHDOG_START_ENABLED) ")");

static void wdwarn_func(unsigned long dummy)
{
	pr_warning("Watchdog warning\n");
	// This generates lots of output that may cause us to lose
	// more interesting information.
	//show_state();
	show_mem();

	{
		// If our application is running send it a SIGKILL so that we
		// get an ltcore dump
		struct task_struct *p;
		for_each_process(p) {
			if (!strcmp(p->comm,"bs")) {
				kill_proc(p->pid,SIGKILL,1);
				break;
			}
		}
	}
}

static DEFINE_TIMER(wdwarn_timer, wdwarn_func, 0, 0);

static unsigned int pnx833x_pibus_hz (void)
{
	unsigned int crystal_hz = 25 * 1000000;
	unsigned int usb_pll_hz = 480 * 1000000;
	unsigned int pcp_ctl = PNX833X_REG(PNX833X_CLOCK + PNX833X_CLOCK_PCP_CTL);

	switch (pcp_ctl & 0x03) {
	case 0x00:
	case 0x02:
	default:
		return 0;

	case 0x01:
		return crystal_hz;

	case 0x03:
		/*
		 * Note: Default divider is 7 (ie PIBus clock == 68.571 MHz)
		 */
		return usb_pll_hz / (6 + ((pcp_ctl >> 2) & 0x03));
	}
}

static int pnx833x_wdt_timeout_init (unsigned int timeout_seconds)
{
	unsigned long long ticks;
	unsigned int pibus_hz = pnx833x_pibus_hz();
	unsigned long flags;

	if (pibus_hz == 0)
		return -ENODEV;

	if (timeout_seconds == 0)
		timeout_seconds = WATCHDOG_DEFAULT_TIMEOUT_SECONDS;

	/*
	 * Max timeout @ 80.000 MHz PI-Bus clock : 53.7 seconds
	 *             @ 68.571 MHz              : 62.6 seconds  <-- default
	 *             @ 60.000 MHz              : 71.6 seconds
	 *             @ 53.333 MHz              : 80.5 seconds
	 */
	ticks = (unsigned long long) pibus_hz * timeout_seconds;
	if (ticks > 0xFFFFFFFF)
		ticks = 0xFFFFFFFF;

	spin_lock_irqsave(&pnx833x_wdt_lock, flags);
	pnx833x_wdt_timeout_ticks = (unsigned int) ticks;
	pnx833x_wdt_timeout_seconds = pnx833x_wdt_timeout_ticks / pibus_hz;
	spin_unlock_irqrestore(&pnx833x_wdt_lock, flags);

	return 0;
}

static void pnx833x_wdt_ping (void)
{
	PNX833X_REG(PNX833X_CONFIG + PNX833X_CONFIG_CPU_WATCHDOG_COMPARE) =
		pnx833x_wdt_timeout_ticks;

	mod_timer(&wdwarn_timer, jiffies + (HZ * 9 * pnx833x_wdt_timeout_seconds)/10);
}

static int pnx833x_wdt_timer_start_stop (int start)
{
	unsigned long flags;
	unsigned int reset_config;
	unsigned int counters_control;

	spin_lock_irqsave(&pnx833x_wdt_lock, flags);

	counters_control =
		PNX833X_REG(PNX833X_CONFIG + PNX833X_CONFIG_CPU_COUNTERS_CONTROL);
	reset_config = PNX833X_REG(PNX833X_RESET + PNX833X_RESET_CONFIG);
	if (start) {
		counters_control |= (1 << 0);
		reset_config |= (1 << 0);
	}
	else {
		counters_control &= ~(1 << 0);
		reset_config &= ~(1 << 0);
	}
	pnx833x_wdt_ping();
	PNX833X_REG(PNX833X_CONFIG + PNX833X_CONFIG_CPU_COUNTERS_CONTROL) =
		counters_control;
	PNX833X_REG(PNX833X_RESET + PNX833X_RESET_CONFIG) = reset_config;

	if (!start)
		del_timer_sync(&wdwarn_timer);

	spin_unlock_irqrestore(&pnx833x_wdt_lock, flags);

	DPRINTK("%s\n", start ? "Started" : "Stopped");

	return 0;
}

static int pnx833x_wdt_start (void)
{
	return pnx833x_wdt_timer_start_stop(1);
}

static int pnx833x_wdt_stop (void)
{
	return pnx833x_wdt_timer_start_stop(0);
}

static int pnx833x_wdt_open (struct inode *inode, struct file *file)
{
	int ret;
	unsigned long flags;

	spin_lock_irqsave(&pnx833x_wdt_lock, flags);
	ret = pnx833x_wdt_opened ? -EBUSY : 0;
	pnx833x_wdt_opened = 1;
	spin_unlock_irqrestore(&pnx833x_wdt_lock, flags);

	if (ret)
		return ret;

	if (nowayout)
		__module_get(THIS_MODULE);

	if (!start_enabled)
		pnx833x_wdt_start();
	else
		pnx833x_wdt_ping();

	DPRINTK("Opened\n");

	return nonseekable_open(inode, file);
}

static int pnx833x_wdt_release (struct inode *inode, struct file *file)
{
	if (!nowayout && expect_close == 42)
		pnx833x_wdt_stop();
	else
		pr_crit(PFX "Unexpected close, not stopping watchdog!\n");

	pnx833x_wdt_opened = 0;

	DPRINTK("Released\n");

	return 0;
}

static ssize_t pnx833x_wdt_write (struct file *file, const char *data,
				  size_t len, loff_t *ppos)
{
	if (len) {
		size_t i;
		if (!nowayout) {
			expect_close = 0;
			for (i = 0; i != len; i++) {
				char c;
				
				if (get_user(c, data + i))
					return -EFAULT;
				if (c == 'V')
					expect_close = 42;
			}
		}
		pnx833x_wdt_ping();
	}

	return len;
}

static const struct watchdog_info pnx833x_wdt_ident = {
	.options		= WDIOF_SETTIMEOUT,
	.firmware_version	= 0,
	.identity		= "PNX833x Hardware Watchdog",
};

static int pnx833x_wdt_ioctl (struct inode *inode, struct file *file,
			      unsigned int cmd, unsigned long arg)
{
	int ret, options;
	unsigned int new_timeout_seconds;

	switch (cmd) {
	case WDIOC_GETSUPPORT:
		if (copy_to_user((struct watchdog_info *) arg,
				 &pnx833x_wdt_ident,
				 sizeof(struct watchdog_info)))
			return -EFAULT;
		return 0;

	case WDIOC_SETOPTIONS:
		if (get_user(options, (int *) arg))
			return -EFAULT;
		if (options & WDIOS_DISABLECARD)
			return pnx833x_wdt_stop();
		if (options & WDIOS_ENABLECARD)
			return pnx833x_wdt_start();
		return 0;

	case WDIOC_KEEPALIVE:
		pnx833x_wdt_ping();
		return 0;

	case WDIOC_SETTIMEOUT:
		if (get_user(new_timeout_seconds, (unsigned int *) arg))
			return -EFAULT;
		if ((ret = pnx833x_wdt_timeout_init(new_timeout_seconds)) != 0)
			return ret;
		/* fallthrough */
	case WDIOC_GETTIMEOUT:
		return put_user(pnx833x_wdt_timeout_seconds,
				(unsigned int *) arg);

	default:
		return -ENOTTY;
	}
}

/*
 * Fixme: What about hangs during restart ?
 *        Maybe setting a medium (10 second ?) timeout would be better here...
 */
static int pnx833x_wdt_notify_sys (struct notifier_block *this,
				   unsigned long code, void *unused)
{
	if (code == SYS_DOWN || code == SYS_HALT)
		pnx833x_wdt_stop();

	return NOTIFY_DONE;
}

static const struct file_operations pnx833x_wdt_fops = {
	.owner		= THIS_MODULE,
	.llseek		= no_llseek,
	.write		= pnx833x_wdt_write,
	.ioctl		= pnx833x_wdt_ioctl,
	.open		= pnx833x_wdt_open,
	.release	= pnx833x_wdt_release,
};

static struct miscdevice pnx833x_wdt_miscdev = {
	.minor		= WATCHDOG_MINOR,
	.name		= "watchdog",
	.fops		= &pnx833x_wdt_fops,
};

static struct notifier_block pnx833x_wdt_notifier = {
	.notifier_call = pnx833x_wdt_notify_sys,
};

static int __init watchdog_init (void)
{
	int ret, cause;

	spin_lock_init(&pnx833x_wdt_lock);

	ret = pnx833x_wdt_timeout_init(pnx833x_wdt_timeout_seconds);
	if (ret) {
		printk(KERN_ERR PFX "cannot determine PI-Bus clock speed\n");
		return ret;
	}

	ret = register_reboot_notifier(&pnx833x_wdt_notifier);
	if (ret) {
		printk(KERN_ERR PFX
		       "cannot register reboot notifier (err=%d)\n", ret);
		return ret;
	}

	ret = misc_register(&pnx833x_wdt_miscdev);
	if (ret) {
		printk(KERN_ERR PFX
		       "cannot register miscdev on minor=%d (err=%d)\n",
		       WATCHDOG_MINOR, ret);
		unregister_reboot_notifier(&pnx833x_wdt_notifier);
		return ret;
	}

	printk(KERN_INFO PFX "Loaded (nowayout: %d, start_enabled: %d)\n",
	       nowayout, start_enabled);

	if (start_enabled)
		pnx833x_wdt_start();

	DPRINTK("PI Bus clock %u Hz\n", pnx833x_pibus_hz());
	DPRINTK("timeout %u seconds\n", pnx833x_wdt_timeout_seconds);
	DPRINTK("timeout %u ticks\n", pnx833x_wdt_timeout_ticks);

	/* Let's check the reason for the reset */
	cause = PNX833X_REG(PNX833X_RESET);
	/* If bit 31 is set then watchdog was cause of reset */
	if (cause & 0x80000000)
		printk(KERN_INFO PFX "The system was previously reset due to the watchdog firing - please investigate...\n");

	return 0;
}

static void __exit watchdog_exit(void)
{
	misc_deregister(&pnx833x_wdt_miscdev);
	unregister_reboot_notifier(&pnx833x_wdt_notifier);
}

module_init(watchdog_init);
module_exit(watchdog_exit);

MODULE_AUTHOR("Daniel Laird/Andre McCurdy");
MODULE_DESCRIPTION("PNX833x Hardware Watchdog");
MODULE_LICENSE("GPL");
MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR);

