mirror of
https://github.com/torvalds/linux.git
synced 2024-11-22 20:22:09 +00:00
8644553588
In case if the DW Watchdog IP core is synthesised with WDT_USE_FIX_TOP == false, the TOP interval indexes make the device to load a custom periods to the counter. These periods are hardwired at the IP synthesis stage and can be within [2^8, 2^(WDT_CNT_WIDTH - 1)]. Alas their values can't be detected at runtime and must be somehow supplied to the driver so one could properly determine the watchdog timeout intervals. For this purpose we suggest to have a vendor- specific dts property "snps,watchdog-tops" utilized, which would provide an array of sixteen counter values. At device probe stage they will be used to initialize the watchdog device timeouts determined from the array values and current clocks source rate. In order to have custom TOP values supported the driver must be altered in the following way. First of all the fixed-top values ready-to-use array must be determined for compatibility with currently supported devices, which were synthesised with WDT_USE_FIX_TOP == true. It will be used if either fixed TOP feature is detected being enabled or no custom TOPs are fetched from the device dt node. Secondly at the probe stage we must initialize an array of the watchdog timeouts corresponding to the detected TOPs list and the reference clock rate. For generality the procedure of initialization is designed in a way to support the TOPs array with no limitations on the items order or value. Finally the watchdog period search methods should be altered to support the new timeouts data structure. Signed-off-by: Serge Semin <Sergey.Semin@baikalelectronics.ru> Reviewed-by: Guenter Roeck <linux@roeck-us.net> Cc: Alexey Malahov <Alexey.Malahov@baikalelectronics.ru> Cc: Thomas Bogendoerfer <tsbogend@alpha.franken.de> Cc: Arnd Bergmann <arnd@arndb.de> Cc: Rob Herring <robh+dt@kernel.org> Cc: linux-mips@vger.kernel.org Cc: devicetree@vger.kernel.org Link: https://lore.kernel.org/r/20200530073557.22661-5-Sergey.Semin@baikalelectronics.ru Signed-off-by: Guenter Roeck <linux@roeck-us.net> Signed-off-by: Wim Van Sebroeck <wim@linux-watchdog.org>
495 lines
13 KiB
C
495 lines
13 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* Copyright 2010-2011 Picochip Ltd., Jamie Iles
|
|
* http://www.picochip.com
|
|
*
|
|
* This file implements a driver for the Synopsys DesignWare watchdog device
|
|
* in the many subsystems. The watchdog has 16 different timeout periods
|
|
* and these are a function of the input clock frequency.
|
|
*
|
|
* The DesignWare watchdog cannot be stopped once it has been started so we
|
|
* do not implement a stop function. The watchdog core will continue to send
|
|
* heartbeat requests after the watchdog device has been closed.
|
|
*/
|
|
|
|
#include <linux/bitops.h>
|
|
#include <linux/limits.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/clk.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/err.h>
|
|
#include <linux/io.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/moduleparam.h>
|
|
#include <linux/of.h>
|
|
#include <linux/pm.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/reset.h>
|
|
#include <linux/watchdog.h>
|
|
|
|
#define WDOG_CONTROL_REG_OFFSET 0x00
|
|
#define WDOG_CONTROL_REG_WDT_EN_MASK 0x01
|
|
#define WDOG_CONTROL_REG_RESP_MODE_MASK 0x02
|
|
#define WDOG_TIMEOUT_RANGE_REG_OFFSET 0x04
|
|
#define WDOG_TIMEOUT_RANGE_TOPINIT_SHIFT 4
|
|
#define WDOG_CURRENT_COUNT_REG_OFFSET 0x08
|
|
#define WDOG_COUNTER_RESTART_REG_OFFSET 0x0c
|
|
#define WDOG_COUNTER_RESTART_KICK_VALUE 0x76
|
|
#define WDOG_COMP_PARAMS_1_REG_OFFSET 0xf4
|
|
#define WDOG_COMP_PARAMS_1_USE_FIX_TOP BIT(6)
|
|
|
|
/* There are sixteen TOPs (timeout periods) that can be set in the watchdog. */
|
|
#define DW_WDT_NUM_TOPS 16
|
|
#define DW_WDT_FIX_TOP(_idx) (1U << (16 + _idx))
|
|
|
|
#define DW_WDT_DEFAULT_SECONDS 30
|
|
|
|
static const u32 dw_wdt_fix_tops[DW_WDT_NUM_TOPS] = {
|
|
DW_WDT_FIX_TOP(0), DW_WDT_FIX_TOP(1), DW_WDT_FIX_TOP(2),
|
|
DW_WDT_FIX_TOP(3), DW_WDT_FIX_TOP(4), DW_WDT_FIX_TOP(5),
|
|
DW_WDT_FIX_TOP(6), DW_WDT_FIX_TOP(7), DW_WDT_FIX_TOP(8),
|
|
DW_WDT_FIX_TOP(9), DW_WDT_FIX_TOP(10), DW_WDT_FIX_TOP(11),
|
|
DW_WDT_FIX_TOP(12), DW_WDT_FIX_TOP(13), DW_WDT_FIX_TOP(14),
|
|
DW_WDT_FIX_TOP(15)
|
|
};
|
|
|
|
static bool nowayout = WATCHDOG_NOWAYOUT;
|
|
module_param(nowayout, bool, 0);
|
|
MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started "
|
|
"(default=" __MODULE_STRING(WATCHDOG_NOWAYOUT) ")");
|
|
|
|
struct dw_wdt_timeout {
|
|
u32 top_val;
|
|
unsigned int sec;
|
|
unsigned int msec;
|
|
};
|
|
|
|
struct dw_wdt {
|
|
void __iomem *regs;
|
|
struct clk *clk;
|
|
unsigned long rate;
|
|
struct dw_wdt_timeout timeouts[DW_WDT_NUM_TOPS];
|
|
struct watchdog_device wdd;
|
|
struct reset_control *rst;
|
|
/* Save/restore */
|
|
u32 control;
|
|
u32 timeout;
|
|
};
|
|
|
|
#define to_dw_wdt(wdd) container_of(wdd, struct dw_wdt, wdd)
|
|
|
|
static inline int dw_wdt_is_enabled(struct dw_wdt *dw_wdt)
|
|
{
|
|
return readl(dw_wdt->regs + WDOG_CONTROL_REG_OFFSET) &
|
|
WDOG_CONTROL_REG_WDT_EN_MASK;
|
|
}
|
|
|
|
static unsigned int dw_wdt_find_best_top(struct dw_wdt *dw_wdt,
|
|
unsigned int timeout, u32 *top_val)
|
|
{
|
|
int idx;
|
|
|
|
/*
|
|
* Find a TOP with timeout greater or equal to the requested number.
|
|
* Note we'll select a TOP with maximum timeout if the requested
|
|
* timeout couldn't be reached.
|
|
*/
|
|
for (idx = 0; idx < DW_WDT_NUM_TOPS; ++idx) {
|
|
if (dw_wdt->timeouts[idx].sec >= timeout)
|
|
break;
|
|
}
|
|
|
|
if (idx == DW_WDT_NUM_TOPS)
|
|
--idx;
|
|
|
|
*top_val = dw_wdt->timeouts[idx].top_val;
|
|
|
|
return dw_wdt->timeouts[idx].sec;
|
|
}
|
|
|
|
static unsigned int dw_wdt_get_min_timeout(struct dw_wdt *dw_wdt)
|
|
{
|
|
int idx;
|
|
|
|
/*
|
|
* We'll find a timeout greater or equal to one second anyway because
|
|
* the driver probe would have failed if there was none.
|
|
*/
|
|
for (idx = 0; idx < DW_WDT_NUM_TOPS; ++idx) {
|
|
if (dw_wdt->timeouts[idx].sec)
|
|
break;
|
|
}
|
|
|
|
return dw_wdt->timeouts[idx].sec;
|
|
}
|
|
|
|
static unsigned int dw_wdt_get_max_timeout_ms(struct dw_wdt *dw_wdt)
|
|
{
|
|
struct dw_wdt_timeout *timeout = &dw_wdt->timeouts[DW_WDT_NUM_TOPS - 1];
|
|
u64 msec;
|
|
|
|
msec = (u64)timeout->sec * MSEC_PER_SEC + timeout->msec;
|
|
|
|
return msec < UINT_MAX ? msec : UINT_MAX;
|
|
}
|
|
|
|
static unsigned int dw_wdt_get_timeout(struct dw_wdt *dw_wdt)
|
|
{
|
|
int top_val = readl(dw_wdt->regs + WDOG_TIMEOUT_RANGE_REG_OFFSET) & 0xF;
|
|
int idx;
|
|
|
|
for (idx = 0; idx < DW_WDT_NUM_TOPS; ++idx) {
|
|
if (dw_wdt->timeouts[idx].top_val == top_val)
|
|
break;
|
|
}
|
|
|
|
return dw_wdt->timeouts[idx].sec;
|
|
}
|
|
|
|
static int dw_wdt_ping(struct watchdog_device *wdd)
|
|
{
|
|
struct dw_wdt *dw_wdt = to_dw_wdt(wdd);
|
|
|
|
writel(WDOG_COUNTER_RESTART_KICK_VALUE, dw_wdt->regs +
|
|
WDOG_COUNTER_RESTART_REG_OFFSET);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_wdt_set_timeout(struct watchdog_device *wdd, unsigned int top_s)
|
|
{
|
|
struct dw_wdt *dw_wdt = to_dw_wdt(wdd);
|
|
unsigned int timeout;
|
|
u32 top_val;
|
|
|
|
timeout = dw_wdt_find_best_top(dw_wdt, top_s, &top_val);
|
|
|
|
/*
|
|
* Set the new value in the watchdog. Some versions of dw_wdt
|
|
* have have TOPINIT in the TIMEOUT_RANGE register (as per
|
|
* CP_WDT_DUAL_TOP in WDT_COMP_PARAMS_1). On those we
|
|
* effectively get a pat of the watchdog right here.
|
|
*/
|
|
writel(top_val | top_val << WDOG_TIMEOUT_RANGE_TOPINIT_SHIFT,
|
|
dw_wdt->regs + WDOG_TIMEOUT_RANGE_REG_OFFSET);
|
|
|
|
/*
|
|
* In case users set bigger timeout value than HW can support,
|
|
* kernel(watchdog_dev.c) helps to feed watchdog before
|
|
* wdd->max_hw_heartbeat_ms
|
|
*/
|
|
if (top_s * 1000 <= wdd->max_hw_heartbeat_ms)
|
|
wdd->timeout = timeout;
|
|
else
|
|
wdd->timeout = top_s;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void dw_wdt_arm_system_reset(struct dw_wdt *dw_wdt)
|
|
{
|
|
u32 val = readl(dw_wdt->regs + WDOG_CONTROL_REG_OFFSET);
|
|
|
|
/* Disable interrupt mode; always perform system reset. */
|
|
val &= ~WDOG_CONTROL_REG_RESP_MODE_MASK;
|
|
/* Enable watchdog. */
|
|
val |= WDOG_CONTROL_REG_WDT_EN_MASK;
|
|
writel(val, dw_wdt->regs + WDOG_CONTROL_REG_OFFSET);
|
|
}
|
|
|
|
static int dw_wdt_start(struct watchdog_device *wdd)
|
|
{
|
|
struct dw_wdt *dw_wdt = to_dw_wdt(wdd);
|
|
|
|
dw_wdt_set_timeout(wdd, wdd->timeout);
|
|
dw_wdt_ping(&dw_wdt->wdd);
|
|
dw_wdt_arm_system_reset(dw_wdt);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_wdt_stop(struct watchdog_device *wdd)
|
|
{
|
|
struct dw_wdt *dw_wdt = to_dw_wdt(wdd);
|
|
|
|
if (!dw_wdt->rst) {
|
|
set_bit(WDOG_HW_RUNNING, &wdd->status);
|
|
return 0;
|
|
}
|
|
|
|
reset_control_assert(dw_wdt->rst);
|
|
reset_control_deassert(dw_wdt->rst);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_wdt_restart(struct watchdog_device *wdd,
|
|
unsigned long action, void *data)
|
|
{
|
|
struct dw_wdt *dw_wdt = to_dw_wdt(wdd);
|
|
|
|
writel(0, dw_wdt->regs + WDOG_TIMEOUT_RANGE_REG_OFFSET);
|
|
if (dw_wdt_is_enabled(dw_wdt))
|
|
writel(WDOG_COUNTER_RESTART_KICK_VALUE,
|
|
dw_wdt->regs + WDOG_COUNTER_RESTART_REG_OFFSET);
|
|
else
|
|
dw_wdt_arm_system_reset(dw_wdt);
|
|
|
|
/* wait for reset to assert... */
|
|
mdelay(500);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static unsigned int dw_wdt_get_timeleft(struct watchdog_device *wdd)
|
|
{
|
|
struct dw_wdt *dw_wdt = to_dw_wdt(wdd);
|
|
|
|
return readl(dw_wdt->regs + WDOG_CURRENT_COUNT_REG_OFFSET) /
|
|
dw_wdt->rate;
|
|
}
|
|
|
|
static const struct watchdog_info dw_wdt_ident = {
|
|
.options = WDIOF_KEEPALIVEPING | WDIOF_SETTIMEOUT |
|
|
WDIOF_MAGICCLOSE,
|
|
.identity = "Synopsys DesignWare Watchdog",
|
|
};
|
|
|
|
static const struct watchdog_ops dw_wdt_ops = {
|
|
.owner = THIS_MODULE,
|
|
.start = dw_wdt_start,
|
|
.stop = dw_wdt_stop,
|
|
.ping = dw_wdt_ping,
|
|
.set_timeout = dw_wdt_set_timeout,
|
|
.get_timeleft = dw_wdt_get_timeleft,
|
|
.restart = dw_wdt_restart,
|
|
};
|
|
|
|
#ifdef CONFIG_PM_SLEEP
|
|
static int dw_wdt_suspend(struct device *dev)
|
|
{
|
|
struct dw_wdt *dw_wdt = dev_get_drvdata(dev);
|
|
|
|
dw_wdt->control = readl(dw_wdt->regs + WDOG_CONTROL_REG_OFFSET);
|
|
dw_wdt->timeout = readl(dw_wdt->regs + WDOG_TIMEOUT_RANGE_REG_OFFSET);
|
|
|
|
clk_disable_unprepare(dw_wdt->clk);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_wdt_resume(struct device *dev)
|
|
{
|
|
struct dw_wdt *dw_wdt = dev_get_drvdata(dev);
|
|
int err = clk_prepare_enable(dw_wdt->clk);
|
|
|
|
if (err)
|
|
return err;
|
|
|
|
writel(dw_wdt->timeout, dw_wdt->regs + WDOG_TIMEOUT_RANGE_REG_OFFSET);
|
|
writel(dw_wdt->control, dw_wdt->regs + WDOG_CONTROL_REG_OFFSET);
|
|
|
|
dw_wdt_ping(&dw_wdt->wdd);
|
|
|
|
return 0;
|
|
}
|
|
#endif /* CONFIG_PM_SLEEP */
|
|
|
|
static SIMPLE_DEV_PM_OPS(dw_wdt_pm_ops, dw_wdt_suspend, dw_wdt_resume);
|
|
|
|
/*
|
|
* In case if DW WDT IP core is synthesized with fixed TOP feature disabled the
|
|
* TOPs array can be arbitrary ordered with nearly any sixteen uint numbers
|
|
* depending on the system engineer imagination. The next method handles the
|
|
* passed TOPs array to pre-calculate the effective timeouts and to sort the
|
|
* TOP items out in the ascending order with respect to the timeouts.
|
|
*/
|
|
|
|
static void dw_wdt_handle_tops(struct dw_wdt *dw_wdt, const u32 *tops)
|
|
{
|
|
struct dw_wdt_timeout tout, *dst;
|
|
int val, tidx;
|
|
u64 msec;
|
|
|
|
/*
|
|
* We walk over the passed TOPs array and calculate corresponding
|
|
* timeouts in seconds and milliseconds. The milliseconds granularity
|
|
* is needed to distinguish the TOPs with very close timeouts and to
|
|
* set the watchdog max heartbeat setting further.
|
|
*/
|
|
for (val = 0; val < DW_WDT_NUM_TOPS; ++val) {
|
|
tout.top_val = val;
|
|
tout.sec = tops[val] / dw_wdt->rate;
|
|
msec = (u64)tops[val] * MSEC_PER_SEC;
|
|
do_div(msec, dw_wdt->rate);
|
|
tout.msec = msec - ((u64)tout.sec * MSEC_PER_SEC);
|
|
|
|
/*
|
|
* Find a suitable place for the current TOP in the timeouts
|
|
* array so that the list is remained in the ascending order.
|
|
*/
|
|
for (tidx = 0; tidx < val; ++tidx) {
|
|
dst = &dw_wdt->timeouts[tidx];
|
|
if (tout.sec > dst->sec || (tout.sec == dst->sec &&
|
|
tout.msec >= dst->msec))
|
|
continue;
|
|
else
|
|
swap(*dst, tout);
|
|
}
|
|
|
|
dw_wdt->timeouts[val] = tout;
|
|
}
|
|
}
|
|
|
|
static int dw_wdt_init_timeouts(struct dw_wdt *dw_wdt, struct device *dev)
|
|
{
|
|
u32 data, of_tops[DW_WDT_NUM_TOPS];
|
|
const u32 *tops;
|
|
int ret;
|
|
|
|
/*
|
|
* Retrieve custom or fixed counter values depending on the
|
|
* WDT_USE_FIX_TOP flag found in the component specific parameters
|
|
* #1 register.
|
|
*/
|
|
data = readl(dw_wdt->regs + WDOG_COMP_PARAMS_1_REG_OFFSET);
|
|
if (data & WDOG_COMP_PARAMS_1_USE_FIX_TOP) {
|
|
tops = dw_wdt_fix_tops;
|
|
} else {
|
|
ret = of_property_read_variable_u32_array(dev_of_node(dev),
|
|
"snps,watchdog-tops", of_tops, DW_WDT_NUM_TOPS,
|
|
DW_WDT_NUM_TOPS);
|
|
if (ret < 0) {
|
|
dev_warn(dev, "No valid TOPs array specified\n");
|
|
tops = dw_wdt_fix_tops;
|
|
} else {
|
|
tops = of_tops;
|
|
}
|
|
}
|
|
|
|
/* Convert the specified TOPs into an array of watchdog timeouts. */
|
|
dw_wdt_handle_tops(dw_wdt, tops);
|
|
if (!dw_wdt->timeouts[DW_WDT_NUM_TOPS - 1].sec) {
|
|
dev_err(dev, "No any valid TOP detected\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dw_wdt_drv_probe(struct platform_device *pdev)
|
|
{
|
|
struct device *dev = &pdev->dev;
|
|
struct watchdog_device *wdd;
|
|
struct dw_wdt *dw_wdt;
|
|
int ret;
|
|
|
|
dw_wdt = devm_kzalloc(dev, sizeof(*dw_wdt), GFP_KERNEL);
|
|
if (!dw_wdt)
|
|
return -ENOMEM;
|
|
|
|
dw_wdt->regs = devm_platform_ioremap_resource(pdev, 0);
|
|
if (IS_ERR(dw_wdt->regs))
|
|
return PTR_ERR(dw_wdt->regs);
|
|
|
|
dw_wdt->clk = devm_clk_get(dev, NULL);
|
|
if (IS_ERR(dw_wdt->clk))
|
|
return PTR_ERR(dw_wdt->clk);
|
|
|
|
ret = clk_prepare_enable(dw_wdt->clk);
|
|
if (ret)
|
|
return ret;
|
|
|
|
dw_wdt->rate = clk_get_rate(dw_wdt->clk);
|
|
if (dw_wdt->rate == 0) {
|
|
ret = -EINVAL;
|
|
goto out_disable_clk;
|
|
}
|
|
|
|
dw_wdt->rst = devm_reset_control_get_optional_shared(&pdev->dev, NULL);
|
|
if (IS_ERR(dw_wdt->rst)) {
|
|
ret = PTR_ERR(dw_wdt->rst);
|
|
goto out_disable_clk;
|
|
}
|
|
|
|
reset_control_deassert(dw_wdt->rst);
|
|
|
|
ret = dw_wdt_init_timeouts(dw_wdt, dev);
|
|
if (ret)
|
|
goto out_disable_clk;
|
|
|
|
wdd = &dw_wdt->wdd;
|
|
wdd->info = &dw_wdt_ident;
|
|
wdd->ops = &dw_wdt_ops;
|
|
wdd->min_timeout = dw_wdt_get_min_timeout(dw_wdt);
|
|
wdd->max_hw_heartbeat_ms = dw_wdt_get_max_timeout_ms(dw_wdt);
|
|
wdd->parent = dev;
|
|
|
|
watchdog_set_drvdata(wdd, dw_wdt);
|
|
watchdog_set_nowayout(wdd, nowayout);
|
|
watchdog_init_timeout(wdd, 0, dev);
|
|
|
|
/*
|
|
* If the watchdog is already running, use its already configured
|
|
* timeout. Otherwise use the default or the value provided through
|
|
* devicetree.
|
|
*/
|
|
if (dw_wdt_is_enabled(dw_wdt)) {
|
|
wdd->timeout = dw_wdt_get_timeout(dw_wdt);
|
|
set_bit(WDOG_HW_RUNNING, &wdd->status);
|
|
} else {
|
|
wdd->timeout = DW_WDT_DEFAULT_SECONDS;
|
|
watchdog_init_timeout(wdd, 0, dev);
|
|
}
|
|
|
|
platform_set_drvdata(pdev, dw_wdt);
|
|
|
|
watchdog_set_restart_priority(wdd, 128);
|
|
|
|
ret = watchdog_register_device(wdd);
|
|
if (ret)
|
|
goto out_disable_clk;
|
|
|
|
return 0;
|
|
|
|
out_disable_clk:
|
|
clk_disable_unprepare(dw_wdt->clk);
|
|
return ret;
|
|
}
|
|
|
|
static int dw_wdt_drv_remove(struct platform_device *pdev)
|
|
{
|
|
struct dw_wdt *dw_wdt = platform_get_drvdata(pdev);
|
|
|
|
watchdog_unregister_device(&dw_wdt->wdd);
|
|
reset_control_assert(dw_wdt->rst);
|
|
clk_disable_unprepare(dw_wdt->clk);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_OF
|
|
static const struct of_device_id dw_wdt_of_match[] = {
|
|
{ .compatible = "snps,dw-wdt", },
|
|
{ /* sentinel */ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, dw_wdt_of_match);
|
|
#endif
|
|
|
|
static struct platform_driver dw_wdt_driver = {
|
|
.probe = dw_wdt_drv_probe,
|
|
.remove = dw_wdt_drv_remove,
|
|
.driver = {
|
|
.name = "dw_wdt",
|
|
.of_match_table = of_match_ptr(dw_wdt_of_match),
|
|
.pm = &dw_wdt_pm_ops,
|
|
},
|
|
};
|
|
|
|
module_platform_driver(dw_wdt_driver);
|
|
|
|
MODULE_AUTHOR("Jamie Iles");
|
|
MODULE_DESCRIPTION("Synopsys DesignWare Watchdog Driver");
|
|
MODULE_LICENSE("GPL");
|