diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 01:02:30 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 01:02:30 +0000 |
commit | 76cb841cb886eef6b3bee341a2266c76578724ad (patch) | |
tree | f5892e5ba6cc11949952a6ce4ecbe6d516d6ce58 /drivers/hwmon/fam15h_power.c | |
parent | Initial commit. (diff) | |
download | linux-76cb841cb886eef6b3bee341a2266c76578724ad.tar.xz linux-76cb841cb886eef6b3bee341a2266c76578724ad.zip |
Adding upstream version 4.19.249.upstream/4.19.249
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drivers/hwmon/fam15h_power.c')
-rw-r--r-- | drivers/hwmon/fam15h_power.c | 516 |
1 files changed, 516 insertions, 0 deletions
diff --git a/drivers/hwmon/fam15h_power.c b/drivers/hwmon/fam15h_power.c new file mode 100644 index 000000000..9545a3460 --- /dev/null +++ b/drivers/hwmon/fam15h_power.c @@ -0,0 +1,516 @@ +/* + * fam15h_power.c - AMD Family 15h processor power monitoring + * + * Copyright (c) 2011-2016 Advanced Micro Devices, Inc. + * Author: Andreas Herrmann <herrmann.der.user@googlemail.com> + * + * + * This driver is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License; either + * version 2 of the License, or (at your option) any later version. + * + * This driver is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this driver; if not, see <http://www.gnu.org/licenses/>. + */ + +#include <linux/err.h> +#include <linux/hwmon.h> +#include <linux/hwmon-sysfs.h> +#include <linux/init.h> +#include <linux/module.h> +#include <linux/pci.h> +#include <linux/bitops.h> +#include <linux/cpu.h> +#include <linux/cpumask.h> +#include <linux/time.h> +#include <linux/sched.h> +#include <asm/processor.h> +#include <asm/msr.h> + +MODULE_DESCRIPTION("AMD Family 15h CPU processor power monitor"); +MODULE_AUTHOR("Andreas Herrmann <herrmann.der.user@googlemail.com>"); +MODULE_LICENSE("GPL"); + +/* D18F3 */ +#define REG_NORTHBRIDGE_CAP 0xe8 + +/* D18F4 */ +#define REG_PROCESSOR_TDP 0x1b8 + +/* D18F5 */ +#define REG_TDP_RUNNING_AVERAGE 0xe0 +#define REG_TDP_LIMIT3 0xe8 + +#define FAM15H_MIN_NUM_ATTRS 2 +#define FAM15H_NUM_GROUPS 2 +#define MAX_CUS 8 + +/* set maximum interval as 1 second */ +#define MAX_INTERVAL 1000 + +#define MSR_F15H_CU_PWR_ACCUMULATOR 0xc001007a +#define MSR_F15H_CU_MAX_PWR_ACCUMULATOR 0xc001007b +#define MSR_F15H_PTSC 0xc0010280 + +#define PCI_DEVICE_ID_AMD_15H_M70H_NB_F4 0x15b4 + +struct fam15h_power_data { + struct pci_dev *pdev; + unsigned int tdp_to_watts; + unsigned int base_tdp; + unsigned int processor_pwr_watts; + unsigned int cpu_pwr_sample_ratio; + const struct attribute_group *groups[FAM15H_NUM_GROUPS]; + struct attribute_group group; + /* maximum accumulated power of a compute unit */ + u64 max_cu_acc_power; + /* accumulated power of the compute units */ + u64 cu_acc_power[MAX_CUS]; + /* performance timestamp counter */ + u64 cpu_sw_pwr_ptsc[MAX_CUS]; + /* online/offline status of current compute unit */ + int cu_on[MAX_CUS]; + unsigned long power_period; +}; + +static bool is_carrizo_or_later(void) +{ + return boot_cpu_data.x86 == 0x15 && boot_cpu_data.x86_model >= 0x60; +} + +static ssize_t power1_input_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + u32 val, tdp_limit, running_avg_range; + s32 running_avg_capture; + u64 curr_pwr_watts; + struct fam15h_power_data *data = dev_get_drvdata(dev); + struct pci_dev *f4 = data->pdev; + + pci_bus_read_config_dword(f4->bus, PCI_DEVFN(PCI_SLOT(f4->devfn), 5), + REG_TDP_RUNNING_AVERAGE, &val); + + /* + * On Carrizo and later platforms, TdpRunAvgAccCap bit field + * is extended to 4:31 from 4:25. + */ + if (is_carrizo_or_later()) { + running_avg_capture = val >> 4; + running_avg_capture = sign_extend32(running_avg_capture, 27); + } else { + running_avg_capture = (val >> 4) & 0x3fffff; + running_avg_capture = sign_extend32(running_avg_capture, 21); + } + + running_avg_range = (val & 0xf) + 1; + + pci_bus_read_config_dword(f4->bus, PCI_DEVFN(PCI_SLOT(f4->devfn), 5), + REG_TDP_LIMIT3, &val); + + /* + * On Carrizo and later platforms, ApmTdpLimit bit field + * is extended to 16:31 from 16:28. + */ + if (is_carrizo_or_later()) + tdp_limit = val >> 16; + else + tdp_limit = (val >> 16) & 0x1fff; + + curr_pwr_watts = ((u64)(tdp_limit + + data->base_tdp)) << running_avg_range; + curr_pwr_watts -= running_avg_capture; + curr_pwr_watts *= data->tdp_to_watts; + + /* + * Convert to microWatt + * + * power is in Watt provided as fixed point integer with + * scaling factor 1/(2^16). For conversion we use + * (10^6)/(2^16) = 15625/(2^10) + */ + curr_pwr_watts = (curr_pwr_watts * 15625) >> (10 + running_avg_range); + return sprintf(buf, "%u\n", (unsigned int) curr_pwr_watts); +} +static DEVICE_ATTR_RO(power1_input); + +static ssize_t power1_crit_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct fam15h_power_data *data = dev_get_drvdata(dev); + + return sprintf(buf, "%u\n", data->processor_pwr_watts); +} +static DEVICE_ATTR_RO(power1_crit); + +static void do_read_registers_on_cu(void *_data) +{ + struct fam15h_power_data *data = _data; + int cpu, cu; + + cpu = smp_processor_id(); + + /* + * With the new x86 topology modelling, cpu core id actually + * is compute unit id. + */ + cu = cpu_data(cpu).cpu_core_id; + + rdmsrl_safe(MSR_F15H_CU_PWR_ACCUMULATOR, &data->cu_acc_power[cu]); + rdmsrl_safe(MSR_F15H_PTSC, &data->cpu_sw_pwr_ptsc[cu]); + + data->cu_on[cu] = 1; +} + +/* + * This function is only able to be called when CPUID + * Fn8000_0007:EDX[12] is set. + */ +static int read_registers(struct fam15h_power_data *data) +{ + int core, this_core; + cpumask_var_t mask; + int ret, cpu; + + ret = zalloc_cpumask_var(&mask, GFP_KERNEL); + if (!ret) + return -ENOMEM; + + memset(data->cu_on, 0, sizeof(int) * MAX_CUS); + + get_online_cpus(); + + /* + * Choose the first online core of each compute unit, and then + * read their MSR value of power and ptsc in a single IPI, + * because the MSR value of CPU core represent the compute + * unit's. + */ + core = -1; + + for_each_online_cpu(cpu) { + this_core = topology_core_id(cpu); + + if (this_core == core) + continue; + + core = this_core; + + /* get any CPU on this compute unit */ + cpumask_set_cpu(cpumask_any(topology_sibling_cpumask(cpu)), mask); + } + + on_each_cpu_mask(mask, do_read_registers_on_cu, data, true); + + put_online_cpus(); + free_cpumask_var(mask); + + return 0; +} + +static ssize_t power1_average_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct fam15h_power_data *data = dev_get_drvdata(dev); + u64 prev_cu_acc_power[MAX_CUS], prev_ptsc[MAX_CUS], + jdelta[MAX_CUS]; + u64 tdelta, avg_acc; + int cu, cu_num, ret; + signed long leftover; + + /* + * With the new x86 topology modelling, x86_max_cores is the + * compute unit number. + */ + cu_num = boot_cpu_data.x86_max_cores; + + ret = read_registers(data); + if (ret) + return 0; + + for (cu = 0; cu < cu_num; cu++) { + prev_cu_acc_power[cu] = data->cu_acc_power[cu]; + prev_ptsc[cu] = data->cpu_sw_pwr_ptsc[cu]; + } + + leftover = schedule_timeout_interruptible(msecs_to_jiffies(data->power_period)); + if (leftover) + return 0; + + ret = read_registers(data); + if (ret) + return 0; + + for (cu = 0, avg_acc = 0; cu < cu_num; cu++) { + /* check if current compute unit is online */ + if (data->cu_on[cu] == 0) + continue; + + if (data->cu_acc_power[cu] < prev_cu_acc_power[cu]) { + jdelta[cu] = data->max_cu_acc_power + data->cu_acc_power[cu]; + jdelta[cu] -= prev_cu_acc_power[cu]; + } else { + jdelta[cu] = data->cu_acc_power[cu] - prev_cu_acc_power[cu]; + } + tdelta = data->cpu_sw_pwr_ptsc[cu] - prev_ptsc[cu]; + jdelta[cu] *= data->cpu_pwr_sample_ratio * 1000; + do_div(jdelta[cu], tdelta); + + /* the unit is microWatt */ + avg_acc += jdelta[cu]; + } + + return sprintf(buf, "%llu\n", (unsigned long long)avg_acc); +} +static DEVICE_ATTR_RO(power1_average); + +static ssize_t power1_average_interval_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct fam15h_power_data *data = dev_get_drvdata(dev); + + return sprintf(buf, "%lu\n", data->power_period); +} + +static ssize_t power1_average_interval_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fam15h_power_data *data = dev_get_drvdata(dev); + unsigned long temp; + int ret; + + ret = kstrtoul(buf, 10, &temp); + if (ret) + return ret; + + if (temp > MAX_INTERVAL) + return -EINVAL; + + /* the interval value should be greater than 0 */ + if (temp <= 0) + return -EINVAL; + + data->power_period = temp; + + return count; +} +static DEVICE_ATTR_RW(power1_average_interval); + +static int fam15h_power_init_attrs(struct pci_dev *pdev, + struct fam15h_power_data *data) +{ + int n = FAM15H_MIN_NUM_ATTRS; + struct attribute **fam15h_power_attrs; + struct cpuinfo_x86 *c = &boot_cpu_data; + + if (c->x86 == 0x15 && + (c->x86_model <= 0xf || + (c->x86_model >= 0x60 && c->x86_model <= 0x7f))) + n += 1; + + /* check if processor supports accumulated power */ + if (boot_cpu_has(X86_FEATURE_ACC_POWER)) + n += 2; + + fam15h_power_attrs = devm_kcalloc(&pdev->dev, n, + sizeof(*fam15h_power_attrs), + GFP_KERNEL); + + if (!fam15h_power_attrs) + return -ENOMEM; + + n = 0; + fam15h_power_attrs[n++] = &dev_attr_power1_crit.attr; + if (c->x86 == 0x15 && + (c->x86_model <= 0xf || + (c->x86_model >= 0x60 && c->x86_model <= 0x7f))) + fam15h_power_attrs[n++] = &dev_attr_power1_input.attr; + + if (boot_cpu_has(X86_FEATURE_ACC_POWER)) { + fam15h_power_attrs[n++] = &dev_attr_power1_average.attr; + fam15h_power_attrs[n++] = &dev_attr_power1_average_interval.attr; + } + + data->group.attrs = fam15h_power_attrs; + + return 0; +} + +static bool should_load_on_this_node(struct pci_dev *f4) +{ + u32 val; + + pci_bus_read_config_dword(f4->bus, PCI_DEVFN(PCI_SLOT(f4->devfn), 3), + REG_NORTHBRIDGE_CAP, &val); + if ((val & BIT(29)) && ((val >> 30) & 3)) + return false; + + return true; +} + +/* + * Newer BKDG versions have an updated recommendation on how to properly + * initialize the running average range (was: 0xE, now: 0x9). This avoids + * counter saturations resulting in bogus power readings. + * We correct this value ourselves to cope with older BIOSes. + */ +static const struct pci_device_id affected_device[] = { + { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_15H_NB_F4) }, + { 0 } +}; + +static void tweak_runavg_range(struct pci_dev *pdev) +{ + u32 val; + + /* + * let this quirk apply only to the current version of the + * northbridge, since future versions may change the behavior + */ + if (!pci_match_id(affected_device, pdev)) + return; + + pci_bus_read_config_dword(pdev->bus, + PCI_DEVFN(PCI_SLOT(pdev->devfn), 5), + REG_TDP_RUNNING_AVERAGE, &val); + if ((val & 0xf) != 0xe) + return; + + val &= ~0xf; + val |= 0x9; + pci_bus_write_config_dword(pdev->bus, + PCI_DEVFN(PCI_SLOT(pdev->devfn), 5), + REG_TDP_RUNNING_AVERAGE, val); +} + +#ifdef CONFIG_PM +static int fam15h_power_resume(struct pci_dev *pdev) +{ + tweak_runavg_range(pdev); + return 0; +} +#else +#define fam15h_power_resume NULL +#endif + +static int fam15h_power_init_data(struct pci_dev *f4, + struct fam15h_power_data *data) +{ + u32 val; + u64 tmp; + int ret; + + pci_read_config_dword(f4, REG_PROCESSOR_TDP, &val); + data->base_tdp = val >> 16; + tmp = val & 0xffff; + + pci_bus_read_config_dword(f4->bus, PCI_DEVFN(PCI_SLOT(f4->devfn), 5), + REG_TDP_LIMIT3, &val); + + data->tdp_to_watts = ((val & 0x3ff) << 6) | ((val >> 10) & 0x3f); + tmp *= data->tdp_to_watts; + + /* result not allowed to be >= 256W */ + if ((tmp >> 16) >= 256) + dev_warn(&f4->dev, + "Bogus value for ProcessorPwrWatts (processor_pwr_watts>=%u)\n", + (unsigned int) (tmp >> 16)); + + /* convert to microWatt */ + data->processor_pwr_watts = (tmp * 15625) >> 10; + + ret = fam15h_power_init_attrs(f4, data); + if (ret) + return ret; + + + /* CPUID Fn8000_0007:EDX[12] indicates to support accumulated power */ + if (!boot_cpu_has(X86_FEATURE_ACC_POWER)) + return 0; + + /* + * determine the ratio of the compute unit power accumulator + * sample period to the PTSC counter period by executing CPUID + * Fn8000_0007:ECX + */ + data->cpu_pwr_sample_ratio = cpuid_ecx(0x80000007); + + if (rdmsrl_safe(MSR_F15H_CU_MAX_PWR_ACCUMULATOR, &tmp)) { + pr_err("Failed to read max compute unit power accumulator MSR\n"); + return -ENODEV; + } + + data->max_cu_acc_power = tmp; + + /* + * Milliseconds are a reasonable interval for the measurement. + * But it shouldn't set too long here, because several seconds + * would cause the read function to hang. So set default + * interval as 10 ms. + */ + data->power_period = 10; + + return read_registers(data); +} + +static int fam15h_power_probe(struct pci_dev *pdev, + const struct pci_device_id *id) +{ + struct fam15h_power_data *data; + struct device *dev = &pdev->dev; + struct device *hwmon_dev; + int ret; + + /* + * though we ignore every other northbridge, we still have to + * do the tweaking on _each_ node in MCM processors as the counters + * are working hand-in-hand + */ + tweak_runavg_range(pdev); + + if (!should_load_on_this_node(pdev)) + return -ENODEV; + + data = devm_kzalloc(dev, sizeof(struct fam15h_power_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + ret = fam15h_power_init_data(pdev, data); + if (ret) + return ret; + + data->pdev = pdev; + + data->groups[0] = &data->group; + + hwmon_dev = devm_hwmon_device_register_with_groups(dev, "fam15h_power", + data, + &data->groups[0]); + return PTR_ERR_OR_ZERO(hwmon_dev); +} + +static const struct pci_device_id fam15h_power_id_table[] = { + { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_15H_NB_F4) }, + { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_15H_M30H_NB_F4) }, + { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_15H_M60H_NB_F4) }, + { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_15H_M70H_NB_F4) }, + { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_16H_NB_F4) }, + { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_16H_M30H_NB_F4) }, + {} +}; +MODULE_DEVICE_TABLE(pci, fam15h_power_id_table); + +static struct pci_driver fam15h_power_driver = { + .name = "fam15h_power", + .id_table = fam15h_power_id_table, + .probe = fam15h_power_probe, + .resume = fam15h_power_resume, +}; + +module_pci_driver(fam15h_power_driver); |