]>
Commit | Line | Data |
---|---|---|
ab272643 RV |
1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* | |
3 | * Xilinx Zynq MPSoC Power Management | |
4 | * | |
ffdbae28 | 5 | * Copyright (C) 2014-2019 Xilinx, Inc. |
ab272643 RV |
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/platform_device.h> | |
15 | #include <linux/reboot.h> | |
16 | #include <linux/suspend.h> | |
17 | ||
18 | #include <linux/firmware/xlnx-zynqmp.h> | |
ffdbae28 TP |
19 | #include <linux/mailbox/zynqmp-ipi-message.h> |
20 | ||
21 | /** | |
22 | * struct zynqmp_pm_work_struct - Wrapper for struct work_struct | |
23 | * @callback_work: Work structure | |
24 | * @args: Callback arguments | |
25 | */ | |
26 | struct zynqmp_pm_work_struct { | |
27 | struct work_struct callback_work; | |
28 | u32 args[CB_ARG_CNT]; | |
29 | }; | |
30 | ||
31 | static struct zynqmp_pm_work_struct *zynqmp_pm_init_suspend_work; | |
32 | static struct mbox_chan *rx_chan; | |
ab272643 RV |
33 | |
34 | enum pm_suspend_mode { | |
35 | PM_SUSPEND_MODE_FIRST = 0, | |
36 | PM_SUSPEND_MODE_STD = PM_SUSPEND_MODE_FIRST, | |
37 | PM_SUSPEND_MODE_POWER_OFF, | |
38 | }; | |
39 | ||
40 | #define PM_SUSPEND_MODE_FIRST PM_SUSPEND_MODE_STD | |
41 | ||
42 | static const char *const suspend_modes[] = { | |
43 | [PM_SUSPEND_MODE_STD] = "standard", | |
44 | [PM_SUSPEND_MODE_POWER_OFF] = "power-off", | |
45 | }; | |
46 | ||
47 | static enum pm_suspend_mode suspend_mode = PM_SUSPEND_MODE_STD; | |
48 | ||
49 | enum pm_api_cb_id { | |
50 | PM_INIT_SUSPEND_CB = 30, | |
51 | PM_ACKNOWLEDGE_CB, | |
52 | PM_NOTIFY_CB, | |
53 | }; | |
54 | ||
55 | static void zynqmp_pm_get_callback_data(u32 *buf) | |
56 | { | |
57 | zynqmp_pm_invoke_fn(GET_CALLBACK_DATA, 0, 0, 0, 0, buf); | |
58 | } | |
59 | ||
60 | static irqreturn_t zynqmp_pm_isr(int irq, void *data) | |
61 | { | |
62 | u32 payload[CB_PAYLOAD_SIZE]; | |
63 | ||
64 | zynqmp_pm_get_callback_data(payload); | |
65 | ||
66 | /* First element is callback API ID, others are callback arguments */ | |
67 | if (payload[0] == PM_INIT_SUSPEND_CB) { | |
68 | switch (payload[1]) { | |
69 | case SUSPEND_SYSTEM_SHUTDOWN: | |
70 | orderly_poweroff(true); | |
71 | break; | |
72 | case SUSPEND_POWER_REQUEST: | |
73 | pm_suspend(PM_SUSPEND_MEM); | |
74 | break; | |
75 | default: | |
76 | pr_err("%s Unsupported InitSuspendCb reason " | |
77 | "code %d\n", __func__, payload[1]); | |
78 | } | |
79 | } | |
80 | ||
81 | return IRQ_HANDLED; | |
82 | } | |
83 | ||
ffdbae28 TP |
84 | static void ipi_receive_callback(struct mbox_client *cl, void *data) |
85 | { | |
86 | struct zynqmp_ipi_message *msg = (struct zynqmp_ipi_message *)data; | |
87 | u32 payload[CB_PAYLOAD_SIZE]; | |
88 | int ret; | |
89 | ||
90 | memcpy(payload, msg->data, sizeof(msg->len)); | |
91 | /* First element is callback API ID, others are callback arguments */ | |
92 | if (payload[0] == PM_INIT_SUSPEND_CB) { | |
93 | if (work_pending(&zynqmp_pm_init_suspend_work->callback_work)) | |
94 | return; | |
95 | ||
96 | /* Copy callback arguments into work's structure */ | |
97 | memcpy(zynqmp_pm_init_suspend_work->args, &payload[1], | |
98 | sizeof(zynqmp_pm_init_suspend_work->args)); | |
99 | ||
100 | queue_work(system_unbound_wq, | |
101 | &zynqmp_pm_init_suspend_work->callback_work); | |
102 | ||
103 | /* Send NULL message to mbox controller to ack the message */ | |
104 | ret = mbox_send_message(rx_chan, NULL); | |
105 | if (ret) | |
106 | pr_err("IPI ack failed. Error %d\n", ret); | |
107 | } | |
108 | } | |
109 | ||
110 | /** | |
111 | * zynqmp_pm_init_suspend_work_fn - Initialize suspend | |
112 | * @work: Pointer to work_struct | |
113 | * | |
114 | * Bottom-half of PM callback IRQ handler. | |
115 | */ | |
116 | static void zynqmp_pm_init_suspend_work_fn(struct work_struct *work) | |
117 | { | |
118 | struct zynqmp_pm_work_struct *pm_work = | |
119 | container_of(work, struct zynqmp_pm_work_struct, callback_work); | |
120 | ||
121 | if (pm_work->args[0] == SUSPEND_SYSTEM_SHUTDOWN) { | |
122 | orderly_poweroff(true); | |
123 | } else if (pm_work->args[0] == SUSPEND_POWER_REQUEST) { | |
124 | pm_suspend(PM_SUSPEND_MEM); | |
125 | } else { | |
126 | pr_err("%s Unsupported InitSuspendCb reason code %d.\n", | |
127 | __func__, pm_work->args[0]); | |
128 | } | |
129 | } | |
130 | ||
ab272643 RV |
131 | static ssize_t suspend_mode_show(struct device *dev, |
132 | struct device_attribute *attr, char *buf) | |
133 | { | |
134 | char *s = buf; | |
135 | int md; | |
136 | ||
137 | for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) | |
138 | if (suspend_modes[md]) { | |
139 | if (md == suspend_mode) | |
140 | s += sprintf(s, "[%s] ", suspend_modes[md]); | |
141 | else | |
142 | s += sprintf(s, "%s ", suspend_modes[md]); | |
143 | } | |
144 | ||
145 | /* Convert last space to newline */ | |
146 | if (s != buf) | |
147 | *(s - 1) = '\n'; | |
148 | return (s - buf); | |
149 | } | |
150 | ||
151 | static ssize_t suspend_mode_store(struct device *dev, | |
152 | struct device_attribute *attr, | |
153 | const char *buf, size_t count) | |
154 | { | |
155 | int md, ret = -EINVAL; | |
ab272643 | 156 | |
ab272643 RV |
157 | for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) |
158 | if (suspend_modes[md] && | |
159 | sysfs_streq(suspend_modes[md], buf)) { | |
160 | ret = 0; | |
161 | break; | |
162 | } | |
163 | ||
164 | if (!ret && md != suspend_mode) { | |
951d0a97 | 165 | ret = zynqmp_pm_set_suspend_mode(md); |
ab272643 RV |
166 | if (likely(!ret)) |
167 | suspend_mode = md; | |
168 | } | |
169 | ||
170 | return ret ? ret : count; | |
171 | } | |
172 | ||
173 | static DEVICE_ATTR_RW(suspend_mode); | |
174 | ||
175 | static int zynqmp_pm_probe(struct platform_device *pdev) | |
176 | { | |
177 | int ret, irq; | |
178 | u32 pm_api_version; | |
ffdbae28 | 179 | struct mbox_client *client; |
ab272643 | 180 | |
9474da95 | 181 | zynqmp_pm_init_finalize(); |
b9b3a8be | 182 | zynqmp_pm_get_api_version(&pm_api_version); |
ab272643 RV |
183 | |
184 | /* Check PM API version number */ | |
185 | if (pm_api_version < ZYNQMP_PM_VERSION) | |
186 | return -ENODEV; | |
187 | ||
ffdbae28 TP |
188 | if (of_find_property(pdev->dev.of_node, "mboxes", NULL)) { |
189 | zynqmp_pm_init_suspend_work = | |
190 | devm_kzalloc(&pdev->dev, | |
191 | sizeof(struct zynqmp_pm_work_struct), | |
192 | GFP_KERNEL); | |
193 | if (!zynqmp_pm_init_suspend_work) | |
194 | return -ENOMEM; | |
195 | ||
196 | INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, | |
197 | zynqmp_pm_init_suspend_work_fn); | |
198 | client = devm_kzalloc(&pdev->dev, sizeof(*client), GFP_KERNEL); | |
199 | if (!client) | |
200 | return -ENOMEM; | |
201 | ||
202 | client->dev = &pdev->dev; | |
203 | client->rx_callback = ipi_receive_callback; | |
204 | ||
205 | rx_chan = mbox_request_channel_byname(client, "rx"); | |
206 | if (IS_ERR(rx_chan)) { | |
207 | dev_err(&pdev->dev, "Failed to request rx channel\n"); | |
a6f2f0fd | 208 | return PTR_ERR(rx_chan); |
ffdbae28 TP |
209 | } |
210 | } else if (of_find_property(pdev->dev.of_node, "interrupts", NULL)) { | |
211 | irq = platform_get_irq(pdev, 0); | |
212 | if (irq <= 0) | |
213 | return -ENXIO; | |
214 | ||
215 | ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, | |
216 | zynqmp_pm_isr, | |
217 | IRQF_NO_SUSPEND | IRQF_ONESHOT, | |
218 | dev_name(&pdev->dev), | |
219 | &pdev->dev); | |
220 | if (ret) { | |
221 | dev_err(&pdev->dev, "devm_request_threaded_irq '%d' " | |
222 | "failed with %d\n", irq, ret); | |
223 | return ret; | |
224 | } | |
225 | } else { | |
226 | dev_err(&pdev->dev, "Required property not found in DT node\n"); | |
227 | return -ENOENT; | |
ab272643 RV |
228 | } |
229 | ||
230 | ret = sysfs_create_file(&pdev->dev.kobj, &dev_attr_suspend_mode.attr); | |
231 | if (ret) { | |
232 | dev_err(&pdev->dev, "unable to create sysfs interface\n"); | |
233 | return ret; | |
234 | } | |
235 | ||
236 | return 0; | |
237 | } | |
238 | ||
239 | static int zynqmp_pm_remove(struct platform_device *pdev) | |
240 | { | |
241 | sysfs_remove_file(&pdev->dev.kobj, &dev_attr_suspend_mode.attr); | |
242 | ||
ffdbae28 TP |
243 | if (!rx_chan) |
244 | mbox_free_channel(rx_chan); | |
245 | ||
ab272643 RV |
246 | return 0; |
247 | } | |
248 | ||
249 | static const struct of_device_id pm_of_match[] = { | |
250 | { .compatible = "xlnx,zynqmp-power", }, | |
251 | { /* end of table */ }, | |
252 | }; | |
253 | MODULE_DEVICE_TABLE(of, pm_of_match); | |
254 | ||
255 | static struct platform_driver zynqmp_pm_platform_driver = { | |
256 | .probe = zynqmp_pm_probe, | |
257 | .remove = zynqmp_pm_remove, | |
258 | .driver = { | |
259 | .name = "zynqmp_power", | |
260 | .of_match_table = pm_of_match, | |
261 | }, | |
262 | }; | |
263 | module_platform_driver(zynqmp_pm_platform_driver); |