diff --git a/drivers/acpi/platform_profile.c b/drivers/acpi/platform_profile.c index 79d2e92e5c28d7..1408a162b60f0f 100644 --- a/drivers/acpi/platform_profile.c +++ b/drivers/acpi/platform_profile.c @@ -43,6 +43,13 @@ static const char * const profile_names[] = { static_assert(ARRAY_SIZE(profile_names) == PLATFORM_PROFILE_LAST); static DEFINE_IDA(platform_profile_ida); +static bool platform_profile_legacy_registered; + +static void platform_profile_legacy_notify(void) +{ + if (platform_profile_legacy_registered) + sysfs_notify(acpi_kobj, NULL, "platform_profile"); +} /** * _commmon_choices_show - Show the available profile choices @@ -216,7 +223,7 @@ static ssize_t profile_store(struct device *dev, return ret; } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); return count; } @@ -436,7 +443,7 @@ static ssize_t platform_profile_store(struct kobject *kobj, return ret; } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); return count; } @@ -473,6 +480,14 @@ static const struct attribute_group platform_profile_group = { .is_visible = profile_class_is_visible, }; +static int platform_profile_legacy_update_group(void) +{ + if (!platform_profile_legacy_registered) + return 0; + + return sysfs_update_group(acpi_kobj, &platform_profile_group); +} + /** * platform_profile_notify - Notify class device and legacy sysfs interface * @dev: The class device @@ -482,7 +497,7 @@ void platform_profile_notify(struct device *dev) scoped_cond_guard(mutex_intr, return, &profile_lock) { _notify_class_profile(dev, NULL); } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); } EXPORT_SYMBOL_GPL(platform_profile_notify); @@ -532,7 +547,7 @@ int platform_profile_cycle(void) return err; } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); return 0; } @@ -605,9 +620,9 @@ struct device *platform_profile_register(struct device *dev, const char *name, goto cleanup_ida; } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); - err = sysfs_update_group(acpi_kobj, &platform_profile_group); + err = platform_profile_legacy_update_group(); if (err) goto cleanup_cur; @@ -641,8 +656,8 @@ void platform_profile_remove(struct device *dev) ida_free(&platform_profile_ida, pprof->minor); device_unregister(&pprof->dev); - sysfs_notify(acpi_kobj, NULL, "platform_profile"); - sysfs_update_group(acpi_kobj, &platform_profile_group); + platform_profile_legacy_notify(); + platform_profile_legacy_update_group(); } EXPORT_SYMBOL_GPL(platform_profile_remove); @@ -690,23 +705,26 @@ static int __init platform_profile_init(void) { int err; - if (acpi_disabled) - return -EOPNOTSUPP; - err = class_register(&platform_profile_class); if (err) return err; + if (acpi_disabled || !acpi_kobj) + return 0; + err = sysfs_create_group(acpi_kobj, &platform_profile_group); if (err) class_unregister(&platform_profile_class); + else + platform_profile_legacy_registered = true; return err; } static void __exit platform_profile_exit(void) { - sysfs_remove_group(acpi_kobj, &platform_profile_group); + if (platform_profile_legacy_registered) + sysfs_remove_group(acpi_kobj, &platform_profile_group); class_unregister(&platform_profile_class); } module_init(platform_profile_init); diff --git a/drivers/platform/surface/surface_aggregator_registry.c b/drivers/platform/surface/surface_aggregator_registry.c index d37a7e4c6b02a6..c482cc06ca5ff9 100644 --- a/drivers/platform/surface/surface_aggregator_registry.c +++ b/drivers/platform/surface/surface_aggregator_registry.c @@ -89,6 +89,19 @@ static const struct software_node ssam_node_tmp_perf_profile_with_fan = { .properties = ssam_node_tmp_perf_profile_has_fan, }; +static const struct property_entry ssam_node_tmp_perf_profile_sp11_props[] = { + PROPERTY_ENTRY_BOOL("has_fan"), + PROPERTY_ENTRY_BOOL("default-low-power"), + PROPERTY_ENTRY_U32("low-power-max-frequency-khz", 2515000), + { } +}; + +static const struct software_node ssam_node_tmp_perf_profile_sp11 = { + .name = "ssam:01:03:01:00:01", + .parent = &ssam_node_root, + .properties = ssam_node_tmp_perf_profile_sp11_props, +}; + /* Thermal sensors. */ static const struct software_node ssam_node_tmp_sensors = { .name = "ssam:01:03:01:00:02", @@ -410,6 +423,8 @@ static const struct software_node *ssam_node_group_sp11[] = { &ssam_node_hub_kip, &ssam_node_bat_ac, &ssam_node_bat_main, + &ssam_node_tmp_perf_profile_sp11, + &ssam_node_fan_speed, &ssam_node_tmp_sensors, &ssam_node_hid_kip_keyboard, &ssam_node_hid_kip_penstash, diff --git a/drivers/platform/surface/surface_platform_profile.c b/drivers/platform/surface/surface_platform_profile.c index 0e479e35e66e16..aa5eba8f95d7fd 100644 --- a/drivers/platform/surface/surface_platform_profile.c +++ b/drivers/platform/surface/surface_platform_profile.c @@ -6,11 +6,17 @@ * Copyright (C) 2021-2022 Maximilian Luz */ -#include +#include +#include +#include #include #include +#include #include +#include +#include #include +#include #include @@ -41,6 +47,26 @@ struct ssam_tmp_profile_info { struct ssam_platform_profile_device { struct ssam_device *sdev; struct device *ppdev; + struct mutex profile_lock; /* Protects TMP, fan, and frequency transitions. */ + bool has_freq_cap; +#if IS_ENABLED(CONFIG_CPU_FREQ) + struct cpufreq_policy **cpufreq_policies; + struct freq_qos_request *freq_qos_reqs; + unsigned int *freq_cap_khz; + unsigned int low_power_max_freq_khz; + struct mutex freq_qos_lock; /* Protects frequency QoS state. */ + struct hlist_node cpuhp_node; + enum cpuhp_state cpuhp_state; + struct notifier_block cpufreq_nb; + int freq_qos_setup_status; + bool accept_policies; + bool cpuhp_instance_registered; + bool cpuhp_replaying; + bool cpuhp_state_registered; + bool cpufreq_notifier_registered; + bool freq_capped; + bool freq_qos_enabled; +#endif bool has_fan; }; @@ -88,6 +114,121 @@ static int ssam_fan_profile_set(struct ssam_device *sdev, enum ssam_fan_profile return ssam_retry(__ssam_fan_profile_set, sdev->ctrl, &profile); } +#if IS_ENABLED(CONFIG_CPU_FREQ) +static unsigned int ssam_find_cap_freq(const struct cpufreq_policy *policy, + unsigned int ceiling_khz) +{ + struct cpufreq_frequency_table *entry; + unsigned int cap_freq = 0; + + if (!policy->freq_table) + return ceiling_khz; + + cpufreq_for_each_valid_entry(entry, policy->freq_table) { + if (entry->frequency <= ceiling_khz && + entry->frequency > cap_freq) + cap_freq = entry->frequency; + } + + return cap_freq ?: ceiling_khz; +} + +static unsigned int +ssam_platform_profile_cap_freq(struct ssam_platform_profile_device *tpd, + struct cpufreq_policy *policy) +{ + unsigned int max_freq; + + down_read(&policy->rwsem); + max_freq = ssam_find_cap_freq(policy, tpd->low_power_max_freq_khz); + up_read(&policy->rwsem); + + return max_freq; +} + +static int +ssam_platform_profile_apply_freq_cap(struct ssam_platform_profile_device *tpd, + enum platform_profile_option profile) +{ + struct cpufreq_policy *policy; + bool capped; + int i, status; + + if (!tpd->freq_qos_enabled) + return 0; + + switch (profile) { + case PLATFORM_PROFILE_LOW_POWER: + capped = true; + break; + case PLATFORM_PROFILE_BALANCED: + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + case PLATFORM_PROFILE_PERFORMANCE: + capped = false; + break; + default: + return -EOPNOTSUPP; + } + + mutex_lock(&tpd->freq_qos_lock); + + if (tpd->freq_capped == capped) { + mutex_unlock(&tpd->freq_qos_lock); + return 0; + } + + for (i = 0; i < num_possible_cpus(); i++) { + unsigned int max_freq; + + policy = tpd->cpufreq_policies[i]; + if (!policy) + continue; + + max_freq = capped ? tpd->freq_cap_khz[i] : + FREQ_QOS_MAX_DEFAULT_VALUE; + status = freq_qos_update_request(&tpd->freq_qos_reqs[i], + max_freq); + if (status < 0) + goto rollback; + } + + tpd->freq_capped = capped; + mutex_unlock(&tpd->freq_qos_lock); + return 0; + +rollback: + dev_err(&tpd->sdev->dev, + "failed to update CPU frequency cap for policy %u: %d\n", + policy->cpu, status); + + while (--i >= 0) { + unsigned int max_freq; + + policy = tpd->cpufreq_policies[i]; + if (!policy) + continue; + + max_freq = tpd->freq_capped ? tpd->freq_cap_khz[i] : + FREQ_QOS_MAX_DEFAULT_VALUE; + if (freq_qos_update_request(&tpd->freq_qos_reqs[i], + max_freq) < 0) + dev_warn(&tpd->sdev->dev, + "failed to roll back CPU frequency cap for policy %u\n", + policy->cpu); + } + + mutex_unlock(&tpd->freq_qos_lock); + return status; +} +#else +static int +ssam_platform_profile_apply_freq_cap(struct ssam_platform_profile_device *tpd, + enum platform_profile_option profile) +{ + return 0; +} +#endif + static int convert_ssam_tmp_to_profile(struct ssam_device *sdev, enum ssam_tmp_profile p) { switch (p) { @@ -109,7 +250,6 @@ static int convert_ssam_tmp_to_profile(struct ssam_device *sdev, enum ssam_tmp_p } } - static int convert_profile_to_ssam_tmp(struct ssam_device *sdev, enum platform_profile_option p) { switch (p) { @@ -154,15 +294,13 @@ static int convert_profile_to_ssam_fan(struct ssam_device *sdev, enum platform_p } } -static int ssam_platform_profile_get(struct device *dev, - enum platform_profile_option *profile) +static int +ssam_platform_profile_get_internal(struct ssam_platform_profile_device *tpd, + enum platform_profile_option *profile) { - struct ssam_platform_profile_device *tpd; enum ssam_tmp_profile tp; int status; - tpd = dev_get_drvdata(dev); - status = ssam_tmp_profile_get(tpd->sdev, &tp); if (status) return status; @@ -175,30 +313,166 @@ static int ssam_platform_profile_get(struct device *dev, return 0; } -static int ssam_platform_profile_set(struct device *dev, - enum platform_profile_option profile) +static int ssam_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) { - struct ssam_platform_profile_device *tpd; - int tp; + struct ssam_platform_profile_device *tpd = dev_get_drvdata(dev); + int status; + + mutex_lock(&tpd->profile_lock); + status = ssam_platform_profile_get_internal(tpd, profile); + mutex_unlock(&tpd->profile_lock); + + return status; +} + +static void +ssam_platform_profile_reconcile(struct ssam_platform_profile_device *tpd, + enum platform_profile_option old_profile, + enum platform_profile_option target_profile, + int transition_status) +{ + enum platform_profile_option current_profile; + bool fan_uncertain = false; + int fan_profile; + int status; + + status = ssam_platform_profile_get_internal(tpd, ¤t_profile); + if (status) { + dev_err(&tpd->sdev->dev, + "profile transition failed (%d) and TMP readback failed: %d\n", + transition_status, status); + + if (old_profile == PLATFORM_PROFILE_LOW_POWER || + target_profile == PLATFORM_PROFILE_LOW_POWER) { + status = ssam_platform_profile_apply_freq_cap(tpd, + PLATFORM_PROFILE_LOW_POWER); + if (status) + dev_err(&tpd->sdev->dev, + "failed to retain the conservative CPU cap: %d\n", + status); + } + return; + } + + if (tpd->has_fan) { + fan_profile = convert_profile_to_ssam_fan(tpd->sdev, current_profile); + if (fan_profile >= 0) { + status = ssam_fan_profile_set(tpd->sdev, fan_profile); + if (status) { + dev_warn(&tpd->sdev->dev, + "failed to reconcile fan profile: %d\n", + status); + fan_uncertain = true; + } + } + } + + if (fan_uncertain && + (old_profile == PLATFORM_PROFILE_LOW_POWER || + target_profile == PLATFORM_PROFILE_LOW_POWER)) + current_profile = PLATFORM_PROFILE_LOW_POWER; + + status = ssam_platform_profile_apply_freq_cap(tpd, current_profile); + if (status) + dev_err(&tpd->sdev->dev, + "failed to reconcile the CPU frequency cap: %d\n", + status); +} + +static int +ssam_platform_profile_set_legacy(struct ssam_platform_profile_device *tpd, + enum platform_profile_option profile) +{ + int fan_profile; + int tmp_profile; + int status; + + tmp_profile = convert_profile_to_ssam_tmp(tpd->sdev, profile); + if (tmp_profile < 0) + return tmp_profile; + + status = ssam_tmp_profile_set(tpd->sdev, tmp_profile); + if (status) + return status; + + if (!tpd->has_fan) + return 0; + + fan_profile = convert_profile_to_ssam_fan(tpd->sdev, profile); + if (fan_profile < 0) + return fan_profile; + + return ssam_fan_profile_set(tpd->sdev, fan_profile); +} + +static int +ssam_platform_profile_set_internal(struct ssam_platform_profile_device *tpd, + enum platform_profile_option profile) +{ + enum platform_profile_option old_profile; + int fan_profile; + int tmp_profile; + int status; + + if (!tpd->has_freq_cap) + return ssam_platform_profile_set_legacy(tpd, profile); - tpd = dev_get_drvdata(dev); + status = ssam_platform_profile_get_internal(tpd, &old_profile); + if (status) + return status; - tp = convert_profile_to_ssam_tmp(tpd->sdev, profile); - if (tp < 0) - return tp; + tmp_profile = convert_profile_to_ssam_tmp(tpd->sdev, profile); + if (tmp_profile < 0) + return tmp_profile; - tp = ssam_tmp_profile_set(tpd->sdev, tp); - if (tp < 0) - return tp; + fan_profile = convert_profile_to_ssam_fan(tpd->sdev, profile); + if (fan_profile < 0) + return fan_profile; + + /* Enter low power only after every tracked CPU policy is capped. */ + if (profile == PLATFORM_PROFILE_LOW_POWER) { + status = ssam_platform_profile_apply_freq_cap(tpd, profile); + if (status) + return status; + } if (tpd->has_fan) { - tp = convert_profile_to_ssam_fan(tpd->sdev, profile); - if (tp < 0) - return tp; - tp = ssam_fan_profile_set(tpd->sdev, tp); + status = ssam_fan_profile_set(tpd->sdev, fan_profile); + if (status) + goto reconcile; + } + + /* TMP is authoritative and is therefore the firmware commit point. */ + status = ssam_tmp_profile_set(tpd->sdev, tmp_profile); + if (status) + goto reconcile; + + /* Keep the cap until TMP has committed a non-low-power profile. */ + if (profile != PLATFORM_PROFILE_LOW_POWER) { + status = ssam_platform_profile_apply_freq_cap(tpd, profile); + if (status) + goto reconcile; } - return tp; + return 0; + +reconcile: + ssam_platform_profile_reconcile(tpd, old_profile, profile, status); + return status; +} + +static int ssam_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct ssam_platform_profile_device *tpd = dev_get_drvdata(dev); + int status; + + mutex_lock(&tpd->profile_lock); + status = ssam_platform_profile_set_internal(tpd, profile); + mutex_unlock(&tpd->profile_lock); + + return status; } static int ssam_platform_profile_probe(void *drvdata, unsigned long *choices) @@ -217,9 +491,374 @@ static const struct platform_profile_ops ssam_platform_profile_ops = { .profile_set = ssam_platform_profile_set, }; +#if IS_ENABLED(CONFIG_CPU_FREQ) +static int +ssam_platform_profile_add_policy_locked(struct ssam_platform_profile_device *tpd, + struct cpufreq_policy *policy, + unsigned int cap_freq) +{ + unsigned int max_freq; + int free_slot = -1; + int i; + int status; + + lockdep_assert_held(&tpd->freq_qos_lock); + + if (!tpd->accept_policies) + return -ESHUTDOWN; + + for (i = 0; i < num_possible_cpus(); i++) { + if (tpd->cpufreq_policies[i] == policy) + return 0; + + if (!tpd->cpufreq_policies[i] && free_slot < 0) + free_slot = i; + } + + if (free_slot < 0) + return -ENOSPC; + + max_freq = tpd->freq_capped ? cap_freq : + FREQ_QOS_MAX_DEFAULT_VALUE; + status = freq_qos_add_request(&policy->constraints, + &tpd->freq_qos_reqs[free_slot], + FREQ_QOS_MAX, max_freq); + if (status < 0) + return status; + + /* CPUFREQ_REMOVE_POLICY synchronously clears this raw pointer. */ + tpd->cpufreq_policies[free_slot] = policy; + tpd->freq_cap_khz[free_slot] = cap_freq; + + return 0; +} + +static void +ssam_platform_profile_remove_policy_locked(struct ssam_platform_profile_device *tpd, + struct cpufreq_policy *policy) +{ + int i; + + lockdep_assert_held(&tpd->freq_qos_lock); + + for (i = 0; i < num_possible_cpus(); i++) { + if (tpd->cpufreq_policies[i] != policy) + continue; + + if (freq_qos_request_active(&tpd->freq_qos_reqs[i])) + freq_qos_remove_request(&tpd->freq_qos_reqs[i]); + tpd->cpufreq_policies[i] = NULL; + tpd->freq_cap_khz[i] = 0; + break; + } +} + +static void +ssam_platform_profile_remove_all_policies_locked(struct ssam_platform_profile_device *tpd) +{ + int i; + + lockdep_assert_held(&tpd->freq_qos_lock); + + for (i = 0; i < num_possible_cpus(); i++) { + if (!tpd->cpufreq_policies[i]) + continue; + + if (freq_qos_request_active(&tpd->freq_qos_reqs[i])) + freq_qos_remove_request(&tpd->freq_qos_reqs[i]); + tpd->cpufreq_policies[i] = NULL; + tpd->freq_cap_khz[i] = 0; + } + + tpd->freq_capped = false; +} + +static int +ssam_platform_profile_add_active_cpu(struct ssam_platform_profile_device *tpd, + unsigned int cpu) +{ + struct cpufreq_policy *validation; + struct cpufreq_policy *policy; + unsigned int cap_freq; + int status; + + policy = cpufreq_cpu_get(cpu); + if (!policy) + return -ENODEV; + + cap_freq = ssam_platform_profile_cap_freq(tpd, policy); + + mutex_lock(&tpd->freq_qos_lock); + validation = cpufreq_cpu_get(cpu); + if (validation != policy) { + status = -ENODEV; + } else { + status = ssam_platform_profile_add_policy_locked(tpd, policy, + cap_freq); + } + mutex_unlock(&tpd->freq_qos_lock); + + if (validation) + cpufreq_cpu_put(validation); + cpufreq_cpu_put(policy); + + return status; +} + +static int +ssam_platform_profile_add_hotplug_cpu(struct ssam_platform_profile_device *tpd, + unsigned int cpu) +{ + struct cpufreq_policy *policy; + unsigned int cap_freq; + int status; + + /* CPU hotplug serialization keeps this inactive policy alive. */ + policy = cpufreq_cpu_policy(cpu); + if (!policy) + return -ENODEV; + + cap_freq = ssam_platform_profile_cap_freq(tpd, policy); + + mutex_lock(&tpd->freq_qos_lock); + status = ssam_platform_profile_add_policy_locked(tpd, policy, + cap_freq); + mutex_unlock(&tpd->freq_qos_lock); + + return status; +} + +static int +ssam_platform_profile_cpuhp_online(unsigned int cpu, + struct hlist_node *node) +{ + struct ssam_platform_profile_device *tpd = + hlist_entry(node, struct ssam_platform_profile_device, cpuhp_node); + int status; + + status = ssam_platform_profile_add_active_cpu(tpd, cpu); + if (status == -ENODEV && !READ_ONCE(tpd->cpuhp_replaying)) + status = ssam_platform_profile_add_hotplug_cpu(tpd, cpu); + + if (status == -ENODEV || status == -ESHUTDOWN) + return 0; + + if (status) { + dev_err(&tpd->sdev->dev, + "failed to add CPU frequency QoS request for CPU%u: %d\n", + cpu, status); + if (READ_ONCE(tpd->cpuhp_replaying) && + !tpd->freq_qos_setup_status) + tpd->freq_qos_setup_status = status; + } + + /* A CPUHP startup callback must not leave a partially replayed setup. */ + return 0; +} + +static int +ssam_platform_profile_cpufreq_event(struct notifier_block *nb, + unsigned long event, void *data) +{ + struct ssam_platform_profile_device *tpd = + container_of(nb, struct ssam_platform_profile_device, cpufreq_nb); + struct cpufreq_policy *policy = data; + unsigned int cap_freq; + unsigned int cpu; + int status; + + switch (event) { + case CPUFREQ_CREATE_POLICY: + /* The cpufreq core holds policy->rwsem for write here. */ + cap_freq = ssam_find_cap_freq(policy, + tpd->low_power_max_freq_khz); + cpu = policy->cpu; + mutex_lock(&tpd->freq_qos_lock); + status = ssam_platform_profile_add_policy_locked(tpd, policy, + cap_freq); + mutex_unlock(&tpd->freq_qos_lock); + if (status < 0 && status != -ESHUTDOWN) + dev_err(&tpd->sdev->dev, + "failed to add CPU frequency QoS request for policy %u: %d\n", + cpu, status); + break; + + case CPUFREQ_REMOVE_POLICY: + mutex_lock(&tpd->freq_qos_lock); + ssam_platform_profile_remove_policy_locked(tpd, policy); + mutex_unlock(&tpd->freq_qos_lock); + break; + } + + return NOTIFY_OK; +} + +static void ssam_platform_profile_remove_qos(void *data) +{ + struct ssam_platform_profile_device *tpd = data; + + mutex_lock(&tpd->freq_qos_lock); + tpd->accept_policies = false; + mutex_unlock(&tpd->freq_qos_lock); + + if (tpd->cpuhp_instance_registered) { + cpuhp_state_remove_instance_nocalls(tpd->cpuhp_state, + &tpd->cpuhp_node); + tpd->cpuhp_instance_registered = false; + } + if (tpd->cpuhp_state_registered) { + cpuhp_remove_multi_state(tpd->cpuhp_state); + tpd->cpuhp_state_registered = false; + } + + /* Keep REMOVE notifications live until no raw policy pointer remains. */ + mutex_lock(&tpd->freq_qos_lock); + ssam_platform_profile_remove_all_policies_locked(tpd); + tpd->freq_qos_enabled = false; + mutex_unlock(&tpd->freq_qos_lock); + + if (tpd->cpufreq_notifier_registered) { + cpufreq_unregister_notifier(&tpd->cpufreq_nb, + CPUFREQ_POLICY_NOTIFIER); + tpd->cpufreq_notifier_registered = false; + } +} + +static int +ssam_platform_profile_add_freq_qos(struct ssam_platform_profile_device *tpd, + unsigned int freq_cap) +{ + struct device *dev = &tpd->sdev->dev; + size_t policy_count = num_possible_cpus(); + unsigned int cpu; + int status; + + tpd->cpufreq_policies = devm_kcalloc(dev, policy_count, + sizeof(*tpd->cpufreq_policies), + GFP_KERNEL); + if (!tpd->cpufreq_policies) + return -ENOMEM; + + tpd->freq_qos_reqs = devm_kcalloc(dev, policy_count, + sizeof(*tpd->freq_qos_reqs), + GFP_KERNEL); + if (!tpd->freq_qos_reqs) + return -ENOMEM; + + tpd->freq_cap_khz = devm_kcalloc(dev, policy_count, + sizeof(*tpd->freq_cap_khz), + GFP_KERNEL); + if (!tpd->freq_cap_khz) + return -ENOMEM; + + mutex_init(&tpd->freq_qos_lock); + tpd->low_power_max_freq_khz = freq_cap; + tpd->cpufreq_nb.notifier_call = ssam_platform_profile_cpufreq_event; + + status = cpufreq_register_notifier(&tpd->cpufreq_nb, + CPUFREQ_POLICY_NOTIFIER); + if (status) { + dev_warn(dev, + "CPU frequency control unavailable; low-power profile will not cap CPUs: %d\n", + status); + return 0; + } + tpd->cpufreq_notifier_registered = true; + + mutex_lock(&tpd->freq_qos_lock); + tpd->accept_policies = true; + mutex_unlock(&tpd->freq_qos_lock); + + status = cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN, + "surface/platform-profile:online", + ssam_platform_profile_cpuhp_online, + NULL); + if (status < 0) + goto disable_cap; + tpd->cpuhp_state = status; + tpd->cpuhp_state_registered = true; + + WRITE_ONCE(tpd->cpuhp_replaying, true); + status = cpuhp_state_add_instance(tpd->cpuhp_state, &tpd->cpuhp_node); + WRITE_ONCE(tpd->cpuhp_replaying, false); + if (status) + goto disable_cap; + tpd->cpuhp_instance_registered = true; + + /* + * Close the interval between add_instance() dropping its CPU read lock + * and cpuhp_replaying becoming false. A CPU that crossed our callback + * in that interval either appears in this scan or is caught on its next + * online transition. + */ + cpus_read_lock(); + for_each_online_cpu(cpu) { + status = ssam_platform_profile_add_active_cpu(tpd, cpu); + if (status == -ENODEV || status == -ESHUTDOWN) + continue; + if (status) { + dev_err(dev, + "failed to add CPU frequency QoS request for CPU%u: %d\n", + cpu, status); + if (!tpd->freq_qos_setup_status) + tpd->freq_qos_setup_status = status; + } + } + cpus_read_unlock(); + + if (tpd->freq_qos_setup_status) { + status = tpd->freq_qos_setup_status; + goto disable_cap; + } + + mutex_lock(&tpd->freq_qos_lock); + tpd->freq_qos_enabled = true; + mutex_unlock(&tpd->freq_qos_lock); + + return 0; + +disable_cap: + dev_warn(dev, + "CPU frequency cap setup failed; platform profiles remain available: %d\n", + status); + ssam_platform_profile_remove_qos(tpd); + return 0; +} +#else +static bool +ssam_platform_profile_freq_qos_enabled(struct ssam_platform_profile_device *tpd) +{ + return false; +} + +static int +ssam_platform_profile_add_freq_qos(struct ssam_platform_profile_device *tpd, + unsigned int freq_cap) +{ + dev_warn(&tpd->sdev->dev, + "CPU frequency control is disabled; low-power profile will not cap CPUs\n"); + return 0; +} + +static void ssam_platform_profile_remove_qos(void *data) +{ +} +#endif + +#if IS_ENABLED(CONFIG_CPU_FREQ) +static bool +ssam_platform_profile_freq_qos_enabled(struct ssam_platform_profile_device *tpd) +{ + return tpd->freq_qos_enabled; +} +#endif + static int surface_platform_profile_probe(struct ssam_device *sdev) { + const char *freq_property = "low-power-max-frequency-khz"; struct ssam_platform_profile_device *tpd; + unsigned int freq_cap; + int status; tpd = devm_kzalloc(&sdev->dev, sizeof(*tpd), GFP_KERNEL); if (!tpd) @@ -227,13 +866,52 @@ static int surface_platform_profile_probe(struct ssam_device *sdev) tpd->sdev = sdev; ssam_device_set_drvdata(sdev, tpd); + mutex_init(&tpd->profile_lock); tpd->has_fan = device_property_read_bool(&sdev->dev, "has_fan"); - tpd->ppdev = devm_platform_profile_register(&sdev->dev, "Surface Platform Profile", - tpd, &ssam_platform_profile_ops); + if (device_property_present(&sdev->dev, freq_property)) { + status = device_property_read_u32(&sdev->dev, freq_property, &freq_cap); + if (status) + return status; + if (!freq_cap) + return -EINVAL; + + tpd->has_freq_cap = true; + status = ssam_platform_profile_add_freq_qos(tpd, freq_cap); + if (status) + return status; + + if (ssam_platform_profile_freq_qos_enabled(tpd)) { + status = devm_add_action_or_reset(&sdev->dev, + ssam_platform_profile_remove_qos, + tpd); + if (status) + return status; + } + } - return PTR_ERR_OR_ZERO(tpd->ppdev); + tpd->ppdev = devm_platform_profile_register(&sdev->dev, + "Surface Platform Profile", + tpd, + &ssam_platform_profile_ops); + if (IS_ERR(tpd->ppdev)) { + status = PTR_ERR(tpd->ppdev); + return status; + } + + if (device_property_read_bool(&sdev->dev, "default-low-power")) { + mutex_lock(&tpd->profile_lock); + status = ssam_platform_profile_set_internal(tpd, + PLATFORM_PROFILE_LOW_POWER); + mutex_unlock(&tpd->profile_lock); + if (status) + dev_warn(&sdev->dev, + "failed to select the default low-power profile: %d\n", + status); + } + + return 0; } static const struct ssam_device_id ssam_platform_profile_match[] = {