| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Platform energy and frequency attributes driver |
| * |
| * This driver creates a sys file at /sys/firmware/papr/ which encapsulates a |
| * directory structure containing files in keyword - value pairs that specify |
| * energy and frequency configuration of the system. |
| * |
| * The format of exposing the sysfs information is as follows: |
| * /sys/firmware/papr/energy_scale_info/ |
| * |-- <id>/ |
| * |-- desc |
| * |-- value |
| * |-- value_desc (if exists) |
| * |-- <id>/ |
| * |-- desc |
| * |-- value |
| * |-- value_desc (if exists) |
| * |
| * Copyright 2022 IBM Corp. |
| */ |
| |
| #include <asm/hvcall.h> |
| #include <asm/machdep.h> |
| #include <asm/firmware.h> |
| |
| #include "pseries.h" |
| |
| /* |
| * Flag attributes to fetch either all or one attribute from the HCALL |
| * flag = BE(0) => fetch all attributes with firstAttributeId = 0 |
| * flag = BE(1) => fetch a single attribute with firstAttributeId = id |
| */ |
| #define ESI_FLAGS_ALL 0 |
| #define ESI_FLAGS_SINGLE (1ull << 63) |
| |
| #define KOBJ_MAX_ATTRS 3 |
| |
| #define ESI_HDR_SIZE sizeof(struct h_energy_scale_info_hdr) |
| #define ESI_ATTR_SIZE sizeof(struct energy_scale_attribute) |
| #define CURR_MAX_ESI_ATTRS 8 |
| |
| struct energy_scale_attribute { |
| __be64 id; |
| __be64 val; |
| u8 desc[64]; |
| u8 value_desc[64]; |
| } __packed; |
| |
| struct h_energy_scale_info_hdr { |
| __be64 num_attrs; |
| __be64 array_offset; |
| u8 data_header_version; |
| } __packed; |
| |
| struct papr_attr { |
| u64 id; |
| struct kobj_attribute kobj_attr; |
| }; |
| |
| struct papr_group { |
| struct attribute_group pg; |
| struct papr_attr pgattrs[KOBJ_MAX_ATTRS]; |
| }; |
| |
| static struct papr_group *papr_groups; |
| /* /sys/firmware/papr */ |
| static struct kobject *papr_kobj; |
| /* /sys/firmware/papr/energy_scale_info */ |
| static struct kobject *esi_kobj; |
| |
| /* |
| * Energy modes can change dynamically hence making a new hcall each time the |
| * information needs to be retrieved |
| */ |
| static int papr_get_attr(u64 id, struct energy_scale_attribute *esi) |
| { |
| int esi_buf_size = ESI_HDR_SIZE + (CURR_MAX_ESI_ATTRS * ESI_ATTR_SIZE); |
| int ret, max_esi_attrs = CURR_MAX_ESI_ATTRS; |
| struct energy_scale_attribute *curr_esi; |
| struct h_energy_scale_info_hdr *hdr; |
| char *buf; |
| |
| buf = kmalloc(esi_buf_size, GFP_KERNEL); |
| if (buf == NULL) |
| return -ENOMEM; |
| |
| retry: |
| ret = plpar_hcall_norets(H_GET_ENERGY_SCALE_INFO, ESI_FLAGS_SINGLE, |
| id, virt_to_phys(buf), |
| esi_buf_size); |
| |
| /* |
| * If the hcall fails with not enough memory for either the |
| * header or data, attempt to allocate more |
| */ |
| if (ret == H_PARTIAL || ret == H_P4) { |
| char *temp_buf; |
| |
| max_esi_attrs += 4; |
| esi_buf_size = ESI_HDR_SIZE + (CURR_MAX_ESI_ATTRS * max_esi_attrs); |
| |
| temp_buf = krealloc(buf, esi_buf_size, GFP_KERNEL); |
| if (temp_buf) { |
| buf = temp_buf; |
| } else { |
| ret = -ENOMEM; |
| goto out_buf; |
| } |
| |
| goto retry; |
| } |
| |
| if (ret != H_SUCCESS) { |
| pr_warn("hcall failed: H_GET_ENERGY_SCALE_INFO"); |
| ret = -EIO; |
| goto out_buf; |
| } |
| |
| hdr = (struct h_energy_scale_info_hdr *) buf; |
| curr_esi = (struct energy_scale_attribute *) |
| (buf + be64_to_cpu(hdr->array_offset)); |
| |
| if (esi_buf_size < |
| be64_to_cpu(hdr->array_offset) + (be64_to_cpu(hdr->num_attrs) |
| * sizeof(struct energy_scale_attribute))) { |
| ret = -EIO; |
| goto out_buf; |
| } |
| |
| *esi = *curr_esi; |
| |
| out_buf: |
| kfree(buf); |
| |
| return ret; |
| } |
| |
| /* |
| * Extract and export the description of the energy scale attributes |
| */ |
| static ssize_t desc_show(struct kobject *kobj, |
| struct kobj_attribute *kobj_attr, |
| char *buf) |
| { |
| struct papr_attr *pattr = container_of(kobj_attr, struct papr_attr, |
| kobj_attr); |
| struct energy_scale_attribute esi; |
| int ret; |
| |
| ret = papr_get_attr(pattr->id, &esi); |
| if (ret) |
| return ret; |
| |
| return sysfs_emit(buf, "%s\n", esi.desc); |
| } |
| |
| /* |
| * Extract and export the numeric value of the energy scale attributes |
| */ |
| static ssize_t val_show(struct kobject *kobj, |
| struct kobj_attribute *kobj_attr, |
| char *buf) |
| { |
| struct papr_attr *pattr = container_of(kobj_attr, struct papr_attr, |
| kobj_attr); |
| struct energy_scale_attribute esi; |
| int ret; |
| |
| ret = papr_get_attr(pattr->id, &esi); |
| if (ret) |
| return ret; |
| |
| return sysfs_emit(buf, "%llu\n", be64_to_cpu(esi.val)); |
| } |
| |
| /* |
| * Extract and export the value description in string format of the energy |
| * scale attributes |
| */ |
| static ssize_t val_desc_show(struct kobject *kobj, |
| struct kobj_attribute *kobj_attr, |
| char *buf) |
| { |
| struct papr_attr *pattr = container_of(kobj_attr, struct papr_attr, |
| kobj_attr); |
| struct energy_scale_attribute esi; |
| int ret; |
| |
| ret = papr_get_attr(pattr->id, &esi); |
| if (ret) |
| return ret; |
| |
| return sysfs_emit(buf, "%s\n", esi.value_desc); |
| } |
| |
| static struct papr_ops_info { |
| const char *attr_name; |
| ssize_t (*show)(struct kobject *kobj, struct kobj_attribute *kobj_attr, |
| char *buf); |
| } ops_info[KOBJ_MAX_ATTRS] = { |
| { "desc", desc_show }, |
| { "value", val_show }, |
| { "value_desc", val_desc_show }, |
| }; |
| |
| static void add_attr(u64 id, int index, struct papr_attr *attr) |
| { |
| attr->id = id; |
| sysfs_attr_init(&attr->kobj_attr.attr); |
| attr->kobj_attr.attr.name = ops_info[index].attr_name; |
| attr->kobj_attr.attr.mode = 0444; |
| attr->kobj_attr.show = ops_info[index].show; |
| } |
| |
| static int add_attr_group(u64 id, struct papr_group *pg, bool show_val_desc) |
| { |
| int i; |
| |
| for (i = 0; i < KOBJ_MAX_ATTRS; i++) { |
| if (!strcmp(ops_info[i].attr_name, "value_desc") && |
| !show_val_desc) { |
| continue; |
| } |
| add_attr(id, i, &pg->pgattrs[i]); |
| pg->pg.attrs[i] = &pg->pgattrs[i].kobj_attr.attr; |
| } |
| |
| return sysfs_create_group(esi_kobj, &pg->pg); |
| } |
| |
| |
| static int __init papr_init(void) |
| { |
| int esi_buf_size = ESI_HDR_SIZE + (CURR_MAX_ESI_ATTRS * ESI_ATTR_SIZE); |
| int ret, idx, i, max_esi_attrs = CURR_MAX_ESI_ATTRS; |
| struct h_energy_scale_info_hdr *esi_hdr; |
| struct energy_scale_attribute *esi_attrs; |
| uint64_t num_attrs; |
| char *esi_buf; |
| |
| if (!firmware_has_feature(FW_FEATURE_LPAR) || |
| !firmware_has_feature(FW_FEATURE_ENERGY_SCALE_INFO)) { |
| return -ENXIO; |
| } |
| |
| esi_buf = kmalloc(esi_buf_size, GFP_KERNEL); |
| if (esi_buf == NULL) |
| return -ENOMEM; |
| /* |
| * hcall( |
| * uint64 H_GET_ENERGY_SCALE_INFO, // Get energy scale info |
| * uint64 flags, // Per the flag request |
| * uint64 firstAttributeId, // The attribute id |
| * uint64 bufferAddress, // Guest physical address of the output buffer |
| * uint64 bufferSize); // The size in bytes of the output buffer |
| */ |
| retry: |
| |
| ret = plpar_hcall_norets(H_GET_ENERGY_SCALE_INFO, ESI_FLAGS_ALL, 0, |
| virt_to_phys(esi_buf), esi_buf_size); |
| |
| /* |
| * If the hcall fails with not enough memory for either the |
| * header or data, attempt to allocate more |
| */ |
| if (ret == H_PARTIAL || ret == H_P4) { |
| char *temp_esi_buf; |
| |
| max_esi_attrs += 4; |
| esi_buf_size = ESI_HDR_SIZE + (CURR_MAX_ESI_ATTRS * max_esi_attrs); |
| |
| temp_esi_buf = krealloc(esi_buf, esi_buf_size, GFP_KERNEL); |
| if (temp_esi_buf) |
| esi_buf = temp_esi_buf; |
| else |
| return -ENOMEM; |
| |
| goto retry; |
| } |
| |
| if (ret != H_SUCCESS) { |
| pr_warn("hcall failed: H_GET_ENERGY_SCALE_INFO, ret: %d\n", ret); |
| goto out_free_esi_buf; |
| } |
| |
| esi_hdr = (struct h_energy_scale_info_hdr *) esi_buf; |
| num_attrs = be64_to_cpu(esi_hdr->num_attrs); |
| esi_attrs = (struct energy_scale_attribute *) |
| (esi_buf + be64_to_cpu(esi_hdr->array_offset)); |
| |
| if (esi_buf_size < |
| be64_to_cpu(esi_hdr->array_offset) + |
| (num_attrs * sizeof(struct energy_scale_attribute))) { |
| goto out_free_esi_buf; |
| } |
| |
| papr_groups = kcalloc(num_attrs, sizeof(*papr_groups), GFP_KERNEL); |
| if (!papr_groups) |
| goto out_free_esi_buf; |
| |
| papr_kobj = kobject_create_and_add("papr", firmware_kobj); |
| if (!papr_kobj) { |
| pr_warn("kobject_create_and_add papr failed\n"); |
| goto out_papr_groups; |
| } |
| |
| esi_kobj = kobject_create_and_add("energy_scale_info", papr_kobj); |
| if (!esi_kobj) { |
| pr_warn("kobject_create_and_add energy_scale_info failed\n"); |
| goto out_kobj; |
| } |
| |
| /* Allocate the groups before registering */ |
| for (idx = 0; idx < num_attrs; idx++) { |
| papr_groups[idx].pg.attrs = kcalloc(KOBJ_MAX_ATTRS + 1, |
| sizeof(*papr_groups[idx].pg.attrs), |
| GFP_KERNEL); |
| if (!papr_groups[idx].pg.attrs) |
| goto out_pgattrs; |
| |
| papr_groups[idx].pg.name = kasprintf(GFP_KERNEL, "%lld", |
| be64_to_cpu(esi_attrs[idx].id)); |
| if (papr_groups[idx].pg.name == NULL) |
| goto out_pgattrs; |
| } |
| |
| for (idx = 0; idx < num_attrs; idx++) { |
| bool show_val_desc = true; |
| |
| /* Do not add the value desc attr if it does not exist */ |
| if (strnlen(esi_attrs[idx].value_desc, |
| sizeof(esi_attrs[idx].value_desc)) == 0) |
| show_val_desc = false; |
| |
| if (add_attr_group(be64_to_cpu(esi_attrs[idx].id), |
| &papr_groups[idx], |
| show_val_desc)) { |
| pr_warn("Failed to create papr attribute group %s\n", |
| papr_groups[idx].pg.name); |
| idx = num_attrs; |
| goto out_pgattrs; |
| } |
| } |
| |
| kfree(esi_buf); |
| return 0; |
| out_pgattrs: |
| for (i = 0; i < idx ; i++) { |
| kfree(papr_groups[i].pg.attrs); |
| kfree(papr_groups[i].pg.name); |
| } |
| kobject_put(esi_kobj); |
| out_kobj: |
| kobject_put(papr_kobj); |
| out_papr_groups: |
| kfree(papr_groups); |
| out_free_esi_buf: |
| kfree(esi_buf); |
| |
| return -ENOMEM; |
| } |
| |
| machine_device_initcall(pseries, papr_init); |