| 1 | // SPDX-License-Identifier: GPL-2.0-only |
| 2 | /* |
| 3 | * Copyright (C) 2024 ROHM Semiconductors |
| 4 | * |
| 5 | * ROHM BD96801 watchdog driver |
| 6 | */ |
| 7 | |
| 8 | #include <linux/bitfield.h> |
| 9 | #include <linux/interrupt.h> |
| 10 | #include <linux/kernel.h> |
| 11 | #include <linux/mfd/rohm-bd96801.h> |
| 12 | #include <linux/mfd/rohm-generic.h> |
| 13 | #include <linux/module.h> |
| 14 | #include <linux/of.h> |
| 15 | #include <linux/platform_device.h> |
| 16 | #include <linux/reboot.h> |
| 17 | #include <linux/regmap.h> |
| 18 | #include <linux/watchdog.h> |
| 19 | |
| 20 | static bool nowayout; |
| 21 | module_param(nowayout, bool, 0); |
| 22 | MODULE_PARM_DESC(nowayout, |
| 23 | "Watchdog cannot be stopped once started (default=\"false\")" ); |
| 24 | |
| 25 | #define BD96801_WD_TMO_SHORT_MASK 0x70 |
| 26 | #define BD96801_WD_RATIO_MASK 0x3 |
| 27 | #define BD96801_WD_TYPE_MASK 0x4 |
| 28 | #define BD96801_WD_TYPE_SLOW 0x4 |
| 29 | #define BD96801_WD_TYPE_WIN 0x0 |
| 30 | |
| 31 | #define BD96801_WD_EN_MASK 0x3 |
| 32 | #define BD96801_WD_IF_EN 0x1 |
| 33 | #define BD96801_WD_QA_EN 0x2 |
| 34 | #define BD96801_WD_DISABLE 0x0 |
| 35 | |
| 36 | #define BD96801_WD_ASSERT_MASK 0x8 |
| 37 | #define BD96801_WD_ASSERT_RST 0x8 |
| 38 | #define BD96801_WD_ASSERT_IRQ 0x0 |
| 39 | |
| 40 | #define BD96801_WD_FEED_MASK 0x1 |
| 41 | #define BD96801_WD_FEED 0x1 |
| 42 | |
| 43 | /* 1.1 mS */ |
| 44 | #define FASTNG_MIN 11 |
| 45 | #define FASTNG_MAX_US (100 * FASTNG_MIN << 7) |
| 46 | #define SLOWNG_MAX_US (16 * FASTNG_MAX_US) |
| 47 | |
| 48 | #define BD96801_WDT_DEFAULT_MARGIN_MS 1843 |
| 49 | /* Unit is seconds */ |
| 50 | #define DEFAULT_TIMEOUT 30 |
| 51 | |
| 52 | /* |
| 53 | * BD96801 WDG supports window mode so the TMO consists of SHORT and LONG |
| 54 | * timeout values. SHORT time is meaningful only in window mode where feeding |
| 55 | * period shorter than SHORT would be an error. LONG time is used to detect if |
| 56 | * feeding is not occurring within given time limit (SoC SW hangs). The LONG |
| 57 | * timeout time is a multiple of (2, 4, 8 or 16 times) the SHORT timeout. |
| 58 | */ |
| 59 | |
| 60 | struct wdtbd96801 { |
| 61 | struct device *dev; |
| 62 | struct regmap *regmap; |
| 63 | struct watchdog_device wdt; |
| 64 | }; |
| 65 | |
| 66 | static int bd96801_wdt_ping(struct watchdog_device *wdt) |
| 67 | { |
| 68 | struct wdtbd96801 *w = watchdog_get_drvdata(wdd: wdt); |
| 69 | |
| 70 | return regmap_update_bits(map: w->regmap, BD96801_REG_WD_FEED, |
| 71 | BD96801_WD_FEED_MASK, BD96801_WD_FEED); |
| 72 | } |
| 73 | |
| 74 | static int bd96801_wdt_start(struct watchdog_device *wdt) |
| 75 | { |
| 76 | struct wdtbd96801 *w = watchdog_get_drvdata(wdd: wdt); |
| 77 | |
| 78 | return regmap_update_bits(map: w->regmap, BD96801_REG_WD_CONF, |
| 79 | BD96801_WD_EN_MASK, BD96801_WD_IF_EN); |
| 80 | } |
| 81 | |
| 82 | static int bd96801_wdt_stop(struct watchdog_device *wdt) |
| 83 | { |
| 84 | struct wdtbd96801 *w = watchdog_get_drvdata(wdd: wdt); |
| 85 | |
| 86 | return regmap_update_bits(map: w->regmap, BD96801_REG_WD_CONF, |
| 87 | BD96801_WD_EN_MASK, BD96801_WD_DISABLE); |
| 88 | } |
| 89 | |
| 90 | static const struct watchdog_info bd96801_wdt_info = { |
| 91 | .options = WDIOF_MAGICCLOSE | WDIOF_KEEPALIVEPING | |
| 92 | WDIOF_SETTIMEOUT, |
| 93 | .identity = "BD96801 Watchdog" , |
| 94 | }; |
| 95 | |
| 96 | static const struct watchdog_ops bd96801_wdt_ops = { |
| 97 | .start = bd96801_wdt_start, |
| 98 | .stop = bd96801_wdt_stop, |
| 99 | .ping = bd96801_wdt_ping, |
| 100 | }; |
| 101 | |
| 102 | static int find_closest_fast(unsigned int target, int *sel, unsigned int *val) |
| 103 | { |
| 104 | unsigned int window = FASTNG_MIN; |
| 105 | int i; |
| 106 | |
| 107 | for (i = 0; i < 8 && window < target; i++) |
| 108 | window <<= 1; |
| 109 | |
| 110 | if (i == 8) |
| 111 | return -EINVAL; |
| 112 | |
| 113 | *val = window; |
| 114 | *sel = i; |
| 115 | |
| 116 | return 0; |
| 117 | } |
| 118 | |
| 119 | static int find_closest_slow_by_fast(unsigned int fast_val, unsigned int *target, |
| 120 | int *slowsel) |
| 121 | { |
| 122 | static const int multipliers[] = {2, 4, 8, 16}; |
| 123 | int sel; |
| 124 | |
| 125 | for (sel = 0; sel < ARRAY_SIZE(multipliers) && |
| 126 | multipliers[sel] * fast_val < *target; sel++) |
| 127 | ; |
| 128 | |
| 129 | if (sel == ARRAY_SIZE(multipliers)) |
| 130 | return -EINVAL; |
| 131 | |
| 132 | *slowsel = sel; |
| 133 | *target = multipliers[sel] * fast_val; |
| 134 | |
| 135 | return 0; |
| 136 | } |
| 137 | |
| 138 | static int find_closest_slow(unsigned int *target, int *slow_sel, int *fast_sel) |
| 139 | { |
| 140 | static const int multipliers[] = {2, 4, 8, 16}; |
| 141 | unsigned int window = FASTNG_MIN; |
| 142 | unsigned int val = 0; |
| 143 | int i, j; |
| 144 | |
| 145 | for (i = 0; i < 8; i++) { |
| 146 | for (j = 0; j < ARRAY_SIZE(multipliers); j++) { |
| 147 | unsigned int slow; |
| 148 | |
| 149 | slow = window * multipliers[j]; |
| 150 | if (slow >= *target && (!val || slow < val)) { |
| 151 | val = slow; |
| 152 | *fast_sel = i; |
| 153 | *slow_sel = j; |
| 154 | } |
| 155 | } |
| 156 | window <<= 1; |
| 157 | } |
| 158 | if (!val) |
| 159 | return -EINVAL; |
| 160 | |
| 161 | *target = val; |
| 162 | |
| 163 | return 0; |
| 164 | } |
| 165 | |
| 166 | static int bd96801_set_wdt_mode(struct wdtbd96801 *w, unsigned int hw_margin, |
| 167 | unsigned int hw_margin_min) |
| 168 | { |
| 169 | int fastng, slowng, type, ret, reg, mask; |
| 170 | struct device *dev = w->dev; |
| 171 | |
| 172 | |
| 173 | if (hw_margin_min * 1000 > FASTNG_MAX_US) { |
| 174 | dev_err(dev, "Unsupported fast timeout %u uS [max %u]\n" , |
| 175 | hw_margin_min * 1000, FASTNG_MAX_US); |
| 176 | |
| 177 | return -EINVAL; |
| 178 | } |
| 179 | |
| 180 | if (hw_margin * 1000 > SLOWNG_MAX_US) { |
| 181 | dev_err(dev, "Unsupported slow timeout %u uS [max %u]\n" , |
| 182 | hw_margin * 1000, SLOWNG_MAX_US); |
| 183 | |
| 184 | return -EINVAL; |
| 185 | } |
| 186 | |
| 187 | /* |
| 188 | * Convert to 100uS to guarantee reasonable timeouts fit in |
| 189 | * 32bit maintaining also a decent accuracy. |
| 190 | */ |
| 191 | hw_margin *= 10; |
| 192 | hw_margin_min *= 10; |
| 193 | |
| 194 | if (hw_margin_min) { |
| 195 | unsigned int min; |
| 196 | |
| 197 | type = BD96801_WD_TYPE_WIN; |
| 198 | dev_dbg(dev, "Setting type WINDOW 0x%x\n" , type); |
| 199 | ret = find_closest_fast(target: hw_margin_min, sel: &fastng, val: &min); |
| 200 | if (ret) |
| 201 | return ret; |
| 202 | |
| 203 | ret = find_closest_slow_by_fast(fast_val: min, target: &hw_margin, slowsel: &slowng); |
| 204 | if (ret) { |
| 205 | dev_err(dev, |
| 206 | "can't support slow timeout %u uS using fast %u uS. [max slow %u uS]\n" , |
| 207 | hw_margin * 100, min * 100, min * 100 * 16); |
| 208 | |
| 209 | return ret; |
| 210 | } |
| 211 | w->wdt.min_hw_heartbeat_ms = min / 10; |
| 212 | } else { |
| 213 | type = BD96801_WD_TYPE_SLOW; |
| 214 | dev_dbg(dev, "Setting type SLOW 0x%x\n" , type); |
| 215 | ret = find_closest_slow(target: &hw_margin, slow_sel: &slowng, fast_sel: &fastng); |
| 216 | if (ret) |
| 217 | return ret; |
| 218 | } |
| 219 | |
| 220 | w->wdt.max_hw_heartbeat_ms = hw_margin / 10; |
| 221 | |
| 222 | fastng = FIELD_PREP(BD96801_WD_TMO_SHORT_MASK, fastng); |
| 223 | |
| 224 | reg = slowng | fastng; |
| 225 | mask = BD96801_WD_RATIO_MASK | BD96801_WD_TMO_SHORT_MASK; |
| 226 | ret = regmap_update_bits(map: w->regmap, BD96801_REG_WD_TMO, |
| 227 | mask, val: reg); |
| 228 | if (ret) |
| 229 | return ret; |
| 230 | |
| 231 | ret = regmap_update_bits(map: w->regmap, BD96801_REG_WD_CONF, |
| 232 | BD96801_WD_TYPE_MASK, val: type); |
| 233 | |
| 234 | return ret; |
| 235 | } |
| 236 | |
| 237 | static int bd96801_set_heartbeat_from_hw(struct wdtbd96801 *w, |
| 238 | unsigned int conf_reg) |
| 239 | { |
| 240 | int ret; |
| 241 | unsigned int val, sel, fast; |
| 242 | |
| 243 | /* |
| 244 | * The BD96801 supports a somewhat peculiar QA-mode, which we do not |
| 245 | * support in this driver. If the QA-mode is enabled then we just |
| 246 | * warn and bail-out. |
| 247 | */ |
| 248 | if ((conf_reg & BD96801_WD_EN_MASK) != BD96801_WD_IF_EN) { |
| 249 | dev_err(w->dev, "watchdog set to Q&A mode - exiting\n" ); |
| 250 | return -EINVAL; |
| 251 | } |
| 252 | |
| 253 | ret = regmap_read(map: w->regmap, BD96801_REG_WD_TMO, val: &val); |
| 254 | if (ret) |
| 255 | return ret; |
| 256 | |
| 257 | sel = FIELD_GET(BD96801_WD_TMO_SHORT_MASK, val); |
| 258 | fast = FASTNG_MIN << sel; |
| 259 | |
| 260 | sel = (val & BD96801_WD_RATIO_MASK) + 1; |
| 261 | w->wdt.max_hw_heartbeat_ms = (fast << sel) / USEC_PER_MSEC; |
| 262 | |
| 263 | if ((conf_reg & BD96801_WD_TYPE_MASK) == BD96801_WD_TYPE_WIN) |
| 264 | w->wdt.min_hw_heartbeat_ms = fast / USEC_PER_MSEC; |
| 265 | |
| 266 | return 0; |
| 267 | } |
| 268 | |
| 269 | static int init_wdg_hw(struct wdtbd96801 *w) |
| 270 | { |
| 271 | u32 hw_margin[2]; |
| 272 | int count, ret; |
| 273 | u32 hw_margin_max = BD96801_WDT_DEFAULT_MARGIN_MS, hw_margin_min = 0; |
| 274 | |
| 275 | count = device_property_count_u32(dev: w->dev->parent, propname: "rohm,hw-timeout-ms" ); |
| 276 | if (count < 0 && count != -EINVAL) |
| 277 | return count; |
| 278 | |
| 279 | if (count > 0) { |
| 280 | if (count > ARRAY_SIZE(hw_margin)) |
| 281 | return -EINVAL; |
| 282 | |
| 283 | ret = device_property_read_u32_array(dev: w->dev->parent, |
| 284 | propname: "rohm,hw-timeout-ms" , |
| 285 | val: &hw_margin[0], nval: count); |
| 286 | if (ret < 0) |
| 287 | return ret; |
| 288 | |
| 289 | if (count == 1) |
| 290 | hw_margin_max = hw_margin[0]; |
| 291 | |
| 292 | if (count == 2) { |
| 293 | if (hw_margin[1] > hw_margin[0]) { |
| 294 | hw_margin_max = hw_margin[1]; |
| 295 | hw_margin_min = hw_margin[0]; |
| 296 | } else { |
| 297 | hw_margin_max = hw_margin[0]; |
| 298 | hw_margin_min = hw_margin[1]; |
| 299 | } |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | ret = bd96801_set_wdt_mode(w, hw_margin: hw_margin_max, hw_margin_min); |
| 304 | if (ret) |
| 305 | return ret; |
| 306 | |
| 307 | ret = device_property_match_string(dev: w->dev->parent, propname: "rohm,wdg-action" , |
| 308 | string: "prstb" ); |
| 309 | if (ret >= 0) { |
| 310 | ret = regmap_update_bits(map: w->regmap, BD96801_REG_WD_CONF, |
| 311 | BD96801_WD_ASSERT_MASK, |
| 312 | BD96801_WD_ASSERT_RST); |
| 313 | return ret; |
| 314 | } |
| 315 | |
| 316 | ret = device_property_match_string(dev: w->dev->parent, propname: "rohm,wdg-action" , |
| 317 | string: "intb-only" ); |
| 318 | if (ret >= 0) { |
| 319 | ret = regmap_update_bits(map: w->regmap, BD96801_REG_WD_CONF, |
| 320 | BD96801_WD_ASSERT_MASK, |
| 321 | BD96801_WD_ASSERT_IRQ); |
| 322 | return ret; |
| 323 | } |
| 324 | |
| 325 | return 0; |
| 326 | } |
| 327 | |
| 328 | static irqreturn_t bd96801_irq_hnd(int irq, void *data) |
| 329 | { |
| 330 | emergency_restart(); |
| 331 | |
| 332 | return IRQ_NONE; |
| 333 | } |
| 334 | |
| 335 | static int bd96801_wdt_probe(struct platform_device *pdev) |
| 336 | { |
| 337 | struct wdtbd96801 *w; |
| 338 | int ret, irq; |
| 339 | unsigned int val; |
| 340 | |
| 341 | w = devm_kzalloc(dev: &pdev->dev, size: sizeof(*w), GFP_KERNEL); |
| 342 | if (!w) |
| 343 | return -ENOMEM; |
| 344 | |
| 345 | w->regmap = dev_get_regmap(dev: pdev->dev.parent, NULL); |
| 346 | w->dev = &pdev->dev; |
| 347 | |
| 348 | w->wdt.info = &bd96801_wdt_info; |
| 349 | w->wdt.ops = &bd96801_wdt_ops; |
| 350 | w->wdt.parent = pdev->dev.parent; |
| 351 | w->wdt.timeout = DEFAULT_TIMEOUT; |
| 352 | watchdog_set_drvdata(wdd: &w->wdt, data: w); |
| 353 | |
| 354 | ret = regmap_read(map: w->regmap, BD96801_REG_WD_CONF, val: &val); |
| 355 | if (ret) |
| 356 | return dev_err_probe(dev: &pdev->dev, err: ret, |
| 357 | fmt: "Failed to get the watchdog state\n" ); |
| 358 | |
| 359 | /* |
| 360 | * If the WDG is already enabled we assume it is configured by boot. |
| 361 | * In this case we just update the hw-timeout based on values set to |
| 362 | * the timeout / mode registers and leave the hardware configs |
| 363 | * untouched. |
| 364 | */ |
| 365 | if ((val & BD96801_WD_EN_MASK) != BD96801_WD_DISABLE) { |
| 366 | dev_dbg(&pdev->dev, "watchdog was running during probe\n" ); |
| 367 | ret = bd96801_set_heartbeat_from_hw(w, conf_reg: val); |
| 368 | if (ret) |
| 369 | return ret; |
| 370 | |
| 371 | set_bit(WDOG_HW_RUNNING, addr: &w->wdt.status); |
| 372 | } else { |
| 373 | /* If WDG is not running so we will initializate it */ |
| 374 | ret = init_wdg_hw(w); |
| 375 | if (ret) |
| 376 | return ret; |
| 377 | } |
| 378 | |
| 379 | dev_dbg(w->dev, "heartbeat set to %u - %u\n" , |
| 380 | w->wdt.min_hw_heartbeat_ms, w->wdt.max_hw_heartbeat_ms); |
| 381 | |
| 382 | watchdog_init_timeout(wdd: &w->wdt, timeout_parm: 0, dev: pdev->dev.parent); |
| 383 | watchdog_set_nowayout(wdd: &w->wdt, nowayout); |
| 384 | watchdog_stop_on_reboot(wdd: &w->wdt); |
| 385 | |
| 386 | irq = platform_get_irq_byname(pdev, "bd96801-wdg" ); |
| 387 | if (irq > 0) { |
| 388 | ret = devm_request_threaded_irq(dev: &pdev->dev, irq, NULL, |
| 389 | thread_fn: bd96801_irq_hnd, |
| 390 | IRQF_ONESHOT, devname: "bd96801-wdg" , |
| 391 | NULL); |
| 392 | if (ret) |
| 393 | return dev_err_probe(dev: &pdev->dev, err: ret, |
| 394 | fmt: "Failed to register IRQ\n" ); |
| 395 | } |
| 396 | |
| 397 | return devm_watchdog_register_device(dev: &pdev->dev, &w->wdt); |
| 398 | } |
| 399 | |
| 400 | static const struct platform_device_id bd96801_wdt_id[] = { |
| 401 | { "bd96801-wdt" , }, |
| 402 | { } |
| 403 | }; |
| 404 | MODULE_DEVICE_TABLE(platform, bd96801_wdt_id); |
| 405 | |
| 406 | static struct platform_driver bd96801_wdt = { |
| 407 | .driver = { |
| 408 | .name = "bd96801-wdt" |
| 409 | }, |
| 410 | .probe = bd96801_wdt_probe, |
| 411 | .id_table = bd96801_wdt_id, |
| 412 | }; |
| 413 | module_platform_driver(bd96801_wdt); |
| 414 | |
| 415 | MODULE_AUTHOR("Matti Vaittinen <matti.vaittinen@fi.rohmeurope.com>" ); |
| 416 | MODULE_DESCRIPTION("BD96801 watchdog driver" ); |
| 417 | MODULE_LICENSE("GPL" ); |
| 418 | |