diff options
Diffstat (limited to 'drivers/hwmon/peci-dimmpower.c')
-rw-r--r-- | drivers/hwmon/peci-dimmpower.c | 640 |
1 files changed, 640 insertions, 0 deletions
diff --git a/drivers/hwmon/peci-dimmpower.c b/drivers/hwmon/peci-dimmpower.c new file mode 100644 index 000000000000..1d15ea6520dc --- /dev/null +++ b/drivers/hwmon/peci-dimmpower.c @@ -0,0 +1,640 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2020 Intel Corporation + +#include <linux/hwmon.h> +#include <linux/jiffies.h> +#include <linux/mfd/intel-peci-client.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include "peci-hwmon.h" + +enum PECI_DIMMPOWER_SENSOR_TYPES { + PECI_DIMMPOWER_SENSOR_TYPE_POWER = 0, + PECI_DIMMPOWER_SENSOR_TYPE_ENERGY, + PECI_DIMMPOWER_SENSOR_TYPES_COUNT, +}; + +#define PECI_DIMMPOWER_POWER_CHANNEL_COUNT 1 /* Supported channels number */ +#define PECI_DIMMPOWER_ENERGY_CHANNEL_COUNT 1 /* Supported channels number */ + +#define PECI_DIMMPOWER_POWER_SENSOR_COUNT 4 /* Supported sensors/readings number */ +#define PECI_DIMMPOWER_ENERGY_SENSOR_COUNT 4 /* Supported sensors/readings number */ + +struct peci_dimmpower { + struct device *dev; + struct peci_client_manager *mgr; + char name[PECI_NAME_SIZE]; + u32 power_config[PECI_DIMMPOWER_POWER_CHANNEL_COUNT + 1]; + u32 energy_config[PECI_DIMMPOWER_ENERGY_CHANNEL_COUNT + 1]; + + struct hwmon_channel_info power_info; + struct hwmon_channel_info energy_info; + const struct hwmon_channel_info *info[PECI_DIMMPOWER_SENSOR_TYPES_COUNT + 1]; + struct hwmon_chip_info chip; + + struct peci_sensor_data + power_sensor_data_list[PECI_DIMMPOWER_POWER_CHANNEL_COUNT] + [PECI_DIMMPOWER_POWER_SENSOR_COUNT]; + struct peci_sensor_data + energy_sensor_data_list[PECI_DIMMPOWER_ENERGY_CHANNEL_COUNT] + [PECI_DIMMPOWER_ENERGY_SENSOR_COUNT]; + + /* Below structs are not exposed to any sensor directly */ + struct peci_sensor_data energy_cache; /* used to limit PECI communication */ + struct peci_sensor_data power_sensor_prev_energy; + struct peci_sensor_data energy_sensor_prev_energy; + + union peci_pkg_power_sku_unit units; + bool units_valid; + + u32 dpl_time_window; + bool dpl_time_window_valid; +}; + +static const char *peci_dimmpower_labels[PECI_DIMMPOWER_SENSOR_TYPES_COUNT] = { + "dimm power", + "dimm energy", +}; + +/** + * peci_dimmpower_read_dram_power_limit - read PCS DRAM Power Limit + * @peci_mgr: PECI client manager handle + * @reg: Pointer to the variable read value is going to be put + * + * Return: 0 if succeeded, other values in case an error. + */ +static inline int +peci_dimmpower_read_dram_power_limit(struct peci_client_manager *peci_mgr, + union peci_dram_power_limit *reg) +{ + return peci_pcs_read(peci_mgr, PECI_MBX_INDEX_DDR_RAPL_PL1, + PECI_PCS_PARAM_ZERO, ®->value); +} + +static int +peci_dimmpower_get_energy_counter(struct peci_dimmpower *priv, + struct peci_sensor_data *sensor_data, + ulong update_interval) +{ + int ret; + + if (!peci_sensor_need_update_with_time(sensor_data, + update_interval)) { + dev_dbg(priv->dev, "skip reading dimm energy over peci\n"); + return 0; + } + + ret = peci_pcs_read(priv->mgr, PECI_MBX_INDEX_ENERGY_STATUS, + PECI_PKG_ID_DIMM, &sensor_data->uvalue); + if (ret) { + dev_dbg(priv->dev, "not able to read dimm energy\n"); + return ret; + } + + peci_sensor_mark_updated(sensor_data); + + dev_dbg(priv->dev, + "energy counter updated %duJ, jif %lu, HZ is %d jiffies\n", + sensor_data->uvalue, sensor_data->last_updated, HZ); + + return ret; +} + +static int +peci_dimmpower_get_avg_power(void *ctx, struct peci_sensor_conf *sensor_conf, + struct peci_sensor_data *sensor_data) +{ + struct peci_dimmpower *priv = (struct peci_dimmpower *)ctx; + int ret; + + if (!peci_sensor_need_update_with_time(sensor_data, + sensor_conf->update_interval)) { + dev_dbg(priv->dev, "skip reading peci, average power %dmW jif %lu\n", + sensor_data->value, jiffies); + return 0; + } + + ret = peci_dimmpower_get_energy_counter(priv, &priv->energy_cache, + sensor_conf->update_interval); + if (ret) { + dev_dbg(priv->dev, "cannot update energy counter\n"); + return ret; + } + + ret = peci_pcs_get_units(priv->mgr, &priv->units, &priv->units_valid); + if (ret) { + dev_dbg(priv->dev, "not able to read units\n"); + return ret; + } + + ret = peci_pcs_calc_pwr_from_eng(priv->dev, + &priv->power_sensor_prev_energy, + &priv->energy_cache, + priv->units.bits.eng_unit, + &sensor_data->value); + if (ret) { + dev_dbg(priv->dev, "power calculation failed\n"); + return ret; + } + + peci_sensor_mark_updated_with_time(sensor_data, priv->energy_cache.last_updated); + + dev_dbg(priv->dev, "average power %dmW, jif %lu, HZ is %d jiffies\n", + sensor_data->value, sensor_data->last_updated, HZ); + + return ret; +} + +static int +peci_dimmpower_get_power_limit(void *ctx, struct peci_sensor_conf *sensor_conf, + struct peci_sensor_data *sensor_data) +{ + struct peci_dimmpower *priv = (struct peci_dimmpower *)ctx; + union peci_dram_power_limit power_limit; + int ret; + + if (!peci_sensor_need_update_with_time(sensor_data, + sensor_conf->update_interval)) { + dev_dbg(priv->dev, "skip reading peci, power limit %dmW\n", + sensor_data->value); + return 0; + } + + ret = peci_pcs_get_units(priv->mgr, &priv->units, &priv->units_valid); + if (ret) { + dev_dbg(priv->dev, "not able to read units\n"); + return ret; + } + + ret = peci_dimmpower_read_dram_power_limit(priv->mgr, &power_limit); + if (ret) { + dev_dbg(priv->dev, "not able to read power limit\n"); + return ret; + } + + peci_sensor_mark_updated(sensor_data); + sensor_data->value = peci_pcs_xn_to_munits(power_limit.bits.pp_pwr_lim, + priv->units.bits.pwr_unit); + + dev_dbg(priv->dev, "raw power limit %u, unit %u, power limit %d\n", + power_limit.bits.pp_pwr_lim, priv->units.bits.pwr_unit, + sensor_data->value); + + return ret; +} + +static int +peci_dimmpower_set_power_limit(void *ctx, struct peci_sensor_conf *sensor_conf, + struct peci_sensor_data *sensor_data, + s32 val) +{ + struct peci_dimmpower *priv = (struct peci_dimmpower *)ctx; + union peci_dram_power_limit power_limit; + int ret; + + ret = peci_pcs_get_units(priv->mgr, &priv->units, &priv->units_valid); + if (ret) { + dev_dbg(priv->dev, "not able to read units\n"); + return ret; + } + + ret = peci_dimmpower_read_dram_power_limit(priv->mgr, &power_limit); + if (ret) { + dev_dbg(priv->dev, "not able to read power limit\n"); + return ret; + } + + /* Calculate DPL time window if needed */ + if (!priv->dpl_time_window_valid) { + priv->dpl_time_window = + peci_pcs_calc_plxy_time_window(peci_pcs_munits_to_xn( + PECI_PCS_PPL1_TIME_WINDOW, + priv->units.bits.tim_unit)); + priv->dpl_time_window_valid = true; + } + + /* Enable or disable power limitation */ + if (val > 0) { + power_limit.bits.pp_pwr_lim = + peci_pcs_munits_to_xn(val, priv->units.bits.pwr_unit); + power_limit.bits.pwr_lim_ctrl_en = 1u; + power_limit.bits.ctrl_time_win = priv->dpl_time_window; + } else { + power_limit.bits.pp_pwr_lim = 0u; + power_limit.bits.pwr_lim_ctrl_en = 0u; + power_limit.bits.ctrl_time_win = 0u; + } + + ret = peci_pcs_write(priv->mgr, PECI_MBX_INDEX_DDR_RAPL_PL1, + PECI_PCS_PARAM_ZERO, power_limit.value); + if (ret) { + dev_dbg(priv->dev, "not able to write power limit\n"); + return ret; + } + + dev_dbg(priv->dev, "power limit %d, unit %u, raw power limit %u,\n", + val, priv->units.bits.pwr_unit, power_limit.bits.pp_pwr_lim); + + return ret; +} + +static int +peci_dimmpower_read_max_power(void *ctx, struct peci_sensor_conf *sensor_conf, + struct peci_sensor_data *sensor_data) +{ + struct peci_dimmpower *priv = (struct peci_dimmpower *)ctx; + union peci_dram_power_info_low power_info; + int ret; + + if (!peci_sensor_need_update_with_time(sensor_data, + sensor_conf->update_interval)) { + dev_dbg(priv->dev, "skip reading peci, max power %dmW\n", + sensor_data->value); + return 0; + } + + ret = peci_pcs_get_units(priv->mgr, &priv->units, &priv->units_valid); + if (ret) { + dev_dbg(priv->dev, "not able to read units\n"); + return ret; + } + + ret = peci_pcs_read(priv->mgr, PECI_MBX_INDEX_DDR_PWR_INFO_LOW, + PECI_PCS_PARAM_ZERO, &power_info.value); + if (ret) { + dev_dbg(priv->dev, "not able to read power info\n"); + return ret; + } + + peci_sensor_mark_updated(sensor_data); + sensor_data->value = peci_pcs_xn_to_munits(power_info.bits.tdp, + priv->units.bits.pwr_unit); + + dev_dbg(priv->dev, "raw max power %u, unit %u, max power %dmW\n", + power_info.bits.tdp, priv->units.bits.pwr_unit, + sensor_data->value); + + return ret; +} + +static int +peci_dimmpower_read_min_power(void *ctx, struct peci_sensor_conf *sensor_conf, + struct peci_sensor_data *sensor_data) +{ + struct peci_dimmpower *priv = (struct peci_dimmpower *)ctx; + + /* DRAM_POWER_INFO.DRAM_MIN_PWR is no more supported in CPU starting from + * SPR. So BIOS doesn't update this. That's why there is still default + * value (15W) which doesn't make sense. There should be a case when + * MAX_PWR/TDP is smaller than 15W. + * 0 seems to be a reasonable value for that parameter. + */ + sensor_data->value = 0; + dev_dbg(priv->dev, "min power %dmW\n", sensor_data->value); + return 0; +} + +static int +peci_dimmpower_read_energy(void *ctx, struct peci_sensor_conf *sensor_conf, + struct peci_sensor_data *sensor_data) +{ + struct peci_dimmpower *priv = (struct peci_dimmpower *)ctx; + int ret; + + if (!peci_sensor_need_update_with_time(sensor_data, + sensor_conf->update_interval)) { + dev_dbg(priv->dev, + "skip generating new energy value %duJ jif %lu\n", + sensor_data->uvalue, jiffies); + return 0; + } + + ret = peci_dimmpower_get_energy_counter(priv, &priv->energy_cache, + sensor_conf->update_interval); + if (ret) { + dev_dbg(priv->dev, "cannot update energy counter\n"); + return ret; + } + + ret = peci_pcs_get_units(priv->mgr, &priv->units, &priv->units_valid); + if (ret) { + dev_dbg(priv->dev, "not able to read units\n"); + return ret; + } + + ret = peci_pcs_calc_acc_eng(priv->dev, + &priv->energy_sensor_prev_energy, + &priv->energy_cache, + priv->units.bits.eng_unit, + &sensor_data->uvalue); + + if (ret) { + dev_dbg(priv->dev, "cumulative energy calculation failed\n"); + return ret; + } + peci_sensor_mark_updated_with_time(sensor_data, + priv->energy_cache.last_updated); + + dev_dbg(priv->dev, "energy %duJ, jif %lu, HZ is %d jiffies\n", + sensor_data->uvalue, sensor_data->last_updated, HZ); + + return 0; +} + +static struct peci_sensor_conf +peci_dimmpower_power_cfg[PECI_DIMMPOWER_POWER_CHANNEL_COUNT] + [PECI_DIMMPOWER_POWER_SENSOR_COUNT] = { + /* Channel 0 - Power */ + { + { + .attribute = hwmon_power_average, + .config = HWMON_P_AVERAGE, + .update_interval = UPDATE_INTERVAL_100MS, + .read = peci_dimmpower_get_avg_power, + .write = NULL, + }, + { + .attribute = hwmon_power_cap, + .config = HWMON_P_CAP, + .update_interval = UPDATE_INTERVAL_100MS, + .read = peci_dimmpower_get_power_limit, + .write = peci_dimmpower_set_power_limit, + }, + { + .attribute = hwmon_power_cap_max, + .config = HWMON_P_CAP_MAX, + .update_interval = UPDATE_INTERVAL_10S, + .read = peci_dimmpower_read_max_power, + .write = NULL, + }, + { + .attribute = hwmon_power_cap_min, + .config = HWMON_P_CAP_MIN, + .update_interval = UPDATE_INTERVAL_10S, + .read = peci_dimmpower_read_min_power, + .write = NULL, + }, + }, +}; + +static struct peci_sensor_conf +peci_dimmpower_energy_cfg[PECI_DIMMPOWER_ENERGY_CHANNEL_COUNT] + [PECI_DIMMPOWER_ENERGY_SENSOR_COUNT] = { + /* Channel 0 - Energy */ + { + { + .attribute = hwmon_energy_input, + .config = HWMON_E_INPUT, + .update_interval = UPDATE_INTERVAL_100MS, + .read = peci_dimmpower_read_energy, + .write = NULL, + }, + } +}; + +static bool +peci_dimmpower_is_channel_valid(enum hwmon_sensor_types type, + int channel) +{ + if ((type == hwmon_power && channel < PECI_DIMMPOWER_POWER_CHANNEL_COUNT) || + (type == hwmon_energy && channel < PECI_DIMMPOWER_ENERGY_CHANNEL_COUNT)) + return true; + + return false; +} + +static int +peci_dimmpower_read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + if (!peci_dimmpower_is_channel_valid(type, channel)) + return -EOPNOTSUPP; + + switch (attr) { + case hwmon_power_label: + *str = peci_dimmpower_labels[PECI_DIMMPOWER_SENSOR_TYPE_POWER]; + break; + case hwmon_energy_label: + *str = peci_dimmpower_labels[PECI_DIMMPOWER_SENSOR_TYPE_ENERGY]; + break; + default: + return -EOPNOTSUPP; + } + + return 0; +} + +static int +peci_dimmpower_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct peci_dimmpower *priv = dev_get_drvdata(dev); + struct peci_sensor_conf *sensor_conf; + struct peci_sensor_data *sensor_data; + int ret; + + if (!priv || !val) + return -EINVAL; + + if (!peci_dimmpower_is_channel_valid(type, channel)) + return -EOPNOTSUPP; + + switch (type) { + case hwmon_power: + ret = peci_sensor_get_ctx(attr, peci_dimmpower_power_cfg[channel], + &sensor_conf, + priv->power_sensor_data_list[channel], + &sensor_data, + ARRAY_SIZE(peci_dimmpower_power_cfg[channel])); + break; + case hwmon_energy: + ret = peci_sensor_get_ctx(attr, peci_dimmpower_energy_cfg[channel], + &sensor_conf, + priv->energy_sensor_data_list[channel], + &sensor_data, + ARRAY_SIZE(peci_dimmpower_energy_cfg[channel])); + break; + default: + ret = -EOPNOTSUPP; + } + + if (ret) + return ret; + + if (sensor_conf->read) { + ret = sensor_conf->read(priv, sensor_conf, sensor_data); + if (!ret) + *val = (long)sensor_data->value; + } else { + ret = -EOPNOTSUPP; + } + + return ret; +} + +static int +peci_dimmpower_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct peci_dimmpower *priv = dev_get_drvdata(dev); + struct peci_sensor_conf *sensor_conf; + struct peci_sensor_data *sensor_data; + int ret; + + if (!priv) + return -EINVAL; + + if (!peci_dimmpower_is_channel_valid(type, channel)) + return -EOPNOTSUPP; + + switch (type) { + case hwmon_power: + ret = peci_sensor_get_ctx(attr, peci_dimmpower_power_cfg[channel], + &sensor_conf, + priv->power_sensor_data_list[channel], + &sensor_data, + ARRAY_SIZE(peci_dimmpower_power_cfg[channel])); + break; + case hwmon_energy: + ret = peci_sensor_get_ctx(attr, peci_dimmpower_energy_cfg[channel], + &sensor_conf, + priv->energy_sensor_data_list[channel], + &sensor_data, + ARRAY_SIZE(peci_dimmpower_energy_cfg[channel])); + break; + default: + ret = -EOPNOTSUPP; + } + + if (ret) + return ret; + + if (sensor_conf->write) { + ret = sensor_conf->write(priv, sensor_conf, sensor_data, + (s32)val); + } else { + ret = -EOPNOTSUPP; + } + + return ret; +} + +static umode_t +peci_dimmpower_is_visible(const void *data, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + struct peci_sensor_conf *sensor_conf; + umode_t mode = 0; + int ret; + + if (!peci_dimmpower_is_channel_valid(type, channel)) + return mode; + + if (attr == hwmon_power_label || attr == hwmon_energy_label) + return 0444; + + switch (type) { + case hwmon_power: + ret = peci_sensor_get_ctx(attr, peci_dimmpower_power_cfg[channel], + &sensor_conf, NULL, NULL, + ARRAY_SIZE(peci_dimmpower_power_cfg[channel])); + break; + case hwmon_energy: + ret = peci_sensor_get_ctx(attr, peci_dimmpower_energy_cfg[channel], + &sensor_conf, NULL, NULL, + ARRAY_SIZE(peci_dimmpower_energy_cfg[channel])); + break; + default: + return mode; + } + + if (!ret) { + if (sensor_conf->read) + mode |= 0444; + if (sensor_conf->write) + mode |= 0200; + } + + return mode; +} + +static const struct hwmon_ops peci_dimmpower_ops = { + .is_visible = peci_dimmpower_is_visible, + .read_string = peci_dimmpower_read_string, + .read = peci_dimmpower_read, + .write = peci_dimmpower_write, +}; + +static int peci_dimmpower_probe(struct platform_device *pdev) +{ + struct peci_client_manager *mgr = dev_get_drvdata(pdev->dev.parent); + struct device *dev = &pdev->dev; + struct peci_dimmpower *priv; + struct device *hwmon_dev; + u32 power_config_idx = 0; + u32 energy_config_idx = 0; + u32 cmd_mask; + + cmd_mask = BIT(PECI_CMD_RD_PKG_CFG) | BIT(PECI_CMD_WR_PKG_CFG); + if ((mgr->client->adapter->cmd_mask & cmd_mask) != cmd_mask) + return -ENODEV; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + dev_set_drvdata(dev, priv); + priv->mgr = mgr; + priv->dev = dev; + + snprintf(priv->name, PECI_NAME_SIZE, "peci_dimmpower.cpu%d", + mgr->client->addr - PECI_BASE_ADDR); + + priv->power_config[power_config_idx] = HWMON_P_LABEL | + peci_sensor_get_config(peci_dimmpower_power_cfg[power_config_idx], + ARRAY_SIZE(peci_dimmpower_power_cfg[power_config_idx])); + + priv->energy_config[energy_config_idx] = HWMON_E_LABEL | + peci_sensor_get_config(peci_dimmpower_energy_cfg[energy_config_idx], + ARRAY_SIZE(peci_dimmpower_energy_cfg[energy_config_idx])); + + priv->info[PECI_DIMMPOWER_SENSOR_TYPE_POWER] = &priv->power_info; + priv->power_info.type = hwmon_power; + priv->power_info.config = priv->power_config; + + priv->info[PECI_DIMMPOWER_SENSOR_TYPE_ENERGY] = &priv->energy_info; + priv->energy_info.type = hwmon_energy; + priv->energy_info.config = priv->energy_config; + + priv->chip.ops = &peci_dimmpower_ops; + priv->chip.info = priv->info; + + hwmon_dev = devm_hwmon_device_register_with_info(priv->dev, priv->name, + priv, &priv->chip, + NULL); + + if (IS_ERR(hwmon_dev)) + return PTR_ERR(hwmon_dev); + + dev_dbg(dev, "%s: sensor '%s'\n", dev_name(hwmon_dev), priv->name); + + return 0; +} + +static const struct platform_device_id peci_dimmpower_ids[] = { + { .name = "peci-dimmpower", .driver_data = 0 }, + { } +}; +MODULE_DEVICE_TABLE(platform, peci_dimmpower_ids); + +static struct platform_driver peci_dimmpower_driver = { + .probe = peci_dimmpower_probe, + .id_table = peci_dimmpower_ids, + .driver = { .name = KBUILD_MODNAME, }, +}; +module_platform_driver(peci_dimmpower_driver); + +MODULE_AUTHOR("Zbigniew Lukwinski <zbigniew.lukwinski@linux.intel.com>"); +MODULE_DESCRIPTION("PECI dimmpower driver"); +MODULE_LICENSE("GPL v2"); |