|  | // SPDX-License-Identifier: GPL-2.0-only | 
|  |  | 
|  | /* | 
|  | * FPDT support for exporting boot and suspend/resume performance data | 
|  | * | 
|  | * Copyright (C) 2021 Intel Corporation. All rights reserved. | 
|  | */ | 
|  |  | 
|  | #define pr_fmt(fmt) "ACPI FPDT: " fmt | 
|  |  | 
|  | #include <linux/acpi.h> | 
|  |  | 
|  | /* | 
|  | * FPDT contains ACPI table header and a number of fpdt_subtable_entries. | 
|  | * Each fpdt_subtable_entry points to a subtable: FBPT or S3PT. | 
|  | * Each FPDT subtable (FBPT/S3PT) is composed of a fpdt_subtable_header | 
|  | * and a number of fpdt performance records. | 
|  | * Each FPDT performance record is composed of a fpdt_record_header and | 
|  | * performance data fields, for boot or suspend or resume phase. | 
|  | */ | 
|  | enum fpdt_subtable_type { | 
|  | SUBTABLE_FBPT, | 
|  | SUBTABLE_S3PT, | 
|  | }; | 
|  |  | 
|  | struct fpdt_subtable_entry { | 
|  | u16 type;		/* refer to enum fpdt_subtable_type */ | 
|  | u8 length; | 
|  | u8 revision; | 
|  | u32 reserved; | 
|  | u64 address;		/* physical address of the S3PT/FBPT table */ | 
|  | }; | 
|  |  | 
|  | struct fpdt_subtable_header { | 
|  | u32 signature; | 
|  | u32 length; | 
|  | }; | 
|  |  | 
|  | enum fpdt_record_type { | 
|  | RECORD_S3_RESUME, | 
|  | RECORD_S3_SUSPEND, | 
|  | RECORD_BOOT, | 
|  | }; | 
|  |  | 
|  | struct fpdt_record_header { | 
|  | u16 type;		/* refer to enum fpdt_record_type */ | 
|  | u8 length; | 
|  | u8 revision; | 
|  | }; | 
|  |  | 
|  | struct resume_performance_record { | 
|  | struct fpdt_record_header header; | 
|  | u32 resume_count; | 
|  | u64 resume_prev; | 
|  | u64 resume_avg; | 
|  | } __attribute__((packed)); | 
|  |  | 
|  | struct boot_performance_record { | 
|  | struct fpdt_record_header header; | 
|  | u32 reserved; | 
|  | u64 firmware_start; | 
|  | u64 bootloader_load; | 
|  | u64 bootloader_launch; | 
|  | u64 exitbootservice_start; | 
|  | u64 exitbootservice_end; | 
|  | } __attribute__((packed)); | 
|  |  | 
|  | struct suspend_performance_record { | 
|  | struct fpdt_record_header header; | 
|  | u64 suspend_start; | 
|  | u64 suspend_end; | 
|  | } __attribute__((packed)); | 
|  |  | 
|  |  | 
|  | static struct resume_performance_record *record_resume; | 
|  | static struct suspend_performance_record *record_suspend; | 
|  | static struct boot_performance_record *record_boot; | 
|  |  | 
|  | #define FPDT_ATTR(phase, name)	\ | 
|  | static ssize_t name##_show(struct kobject *kobj,	\ | 
|  | struct kobj_attribute *attr, char *buf)	\ | 
|  | {	\ | 
|  | return sprintf(buf, "%llu\n", record_##phase->name);	\ | 
|  | }	\ | 
|  | static struct kobj_attribute name##_attr =	\ | 
|  | __ATTR(name##_ns, 0444, name##_show, NULL) | 
|  |  | 
|  | FPDT_ATTR(resume, resume_prev); | 
|  | FPDT_ATTR(resume, resume_avg); | 
|  | FPDT_ATTR(suspend, suspend_start); | 
|  | FPDT_ATTR(suspend, suspend_end); | 
|  | FPDT_ATTR(boot, firmware_start); | 
|  | FPDT_ATTR(boot, bootloader_load); | 
|  | FPDT_ATTR(boot, bootloader_launch); | 
|  | FPDT_ATTR(boot, exitbootservice_start); | 
|  | FPDT_ATTR(boot, exitbootservice_end); | 
|  |  | 
|  | static ssize_t resume_count_show(struct kobject *kobj, | 
|  | struct kobj_attribute *attr, char *buf) | 
|  | { | 
|  | return sprintf(buf, "%u\n", record_resume->resume_count); | 
|  | } | 
|  |  | 
|  | static struct kobj_attribute resume_count_attr = | 
|  | __ATTR_RO(resume_count); | 
|  |  | 
|  | static struct attribute *resume_attrs[] = { | 
|  | &resume_count_attr.attr, | 
|  | &resume_prev_attr.attr, | 
|  | &resume_avg_attr.attr, | 
|  | NULL | 
|  | }; | 
|  |  | 
|  | static const struct attribute_group resume_attr_group = { | 
|  | .attrs = resume_attrs, | 
|  | .name = "resume", | 
|  | }; | 
|  |  | 
|  | static struct attribute *suspend_attrs[] = { | 
|  | &suspend_start_attr.attr, | 
|  | &suspend_end_attr.attr, | 
|  | NULL | 
|  | }; | 
|  |  | 
|  | static const struct attribute_group suspend_attr_group = { | 
|  | .attrs = suspend_attrs, | 
|  | .name = "suspend", | 
|  | }; | 
|  |  | 
|  | static struct attribute *boot_attrs[] = { | 
|  | &firmware_start_attr.attr, | 
|  | &bootloader_load_attr.attr, | 
|  | &bootloader_launch_attr.attr, | 
|  | &exitbootservice_start_attr.attr, | 
|  | &exitbootservice_end_attr.attr, | 
|  | NULL | 
|  | }; | 
|  |  | 
|  | static const struct attribute_group boot_attr_group = { | 
|  | .attrs = boot_attrs, | 
|  | .name = "boot", | 
|  | }; | 
|  |  | 
|  | static struct kobject *fpdt_kobj; | 
|  |  | 
|  | #if defined CONFIG_X86 && defined CONFIG_PHYS_ADDR_T_64BIT | 
|  | #include <linux/processor.h> | 
|  | static bool fpdt_address_valid(u64 address) | 
|  | { | 
|  | /* | 
|  | * On some systems the table contains invalid addresses | 
|  | * with unsuppored high address bits set, check for this. | 
|  | */ | 
|  | return !(address >> boot_cpu_data.x86_phys_bits); | 
|  | } | 
|  | #else | 
|  | static bool fpdt_address_valid(u64 address) | 
|  | { | 
|  | return true; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | static int fpdt_process_subtable(u64 address, u32 subtable_type) | 
|  | { | 
|  | struct fpdt_subtable_header *subtable_header; | 
|  | struct fpdt_record_header *record_header; | 
|  | char *signature = (subtable_type == SUBTABLE_FBPT ? "FBPT" : "S3PT"); | 
|  | u32 length, offset; | 
|  | int result; | 
|  |  | 
|  | if (!fpdt_address_valid(address)) { | 
|  | pr_info(FW_BUG "invalid physical address: 0x%llx!\n", address); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | subtable_header = acpi_os_map_memory(address, sizeof(*subtable_header)); | 
|  | if (!subtable_header) | 
|  | return -ENOMEM; | 
|  |  | 
|  | if (strncmp((char *)&subtable_header->signature, signature, 4)) { | 
|  | pr_info(FW_BUG "subtable signature and type mismatch!\n"); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | length = subtable_header->length; | 
|  | acpi_os_unmap_memory(subtable_header, sizeof(*subtable_header)); | 
|  |  | 
|  | subtable_header = acpi_os_map_memory(address, length); | 
|  | if (!subtable_header) | 
|  | return -ENOMEM; | 
|  |  | 
|  | offset = sizeof(*subtable_header); | 
|  | while (offset < length) { | 
|  | record_header = (void *)subtable_header + offset; | 
|  | offset += record_header->length; | 
|  |  | 
|  | if (!record_header->length) { | 
|  | pr_err(FW_BUG "Zero-length record found in FPTD.\n"); | 
|  | result = -EINVAL; | 
|  | goto err; | 
|  | } | 
|  |  | 
|  | switch (record_header->type) { | 
|  | case RECORD_S3_RESUME: | 
|  | if (subtable_type != SUBTABLE_S3PT) { | 
|  | pr_err(FW_BUG "Invalid record %d for subtable %s\n", | 
|  | record_header->type, signature); | 
|  | result = -EINVAL; | 
|  | goto err; | 
|  | } | 
|  | if (record_resume) { | 
|  | pr_err("Duplicate resume performance record found.\n"); | 
|  | continue; | 
|  | } | 
|  | record_resume = (struct resume_performance_record *)record_header; | 
|  | result = sysfs_create_group(fpdt_kobj, &resume_attr_group); | 
|  | if (result) | 
|  | goto err; | 
|  | break; | 
|  | case RECORD_S3_SUSPEND: | 
|  | if (subtable_type != SUBTABLE_S3PT) { | 
|  | pr_err(FW_BUG "Invalid %d for subtable %s\n", | 
|  | record_header->type, signature); | 
|  | continue; | 
|  | } | 
|  | if (record_suspend) { | 
|  | pr_err("Duplicate suspend performance record found.\n"); | 
|  | continue; | 
|  | } | 
|  | record_suspend = (struct suspend_performance_record *)record_header; | 
|  | result = sysfs_create_group(fpdt_kobj, &suspend_attr_group); | 
|  | if (result) | 
|  | goto err; | 
|  | break; | 
|  | case RECORD_BOOT: | 
|  | if (subtable_type != SUBTABLE_FBPT) { | 
|  | pr_err(FW_BUG "Invalid %d for subtable %s\n", | 
|  | record_header->type, signature); | 
|  | result = -EINVAL; | 
|  | goto err; | 
|  | } | 
|  | if (record_boot) { | 
|  | pr_err("Duplicate boot performance record found.\n"); | 
|  | continue; | 
|  | } | 
|  | record_boot = (struct boot_performance_record *)record_header; | 
|  | result = sysfs_create_group(fpdt_kobj, &boot_attr_group); | 
|  | if (result) | 
|  | goto err; | 
|  | break; | 
|  |  | 
|  | default: | 
|  | /* Other types are reserved in ACPI 6.4 spec. */ | 
|  | break; | 
|  | } | 
|  | } | 
|  | return 0; | 
|  |  | 
|  | err: | 
|  | if (record_boot) | 
|  | sysfs_remove_group(fpdt_kobj, &boot_attr_group); | 
|  |  | 
|  | if (record_suspend) | 
|  | sysfs_remove_group(fpdt_kobj, &suspend_attr_group); | 
|  |  | 
|  | if (record_resume) | 
|  | sysfs_remove_group(fpdt_kobj, &resume_attr_group); | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | static int __init acpi_init_fpdt(void) | 
|  | { | 
|  | acpi_status status; | 
|  | struct acpi_table_header *header; | 
|  | struct fpdt_subtable_entry *subtable; | 
|  | u32 offset = sizeof(*header); | 
|  | int result; | 
|  |  | 
|  | status = acpi_get_table(ACPI_SIG_FPDT, 0, &header); | 
|  |  | 
|  | if (ACPI_FAILURE(status)) | 
|  | return 0; | 
|  |  | 
|  | fpdt_kobj = kobject_create_and_add("fpdt", acpi_kobj); | 
|  | if (!fpdt_kobj) { | 
|  | result = -ENOMEM; | 
|  | goto err_nomem; | 
|  | } | 
|  |  | 
|  | while (offset < header->length) { | 
|  | subtable = (void *)header + offset; | 
|  | switch (subtable->type) { | 
|  | case SUBTABLE_FBPT: | 
|  | case SUBTABLE_S3PT: | 
|  | result = fpdt_process_subtable(subtable->address, | 
|  | subtable->type); | 
|  | if (result) | 
|  | goto err_subtable; | 
|  | break; | 
|  | default: | 
|  | /* Other types are reserved in ACPI 6.4 spec. */ | 
|  | break; | 
|  | } | 
|  | offset += sizeof(*subtable); | 
|  | } | 
|  | return 0; | 
|  | err_subtable: | 
|  | kobject_put(fpdt_kobj); | 
|  |  | 
|  | err_nomem: | 
|  | acpi_put_table(header); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | fs_initcall(acpi_init_fpdt); |