1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | |
3 | /* |
4 | * FPDT support for exporting boot and suspend/resume performance data |
5 | * |
6 | * Copyright (C) 2021 Intel Corporation. All rights reserved. |
7 | */ |
8 | |
9 | #define pr_fmt(fmt) "ACPI FPDT: " fmt |
10 | |
11 | #include <linux/acpi.h> |
12 | |
13 | /* |
14 | * FPDT contains ACPI table header and a number of fpdt_subtable_entries. |
15 | * Each fpdt_subtable_entry points to a subtable: FBPT or S3PT. |
16 | * Each FPDT subtable (FBPT/S3PT) is composed of a fpdt_subtable_header |
17 | * and a number of fpdt performance records. |
18 | * Each FPDT performance record is composed of a fpdt_record_header and |
19 | * performance data fields, for boot or suspend or resume phase. |
20 | */ |
21 | enum fpdt_subtable_type { |
22 | SUBTABLE_FBPT, |
23 | SUBTABLE_S3PT, |
24 | }; |
25 | |
26 | struct fpdt_subtable_entry { |
27 | u16 type; /* refer to enum fpdt_subtable_type */ |
28 | u8 length; |
29 | u8 revision; |
30 | u32 reserved; |
31 | u64 address; /* physical address of the S3PT/FBPT table */ |
32 | }; |
33 | |
34 | struct { |
35 | u32 ; |
36 | u32 ; |
37 | }; |
38 | |
39 | enum fpdt_record_type { |
40 | RECORD_S3_RESUME, |
41 | RECORD_S3_SUSPEND, |
42 | RECORD_BOOT, |
43 | }; |
44 | |
45 | struct { |
46 | u16 ; /* refer to enum fpdt_record_type */ |
47 | u8 ; |
48 | u8 ; |
49 | }; |
50 | |
51 | struct resume_performance_record { |
52 | struct fpdt_record_header ; |
53 | u32 resume_count; |
54 | u64 resume_prev; |
55 | u64 resume_avg; |
56 | } __attribute__((packed)); |
57 | |
58 | struct boot_performance_record { |
59 | struct fpdt_record_header ; |
60 | u32 reserved; |
61 | u64 firmware_start; |
62 | u64 bootloader_load; |
63 | u64 bootloader_launch; |
64 | u64 exitbootservice_start; |
65 | u64 exitbootservice_end; |
66 | } __attribute__((packed)); |
67 | |
68 | struct suspend_performance_record { |
69 | struct fpdt_record_header ; |
70 | u64 suspend_start; |
71 | u64 suspend_end; |
72 | } __attribute__((packed)); |
73 | |
74 | |
75 | static struct resume_performance_record *record_resume; |
76 | static struct suspend_performance_record *record_suspend; |
77 | static struct boot_performance_record *record_boot; |
78 | |
79 | #define FPDT_ATTR(phase, name) \ |
80 | static ssize_t name##_show(struct kobject *kobj, \ |
81 | struct kobj_attribute *attr, char *buf) \ |
82 | { \ |
83 | return sprintf(buf, "%llu\n", record_##phase->name); \ |
84 | } \ |
85 | static struct kobj_attribute name##_attr = \ |
86 | __ATTR(name##_ns, 0444, name##_show, NULL) |
87 | |
88 | FPDT_ATTR(resume, resume_prev); |
89 | FPDT_ATTR(resume, resume_avg); |
90 | FPDT_ATTR(suspend, suspend_start); |
91 | FPDT_ATTR(suspend, suspend_end); |
92 | FPDT_ATTR(boot, firmware_start); |
93 | FPDT_ATTR(boot, bootloader_load); |
94 | FPDT_ATTR(boot, bootloader_launch); |
95 | FPDT_ATTR(boot, exitbootservice_start); |
96 | FPDT_ATTR(boot, exitbootservice_end); |
97 | |
98 | static ssize_t resume_count_show(struct kobject *kobj, |
99 | struct kobj_attribute *attr, char *buf) |
100 | { |
101 | return sprintf(buf, fmt: "%u\n" , record_resume->resume_count); |
102 | } |
103 | |
104 | static struct kobj_attribute resume_count_attr = |
105 | __ATTR_RO(resume_count); |
106 | |
107 | static struct attribute *resume_attrs[] = { |
108 | &resume_count_attr.attr, |
109 | &resume_prev_attr.attr, |
110 | &resume_avg_attr.attr, |
111 | NULL |
112 | }; |
113 | |
114 | static const struct attribute_group resume_attr_group = { |
115 | .attrs = resume_attrs, |
116 | .name = "resume" , |
117 | }; |
118 | |
119 | static struct attribute *suspend_attrs[] = { |
120 | &suspend_start_attr.attr, |
121 | &suspend_end_attr.attr, |
122 | NULL |
123 | }; |
124 | |
125 | static const struct attribute_group suspend_attr_group = { |
126 | .attrs = suspend_attrs, |
127 | .name = "suspend" , |
128 | }; |
129 | |
130 | static struct attribute *boot_attrs[] = { |
131 | &firmware_start_attr.attr, |
132 | &bootloader_load_attr.attr, |
133 | &bootloader_launch_attr.attr, |
134 | &exitbootservice_start_attr.attr, |
135 | &exitbootservice_end_attr.attr, |
136 | NULL |
137 | }; |
138 | |
139 | static const struct attribute_group boot_attr_group = { |
140 | .attrs = boot_attrs, |
141 | .name = "boot" , |
142 | }; |
143 | |
144 | static struct kobject *fpdt_kobj; |
145 | |
146 | #if defined CONFIG_X86 && defined CONFIG_PHYS_ADDR_T_64BIT |
147 | #include <linux/processor.h> |
148 | static bool fpdt_address_valid(u64 address) |
149 | { |
150 | /* |
151 | * On some systems the table contains invalid addresses |
152 | * with unsuppored high address bits set, check for this. |
153 | */ |
154 | return !(address >> boot_cpu_data.x86_phys_bits); |
155 | } |
156 | #else |
157 | static bool fpdt_address_valid(u64 address) |
158 | { |
159 | return true; |
160 | } |
161 | #endif |
162 | |
163 | static int fpdt_process_subtable(u64 address, u32 subtable_type) |
164 | { |
165 | struct fpdt_subtable_header *; |
166 | struct fpdt_record_header *; |
167 | char *signature = (subtable_type == SUBTABLE_FBPT ? "FBPT" : "S3PT" ); |
168 | u32 length, offset; |
169 | int result; |
170 | |
171 | if (!fpdt_address_valid(address)) { |
172 | pr_info(FW_BUG "invalid physical address: 0x%llx!\n" , address); |
173 | return -EINVAL; |
174 | } |
175 | |
176 | subtable_header = acpi_os_map_memory(where: address, length: sizeof(*subtable_header)); |
177 | if (!subtable_header) |
178 | return -ENOMEM; |
179 | |
180 | if (strncmp((char *)&subtable_header->signature, signature, 4)) { |
181 | pr_info(FW_BUG "subtable signature and type mismatch!\n" ); |
182 | return -EINVAL; |
183 | } |
184 | |
185 | length = subtable_header->length; |
186 | acpi_os_unmap_memory(logical_address: subtable_header, size: sizeof(*subtable_header)); |
187 | |
188 | subtable_header = acpi_os_map_memory(where: address, length); |
189 | if (!subtable_header) |
190 | return -ENOMEM; |
191 | |
192 | offset = sizeof(*subtable_header); |
193 | while (offset < length) { |
194 | record_header = (void *)subtable_header + offset; |
195 | offset += record_header->length; |
196 | |
197 | if (!record_header->length) { |
198 | pr_err(FW_BUG "Zero-length record found in FPTD.\n" ); |
199 | result = -EINVAL; |
200 | goto err; |
201 | } |
202 | |
203 | switch (record_header->type) { |
204 | case RECORD_S3_RESUME: |
205 | if (subtable_type != SUBTABLE_S3PT) { |
206 | pr_err(FW_BUG "Invalid record %d for subtable %s\n" , |
207 | record_header->type, signature); |
208 | result = -EINVAL; |
209 | goto err; |
210 | } |
211 | if (record_resume) { |
212 | pr_err("Duplicate resume performance record found.\n" ); |
213 | continue; |
214 | } |
215 | record_resume = (struct resume_performance_record *)record_header; |
216 | result = sysfs_create_group(kobj: fpdt_kobj, grp: &resume_attr_group); |
217 | if (result) |
218 | goto err; |
219 | break; |
220 | case RECORD_S3_SUSPEND: |
221 | if (subtable_type != SUBTABLE_S3PT) { |
222 | pr_err(FW_BUG "Invalid %d for subtable %s\n" , |
223 | record_header->type, signature); |
224 | continue; |
225 | } |
226 | if (record_suspend) { |
227 | pr_err("Duplicate suspend performance record found.\n" ); |
228 | continue; |
229 | } |
230 | record_suspend = (struct suspend_performance_record *)record_header; |
231 | result = sysfs_create_group(kobj: fpdt_kobj, grp: &suspend_attr_group); |
232 | if (result) |
233 | goto err; |
234 | break; |
235 | case RECORD_BOOT: |
236 | if (subtable_type != SUBTABLE_FBPT) { |
237 | pr_err(FW_BUG "Invalid %d for subtable %s\n" , |
238 | record_header->type, signature); |
239 | result = -EINVAL; |
240 | goto err; |
241 | } |
242 | if (record_boot) { |
243 | pr_err("Duplicate boot performance record found.\n" ); |
244 | continue; |
245 | } |
246 | record_boot = (struct boot_performance_record *)record_header; |
247 | result = sysfs_create_group(kobj: fpdt_kobj, grp: &boot_attr_group); |
248 | if (result) |
249 | goto err; |
250 | break; |
251 | |
252 | default: |
253 | /* Other types are reserved in ACPI 6.4 spec. */ |
254 | break; |
255 | } |
256 | } |
257 | return 0; |
258 | |
259 | err: |
260 | if (record_boot) |
261 | sysfs_remove_group(kobj: fpdt_kobj, grp: &boot_attr_group); |
262 | |
263 | if (record_suspend) |
264 | sysfs_remove_group(kobj: fpdt_kobj, grp: &suspend_attr_group); |
265 | |
266 | if (record_resume) |
267 | sysfs_remove_group(kobj: fpdt_kobj, grp: &resume_attr_group); |
268 | |
269 | return result; |
270 | } |
271 | |
272 | static int __init acpi_init_fpdt(void) |
273 | { |
274 | acpi_status status; |
275 | struct acpi_table_header *; |
276 | struct fpdt_subtable_entry *subtable; |
277 | u32 offset = sizeof(*header); |
278 | int result; |
279 | |
280 | status = acpi_get_table(ACPI_SIG_FPDT, instance: 0, out_table: &header); |
281 | |
282 | if (ACPI_FAILURE(status)) |
283 | return 0; |
284 | |
285 | fpdt_kobj = kobject_create_and_add(name: "fpdt" , parent: acpi_kobj); |
286 | if (!fpdt_kobj) { |
287 | result = -ENOMEM; |
288 | goto err_nomem; |
289 | } |
290 | |
291 | while (offset < header->length) { |
292 | subtable = (void *)header + offset; |
293 | switch (subtable->type) { |
294 | case SUBTABLE_FBPT: |
295 | case SUBTABLE_S3PT: |
296 | result = fpdt_process_subtable(address: subtable->address, |
297 | subtable_type: subtable->type); |
298 | if (result) |
299 | goto err_subtable; |
300 | break; |
301 | default: |
302 | /* Other types are reserved in ACPI 6.4 spec. */ |
303 | break; |
304 | } |
305 | offset += sizeof(*subtable); |
306 | } |
307 | return 0; |
308 | err_subtable: |
309 | kobject_put(kobj: fpdt_kobj); |
310 | |
311 | err_nomem: |
312 | acpi_put_table(table: header); |
313 | return result; |
314 | } |
315 | |
316 | fs_initcall(acpi_init_fpdt); |
317 | |