Commit | Line | Data |
---|---|---|
ab272643 RV |
1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* | |
3 | * Xilinx Zynq MPSoC Power Management | |
4 | * | |
5 | * Copyright (C) 2014-2018 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/platform_device.h> | |
15 | #include <linux/reboot.h> | |
16 | #include <linux/suspend.h> | |
17 | ||
18 | #include <linux/firmware/xlnx-zynqmp.h> | |
19 | ||
20 | enum pm_suspend_mode { | |
21 | PM_SUSPEND_MODE_FIRST = 0, | |
22 | PM_SUSPEND_MODE_STD = PM_SUSPEND_MODE_FIRST, | |
23 | PM_SUSPEND_MODE_POWER_OFF, | |
24 | }; | |
25 | ||
26 | #define PM_SUSPEND_MODE_FIRST PM_SUSPEND_MODE_STD | |
27 | ||
28 | static const char *const suspend_modes[] = { | |
29 | [PM_SUSPEND_MODE_STD] = "standard", | |
30 | [PM_SUSPEND_MODE_POWER_OFF] = "power-off", | |
31 | }; | |
32 | ||
33 | static enum pm_suspend_mode suspend_mode = PM_SUSPEND_MODE_STD; | |
3d031378 | 34 | static const struct zynqmp_eemi_ops *eemi_ops; |
ab272643 RV |
35 | |
36 | enum pm_api_cb_id { | |
37 | PM_INIT_SUSPEND_CB = 30, | |
38 | PM_ACKNOWLEDGE_CB, | |
39 | PM_NOTIFY_CB, | |
40 | }; | |
41 | ||
42 | static void zynqmp_pm_get_callback_data(u32 *buf) | |
43 | { | |
44 | zynqmp_pm_invoke_fn(GET_CALLBACK_DATA, 0, 0, 0, 0, buf); | |
45 | } | |
46 | ||
47 | static irqreturn_t zynqmp_pm_isr(int irq, void *data) | |
48 | { | |
49 | u32 payload[CB_PAYLOAD_SIZE]; | |
50 | ||
51 | zynqmp_pm_get_callback_data(payload); | |
52 | ||
53 | /* First element is callback API ID, others are callback arguments */ | |
54 | if (payload[0] == PM_INIT_SUSPEND_CB) { | |
55 | switch (payload[1]) { | |
56 | case SUSPEND_SYSTEM_SHUTDOWN: | |
57 | orderly_poweroff(true); | |
58 | break; | |
59 | case SUSPEND_POWER_REQUEST: | |
60 | pm_suspend(PM_SUSPEND_MEM); | |
61 | break; | |
62 | default: | |
63 | pr_err("%s Unsupported InitSuspendCb reason " | |
64 | "code %d\n", __func__, payload[1]); | |
65 | } | |
66 | } | |
67 | ||
68 | return IRQ_HANDLED; | |
69 | } | |
70 | ||
71 | static ssize_t suspend_mode_show(struct device *dev, | |
72 | struct device_attribute *attr, char *buf) | |
73 | { | |
74 | char *s = buf; | |
75 | int md; | |
76 | ||
77 | for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) | |
78 | if (suspend_modes[md]) { | |
79 | if (md == suspend_mode) | |
80 | s += sprintf(s, "[%s] ", suspend_modes[md]); | |
81 | else | |
82 | s += sprintf(s, "%s ", suspend_modes[md]); | |
83 | } | |
84 | ||
85 | /* Convert last space to newline */ | |
86 | if (s != buf) | |
87 | *(s - 1) = '\n'; | |
88 | return (s - buf); | |
89 | } | |
90 | ||
91 | static ssize_t suspend_mode_store(struct device *dev, | |
92 | struct device_attribute *attr, | |
93 | const char *buf, size_t count) | |
94 | { | |
95 | int md, ret = -EINVAL; | |
ab272643 | 96 | |
3d031378 | 97 | if (!eemi_ops->set_suspend_mode) |
ab272643 RV |
98 | return ret; |
99 | ||
100 | for (md = PM_SUSPEND_MODE_FIRST; md < ARRAY_SIZE(suspend_modes); md++) | |
101 | if (suspend_modes[md] && | |
102 | sysfs_streq(suspend_modes[md], buf)) { | |
103 | ret = 0; | |
104 | break; | |
105 | } | |
106 | ||
107 | if (!ret && md != suspend_mode) { | |
108 | ret = eemi_ops->set_suspend_mode(md); | |
109 | if (likely(!ret)) | |
110 | suspend_mode = md; | |
111 | } | |
112 | ||
113 | return ret ? ret : count; | |
114 | } | |
115 | ||
116 | static DEVICE_ATTR_RW(suspend_mode); | |
117 | ||
118 | static int zynqmp_pm_probe(struct platform_device *pdev) | |
119 | { | |
120 | int ret, irq; | |
121 | u32 pm_api_version; | |
122 | ||
3d031378 RV |
123 | eemi_ops = zynqmp_pm_get_eemi_ops(); |
124 | if (IS_ERR(eemi_ops)) | |
125 | return PTR_ERR(eemi_ops); | |
ab272643 | 126 | |
3d031378 | 127 | if (!eemi_ops->get_api_version || !eemi_ops->init_finalize) |
ab272643 RV |
128 | return -ENXIO; |
129 | ||
130 | eemi_ops->init_finalize(); | |
131 | eemi_ops->get_api_version(&pm_api_version); | |
132 | ||
133 | /* Check PM API version number */ | |
134 | if (pm_api_version < ZYNQMP_PM_VERSION) | |
135 | return -ENODEV; | |
136 | ||
137 | irq = platform_get_irq(pdev, 0); | |
138 | if (irq <= 0) | |
139 | return -ENXIO; | |
140 | ||
141 | ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, zynqmp_pm_isr, | |
142 | IRQF_NO_SUSPEND | IRQF_ONESHOT, | |
143 | dev_name(&pdev->dev), &pdev->dev); | |
144 | if (ret) { | |
145 | dev_err(&pdev->dev, "devm_request_threaded_irq '%d' failed " | |
146 | "with %d\n", irq, ret); | |
147 | return ret; | |
148 | } | |
149 | ||
150 | ret = sysfs_create_file(&pdev->dev.kobj, &dev_attr_suspend_mode.attr); | |
151 | if (ret) { | |
152 | dev_err(&pdev->dev, "unable to create sysfs interface\n"); | |
153 | return ret; | |
154 | } | |
155 | ||
156 | return 0; | |
157 | } | |
158 | ||
159 | static int zynqmp_pm_remove(struct platform_device *pdev) | |
160 | { | |
161 | sysfs_remove_file(&pdev->dev.kobj, &dev_attr_suspend_mode.attr); | |
162 | ||
163 | return 0; | |
164 | } | |
165 | ||
166 | static const struct of_device_id pm_of_match[] = { | |
167 | { .compatible = "xlnx,zynqmp-power", }, | |
168 | { /* end of table */ }, | |
169 | }; | |
170 | MODULE_DEVICE_TABLE(of, pm_of_match); | |
171 | ||
172 | static struct platform_driver zynqmp_pm_platform_driver = { | |
173 | .probe = zynqmp_pm_probe, | |
174 | .remove = zynqmp_pm_remove, | |
175 | .driver = { | |
176 | .name = "zynqmp_power", | |
177 | .of_match_table = pm_of_match, | |
178 | }, | |
179 | }; | |
180 | module_platform_driver(zynqmp_pm_platform_driver); |