blob: 4ce881b69aadb61c60458b0d5702ecf469632564 [file] [log] [blame]
/*
* drivers/watchdog/tegra_wdt.c
*
* watchdog driver for NVIDIA tegra internal watchdog
*
* Copyright (c) 2012-2013, NVIDIA Corporation.
*
* 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/of.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/uaccess.h>
#include <linux/watchdog.h>
#include <linux/trusty/trusty.h>
#include <linux/trusty/smcall.h>
#include <asm/fiq_glue.h>
#include <asm/system_misc.h>
#include "../staging/android/fiq_debugger/fiq_watchdog.h"
/* minimum and maximum watchdog trigger periods, in seconds */
#define MIN_WDT_PERIOD 5
#define MAX_WDT_PERIOD 1000
#define MIN_WDT_TIMER_FIQ 10000
enum tegra_wdt_status {
WDT_DISABLED = 1 << 0,
WDT_ENABLED = 1 << 1,
};
struct tegra_wdt {
struct watchdog_device wdt;
struct resource *res_src;
struct resource *res_wdt;
unsigned long users;
void __iomem *wdt_source;
void __iomem *wdt_timer;
bool useirq;
int tmrsrc;
int status;
struct fiq_glue_handler fiq_handler;
struct notifier_block panic_notifier;
};
/*
* 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 */
#ifdef CONFIG_TEGRA_WATCHDOG_ENABLE_ON_PROBE
static bool enable_on_probe = true;
#else
static bool enable_on_probe;
#endif
static inline struct tegra_wdt *to_tegra_wdt(struct watchdog_device *wdt)
{
return container_of(wdt, struct tegra_wdt, wdt);
}
#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 int __tegra_wdt_enable(struct tegra_wdt *tegra_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 = tegra_wdt->timeout * 1000000ul / 2;
val |= (TIMER_EN | TIMER_PERIODIC);
writel(val, tegra_wdt->wdt_timer + TIMER_PTV);
val = WDT_EN | WDT_SEL_TMR1 | WDT_SYS_RST;
writel(val, tegra_wdt->wdt_source);
return 0;
}
static int __tegra_wdt_disable(struct tegra_wdt *tegra_wdt)
{
writel(0, tegra_wdt->wdt_source);
writel(0, tegra_wdt->wdt_timer + TIMER_PTV);
return 0;
}
static int __tegra_wdt_ping(struct tegra_wdt *tegra_wdt)
{
writel(TIMER_PCR_INTR, tegra_wdt->wdt_timer + TIMER_PCR);
return 0;
}
#elif defined(CONFIG_ARCH_TEGRA_3x_SOC) || defined(CONFIG_ARCH_TEGRA_11x_SOC) \
|| defined(CONFIG_ARCH_TEGRA_12x_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_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 MAX_NR_CPU_WDT 0x4
static int __tegra_wdt_ping(struct tegra_wdt *tegra_wdt)
{
writel(WDT_CMD_START_COUNTER, tegra_wdt->wdt_source + WDT_CMD);
return 0;
}
static int __tegra_wdt_enable(struct tegra_wdt *tegra_wdt)
{
u32 val;
int period = 16; /* number of timer wraps before watchdog triggers */
writel(TIMER_PCR_INTR, tegra_wdt->wdt_timer + TIMER_PCR);
val = (tegra_wdt->wdt.timeout * USEC_PER_SEC) / period;
/* give the fiq handler a chance to run */
if (val < MIN_WDT_TIMER_FIQ && tegra_wdt->useirq)
val = MIN_WDT_TIMER_FIQ;
val |= (TIMER_EN | TIMER_PERIODIC);
writel(val, tegra_wdt->wdt_timer + TIMER_PTV);
val = tegra_wdt->tmrsrc | WDT_CFG_PMC2CAR_RST_EN;
if (tegra_wdt->useirq)
val |= WDT_CFG_INT_EN | (WDT_CFG_PERIOD * period);
else
val |= WDT_CFG_PERIOD * (period / 4);
writel(val, tegra_wdt->wdt_source + WDT_CFG);
writel(WDT_CMD_START_COUNTER, tegra_wdt->wdt_source + WDT_CMD);
return 0;
}
static int __tegra_wdt_disable(struct tegra_wdt *tegra_wdt)
{
writel(WDT_UNLOCK_PATTERN, tegra_wdt->wdt_source + WDT_UNLOCK);
writel(WDT_CMD_DISABLE_COUNTER, tegra_wdt->wdt_source + WDT_CMD);
writel(0, tegra_wdt->wdt_timer + TIMER_PTV);
tegra_wdt->status = WDT_DISABLED;
return 0;
}
#endif
static int tegra_wdt_enable(struct watchdog_device *wdt)
{
struct tegra_wdt *tegra_wdt = to_tegra_wdt(wdt);
return __tegra_wdt_enable(tegra_wdt);
}
static int tegra_wdt_disable(struct watchdog_device *wdt)
{
struct tegra_wdt *tegra_wdt = to_tegra_wdt(wdt);
return __tegra_wdt_disable(tegra_wdt);
}
static int tegra_wdt_ping(struct watchdog_device *wdt)
{
struct tegra_wdt *tegra_wdt = to_tegra_wdt(wdt);
return __tegra_wdt_ping(tegra_wdt);
}
static int tegra_wdt_set_timeout(struct watchdog_device *wdt, unsigned int timeout)
{
tegra_wdt_disable(wdt);
wdt->timeout = timeout;
tegra_wdt_enable(wdt);
return 0;
}
static const struct watchdog_info tegra_wdt_info = {
.options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE,
.identity = "Tegra WDT",
.firmware_version = 1,
};
static const struct watchdog_ops tegra_wdt_ops = {
.owner = THIS_MODULE,
.start = tegra_wdt_enable,
.stop = tegra_wdt_disable,
.ping = tegra_wdt_ping,
.set_timeout = tegra_wdt_set_timeout,
};
#ifdef CONFIG_TEGRA_WATCHDOG_FIQ
static void tegra_wdt_fiq(struct fiq_glue_handler *h,
const struct pt_regs *regs, void *svc_sp)
{
struct tegra_wdt *tegra_wdt;
u32 wdt_status;
tegra_wdt = container_of(h, struct tegra_wdt, fiq_handler);
wdt_status = __raw_readl(tegra_wdt->wdt_source + WDT_STATUS);
if (!(wdt_status & WDT_INTR_STAT))
return;
/* speed up watchdog reset */
__raw_writel(tegra_wdt->tmrsrc | WDT_CFG_PERIOD |
WDT_CFG_PMC2CAR_RST_EN | WDT_CFG_INT_EN,
tegra_wdt->wdt_source + WDT_CFG);
fiq_watchdog_triggered(regs, svc_sp);
while (true)
;
}
static int tegra_wdt_panic_handler(struct notifier_block *this,
unsigned long event, void *unused)
{
struct tegra_wdt *tegra_wdt;
tegra_wdt = container_of(this, struct tegra_wdt, panic_notifier);
if (panic_timeout) {
/* trigger watchdog fiq 1 second before panic reboots */
tegra_wdt->wdt.timeout = panic_timeout - 1;
pr_info("%s: set watchdog timeout to %d\n",
__func__, tegra_wdt->wdt.timeout);
__tegra_wdt_enable(tegra_wdt);
}
return 0;
}
static void tegra_wdt_fiq_setup(struct platform_device *pdev,
struct tegra_wdt *tegra_wdt, int fiq)
{
int ret;
struct device *trusty_dev = pdev->dev.parent->parent;
tegra_wdt->fiq_handler.fiq = &tegra_wdt_fiq;
ret = fiq_glue_register_handler(&tegra_wdt->fiq_handler);
if (ret) {
dev_err(&pdev->dev, "request fiq failed, %d\n", ret);
return;
}
ret = trusty_fast_call32(trusty_dev, SMC_FC_REQUEST_FIQ,
fiq, true, 0);
if (ret) {
dev_err(&pdev->dev, "request fiq failed, %d\n", ret);
return;
}
tegra_wdt->panic_notifier.priority = INT_MIN;
tegra_wdt->panic_notifier.notifier_call = tegra_wdt_panic_handler;
ret = atomic_notifier_chain_register(&panic_notifier_list,
&tegra_wdt->panic_notifier);
if (ret) {
dev_err(&pdev->dev, "failed to register panic notifier, %d\n",
ret);
return;
}
tegra_wdt->useirq = true;
}
static void tegra_wdt_fiq_cleanup(struct platform_device *pdev,
struct tegra_wdt *tegra_wdt)
{
if (!tegra_wdt->useirq)
return;
atomic_notifier_chain_unregister(&panic_notifier_list,
&tegra_wdt->panic_notifier);
}
#else
static void tegra_wdt_fiq_setup(struct platform_device *pdev,
struct tegra_wdt *tegra_wdt, int fiq)
{
dev_err(&pdev->dev, "tegra watchdog fiq config option not enabled\n");
}
static void tegra_wdt_fiq_cleanup(struct platform_device *pdev,
struct tegra_wdt *tegra_wdt)
{
}
#endif
static int tegra_wdt_probe(struct platform_device *pdev)
{
struct resource *res_src, *res_wdt, *res_fiq;
struct tegra_wdt *tegra_wdt;
int ret = 0;
res_src = platform_get_resource(pdev, IORESOURCE_MEM, 0);
res_wdt = platform_get_resource(pdev, IORESOURCE_MEM, 1);
res_fiq = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "fiq");
if (!res_src || !res_wdt) {
dev_err(&pdev->dev, "incorrect resources\n");
return -ENOENT;
}
tegra_wdt = kzalloc(sizeof(*tegra_wdt), GFP_KERNEL);
if (!tegra_wdt) {
dev_err(&pdev->dev, "out of memory\n");
return -ENOMEM;
}
tegra_wdt->wdt.info = &tegra_wdt_info;
tegra_wdt->wdt.ops = &tegra_wdt_ops;
tegra_wdt->wdt.min_timeout = MIN_WDT_PERIOD;
tegra_wdt->wdt.max_timeout = MAX_WDT_PERIOD;
tegra_wdt->wdt.timeout = 120;
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;
}
tegra_wdt->wdt_source = ioremap(res_src->start, resource_size(res_src));
tegra_wdt->wdt_timer = ioremap(res_wdt->start, resource_size(res_wdt));
/* tmrsrc will be used to set WDT_CFG */
if ((res_wdt->start & 0xff) < 0x50)
tegra_wdt->tmrsrc = 1 + (res_wdt->start & 0xf) / 8;
else
tegra_wdt->tmrsrc = (3 + ((res_wdt->start & 0xff) - 0x50) / 8) % 10;
if (!tegra_wdt->wdt_source || !tegra_wdt->wdt_timer) {
dev_err(&pdev->dev, "unable to map registers\n");
ret = -ENOMEM;
goto fail;
}
tegra_wdt_disable(&tegra_wdt->wdt);
writel(TIMER_PCR_INTR, tegra_wdt->wdt_timer + TIMER_PCR);
if (res_fiq) {
tegra_wdt_fiq_setup(pdev, tegra_wdt, res_fiq->start);
}
tegra_wdt->res_src = res_src;
tegra_wdt->res_wdt = res_wdt;
#ifndef CONFIG_ARCH_TEGRA_2x_SOC
/* Init and enable watchdog on WDT0 with timer 8 during probe */
if (enable_on_probe) {
set_bit(WDOG_ACTIVE, &tegra_wdt->wdt.status);
tegra_wdt_enable(&tegra_wdt->wdt);
pr_info("WDT heartbeat enabled on probe\n");
}
#endif
watchdog_init_timeout(&tegra_wdt->wdt, heartbeat, &pdev->dev);
ret = watchdog_register_device(&tegra_wdt->wdt);
if (ret) {
dev_err(&pdev->dev, "failed to register watchdog device\n");
goto fail;
}
platform_set_drvdata(pdev, tegra_wdt);
dev_info(&pdev->dev, "%s done\n", __func__);
return 0;
fail:
tegra_wdt_fiq_cleanup(pdev, tegra_wdt);
if (tegra_wdt->wdt_source)
iounmap(tegra_wdt->wdt_source);
if (tegra_wdt->wdt_timer)
iounmap(tegra_wdt->wdt_timer);
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));
kfree(tegra_wdt);
return ret;
}
static int tegra_wdt_remove(struct platform_device *pdev)
{
struct tegra_wdt *tegra_wdt = platform_get_drvdata(pdev);
tegra_wdt_disable(&tegra_wdt->wdt);
watchdog_unregister_device(&tegra_wdt->wdt);
tegra_wdt_fiq_cleanup(pdev, tegra_wdt);
iounmap(tegra_wdt->wdt_source);
iounmap(tegra_wdt->wdt_timer);
release_mem_region(tegra_wdt->res_src->start, resource_size(tegra_wdt->res_src));
release_mem_region(tegra_wdt->res_wdt->start, resource_size(tegra_wdt->res_wdt));
kfree(tegra_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 *tegra_wdt = platform_get_drvdata(pdev);
__tegra_wdt_disable(tegra_wdt);
return 0;
}
static int tegra_wdt_resume(struct platform_device *pdev)
{
struct tegra_wdt *tegra_wdt = platform_get_drvdata(pdev);
if (watchdog_active(&tegra_wdt->wdt))
__tegra_wdt_enable(tegra_wdt);
return 0;
}
#endif
static const struct of_device_id tegra_wdt_match[] = {
{ .compatible = "nvidia,tegra-wdt", },
{}
};
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",
.of_match_table = of_match_ptr(tegra_wdt_match),
},
};
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);
}
subsys_initcall(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_param(enable_on_probe, bool, 0);
MODULE_PARM_DESC(enable_on_probe,
"Start watchdog during boot");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:tegra_wdt");