1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* Copyright (C) 2022 Hewlett-Packard Enterprise Development Company, L.P. */ |
3 | |
4 | #include <linux/delay.h> |
5 | #include <linux/io.h> |
6 | #include <linux/module.h> |
7 | #include <linux/platform_device.h> |
8 | #include <linux/types.h> |
9 | #include <linux/watchdog.h> |
10 | |
11 | #define MASK_WDGCS_ENABLE 0x01 |
12 | #define MASK_WDGCS_RELOAD 0x04 |
13 | #define MASK_WDGCS_NMIEN 0x08 |
14 | #define MASK_WDGCS_WARN 0x80 |
15 | |
16 | #define WDT_MAX_TIMEOUT_MS 655350 |
17 | #define WDT_DEFAULT_TIMEOUT 30 |
18 | #define SECS_TO_WDOG_TICKS(x) ((x) * 100) |
19 | #define WDOG_TICKS_TO_SECS(x) ((x) / 100) |
20 | |
21 | #define GXP_WDT_CNT_OFS 0x10 |
22 | #define GXP_WDT_CTRL_OFS 0x16 |
23 | |
24 | struct gxp_wdt { |
25 | void __iomem *base; |
26 | struct watchdog_device wdd; |
27 | }; |
28 | |
29 | static void gxp_wdt_enable_reload(struct gxp_wdt *drvdata) |
30 | { |
31 | u8 val; |
32 | |
33 | val = readb(addr: drvdata->base + GXP_WDT_CTRL_OFS); |
34 | val |= (MASK_WDGCS_ENABLE | MASK_WDGCS_RELOAD); |
35 | writeb(val, addr: drvdata->base + GXP_WDT_CTRL_OFS); |
36 | } |
37 | |
38 | static int gxp_wdt_start(struct watchdog_device *wdd) |
39 | { |
40 | struct gxp_wdt *drvdata = watchdog_get_drvdata(wdd); |
41 | |
42 | writew(SECS_TO_WDOG_TICKS(wdd->timeout), addr: drvdata->base + GXP_WDT_CNT_OFS); |
43 | gxp_wdt_enable_reload(drvdata); |
44 | return 0; |
45 | } |
46 | |
47 | static int gxp_wdt_stop(struct watchdog_device *wdd) |
48 | { |
49 | struct gxp_wdt *drvdata = watchdog_get_drvdata(wdd); |
50 | u8 val; |
51 | |
52 | val = readb_relaxed(drvdata->base + GXP_WDT_CTRL_OFS); |
53 | val &= ~MASK_WDGCS_ENABLE; |
54 | writeb(val, addr: drvdata->base + GXP_WDT_CTRL_OFS); |
55 | return 0; |
56 | } |
57 | |
58 | static int gxp_wdt_set_timeout(struct watchdog_device *wdd, |
59 | unsigned int timeout) |
60 | { |
61 | struct gxp_wdt *drvdata = watchdog_get_drvdata(wdd); |
62 | u32 actual; |
63 | |
64 | wdd->timeout = timeout; |
65 | actual = min(timeout * 100, wdd->max_hw_heartbeat_ms / 10); |
66 | writew(val: actual, addr: drvdata->base + GXP_WDT_CNT_OFS); |
67 | |
68 | return 0; |
69 | } |
70 | |
71 | static unsigned int gxp_wdt_get_timeleft(struct watchdog_device *wdd) |
72 | { |
73 | struct gxp_wdt *drvdata = watchdog_get_drvdata(wdd); |
74 | u32 val = readw(addr: drvdata->base + GXP_WDT_CNT_OFS); |
75 | |
76 | return WDOG_TICKS_TO_SECS(val); |
77 | } |
78 | |
79 | static int gxp_wdt_ping(struct watchdog_device *wdd) |
80 | { |
81 | struct gxp_wdt *drvdata = watchdog_get_drvdata(wdd); |
82 | |
83 | gxp_wdt_enable_reload(drvdata); |
84 | return 0; |
85 | } |
86 | |
87 | static int gxp_restart(struct watchdog_device *wdd, unsigned long action, |
88 | void *data) |
89 | { |
90 | struct gxp_wdt *drvdata = watchdog_get_drvdata(wdd); |
91 | |
92 | writew(val: 1, addr: drvdata->base + GXP_WDT_CNT_OFS); |
93 | gxp_wdt_enable_reload(drvdata); |
94 | mdelay(100); |
95 | return 0; |
96 | } |
97 | |
98 | static const struct watchdog_ops gxp_wdt_ops = { |
99 | .owner = THIS_MODULE, |
100 | .start = gxp_wdt_start, |
101 | .stop = gxp_wdt_stop, |
102 | .ping = gxp_wdt_ping, |
103 | .set_timeout = gxp_wdt_set_timeout, |
104 | .get_timeleft = gxp_wdt_get_timeleft, |
105 | .restart = gxp_restart, |
106 | }; |
107 | |
108 | static const struct watchdog_info gxp_wdt_info = { |
109 | .options = WDIOF_SETTIMEOUT | WDIOF_MAGICCLOSE | WDIOF_KEEPALIVEPING, |
110 | .identity = "HPE GXP Watchdog timer" , |
111 | }; |
112 | |
113 | static int gxp_wdt_probe(struct platform_device *pdev) |
114 | { |
115 | struct device *dev = &pdev->dev; |
116 | struct gxp_wdt *drvdata; |
117 | int err; |
118 | u8 val; |
119 | |
120 | drvdata = devm_kzalloc(dev, size: sizeof(struct gxp_wdt), GFP_KERNEL); |
121 | if (!drvdata) |
122 | return -ENOMEM; |
123 | |
124 | /* |
125 | * The register area where the timer and watchdog reside is disarranged. |
126 | * Hence mapping individual register blocks for the timer and watchdog |
127 | * is not recommended as they would have access to each others |
128 | * registers. Based on feedback the watchdog is no longer part of the |
129 | * device tree file and the timer driver now creates the watchdog as a |
130 | * child device. During the watchdogs creation, the timer driver passes |
131 | * the base address to the watchdog over the private interface. |
132 | */ |
133 | |
134 | drvdata->base = (void __iomem *)dev->platform_data; |
135 | |
136 | drvdata->wdd.info = &gxp_wdt_info; |
137 | drvdata->wdd.ops = &gxp_wdt_ops; |
138 | drvdata->wdd.max_hw_heartbeat_ms = WDT_MAX_TIMEOUT_MS; |
139 | drvdata->wdd.parent = dev; |
140 | drvdata->wdd.timeout = WDT_DEFAULT_TIMEOUT; |
141 | |
142 | watchdog_set_drvdata(wdd: &drvdata->wdd, data: drvdata); |
143 | watchdog_set_nowayout(wdd: &drvdata->wdd, WATCHDOG_NOWAYOUT); |
144 | |
145 | val = readb(addr: drvdata->base + GXP_WDT_CTRL_OFS); |
146 | |
147 | if (val & MASK_WDGCS_ENABLE) |
148 | set_bit(WDOG_HW_RUNNING, addr: &drvdata->wdd.status); |
149 | |
150 | watchdog_set_restart_priority(wdd: &drvdata->wdd, priority: 128); |
151 | |
152 | watchdog_stop_on_reboot(wdd: &drvdata->wdd); |
153 | err = devm_watchdog_register_device(dev, &drvdata->wdd); |
154 | if (err) { |
155 | dev_err(dev, "Failed to register watchdog device" ); |
156 | return err; |
157 | } |
158 | |
159 | dev_info(dev, "HPE GXP watchdog timer" ); |
160 | |
161 | return 0; |
162 | } |
163 | |
164 | static struct platform_driver gxp_wdt_driver = { |
165 | .probe = gxp_wdt_probe, |
166 | .driver = { |
167 | .name = "gxp-wdt" , |
168 | }, |
169 | }; |
170 | module_platform_driver(gxp_wdt_driver); |
171 | |
172 | MODULE_AUTHOR("Nick Hawkins <nick.hawkins@hpe.com>" ); |
173 | MODULE_AUTHOR("Jean-Marie Verdun <verdun@hpe.com>" ); |
174 | MODULE_DESCRIPTION("Driver for GXP watchdog timer" ); |
175 | MODULE_LICENSE("GPL" ); |
176 | |