1 | // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause |
2 | /* |
3 | * Copyright (c) 2016 BayLibre, SAS. |
4 | * Author: Neil Armstrong <narmstrong@baylibre.com> |
5 | * |
6 | */ |
7 | #include <linux/clk.h> |
8 | #include <linux/err.h> |
9 | #include <linux/io.h> |
10 | #include <linux/module.h> |
11 | #include <linux/of.h> |
12 | #include <linux/platform_device.h> |
13 | #include <linux/slab.h> |
14 | #include <linux/types.h> |
15 | #include <linux/watchdog.h> |
16 | |
17 | #define DEFAULT_TIMEOUT 30 /* seconds */ |
18 | |
19 | #define GXBB_WDT_CTRL_REG 0x0 |
20 | #define GXBB_WDT_TCNT_REG 0x8 |
21 | #define GXBB_WDT_RSET_REG 0xc |
22 | |
23 | #define GXBB_WDT_CTRL_CLKDIV_EN BIT(25) |
24 | #define GXBB_WDT_CTRL_CLK_EN BIT(24) |
25 | #define GXBB_WDT_CTRL_EN BIT(18) |
26 | #define GXBB_WDT_CTRL_DIV_MASK (BIT(18) - 1) |
27 | |
28 | #define GXBB_WDT_TCNT_SETUP_MASK (BIT(16) - 1) |
29 | #define GXBB_WDT_TCNT_CNT_SHIFT 16 |
30 | |
31 | static bool nowayout = WATCHDOG_NOWAYOUT; |
32 | module_param(nowayout, bool, 0); |
33 | MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started default=" |
34 | __MODULE_STRING(WATCHDOG_NOWAYOUT) ")" ); |
35 | |
36 | static unsigned int timeout; |
37 | module_param(timeout, uint, 0); |
38 | MODULE_PARM_DESC(timeout, "Watchdog heartbeat in seconds=" |
39 | __MODULE_STRING(DEFAULT_TIMEOUT) ")" ); |
40 | |
41 | struct meson_gxbb_wdt { |
42 | void __iomem *reg_base; |
43 | struct watchdog_device wdt_dev; |
44 | struct clk *clk; |
45 | }; |
46 | |
47 | struct wdt_params { |
48 | u32 rst; |
49 | }; |
50 | |
51 | static int meson_gxbb_wdt_start(struct watchdog_device *wdt_dev) |
52 | { |
53 | struct meson_gxbb_wdt *data = watchdog_get_drvdata(wdd: wdt_dev); |
54 | |
55 | writel(readl(addr: data->reg_base + GXBB_WDT_CTRL_REG) | GXBB_WDT_CTRL_EN, |
56 | addr: data->reg_base + GXBB_WDT_CTRL_REG); |
57 | |
58 | return 0; |
59 | } |
60 | |
61 | static int meson_gxbb_wdt_stop(struct watchdog_device *wdt_dev) |
62 | { |
63 | struct meson_gxbb_wdt *data = watchdog_get_drvdata(wdd: wdt_dev); |
64 | |
65 | writel(readl(addr: data->reg_base + GXBB_WDT_CTRL_REG) & ~GXBB_WDT_CTRL_EN, |
66 | addr: data->reg_base + GXBB_WDT_CTRL_REG); |
67 | |
68 | return 0; |
69 | } |
70 | |
71 | static int meson_gxbb_wdt_ping(struct watchdog_device *wdt_dev) |
72 | { |
73 | struct meson_gxbb_wdt *data = watchdog_get_drvdata(wdd: wdt_dev); |
74 | |
75 | writel(val: 0, addr: data->reg_base + GXBB_WDT_RSET_REG); |
76 | |
77 | return 0; |
78 | } |
79 | |
80 | static int meson_gxbb_wdt_set_timeout(struct watchdog_device *wdt_dev, |
81 | unsigned int timeout) |
82 | { |
83 | struct meson_gxbb_wdt *data = watchdog_get_drvdata(wdd: wdt_dev); |
84 | unsigned long tcnt = timeout * 1000; |
85 | |
86 | if (tcnt > GXBB_WDT_TCNT_SETUP_MASK) |
87 | tcnt = GXBB_WDT_TCNT_SETUP_MASK; |
88 | |
89 | wdt_dev->timeout = timeout; |
90 | |
91 | meson_gxbb_wdt_ping(wdt_dev); |
92 | |
93 | writel(val: tcnt, addr: data->reg_base + GXBB_WDT_TCNT_REG); |
94 | |
95 | return 0; |
96 | } |
97 | |
98 | static unsigned int meson_gxbb_wdt_get_timeleft(struct watchdog_device *wdt_dev) |
99 | { |
100 | struct meson_gxbb_wdt *data = watchdog_get_drvdata(wdd: wdt_dev); |
101 | unsigned long reg; |
102 | |
103 | reg = readl(addr: data->reg_base + GXBB_WDT_TCNT_REG); |
104 | |
105 | return ((reg & GXBB_WDT_TCNT_SETUP_MASK) - |
106 | (reg >> GXBB_WDT_TCNT_CNT_SHIFT)) / 1000; |
107 | } |
108 | |
109 | static const struct watchdog_ops meson_gxbb_wdt_ops = { |
110 | .start = meson_gxbb_wdt_start, |
111 | .stop = meson_gxbb_wdt_stop, |
112 | .ping = meson_gxbb_wdt_ping, |
113 | .set_timeout = meson_gxbb_wdt_set_timeout, |
114 | .get_timeleft = meson_gxbb_wdt_get_timeleft, |
115 | }; |
116 | |
117 | static const struct watchdog_info meson_gxbb_wdt_info = { |
118 | .identity = "Meson GXBB Watchdog" , |
119 | .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE, |
120 | }; |
121 | |
122 | static int __maybe_unused meson_gxbb_wdt_resume(struct device *dev) |
123 | { |
124 | struct meson_gxbb_wdt *data = dev_get_drvdata(dev); |
125 | |
126 | if (watchdog_active(wdd: &data->wdt_dev)) |
127 | meson_gxbb_wdt_start(wdt_dev: &data->wdt_dev); |
128 | |
129 | return 0; |
130 | } |
131 | |
132 | static int __maybe_unused meson_gxbb_wdt_suspend(struct device *dev) |
133 | { |
134 | struct meson_gxbb_wdt *data = dev_get_drvdata(dev); |
135 | |
136 | if (watchdog_active(wdd: &data->wdt_dev)) |
137 | meson_gxbb_wdt_stop(wdt_dev: &data->wdt_dev); |
138 | |
139 | return 0; |
140 | } |
141 | |
142 | static const struct dev_pm_ops meson_gxbb_wdt_pm_ops = { |
143 | SET_SYSTEM_SLEEP_PM_OPS(meson_gxbb_wdt_suspend, meson_gxbb_wdt_resume) |
144 | }; |
145 | |
146 | static const struct wdt_params gxbb_params = { |
147 | .rst = BIT(21), |
148 | }; |
149 | |
150 | static const struct wdt_params t7_params = { |
151 | .rst = BIT(22), |
152 | }; |
153 | |
154 | static const struct of_device_id meson_gxbb_wdt_dt_ids[] = { |
155 | { .compatible = "amlogic,meson-gxbb-wdt" , .data = &gxbb_params, }, |
156 | { .compatible = "amlogic,t7-wdt" , .data = &t7_params, }, |
157 | { /* sentinel */ }, |
158 | }; |
159 | MODULE_DEVICE_TABLE(of, meson_gxbb_wdt_dt_ids); |
160 | |
161 | static int meson_gxbb_wdt_probe(struct platform_device *pdev) |
162 | { |
163 | struct device *dev = &pdev->dev; |
164 | struct meson_gxbb_wdt *data; |
165 | struct wdt_params *params; |
166 | u32 ctrl_reg; |
167 | |
168 | data = devm_kzalloc(dev, size: sizeof(*data), GFP_KERNEL); |
169 | if (!data) |
170 | return -ENOMEM; |
171 | |
172 | data->reg_base = devm_platform_ioremap_resource(pdev, index: 0); |
173 | if (IS_ERR(ptr: data->reg_base)) |
174 | return PTR_ERR(ptr: data->reg_base); |
175 | |
176 | data->clk = devm_clk_get_enabled(dev, NULL); |
177 | if (IS_ERR(ptr: data->clk)) |
178 | return PTR_ERR(ptr: data->clk); |
179 | |
180 | params = (struct wdt_params *)of_device_get_match_data(dev); |
181 | |
182 | platform_set_drvdata(pdev, data); |
183 | |
184 | data->wdt_dev.parent = dev; |
185 | data->wdt_dev.info = &meson_gxbb_wdt_info; |
186 | data->wdt_dev.ops = &meson_gxbb_wdt_ops; |
187 | data->wdt_dev.max_hw_heartbeat_ms = GXBB_WDT_TCNT_SETUP_MASK; |
188 | data->wdt_dev.min_timeout = 1; |
189 | data->wdt_dev.timeout = DEFAULT_TIMEOUT; |
190 | watchdog_init_timeout(wdd: &data->wdt_dev, timeout_parm: timeout, dev); |
191 | watchdog_set_nowayout(wdd: &data->wdt_dev, nowayout); |
192 | watchdog_set_drvdata(wdd: &data->wdt_dev, data); |
193 | |
194 | ctrl_reg = readl(addr: data->reg_base + GXBB_WDT_CTRL_REG) & |
195 | GXBB_WDT_CTRL_EN; |
196 | |
197 | if (ctrl_reg) { |
198 | /* Watchdog is running - keep it running but extend timeout |
199 | * to the maximum while setting the timebase |
200 | */ |
201 | set_bit(WDOG_HW_RUNNING, addr: &data->wdt_dev.status); |
202 | meson_gxbb_wdt_set_timeout(wdt_dev: &data->wdt_dev, |
203 | GXBB_WDT_TCNT_SETUP_MASK / 1000); |
204 | } |
205 | |
206 | /* Setup with 1ms timebase */ |
207 | ctrl_reg |= ((clk_get_rate(clk: data->clk) / 1000) & |
208 | GXBB_WDT_CTRL_DIV_MASK) | |
209 | params->rst | |
210 | GXBB_WDT_CTRL_CLK_EN | |
211 | GXBB_WDT_CTRL_CLKDIV_EN; |
212 | |
213 | writel(val: ctrl_reg, addr: data->reg_base + GXBB_WDT_CTRL_REG); |
214 | meson_gxbb_wdt_set_timeout(wdt_dev: &data->wdt_dev, timeout: data->wdt_dev.timeout); |
215 | |
216 | return devm_watchdog_register_device(dev, &data->wdt_dev); |
217 | } |
218 | |
219 | static struct platform_driver meson_gxbb_wdt_driver = { |
220 | .probe = meson_gxbb_wdt_probe, |
221 | .driver = { |
222 | .name = "meson-gxbb-wdt" , |
223 | .pm = &meson_gxbb_wdt_pm_ops, |
224 | .of_match_table = meson_gxbb_wdt_dt_ids, |
225 | }, |
226 | }; |
227 | |
228 | module_platform_driver(meson_gxbb_wdt_driver); |
229 | |
230 | MODULE_AUTHOR("Neil Armstrong <narmstrong@baylibre.com>" ); |
231 | MODULE_DESCRIPTION("Amlogic Meson GXBB Watchdog timer driver" ); |
232 | MODULE_LICENSE("Dual BSD/GPL" ); |
233 | |