592 lines
15 KiB
C
592 lines
15 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* AMD Processor P-state Frequency Driver Unit Test
|
|
*
|
|
* Copyright (C) 2022 Advanced Micro Devices, Inc. All Rights Reserved.
|
|
*
|
|
* Author: Meng Li <li.meng@amd.com>
|
|
*
|
|
* The AMD P-State Unit Test is a test module for testing the amd-pstate
|
|
* driver. 1) It can help all users to verify their processor support
|
|
* (SBIOS/Firmware or Hardware). 2) Kernel can have a basic function
|
|
* test to avoid the kernel regression during the update. 3) We can
|
|
* introduce more functional or performance tests to align the result
|
|
* together, it will benefit power and performance scale optimization.
|
|
*
|
|
* This driver implements basic framework with plans to enhance it with
|
|
* additional test cases to improve the depth and coverage of the test.
|
|
*
|
|
* See Documentation/admin-guide/pm/amd-pstate.rst Unit Tests for
|
|
* amd-pstate to get more detail.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
|
|
#include <linux/bitfield.h>
|
|
#include <linux/cpufeature.h>
|
|
#include <linux/cpufreq.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/moduleparam.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/cleanup.h>
|
|
|
|
#include <acpi/cppc_acpi.h>
|
|
|
|
#include <asm/msr.h>
|
|
|
|
#include "amd-pstate.h"
|
|
|
|
static char *test_list;
|
|
module_param(test_list, charp, 0444);
|
|
MODULE_PARM_DESC(test_list,
|
|
"Comma-delimited list of tests to run (empty means run all tests)");
|
|
DEFINE_FREE(cleanup_page, void *, if (_T) free_page((unsigned long)_T))
|
|
|
|
struct amd_pstate_ut_struct {
|
|
const char *name;
|
|
int (*func)(u32 index);
|
|
};
|
|
|
|
/*
|
|
* Kernel module for testing the AMD P-State unit test
|
|
*/
|
|
static int amd_pstate_ut_acpi_cpc_valid(u32 index);
|
|
static int amd_pstate_ut_check_enabled(u32 index);
|
|
static int amd_pstate_ut_check_perf(u32 index);
|
|
static int amd_pstate_ut_check_freq(u32 index);
|
|
static int amd_pstate_ut_epp(u32 index);
|
|
static int amd_pstate_ut_check_driver(u32 index);
|
|
static int amd_pstate_ut_check_freq_attrs(u32 index);
|
|
|
|
static struct amd_pstate_ut_struct amd_pstate_ut_cases[] = {
|
|
{"amd_pstate_ut_acpi_cpc_valid", amd_pstate_ut_acpi_cpc_valid },
|
|
{"amd_pstate_ut_check_enabled", amd_pstate_ut_check_enabled },
|
|
{"amd_pstate_ut_check_perf", amd_pstate_ut_check_perf },
|
|
{"amd_pstate_ut_check_freq", amd_pstate_ut_check_freq },
|
|
{"amd_pstate_ut_epp", amd_pstate_ut_epp },
|
|
{"amd_pstate_ut_check_driver", amd_pstate_ut_check_driver },
|
|
{"amd_pstate_ut_check_freq_attrs", amd_pstate_ut_check_freq_attrs },
|
|
};
|
|
|
|
static bool test_in_list(const char *list, const char *name)
|
|
{
|
|
size_t name_len = strlen(name);
|
|
const char *p = list;
|
|
|
|
while (*p) {
|
|
const char *sep = strchr(p, ',');
|
|
size_t token_len = sep ? sep - p : strlen(p);
|
|
|
|
if (token_len == name_len && !strncmp(p, name, token_len))
|
|
return true;
|
|
if (!sep)
|
|
break;
|
|
p = sep + 1;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool get_shared_mem(void)
|
|
{
|
|
bool result = false;
|
|
|
|
if (!boot_cpu_has(X86_FEATURE_CPPC))
|
|
result = true;
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* check the _CPC object is present in SBIOS.
|
|
*/
|
|
static int amd_pstate_ut_acpi_cpc_valid(u32 index)
|
|
{
|
|
if (!acpi_cpc_valid()) {
|
|
pr_err("%s the _CPC object is not present in SBIOS!\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* check if amd pstate is enabled
|
|
*/
|
|
static int amd_pstate_ut_check_enabled(u32 index)
|
|
{
|
|
u64 cppc_enable = 0;
|
|
int ret;
|
|
|
|
if (get_shared_mem())
|
|
return 0;
|
|
|
|
ret = rdmsrq_safe(MSR_AMD_CPPC_ENABLE, &cppc_enable);
|
|
if (ret) {
|
|
pr_err("%s rdmsrq_safe MSR_AMD_CPPC_ENABLE ret=%d error!\n", __func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
if (!cppc_enable) {
|
|
pr_err("%s amd pstate must be enabled!\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* check if performance values are reasonable.
|
|
* highest_perf >= nominal_perf > lowest_nonlinear_perf > lowest_perf > 0
|
|
*/
|
|
static int amd_pstate_ut_check_perf(u32 index)
|
|
{
|
|
int cpu = 0, ret = 0;
|
|
u32 highest_perf = 0, nominal_perf = 0, lowest_nonlinear_perf = 0, lowest_perf = 0;
|
|
u64 cap1 = 0;
|
|
struct cppc_perf_caps cppc_perf;
|
|
union perf_cached cur_perf;
|
|
|
|
for_each_online_cpu(cpu) {
|
|
struct cpufreq_policy *policy __free(put_cpufreq_policy) = NULL;
|
|
struct amd_cpudata *cpudata;
|
|
|
|
policy = cpufreq_cpu_get(cpu);
|
|
if (!policy)
|
|
continue;
|
|
cpudata = policy->driver_data;
|
|
|
|
if (get_shared_mem()) {
|
|
ret = cppc_get_perf_caps(cpu, &cppc_perf);
|
|
if (ret) {
|
|
pr_err("%s cppc_get_perf_caps ret=%d error!\n", __func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
highest_perf = cppc_perf.highest_perf;
|
|
nominal_perf = cppc_perf.nominal_perf;
|
|
lowest_nonlinear_perf = cppc_perf.lowest_nonlinear_perf;
|
|
lowest_perf = cppc_perf.lowest_perf;
|
|
} else {
|
|
ret = rdmsrq_safe_on_cpu(cpu, MSR_AMD_CPPC_CAP1, &cap1);
|
|
if (ret) {
|
|
pr_err("%s read CPPC_CAP1 ret=%d error!\n", __func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
highest_perf = FIELD_GET(AMD_CPPC_HIGHEST_PERF_MASK, cap1);
|
|
nominal_perf = FIELD_GET(AMD_CPPC_NOMINAL_PERF_MASK, cap1);
|
|
lowest_nonlinear_perf = FIELD_GET(AMD_CPPC_LOWNONLIN_PERF_MASK, cap1);
|
|
lowest_perf = FIELD_GET(AMD_CPPC_LOWEST_PERF_MASK, cap1);
|
|
}
|
|
|
|
cur_perf = READ_ONCE(cpudata->perf);
|
|
if (highest_perf != cur_perf.highest_perf && !cpudata->hw_prefcore) {
|
|
pr_err("%s cpu%d highest=%d %d highest perf doesn't match\n",
|
|
__func__, cpu, highest_perf, cur_perf.highest_perf);
|
|
return -EINVAL;
|
|
}
|
|
if (nominal_perf != cur_perf.nominal_perf ||
|
|
(lowest_nonlinear_perf != cur_perf.lowest_nonlinear_perf) ||
|
|
(lowest_perf != cur_perf.lowest_perf)) {
|
|
pr_err("%s cpu%d nominal=%d %d lowest_nonlinear=%d %d lowest=%d %d, they should be equal!\n",
|
|
__func__, cpu, nominal_perf, cur_perf.nominal_perf,
|
|
lowest_nonlinear_perf, cur_perf.lowest_nonlinear_perf,
|
|
lowest_perf, cur_perf.lowest_perf);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!((highest_perf >= nominal_perf) &&
|
|
(nominal_perf > lowest_nonlinear_perf) &&
|
|
(lowest_nonlinear_perf >= lowest_perf) &&
|
|
(lowest_perf > 0))) {
|
|
pr_err("%s cpu%d highest=%d >= nominal=%d > lowest_nonlinear=%d > lowest=%d > 0, the formula is incorrect!\n",
|
|
__func__, cpu, highest_perf, nominal_perf,
|
|
lowest_nonlinear_perf, lowest_perf);
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Check if frequency values are reasonable.
|
|
* max_freq >= nominal_freq > lowest_nonlinear_freq > min_freq > 0
|
|
* check max freq when set support boost mode.
|
|
*/
|
|
static int amd_pstate_ut_check_freq(u32 index)
|
|
{
|
|
int cpu = 0;
|
|
|
|
for_each_online_cpu(cpu) {
|
|
struct cpufreq_policy *policy __free(put_cpufreq_policy) = NULL;
|
|
struct amd_cpudata *cpudata;
|
|
|
|
policy = cpufreq_cpu_get(cpu);
|
|
if (!policy)
|
|
continue;
|
|
cpudata = policy->driver_data;
|
|
|
|
if (!((policy->cpuinfo.max_freq >= cpudata->nominal_freq) &&
|
|
(cpudata->nominal_freq > cpudata->lowest_nonlinear_freq) &&
|
|
(cpudata->lowest_nonlinear_freq >= policy->cpuinfo.min_freq) &&
|
|
(policy->cpuinfo.min_freq > 0))) {
|
|
pr_err("%s cpu%d max=%d >= nominal=%d > lowest_nonlinear=%d > min=%d > 0, the formula is incorrect!\n",
|
|
__func__, cpu, policy->cpuinfo.max_freq, cpudata->nominal_freq,
|
|
cpudata->lowest_nonlinear_freq, policy->cpuinfo.min_freq);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (cpudata->lowest_nonlinear_freq != policy->min) {
|
|
pr_err("%s cpu%d cpudata_lowest_nonlinear_freq=%d policy_min=%d, they should be equal!\n",
|
|
__func__, cpu, cpudata->lowest_nonlinear_freq, policy->min);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (cpudata->boost_supported) {
|
|
if ((policy->max != policy->cpuinfo.max_freq) &&
|
|
(policy->max != cpudata->nominal_freq)) {
|
|
pr_err("%s cpu%d policy_max=%d should be equal cpu_max=%d or cpu_nominal=%d !\n",
|
|
__func__, cpu, policy->max, policy->cpuinfo.max_freq,
|
|
cpudata->nominal_freq);
|
|
return -EINVAL;
|
|
}
|
|
} else {
|
|
pr_err("%s cpu%d must support boost!\n", __func__, cpu);
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int amd_pstate_set_mode(enum amd_pstate_mode mode)
|
|
{
|
|
const char *mode_str = amd_pstate_get_mode_string(mode);
|
|
|
|
pr_debug("->setting mode to %s\n", mode_str);
|
|
|
|
return amd_pstate_update_status(mode_str, strlen(mode_str));
|
|
}
|
|
|
|
static int amd_pstate_ut_epp(u32 index)
|
|
{
|
|
static const char * const epp_strings[] = {
|
|
"power",
|
|
"balance_power",
|
|
"balance_performance",
|
|
"performance",
|
|
};
|
|
char *buf __free(cleanup_page) = NULL;
|
|
struct cpufreq_policy *policy = NULL;
|
|
enum amd_pstate_mode orig_mode;
|
|
struct amd_cpudata *cpudata;
|
|
unsigned long orig_policy;
|
|
bool orig_dynamic_epp;
|
|
int ret, cpu = 0;
|
|
u16 epp;
|
|
int i;
|
|
|
|
policy = cpufreq_cpu_get(cpu);
|
|
if (!policy)
|
|
return -ENODEV;
|
|
|
|
cpudata = policy->driver_data;
|
|
orig_mode = amd_pstate_get_status();
|
|
orig_dynamic_epp = cpudata->dynamic_epp;
|
|
|
|
/* Drop reference before potential driver change. */
|
|
cpufreq_cpu_put(policy);
|
|
policy = NULL;
|
|
|
|
buf = (char *)__get_free_page(GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
|
|
ret = amd_pstate_set_mode(AMD_PSTATE_ACTIVE);
|
|
if (ret)
|
|
goto out;
|
|
|
|
policy = cpufreq_cpu_get(cpu);
|
|
if (!policy) {
|
|
ret = -ENODEV;
|
|
goto out;
|
|
}
|
|
|
|
down_write(&policy->rwsem);
|
|
cpudata = policy->driver_data;
|
|
orig_policy = cpudata->policy;
|
|
cpudata->policy = CPUFREQ_POLICY_POWERSAVE;
|
|
|
|
/*
|
|
* Disable dynamic EPP before running test. If "orig_dynamic_epp" is
|
|
* true, the driver will do a redundant switch at the end and there
|
|
* is no need for enabling it again at the end of the test.
|
|
*/
|
|
if (cpudata->dynamic_epp) {
|
|
pr_debug("Dynamic EPP is enabled, disabling it\n");
|
|
amd_pstate_clear_dynamic_epp(policy);
|
|
}
|
|
|
|
for (epp = 0; epp <= U8_MAX; epp++) {
|
|
u8 val;
|
|
|
|
/* write all EPP values */
|
|
memset(buf, 0, PAGE_SIZE);
|
|
snprintf(buf, PAGE_SIZE, "%d", epp);
|
|
ret = store_energy_performance_preference(policy, buf, strlen(buf));
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
/* check if the EPP value reads back correctly for raw numbers */
|
|
memset(buf, 0, PAGE_SIZE);
|
|
ret = show_energy_performance_preference(policy, buf);
|
|
if (ret < 0)
|
|
goto out;
|
|
strreplace(buf, '\n', '\0');
|
|
ret = kstrtou8(buf, 0, &val);
|
|
if (!ret && epp != val) {
|
|
pr_err("Raw EPP value mismatch: %d != %d\n", epp, val);
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < ARRAY_SIZE(epp_strings); i++) {
|
|
memset(buf, 0, PAGE_SIZE);
|
|
snprintf(buf, PAGE_SIZE, "%s", epp_strings[i]);
|
|
ret = store_energy_performance_preference(policy, buf, strlen(buf));
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
memset(buf, 0, PAGE_SIZE);
|
|
ret = show_energy_performance_preference(policy, buf);
|
|
if (ret < 0)
|
|
goto out;
|
|
strreplace(buf, '\n', '\0');
|
|
|
|
if (strcmp(buf, epp_strings[i])) {
|
|
pr_err("String EPP value mismatch: %s != %s\n", buf, epp_strings[i]);
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
if (policy) {
|
|
cpudata->policy = orig_policy;
|
|
up_write(&policy->rwsem);
|
|
cpufreq_cpu_put(policy);
|
|
}
|
|
|
|
if (orig_dynamic_epp) {
|
|
int ret2;
|
|
|
|
ret2 = amd_pstate_set_mode(AMD_PSTATE_DISABLE);
|
|
if (!ret && ret2)
|
|
ret = ret2;
|
|
}
|
|
|
|
if (orig_mode != amd_pstate_get_status()) {
|
|
int ret2;
|
|
|
|
ret2 = amd_pstate_set_mode(orig_mode);
|
|
if (!ret && ret2)
|
|
ret = ret2;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int amd_pstate_ut_check_driver(u32 index)
|
|
{
|
|
enum amd_pstate_mode mode1, mode2 = AMD_PSTATE_DISABLE;
|
|
enum amd_pstate_mode orig_mode = amd_pstate_get_status();
|
|
int ret;
|
|
|
|
for (mode1 = AMD_PSTATE_DISABLE; mode1 < AMD_PSTATE_MAX; mode1++) {
|
|
ret = amd_pstate_set_mode(mode1);
|
|
if (ret)
|
|
return ret;
|
|
for (mode2 = AMD_PSTATE_DISABLE; mode2 < AMD_PSTATE_MAX; mode2++) {
|
|
if (mode1 == mode2)
|
|
continue;
|
|
ret = amd_pstate_set_mode(mode2);
|
|
if (ret)
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (ret)
|
|
pr_warn("%s: failed to update status for %s->%s: %d\n", __func__,
|
|
amd_pstate_get_mode_string(mode1),
|
|
amd_pstate_get_mode_string(mode2), ret);
|
|
|
|
amd_pstate_set_mode(orig_mode);
|
|
return ret;
|
|
}
|
|
|
|
enum attr_category {
|
|
ATTR_ALWAYS,
|
|
ATTR_PREFCORE,
|
|
ATTR_EPP,
|
|
ATTR_FLOOR_FREQ,
|
|
};
|
|
|
|
static const struct {
|
|
const char *name;
|
|
enum attr_category category;
|
|
} expected_freq_attrs[] = {
|
|
{"amd_pstate_max_freq", ATTR_ALWAYS},
|
|
{"amd_pstate_lowest_nonlinear_freq", ATTR_ALWAYS},
|
|
{"amd_pstate_highest_perf", ATTR_ALWAYS},
|
|
{"amd_pstate_prefcore_ranking", ATTR_PREFCORE},
|
|
{"amd_pstate_hw_prefcore", ATTR_PREFCORE},
|
|
{"energy_performance_preference", ATTR_EPP},
|
|
{"energy_performance_available_preferences", ATTR_EPP},
|
|
{"amd_pstate_floor_freq", ATTR_FLOOR_FREQ},
|
|
{"amd_pstate_floor_count", ATTR_FLOOR_FREQ},
|
|
};
|
|
|
|
static bool attr_in_driver(struct freq_attr **driver_attrs, const char *name)
|
|
{
|
|
int j;
|
|
|
|
for (j = 0; driver_attrs[j]; j++) {
|
|
if (!strcmp(driver_attrs[j]->attr.name, name))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Verify that for each mode the driver's live ->attr array contains exactly
|
|
* the attributes that should be visible. Expected visibility is derived
|
|
* independently from hw_prefcore, cpu features, and the current mode —
|
|
* not from the driver's own visibility functions.
|
|
*/
|
|
static int amd_pstate_ut_check_freq_attrs(u32 index)
|
|
{
|
|
enum amd_pstate_mode orig_mode = amd_pstate_get_status();
|
|
static const enum amd_pstate_mode modes[] = {
|
|
AMD_PSTATE_PASSIVE, AMD_PSTATE_ACTIVE, AMD_PSTATE_GUIDED,
|
|
};
|
|
bool has_prefcore, has_floor_freq;
|
|
int m, i, ret;
|
|
|
|
has_floor_freq = cpu_feature_enabled(X86_FEATURE_CPPC_PERF_PRIO);
|
|
|
|
/*
|
|
* Determine prefcore support from any online CPU's cpudata.
|
|
* hw_prefcore reflects the platform-wide decision made at init.
|
|
*/
|
|
has_prefcore = false;
|
|
for_each_online_cpu(i) {
|
|
struct cpufreq_policy *policy __free(put_cpufreq_policy) = NULL;
|
|
struct amd_cpudata *cpudata;
|
|
|
|
policy = cpufreq_cpu_get(i);
|
|
if (!policy)
|
|
continue;
|
|
cpudata = policy->driver_data;
|
|
has_prefcore = cpudata->hw_prefcore;
|
|
break;
|
|
}
|
|
|
|
for (m = 0; m < ARRAY_SIZE(modes); m++) {
|
|
struct freq_attr **driver_attrs;
|
|
|
|
ret = amd_pstate_set_mode(modes[m]);
|
|
if (ret)
|
|
goto out;
|
|
|
|
driver_attrs = amd_pstate_get_current_attrs();
|
|
if (!driver_attrs) {
|
|
pr_err("%s: no driver attrs in mode %s\n",
|
|
__func__, amd_pstate_get_mode_string(modes[m]));
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
for (i = 0; i < ARRAY_SIZE(expected_freq_attrs); i++) {
|
|
bool expected, found;
|
|
|
|
switch (expected_freq_attrs[i].category) {
|
|
case ATTR_ALWAYS:
|
|
expected = true;
|
|
break;
|
|
case ATTR_PREFCORE:
|
|
expected = has_prefcore;
|
|
break;
|
|
case ATTR_EPP:
|
|
expected = (modes[m] == AMD_PSTATE_ACTIVE);
|
|
break;
|
|
case ATTR_FLOOR_FREQ:
|
|
expected = has_floor_freq;
|
|
break;
|
|
default:
|
|
expected = false;
|
|
break;
|
|
}
|
|
|
|
found = attr_in_driver(driver_attrs,
|
|
expected_freq_attrs[i].name);
|
|
|
|
if (expected != found) {
|
|
pr_err("%s: mode %s: attr %s expected %s but is %s\n",
|
|
__func__,
|
|
amd_pstate_get_mode_string(modes[m]),
|
|
expected_freq_attrs[i].name,
|
|
expected ? "visible" : "hidden",
|
|
found ? "visible" : "hidden");
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
ret = 0;
|
|
out:
|
|
amd_pstate_set_mode(orig_mode);
|
|
return ret;
|
|
}
|
|
|
|
static int __init amd_pstate_ut_init(void)
|
|
{
|
|
u32 i = 0, arr_size = ARRAY_SIZE(amd_pstate_ut_cases);
|
|
|
|
for (i = 0; i < arr_size; i++) {
|
|
int ret;
|
|
|
|
if (test_list && *test_list &&
|
|
!test_in_list(test_list, amd_pstate_ut_cases[i].name))
|
|
continue;
|
|
|
|
ret = amd_pstate_ut_cases[i].func(i);
|
|
|
|
if (ret)
|
|
pr_err("%-4d %-20s\t fail: %d!\n", i+1, amd_pstate_ut_cases[i].name, ret);
|
|
else
|
|
pr_info("%-4d %-20s\t success!\n", i+1, amd_pstate_ut_cases[i].name);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void __exit amd_pstate_ut_exit(void)
|
|
{
|
|
}
|
|
|
|
module_init(amd_pstate_ut_init);
|
|
module_exit(amd_pstate_ut_exit);
|
|
|
|
MODULE_AUTHOR("Meng Li <li.meng@amd.com>");
|
|
MODULE_DESCRIPTION("AMD P-state driver Test module");
|
|
MODULE_LICENSE("GPL");
|