diff options
Diffstat (limited to 'drivers/watchdog/qcom-wdt.c')
| -rw-r--r-- | drivers/watchdog/qcom-wdt.c | 201 |
1 files changed, 142 insertions, 59 deletions
diff --git a/drivers/watchdog/qcom-wdt.c b/drivers/watchdog/qcom-wdt.c index 780971318810..dfaac5995c84 100644 --- a/drivers/watchdog/qcom-wdt.c +++ b/drivers/watchdog/qcom-wdt.c @@ -1,24 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-only /* Copyright (c) 2014, The Linux Foundation. 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 version 2 and - * only version 2 as published by the Free Software Foundation. - * - * 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. - * */ +#include <linux/bits.h> #include <linux/clk.h> #include <linux/delay.h> +#include <linux/interrupt.h> #include <linux/io.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/of.h> #include <linux/platform_device.h> #include <linux/watchdog.h> -#include <linux/of_device.h> enum wdt_reg { WDT_RST, @@ -28,6 +20,8 @@ enum wdt_reg { WDT_BITE_TIME, }; +#define QCOM_WDT_ENABLE BIT(0) + static const u32 reg_offset_data_apcs_tmr[] = { [WDT_RST] = 0x38, [WDT_EN] = 0x40, @@ -44,9 +38,14 @@ static const u32 reg_offset_data_kpss[] = { [WDT_BITE_TIME] = 0x14, }; +struct qcom_wdt_match_data { + const u32 *offset; + bool pretimeout; + u32 max_tick_count; +}; + struct qcom_wdt { struct watchdog_device wdd; - struct clk *clk; unsigned long rate; void __iomem *base; const u32 *layout; @@ -63,15 +62,25 @@ struct qcom_wdt *to_qcom_wdt(struct watchdog_device *wdd) return container_of(wdd, struct qcom_wdt, wdd); } +static irqreturn_t qcom_wdt_isr(int irq, void *arg) +{ + struct watchdog_device *wdd = arg; + + watchdog_notify_pretimeout(wdd); + + return IRQ_HANDLED; +} + static int qcom_wdt_start(struct watchdog_device *wdd) { struct qcom_wdt *wdt = to_qcom_wdt(wdd); + unsigned int bark = wdd->timeout - wdd->pretimeout; writel(0, wdt_addr(wdt, WDT_EN)); writel(1, wdt_addr(wdt, WDT_RST)); - writel(wdd->timeout * wdt->rate, wdt_addr(wdt, WDT_BARK_TIME)); + writel(bark * wdt->rate, wdt_addr(wdt, WDT_BARK_TIME)); writel(wdd->timeout * wdt->rate, wdt_addr(wdt, WDT_BITE_TIME)); - writel(1, wdt_addr(wdt, WDT_EN)); + writel(QCOM_WDT_ENABLE, wdt_addr(wdt, WDT_EN)); return 0; } @@ -98,6 +107,13 @@ static int qcom_wdt_set_timeout(struct watchdog_device *wdd, return qcom_wdt_start(wdd); } +static int qcom_wdt_set_pretimeout(struct watchdog_device *wdd, + unsigned int timeout) +{ + wdd->pretimeout = timeout; + return qcom_wdt_start(wdd); +} + static int qcom_wdt_restart(struct watchdog_device *wdd, unsigned long action, void *data) { @@ -114,22 +130,30 @@ static int qcom_wdt_restart(struct watchdog_device *wdd, unsigned long action, writel(1, wdt_addr(wdt, WDT_RST)); writel(timeout, wdt_addr(wdt, WDT_BARK_TIME)); writel(timeout, wdt_addr(wdt, WDT_BITE_TIME)); - writel(1, wdt_addr(wdt, WDT_EN)); + writel(QCOM_WDT_ENABLE, wdt_addr(wdt, WDT_EN)); /* * Actually make sure the above sequence hits hardware before sleeping. */ wmb(); - msleep(150); + mdelay(150); return 0; } +static int qcom_wdt_is_running(struct watchdog_device *wdd) +{ + struct qcom_wdt *wdt = to_qcom_wdt(wdd); + + return (readl(wdt_addr(wdt, WDT_EN)) & QCOM_WDT_ENABLE); +} + static const struct watchdog_ops qcom_wdt_ops = { .start = qcom_wdt_start, .stop = qcom_wdt_stop, .ping = qcom_wdt_ping, .set_timeout = qcom_wdt_set_timeout, + .set_pretimeout = qcom_wdt_set_pretimeout, .restart = qcom_wdt_restart, .owner = THIS_MODULE, }; @@ -142,22 +166,51 @@ static const struct watchdog_info qcom_wdt_info = { .identity = KBUILD_MODNAME, }; +static const struct watchdog_info qcom_wdt_pt_info = { + .options = WDIOF_KEEPALIVEPING + | WDIOF_MAGICCLOSE + | WDIOF_SETTIMEOUT + | WDIOF_PRETIMEOUT + | WDIOF_CARDRESET, + .identity = KBUILD_MODNAME, +}; + +static const struct qcom_wdt_match_data match_data_apcs_tmr = { + .offset = reg_offset_data_apcs_tmr, + .pretimeout = false, + .max_tick_count = 0x10000000U, +}; + +static const struct qcom_wdt_match_data match_data_ipq5424 = { + .offset = reg_offset_data_kpss, + .pretimeout = true, + .max_tick_count = 0xFFFFFU, +}; + +static const struct qcom_wdt_match_data match_data_kpss = { + .offset = reg_offset_data_kpss, + .pretimeout = true, + .max_tick_count = 0xFFFFFU, +}; + static int qcom_wdt_probe(struct platform_device *pdev) { + struct device *dev = &pdev->dev; struct qcom_wdt *wdt; struct resource *res; - struct device_node *np = pdev->dev.of_node; - const u32 *regs; + struct device_node *np = dev->of_node; + const struct qcom_wdt_match_data *data; u32 percpu_offset; - int ret; + int irq, ret; + struct clk *clk; - regs = of_device_get_match_data(&pdev->dev); - if (!regs) { - dev_err(&pdev->dev, "Unsupported QCOM WDT module\n"); + data = of_device_get_match_data(dev); + if (!data) { + dev_err(dev, "Unsupported QCOM WDT module\n"); return -ENODEV; } - wdt = devm_kzalloc(&pdev->dev, sizeof(*wdt), GFP_KERNEL); + wdt = devm_kzalloc(dev, sizeof(*wdt), GFP_KERNEL); if (!wdt) return -ENOMEM; @@ -172,20 +225,14 @@ static int qcom_wdt_probe(struct platform_device *pdev) res->start += percpu_offset; res->end += percpu_offset; - wdt->base = devm_ioremap_resource(&pdev->dev, res); + wdt->base = devm_ioremap_resource(dev, res); if (IS_ERR(wdt->base)) return PTR_ERR(wdt->base); - wdt->clk = devm_clk_get(&pdev->dev, NULL); - if (IS_ERR(wdt->clk)) { - dev_err(&pdev->dev, "failed to get input clock\n"); - return PTR_ERR(wdt->clk); - } - - ret = clk_prepare_enable(wdt->clk); - if (ret) { - dev_err(&pdev->dev, "failed to setup clock\n"); - return ret; + clk = devm_clk_get_enabled(dev, NULL); + if (IS_ERR(clk)) { + dev_err(dev, "failed to get input clock\n"); + return PTR_ERR(clk); } /* @@ -196,20 +243,35 @@ static int qcom_wdt_probe(struct platform_device *pdev) * that it would bite before a second elapses it's usefulness is * limited. Bail if this is the case. */ - wdt->rate = clk_get_rate(wdt->clk); + wdt->rate = clk_get_rate(clk); if (wdt->rate == 0 || - wdt->rate > 0x10000000U) { - dev_err(&pdev->dev, "invalid clock rate\n"); - ret = -EINVAL; - goto err_clk_unprepare; + wdt->rate > data->max_tick_count) { + dev_err(dev, "invalid clock rate\n"); + return -EINVAL; + } + + /* check if there is pretimeout support */ + irq = platform_get_irq_optional(pdev, 0); + if (data->pretimeout && irq > 0) { + ret = devm_request_irq(dev, irq, qcom_wdt_isr, 0, + "wdt_bark", &wdt->wdd); + if (ret) + return ret; + + wdt->wdd.info = &qcom_wdt_pt_info; + wdt->wdd.pretimeout = 1; + } else { + if (irq == -EPROBE_DEFER) + return -EPROBE_DEFER; + + wdt->wdd.info = &qcom_wdt_info; } - wdt->wdd.info = &qcom_wdt_info; wdt->wdd.ops = &qcom_wdt_ops; wdt->wdd.min_timeout = 1; - wdt->wdd.max_timeout = 0x10000000U / wdt->rate; - wdt->wdd.parent = &pdev->dev; - wdt->layout = regs; + wdt->wdd.max_timeout = data->max_tick_count / wdt->rate; + wdt->wdd.parent = dev; + wdt->layout = data->offset; if (readl(wdt_addr(wdt, WDT_STS)) & 1) wdt->wdd.bootstatus = WDIOF_CARDRESET; @@ -220,45 +282,66 @@ static int qcom_wdt_probe(struct platform_device *pdev) * the max instead. */ wdt->wdd.timeout = min(wdt->wdd.max_timeout, 30U); - watchdog_init_timeout(&wdt->wdd, 0, &pdev->dev); + watchdog_init_timeout(&wdt->wdd, 0, dev); - ret = watchdog_register_device(&wdt->wdd); - if (ret) { - dev_err(&pdev->dev, "failed to register watchdog\n"); - goto err_clk_unprepare; + /* + * If WDT is already running, call WDT start which + * will stop the WDT, set timeouts as bootloader + * might use different ones and set running bit + * to inform the WDT subsystem to ping the WDT + */ + if (qcom_wdt_is_running(&wdt->wdd)) { + qcom_wdt_start(&wdt->wdd); + set_bit(WDOG_HW_RUNNING, &wdt->wdd.status); } + ret = devm_watchdog_register_device(dev, &wdt->wdd); + if (ret) + return ret; + platform_set_drvdata(pdev, wdt); return 0; +} + +static int __maybe_unused qcom_wdt_suspend(struct device *dev) +{ + struct qcom_wdt *wdt = dev_get_drvdata(dev); -err_clk_unprepare: - clk_disable_unprepare(wdt->clk); - return ret; + if (watchdog_active(&wdt->wdd)) + qcom_wdt_stop(&wdt->wdd); + + return 0; } -static int qcom_wdt_remove(struct platform_device *pdev) +static int __maybe_unused qcom_wdt_resume(struct device *dev) { - struct qcom_wdt *wdt = platform_get_drvdata(pdev); + struct qcom_wdt *wdt = dev_get_drvdata(dev); + + if (watchdog_active(&wdt->wdd)) + qcom_wdt_start(&wdt->wdd); - watchdog_unregister_device(&wdt->wdd); - clk_disable_unprepare(wdt->clk); return 0; } +static const struct dev_pm_ops qcom_wdt_pm_ops = { + SET_LATE_SYSTEM_SLEEP_PM_OPS(qcom_wdt_suspend, qcom_wdt_resume) +}; + static const struct of_device_id qcom_wdt_of_table[] = { - { .compatible = "qcom,kpss-timer", .data = reg_offset_data_apcs_tmr }, - { .compatible = "qcom,scss-timer", .data = reg_offset_data_apcs_tmr }, - { .compatible = "qcom,kpss-wdt", .data = reg_offset_data_kpss }, + { .compatible = "qcom,apss-wdt-ipq5424", .data = &match_data_ipq5424 }, + { .compatible = "qcom,kpss-timer", .data = &match_data_apcs_tmr }, + { .compatible = "qcom,scss-timer", .data = &match_data_apcs_tmr }, + { .compatible = "qcom,kpss-wdt", .data = &match_data_kpss }, { }, }; MODULE_DEVICE_TABLE(of, qcom_wdt_of_table); static struct platform_driver qcom_watchdog_driver = { .probe = qcom_wdt_probe, - .remove = qcom_wdt_remove, .driver = { .name = KBUILD_MODNAME, .of_match_table = qcom_wdt_of_table, + .pm = &qcom_wdt_pm_ops, }, }; module_platform_driver(qcom_watchdog_driver); |
