blob: 2674684f86c8fa508677fba227f5caaefd440ba5 [file] [log] [blame]
/*
* ImgTec PowerDown Controller Watchdog Timer as found in Meta SoCs.
*
* Copyright 2010-2012 Imagination Technologies Ltd.
*
* Parts derived from mpcore_wdt.
*/
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/watchdog.h>
#include <linux/platform_device.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/log2.h>
#include <linux/spinlock.h>
#include <linux/slab.h>
/* registers */
#define PDC_WD_SW_RESET 0x000
#define PDC_WD_CONFIG 0x004
#define PDC_WD_TICKLE1 0x008
#define PDC_WD_TICKLE2 0x00c
#define PDC_WD_IRQ_STATUS 0x010
#define PDC_WD_IRQ_CLEAR 0x014
#define PDC_WD_IRQ_EN 0x018
/* field masks */
#define PDC_WD_CONFIG_ENABLE 0x80000000
#define PDC_WD_CONFIG_REMIND 0x00001f00
#define PDC_WD_CONFIG_REMIND_SHIFT 8
#define PDC_WD_CONFIG_DELAY 0x0000001f
#define PDC_WD_CONFIG_DELAY_SHIFT 0
#define PDC_WD_TICKLE_NEW 0x00000010
#define PDC_WD_TICKLE_STATUS 0x00000007
#define PDC_WD_TICKLE_STATUS_SHIFT 0
#define PDC_WD_IRQ_REMIND 0x00000001
/* constants */
#define PDC_WD_TICKLE1_MAGIC 0xabcd1234
#define PDC_WD_TICKLE2_MAGIC 0x4321dcba
#define PDC_WD_TICKLE_STATUS_HRESET 0x0 /* Hard reset */
#define PDC_WD_TICKLE_STATUS_TIMEOUT 0x1 /* Timeout */
#define PDC_WD_TICKLE_STATUS_TICKLE 0x2 /* Tickled incorrectly */
#define PDC_WD_TICKLE_STATUS_SRESET 0x3 /* Soft reset */
#define PDC_WD_TICKLE_STATUS_USER 0x4 /* User reset */
#define TIMER_MIN (-14)
#define TIMER_MAX 17
#define TIMER_DELAY_SHIFT 6 /* 64 seconds */
static int delay_shift = TIMER_DELAY_SHIFT;
module_param(delay_shift, int, 0);
MODULE_PARM_DESC(delay_shift,
"Log2 of PDC watchdog timer delay in seconds ("
"-14 <= delay_shift <= 17, "
"default=" __MODULE_STRING(TIMER_DELAY_SHIFT) ")");
#define TIMER_REMIND_SHIFT TIMER_MAX /* disabled */
static int remind_shift = TIMER_REMIND_SHIFT;
module_param(remind_shift, int, 0);
MODULE_PARM_DESC(remind_shift,
"Log2 of PDC watchdog timer remind in seconds ("
"-14 <= remind_shift <= 17, "
"default=" __MODULE_STRING(TIMER_REMIND_SHIFT) ")");
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) ")");
struct pdc_wdt_priv {
struct device *dev;
int irq;
void __iomem *reg_base;
unsigned long timer_alive;
char expect_close;
char card_reset; /* whether last reset by the WD */
spinlock_t lock;
};
static struct platform_device *pdc_wdt_dev;
/* Hardware access */
static void pdc_wdt_write(struct pdc_wdt_priv *wdt,
unsigned int reg_offs, unsigned int data)
{
iowrite32(data, wdt->reg_base + reg_offs);
}
static unsigned int pdc_wdt_read(struct pdc_wdt_priv *wdt,
unsigned int reg_offs)
{
return ioread32(wdt->reg_base + reg_offs);
}
static void pdc_wdt_keepalive(struct pdc_wdt_priv *wdt)
{
spin_lock(&wdt->lock);
pdc_wdt_write(wdt, PDC_WD_TICKLE1, PDC_WD_TICKLE1_MAGIC);
pdc_wdt_write(wdt, PDC_WD_TICKLE2, PDC_WD_TICKLE2_MAGIC);
spin_unlock(&wdt->lock);
}
/* Start the watchdog timer (delay should already be set */
static void pdc_wdt_start(struct pdc_wdt_priv *wdt)
{
u32 config;
spin_lock(&wdt->lock);
config = pdc_wdt_read(wdt, PDC_WD_CONFIG);
config |= PDC_WD_CONFIG_ENABLE;
pdc_wdt_write(wdt, PDC_WD_CONFIG, config);
spin_unlock(&wdt->lock);
}
/* Safely stop the watchdog timer */
static void pdc_wdt_stop(struct pdc_wdt_priv *wdt)
{
u32 config;
spin_lock(&wdt->lock);
config = pdc_wdt_read(wdt, PDC_WD_CONFIG);
config &= ~PDC_WD_CONFIG_ENABLE;
pdc_wdt_write(wdt, PDC_WD_CONFIG, config);
spin_unlock(&wdt->lock);
/* Must tickle to finish the stop */
pdc_wdt_keepalive(wdt);
}
/* Find whether the watchdog hardware is enabled */
static int pdc_wdt_get_started(struct pdc_wdt_priv *wdt)
{
return !!(pdc_wdt_read(wdt, PDC_WD_CONFIG) & PDC_WD_CONFIG_ENABLE);
}
static void pdc_wdt_set_delay_shift(struct pdc_wdt_priv *wdt, int delay_sh)
{
u32 config;
delay_shift = delay_sh;
spin_lock(&wdt->lock);
config = pdc_wdt_read(wdt, PDC_WD_CONFIG);
/* number of 32.768KHz clocks, 2^(n+1) (14 is 1 sec) */
config &= ~PDC_WD_CONFIG_DELAY;
config |= (delay_shift-TIMER_MIN) << PDC_WD_CONFIG_DELAY_SHIFT;
pdc_wdt_write(wdt, PDC_WD_CONFIG, config);
spin_unlock(&wdt->lock);
}
static void pdc_wdt_set_remind_shift(struct pdc_wdt_priv *wdt, int remind_sh)
{
u32 config;
remind_shift = remind_sh;
spin_lock(&wdt->lock);
config = pdc_wdt_read(wdt, PDC_WD_CONFIG);
/* number of 32.768KHz clocks, 2^(n+1) (14 is 1 sec) */
config &= ~PDC_WD_CONFIG_REMIND;
config |= (remind_shift-TIMER_MIN) << PDC_WD_CONFIG_REMIND_SHIFT;
pdc_wdt_write(wdt, PDC_WD_CONFIG, config);
spin_unlock(&wdt->lock);
}
static const char *pdc_wdt_tickle_status_str(u32 status)
{
switch (status) {
case PDC_WD_TICKLE_STATUS_HRESET: return "hard reset";
case PDC_WD_TICKLE_STATUS_TIMEOUT: return "timeout";
case PDC_WD_TICKLE_STATUS_TICKLE: return "incorrect tickle";
case PDC_WD_TICKLE_STATUS_SRESET: return "soft reset";
case PDC_WD_TICKLE_STATUS_USER: return "user reset";
default: return "unknown";
};
}
/* Initialise the watchdog hardware */
static void pdc_wdt_setup(struct pdc_wdt_priv *wdt)
{
u32 status;
/* Enable remind interrupts */
pdc_wdt_write(wdt, PDC_WD_IRQ_EN, PDC_WD_IRQ_REMIND);
/* Ensure the watchdog is stopped */
pdc_wdt_stop(wdt);
/* Set the timeouts */
if (delay_shift < TIMER_MIN)
delay_shift = TIMER_MIN;
if (delay_shift > TIMER_MAX)
delay_shift = TIMER_MAX;
pdc_wdt_set_delay_shift(wdt, delay_shift);
if (remind_shift < TIMER_MIN)
remind_shift = TIMER_MIN;
if (remind_shift > TIMER_MAX)
remind_shift = TIMER_MAX;
pdc_wdt_set_remind_shift(wdt, remind_shift);
/* Find what caused the last reset */
status = pdc_wdt_read(wdt, PDC_WD_TICKLE1);
status = (status & PDC_WD_TICKLE_STATUS) >> PDC_WD_TICKLE_STATUS_SHIFT;
dev_info(wdt->dev,
"Watchdog module loaded (last reset due to %s)\n",
pdc_wdt_tickle_status_str(status));
/* Was it the watchdog? (userland may want to know) */
switch (status) {
case PDC_WD_TICKLE_STATUS_TICKLE:
case PDC_WD_TICKLE_STATUS_TIMEOUT:
wdt->card_reset = 1;
break;
default:
wdt->card_reset = 0;
}
}
/* round up to power of 2 */
static inline int pdc_wdt_delay_to_shift(int secs)
{
return order_base_2(secs);
}
/* round down to power of 2 */
static inline int pdc_wdt_remind_to_shift(int secs)
{
return ilog2(secs);
}
static inline int pdc_wdt_shift_to_secs(int shift)
{
if (shift >= 0)
return 1 << shift;
else
return 1;
}
static irqreturn_t pdc_wdt_isr(int irq, void *dev_id)
{
struct pdc_wdt_priv *wdt = dev_id;
u32 stat = pdc_wdt_read(wdt, PDC_WD_IRQ_STATUS);
/*
* The behaviour of the remind interrupt should depend on what userland
* asks for, either do nothing, panic the system or inform userland.
* Unfortunately this is not part of the main linux interface, and is
* currently implemented only in the ipmi watchdog driver (in
* drivers/char). This could be added at a later time.
*/
pdc_wdt_write(wdt, PDC_WD_IRQ_CLEAR, stat);
return IRQ_HANDLED;
}
/* /dev/watchdog handling */
static int pdc_wdt_open(struct inode *inode, struct file *file)
{
struct pdc_wdt_priv *wdt = platform_get_drvdata(pdc_wdt_dev);
/* one at a time */
if (test_and_set_bit(0, &wdt->timer_alive))
return -EBUSY;
/* don't unload, there's no way out */
if (nowayout)
__module_get(THIS_MODULE);
file->private_data = wdt;
pdc_wdt_start(wdt);
return nonseekable_open(inode, file);
}
static int pdc_wdt_release(struct inode *inode, struct file *file)
{
struct pdc_wdt_priv *wdt = file->private_data;
/*
* Shut off the timer.
* Lock it in if it's a module and we set nowayout
*/
if (wdt->expect_close == 42)
pdc_wdt_stop(wdt);
else if (pdc_wdt_get_started(wdt)) {
dev_crit(wdt->dev,
"unexpected close, not stopping watchdog!\n");
pdc_wdt_keepalive(wdt);
}
clear_bit(0, &wdt->timer_alive);
wdt->expect_close = 0;
return 0;
}
static ssize_t pdc_wdt_fwrite(struct file *file, const char __user *data,
size_t len, loff_t *ppos)
{
struct pdc_wdt_priv *wdt = file->private_data;
/* Refresh the timer. */
if (len) {
if (!nowayout) {
size_t i;
/* In case it was set long ago */
wdt->expect_close = 0;
for (i = 0; i != len; i++) {
char c;
if (get_user(c, data + i))
return -EFAULT;
if (c == 'V')
wdt->expect_close = 42;
}
}
pdc_wdt_keepalive(wdt);
}
return len;
}
static struct watchdog_info pdc_wdt_ident = {
.options = WDIOF_SETTIMEOUT |
WDIOF_PRETIMEOUT |
WDIOF_CARDRESET |
WDIOF_MAGICCLOSE,
.identity = "PDC Watchdog",
};
static long pdc_wdt_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct pdc_wdt_priv *wdt = file->private_data;
int ret;
int delay;
union {
struct watchdog_info ident;
int i;
} uarg;
if (_IOC_DIR(cmd) && _IOC_SIZE(cmd) > sizeof(uarg))
return -ENOTTY;
if (_IOC_DIR(cmd) & _IOC_WRITE) {
ret = copy_from_user(&uarg, (void __user *)arg, _IOC_SIZE(cmd));
if (ret)
return -EFAULT;
}
switch (cmd) {
case WDIOC_GETSUPPORT:
uarg.ident = pdc_wdt_ident;
ret = 0;
break;
case WDIOC_GETSTATUS:
uarg.i = 0;
ret = 0;
break;
case WDIOC_GETBOOTSTATUS:
uarg.i = 0;
if (wdt->card_reset)
uarg.i |= WDIOF_CARDRESET;
ret = 0;
break;
case WDIOC_SETOPTIONS:
/*
* Work around bad definition of WDIOC_SETOPTIONS until it's
* fixed. WDIOC_SETOPTIONS is a writing ioctl.
*/
if (!(_IOC_DIR(cmd) & _IOC_WRITE)) {
ret = copy_from_user(&uarg, (void __user *)arg,
_IOC_SIZE(cmd));
if (ret)
return -EFAULT;
}
ret = -EINVAL;
if (uarg.i & WDIOS_DISABLECARD) {
pdc_wdt_stop(wdt);
ret = 0;
}
if (uarg.i & WDIOS_ENABLECARD) {
pdc_wdt_start(wdt);
ret = 0;
}
break;
case WDIOC_KEEPALIVE:
pdc_wdt_keepalive(wdt);
ret = 0;
break;
case WDIOC_SETTIMEOUT:
if (uarg.i <= 0 || uarg.i > pdc_wdt_shift_to_secs(TIMER_MAX))
return -EINVAL;
uarg.i = pdc_wdt_delay_to_shift(uarg.i);
pdc_wdt_set_delay_shift(wdt, uarg.i);
/* fallthrough */
case WDIOC_GETTIMEOUT:
uarg.i = pdc_wdt_shift_to_secs(delay_shift);
ret = 0;
break;
case WDIOC_SETPRETIMEOUT:
/*
* Pretimeout is measured in seconds before main timeout.
* Subtract and round it once, and it will effectively change
* if the main timeout is changed.
*/
delay = pdc_wdt_shift_to_secs(delay_shift);
if (!uarg.i)
uarg.i = TIMER_MAX;
else if (uarg.i > 0 && uarg.i < delay)
uarg.i = pdc_wdt_remind_to_shift(delay - uarg.i);
else
return -EINVAL;
pdc_wdt_set_remind_shift(wdt, uarg.i);
/* fallthrough */
case WDIOC_GETPRETIMEOUT:
if (remind_shift >= TIMER_MAX)
uarg.i = 0;
else
uarg.i = pdc_wdt_shift_to_secs(delay_shift) -
pdc_wdt_shift_to_secs(remind_shift);
ret = 0;
break;
default:
return -ENOTTY;
}
if (ret == 0 && (_IOC_DIR(cmd) & _IOC_READ) && cmd != WDIOC_SETOPTIONS) {
ret = copy_to_user((void __user *)arg, &uarg, _IOC_SIZE(cmd));
if (ret)
ret = -EFAULT;
}
return ret;
}
/* Kernel interface */
static const struct file_operations pdc_wdt_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.write = pdc_wdt_fwrite,
.unlocked_ioctl = pdc_wdt_ioctl,
.open = pdc_wdt_open,
.release = pdc_wdt_release,
};
static struct miscdevice pdc_wdt_miscdev = {
.minor = WATCHDOG_MINOR,
.name = "watchdog",
.fops = &pdc_wdt_fops,
};
static int pdc_wdt_probe(struct platform_device *pdev)
{
struct pdc_wdt_priv *wdt;
struct resource *res_regs;
int irq, error;
/* Get resources from platform device */
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "cannot find IRQ resource\n");
error = irq;
goto err_pdata;
}
res_regs = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res_regs == NULL) {
dev_err(&pdev->dev, "cannot find registers resource\n");
error = -ENOENT;
goto err_pdata;
}
/* Private driver data */
wdt = devm_kzalloc(&pdev->dev, sizeof(*wdt), GFP_KERNEL);
if (!wdt) {
dev_err(&pdev->dev, "cannot allocate device data\n");
error = -ENOMEM;
goto err_dev;
}
platform_set_drvdata(pdev, wdt);
pdc_wdt_dev = pdev;
wdt->dev = &pdev->dev;
spin_lock_init(&wdt->lock);
/* Ioremap the registers */
wdt->reg_base = devm_ioremap(&pdev->dev, res_regs->start,
res_regs->end - res_regs->start);
if (!wdt->reg_base) {
error = -EIO;
goto err_regs;
}
/* Set timeouts before userland has a chance to start the timer */
pdc_wdt_setup(wdt);
device_set_wakeup_capable(&pdev->dev, 1);
pdc_wdt_miscdev.parent = &pdev->dev;
error = misc_register(&pdc_wdt_miscdev);
if (error) {
dev_err(&pdev->dev,
"cannot register miscdev on minor=%d (err=%d)\n",
WATCHDOG_MINOR, error);
goto err_misc;
}
wdt->irq = irq;
error = devm_request_irq(&pdev->dev, wdt->irq, pdc_wdt_isr, 0,
"pdc-wdt", wdt);
if (error) {
dev_err(&pdev->dev, "cannot register IRQ %u\n",
wdt->irq);
error = -EIO;
goto err_irq;
}
return 0;
err_irq:
misc_deregister(&pdc_wdt_miscdev);
err_misc:
err_regs:
pdc_wdt_dev = NULL;
err_dev:
err_pdata:
return error;
}
static int pdc_wdt_remove(struct platform_device *pdev)
{
pdc_wdt_dev = NULL;
misc_deregister(&pdc_wdt_miscdev);
return 0;
}
/*
* System shutdown handler. Turn off the watchdog if we're
* restarting or halting the system.
*/
static void pdc_wdt_shutdown(struct platform_device *pdev)
{
struct pdc_wdt_priv *wdt = platform_get_drvdata(pdev);
if (system_state == SYSTEM_RESTART || system_state == SYSTEM_HALT)
pdc_wdt_stop(wdt);
}
#ifdef CONFIG_PM
static int pdc_wdt_remind_enabled(struct pdc_wdt_priv *wdt)
{
return remind_shift != TIMER_MAX;
}
/*
* During suspend we don't want the watchdog to think we've crashed, so
* stop the watchdog until resume.
*/
static int pdc_wdt_suspend(struct platform_device *pdev, pm_message_t state)
{
struct pdc_wdt_priv *wdt = platform_get_drvdata(pdev);
/* Only wake if the remind is enabled. */
if (device_may_wakeup(&pdev->dev) && pdc_wdt_remind_enabled(wdt))
enable_irq_wake(wdt->irq);
else if (wdt->timer_alive)
pdc_wdt_stop(wdt);
return 0;
}
static int pdc_wdt_resume(struct platform_device *pdev)
{
struct pdc_wdt_priv *wdt = platform_get_drvdata(pdev);
if (device_may_wakeup(&pdev->dev) && pdc_wdt_remind_enabled(wdt))
disable_irq_wake(wdt->irq);
else if (wdt->timer_alive)
pdc_wdt_start(wdt);
return 0;
}
#else
#define pdc_wdt_suspend NULL
#define pdc_wdt_resume NULL
#endif /* CONFIG_PM */
static const struct of_device_id pdc_wdt_match[] = {
{ .compatible = "img,pdc-wdt" },
{}
};
MODULE_DEVICE_TABLE(of, pdc_wdt_match);
static struct platform_driver pdc_wdt_driver = {
.driver = {
.name = "imgpdc-wdt",
.owner = THIS_MODULE,
.of_match_table = pdc_wdt_match,
},
.probe = pdc_wdt_probe,
.remove = pdc_wdt_remove,
.shutdown = pdc_wdt_shutdown,
.suspend = pdc_wdt_suspend,
.resume = pdc_wdt_resume,
/* pdc_wdt has shutdown handler too */
};
module_platform_driver(pdc_wdt_driver);
MODULE_AUTHOR("Imagination Technologies Ltd.");
MODULE_DESCRIPTION("ImgTec PDC WDT");
MODULE_LICENSE("GPL");
MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR);