summaryrefslogtreecommitdiff
path: root/drivers/watchdog/atcwdt200_wdt.c
blob: 8e3b18aea368ff1a8049d21d30b1f064af1eaadc (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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Andes ATCWDT200 watchdog timer driver.
 *
 * Copyright (C) 2025 Andes Technology Corporation
 */

#include <linux/bitfield.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <linux/dev_printk.h>
#include <linux/math64.h>
#include <linux/minmax.h>
#include <linux/moduleparam.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>
#include <linux/pm.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <linux/watchdog.h>

/* Register definitions */
#define REG_CTRL		0x10
#define REG_RESTART		0x14
#define REG_WRITE_EN		0x18
#define REG_STATUS		0x1C

/* Control Register */
#define CTRL_RST_TIME_MSK	GENMASK(10, 8)
#define CTRL_RST_TIME_SET(x)	FIELD_PREP(CTRL_RST_TIME_MSK, x)
#define CTRL_INT_TIME_MSK	GENMASK(7, 4)
#define CTRL_INT_TIME_SET(x)	FIELD_PREP(CTRL_INT_TIME_MSK, x)
#define CTRL_INT_TIME_GET(x)	FIELD_GET(CTRL_INT_TIME_MSK, x)
#define CTRL_RST_EN		BIT(3)
#define CTRL_CLK_SEL		BIT(1)
#define CTRL_CLK_SEL_PCLK	1
#define CTRL_CLK_SEL_SET(x)	FIELD_PREP(CTRL_CLK_SEL, x)
#define CTRL_WDT_EN		BIT(0)

/* Restart Register */
#define RESTART_MAGIC		0xCAFE

/* Write Enable Register */
#define WRITE_EN_MAGIC		0x5AA5

/* Status Register */
#define STATUS_INT_EXPIRED	BIT(1)

/* The default timeout value in seconds */
#define ATCWDT_TIMEOUT		4

/* Define the array size for each timer type */
#define TMR_SZ_RST		8
#define TMR_SZ_INT_16		8
#define TMR_SZ_INT_32		16

#define DRV_NAME		"atcwdt200"
/**
 * enum timer_type - Supported timer types for ATCWDT200 watchdog driver
 * @TMR_RST:      Reset timer (non-interrupt).
 * @TMR_INT_16:   16-bit interrupt timer supported by hardware.
 * @TMR_INT_32:   32-bit interrupt timer supported by hardware.
 * @TMR_UNKNOWN:  Timer type cannot be determined.
 */
enum timer_type {
	TMR_RST,
	TMR_INT_16,
	TMR_INT_32,
	TMR_UNKNOWN
};

static unsigned int timeout = ATCWDT_TIMEOUT;
static bool nowayout = WATCHDOG_NOWAYOUT;

/**
 * struct atcwdt_drv - ATCWDT200 watchdog driver private data
 * @wdt_dev:        Watchdog device used by the watchdog framework.
 * @regmap:         Register map for accessing hardware registers.
 * @clk:            Hardware clock used by the watchdog timer.
 * @lock:           Spinlock protecting register accesses and driver state.
 * @clk_freq:       Input clock frequency of the ATCWDT200.
 * @clk_src:        Selected clock source for the watchdog timer.
 * @int_timer_type: Detected interrupt timer type (16-bit, 32-bit, or unknown).
 */
struct atcwdt_drv {
	struct watchdog_device	wdt_dev;
	struct regmap		*regmap;
	struct clk		*clk;
	spinlock_t		lock;
	unsigned int		clk_freq;
	unsigned char		clk_src;
	unsigned char		int_timer_type;
};

static const struct watchdog_info atcwdt_info = {
	.identity = DRV_NAME,
	.options = WDIOF_SETTIMEOUT |
		   WDIOF_KEEPALIVEPING |
		   WDIOF_MAGICCLOSE,
};

/**
 * atcwdt_get_index - Get the interval value for the specified timer type
 * @index: The index of the interval in the array
 * @timer_type: The type of timer, which can be TMR_RST, TMR_INT_16, or
 *              TMR_INT_32.
 *
 * This function retrieves the interval value based on the timer type and
 * ensures the index stays within the valid range for the given timer type.
 * For TMR_RST:
 *  - The maximum array size is 8 (index range: 0-7).
 * For TMR_INT_16:
 *  - The maximum array size is 8 (index range: 0-7).
 * For TMR_INT_32:
 *  - The maximum array size is 16 (index range: 0-15).
 *
 * If the index exceeds the maximum array size, the function will return
 * the last element of the respective array.
 */
static inline unsigned char atcwdt_get_index(unsigned char index,
					     enum timer_type timer_type)
{
	static const unsigned char rst_timer_interval[TMR_SZ_RST] = {
		7, 8, 9, 10, 11, 12, 13, 14};
	static const unsigned char int_timer_interval[TMR_SZ_INT_32] = {
		6, 8, 10, 11, 12, 13, 14, 15, 17, 19, 21, 23, 25, 27, 29, 31};
	unsigned char array_index;

	if (timer_type == TMR_RST) {
		array_index = min(index, TMR_SZ_RST - 1);
		return rst_timer_interval[array_index];
	}

	if (timer_type == TMR_INT_32)
		array_index = min(index, TMR_SZ_INT_32 - 1);
	else
		array_index = min(index, TMR_SZ_INT_16 - 1);

	return int_timer_interval[array_index];
}

/**
 * atcwdt_get_clock_period - Calculate the closest clock period based on a
 *                           given tick count
 * @tick: The target tick count to match
 * @timer_type: The type of timer, which can be TMR_RST, TMR_INT_16, or
 *              TMR_INT_32.
 * @index: Pointer to store the index of the selected parameter
 *
 * This function calculates the closest clock period to the given tick count
 * by iterating through the timer parameters and selecting the one that
 * minimizes the difference between the target tick count and the calculated
 * clock period. The function determines the index of the closest parameter
 * and returns the difference between the target tick count and the selected
 * clock period.
 *
 * Return: The difference between the target tick count and the selected
 * clock period.
 */
static long long atcwdt_get_clock_period(long long tick,
					 enum timer_type timer_type,
					 unsigned char *index)
{
	long long result;
	unsigned char size;
	char i;

	if (timer_type == TMR_RST)
		size = TMR_SZ_RST;
	else if (timer_type == TMR_INT_32)
		size = TMR_SZ_INT_32;
	else
		size = TMR_SZ_INT_16;

	*index = size - 1;
	for (i = 0; i < size; i++) {
		result = tick - (1LL << atcwdt_get_index(i, timer_type));

		if (result <= 1) {
			*index = i;
			break;
		}
	}

	return result;
}

/**
 * atcwdt_get_timeout_params - Calculate optimal parameters for Watchdog Timer
 * @drv_data: Pointer to the Watchdog driver data structure
 * @timeout: Desired timeout value (in seconds)
 * @int_timer_params: Pointer to store the calculated interrupt timer
 *                    parameter index
 * @rst_timer_params: Pointer to store the calculated reset timer parameter
 *                    index
 *
 * This function calculates the optimal parameter combination for the
 * interrupt timer and reset timer of the Watchdog Timer to achieve a
 * timeout value closest to, but not less than the specified timeout.
 *
 * Algorithm:
 * 1. The parameters for both the interrupt timer and reset timer are
 *    predefined as a series of options represented as powers of 2.
 * 2. The function first determines the interrupt timer's parameter index
 *    that provides a time closest to and not exceeding the desired timeout.
 * 3. Based on the selected interrupt timer, it calculates the required
 *    reset timer parameter to ensure the total timeout matches the target.
 *
 * Return: The calculated parameter indices are stored in the provided
 *         pointers.
 */
static void atcwdt_get_timeout_params(struct atcwdt_drv *drv_data,
				      unsigned int timeout,
				      unsigned char *int_timer_params,
				      unsigned char *rst_timer_params)
{
	long long rest_time_ms;
	long long result;
	long long tick;
	unsigned char rst_index;
	unsigned char int_index;
	unsigned char above;
	unsigned char below;

	tick = (long long)timeout * drv_data->clk_freq;
	result = atcwdt_get_clock_period(tick,
					 drv_data->int_timer_type,
					 &above);
	if (result == 0 || above == 0) {
		*int_timer_params = above;
		*rst_timer_params = 0;
		return;
	}
	below = above - 1;

	int_index = atcwdt_get_index(below, drv_data->int_timer_type);
	rest_time_ms = timeout * 1000LL
		       - div64_s64(1000LL << int_index, drv_data->clk_freq);

	result = atcwdt_get_clock_period(rest_time_ms * drv_data->clk_freq,
					 TMR_RST,
					 &rst_index);

	if (result > 1) {
		*int_timer_params = above;
		*rst_timer_params = 0;
	} else {
		*int_timer_params = below;
		*rst_timer_params = rst_index;
	}
}

/**
 * atcwdt_get_int_timer_type - Get the supported interrupt timer type.
 * @drv_data: Pointer to the watchdog driver data structure.
 *
 * This function tests the writable bits in the IntTime field of the control
 * register to determine the interrupt timer type supported by the hardware.
 *
 * Note: This function must only be called when the ATCWDT200 watchdog is
 * disabled. If the watchdog is enabled, this function returns TMR_UNKNOWN.
 *
 * Returns: The interrupt timer type supported by the hardware.
 */
static int atcwdt_get_int_timer_type(struct atcwdt_drv *drv_data)
{
	struct device *dev = drv_data->wdt_dev.parent;
	unsigned int val;
	int ret  = 0;

	spin_lock(&drv_data->lock);
	regmap_read(drv_data->regmap, REG_CTRL, &val);
	if (val & CTRL_WDT_EN) {
		spin_unlock(&drv_data->lock);
		return TMR_UNKNOWN;
	}

	/*
	 * Configures the IntTime field with the maximum mask value
	 * (CTRL_INT_TIME_MSK), reads its value from the control register
	 * to identify the maximum writable bits.
	 */
	regmap_write(drv_data->regmap, REG_WRITE_EN, WRITE_EN_MAGIC);
	regmap_write(drv_data->regmap, REG_CTRL, CTRL_INT_TIME_MSK);
	regmap_read(drv_data->regmap, REG_CTRL, &val);
	spin_unlock(&drv_data->lock);

	val = CTRL_INT_TIME_GET(val);
	switch (val) {
	case 7:
		drv_data->int_timer_type = TMR_INT_16;
		break;
	case 15:
		drv_data->int_timer_type = TMR_INT_32;
		break;
	default:
		drv_data->int_timer_type = TMR_UNKNOWN;
		ret = dev_err_probe(dev, -ENODEV,
				    "Failed to detect interrupt timer type\n");
	}

	return ret;
}

static int atcwdt_ping(struct watchdog_device *wdt_dev)
{
	struct atcwdt_drv *drv_data = watchdog_get_drvdata(wdt_dev);

	spin_lock(&drv_data->lock);
	regmap_write(drv_data->regmap, REG_WRITE_EN, WRITE_EN_MAGIC);
	regmap_write(drv_data->regmap, REG_RESTART, RESTART_MAGIC);
	regmap_update_bits(drv_data->regmap, REG_STATUS, STATUS_INT_EXPIRED,
			   STATUS_INT_EXPIRED);
	spin_unlock(&drv_data->lock);

	return 0;
}

static int atcwdt_set_timeout(struct watchdog_device *wdt_dev,
			      unsigned int timeout)
{
	struct atcwdt_drv *drv_data = watchdog_get_drvdata(wdt_dev);
	unsigned int value;
	unsigned char  rst_val;
	unsigned char  int_val;

	wdt_dev->timeout = timeout;
	atcwdt_get_timeout_params(drv_data, timeout, &int_val, &rst_val);

	spin_lock(&drv_data->lock);
	regmap_write(drv_data->regmap, REG_WRITE_EN, WRITE_EN_MAGIC);

	value = CTRL_RST_TIME_SET(rst_val) |
		CTRL_INT_TIME_SET(int_val) |
		CTRL_CLK_SEL_SET(drv_data->clk_src);
	regmap_update_bits(drv_data->regmap,
			   REG_CTRL,
			   CTRL_RST_TIME_MSK |
			   CTRL_INT_TIME_MSK |
			   CTRL_CLK_SEL,
			   value);

	spin_unlock(&drv_data->lock);
	atcwdt_ping(wdt_dev);

	return 0;
}

static int atcwdt_start(struct watchdog_device *wdt_dev)
{
	struct atcwdt_drv *drv_data = watchdog_get_drvdata(wdt_dev);

	atcwdt_set_timeout(wdt_dev, wdt_dev->timeout);

	spin_lock(&drv_data->lock);
	regmap_write(drv_data->regmap, REG_WRITE_EN, WRITE_EN_MAGIC);
	regmap_update_bits(drv_data->regmap,
			   REG_CTRL,
			   CTRL_RST_EN | CTRL_WDT_EN,
			   CTRL_RST_EN | CTRL_WDT_EN);

	spin_unlock(&drv_data->lock);

	return 0;
}

static int atcwdt_stop(struct watchdog_device *wdt_dev)
{
	struct atcwdt_drv *drv_data = watchdog_get_drvdata(wdt_dev);

	spin_lock(&drv_data->lock);
	regmap_write(drv_data->regmap, REG_WRITE_EN, WRITE_EN_MAGIC);
	regmap_update_bits(drv_data->regmap,
			   REG_CTRL,
			   CTRL_RST_EN | CTRL_WDT_EN,
			   0);
	spin_unlock(&drv_data->lock);

	return 0;
}

static int atcwdt_restart(struct watchdog_device *wdt_dev,
			  unsigned long action, void *data)
{
	struct atcwdt_drv *drv_data = watchdog_get_drvdata(wdt_dev);

	atcwdt_set_timeout(wdt_dev, 0);

	spin_lock(&drv_data->lock);
	regmap_write(drv_data->regmap, REG_WRITE_EN, WRITE_EN_MAGIC);
	regmap_update_bits(drv_data->regmap,
			   REG_CTRL,
			   CTRL_RST_EN | CTRL_WDT_EN,
			   CTRL_RST_EN | CTRL_WDT_EN);
	spin_unlock(&drv_data->lock);

	return 0;
}

static const struct watchdog_ops atcwdt_ops = {
	.owner = THIS_MODULE,
	.start = atcwdt_start,
	.stop = atcwdt_stop,
	.ping = atcwdt_ping,
	.set_timeout = atcwdt_set_timeout,
	.restart = atcwdt_restart,
};

static int atcwdt_init_resource(struct platform_device *pdev,
				struct atcwdt_drv *drv_data)
{
	struct device *dev = &pdev->dev;
	void __iomem *base;
	const struct regmap_config cfg = {
		.name = "atcwdt",
		.reg_bits = 32,
		.val_bits = 32,
		.cache_type = REGCACHE_NONE,
		.reg_stride = 4,
		.max_register = REG_STATUS,
	};

	base = devm_platform_ioremap_resource(pdev, 0);
	if (IS_ERR(base))
		return dev_err_probe(dev, PTR_ERR(base),
				     "Failed to ioremap I/O resource\n");

	drv_data->regmap = devm_regmap_init_mmio(dev, base, &cfg);
	if (IS_ERR(drv_data->regmap))
		return dev_err_probe(dev, PTR_ERR(drv_data->regmap),
				     "Failed to create regmap\n");

	return 0;
}

static int atcwdt_enable_clk(struct atcwdt_drv *drv_data)
{
	struct device *dev = drv_data->wdt_dev.parent;
	unsigned int val;
	int clk_src;

	drv_data->clk = devm_clk_get_enabled(dev, NULL);
	if (IS_ERR(drv_data->clk))
		return dev_err_probe(dev, PTR_ERR(drv_data->clk),
				     "Failed to get watchdog clock\n");

	drv_data->clk_freq = clk_get_rate(drv_data->clk);
	if (!drv_data->clk_freq)
		return dev_err_probe(dev, -EINVAL,
				     "Failed to get clock rate\n");

	clk_src = device_property_read_u32(dev, "andestech,clock-source", &val);
	drv_data->clk_src = (!clk_src && val != 0) ? CTRL_CLK_SEL_PCLK : 0;

	return 0;
}

static int atcwdt_init_wdt_device(struct device *dev,
				  struct atcwdt_drv *drv_data)
{
	struct watchdog_device *wdd = &drv_data->wdt_dev;

	wdd->parent = dev;
	wdd->info = &atcwdt_info;
	wdd->ops = &atcwdt_ops;
	wdd->timeout = ATCWDT_TIMEOUT;
	wdd->min_timeout = 1;

	watchdog_set_nowayout(wdd, nowayout);
	watchdog_set_drvdata(wdd, drv_data);

	return 0;
}

static void atcwdt_calc_max_timeout(struct atcwdt_drv *drv_data)
{
	unsigned char rst_idx = atcwdt_get_index(0xFF, TMR_RST);
	unsigned char int_idx = atcwdt_get_index(0xFF,
						 drv_data->int_timer_type);

	drv_data->wdt_dev.max_timeout =
		((1U << rst_idx) + (1U << int_idx)) / drv_data->clk_freq;
}

static int atcwdt_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct atcwdt_drv *drv_data;
	int ret;

	drv_data = devm_kzalloc(dev, sizeof(*drv_data), GFP_KERNEL);
	if (!drv_data)
		return -ENOMEM;

	platform_set_drvdata(pdev, drv_data);
	spin_lock_init(&drv_data->lock);

	ret = atcwdt_init_wdt_device(dev, drv_data);
	if (ret)
		return ret;

	ret = atcwdt_init_resource(pdev, drv_data);
	if (ret)
		return ret;

	ret = atcwdt_enable_clk(drv_data);
	if (ret)
		return ret;

	ret = atcwdt_get_int_timer_type(drv_data);
	if (ret)
		return ret;

	atcwdt_calc_max_timeout(drv_data);

	ret = devm_watchdog_register_device(dev, &drv_data->wdt_dev);

	return ret;
}

static int atcwdt_suspend(struct device *dev)
{
	struct atcwdt_drv *drv_data = dev_get_drvdata(dev);

	if (watchdog_active(&drv_data->wdt_dev)) {
		atcwdt_stop(&drv_data->wdt_dev);
		clk_disable_unprepare(drv_data->clk);
	}

	return 0;
}

static int atcwdt_resume(struct device *dev)
{
	struct atcwdt_drv *drv_data = dev_get_drvdata(dev);
	int ret = 0;

	if (watchdog_active(&drv_data->wdt_dev)) {
		ret = clk_prepare_enable(drv_data->clk);
		if (ret)
			return ret;
		atcwdt_start(&drv_data->wdt_dev);
		atcwdt_ping(&drv_data->wdt_dev);
	}

	return ret;
}

static const struct of_device_id atcwdt_match[] = {
	{ .compatible = "andestech,ae350-wdt" },
	{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, atcwdt_match);

static DEFINE_SIMPLE_DEV_PM_OPS(atcwdt_pm_ops, atcwdt_suspend, atcwdt_resume);

static struct platform_driver atcwdt_driver = {
	.probe = atcwdt_probe,
	.driver = {
		.name = DRV_NAME,
		.of_match_table = atcwdt_match,
		.pm = pm_sleep_ptr(&atcwdt_pm_ops),
	},
};

module_platform_driver(atcwdt_driver);

module_param(timeout, uint, 0);
MODULE_PARM_DESC(timeout, "Watchdog timeout in seconds (default="
		 __MODULE_STRING(ATCWDT_TIMEOUT) ")");

module_param(nowayout, bool, 0);
MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default="
		 __MODULE_STRING(WATCHDOG_NOWAYOUT) ")");

MODULE_LICENSE("GPL");
MODULE_AUTHOR("CL Wang <cl634@andestech.com>");
MODULE_DESCRIPTION("Andes ATCWDT200 Watchdog timer driver");