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, arg0: 0, arg1: 0, arg2: 0, arg3: 0, ret_payload: buf); |
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 " |
87 | "code %d\n" , __func__, payload[1]); |
88 | } |
89 | } |
90 | |
91 | return IRQ_HANDLED; |
92 | } |
93 | |
94 | static void ipi_receive_callback(struct mbox_client *cl, void *data) |
95 | { |
96 | struct zynqmp_ipi_message *msg = (struct zynqmp_ipi_message *)data; |
97 | u32 payload[CB_PAYLOAD_SIZE]; |
98 | int ret; |
99 | |
100 | memcpy(payload, msg->data, sizeof(msg->len)); |
101 | /* First element is callback API ID, others are callback arguments */ |
102 | if (payload[0] == PM_INIT_SUSPEND_CB) { |
103 | if (work_pending(&zynqmp_pm_init_suspend_work->callback_work)) |
104 | return; |
105 | |
106 | /* Copy callback arguments into work's structure */ |
107 | memcpy(zynqmp_pm_init_suspend_work->args, &payload[1], |
108 | sizeof(zynqmp_pm_init_suspend_work->args)); |
109 | |
110 | queue_work(wq: system_unbound_wq, |
111 | work: &zynqmp_pm_init_suspend_work->callback_work); |
112 | |
113 | /* Send NULL message to mbox controller to ack the message */ |
114 | ret = mbox_send_message(chan: rx_chan, NULL); |
115 | if (ret) |
116 | pr_err("IPI ack failed. Error %d\n" , ret); |
117 | } |
118 | } |
119 | |
120 | /** |
121 | * zynqmp_pm_init_suspend_work_fn - Initialize suspend |
122 | * @work: Pointer to work_struct |
123 | * |
124 | * Bottom-half of PM callback IRQ handler. |
125 | */ |
126 | static void zynqmp_pm_init_suspend_work_fn(struct work_struct *work) |
127 | { |
128 | struct zynqmp_pm_work_struct *pm_work = |
129 | container_of(work, struct zynqmp_pm_work_struct, callback_work); |
130 | |
131 | if (pm_work->args[0] == SUSPEND_SYSTEM_SHUTDOWN) { |
132 | orderly_poweroff(force: true); |
133 | } else if (pm_work->args[0] == SUSPEND_POWER_REQUEST) { |
134 | pm_suspend(PM_SUSPEND_MEM); |
135 | } else { |
136 | pr_err("%s Unsupported InitSuspendCb reason code %d.\n" , |
137 | __func__, pm_work->args[0]); |
138 | } |
139 | } |
140 | |
141 | static ssize_t suspend_mode_show(struct device *dev, |
142 | struct device_attribute *attr, char *buf) |
143 | { |
144 | char *s = buf; |
145 | int md; |
146 | |
147 | for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) |
148 | if (suspend_modes[md]) { |
149 | if (md == suspend_mode) |
150 | s += sprintf(buf: s, fmt: "[%s] " , suspend_modes[md]); |
151 | else |
152 | s += sprintf(buf: s, fmt: "%s " , suspend_modes[md]); |
153 | } |
154 | |
155 | /* Convert last space to newline */ |
156 | if (s != buf) |
157 | *(s - 1) = '\n'; |
158 | return (s - buf); |
159 | } |
160 | |
161 | static ssize_t suspend_mode_store(struct device *dev, |
162 | struct device_attribute *attr, |
163 | const char *buf, size_t count) |
164 | { |
165 | int md, ret = -EINVAL; |
166 | |
167 | for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) |
168 | if (suspend_modes[md] && |
169 | sysfs_streq(s1: suspend_modes[md], s2: buf)) { |
170 | ret = 0; |
171 | break; |
172 | } |
173 | |
174 | if (!ret && md != suspend_mode) { |
175 | ret = zynqmp_pm_set_suspend_mode(mode: md); |
176 | if (likely(!ret)) |
177 | suspend_mode = md; |
178 | } |
179 | |
180 | return ret ? ret : count; |
181 | } |
182 | |
183 | static DEVICE_ATTR_RW(suspend_mode); |
184 | |
185 | static int zynqmp_pm_probe(struct platform_device *pdev) |
186 | { |
187 | int ret, irq; |
188 | u32 pm_api_version; |
189 | struct mbox_client *client; |
190 | |
191 | zynqmp_pm_get_api_version(version: &pm_api_version); |
192 | |
193 | /* Check PM API version number */ |
194 | if (pm_api_version < ZYNQMP_PM_VERSION) |
195 | return -ENODEV; |
196 | |
197 | /* |
198 | * First try to use Xilinx Event Manager by registering suspend_event_callback |
199 | * for suspend/shutdown event. |
200 | * If xlnx_register_event() returns -EACCES (Xilinx Event Manager |
201 | * is not available to use) or -ENODEV(Xilinx Event Manager not compiled), |
202 | * then use ipi-mailbox or interrupt method. |
203 | */ |
204 | ret = xlnx_register_event(cb_type: PM_INIT_SUSPEND_CB, node_id: 0, event: 0, wake: false, |
205 | cb_fun: suspend_event_callback, NULL); |
206 | if (!ret) { |
207 | zynqmp_pm_init_suspend_work = devm_kzalloc(dev: &pdev->dev, |
208 | size: sizeof(struct zynqmp_pm_work_struct), |
209 | GFP_KERNEL); |
210 | if (!zynqmp_pm_init_suspend_work) { |
211 | xlnx_unregister_event(cb_type: PM_INIT_SUSPEND_CB, node_id: 0, event: 0, |
212 | cb_fun: suspend_event_callback, NULL); |
213 | return -ENOMEM; |
214 | } |
215 | event_registered = true; |
216 | |
217 | INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, |
218 | zynqmp_pm_init_suspend_work_fn); |
219 | } else if (ret != -EACCES && ret != -ENODEV) { |
220 | dev_err(&pdev->dev, "Failed to Register with Xilinx Event manager %d\n" , ret); |
221 | return ret; |
222 | } else if (of_property_present(np: pdev->dev.of_node, propname: "mboxes" )) { |
223 | zynqmp_pm_init_suspend_work = |
224 | devm_kzalloc(dev: &pdev->dev, |
225 | size: sizeof(struct zynqmp_pm_work_struct), |
226 | GFP_KERNEL); |
227 | if (!zynqmp_pm_init_suspend_work) |
228 | return -ENOMEM; |
229 | |
230 | INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, |
231 | zynqmp_pm_init_suspend_work_fn); |
232 | client = devm_kzalloc(dev: &pdev->dev, size: sizeof(*client), GFP_KERNEL); |
233 | if (!client) |
234 | return -ENOMEM; |
235 | |
236 | client->dev = &pdev->dev; |
237 | client->rx_callback = ipi_receive_callback; |
238 | |
239 | rx_chan = mbox_request_channel_byname(cl: client, name: "rx" ); |
240 | if (IS_ERR(ptr: rx_chan)) { |
241 | dev_err(&pdev->dev, "Failed to request rx channel\n" ); |
242 | return PTR_ERR(ptr: rx_chan); |
243 | } |
244 | } else if (of_property_present(np: pdev->dev.of_node, propname: "interrupts" )) { |
245 | irq = platform_get_irq(pdev, 0); |
246 | if (irq < 0) |
247 | return irq; |
248 | |
249 | ret = devm_request_threaded_irq(dev: &pdev->dev, irq, NULL, |
250 | thread_fn: zynqmp_pm_isr, |
251 | IRQF_NO_SUSPEND | IRQF_ONESHOT, |
252 | devname: dev_name(dev: &pdev->dev), |
253 | dev_id: &pdev->dev); |
254 | if (ret) { |
255 | dev_err(&pdev->dev, "devm_request_threaded_irq '%d' " |
256 | "failed with %d\n" , irq, ret); |
257 | return ret; |
258 | } |
259 | } else { |
260 | dev_err(&pdev->dev, "Required property not found in DT node\n" ); |
261 | return -ENOENT; |
262 | } |
263 | |
264 | ret = sysfs_create_file(kobj: &pdev->dev.kobj, attr: &dev_attr_suspend_mode.attr); |
265 | if (ret) { |
266 | if (event_registered) { |
267 | xlnx_unregister_event(cb_type: PM_INIT_SUSPEND_CB, node_id: 0, event: 0, cb_fun: suspend_event_callback, |
268 | NULL); |
269 | event_registered = false; |
270 | } |
271 | dev_err(&pdev->dev, "unable to create sysfs interface\n" ); |
272 | return ret; |
273 | } |
274 | |
275 | return 0; |
276 | } |
277 | |
278 | static int zynqmp_pm_remove(struct platform_device *pdev) |
279 | { |
280 | sysfs_remove_file(kobj: &pdev->dev.kobj, attr: &dev_attr_suspend_mode.attr); |
281 | if (event_registered) |
282 | xlnx_unregister_event(cb_type: PM_INIT_SUSPEND_CB, node_id: 0, event: 0, cb_fun: suspend_event_callback, NULL); |
283 | |
284 | if (!rx_chan) |
285 | mbox_free_channel(chan: rx_chan); |
286 | |
287 | return 0; |
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 = 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 | |