| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. |
| */ |
| |
| #include <linux/arm-smccc.h> |
| #include <linux/delay.h> |
| #include <linux/errno.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/watchdog.h> |
| |
| #define GUNYAH_WDT_SMCCC_CALL_VAL(func_id) \ |
| ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_32,\ |
| ARM_SMCCC_OWNER_VENDOR_HYP, func_id) |
| |
| /* SMCCC function IDs for watchdog operations */ |
| #define GUNYAH_WDT_CONTROL GUNYAH_WDT_SMCCC_CALL_VAL(0x0005) |
| #define GUNYAH_WDT_STATUS GUNYAH_WDT_SMCCC_CALL_VAL(0x0006) |
| #define GUNYAH_WDT_PING GUNYAH_WDT_SMCCC_CALL_VAL(0x0007) |
| #define GUNYAH_WDT_SET_TIME GUNYAH_WDT_SMCCC_CALL_VAL(0x0008) |
| |
| /* |
| * Control values for GUNYAH_WDT_CONTROL. |
| * Bit 0 is used to enable or disable the watchdog. If this bit is set, |
| * then the watchdog is enabled and vice versa. |
| * Bit 1 should always be set to 1 as this bit is reserved in Gunyah and |
| * it's expected to be 1. |
| */ |
| #define WDT_CTRL_ENABLE (BIT(1) | BIT(0)) |
| #define WDT_CTRL_DISABLE BIT(1) |
| |
| enum gunyah_error { |
| GUNYAH_ERROR_OK = 0, |
| GUNYAH_ERROR_UNIMPLEMENTED = -1, |
| GUNYAH_ERROR_ARG_INVAL = 1, |
| }; |
| |
| /** |
| * gunyah_error_remap() - Remap Gunyah hypervisor errors into a Linux error code |
| * @gunyah_error: Gunyah hypercall return value |
| */ |
| static inline int gunyah_error_remap(enum gunyah_error gunyah_error) |
| { |
| switch (gunyah_error) { |
| case GUNYAH_ERROR_OK: |
| return 0; |
| case GUNYAH_ERROR_UNIMPLEMENTED: |
| return -EOPNOTSUPP; |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| static int gunyah_wdt_call(unsigned long func_id, unsigned long arg1, |
| unsigned long arg2) |
| { |
| struct arm_smccc_res res; |
| |
| arm_smccc_1_1_smc(func_id, arg1, arg2, &res); |
| return gunyah_error_remap(res.a0); |
| } |
| |
| static int gunyah_wdt_start(struct watchdog_device *wdd) |
| { |
| unsigned int timeout_ms; |
| struct device *dev = wdd->parent; |
| int ret; |
| |
| ret = gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_DISABLE, 0); |
| if (ret && watchdog_active(wdd)) { |
| dev_err(dev, "%s: Failed to stop gunyah wdt %d\n", __func__, ret); |
| return ret; |
| } |
| |
| timeout_ms = wdd->timeout * 1000; |
| ret = gunyah_wdt_call(GUNYAH_WDT_SET_TIME, timeout_ms, timeout_ms); |
| if (ret) { |
| dev_err(dev, "%s: Failed to set timeout for gunyah wdt %d\n", |
| __func__, ret); |
| return ret; |
| } |
| |
| ret = gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_ENABLE, 0); |
| if (ret) |
| dev_err(dev, "%s: Failed to start gunyah wdt %d\n", __func__, ret); |
| |
| return ret; |
| } |
| |
| static int gunyah_wdt_stop(struct watchdog_device *wdd) |
| { |
| return gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_DISABLE, 0); |
| } |
| |
| static int gunyah_wdt_ping(struct watchdog_device *wdd) |
| { |
| return gunyah_wdt_call(GUNYAH_WDT_PING, 0, 0); |
| } |
| |
| static int gunyah_wdt_set_timeout(struct watchdog_device *wdd, |
| unsigned int timeout_sec) |
| { |
| wdd->timeout = timeout_sec; |
| |
| if (watchdog_active(wdd)) |
| return gunyah_wdt_start(wdd); |
| |
| return 0; |
| } |
| |
| static int gunyah_wdt_get_time_since_last_ping(void) |
| { |
| struct arm_smccc_res res; |
| |
| arm_smccc_1_1_smc(GUNYAH_WDT_STATUS, 0, 0, &res); |
| if (res.a0) |
| return gunyah_error_remap(res.a0); |
| |
| return res.a2 / 1000; |
| } |
| |
| static unsigned int gunyah_wdt_get_timeleft(struct watchdog_device *wdd) |
| { |
| int seconds_since_last_ping; |
| |
| seconds_since_last_ping = gunyah_wdt_get_time_since_last_ping(); |
| if (seconds_since_last_ping < 0 || |
| seconds_since_last_ping > wdd->timeout) |
| return 0; |
| |
| return wdd->timeout - seconds_since_last_ping; |
| } |
| |
| static int gunyah_wdt_restart(struct watchdog_device *wdd, |
| unsigned long action, void *data) |
| { |
| /* Set timeout to 1ms and send a ping */ |
| gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_DISABLE, 0); |
| gunyah_wdt_call(GUNYAH_WDT_SET_TIME, 1, 1); |
| gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_ENABLE, 0); |
| gunyah_wdt_call(GUNYAH_WDT_PING, 0, 0); |
| |
| /* Wait to make sure reset occurs */ |
| mdelay(100); |
| |
| return 0; |
| } |
| |
| static const struct watchdog_info gunyah_wdt_info = { |
| .identity = "Gunyah Watchdog", |
| .options = WDIOF_SETTIMEOUT |
| | WDIOF_KEEPALIVEPING |
| | WDIOF_MAGICCLOSE, |
| }; |
| |
| static const struct watchdog_ops gunyah_wdt_ops = { |
| .owner = THIS_MODULE, |
| .start = gunyah_wdt_start, |
| .stop = gunyah_wdt_stop, |
| .ping = gunyah_wdt_ping, |
| .set_timeout = gunyah_wdt_set_timeout, |
| .get_timeleft = gunyah_wdt_get_timeleft, |
| .restart = gunyah_wdt_restart |
| }; |
| |
| static int gunyah_wdt_probe(struct platform_device *pdev) |
| { |
| struct watchdog_device *wdd; |
| struct device *dev = &pdev->dev; |
| int ret; |
| |
| ret = gunyah_wdt_call(GUNYAH_WDT_STATUS, 0, 0); |
| if (ret == -EOPNOTSUPP) |
| return -ENODEV; |
| |
| if (ret) |
| return dev_err_probe(dev, ret, "status check failed\n"); |
| |
| wdd = devm_kzalloc(dev, sizeof(*wdd), GFP_KERNEL); |
| if (!wdd) |
| return -ENOMEM; |
| |
| wdd->info = &gunyah_wdt_info; |
| wdd->ops = &gunyah_wdt_ops; |
| wdd->parent = dev; |
| |
| /* |
| * Although Gunyah expects 16-bit unsigned int values as timeout values |
| * in milliseconds, values above 0x8000 are reserved. This limits the |
| * max timeout value to 32 seconds. |
| */ |
| wdd->max_timeout = 32; /* seconds */ |
| wdd->min_timeout = 1; /* seconds */ |
| wdd->timeout = wdd->max_timeout; |
| |
| gunyah_wdt_stop(wdd); |
| platform_set_drvdata(pdev, wdd); |
| watchdog_set_restart_priority(wdd, 0); |
| |
| return devm_watchdog_register_device(dev, wdd); |
| } |
| |
| static void gunyah_wdt_remove(struct platform_device *pdev) |
| { |
| struct watchdog_device *wdd = platform_get_drvdata(pdev); |
| |
| gunyah_wdt_stop(wdd); |
| } |
| |
| static int gunyah_wdt_suspend(struct device *dev) |
| { |
| struct watchdog_device *wdd = dev_get_drvdata(dev); |
| |
| if (watchdog_active(wdd)) |
| gunyah_wdt_stop(wdd); |
| |
| return 0; |
| } |
| |
| static int gunyah_wdt_resume(struct device *dev) |
| { |
| struct watchdog_device *wdd = dev_get_drvdata(dev); |
| |
| if (watchdog_active(wdd)) |
| gunyah_wdt_start(wdd); |
| |
| return 0; |
| } |
| |
| static DEFINE_SIMPLE_DEV_PM_OPS(gunyah_wdt_pm_ops, gunyah_wdt_suspend, gunyah_wdt_resume); |
| |
| /* |
| * Gunyah watchdog is a vendor-specific hypervisor interface provided by the |
| * Gunyah hypervisor. Using QCOM SCM driver to detect Gunyah watchdog SMCCC |
| * hypervisor service and register platform device when the service is available |
| * allows this driver to operate independently of the devicetree and avoids |
| * adding the non-hardware nodes to the devicetree. |
| */ |
| static const struct platform_device_id gunyah_wdt_id[] = { |
| { .name = "gunyah-wdt" }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(platform, gunyah_wdt_id); |
| |
| static struct platform_driver gunyah_wdt_driver = { |
| .driver = { |
| .name = "gunyah-wdt", |
| .pm = pm_sleep_ptr(&gunyah_wdt_pm_ops), |
| }, |
| .id_table = gunyah_wdt_id, |
| .probe = gunyah_wdt_probe, |
| .remove = gunyah_wdt_remove, |
| }; |
| |
| module_platform_driver(gunyah_wdt_driver); |
| |
| MODULE_DESCRIPTION("Gunyah Watchdog Driver"); |
| MODULE_LICENSE("GPL"); |