blob: a2923d2a63e2ec7875d35197ce7996e417a51094 [file] [log] [blame]
/*
* drivers/watchdog/tegra_wdt.c
*
* watchdog driver for NVIDIA tegra internal watchdog
*
* Copyright (c) 2012-2013, NVIDIA CORPORATION. All rights reserved.
*
* based on drivers/watchdog/softdog.c and drivers/watchdog/omap_wdt.c
*
* 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.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/notifier.h>
#include <linux/platform_device.h>
#include <linux/reboot.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/uaccess.h>
#include <linux/watchdog.h>
#ifdef CONFIG_TEGRA_FIQ_DEBUGGER
#include <mach/irqs.h>
#endif
#include <mach/iomap.h>
/* minimum and maximum watchdog trigger periods, in seconds */
#define MIN_WDT_PERIOD 5
#define MAX_WDT_PERIOD 1000
/* Assign Timer 7 to Timer 10 for WDT0 to WDT3, respectively */
#define TMR_SRC_START 7
enum tegra_wdt_status {
WDT_DISABLED = 1 << 0,
WDT_ENABLED = 1 << 1,
WDT_ENABLED_AT_PROBE = 1 << 2,
};
struct tegra_wdt {
struct miscdevice miscdev;
struct notifier_block notifier;
struct resource *res_src;
struct resource *res_wdt;
struct resource *res_int_base;
unsigned long users;
void __iomem *wdt_source;
void __iomem *wdt_timer;
void __iomem *int_base;
int irq;
int tmrsrc;
int timeout;
int status;
};
/*
* For spinlock lockup detection to work, the heartbeat should be 2*lockup
* for cases where the spinlock disabled irqs.
*/
static int heartbeat = 120; /* must be greater than MIN_WDT_PERIOD and lower than MAX_WDT_PERIOD */
#if defined(CONFIG_ARCH_TEGRA_2x_SOC)
#define TIMER_PTV 0x0
#define TIMER_EN (1 << 31)
#define TIMER_PERIODIC (1 << 30)
#define TIMER_PCR 0x4
#define TIMER_PCR_INTR (1 << 30)
#define WDT_EN (1 << 5)
#define WDT_SEL_TMR1 (0 << 4)
#define WDT_SYS_RST (1 << 2)
static void tegra_wdt_enable(struct tegra_wdt *wdt)
{
u32 val;
/* since the watchdog reset occurs when a second interrupt
* is asserted before the first is processed, program the
* timer period to one-half of the watchdog period */
val = wdt->timeout * 1000000ul / 2;
val |= (TIMER_EN | TIMER_PERIODIC);
writel(val, wdt->wdt_timer + TIMER_PTV);
val = WDT_EN | WDT_SEL_TMR1 | WDT_SYS_RST;
writel(val, wdt->wdt_source);
}
static void tegra_wdt_disable(struct tegra_wdt *wdt)
{
writel(0, wdt->wdt_source);
writel(0, wdt->wdt_timer + TIMER_PTV);
}
static inline void tegra_wdt_ping(struct tegra_wdt *wdt)
{
return;
}
static irqreturn_t tegra_wdt_interrupt(int irq, void *dev_id)
{
struct tegra_wdt *wdt = dev_id;
writel(TIMER_PCR_INTR, wdt->wdt_timer + TIMER_PCR);
return IRQ_HANDLED;
}
#elif defined(CONFIG_ARCH_TEGRA_3x_SOC) || defined(CONFIG_ARCH_TEGRA_11x_SOC)
#define TIMER_PTV 0
#define TIMER_EN (1 << 31)
#define TIMER_PERIODIC (1 << 30)
#define TIMER_PCR 0x4
#define TIMER_PCR_INTR (1 << 30)
#define WDT_CFG (0)
#define WDT_CFG_PERIOD (1 << 4)
#define WDT_CFG_INT_EN (1 << 12)
#define WDT_CFG_FIQ_INT_EN (1 << 13)
#define WDT_CFG_SYS_RST_EN (1 << 14)
#define WDT_CFG_PMC2CAR_RST_EN (1 << 15)
#define WDT_STATUS (4)
#define WDT_INTR_STAT (1 << 1)
#define WDT_CMD (8)
#define WDT_CMD_START_COUNTER (1 << 0)
#define WDT_CMD_DISABLE_COUNTER (1 << 1)
#define WDT_UNLOCK (0xC)
#define WDT_UNLOCK_PATTERN (0xC45A << 0)
#define ICTLR_IEP_CLASS 0x2C
#define MAX_NR_CPU_WDT 0x4
#define PMC_RST_STATUS 0x1b4
struct tegra_wdt *tegra_wdt[MAX_NR_CPU_WDT];
static inline void tegra_wdt_ping(struct tegra_wdt *wdt)
{
writel(WDT_CMD_START_COUNTER, wdt->wdt_source + WDT_CMD);
}
#ifdef CONFIG_TEGRA_FIQ_DEBUGGER
static void tegra_wdt_int_priority(struct tegra_wdt *wdt)
{
unsigned val = 0;
if (!wdt->int_base)
return;
val = readl(wdt->int_base + ICTLR_IEP_CLASS);
val &= ~(1 << (INT_WDT_CPU & 31));
writel(val, wdt->int_base + ICTLR_IEP_CLASS);
}
#endif
static void tegra_wdt_enable(struct tegra_wdt *wdt)
{
u32 val;
writel(TIMER_PCR_INTR, wdt->wdt_timer + TIMER_PCR);
val = (wdt->timeout * 1000000ul) / 4;
val |= (TIMER_EN | TIMER_PERIODIC);
writel(val, wdt->wdt_timer + TIMER_PTV);
/* Interrupt handler is not required for user space
* WDT accesses, since the caller is responsible to ping the
* WDT to reset the counter before expiration, through ioctls.
* SYS_RST_EN doesnt work as there is no external reset
* from Tegra.
*/
val = wdt->tmrsrc | WDT_CFG_PERIOD | /*WDT_CFG_INT_EN |*/
/*WDT_CFG_SYS_RST_EN |*/ WDT_CFG_PMC2CAR_RST_EN;
#ifdef CONFIG_TEGRA_FIQ_DEBUGGER
val |= WDT_CFG_FIQ_INT_EN;
#endif
writel(val, wdt->wdt_source + WDT_CFG);
writel(WDT_CMD_START_COUNTER, wdt->wdt_source + WDT_CMD);
}
static void tegra_wdt_disable(struct tegra_wdt *wdt)
{
writel(WDT_UNLOCK_PATTERN, wdt->wdt_source + WDT_UNLOCK);
writel(WDT_CMD_DISABLE_COUNTER, wdt->wdt_source + WDT_CMD);
writel(0, wdt->wdt_timer + TIMER_PTV);
}
static irqreturn_t tegra_wdt_interrupt(int irq, void *dev_id)
{
unsigned i, status;
for (i = 0; i < MAX_NR_CPU_WDT; i++) {
if (tegra_wdt[i] == NULL)
continue;
status = readl(tegra_wdt[i]->wdt_source + WDT_STATUS);
if ((tegra_wdt[i]->status & WDT_ENABLED) &&
(status & WDT_INTR_STAT))
tegra_wdt_ping(tegra_wdt[i]);
}
return IRQ_HANDLED;
}
#endif
static int tegra_wdt_notify(struct notifier_block *this,
unsigned long code, void *dev)
{
struct tegra_wdt *wdt = container_of(this, struct tegra_wdt, notifier);
if (code == SYS_DOWN || code == SYS_HALT)
tegra_wdt_disable(wdt);
return NOTIFY_DONE;
}
static int tegra_wdt_open(struct inode *inode, struct file *file)
{
struct miscdevice *mdev = file->private_data;
struct tegra_wdt *wdt = container_of(mdev, struct tegra_wdt,
miscdev);
if (test_and_set_bit(1, &wdt->users))
return -EBUSY;
wdt->status |= WDT_ENABLED;
wdt->timeout = heartbeat;
tegra_wdt_enable(wdt);
file->private_data = wdt;
return nonseekable_open(inode, file);
}
static int tegra_wdt_release(struct inode *inode, struct file *file)
{
struct tegra_wdt *wdt = file->private_data;
if (wdt->status == WDT_ENABLED) {
#ifndef CONFIG_WATCHDOG_NOWAYOUT
tegra_wdt_disable(wdt);
wdt->status = WDT_DISABLED;
#endif
}
wdt->users = 0;
return 0;
}
static long tegra_wdt_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct tegra_wdt *wdt = file->private_data;
static DEFINE_SPINLOCK(lock);
int new_timeout;
int option;
static const struct watchdog_info ident = {
.identity = "Tegra Watchdog",
.options = WDIOF_SETTIMEOUT,
.firmware_version = 0,
};
switch (cmd) {
case WDIOC_GETSUPPORT:
return copy_to_user((struct watchdog_info __user *)arg, &ident,
sizeof(ident));
case WDIOC_GETSTATUS:
case WDIOC_GETBOOTSTATUS:
return put_user(0, (int __user *)arg);
case WDIOC_KEEPALIVE:
spin_lock(&lock);
tegra_wdt_ping(wdt);
spin_unlock(&lock);
return 0;
case WDIOC_SETTIMEOUT:
if (get_user(new_timeout, (int __user *)arg))
return -EFAULT;
spin_lock(&lock);
tegra_wdt_disable(wdt);
wdt->timeout = clamp(new_timeout, MIN_WDT_PERIOD, MAX_WDT_PERIOD);
tegra_wdt_enable(wdt);
spin_unlock(&lock);
case WDIOC_GETTIMEOUT:
return put_user(wdt->timeout, (int __user *)arg);
case WDIOC_SETOPTIONS:
#ifndef CONFIG_WATCHDOG_NOWAYOUT
if (get_user(option, (int __user *)arg))
return -EFAULT;
spin_lock(&lock);
if (option & WDIOS_DISABLECARD) {
wdt->status &= ~WDT_ENABLED;
wdt->status |= WDT_DISABLED;
tegra_wdt_disable(wdt);
} else if (option & WDIOS_ENABLECARD) {
tegra_wdt_enable(wdt);
wdt->status |= WDT_ENABLED;
wdt->status &= ~WDT_DISABLED;
} else {
spin_unlock(&lock);
return -EINVAL;
}
spin_unlock(&lock);
return 0;
#else
return -EINVAL;
#endif
}
return -ENOTTY;
}
static ssize_t tegra_wdt_write(struct file *file, const char __user *data,
size_t len, loff_t *ppos)
{
return len;
}
static void tegra_wdt_log_reset_reason(struct platform_device *pdev,
struct tegra_wdt *wdt)
{
#if defined(CONFIG_ARCH_TEGRA_3x_SOC) || defined(CONFIG_ARCH_TEGRA_11x_SOC)
/*
* There are two pathes to make the WDT reset:
* (a) WDT -> PMC -> CAR
* ^
* |
* v
* PMIC
*
* (b) WDT -> CAR
*
* Path (a) is enabled by WDT_CFG_PMC2CAR_RST_EN bit in the WDT
* configuration register, as it will reset the CAR module, and we
* cannot read back the reset reason from the CAR module. However, we
* can read back the reaset reason from the PMC module.
*
* Path (b) is enabled by the WDT_CFG_SYS_RST_EN bit, and we can
* read back the reset reason from the CAR moudle. However, this reset
* path will not reset the peripherals which might be the hard hang
* source. We will not use this path.
*/
u32 val;
void __iomem *pmc_base;
#define RESET_STR(REASON) "last reset is due to "#REASON"\n"
char *reset_reason[] = {
RESET_STR(power on reset),
RESET_STR(watchdog timeout),
RESET_STR(sensor),
RESET_STR(software reset),
RESET_STR(deep sleep reset),
};
/* report reset reason only once */
if (pdev->id > 0)
return;
pmc_base = IO_ADDRESS(TEGRA_PMC_BASE);
val = readl(pmc_base + PMC_RST_STATUS) & 0x7;
if (val >= ARRAY_SIZE(reset_reason))
dev_info(&pdev->dev, "last reset value is invalid 0x%x\n", val);
else
dev_info(&pdev->dev, reset_reason[val]);
#else
u32 val;
val = readl(wdt->wdt_source);
if (val & BIT(12))
dev_info(&pdev->dev, "last reset due to watchdog timeout\n");
#endif
}
static const struct file_operations tegra_wdt_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.write = tegra_wdt_write,
.unlocked_ioctl = tegra_wdt_ioctl,
.open = tegra_wdt_open,
.release = tegra_wdt_release,
};
static int tegra_wdt_probe(struct platform_device *pdev)
{
struct resource *res_src, *res_wdt, *res_irq;
struct resource *res_int_base = NULL;
struct tegra_wdt *wdt;
int ret = 0;
if ((pdev->id < -1) || (pdev->id > 0)) {
dev_err(&pdev->dev, "Only support IDs -1 and 0\n");
return -ENODEV;
}
res_src = platform_get_resource(pdev, IORESOURCE_MEM, 0);
res_wdt = platform_get_resource(pdev, IORESOURCE_MEM, 1);
res_irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
if (!res_src || !res_wdt || (!pdev->id && !res_irq)) {
dev_err(&pdev->dev, "incorrect resources\n");
return -ENOENT;
}
#ifdef CONFIG_TEGRA_FIQ_DEBUGGER
res_int_base = platform_get_resource(pdev, IORESOURCE_MEM, 2);
if (!pdev->id && !res_int_base) {
dev_err(&pdev->dev, "FIQ_DBG: INT base not defined\n");
return -ENOENT;
}
#endif
if (pdev->id == -1 && !res_irq) {
dev_err(&pdev->dev, "incorrect irq\n");
return -ENOENT;
}
wdt = kzalloc(sizeof(*wdt), GFP_KERNEL);
if (!wdt) {
dev_err(&pdev->dev, "out of memory\n");
return -ENOMEM;
}
wdt->irq = -1;
wdt->miscdev.parent = &pdev->dev;
if (pdev->id == -1) {
wdt->miscdev.minor = WATCHDOG_MINOR;
wdt->miscdev.name = "watchdog";
} else {
wdt->miscdev.minor = MISC_DYNAMIC_MINOR;
if (pdev->id == 0)
wdt->miscdev.name = "watchdog0";
else if (pdev->id == 1)
wdt->miscdev.name = "watchdog1";
else if (pdev->id == 2)
wdt->miscdev.name = "watchdog2";
else if (pdev->id == 3)
wdt->miscdev.name = "watchdog3";
}
wdt->miscdev.fops = &tegra_wdt_fops;
wdt->notifier.notifier_call = tegra_wdt_notify;
res_src = request_mem_region(res_src->start, resource_size(res_src),
pdev->name);
res_wdt = request_mem_region(res_wdt->start, resource_size(res_wdt),
pdev->name);
if (!res_src || !res_wdt) {
dev_err(&pdev->dev, "unable to request memory resources\n");
ret = -EBUSY;
goto fail;
}
wdt->wdt_source = ioremap(res_src->start, resource_size(res_src));
wdt->wdt_timer = ioremap(res_wdt->start, resource_size(res_wdt));
/* tmrsrc will be used to set WDT_CFG */
wdt->tmrsrc = (TMR_SRC_START + pdev->id) % 10;
if (!wdt->wdt_source || !wdt->wdt_timer) {
dev_err(&pdev->dev, "unable to map registers\n");
ret = -ENOMEM;
goto fail;
}
tegra_wdt_log_reset_reason(pdev, wdt);
tegra_wdt_disable(wdt);
writel(TIMER_PCR_INTR, wdt->wdt_timer + TIMER_PCR);
if (res_irq != NULL) {
#ifdef CONFIG_TEGRA_FIQ_DEBUGGER
/* FIQ debugger enables FIQ priority for INT_WDT_CPU.
* But that will disable IRQ on WDT expiration.
* Reset the priority back to IRQ on INT_WDT_CPU so
* that tegra_wdt_interrupt gets its chance to restart the
* counter before expiration.
*/
res_int_base = request_mem_region(res_int_base->start,
resource_size(res_int_base),
pdev->name);
if (!res_int_base)
goto fail;
wdt->int_base = ioremap(res_int_base->start,
resource_size(res_int_base));
if (!wdt->int_base)
goto fail;
tegra_wdt_int_priority(wdt);
#endif
ret = request_irq(res_irq->start, tegra_wdt_interrupt,
IRQF_DISABLED, dev_name(&pdev->dev), wdt);
if (ret) {
dev_err(&pdev->dev, "unable to configure IRQ\n");
goto fail;
}
wdt->irq = res_irq->start;
}
wdt->res_src = res_src;
wdt->res_wdt = res_wdt;
wdt->res_int_base = res_int_base;
wdt->status = WDT_DISABLED;
ret = register_reboot_notifier(&wdt->notifier);
if (ret) {
dev_err(&pdev->dev, "cannot register reboot notifier\n");
goto fail;
}
ret = misc_register(&wdt->miscdev);
if (ret) {
dev_err(&pdev->dev, "failed to register misc device\n");
unregister_reboot_notifier(&wdt->notifier);
goto fail;
}
platform_set_drvdata(pdev, wdt);
#ifndef CONFIG_ARCH_TEGRA_2x_SOC
#ifdef CONFIG_TEGRA_WATCHDOG_ENABLE_ON_PROBE
/* Init and enable watchdog on WDT0 with timer 8 during probe */
if (!(pdev->id)) {
u32 val = 0;
wdt->status = WDT_ENABLED | WDT_ENABLED_AT_PROBE;
wdt->timeout = heartbeat;
tegra_wdt_enable(wdt);
val = readl(wdt->wdt_source + WDT_CFG);
val |= WDT_CFG_INT_EN;
writel(val, wdt->wdt_source + WDT_CFG);
pr_info("WDT heartbeat enabled on probe\n");
}
#endif
tegra_wdt[pdev->id] = wdt;
#endif
pr_info("%s done\n", __func__);
return 0;
fail:
if (wdt->irq != -1)
free_irq(wdt->irq, wdt);
if (wdt->wdt_source)
iounmap(wdt->wdt_source);
if (wdt->wdt_timer)
iounmap(wdt->wdt_timer);
if (wdt->int_base)
iounmap(wdt->int_base);
if (res_src)
release_mem_region(res_src->start, resource_size(res_src));
if (res_wdt)
release_mem_region(res_wdt->start, resource_size(res_wdt));
if (res_int_base)
release_mem_region(res_int_base->start,
resource_size(res_int_base));
kfree(wdt);
return ret;
}
static int tegra_wdt_remove(struct platform_device *pdev)
{
struct tegra_wdt *wdt = platform_get_drvdata(pdev);
tegra_wdt_disable(wdt);
unregister_reboot_notifier(&wdt->notifier);
misc_deregister(&wdt->miscdev);
if (wdt->irq != -1)
free_irq(wdt->irq, wdt);
iounmap(wdt->wdt_source);
iounmap(wdt->wdt_timer);
if (wdt->int_base)
iounmap(wdt->int_base);
release_mem_region(wdt->res_src->start, resource_size(wdt->res_src));
release_mem_region(wdt->res_wdt->start, resource_size(wdt->res_wdt));
if (wdt->res_int_base)
release_mem_region(wdt->res_int_base->start,
resource_size(wdt->res_int_base));
kfree(wdt);
platform_set_drvdata(pdev, NULL);
return 0;
}
#ifdef CONFIG_PM
static int tegra_wdt_suspend(struct platform_device *pdev, pm_message_t state)
{
struct tegra_wdt *wdt = platform_get_drvdata(pdev);
tegra_wdt_disable(wdt);
return 0;
}
static int tegra_wdt_resume(struct platform_device *pdev)
{
struct tegra_wdt *wdt = platform_get_drvdata(pdev);
if (wdt->status & WDT_ENABLED)
tegra_wdt_enable(wdt);
#ifndef CONFIG_ARCH_TEGRA_2x_SOC
/* Enable interrupt for WDT3 heartbeat watchdog */
if (wdt->status & WDT_ENABLED_AT_PROBE) {
u32 val = 0;
val = readl(wdt->wdt_source + WDT_CFG);
val |= WDT_CFG_INT_EN;
writel(val, wdt->wdt_source + WDT_CFG);
pr_info("WDT heartbeat enabled on probe\n");
}
#endif
return 0;
}
#endif
static struct platform_driver tegra_wdt_driver = {
.probe = tegra_wdt_probe,
.remove = tegra_wdt_remove,
#ifdef CONFIG_PM
.suspend = tegra_wdt_suspend,
.resume = tegra_wdt_resume,
#endif
.driver = {
.owner = THIS_MODULE,
.name = "tegra_wdt",
},
};
static int __init tegra_wdt_init(void)
{
return platform_driver_register(&tegra_wdt_driver);
}
static void __exit tegra_wdt_exit(void)
{
platform_driver_unregister(&tegra_wdt_driver);
}
module_init(tegra_wdt_init);
module_exit(tegra_wdt_exit);
MODULE_AUTHOR("NVIDIA Corporation");
MODULE_DESCRIPTION("Tegra Watchdog Driver");
module_param(heartbeat, int, 0);
MODULE_PARM_DESC(heartbeat,
"Watchdog heartbeat period in seconds");
MODULE_LICENSE("GPL");
MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR);
MODULE_ALIAS("platform:tegra_wdt");