1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Xilinx Zynq MPSoC Power Management |
4 | * |
5 | * Copyright (C) 2014-2019 Xilinx, Inc. |
6 | * |
7 | * Davorin Mista <davorin.mista@aggios.com> |
8 | * Jolly Shah <jollys@xilinx.com> |
9 | * Rajan Vaja <rajan.vaja@xilinx.com> |
10 | */ |
11 | |
12 | #include <linux/mailbox_client.h> |
13 | #include <linux/module.h> |
14 | #include <linux/of.h> |
15 | #include <linux/platform_device.h> |
16 | #include <linux/reboot.h> |
17 | #include <linux/suspend.h> |
18 | |
19 | #include <linux/firmware/xlnx-zynqmp.h> |
20 | #include <linux/firmware/xlnx-event-manager.h> |
21 | #include <linux/mailbox/zynqmp-ipi-message.h> |
22 | |
23 | /** |
24 | * struct zynqmp_pm_work_struct - Wrapper for struct work_struct |
25 | * @callback_work: Work structure |
26 | * @args: Callback arguments |
27 | */ |
28 | struct zynqmp_pm_work_struct { |
29 | struct work_struct callback_work; |
30 | u32 args[CB_ARG_CNT]; |
31 | }; |
32 | |
33 | static struct zynqmp_pm_work_struct *zynqmp_pm_init_suspend_work; |
34 | static struct mbox_chan *rx_chan; |
35 | static bool event_registered; |
36 | |
37 | enum pm_suspend_mode { |
38 | PM_SUSPEND_MODE_FIRST = 0, |
39 | PM_SUSPEND_MODE_STD = PM_SUSPEND_MODE_FIRST, |
40 | PM_SUSPEND_MODE_POWER_OFF, |
41 | }; |
42 | |
43 | #define PM_SUSPEND_MODE_FIRST PM_SUSPEND_MODE_STD |
44 | |
45 | static const char *const suspend_modes[] = { |
46 | [PM_SUSPEND_MODE_STD] = "standard" , |
47 | [PM_SUSPEND_MODE_POWER_OFF] = "power-off" , |
48 | }; |
49 | |
50 | static enum pm_suspend_mode suspend_mode = PM_SUSPEND_MODE_STD; |
51 | |
52 | static void zynqmp_pm_get_callback_data(u32 *buf) |
53 | { |
54 | zynqmp_pm_invoke_fn(GET_CALLBACK_DATA, ret_payload: buf, num_args: 0); |
55 | } |
56 | |
57 | static void suspend_event_callback(const u32 *payload, void *data) |
58 | { |
59 | /* First element is callback API ID, others are callback arguments */ |
60 | if (work_pending(&zynqmp_pm_init_suspend_work->callback_work)) |
61 | return; |
62 | |
63 | /* Copy callback arguments into work's structure */ |
64 | memcpy(zynqmp_pm_init_suspend_work->args, &payload[1], |
65 | sizeof(zynqmp_pm_init_suspend_work->args)); |
66 | |
67 | queue_work(wq: system_unbound_wq, work: &zynqmp_pm_init_suspend_work->callback_work); |
68 | } |
69 | |
70 | static irqreturn_t zynqmp_pm_isr(int irq, void *data) |
71 | { |
72 | u32 payload[CB_PAYLOAD_SIZE]; |
73 | |
74 | zynqmp_pm_get_callback_data(buf: payload); |
75 | |
76 | /* First element is callback API ID, others are callback arguments */ |
77 | if (payload[0] == PM_INIT_SUSPEND_CB) { |
78 | switch (payload[1]) { |
79 | case SUSPEND_SYSTEM_SHUTDOWN: |
80 | orderly_poweroff(force: true); |
81 | break; |
82 | case SUSPEND_POWER_REQUEST: |
83 | pm_suspend(PM_SUSPEND_MEM); |
84 | break; |
85 | default: |
86 | pr_err("%s Unsupported InitSuspendCb reason code %d\n" , |
87 | __func__, payload[1]); |
88 | } |
89 | } else { |
90 | pr_err("%s() Unsupported Callback %d\n" , __func__, payload[0]); |
91 | } |
92 | |
93 | return IRQ_HANDLED; |
94 | } |
95 | |
96 | static void ipi_receive_callback(struct mbox_client *cl, void *data) |
97 | { |
98 | struct zynqmp_ipi_message *msg = (struct zynqmp_ipi_message *)data; |
99 | u32 payload[CB_PAYLOAD_SIZE]; |
100 | int ret; |
101 | |
102 | memcpy(payload, msg->data, sizeof(msg->len)); |
103 | /* First element is callback API ID, others are callback arguments */ |
104 | if (payload[0] == PM_INIT_SUSPEND_CB) { |
105 | if (work_pending(&zynqmp_pm_init_suspend_work->callback_work)) |
106 | return; |
107 | |
108 | /* Copy callback arguments into work's structure */ |
109 | memcpy(zynqmp_pm_init_suspend_work->args, &payload[1], |
110 | sizeof(zynqmp_pm_init_suspend_work->args)); |
111 | |
112 | queue_work(wq: system_unbound_wq, |
113 | work: &zynqmp_pm_init_suspend_work->callback_work); |
114 | |
115 | /* Send NULL message to mbox controller to ack the message */ |
116 | ret = mbox_send_message(chan: rx_chan, NULL); |
117 | if (ret) |
118 | pr_err("IPI ack failed. Error %d\n" , ret); |
119 | } |
120 | } |
121 | |
122 | /** |
123 | * zynqmp_pm_init_suspend_work_fn - Initialize suspend |
124 | * @work: Pointer to work_struct |
125 | * |
126 | * Bottom-half of PM callback IRQ handler. |
127 | */ |
128 | static void zynqmp_pm_init_suspend_work_fn(struct work_struct *work) |
129 | { |
130 | struct zynqmp_pm_work_struct *pm_work = |
131 | container_of(work, struct zynqmp_pm_work_struct, callback_work); |
132 | |
133 | if (pm_work->args[0] == SUSPEND_SYSTEM_SHUTDOWN) { |
134 | orderly_poweroff(force: true); |
135 | } else if (pm_work->args[0] == SUSPEND_POWER_REQUEST) { |
136 | pm_suspend(PM_SUSPEND_MEM); |
137 | } else { |
138 | pr_err("%s Unsupported InitSuspendCb reason code %d.\n" , |
139 | __func__, pm_work->args[0]); |
140 | } |
141 | } |
142 | |
143 | static ssize_t suspend_mode_show(struct device *dev, |
144 | struct device_attribute *attr, char *buf) |
145 | { |
146 | char *s = buf; |
147 | int md; |
148 | |
149 | for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) |
150 | if (suspend_modes[md]) { |
151 | if (md == suspend_mode) |
152 | s += sprintf(buf: s, fmt: "[%s] " , suspend_modes[md]); |
153 | else |
154 | s += sprintf(buf: s, fmt: "%s " , suspend_modes[md]); |
155 | } |
156 | |
157 | /* Convert last space to newline */ |
158 | if (s != buf) |
159 | *(s - 1) = '\n'; |
160 | return (s - buf); |
161 | } |
162 | |
163 | static ssize_t suspend_mode_store(struct device *dev, |
164 | struct device_attribute *attr, |
165 | const char *buf, size_t count) |
166 | { |
167 | int md, ret = -EINVAL; |
168 | |
169 | for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) |
170 | if (suspend_modes[md] && |
171 | sysfs_streq(s1: suspend_modes[md], s2: buf)) { |
172 | ret = 0; |
173 | break; |
174 | } |
175 | |
176 | if (!ret && md != suspend_mode) { |
177 | ret = zynqmp_pm_set_suspend_mode(mode: md); |
178 | if (likely(!ret)) |
179 | suspend_mode = md; |
180 | } |
181 | |
182 | return ret ? ret : count; |
183 | } |
184 | |
185 | static DEVICE_ATTR_RW(suspend_mode); |
186 | |
187 | static int zynqmp_pm_probe(struct platform_device *pdev) |
188 | { |
189 | int ret, irq; |
190 | u32 pm_api_version; |
191 | struct mbox_client *client; |
192 | |
193 | zynqmp_pm_get_api_version(version: &pm_api_version); |
194 | |
195 | /* Check PM API version number */ |
196 | if (pm_api_version < ZYNQMP_PM_VERSION) |
197 | return -ENODEV; |
198 | |
199 | /* |
200 | * First try to use Xilinx Event Manager by registering suspend_event_callback |
201 | * for suspend/shutdown event. |
202 | * If xlnx_register_event() returns -EACCES (Xilinx Event Manager |
203 | * is not available to use) or -ENODEV(Xilinx Event Manager not compiled), |
204 | * then use ipi-mailbox or interrupt method. |
205 | */ |
206 | ret = xlnx_register_event(cb_type: PM_INIT_SUSPEND_CB, node_id: 0, event: 0, wake: false, |
207 | cb_fun: suspend_event_callback, NULL); |
208 | if (!ret) { |
209 | zynqmp_pm_init_suspend_work = devm_kzalloc(dev: &pdev->dev, |
210 | size: sizeof(struct zynqmp_pm_work_struct), |
211 | GFP_KERNEL); |
212 | if (!zynqmp_pm_init_suspend_work) { |
213 | xlnx_unregister_event(cb_type: PM_INIT_SUSPEND_CB, node_id: 0, event: 0, |
214 | cb_fun: suspend_event_callback, NULL); |
215 | return -ENOMEM; |
216 | } |
217 | event_registered = true; |
218 | |
219 | INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, |
220 | zynqmp_pm_init_suspend_work_fn); |
221 | } else if (ret != -EACCES && ret != -ENODEV) { |
222 | dev_err(&pdev->dev, "Failed to Register with Xilinx Event manager %d\n" , ret); |
223 | return ret; |
224 | } else if (of_property_present(np: pdev->dev.of_node, propname: "mboxes" )) { |
225 | zynqmp_pm_init_suspend_work = |
226 | devm_kzalloc(dev: &pdev->dev, |
227 | size: sizeof(struct zynqmp_pm_work_struct), |
228 | GFP_KERNEL); |
229 | if (!zynqmp_pm_init_suspend_work) |
230 | return -ENOMEM; |
231 | |
232 | INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, |
233 | zynqmp_pm_init_suspend_work_fn); |
234 | client = devm_kzalloc(dev: &pdev->dev, size: sizeof(*client), GFP_KERNEL); |
235 | if (!client) |
236 | return -ENOMEM; |
237 | |
238 | client->dev = &pdev->dev; |
239 | client->rx_callback = ipi_receive_callback; |
240 | |
241 | rx_chan = mbox_request_channel_byname(cl: client, name: "rx" ); |
242 | if (IS_ERR(ptr: rx_chan)) { |
243 | dev_err(&pdev->dev, "Failed to request rx channel\n" ); |
244 | return PTR_ERR(ptr: rx_chan); |
245 | } |
246 | } else if (of_property_present(np: pdev->dev.of_node, propname: "interrupts" )) { |
247 | irq = platform_get_irq(pdev, 0); |
248 | if (irq < 0) |
249 | return irq; |
250 | |
251 | ret = devm_request_threaded_irq(dev: &pdev->dev, irq, NULL, |
252 | thread_fn: zynqmp_pm_isr, |
253 | IRQF_NO_SUSPEND | IRQF_ONESHOT, |
254 | devname: dev_name(dev: &pdev->dev), |
255 | dev_id: &pdev->dev); |
256 | if (ret) { |
257 | dev_err(&pdev->dev, "devm_request_threaded_irq '%d' failed with %d\n" , |
258 | irq, ret); |
259 | return ret; |
260 | } |
261 | } else { |
262 | dev_err(&pdev->dev, "Required property not found in DT node\n" ); |
263 | return -ENOENT; |
264 | } |
265 | |
266 | ret = sysfs_create_file(kobj: &pdev->dev.kobj, attr: &dev_attr_suspend_mode.attr); |
267 | if (ret) { |
268 | if (event_registered) { |
269 | xlnx_unregister_event(cb_type: PM_INIT_SUSPEND_CB, node_id: 0, event: 0, cb_fun: suspend_event_callback, |
270 | NULL); |
271 | event_registered = false; |
272 | } |
273 | dev_err(&pdev->dev, "unable to create sysfs interface\n" ); |
274 | return ret; |
275 | } |
276 | |
277 | return 0; |
278 | } |
279 | |
280 | static void zynqmp_pm_remove(struct platform_device *pdev) |
281 | { |
282 | sysfs_remove_file(kobj: &pdev->dev.kobj, attr: &dev_attr_suspend_mode.attr); |
283 | if (event_registered) |
284 | xlnx_unregister_event(cb_type: PM_INIT_SUSPEND_CB, node_id: 0, event: 0, cb_fun: suspend_event_callback, NULL); |
285 | |
286 | if (!rx_chan) |
287 | mbox_free_channel(chan: rx_chan); |
288 | } |
289 | |
290 | static const struct of_device_id pm_of_match[] = { |
291 | { .compatible = "xlnx,zynqmp-power" , }, |
292 | { /* end of table */ }, |
293 | }; |
294 | MODULE_DEVICE_TABLE(of, pm_of_match); |
295 | |
296 | static struct platform_driver zynqmp_pm_platform_driver = { |
297 | .probe = zynqmp_pm_probe, |
298 | .remove_new = zynqmp_pm_remove, |
299 | .driver = { |
300 | .name = "zynqmp_power" , |
301 | .of_match_table = pm_of_match, |
302 | }, |
303 | }; |
304 | module_platform_driver(zynqmp_pm_platform_driver); |
305 | |