1 // SPDX-License-Identifier: GPL-2.0-only 2 /* 3 * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 4 */ 5 6 #include <linux/arm-smccc.h> 7 #include <linux/delay.h> 8 #include <linux/errno.h> 9 #include <linux/kernel.h> 10 #include <linux/mod_devicetable.h> 11 #include <linux/module.h> 12 #include <linux/platform_device.h> 13 #include <linux/watchdog.h> 14 15 #define GUNYAH_WDT_SMCCC_CALL_VAL(func_id) \ 16 ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_32,\ 17 ARM_SMCCC_OWNER_VENDOR_HYP, func_id) 18 19 /* SMCCC function IDs for watchdog operations */ 20 #define GUNYAH_WDT_CONTROL GUNYAH_WDT_SMCCC_CALL_VAL(0x0005) 21 #define GUNYAH_WDT_STATUS GUNYAH_WDT_SMCCC_CALL_VAL(0x0006) 22 #define GUNYAH_WDT_PING GUNYAH_WDT_SMCCC_CALL_VAL(0x0007) 23 #define GUNYAH_WDT_SET_TIME GUNYAH_WDT_SMCCC_CALL_VAL(0x0008) 24 25 /* 26 * Control values for GUNYAH_WDT_CONTROL. 27 * Bit 0 is used to enable or disable the watchdog. If this bit is set, 28 * then the watchdog is enabled and vice versa. 29 * Bit 1 should always be set to 1 as this bit is reserved in Gunyah and 30 * it's expected to be 1. 31 */ 32 #define WDT_CTRL_ENABLE (BIT(1) | BIT(0)) 33 #define WDT_CTRL_DISABLE BIT(1) 34 35 enum gunyah_error { 36 GUNYAH_ERROR_OK = 0, 37 GUNYAH_ERROR_UNIMPLEMENTED = -1, 38 GUNYAH_ERROR_ARG_INVAL = 1, 39 }; 40 41 /** 42 * gunyah_error_remap() - Remap Gunyah hypervisor errors into a Linux error code 43 * @gunyah_error: Gunyah hypercall return value 44 */ 45 static inline int gunyah_error_remap(enum gunyah_error gunyah_error) 46 { 47 switch (gunyah_error) { 48 case GUNYAH_ERROR_OK: 49 return 0; 50 case GUNYAH_ERROR_UNIMPLEMENTED: 51 return -EOPNOTSUPP; 52 default: 53 return -EINVAL; 54 } 55 } 56 57 static int gunyah_wdt_call(unsigned long func_id, unsigned long arg1, 58 unsigned long arg2) 59 { 60 struct arm_smccc_res res; 61 62 arm_smccc_1_1_smc(func_id, arg1, arg2, &res); 63 return gunyah_error_remap(res.a0); 64 } 65 66 static int gunyah_wdt_start(struct watchdog_device *wdd) 67 { 68 unsigned int timeout_ms; 69 struct device *dev = wdd->parent; 70 int ret; 71 72 ret = gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_DISABLE, 0); 73 if (ret && watchdog_active(wdd)) { 74 dev_err(dev, "%s: Failed to stop gunyah wdt %d\n", __func__, ret); 75 return ret; 76 } 77 78 timeout_ms = wdd->timeout * 1000; 79 ret = gunyah_wdt_call(GUNYAH_WDT_SET_TIME, timeout_ms, timeout_ms); 80 if (ret) { 81 dev_err(dev, "%s: Failed to set timeout for gunyah wdt %d\n", 82 __func__, ret); 83 return ret; 84 } 85 86 ret = gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_ENABLE, 0); 87 if (ret) 88 dev_err(dev, "%s: Failed to start gunyah wdt %d\n", __func__, ret); 89 90 return ret; 91 } 92 93 static int gunyah_wdt_stop(struct watchdog_device *wdd) 94 { 95 return gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_DISABLE, 0); 96 } 97 98 static int gunyah_wdt_ping(struct watchdog_device *wdd) 99 { 100 return gunyah_wdt_call(GUNYAH_WDT_PING, 0, 0); 101 } 102 103 static int gunyah_wdt_set_timeout(struct watchdog_device *wdd, 104 unsigned int timeout_sec) 105 { 106 wdd->timeout = timeout_sec; 107 108 if (watchdog_active(wdd)) 109 return gunyah_wdt_start(wdd); 110 111 return 0; 112 } 113 114 static int gunyah_wdt_get_time_since_last_ping(void) 115 { 116 struct arm_smccc_res res; 117 118 arm_smccc_1_1_smc(GUNYAH_WDT_STATUS, 0, 0, &res); 119 if (res.a0) 120 return gunyah_error_remap(res.a0); 121 122 return res.a2 / 1000; 123 } 124 125 static unsigned int gunyah_wdt_get_timeleft(struct watchdog_device *wdd) 126 { 127 int seconds_since_last_ping; 128 129 seconds_since_last_ping = gunyah_wdt_get_time_since_last_ping(); 130 if (seconds_since_last_ping < 0 || 131 seconds_since_last_ping > wdd->timeout) 132 return 0; 133 134 return wdd->timeout - seconds_since_last_ping; 135 } 136 137 static int gunyah_wdt_restart(struct watchdog_device *wdd, 138 unsigned long action, void *data) 139 { 140 /* Set timeout to 1ms and send a ping */ 141 gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_DISABLE, 0); 142 gunyah_wdt_call(GUNYAH_WDT_SET_TIME, 1, 1); 143 gunyah_wdt_call(GUNYAH_WDT_CONTROL, WDT_CTRL_ENABLE, 0); 144 gunyah_wdt_call(GUNYAH_WDT_PING, 0, 0); 145 146 /* Wait to make sure reset occurs */ 147 mdelay(100); 148 149 return 0; 150 } 151 152 static const struct watchdog_info gunyah_wdt_info = { 153 .identity = "Gunyah Watchdog", 154 .options = WDIOF_SETTIMEOUT 155 | WDIOF_KEEPALIVEPING 156 | WDIOF_MAGICCLOSE, 157 }; 158 159 static const struct watchdog_ops gunyah_wdt_ops = { 160 .owner = THIS_MODULE, 161 .start = gunyah_wdt_start, 162 .stop = gunyah_wdt_stop, 163 .ping = gunyah_wdt_ping, 164 .set_timeout = gunyah_wdt_set_timeout, 165 .get_timeleft = gunyah_wdt_get_timeleft, 166 .restart = gunyah_wdt_restart 167 }; 168 169 static int gunyah_wdt_probe(struct platform_device *pdev) 170 { 171 struct watchdog_device *wdd; 172 struct device *dev = &pdev->dev; 173 int ret; 174 175 ret = gunyah_wdt_call(GUNYAH_WDT_STATUS, 0, 0); 176 if (ret == -EOPNOTSUPP) 177 return -ENODEV; 178 179 if (ret) 180 return dev_err_probe(dev, ret, "status check failed\n"); 181 182 wdd = devm_kzalloc(dev, sizeof(*wdd), GFP_KERNEL); 183 if (!wdd) 184 return -ENOMEM; 185 186 wdd->info = &gunyah_wdt_info; 187 wdd->ops = &gunyah_wdt_ops; 188 wdd->parent = dev; 189 190 /* 191 * Although Gunyah expects 16-bit unsigned int values as timeout values 192 * in milliseconds, values above 0x8000 are reserved. This limits the 193 * max timeout value to 32 seconds. 194 */ 195 wdd->max_timeout = 32; /* seconds */ 196 wdd->min_timeout = 1; /* seconds */ 197 wdd->timeout = wdd->max_timeout; 198 199 gunyah_wdt_stop(wdd); 200 platform_set_drvdata(pdev, wdd); 201 watchdog_set_restart_priority(wdd, 0); 202 203 return devm_watchdog_register_device(dev, wdd); 204 } 205 206 static void gunyah_wdt_remove(struct platform_device *pdev) 207 { 208 struct watchdog_device *wdd = platform_get_drvdata(pdev); 209 210 gunyah_wdt_stop(wdd); 211 } 212 213 static int gunyah_wdt_suspend(struct device *dev) 214 { 215 struct watchdog_device *wdd = dev_get_drvdata(dev); 216 217 if (watchdog_active(wdd)) 218 gunyah_wdt_stop(wdd); 219 220 return 0; 221 } 222 223 static int gunyah_wdt_resume(struct device *dev) 224 { 225 struct watchdog_device *wdd = dev_get_drvdata(dev); 226 227 if (watchdog_active(wdd)) 228 gunyah_wdt_start(wdd); 229 230 return 0; 231 } 232 233 static DEFINE_SIMPLE_DEV_PM_OPS(gunyah_wdt_pm_ops, gunyah_wdt_suspend, gunyah_wdt_resume); 234 235 /* 236 * Gunyah watchdog is a vendor-specific hypervisor interface provided by the 237 * Gunyah hypervisor. Using QCOM SCM driver to detect Gunyah watchdog SMCCC 238 * hypervisor service and register platform device when the service is available 239 * allows this driver to operate independently of the devicetree and avoids 240 * adding the non-hardware nodes to the devicetree. 241 */ 242 static const struct platform_device_id gunyah_wdt_id[] = { 243 { .name = "gunyah-wdt" }, 244 {} 245 }; 246 MODULE_DEVICE_TABLE(platform, gunyah_wdt_id); 247 248 static struct platform_driver gunyah_wdt_driver = { 249 .driver = { 250 .name = "gunyah-wdt", 251 .pm = pm_sleep_ptr(&gunyah_wdt_pm_ops), 252 }, 253 .id_table = gunyah_wdt_id, 254 .probe = gunyah_wdt_probe, 255 .remove = gunyah_wdt_remove, 256 }; 257 258 module_platform_driver(gunyah_wdt_driver); 259 260 MODULE_DESCRIPTION("Gunyah Watchdog Driver"); 261 MODULE_LICENSE("GPL"); 262