[PATCH v2] hwmon: (pmbus/ltc4286) Add writable shunt_resistor sysfs attribute
From: Carl Lee
Date: Mon Sep 07 2026 - 04:06:35 EST
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@xxxxxxx>
---
This series adds runtime shunt resistor calibration support for
ltc4286/ltc4287 via a writable sysfs attribute.
Practical use case: a single software image may be deployed across
qualified platform/BOM variants with different shunt resistor values.
After platform identification in userspace, rsense can be updated once
to match installed hardware, preventing mis-scaled current/power
telemetry.
---
Changes in v2:
- Clarify the practical deployment use case and refine commit message wording.
- Protect shunt_resistor show path with PMBus lock to avoid read/write race with store.
- Link to v1: https://lore.kernel.org/r/20260903-hwmon-pmbus-ltc4286-add-runtime-shunt-resistor-control-v1-1-aca2d49a1418@xxxxxxx
---
drivers/hwmon/pmbus/ltc4286.c | 96 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 92 insertions(+), 4 deletions(-)
diff --git a/drivers/hwmon/pmbus/ltc4286.c b/drivers/hwmon/pmbus/ltc4286.c
index 8715d380784a..8190706d1beb 100644
--- a/drivers/hwmon/pmbus/ltc4286.c
+++ b/drivers/hwmon/pmbus/ltc4286.c
@@ -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[] = {
+ <c4286_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, <c4286_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 */
---
base-commit: 32b6ef9a5d0eca44f9cd91f52f4faa89f145a0de
change-id: 20260903-hwmon-pmbus-ltc4286-add-runtime-shunt-resistor-control-44b1fcc18d38
Best regards,
--
Carl Lee <carl.lee@xxxxxxx>