// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2024 ROHM Semiconductors * * ROHM BD96801 watchdog driver */ #include #include #include #include #include #include #include #include #include #include #include static bool nowayout; module_param(nowayout, bool, 0); MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default=\"false\")"); #define BD96801_WD_TMO_SHORT_MASK 0x70 #define BD96801_WD_RATIO_MASK 0x3 #define BD96801_WD_TYPE_MASK 0x4 #define BD96801_WD_TYPE_SLOW 0x4 #define BD96801_WD_TYPE_WIN 0x0 #define BD96801_WD_EN_MASK 0x3 #define BD96801_WD_IF_EN 0x1 #define BD96801_WD_QA_EN 0x2 #define BD96801_WD_DISABLE 0x0 #define BD96801_WD_ASSERT_MASK 0x8 #define BD96801_WD_ASSERT_RST 0x8 #define BD96801_WD_ASSERT_IRQ 0x0 #define BD96801_WD_FEED_MASK 0x1 #define BD96801_WD_FEED 0x1 /* 1.1 mS */ #define FASTNG_MIN 11 #define FASTNG_MAX_US (100 * FASTNG_MIN << 7) #define SLOWNG_MAX_US (16 * FASTNG_MAX_US) #define BD96801_WDT_DEFAULT_MARGIN_MS 1843 /* Unit is seconds */ #define DEFAULT_TIMEOUT 30 /* * BD96801 WDG supports window mode so the TMO consists of SHORT and LONG * timeout values. SHORT time is meaningful only in window mode where feeding * period shorter than SHORT would be an error. LONG time is used to detect if * feeding is not occurring within given time limit (SoC SW hangs). The LONG * timeout time is a multiple of (2, 4, 8 or 16 times) the SHORT timeout. */ struct wdtbd96801 { struct device *dev; struct regmap *regmap; struct watchdog_device wdt; }; static int bd96801_wdt_ping(struct watchdog_device *wdt) { struct wdtbd96801 *w = watchdog_get_drvdata(wdt); return regmap_update_bits(w->regmap, BD96801_REG_WD_FEED, BD96801_WD_FEED_MASK, BD96801_WD_FEED); } static int bd96801_wdt_start(struct watchdog_device *wdt) { struct wdtbd96801 *w = watchdog_get_drvdata(wdt); return regmap_update_bits(w->regmap, BD96801_REG_WD_CONF, BD96801_WD_EN_MASK, BD96801_WD_IF_EN); } static int bd96801_wdt_stop(struct watchdog_device *wdt) { struct wdtbd96801 *w = watchdog_get_drvdata(wdt); return regmap_update_bits(w->regmap, BD96801_REG_WD_CONF, BD96801_WD_EN_MASK, BD96801_WD_DISABLE); } static const struct watchdog_info bd96801_wdt_info = { .options = WDIOF_MAGICCLOSE | WDIOF_KEEPALIVEPING | WDIOF_SETTIMEOUT, .identity = "BD96801 Watchdog", }; static const struct watchdog_ops bd96801_wdt_ops = { .start = bd96801_wdt_start, .stop = bd96801_wdt_stop, .ping = bd96801_wdt_ping, }; static int find_closest_fast(unsigned int target, int *sel, unsigned int *val) { unsigned int window = FASTNG_MIN; int i; for (i = 0; i < 8 && window < target; i++) window <<= 1; if (i == 8) return -EINVAL; *val = window; *sel = i; return 0; } static int find_closest_slow_by_fast(unsigned int fast_val, unsigned int *target, int *slowsel) { static const int multipliers[] = {2, 4, 8, 16}; int sel; for (sel = 0; sel < ARRAY_SIZE(multipliers) && multipliers[sel] * fast_val < *target; sel++) ; if (sel == ARRAY_SIZE(multipliers)) return -EINVAL; *slowsel = sel; *target = multipliers[sel] * fast_val; return 0; } static int find_closest_slow(unsigned int *target, int *slow_sel, int *fast_sel) { static const int multipliers[] = {2, 4, 8, 16}; unsigned int window = FASTNG_MIN; unsigned int val = 0; int i, j; for (i = 0; i < 8; i++) { for (j = 0; j < ARRAY_SIZE(multipliers); j++) { unsigned int slow; slow = window * multipliers[j]; if (slow >= *target && (!val || slow < val)) { val = slow; *fast_sel = i; *slow_sel = j; } } window <<= 1; } if (!val) return -EINVAL; *target = val; return 0; } static int bd96801_set_wdt_mode(struct wdtbd96801 *w, unsigned int hw_margin, unsigned int hw_margin_min) { int fastng, slowng, type, ret, reg, mask; struct device *dev = w->dev; if (hw_margin_min * 1000 > FASTNG_MAX_US) { dev_err(dev, "Unsupported fast timeout %u uS [max %u]\n", hw_margin_min * 1000, FASTNG_MAX_US); return -EINVAL; } if (hw_margin * 1000 > SLOWNG_MAX_US) { dev_err(dev, "Unsupported slow timeout %u uS [max %u]\n", hw_margin * 1000, SLOWNG_MAX_US); return -EINVAL; } /* * Convert to 100uS to guarantee reasonable timeouts fit in * 32bit maintaining also a decent accuracy. */ hw_margin *= 10; hw_margin_min *= 10; if (hw_margin_min) { unsigned int min; type = BD96801_WD_TYPE_WIN; dev_dbg(dev, "Setting type WINDOW 0x%x\n", type); ret = find_closest_fast(hw_margin_min, &fastng, &min); if (ret) return ret; ret = find_closest_slow_by_fast(min, &hw_margin, &slowng); if (ret) { dev_err(dev, "can't support slow timeout %u uS using fast %u uS. [max slow %u uS]\n", hw_margin * 100, min * 100, min * 100 * 16); return ret; } w->wdt.min_hw_heartbeat_ms = min / 10; } else { type = BD96801_WD_TYPE_SLOW; dev_dbg(dev, "Setting type SLOW 0x%x\n", type); ret = find_closest_slow(&hw_margin, &slowng, &fastng); if (ret) return ret; } w->wdt.max_hw_heartbeat_ms = hw_margin / 10; fastng = FIELD_PREP(BD96801_WD_TMO_SHORT_MASK, fastng); reg = slowng | fastng; mask = BD96801_WD_RATIO_MASK | BD96801_WD_TMO_SHORT_MASK; ret = regmap_update_bits(w->regmap, BD96801_REG_WD_TMO, mask, reg); if (ret) return ret; ret = regmap_update_bits(w->regmap, BD96801_REG_WD_CONF, BD96801_WD_TYPE_MASK, type); return ret; } static int bd96801_set_heartbeat_from_hw(struct wdtbd96801 *w, unsigned int conf_reg) { int ret; unsigned int val, sel, fast; /* * The BD96801 supports a somewhat peculiar QA-mode, which we do not * support in this driver. If the QA-mode is enabled then we just * warn and bail-out. */ if ((conf_reg & BD96801_WD_EN_MASK) != BD96801_WD_IF_EN) { dev_err(w->dev, "watchdog set to Q&A mode - exiting\n"); return -EINVAL; } ret = regmap_read(w->regmap, BD96801_REG_WD_TMO, &val); if (ret) return ret; sel = FIELD_GET(BD96801_WD_TMO_SHORT_MASK, val); fast = FASTNG_MIN << sel; sel = (val & BD96801_WD_RATIO_MASK) + 1; w->wdt.max_hw_heartbeat_ms = (fast << sel) / USEC_PER_MSEC; if ((conf_reg & BD96801_WD_TYPE_MASK) == BD96801_WD_TYPE_WIN) w->wdt.min_hw_heartbeat_ms = fast / USEC_PER_MSEC; return 0; } static int init_wdg_hw(struct wdtbd96801 *w) { u32 hw_margin[2]; int count, ret; u32 hw_margin_max = BD96801_WDT_DEFAULT_MARGIN_MS, hw_margin_min = 0; count = device_property_count_u32(w->dev->parent, "rohm,hw-timeout-ms"); if (count < 0 && count != -EINVAL) return count; if (count > 0) { if (count > ARRAY_SIZE(hw_margin)) return -EINVAL; ret = device_property_read_u32_array(w->dev->parent, "rohm,hw-timeout-ms", &hw_margin[0], count); if (ret < 0) return ret; if (count == 1) hw_margin_max = hw_margin[0]; if (count == 2) { if (hw_margin[1] > hw_margin[0]) { hw_margin_max = hw_margin[1]; hw_margin_min = hw_margin[0]; } else { hw_margin_max = hw_margin[0]; hw_margin_min = hw_margin[1]; } } } ret = bd96801_set_wdt_mode(w, hw_margin_max, hw_margin_min); if (ret) return ret; ret = device_property_match_string(w->dev->parent, "rohm,wdg-action", "prstb"); if (ret >= 0) { ret = regmap_update_bits(w->regmap, BD96801_REG_WD_CONF, BD96801_WD_ASSERT_MASK, BD96801_WD_ASSERT_RST); return ret; } ret = device_property_match_string(w->dev->parent, "rohm,wdg-action", "intb-only"); if (ret >= 0) { ret = regmap_update_bits(w->regmap, BD96801_REG_WD_CONF, BD96801_WD_ASSERT_MASK, BD96801_WD_ASSERT_IRQ); return ret; } return 0; } static irqreturn_t bd96801_irq_hnd(int irq, void *data) { emergency_restart(); return IRQ_NONE; } static int bd96801_wdt_probe(struct platform_device *pdev) { struct wdtbd96801 *w; int ret, irq; unsigned int val; w = devm_kzalloc(&pdev->dev, sizeof(*w), GFP_KERNEL); if (!w) return -ENOMEM; w->regmap = dev_get_regmap(pdev->dev.parent, NULL); w->dev = &pdev->dev; w->wdt.info = &bd96801_wdt_info; w->wdt.ops = &bd96801_wdt_ops; w->wdt.parent = pdev->dev.parent; w->wdt.timeout = DEFAULT_TIMEOUT; watchdog_set_drvdata(&w->wdt, w); ret = regmap_read(w->regmap, BD96801_REG_WD_CONF, &val); if (ret) return dev_err_probe(&pdev->dev, ret, "Failed to get the watchdog state\n"); /* * If the WDG is already enabled we assume it is configured by boot. * In this case we just update the hw-timeout based on values set to * the timeout / mode registers and leave the hardware configs * untouched. */ if ((val & BD96801_WD_EN_MASK) != BD96801_WD_DISABLE) { dev_dbg(&pdev->dev, "watchdog was running during probe\n"); ret = bd96801_set_heartbeat_from_hw(w, val); if (ret) return ret; set_bit(WDOG_HW_RUNNING, &w->wdt.status); } else { /* If WDG is not running so we will initializate it */ ret = init_wdg_hw(w); if (ret) return ret; } dev_dbg(w->dev, "heartbeat set to %u - %u\n", w->wdt.min_hw_heartbeat_ms, w->wdt.max_hw_heartbeat_ms); watchdog_init_timeout(&w->wdt, 0, pdev->dev.parent); watchdog_set_nowayout(&w->wdt, nowayout); watchdog_stop_on_reboot(&w->wdt); irq = platform_get_irq_byname(pdev, "bd96801-wdg"); if (irq > 0) { ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, bd96801_irq_hnd, IRQF_ONESHOT, "bd96801-wdg", NULL); if (ret) return dev_err_probe(&pdev->dev, ret, "Failed to register IRQ\n"); } return devm_watchdog_register_device(&pdev->dev, &w->wdt); } static const struct platform_device_id bd96801_wdt_id[] = { { "bd96801-wdt", }, { } }; MODULE_DEVICE_TABLE(platform, bd96801_wdt_id); static struct platform_driver bd96801_wdt = { .driver = { .name = "bd96801-wdt" }, .probe = bd96801_wdt_probe, .id_table = bd96801_wdt_id, }; module_platform_driver(bd96801_wdt); MODULE_AUTHOR("Matti Vaittinen "); MODULE_DESCRIPTION("BD96801 watchdog driver"); MODULE_LICENSE("GPL");