summaryrefslogtreecommitdiffstats
path: root/drivers/watchdog/bd96801_wdt.c
blob: 12b74fd2bc052faa056fab8560e0d91759e5924d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
// 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");