From 3953c459a0c63176bce0dcc5ef42d8486a60702e Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 05:04:25 +0100 Subject: [PATCH 1/3] ACPI: platform_profile: keep class interface without ACPI Device-tree systems can use platform-profile providers even when ACPI is disabled, but the framework currently declines to register its class in that case. Register the class unconditionally and create the legacy ACPI attribute group only after ACPI has initialized successfully. Record that group registration and use it to guard legacy notifications and teardown. This keeps the class path independent of ACPI while also avoiding a stale acpi_kobj after an ACPI initialization failure. This exposes the native /sys/class/platform-profile/ interface without creating a synthetic /sys/firmware/acpi hierarchy. Existing ACPI systems retain the legacy aggregate attributes unchanged. This is a contained rework of the original SP11 non-ACPI enablement. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/106ad0d2cd9e0fea88c7281de245424d3aff8910 Signed-off-by: Leon Silcott --- drivers/acpi/platform_profile.c | 42 +++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 12 deletions(-) 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); From e42af85ab36ac186be1fbd48719120e9a08aaff8 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Mon, 10 Aug 2026 08:58:45 +0100 Subject: [PATCH 2/3] platform/surface: add profile and fan nodes for SP11 The Surface Pro 11 has active cooling and supports SAM performance profiles, but its registry group does not instantiate the existing platform-profile and fan-speed devices. Add the existing fan-capable profile node and fan-speed node only to the Denali software-node group. Other Surface device groups are unchanged. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/5c513ff791fd968fc5965d9be780674f1014a5a8 Signed-off-by: Leon Silcott --- drivers/platform/surface/surface_aggregator_registry.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/platform/surface/surface_aggregator_registry.c b/drivers/platform/surface/surface_aggregator_registry.c index d37a7e4c6b02a6..59049a597602bf 100644 --- a/drivers/platform/surface/surface_aggregator_registry.c +++ b/drivers/platform/surface/surface_aggregator_registry.c @@ -410,6 +410,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_with_fan, + &ssam_node_fan_speed, &ssam_node_tmp_sensors, &ssam_node_hid_kip_keyboard, &ssam_node_hid_kip_penstash, From 47f32837229ecfacabcfa305c446be9fd781f422 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sat, 22 Aug 2026 09:59:22 +0100 Subject: [PATCH 3/3] platform/surface: gate low-power CPU cap to SP11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Surface Pro 11 needs a lower CPU-frequency ceiling in its low-power profile to match the validated cool-running baseline. Describe the 2.515 GHz ceiling and default low-power selection on an SP11-only software node. Set up frequency-QoS only when that per-device property is present. Devices without it retain the legacy TMP-then-fan write order, including writes that repeat the current profile. Track cpufreq policies through CREATE and REMOVE notifications plus a per-device CPU-hotplug state, so a policy that is fully offline at probe is capped when a CPU returns. Hold policy references while attaching requests, clear stored raw pointers synchronously on REMOVE, and stop hotplug callbacks before tearing down requests and the notifier. When entering low power, apply the cap before fan and TMP changes; when leaving, restore fan and TMP before dropping the cap. If a SAM write fails, read TMP back and reconcile fan and cap conservatively. An unavailable cpufreq subsystem degrades to profile-only operation. Omit the staging implementation’s no-op power-supply notifier. This is a contained rework of the original SP11 low-power implementation. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/30972bdca0879e8f33adcfd620b8f9dd09755ac7 Signed-off-by: Leon Silcott --- .../surface/surface_aggregator_registry.c | 15 +- .../surface/surface_platform_profile.c | 730 +++++++++++++++++- 2 files changed, 718 insertions(+), 27 deletions(-) diff --git a/drivers/platform/surface/surface_aggregator_registry.c b/drivers/platform/surface/surface_aggregator_registry.c index 59049a597602bf..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,7 +423,7 @@ 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_with_fan, + &ssam_node_tmp_perf_profile_sp11, &ssam_node_fan_speed, &ssam_node_tmp_sensors, &ssam_node_hid_kip_keyboard, 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[] = {