[PATCH] ACPI: CPPC: Order performance-control updates
From: Christian Loehle
Date: Thu Sep 17 2026 - 18:14:36 EST
The ACPI specification requires Minimum Performance to remain below
Maximum Performance. When autonomous selection is disabled, Desired
Performance must also remain within those limits.
cppc_set_perf() writes the three controls in a fixed order. This can expose
an invalid tuple after firmware resets the controls or while policy bounds
move across the current desired value.
Read the live limits. Apply non-atomic updates in three phases: widen the
interval, update Desired Performance, then narrow it. Submit PCC controls
in each phase together. Keep the existing single-command path when the
complete tuple is in PCC, reject invalid requests, and stop after the first
failed access.
Make cppc_cpufreq fast switches include the limits only when they differ
from the last successful request. Invalidate the cache across resume and
after a failed request, since firmware may have reset the controls or an
earlier phase of a failed update may have reached the platform. Publish
slow-path limits only after the complete request succeeds.
At policy teardown, lower Minimum Performance together with Desired
Performance so the final request remains valid. On initialization, clamp
Desired Performance to the live interval left by firmware or an earlier
driver instance.
Fixes: 76531df5e13b ("ACPI: CPPC: Add min and max perf register writing support")
Signed-off-by: Christian Loehle <christian.loehle@xxxxxxx>
---
drivers/acpi/cppc_acpi.c | 279 +++++++++++++++++++++++++--------
drivers/cpufreq/cppc_cpufreq.c | 84 ++++++++--
include/acpi/cppc_acpi.h | 1 +
3 files changed, 288 insertions(+), 76 deletions(-)
diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c
index 80e2e6b32ce3..4f68ff3d8555 100644
--- a/drivers/acpi/cppc_acpi.c
+++ b/drivers/acpi/cppc_acpi.c
@@ -3471,11 +3471,200 @@ int cppc_get_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls)
}
EXPORT_SYMBOL_GPL(cppc_get_perf);
+enum cppc_perf_update {
+ CPPC_UPDATE_DESIRED = BIT(0),
+ CPPC_UPDATE_MIN = BIT(1),
+ CPPC_UPDATE_MAX = BIT(2),
+};
+
+/* The caller holds pcc_lock for write if either limit is in PCC. */
+static int cppc_read_perf_limits(int cpu, u32 *min_perf, u32 *max_perf)
+{
+ struct cpc_desc *cpc_desc = per_cpu(cpc_desc_ptr, cpu);
+ struct cpc_register_resource *min_reg = &cpc_desc->cpc_regs[MIN_PERF];
+ struct cpc_register_resource *max_reg = &cpc_desc->cpc_regs[MAX_PERF];
+ u64 min, max;
+ int ret;
+
+ if (!cpc_is_readable(min_reg) || !cpc_is_readable(max_reg))
+ return -EOPNOTSUPP;
+
+ /* Firmware may have reset the controls, so read the live limits. */
+ if (CPC_IN_PCC(min_reg) || CPC_IN_PCC(max_reg)) {
+ ret = send_pcc_cmd(per_cpu(cpu_pcc_subspace_idx, cpu), CMD_READ);
+ if (ret)
+ return ret;
+ }
+
+ ret = cpc_read(cpu, min_reg, &min);
+ if (ret)
+ return ret;
+ ret = cpc_read(cpu, max_reg, &max);
+ if (ret)
+ return ret;
+ if (min > U32_MAX || max > U32_MAX)
+ return -EFAULT;
+ if (min > max)
+ return -EINVAL;
+
+ *min_perf = min;
+ *max_perf = max;
+ return 0;
+}
+
+static bool cppc_perf_updates_in_pcc(int cpu, unsigned int updates)
+{
+ struct cpc_desc *cpc_desc = per_cpu(cpc_desc_ptr, cpu);
+
+ return ((updates & CPPC_UPDATE_DESIRED) &&
+ CPC_IN_PCC(&cpc_desc->cpc_regs[DESIRED_PERF])) ||
+ ((updates & CPPC_UPDATE_MIN) &&
+ CPC_IN_PCC(&cpc_desc->cpc_regs[MIN_PERF])) ||
+ ((updates & CPPC_UPDATE_MAX) &&
+ CPC_IN_PCC(&cpc_desc->cpc_regs[MAX_PERF]));
+}
+
+/* The caller holds pcc_lock for write if an updated control is in PCC. */
+static int cppc_write_perf_controls(int cpu,
+ struct cppc_perf_ctrls *perf_ctrls,
+ unsigned int updates, bool sync_desired)
+{
+ struct cpc_desc *cpc_desc = per_cpu(cpc_desc_ptr, cpu);
+ struct cpc_register_resource *desired_reg =
+ &cpc_desc->cpc_regs[DESIRED_PERF];
+ struct cpc_register_resource *min_reg = &cpc_desc->cpc_regs[MIN_PERF];
+ struct cpc_register_resource *max_reg = &cpc_desc->cpc_regs[MAX_PERF];
+ struct cppc_pcc_data *pcc_ss_data;
+ int pcc_ss_id, ret;
+ bool pcc_update = false;
+
+ /* Direct writes precede the PCC command in each ordered phase. */
+ if ((updates & CPPC_UPDATE_DESIRED) && !CPC_IN_PCC(desired_reg)) {
+ ret = cpc_write(cpu, desired_reg, perf_ctrls->desired_perf);
+ if (ret)
+ return ret;
+ }
+ if ((updates & CPPC_UPDATE_MIN) && !CPC_IN_PCC(min_reg)) {
+ ret = cpc_write(cpu, min_reg, perf_ctrls->min_perf);
+ if (ret)
+ return ret;
+ }
+ if ((updates & CPPC_UPDATE_MAX) && !CPC_IN_PCC(max_reg)) {
+ ret = cpc_write(cpu, max_reg, perf_ctrls->max_perf);
+ if (ret)
+ return ret;
+ }
+
+ /* Keep Desired Performance valid in every intermediate PCC command. */
+ if (((updates & CPPC_UPDATE_DESIRED) || sync_desired) &&
+ CPC_IN_PCC(desired_reg)) {
+ ret = cpc_write(cpu, desired_reg, perf_ctrls->desired_perf);
+ if (ret)
+ return ret;
+ pcc_update = true;
+ }
+ if ((updates & CPPC_UPDATE_MIN) && CPC_IN_PCC(min_reg)) {
+ ret = cpc_write(cpu, min_reg, perf_ctrls->min_perf);
+ if (ret)
+ return ret;
+ pcc_update = true;
+ }
+ if ((updates & CPPC_UPDATE_MAX) && CPC_IN_PCC(max_reg)) {
+ ret = cpc_write(cpu, max_reg, perf_ctrls->max_perf);
+ if (ret)
+ return ret;
+ pcc_update = true;
+ }
+ if (!pcc_update)
+ return 0;
+
+ pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, cpu);
+ pcc_ss_data = pcc_data[pcc_ss_id];
+ WRITE_ONCE(pcc_ss_data->pending_pcc_write_cmd, true);
+ cpc_desc->write_cmd_id = pcc_ss_data->pcc_write_cnt;
+ cpc_desc->write_cmd_status = 0;
+ return send_pcc_cmd(pcc_ss_id, CMD_WRITE);
+}
+
+/*
+ * Move from the live interval to the requested interval without exposing an
+ * invalid control tuple: widen, update Desired Performance, then narrow.
+ */
+static int cppc_write_ordered_perf(int cpu,
+ struct cppc_perf_ctrls *perf_ctrls,
+ bool desired_update, bool min_update,
+ bool max_update)
+{
+ struct cpc_desc *cpc_desc = per_cpu(cpc_desc_ptr, cpu);
+ struct cpc_register_resource *desired_reg =
+ &cpc_desc->cpc_regs[DESIRED_PERF];
+ unsigned int widen = 0, narrow = 0;
+ u32 current_min, current_max;
+ u32 target_min, target_max;
+ bool desired_done = false;
+ bool sync_desired;
+ int ret;
+
+ ret = cppc_read_perf_limits(cpu, ¤t_min, ¤t_max);
+ if (ret)
+ return ret;
+
+ target_min = min_update ? perf_ctrls->min_perf : current_min;
+ target_max = max_update ? perf_ctrls->max_perf : current_max;
+ if (target_min > target_max)
+ return -EINVAL;
+ if (desired_update && perf_ctrls->desired_perf &&
+ (perf_ctrls->desired_perf < target_min ||
+ perf_ctrls->desired_perf > target_max))
+ return -EINVAL;
+
+ if (min_update) {
+ if (perf_ctrls->min_perf < current_min)
+ widen |= CPPC_UPDATE_MIN;
+ else
+ narrow |= CPPC_UPDATE_MIN;
+ }
+ if (max_update) {
+ if (perf_ctrls->max_perf > current_max)
+ widen |= CPPC_UPDATE_MAX;
+ else
+ narrow |= CPPC_UPDATE_MAX;
+ }
+
+ if (widen) {
+ sync_desired = desired_update && CPC_IN_PCC(desired_reg) &&
+ cppc_perf_updates_in_pcc(cpu, widen);
+ ret = cppc_write_perf_controls(cpu, perf_ctrls, widen,
+ sync_desired);
+ if (ret)
+ return ret;
+ desired_done = sync_desired;
+ }
+
+ if (desired_update && !desired_done) {
+ ret = cppc_write_perf_controls(cpu, perf_ctrls,
+ CPPC_UPDATE_DESIRED, false);
+ if (ret)
+ return ret;
+ }
+
+ if (!narrow)
+ return 0;
+
+ sync_desired = desired_update && CPC_IN_PCC(desired_reg) &&
+ cppc_perf_updates_in_pcc(cpu, narrow);
+ return cppc_write_perf_controls(cpu, perf_ctrls, narrow, sync_desired);
+}
+
/**
* cppc_set_perf - Set a CPU's performance controls.
* @cpu: CPU for which to set performance controls.
* @perf_ctrls: ptr to cppc_perf_ctrls. See cppc_acpi.h
*
+ * Callers must serialize updates to the same CPU's performance controls.
+ * Limit updates preserve MIN <= MAX, including intermediate states on
+ * directly accessed registers. An error may leave an earlier write applied.
+ *
* Return: 0 for success, -ERRNO otherwise.
*/
int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls)
@@ -3502,6 +3691,15 @@ int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls)
(perf_ctrls->min_perf || perf_ctrls->min_perf_valid);
max_update = cpc_is_writable(max_perf_reg) &&
perf_ctrls->max_perf;
+ if (min_update && max_update &&
+ perf_ctrls->min_perf > perf_ctrls->max_perf)
+ return -EINVAL;
+ if (desired_update && perf_ctrls->desired_perf &&
+ min_update && max_update &&
+ (perf_ctrls->desired_perf < perf_ctrls->min_perf ||
+ perf_ctrls->desired_perf > perf_ctrls->max_perf))
+ return -EINVAL;
+
desired_pcc = desired_update && CPC_IN_PCC(desired_reg);
min_pcc = min_update && CPC_IN_PCC(min_perf_reg);
max_pcc = max_update && CPC_IN_PCC(max_perf_reg);
@@ -3531,9 +3729,10 @@ int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls)
* A mixed layout cannot batch fallible direct writes safely: another
* CPU's staged PCC values may no longer match if a direct write fails.
* Serialize the complete mixed transaction and drain an older batch
- * before changing a direct control.
+ * before changing a direct control. A single PCC limit update also
+ * needs the exclusive lock to read and check the unchanged limit.
*/
- if (mixed_layout) {
+ if (mixed_layout || (pcc_update && min_update != max_update)) {
down_write(&pcc_ss_data->pcc_lock);
if (pcc_ss_data->pending_pcc_write_cmd) {
ret = send_pcc_cmd(pcc_ss_id, CMD_WRITE);
@@ -3547,50 +3746,14 @@ int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls)
goto out_mixed_unlock;
}
- if (desired_update && !desired_pcc) {
- ret = cpc_write(cpu, desired_reg,
- perf_ctrls->desired_perf);
- if (ret)
- goto out_mixed_unlock;
- }
- if (min_update && !min_pcc) {
- ret = cpc_write(cpu, min_perf_reg,
- perf_ctrls->min_perf);
- if (ret)
- goto out_mixed_unlock;
- }
- if (max_update && !max_pcc) {
- ret = cpc_write(cpu, max_perf_reg,
- perf_ctrls->max_perf);
- if (ret)
- goto out_mixed_unlock;
- }
-
- if (desired_pcc) {
- ret = cpc_write(cpu, desired_reg,
- perf_ctrls->desired_perf);
- if (ret)
- goto out_mixed_unlock;
- }
- if (min_pcc) {
- ret = cpc_write(cpu, min_perf_reg,
- perf_ctrls->min_perf);
- if (ret)
- goto out_mixed_unlock;
- }
- if (max_pcc) {
- ret = cpc_write(cpu, max_perf_reg,
- perf_ctrls->max_perf);
- if (ret)
- goto out_mixed_unlock;
- }
-
- if (pcc_update) {
- WRITE_ONCE(pcc_ss_data->pending_pcc_write_cmd, true);
- cpc_desc->write_cmd_id = pcc_ss_data->pcc_write_cnt;
- cpc_desc->write_cmd_status = 0;
- ret = send_pcc_cmd(pcc_ss_id, CMD_WRITE);
- }
+ if (min_update || max_update)
+ ret = cppc_write_ordered_perf(cpu, perf_ctrls,
+ desired_update,
+ min_update, max_update);
+ else if (desired_update)
+ ret = cppc_write_perf_controls(cpu, perf_ctrls,
+ CPPC_UPDATE_DESIRED,
+ false);
out_mixed_unlock:
up_write(&pcc_ss_data->pcc_lock);
@@ -3599,22 +3762,14 @@ int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls)
/* A request without PCC updates has no payload to coordinate. */
if (!pcc_update) {
- if (desired_update) {
- ret = cpc_write(cpu, desired_reg,
- perf_ctrls->desired_perf);
- if (ret)
- return ret;
- }
- if (min_update) {
- ret = cpc_write(cpu, min_perf_reg,
- perf_ctrls->min_perf);
- if (ret)
- return ret;
- }
- if (max_update)
- ret = cpc_write(cpu, max_perf_reg,
- perf_ctrls->max_perf);
- return ret;
+ if (min_update || max_update)
+ return cppc_write_ordered_perf(cpu, perf_ctrls,
+ desired_update,
+ min_update, max_update);
+ if (desired_update)
+ return cpc_write(cpu, desired_reg,
+ perf_ctrls->desired_perf);
+ return 0;
}
down_read(&pcc_ss_data->pcc_lock); /* BEGIN Phase-I */
diff --git a/drivers/cpufreq/cppc_cpufreq.c b/drivers/cpufreq/cppc_cpufreq.c
index 09e55bfdca88..5d79e275f79a 100644
--- a/drivers/cpufreq/cppc_cpufreq.c
+++ b/drivers/cpufreq/cppc_cpufreq.c
@@ -335,47 +335,77 @@ static int cppc_cpufreq_set_target(struct cpufreq_policy *policy,
unsigned int relation)
{
struct cppc_cpudata *cpu_data = policy->driver_data;
+ struct cppc_perf_ctrls perf_ctrls = {};
unsigned int cpu = policy->cpu;
struct cpufreq_freqs freqs;
int ret = 0;
- cpu_data->perf_ctrls.desired_perf =
- cppc_khz_to_perf(&cpu_data->perf_caps, target_freq);
- cppc_cpufreq_update_perf_limits(cpu_data, policy);
+ perf_ctrls.desired_perf =
+ cppc_khz_to_perf(&cpu_data->perf_caps, target_freq);
+ cppc_cpufreq_get_perf_limits(cpu_data, policy, &perf_ctrls.min_perf,
+ &perf_ctrls.max_perf);
+ perf_ctrls.min_perf_valid = true;
freqs.old = policy->cur;
freqs.new = target_freq;
cpufreq_freq_transition_begin(policy, &freqs);
- ret = cppc_set_perf(cpu, &cpu_data->perf_ctrls);
+ ret = cppc_set_perf(cpu, &perf_ctrls);
cpufreq_freq_transition_end(policy, &freqs, ret != 0);
- if (ret)
+ if (ret) {
+ WRITE_ONCE(cpu_data->perf_limits_valid, false);
pr_debug("Failed to set target on CPU:%d. ret:%d\n",
cpu, ret);
+ return ret;
+ }
- return ret;
+ WRITE_ONCE(cpu_data->perf_ctrls.desired_perf,
+ perf_ctrls.desired_perf);
+ WRITE_ONCE(cpu_data->perf_ctrls.min_perf, perf_ctrls.min_perf);
+ WRITE_ONCE(cpu_data->perf_ctrls.max_perf, perf_ctrls.max_perf);
+ WRITE_ONCE(cpu_data->perf_limits_valid, true);
+
+ return 0;
}
static unsigned int cppc_cpufreq_fast_switch(struct cpufreq_policy *policy,
unsigned int target_freq)
{
struct cppc_cpudata *cpu_data = policy->driver_data;
+ struct cppc_perf_ctrls perf_ctrls = {};
unsigned int cpu = policy->cpu;
- u32 desired_perf;
+ u32 desired_perf, min_perf, max_perf;
+ bool update_limits;
int ret;
desired_perf = cppc_khz_to_perf(&cpu_data->perf_caps, target_freq);
- cpu_data->perf_ctrls.desired_perf = desired_perf;
- cppc_cpufreq_update_perf_limits(cpu_data, policy);
+ perf_ctrls.desired_perf = desired_perf;
+ cppc_cpufreq_get_perf_limits(cpu_data, policy, &min_perf, &max_perf);
+ update_limits = !READ_ONCE(cpu_data->perf_limits_valid) ||
+ min_perf != READ_ONCE(cpu_data->perf_ctrls.min_perf) ||
+ max_perf != READ_ONCE(cpu_data->perf_ctrls.max_perf);
+ if (update_limits) {
+ perf_ctrls.min_perf = min_perf;
+ perf_ctrls.max_perf = max_perf;
+ perf_ctrls.min_perf_valid = true;
+ }
- ret = cppc_set_perf(cpu, &cpu_data->perf_ctrls);
+ ret = cppc_set_perf(cpu, &perf_ctrls);
if (ret) {
+ WRITE_ONCE(cpu_data->perf_limits_valid, false);
pr_debug("Failed to set target on CPU:%d. ret:%d\n",
cpu, ret);
return 0;
}
+ WRITE_ONCE(cpu_data->perf_ctrls.desired_perf, desired_perf);
+ if (update_limits) {
+ WRITE_ONCE(cpu_data->perf_ctrls.min_perf, min_perf);
+ WRITE_ONCE(cpu_data->perf_ctrls.max_perf, max_perf);
+ WRITE_ONCE(cpu_data->perf_limits_valid, true);
+ }
+
return target_freq;
}
@@ -670,6 +700,7 @@ static int cppc_cpufreq_cpu_init(struct cpufreq_policy *policy)
unsigned int cpu = policy->cpu;
struct cppc_cpudata *cpu_data;
struct cppc_perf_caps *caps;
+ u32 desired_perf;
int ret;
cpu_data = cppc_cpufreq_get_cpu_data(cpu);
@@ -728,16 +759,26 @@ static int cppc_cpufreq_cpu_init(struct cpufreq_policy *policy)
if (caps->highest_perf > caps->nominal_perf)
policy->boost_supported = true;
- /* Set policy->cur to max now. The governors will adjust later. */
- policy->cur = cppc_perf_to_khz(caps, caps->highest_perf);
- cpu_data->perf_ctrls.desired_perf = caps->highest_perf;
+ /*
+ * Keep the initial request within controls left by an earlier driver
+ * instance or firmware. The governor will update the full tuple later.
+ */
+ desired_perf = caps->highest_perf;
+ if (cpu_data->perf_ctrls.max_perf)
+ desired_perf = min(desired_perf, cpu_data->perf_ctrls.max_perf);
+ if (cpu_data->perf_ctrls.min_perf_valid)
+ desired_perf = max(desired_perf, cpu_data->perf_ctrls.min_perf);
+
+ policy->cur = cppc_perf_to_khz(caps, desired_perf);
+ cpu_data->perf_ctrls.desired_perf = desired_perf;
ret = cppc_set_perf(cpu, &cpu_data->perf_ctrls);
if (ret) {
pr_debug("Err setting perf value:%d on CPU:%d. ret:%d\n",
- caps->highest_perf, cpu, ret);
+ desired_perf, cpu, ret);
goto out;
}
+ cpu_data->perf_limits_valid = true;
cppc_cpufreq_cpu_fie_init(policy);
return 0;
@@ -757,6 +798,8 @@ static void cppc_cpufreq_cpu_exit(struct cpufreq_policy *policy)
cppc_cpufreq_cpu_fie_exit(policy);
cpu_data->perf_ctrls.desired_perf = caps->lowest_perf;
+ cpu_data->perf_ctrls.min_perf = caps->lowest_perf;
+ cpu_data->perf_ctrls.min_perf_valid = true;
ret = cppc_set_perf(cpu, &cpu_data->perf_ctrls);
if (ret)
@@ -766,6 +809,16 @@ static void cppc_cpufreq_cpu_exit(struct cpufreq_policy *policy)
cppc_cpufreq_put_cpu_data(policy);
}
+static int cppc_cpufreq_cpu_resume(struct cpufreq_policy *policy)
+{
+ struct cppc_cpudata *cpu_data = policy->driver_data;
+
+ /* The platform may have reset the controls during suspend. */
+ WRITE_ONCE(cpu_data->perf_limits_valid, false);
+
+ return 0;
+}
+
static inline u64 get_delta(u64 t1, u64 t0)
{
if (t1 > t0 || t0 > ~(u32)0)
@@ -922,12 +975,14 @@ static ssize_t store_auto_select(struct cpufreq_policy *policy,
ret = cppc_set_perf(policy->cpu, &cpu_data->perf_ctrls);
if (ret) {
+ WRITE_ONCE(cpu_data->perf_limits_valid, false);
cpu_data->perf_ctrls.min_perf = old_min_perf;
cpu_data->perf_ctrls.max_perf = old_max_perf;
cppc_set_auto_sel(policy->cpu, old_auto_sel);
cpu_data->perf_ctrls.auto_sel = old_auto_sel;
return ret;
}
+ WRITE_ONCE(cpu_data->perf_limits_valid, true);
}
return count;
@@ -1060,6 +1115,7 @@ static struct cpufreq_driver cppc_cpufreq_driver = {
.fast_switch = cppc_cpufreq_fast_switch,
.init = cppc_cpufreq_cpu_init,
.exit = cppc_cpufreq_cpu_exit,
+ .resume = cppc_cpufreq_cpu_resume,
.set_boost = cppc_cpufreq_set_boost,
.attr = cppc_cpufreq_attr,
.name = "cppc_cpufreq",
diff --git a/include/acpi/cppc_acpi.h b/include/acpi/cppc_acpi.h
index 3f0005abac64..fd1e891a045e 100644
--- a/include/acpi/cppc_acpi.h
+++ b/include/acpi/cppc_acpi.h
@@ -165,6 +165,7 @@ struct cppc_cpudata {
struct cppc_perf_fb_ctrs perf_fb_ctrs;
unsigned int shared_type;
cpumask_var_t shared_cpu_map;
+ bool perf_limits_valid;
};
#ifdef CONFIG_ACPI_CPPC_LIB
--
2.34.1