| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2024 ROHM Semiconductors |
| * |
| * ROHM BD96801 watchdog driver |
| */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/interrupt.h> |
| #include <linux/kernel.h> |
| #include <linux/mfd/rohm-bd96801.h> |
| #include <linux/mfd/rohm-generic.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| #include <linux/reboot.h> |
| #include <linux/regmap.h> |
| #include <linux/watchdog.h> |
| |
| 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 <matti.vaittinen@fi.rohmeurope.com>"); |
| MODULE_DESCRIPTION("BD96801 watchdog driver"); |
| MODULE_LICENSE("GPL"); |