1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Copyright (c) 2011-2021, The Linux Foundation. All rights reserved. |
4 | */ |
5 | |
6 | #include <linux/debugfs.h> |
7 | #include <linux/device.h> |
8 | #include <linux/io.h> |
9 | #include <linux/module.h> |
10 | #include <linux/of.h> |
11 | #include <linux/platform_device.h> |
12 | #include <linux/seq_file.h> |
13 | |
14 | #include <linux/soc/qcom/smem.h> |
15 | #include <clocksource/arm_arch_timer.h> |
16 | |
17 | #define RPM_DYNAMIC_ADDR 0x14 |
18 | #define RPM_DYNAMIC_ADDR_MASK 0xFFFF |
19 | |
20 | #define STAT_TYPE_OFFSET 0x0 |
21 | #define COUNT_OFFSET 0x4 |
22 | #define LAST_ENTERED_AT_OFFSET 0x8 |
23 | #define LAST_EXITED_AT_OFFSET 0x10 |
24 | #define ACCUMULATED_OFFSET 0x18 |
25 | #define CLIENT_VOTES_OFFSET 0x20 |
26 | |
27 | struct subsystem_data { |
28 | const char *name; |
29 | u32 smem_item; |
30 | u32 pid; |
31 | }; |
32 | |
33 | static const struct subsystem_data subsystems[] = { |
34 | { "modem" , 605, 1 }, |
35 | { "wpss" , 605, 13 }, |
36 | { "adsp" , 606, 2 }, |
37 | { "cdsp" , 607, 5 }, |
38 | { "slpi" , 608, 3 }, |
39 | { "gpu" , 609, 0 }, |
40 | { "display" , 610, 0 }, |
41 | { "adsp_island" , 613, 2 }, |
42 | { "slpi_island" , 613, 3 }, |
43 | }; |
44 | |
45 | struct stats_config { |
46 | size_t stats_offset; |
47 | size_t num_records; |
48 | bool appended_stats_avail; |
49 | bool dynamic_offset; |
50 | bool subsystem_stats_in_smem; |
51 | }; |
52 | |
53 | struct stats_data { |
54 | bool appended_stats_avail; |
55 | void __iomem *base; |
56 | }; |
57 | |
58 | struct sleep_stats { |
59 | u32 stat_type; |
60 | u32 count; |
61 | u64 last_entered_at; |
62 | u64 last_exited_at; |
63 | u64 accumulated; |
64 | }; |
65 | |
66 | struct appended_stats { |
67 | u32 client_votes; |
68 | u32 reserved[3]; |
69 | }; |
70 | |
71 | static void qcom_print_stats(struct seq_file *s, const struct sleep_stats *stat) |
72 | { |
73 | u64 accumulated = stat->accumulated; |
74 | /* |
75 | * If a subsystem is in sleep when reading the sleep stats adjust |
76 | * the accumulated sleep duration to show actual sleep time. |
77 | */ |
78 | if (stat->last_entered_at > stat->last_exited_at) |
79 | accumulated += arch_timer_read_counter() - stat->last_entered_at; |
80 | |
81 | seq_printf(m: s, fmt: "Count: %u\n" , stat->count); |
82 | seq_printf(m: s, fmt: "Last Entered At: %llu\n" , stat->last_entered_at); |
83 | seq_printf(m: s, fmt: "Last Exited At: %llu\n" , stat->last_exited_at); |
84 | seq_printf(m: s, fmt: "Accumulated Duration: %llu\n" , accumulated); |
85 | } |
86 | |
87 | static int qcom_subsystem_sleep_stats_show(struct seq_file *s, void *unused) |
88 | { |
89 | struct subsystem_data *subsystem = s->private; |
90 | struct sleep_stats *stat; |
91 | |
92 | /* Items are allocated lazily, so lookup pointer each time */ |
93 | stat = qcom_smem_get(host: subsystem->pid, item: subsystem->smem_item, NULL); |
94 | if (IS_ERR(ptr: stat)) |
95 | return 0; |
96 | |
97 | qcom_print_stats(s, stat); |
98 | |
99 | return 0; |
100 | } |
101 | |
102 | static int qcom_soc_sleep_stats_show(struct seq_file *s, void *unused) |
103 | { |
104 | struct stats_data *d = s->private; |
105 | void __iomem *reg = d->base; |
106 | struct sleep_stats stat; |
107 | |
108 | memcpy_fromio(&stat, reg, sizeof(stat)); |
109 | qcom_print_stats(s, stat: &stat); |
110 | |
111 | if (d->appended_stats_avail) { |
112 | struct appended_stats votes; |
113 | |
114 | memcpy_fromio(&votes, reg + CLIENT_VOTES_OFFSET, sizeof(votes)); |
115 | seq_printf(m: s, fmt: "Client Votes: %#x\n" , votes.client_votes); |
116 | } |
117 | |
118 | return 0; |
119 | } |
120 | |
121 | DEFINE_SHOW_ATTRIBUTE(qcom_soc_sleep_stats); |
122 | DEFINE_SHOW_ATTRIBUTE(qcom_subsystem_sleep_stats); |
123 | |
124 | static void qcom_create_soc_sleep_stat_files(struct dentry *root, void __iomem *reg, |
125 | struct stats_data *d, |
126 | const struct stats_config *config) |
127 | { |
128 | char stat_type[sizeof(u32) + 1] = {0}; |
129 | size_t stats_offset = config->stats_offset; |
130 | u32 offset = 0, type; |
131 | int i, j; |
132 | |
133 | /* |
134 | * On RPM targets, stats offset location is dynamic and changes from target |
135 | * to target and sometimes from build to build for same target. |
136 | * |
137 | * In such cases the dynamic address is present at 0x14 offset from base |
138 | * address in devicetree. The last 16bits indicates the stats_offset. |
139 | */ |
140 | if (config->dynamic_offset) { |
141 | stats_offset = readl(addr: reg + RPM_DYNAMIC_ADDR); |
142 | stats_offset &= RPM_DYNAMIC_ADDR_MASK; |
143 | } |
144 | |
145 | for (i = 0; i < config->num_records; i++) { |
146 | d[i].base = reg + offset + stats_offset; |
147 | |
148 | /* |
149 | * Read the low power mode name and create debugfs file for it. |
150 | * The names read could be of below, |
151 | * (may change depending on low power mode supported). |
152 | * For rpmh-sleep-stats: "aosd", "cxsd" and "ddr". |
153 | * For rpm-sleep-stats: "vmin" and "vlow". |
154 | */ |
155 | type = readl(addr: d[i].base); |
156 | for (j = 0; j < sizeof(u32); j++) { |
157 | stat_type[j] = type & 0xff; |
158 | type = type >> 8; |
159 | } |
160 | strim(stat_type); |
161 | debugfs_create_file(name: stat_type, mode: 0400, parent: root, data: &d[i], |
162 | fops: &qcom_soc_sleep_stats_fops); |
163 | |
164 | offset += sizeof(struct sleep_stats); |
165 | if (d[i].appended_stats_avail) |
166 | offset += sizeof(struct appended_stats); |
167 | } |
168 | } |
169 | |
170 | static void qcom_create_subsystem_stat_files(struct dentry *root, |
171 | const struct stats_config *config) |
172 | { |
173 | int i; |
174 | |
175 | if (!config->subsystem_stats_in_smem) |
176 | return; |
177 | |
178 | for (i = 0; i < ARRAY_SIZE(subsystems); i++) |
179 | debugfs_create_file(name: subsystems[i].name, mode: 0400, parent: root, data: (void *)&subsystems[i], |
180 | fops: &qcom_subsystem_sleep_stats_fops); |
181 | } |
182 | |
183 | static int qcom_stats_probe(struct platform_device *pdev) |
184 | { |
185 | void __iomem *reg; |
186 | struct dentry *root; |
187 | const struct stats_config *config; |
188 | struct stats_data *d; |
189 | int i; |
190 | |
191 | config = device_get_match_data(dev: &pdev->dev); |
192 | if (!config) |
193 | return -ENODEV; |
194 | |
195 | reg = devm_platform_get_and_ioremap_resource(pdev, index: 0, NULL); |
196 | if (IS_ERR(ptr: reg)) |
197 | return -ENOMEM; |
198 | |
199 | d = devm_kcalloc(dev: &pdev->dev, n: config->num_records, |
200 | size: sizeof(*d), GFP_KERNEL); |
201 | if (!d) |
202 | return -ENOMEM; |
203 | |
204 | for (i = 0; i < config->num_records; i++) |
205 | d[i].appended_stats_avail = config->appended_stats_avail; |
206 | |
207 | root = debugfs_create_dir(name: "qcom_stats" , NULL); |
208 | |
209 | qcom_create_subsystem_stat_files(root, config); |
210 | qcom_create_soc_sleep_stat_files(root, reg, d, config); |
211 | |
212 | platform_set_drvdata(pdev, data: root); |
213 | |
214 | device_set_pm_not_required(dev: &pdev->dev); |
215 | |
216 | return 0; |
217 | } |
218 | |
219 | static void qcom_stats_remove(struct platform_device *pdev) |
220 | { |
221 | struct dentry *root = platform_get_drvdata(pdev); |
222 | |
223 | debugfs_remove_recursive(dentry: root); |
224 | } |
225 | |
226 | static const struct stats_config rpm_data = { |
227 | .stats_offset = 0, |
228 | .num_records = 2, |
229 | .appended_stats_avail = true, |
230 | .dynamic_offset = true, |
231 | .subsystem_stats_in_smem = false, |
232 | }; |
233 | |
234 | /* Older RPM firmwares have the stats at a fixed offset instead */ |
235 | static const struct stats_config rpm_data_dba0 = { |
236 | .stats_offset = 0xdba0, |
237 | .num_records = 2, |
238 | .appended_stats_avail = true, |
239 | .dynamic_offset = false, |
240 | .subsystem_stats_in_smem = false, |
241 | }; |
242 | |
243 | static const struct stats_config rpmh_data_sdm845 = { |
244 | .stats_offset = 0x48, |
245 | .num_records = 2, |
246 | .appended_stats_avail = false, |
247 | .dynamic_offset = false, |
248 | .subsystem_stats_in_smem = true, |
249 | }; |
250 | |
251 | static const struct stats_config rpmh_data = { |
252 | .stats_offset = 0x48, |
253 | .num_records = 3, |
254 | .appended_stats_avail = false, |
255 | .dynamic_offset = false, |
256 | .subsystem_stats_in_smem = true, |
257 | }; |
258 | |
259 | static const struct of_device_id qcom_stats_table[] = { |
260 | { .compatible = "qcom,apq8084-rpm-stats" , .data = &rpm_data_dba0 }, |
261 | { .compatible = "qcom,msm8226-rpm-stats" , .data = &rpm_data_dba0 }, |
262 | { .compatible = "qcom,msm8916-rpm-stats" , .data = &rpm_data_dba0 }, |
263 | { .compatible = "qcom,msm8974-rpm-stats" , .data = &rpm_data_dba0 }, |
264 | { .compatible = "qcom,rpm-stats" , .data = &rpm_data }, |
265 | { .compatible = "qcom,rpmh-stats" , .data = &rpmh_data }, |
266 | { .compatible = "qcom,sdm845-rpmh-stats" , .data = &rpmh_data_sdm845 }, |
267 | { } |
268 | }; |
269 | MODULE_DEVICE_TABLE(of, qcom_stats_table); |
270 | |
271 | static struct platform_driver qcom_stats = { |
272 | .probe = qcom_stats_probe, |
273 | .remove_new = qcom_stats_remove, |
274 | .driver = { |
275 | .name = "qcom_stats" , |
276 | .of_match_table = qcom_stats_table, |
277 | }, |
278 | }; |
279 | |
280 | static int __init qcom_stats_init(void) |
281 | { |
282 | return platform_driver_register(&qcom_stats); |
283 | } |
284 | late_initcall(qcom_stats_init); |
285 | |
286 | static void __exit qcom_stats_exit(void) |
287 | { |
288 | platform_driver_unregister(&qcom_stats); |
289 | } |
290 | module_exit(qcom_stats_exit) |
291 | |
292 | MODULE_DESCRIPTION("Qualcomm Technologies, Inc. (QTI) Stats driver" ); |
293 | MODULE_LICENSE("GPL v2" ); |
294 | |