Commit | Line | Data |
---|---|---|
fa0f8d51 VA |
1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* | |
3 | * Watchdog driver for Intel Keem Bay non-secure watchdog. | |
4 | * | |
5 | * Copyright (C) 2020 Intel Corporation | |
6 | */ | |
7 | ||
8 | #include <linux/arm-smccc.h> | |
9 | #include <linux/bits.h> | |
10 | #include <linux/clk.h> | |
11 | #include <linux/interrupt.h> | |
12 | #include <linux/io.h> | |
13 | #include <linux/limits.h> | |
14 | #include <linux/module.h> | |
15 | #include <linux/mod_devicetable.h> | |
16 | #include <linux/platform_device.h> | |
17 | #include <linux/reboot.h> | |
18 | #include <linux/watchdog.h> | |
19 | ||
20 | /* Non-secure watchdog register offsets */ | |
21 | #define TIM_WATCHDOG 0x0 | |
22 | #define TIM_WATCHDOG_INT_THRES 0x4 | |
23 | #define TIM_WDOG_EN 0x8 | |
24 | #define TIM_SAFE 0xc | |
25 | ||
0e36a09f SS |
26 | #define WDT_TH_INT_MASK BIT(8) |
27 | #define WDT_TO_INT_MASK BIT(9) | |
613c4db2 | 28 | #define WDT_INT_CLEAR_SMC 0x8200ff18 |
d1fb8bbd | 29 | |
fa0f8d51 | 30 | #define WDT_UNLOCK 0xf1d0dead |
624873f1 SS |
31 | #define WDT_DISABLE 0x0 |
32 | #define WDT_ENABLE 0x1 | |
d1fb8bbd | 33 | |
fa0f8d51 VA |
34 | #define WDT_LOAD_MAX U32_MAX |
35 | #define WDT_LOAD_MIN 1 | |
d1fb8bbd | 36 | |
fa0f8d51 | 37 | #define WDT_TIMEOUT 5 |
29353816 | 38 | #define WDT_PRETIMEOUT 4 |
fa0f8d51 VA |
39 | |
40 | static unsigned int timeout = WDT_TIMEOUT; | |
41 | module_param(timeout, int, 0); | |
42 | MODULE_PARM_DESC(timeout, "Watchdog timeout period in seconds (default = " | |
43 | __MODULE_STRING(WDT_TIMEOUT) ")"); | |
44 | ||
45 | static bool nowayout = WATCHDOG_NOWAYOUT; | |
46 | module_param(nowayout, bool, 0); | |
47 | MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default = " | |
48 | __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); | |
49 | ||
50 | struct keembay_wdt { | |
51 | struct watchdog_device wdd; | |
52 | struct clk *clk; | |
53 | unsigned int rate; | |
54 | int to_irq; | |
55 | int th_irq; | |
56 | void __iomem *base; | |
57 | }; | |
58 | ||
59 | static inline u32 keembay_wdt_readl(struct keembay_wdt *wdt, u32 offset) | |
60 | { | |
61 | return readl(wdt->base + offset); | |
62 | } | |
63 | ||
64 | static inline void keembay_wdt_writel(struct keembay_wdt *wdt, u32 offset, u32 val) | |
65 | { | |
66 | writel(WDT_UNLOCK, wdt->base + TIM_SAFE); | |
67 | writel(val, wdt->base + offset); | |
68 | } | |
69 | ||
70 | static void keembay_wdt_set_timeout_reg(struct watchdog_device *wdog) | |
71 | { | |
72 | struct keembay_wdt *wdt = watchdog_get_drvdata(wdog); | |
73 | ||
74 | keembay_wdt_writel(wdt, TIM_WATCHDOG, wdog->timeout * wdt->rate); | |
75 | } | |
76 | ||
77 | static void keembay_wdt_set_pretimeout_reg(struct watchdog_device *wdog) | |
78 | { | |
79 | struct keembay_wdt *wdt = watchdog_get_drvdata(wdog); | |
80 | u32 th_val = 0; | |
81 | ||
82 | if (wdog->pretimeout) | |
83 | th_val = wdog->timeout - wdog->pretimeout; | |
84 | ||
85 | keembay_wdt_writel(wdt, TIM_WATCHDOG_INT_THRES, th_val * wdt->rate); | |
86 | } | |
87 | ||
88 | static int keembay_wdt_start(struct watchdog_device *wdog) | |
89 | { | |
90 | struct keembay_wdt *wdt = watchdog_get_drvdata(wdog); | |
91 | ||
624873f1 | 92 | keembay_wdt_writel(wdt, TIM_WDOG_EN, WDT_ENABLE); |
fa0f8d51 VA |
93 | |
94 | return 0; | |
95 | } | |
96 | ||
97 | static int keembay_wdt_stop(struct watchdog_device *wdog) | |
98 | { | |
99 | struct keembay_wdt *wdt = watchdog_get_drvdata(wdog); | |
100 | ||
624873f1 | 101 | keembay_wdt_writel(wdt, TIM_WDOG_EN, WDT_DISABLE); |
fa0f8d51 VA |
102 | |
103 | return 0; | |
104 | } | |
105 | ||
106 | static int keembay_wdt_ping(struct watchdog_device *wdog) | |
107 | { | |
108 | keembay_wdt_set_timeout_reg(wdog); | |
109 | ||
110 | return 0; | |
111 | } | |
112 | ||
113 | static int keembay_wdt_set_timeout(struct watchdog_device *wdog, u32 t) | |
114 | { | |
115 | wdog->timeout = t; | |
116 | keembay_wdt_set_timeout_reg(wdog); | |
0f7bfaf1 | 117 | keembay_wdt_set_pretimeout_reg(wdog); |
fa0f8d51 VA |
118 | |
119 | return 0; | |
120 | } | |
121 | ||
122 | static int keembay_wdt_set_pretimeout(struct watchdog_device *wdog, u32 t) | |
123 | { | |
124 | if (t > wdog->timeout) | |
125 | return -EINVAL; | |
126 | ||
127 | wdog->pretimeout = t; | |
128 | keembay_wdt_set_pretimeout_reg(wdog); | |
129 | ||
130 | return 0; | |
131 | } | |
132 | ||
133 | static unsigned int keembay_wdt_get_timeleft(struct watchdog_device *wdog) | |
134 | { | |
135 | struct keembay_wdt *wdt = watchdog_get_drvdata(wdog); | |
136 | ||
137 | return keembay_wdt_readl(wdt, TIM_WATCHDOG) / wdt->rate; | |
138 | } | |
139 | ||
140 | /* | |
141 | * SMC call is used to clear the interrupt bits, because the TIM_GEN_CONFIG | |
142 | * register is in the secure bank. | |
143 | */ | |
144 | static irqreturn_t keembay_wdt_to_isr(int irq, void *dev_id) | |
145 | { | |
146 | struct keembay_wdt *wdt = dev_id; | |
147 | struct arm_smccc_res res; | |
148 | ||
613c4db2 | 149 | arm_smccc_smc(WDT_INT_CLEAR_SMC, WDT_TO_INT_MASK, 0, 0, 0, 0, 0, 0, &res); |
d1fb8bbd | 150 | dev_crit(wdt->wdd.parent, "Intel Keem Bay non-secure wdt timeout.\n"); |
fa0f8d51 VA |
151 | emergency_restart(); |
152 | ||
153 | return IRQ_HANDLED; | |
154 | } | |
155 | ||
156 | static irqreturn_t keembay_wdt_th_isr(int irq, void *dev_id) | |
157 | { | |
158 | struct keembay_wdt *wdt = dev_id; | |
159 | struct arm_smccc_res res; | |
160 | ||
75f6c56d SS |
161 | keembay_wdt_set_pretimeout(&wdt->wdd, 0x0); |
162 | ||
613c4db2 | 163 | arm_smccc_smc(WDT_INT_CLEAR_SMC, WDT_TH_INT_MASK, 0, 0, 0, 0, 0, 0, &res); |
d1fb8bbd | 164 | dev_crit(wdt->wdd.parent, "Intel Keem Bay non-secure wdt pre-timeout.\n"); |
fa0f8d51 VA |
165 | watchdog_notify_pretimeout(&wdt->wdd); |
166 | ||
167 | return IRQ_HANDLED; | |
168 | } | |
169 | ||
170 | static const struct watchdog_info keembay_wdt_info = { | |
171 | .identity = "Intel Keem Bay Watchdog Timer", | |
172 | .options = WDIOF_SETTIMEOUT | | |
173 | WDIOF_PRETIMEOUT | | |
174 | WDIOF_MAGICCLOSE | | |
175 | WDIOF_KEEPALIVEPING, | |
176 | }; | |
177 | ||
178 | static const struct watchdog_ops keembay_wdt_ops = { | |
179 | .owner = THIS_MODULE, | |
180 | .start = keembay_wdt_start, | |
181 | .stop = keembay_wdt_stop, | |
182 | .ping = keembay_wdt_ping, | |
183 | .set_timeout = keembay_wdt_set_timeout, | |
184 | .set_pretimeout = keembay_wdt_set_pretimeout, | |
185 | .get_timeleft = keembay_wdt_get_timeleft, | |
186 | }; | |
187 | ||
188 | static int keembay_wdt_probe(struct platform_device *pdev) | |
189 | { | |
190 | struct device *dev = &pdev->dev; | |
191 | struct keembay_wdt *wdt; | |
192 | int ret; | |
193 | ||
194 | wdt = devm_kzalloc(dev, sizeof(*wdt), GFP_KERNEL); | |
195 | if (!wdt) | |
196 | return -ENOMEM; | |
197 | ||
198 | wdt->base = devm_platform_ioremap_resource(pdev, 0); | |
199 | if (IS_ERR(wdt->base)) | |
200 | return PTR_ERR(wdt->base); | |
201 | ||
202 | /* we do not need to enable the clock as it is enabled by default */ | |
203 | wdt->clk = devm_clk_get(dev, NULL); | |
204 | if (IS_ERR(wdt->clk)) | |
205 | return dev_err_probe(dev, PTR_ERR(wdt->clk), "Failed to get clock\n"); | |
206 | ||
207 | wdt->rate = clk_get_rate(wdt->clk); | |
208 | if (!wdt->rate) | |
209 | return dev_err_probe(dev, -EINVAL, "Failed to get clock rate\n"); | |
210 | ||
211 | wdt->th_irq = platform_get_irq_byname(pdev, "threshold"); | |
212 | if (wdt->th_irq < 0) | |
213 | return dev_err_probe(dev, wdt->th_irq, "Failed to get IRQ for threshold\n"); | |
214 | ||
215 | ret = devm_request_irq(dev, wdt->th_irq, keembay_wdt_th_isr, 0, | |
216 | "keembay-wdt", wdt); | |
217 | if (ret) | |
218 | return dev_err_probe(dev, ret, "Failed to request IRQ for threshold\n"); | |
219 | ||
220 | wdt->to_irq = platform_get_irq_byname(pdev, "timeout"); | |
221 | if (wdt->to_irq < 0) | |
222 | return dev_err_probe(dev, wdt->to_irq, "Failed to get IRQ for timeout\n"); | |
223 | ||
224 | ret = devm_request_irq(dev, wdt->to_irq, keembay_wdt_to_isr, 0, | |
225 | "keembay-wdt", wdt); | |
226 | if (ret) | |
227 | return dev_err_probe(dev, ret, "Failed to request IRQ for timeout\n"); | |
228 | ||
229 | wdt->wdd.parent = dev; | |
230 | wdt->wdd.info = &keembay_wdt_info; | |
231 | wdt->wdd.ops = &keembay_wdt_ops; | |
232 | wdt->wdd.min_timeout = WDT_LOAD_MIN; | |
233 | wdt->wdd.max_timeout = WDT_LOAD_MAX / wdt->rate; | |
234 | wdt->wdd.timeout = WDT_TIMEOUT; | |
29353816 | 235 | wdt->wdd.pretimeout = WDT_PRETIMEOUT; |
fa0f8d51 VA |
236 | |
237 | watchdog_set_drvdata(&wdt->wdd, wdt); | |
238 | watchdog_set_nowayout(&wdt->wdd, nowayout); | |
239 | watchdog_init_timeout(&wdt->wdd, timeout, dev); | |
240 | keembay_wdt_set_timeout(&wdt->wdd, wdt->wdd.timeout); | |
29353816 | 241 | keembay_wdt_set_pretimeout(&wdt->wdd, wdt->wdd.pretimeout); |
fa0f8d51 VA |
242 | |
243 | ret = devm_watchdog_register_device(dev, &wdt->wdd); | |
244 | if (ret) | |
245 | return dev_err_probe(dev, ret, "Failed to register watchdog device.\n"); | |
246 | ||
247 | platform_set_drvdata(pdev, wdt); | |
248 | dev_info(dev, "Initial timeout %d sec%s.\n", | |
249 | wdt->wdd.timeout, nowayout ? ", nowayout" : ""); | |
250 | ||
251 | return 0; | |
252 | } | |
253 | ||
254 | static int __maybe_unused keembay_wdt_suspend(struct device *dev) | |
255 | { | |
256 | struct keembay_wdt *wdt = dev_get_drvdata(dev); | |
257 | ||
258 | if (watchdog_active(&wdt->wdd)) | |
259 | return keembay_wdt_stop(&wdt->wdd); | |
260 | ||
261 | return 0; | |
262 | } | |
263 | ||
264 | static int __maybe_unused keembay_wdt_resume(struct device *dev) | |
265 | { | |
266 | struct keembay_wdt *wdt = dev_get_drvdata(dev); | |
267 | ||
268 | if (watchdog_active(&wdt->wdd)) | |
269 | return keembay_wdt_start(&wdt->wdd); | |
270 | ||
271 | return 0; | |
272 | } | |
273 | ||
274 | static SIMPLE_DEV_PM_OPS(keembay_wdt_pm_ops, keembay_wdt_suspend, | |
275 | keembay_wdt_resume); | |
276 | ||
277 | static const struct of_device_id keembay_wdt_match[] = { | |
278 | { .compatible = "intel,keembay-wdt" }, | |
279 | { } | |
280 | }; | |
281 | MODULE_DEVICE_TABLE(of, keembay_wdt_match); | |
282 | ||
283 | static struct platform_driver keembay_wdt_driver = { | |
d1fb8bbd SS |
284 | .probe = keembay_wdt_probe, |
285 | .driver = { | |
fa0f8d51 VA |
286 | .name = "keembay_wdt", |
287 | .of_match_table = keembay_wdt_match, | |
288 | .pm = &keembay_wdt_pm_ops, | |
289 | }, | |
290 | }; | |
291 | ||
292 | module_platform_driver(keembay_wdt_driver); | |
293 | ||
294 | MODULE_DESCRIPTION("Intel Keem Bay SoC watchdog driver"); | |
295 | MODULE_AUTHOR("Wan Ahmad Zainie <wan.ahmad.zainie.wan.mohamad@intel.com"); | |
296 | MODULE_LICENSE("GPL v2"); |