| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * SCMI Powercap support. |
| * |
| * Copyright (C) 2022 ARM Ltd. |
| */ |
| |
| #include <linux/device.h> |
| #include <linux/math.h> |
| #include <linux/limits.h> |
| #include <linux/list.h> |
| #include <linux/module.h> |
| #include <linux/powercap.h> |
| #include <linux/scmi_protocol.h> |
| |
| #define to_scmi_powercap_zone(z) \ |
| container_of(z, struct scmi_powercap_zone, zone) |
| |
| static const struct scmi_powercap_proto_ops *powercap_ops; |
| |
| struct scmi_powercap_zone { |
| unsigned int height; |
| struct device *dev; |
| struct scmi_protocol_handle *ph; |
| const struct scmi_powercap_info *info; |
| struct scmi_powercap_zone *spzones; |
| struct powercap_zone zone; |
| struct list_head node; |
| }; |
| |
| struct scmi_powercap_root { |
| unsigned int num_zones; |
| struct scmi_powercap_zone *spzones; |
| struct list_head *registered_zones; |
| }; |
| |
| static struct powercap_control_type *scmi_top_pcntrl; |
| |
| static int scmi_powercap_zone_release(struct powercap_zone *pz) |
| { |
| return 0; |
| } |
| |
| static int scmi_powercap_get_max_power_range_uw(struct powercap_zone *pz, |
| u64 *max_power_range_uw) |
| { |
| *max_power_range_uw = U32_MAX; |
| return 0; |
| } |
| |
| static int scmi_powercap_get_power_uw(struct powercap_zone *pz, |
| u64 *power_uw) |
| { |
| struct scmi_powercap_zone *spz = to_scmi_powercap_zone(pz); |
| u32 avg_power, pai; |
| int ret; |
| |
| if (!spz->info->powercap_monitoring) |
| return -EINVAL; |
| |
| ret = powercap_ops->measurements_get(spz->ph, spz->info->id, &avg_power, |
| &pai); |
| if (ret) |
| return ret; |
| |
| *power_uw = avg_power; |
| if (spz->info->powercap_scale_mw) |
| *power_uw *= 1000; |
| |
| return 0; |
| } |
| |
| static const struct powercap_zone_ops zone_ops = { |
| .get_max_power_range_uw = scmi_powercap_get_max_power_range_uw, |
| .get_power_uw = scmi_powercap_get_power_uw, |
| .release = scmi_powercap_zone_release, |
| }; |
| |
| static void scmi_powercap_normalize_cap(const struct scmi_powercap_zone *spz, |
| u64 power_limit_uw, u32 *norm) |
| { |
| bool scale_mw = spz->info->powercap_scale_mw; |
| u64 val; |
| |
| val = scale_mw ? DIV_ROUND_UP_ULL(power_limit_uw, 1000) : power_limit_uw; |
| /* |
| * This cast is lossless since here @req_power is certain to be within |
| * the range [min_power_cap, max_power_cap] whose bounds are assured to |
| * be two unsigned 32bits quantities. |
| */ |
| *norm = clamp_t(u32, val, spz->info->min_power_cap, |
| spz->info->max_power_cap); |
| *norm = rounddown(*norm, spz->info->power_cap_step); |
| |
| val = (scale_mw) ? *norm * 1000 : *norm; |
| if (power_limit_uw != val) |
| dev_dbg(spz->dev, |
| "Normalized %s:CAP - requested:%llu - normalized:%llu\n", |
| spz->info->name, power_limit_uw, val); |
| } |
| |
| static int scmi_powercap_set_power_limit_uw(struct powercap_zone *pz, int cid, |
| u64 power_uw) |
| { |
| struct scmi_powercap_zone *spz = to_scmi_powercap_zone(pz); |
| u32 norm_power; |
| |
| if (!spz->info->powercap_cap_config) |
| return -EINVAL; |
| |
| scmi_powercap_normalize_cap(spz, power_uw, &norm_power); |
| |
| return powercap_ops->cap_set(spz->ph, spz->info->id, norm_power, false); |
| } |
| |
| static int scmi_powercap_get_power_limit_uw(struct powercap_zone *pz, int cid, |
| u64 *power_limit_uw) |
| { |
| struct scmi_powercap_zone *spz = to_scmi_powercap_zone(pz); |
| u32 power; |
| int ret; |
| |
| ret = powercap_ops->cap_get(spz->ph, spz->info->id, &power); |
| if (ret) |
| return ret; |
| |
| *power_limit_uw = power; |
| if (spz->info->powercap_scale_mw) |
| *power_limit_uw *= 1000; |
| |
| return 0; |
| } |
| |
| static void scmi_powercap_normalize_time(const struct scmi_powercap_zone *spz, |
| u64 time_us, u32 *norm) |
| { |
| /* |
| * This cast is lossless since here @time_us is certain to be within the |
| * range [min_pai, max_pai] whose bounds are assured to be two unsigned |
| * 32bits quantities. |
| */ |
| *norm = clamp_t(u32, time_us, spz->info->min_pai, spz->info->max_pai); |
| *norm = rounddown(*norm, spz->info->pai_step); |
| |
| if (time_us != *norm) |
| dev_dbg(spz->dev, |
| "Normalized %s:PAI - requested:%llu - normalized:%u\n", |
| spz->info->name, time_us, *norm); |
| } |
| |
| static int scmi_powercap_set_time_window_us(struct powercap_zone *pz, int cid, |
| u64 time_window_us) |
| { |
| struct scmi_powercap_zone *spz = to_scmi_powercap_zone(pz); |
| u32 norm_pai; |
| |
| if (!spz->info->powercap_pai_config) |
| return -EINVAL; |
| |
| scmi_powercap_normalize_time(spz, time_window_us, &norm_pai); |
| |
| return powercap_ops->pai_set(spz->ph, spz->info->id, norm_pai); |
| } |
| |
| static int scmi_powercap_get_time_window_us(struct powercap_zone *pz, int cid, |
| u64 *time_window_us) |
| { |
| struct scmi_powercap_zone *spz = to_scmi_powercap_zone(pz); |
| int ret; |
| u32 pai; |
| |
| ret = powercap_ops->pai_get(spz->ph, spz->info->id, &pai); |
| if (ret) |
| return ret; |
| |
| *time_window_us = pai; |
| |
| return 0; |
| } |
| |
| static int scmi_powercap_get_max_power_uw(struct powercap_zone *pz, int cid, |
| u64 *max_power_uw) |
| { |
| struct scmi_powercap_zone *spz = to_scmi_powercap_zone(pz); |
| |
| *max_power_uw = spz->info->max_power_cap; |
| if (spz->info->powercap_scale_mw) |
| *max_power_uw *= 1000; |
| |
| return 0; |
| } |
| |
| static int scmi_powercap_get_min_power_uw(struct powercap_zone *pz, int cid, |
| u64 *min_power_uw) |
| { |
| struct scmi_powercap_zone *spz = to_scmi_powercap_zone(pz); |
| |
| *min_power_uw = spz->info->min_power_cap; |
| if (spz->info->powercap_scale_mw) |
| *min_power_uw *= 1000; |
| |
| return 0; |
| } |
| |
| static int scmi_powercap_get_max_time_window_us(struct powercap_zone *pz, |
| int cid, u64 *time_window_us) |
| { |
| struct scmi_powercap_zone *spz = to_scmi_powercap_zone(pz); |
| |
| *time_window_us = spz->info->max_pai; |
| |
| return 0; |
| } |
| |
| static int scmi_powercap_get_min_time_window_us(struct powercap_zone *pz, |
| int cid, u64 *time_window_us) |
| { |
| struct scmi_powercap_zone *spz = to_scmi_powercap_zone(pz); |
| |
| *time_window_us = (u64)spz->info->min_pai; |
| |
| return 0; |
| } |
| |
| static const char *scmi_powercap_get_name(struct powercap_zone *pz, int cid) |
| { |
| return "SCMI power-cap"; |
| } |
| |
| static const struct powercap_zone_constraint_ops constraint_ops = { |
| .set_power_limit_uw = scmi_powercap_set_power_limit_uw, |
| .get_power_limit_uw = scmi_powercap_get_power_limit_uw, |
| .set_time_window_us = scmi_powercap_set_time_window_us, |
| .get_time_window_us = scmi_powercap_get_time_window_us, |
| .get_max_power_uw = scmi_powercap_get_max_power_uw, |
| .get_min_power_uw = scmi_powercap_get_min_power_uw, |
| .get_max_time_window_us = scmi_powercap_get_max_time_window_us, |
| .get_min_time_window_us = scmi_powercap_get_min_time_window_us, |
| .get_name = scmi_powercap_get_name, |
| }; |
| |
| static void scmi_powercap_unregister_all_zones(struct scmi_powercap_root *pr) |
| { |
| int i; |
| |
| /* Un-register children zones first starting from the leaves */ |
| for (i = pr->num_zones - 1; i >= 0; i--) { |
| if (!list_empty(&pr->registered_zones[i])) { |
| struct scmi_powercap_zone *spz; |
| |
| list_for_each_entry(spz, &pr->registered_zones[i], node) |
| powercap_unregister_zone(scmi_top_pcntrl, |
| &spz->zone); |
| } |
| } |
| } |
| |
| static inline bool |
| scmi_powercap_is_zone_registered(struct scmi_powercap_zone *spz) |
| { |
| return !list_empty(&spz->node); |
| } |
| |
| static inline unsigned int |
| scmi_powercap_get_zone_height(struct scmi_powercap_zone *spz) |
| { |
| if (spz->info->parent_id == SCMI_POWERCAP_ROOT_ZONE_ID) |
| return 0; |
| |
| return spz->spzones[spz->info->parent_id].height + 1; |
| } |
| |
| static inline struct scmi_powercap_zone * |
| scmi_powercap_get_parent_zone(struct scmi_powercap_zone *spz) |
| { |
| if (spz->info->parent_id == SCMI_POWERCAP_ROOT_ZONE_ID) |
| return NULL; |
| |
| return &spz->spzones[spz->info->parent_id]; |
| } |
| |
| /** |
| * scmi_powercap_register_zone - Register an SCMI powercap zone recursively |
| * |
| * @pr: A reference to the root powercap zones descriptors |
| * @spz: A reference to the SCMI powercap zone to register |
| * |
| * When registering SCMI powercap zones with the powercap framework we should |
| * take care to always register zones starting from the root ones and to |
| * deregister starting from the leaves. |
| * |
| * Unfortunately we cannot assume that the array of available SCMI powercap |
| * zones provided by the SCMI platform firmware is built to comply with such |
| * requirement. |
| * |
| * This function, given an SCMI powercap zone to register, takes care to walk |
| * the SCMI powercap zones tree up to the root looking recursively for |
| * unregistered parent zones before registering the provided zone; at the same |
| * time each registered zone height in such a tree is accounted for and each |
| * zone, once registered, is stored in the @registered_zones array that is |
| * indexed by zone height: this way will be trivial, at unregister time, to walk |
| * the @registered_zones array backward and unregister all the zones starting |
| * from the leaves, removing children zones before parents. |
| * |
| * While doing this, we prune away any zone marked as invalid (like the ones |
| * sporting an SCMI abstract power scale) as long as they are positioned as |
| * leaves in the SCMI powercap zones hierarchy: any non-leaf invalid zone causes |
| * the entire process to fail since we cannot assume the correctness of an SCMI |
| * powercap zones hierarchy if some of the internal nodes are missing. |
| * |
| * Note that the array of SCMI powercap zones as returned by the SCMI platform |
| * is known to be sane, i.e. zones relationships have been validated at the |
| * protocol layer. |
| * |
| * Return: 0 on Success |
| */ |
| static int scmi_powercap_register_zone(struct scmi_powercap_root *pr, |
| struct scmi_powercap_zone *spz) |
| { |
| int ret = 0; |
| struct scmi_powercap_zone *parent; |
| |
| if (!spz->info) |
| return ret; |
| |
| parent = scmi_powercap_get_parent_zone(spz); |
| if (parent && !scmi_powercap_is_zone_registered(parent)) { |
| /* |
| * Bail out if a parent domain was marked as unsupported: |
| * only domains participating as leaves can be skipped. |
| */ |
| if (!parent->info) |
| return -ENODEV; |
| |
| ret = scmi_powercap_register_zone(pr, parent); |
| if (ret) |
| return ret; |
| } |
| |
| if (!scmi_powercap_is_zone_registered(spz)) { |
| struct powercap_zone *z; |
| |
| z = powercap_register_zone(&spz->zone, |
| scmi_top_pcntrl, |
| spz->info->name, |
| parent ? &parent->zone : NULL, |
| &zone_ops, 1, &constraint_ops); |
| if (!IS_ERR(z)) { |
| spz->height = scmi_powercap_get_zone_height(spz); |
| list_add(&spz->node, |
| &pr->registered_zones[spz->height]); |
| dev_dbg(spz->dev, |
| "Registered node %s - parent %s - height:%d\n", |
| spz->info->name, |
| parent ? parent->info->name : "ROOT", |
| spz->height); |
| ret = 0; |
| } else { |
| ret = PTR_ERR(z); |
| dev_err(spz->dev, |
| "Error registering node:%s - parent:%s - h:%d - ret:%d\n", |
| spz->info->name, |
| parent ? parent->info->name : "ROOT", |
| spz->height, ret); |
| } |
| } |
| |
| return ret; |
| } |
| |
| static int scmi_powercap_probe(struct scmi_device *sdev) |
| { |
| int ret, i; |
| struct scmi_powercap_root *pr; |
| struct scmi_powercap_zone *spz; |
| struct scmi_protocol_handle *ph; |
| struct device *dev = &sdev->dev; |
| |
| if (!sdev->handle) |
| return -ENODEV; |
| |
| powercap_ops = sdev->handle->devm_protocol_get(sdev, |
| SCMI_PROTOCOL_POWERCAP, |
| &ph); |
| if (IS_ERR(powercap_ops)) |
| return PTR_ERR(powercap_ops); |
| |
| pr = devm_kzalloc(dev, sizeof(*pr), GFP_KERNEL); |
| if (!pr) |
| return -ENOMEM; |
| |
| ret = powercap_ops->num_domains_get(ph); |
| if (ret < 0) { |
| dev_err(dev, "number of powercap domains not found\n"); |
| return ret; |
| } |
| pr->num_zones = ret; |
| |
| pr->spzones = devm_kcalloc(dev, pr->num_zones, |
| sizeof(*pr->spzones), GFP_KERNEL); |
| if (!pr->spzones) |
| return -ENOMEM; |
| |
| /* Allocate for worst possible scenario of maximum tree height. */ |
| pr->registered_zones = devm_kcalloc(dev, pr->num_zones, |
| sizeof(*pr->registered_zones), |
| GFP_KERNEL); |
| if (!pr->registered_zones) |
| return -ENOMEM; |
| |
| for (i = 0, spz = pr->spzones; i < pr->num_zones; i++, spz++) { |
| /* |
| * Powercap domains are validate by the protocol layer, i.e. |
| * when only non-NULL domains are returned here, whose |
| * parent_id is assured to point to another valid domain. |
| */ |
| spz->info = powercap_ops->info_get(ph, i); |
| |
| spz->dev = dev; |
| spz->ph = ph; |
| spz->spzones = pr->spzones; |
| INIT_LIST_HEAD(&spz->node); |
| INIT_LIST_HEAD(&pr->registered_zones[i]); |
| |
| /* |
| * Forcibly skip powercap domains using an abstract scale. |
| * Note that only leaves domains can be skipped, so this could |
| * lead later to a global failure. |
| */ |
| if (!spz->info->powercap_scale_uw && |
| !spz->info->powercap_scale_mw) { |
| dev_warn(dev, |
| "Abstract power scale not supported. Skip %s.\n", |
| spz->info->name); |
| spz->info = NULL; |
| continue; |
| } |
| } |
| |
| /* |
| * Scan array of retrieved SCMI powercap domains and register them |
| * recursively starting from the root domains. |
| */ |
| for (i = 0, spz = pr->spzones; i < pr->num_zones; i++, spz++) { |
| ret = scmi_powercap_register_zone(pr, spz); |
| if (ret) { |
| dev_err(dev, |
| "Failed to register powercap zone %s - ret:%d\n", |
| spz->info->name, ret); |
| scmi_powercap_unregister_all_zones(pr); |
| return ret; |
| } |
| } |
| |
| dev_set_drvdata(dev, pr); |
| |
| dev_info(dev, "Registered %d SCMI Powercap domains !\n", pr->num_zones); |
| |
| return ret; |
| } |
| |
| static void scmi_powercap_remove(struct scmi_device *sdev) |
| { |
| struct device *dev = &sdev->dev; |
| struct scmi_powercap_root *pr = dev_get_drvdata(dev); |
| |
| scmi_powercap_unregister_all_zones(pr); |
| } |
| |
| static const struct scmi_device_id scmi_id_table[] = { |
| { SCMI_PROTOCOL_POWERCAP, "powercap" }, |
| { }, |
| }; |
| MODULE_DEVICE_TABLE(scmi, scmi_id_table); |
| |
| static struct scmi_driver scmi_powercap_driver = { |
| .name = "scmi-powercap", |
| .probe = scmi_powercap_probe, |
| .remove = scmi_powercap_remove, |
| .id_table = scmi_id_table, |
| }; |
| |
| static int __init scmi_powercap_init(void) |
| { |
| int ret; |
| |
| scmi_top_pcntrl = powercap_register_control_type(NULL, "arm-scmi", NULL); |
| if (IS_ERR(scmi_top_pcntrl)) |
| return PTR_ERR(scmi_top_pcntrl); |
| |
| ret = scmi_register(&scmi_powercap_driver); |
| if (ret) |
| powercap_unregister_control_type(scmi_top_pcntrl); |
| |
| return ret; |
| } |
| module_init(scmi_powercap_init); |
| |
| static void __exit scmi_powercap_exit(void) |
| { |
| scmi_unregister(&scmi_powercap_driver); |
| |
| powercap_unregister_control_type(scmi_top_pcntrl); |
| } |
| module_exit(scmi_powercap_exit); |
| |
| MODULE_AUTHOR("Cristian Marussi <cristian.marussi@arm.com>"); |
| MODULE_DESCRIPTION("ARM SCMI Powercap driver"); |
| MODULE_LICENSE("GPL"); |