hwmon: (pmbus/ltc4286) Add writable shunt_resistor sysfs attribute

The LTC4286/LTC4287 driver uses rsense to scale current and power
telemetry. rsense is board-specific rather than a fixed device
characteristic, and may differ across qualified BOM variants using the
same software image.

If rsense does not match the installed board configuration, current and
power readings are mis-scaled.

Add a writable shunt_resistor sysfs attribute so userspace can update
the board calibration value after platform identification. Initialize
rsense from shunt-resistor-micro-ohms as before (or the existing default
when absent), and validate runtime updates before recalculating the
current/power scaling coefficients.

Signed-off-by: Carl Lee <carl.lee@amd.com>
Link: https://patch.msgid.link/20260907-hwmon-pmbus-ltc4286-add-runtime-shunt-resistor-control-v2-1-5f47f7e5a1bb@amd.com
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
This commit is contained in:
Carl Lee 2026-09-07 15:58:32 +08:00 committed by Guenter Roeck
parent 2419568804
commit bc8c5bfc10

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <linux/err.h>
#include <linux/hwmon-sysfs.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/kernel.h>
@ -16,6 +17,85 @@
#define LTC4286_MFR_ID_SIZE 3
struct ltc4286_data {
struct pmbus_driver_info info;
u32 rsense;
bool vrange_low_enable;
};
#define to_ltc4286_data(x) container_of((x), struct ltc4286_data, info)
static ssize_t ltc4286_rsense_show(struct device *dev,
struct device_attribute *devattr,
char *buf)
{
struct i2c_client *client = to_i2c_client(dev->parent);
const struct pmbus_driver_info *info = pmbus_get_driver_info(client);
struct ltc4286_data *data = to_ltc4286_data(info);
u32 rsense;
int ret;
ret = pmbus_lock_interruptible(client);
if (ret)
return ret;
rsense = data->rsense;
pmbus_unlock(client);
return sysfs_emit(buf, "%u\n", rsense);
}
static ssize_t ltc4286_rsense_store(struct device *dev,
struct device_attribute *devattr,
const char *buf, size_t count)
{
struct i2c_client *client = to_i2c_client(dev->parent);
const struct pmbus_driver_info *info_ro = pmbus_get_driver_info(client);
struct ltc4286_data *data = to_ltc4286_data(info_ro);
struct pmbus_driver_info *info = &data->info;
u32 rsense;
int ret;
ret = kstrtou32(buf, 10, &rsense);
if (ret)
return ret;
if (rsense == 0)
return -EINVAL;
if (rsense > (INT_MAX / 1024))
return -EINVAL;
ret = pmbus_lock_interruptible(client);
if (ret)
return ret;
data->rsense = rsense;
info->m[PSC_CURRENT_OUT] = 1024 * rsense;
info->m[PSC_POWER] = data->vrange_low_enable ? 4 * rsense : rsense;
pmbus_unlock(client);
return count;
}
static SENSOR_DEVICE_ATTR_RW(shunt_resistor, ltc4286_rsense, 0);
static struct attribute *ltc4286_attrs[] = {
&sensor_dev_attr_shunt_resistor.dev_attr.attr,
NULL,
};
static const struct attribute_group ltc4286_group = {
.attrs = ltc4286_attrs,
};
static const struct attribute_group *ltc4286_attribute_groups[] = {
&ltc4286_group,
NULL,
};
/*
* Initialize the MBR as default settings which is referred to LTC4286 datasheet
* (March 22, 2022 version) table 3 page 16
@ -55,6 +135,7 @@ static struct pmbus_driver_info ltc4286_info = {
.func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT |
PMBUS_HAVE_PIN | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_VOUT |
PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_TEMP,
.groups = ltc4286_attribute_groups,
};
static const struct i2c_device_id ltc4286_id[] = {
@ -69,6 +150,7 @@ static int ltc4286_probe(struct i2c_client *client)
int ret;
const struct i2c_device_id *mid;
u8 block_buffer[I2C_SMBUS_BLOCK_MAX + 1];
struct ltc4286_data *data;
struct pmbus_driver_info *info;
u32 rsense;
int vrange_nval, vrange_oval;
@ -114,11 +196,14 @@ static int ltc4286_probe(struct i2c_client *client)
if (rsense > (INT_MAX / 1024))
return -EINVAL;
info = devm_kmemdup(&client->dev, &ltc4286_info, sizeof(*info),
GFP_KERNEL);
if (!info)
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->info = ltc4286_info;
data->rsense = rsense;
info = &data->info;
/* Check MFR1 CONFIG register bit 1 VRANGE_SELECT before driver loading */
vrange_oval = i2c_smbus_read_word_data(client, LTC4286_MFR_CONFIG1);
if (vrange_oval < 0)
@ -126,7 +211,10 @@ static int ltc4286_probe(struct i2c_client *client)
"Failed to read manufacturer configuration one\n");
vrange_nval = vrange_oval;
if (device_property_read_bool(&client->dev, "adi,vrange-low-enable")) {
data->vrange_low_enable =
device_property_read_bool(&client->dev, "adi,vrange-low-enable");
if (data->vrange_low_enable) {
vrange_nval &=
~VRANGE_SELECT_BIT; /* VRANGE_SELECT = 0, 25.6 volts */