Contributors: 2
Author Tokens Token Proportion Commits Commit Proportion
CL Wang 2125 99.77% 1 50.00%
Surendra Singh Chouhan 5 0.23% 1 50.00%
Total 2130 2


// 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 -EBUSY.
 *
 * Returns: 0 on success or negative error code on failure.
 */
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 dev_err_probe(dev, -EBUSY,
				     "Watchdog is enabled, cannot detect timer type\n");
	}

	/*
	 * 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");