TI TPS38900x are voltage monitor IC's.
All device variants offer an I2C interface and depending on the
part number, x monitor inputs.
Support chips by the driver are TPS389008, TPS389006 and TPS389004.
The driver adds support for reading the 8-bit ADC value of any of
the VMON inputs. By default the inputs are disabled and have a scalingDefault must be the current chip configuration.
factor of 1x. They need to be enabled in the device tree, or using the
sysfs attribute from user space.
Signed-off-by: Flaviu Nistor <flaviu.nistor@xxxxxxxxx>
---
Documentation/hwmon/index.rst | 1 +
Documentation/hwmon/tps389008.rst | 56 ++++
MAINTAINERS | 8 +
drivers/hwmon/Kconfig | 10 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/tps389008.c | 466 ++++++++++++++++++++++++++++++
6 files changed, 542 insertions(+)
create mode 100644 Documentation/hwmon/tps389008.rst
create mode 100644 drivers/hwmon/tps389008.c
diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index 874f8fd26325..b04643d14972 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -239,6 +239,7 @@ Hardware Monitoring Kernel Drivers
tmp513
tps23861
tps25990
+ tps389008
tps40422
tps53679
tps546d24
diff --git a/Documentation/hwmon/tps389008.rst b/Documentation/hwmon/tps389008.rst
new file mode 100644
index 000000000000..6e1166165ac4
--- /dev/null
+++ b/Documentation/hwmon/tps389008.rst
@@ -0,0 +1,56 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+Kernel driver tps389008
+======================
+
+Supported chips:
+
+ * TI TPS389008, TPS389006, TPS389004
+
+ Prefix: 'tps389008'
+
+ Addresses scanned: -
+ + Datasheet: https://www.ti.com/lit/ds/symlink/tps389006.pdf?ts=1741000787840&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FTPS389006
+
+Author:
+
+ - Flaviu Nistor <flaviu.nistor@xxxxxxxxx>
+
+Description
+-----------
+
+This driver implements support for TI TPS389008, TPS389006 and TPS389004 voltage monitor chips.
+The driver supports only the chips that have default values based on datasheet and not OTP NVM settings.
+Monitored voltages can be read out via an internal ADC with one register per input channel.
+Measured voltage is expressed in mV per LSB.
+The measurement voltage ranges depends on the scaling factor used as following:
+
+ - 1x scaling: 200 to 1475 mV (8-bit resolution)
+ - 4x scaling: 800 to 5900 mV (8-bit resolution)
+
+The scaling factor is 1 by default for all channels.
+
+All input VMON channel are disabled by default, and they can be enabled via the dts (during probe)
+or using the provided sysfs attribute from user space.
+
+The device communicates with the I2C protocol and uses the I2C address 0x30 by default.
+
+
+Known Issues
+------------
+
+The driver does not support usage of alarms and setting of thresholds (for the alarms).
+
+sysfs-Interface
+---------------
+
+The following list includes the sysfs attributes that the driver will provide for each added input channel:
+
+=============================== ======= ========================================
+Name Perm Description
+=============================== ======= ========================================
+in[12345678]_input: RO Voltage channel input
+in[12345678]_label: RO Voltage channel label
+in[12345678]_enable: RW Voltage channel enable controls
+=============================== ======= ========================================
diff --git a/MAINTAINERS b/MAINTAINERS
index 16f51eb6ebe8..fbf07f26d933 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23510,6 +23510,14 @@ F: Documentation/devicetree/bindings/hwmon/ti,tps23861.yaml
F: Documentation/hwmon/tps23861.rst
F: drivers/hwmon/tps23861.c
+TEXAS INSTRUMENTS TPS389008 VMON DRIVER
+M: Flaviu Nistor <flaviu.nistor@xxxxxxxxx>
+L: linux-hwmon@xxxxxxxxxxxxxxx
+S: Maintained
+F: Documentation/devicetree/bindings/hwmon/ti,tps389008.yaml
+F: Documentation/hwmon/tps389008.rst
+F: drivers/hwmon/tps389008.c
+
TEXAS INSTRUMENTS' DAC7612 DAC DRIVER
M: Ricardo Ribalda <ribalda@xxxxxxxxxx>
L: linux-iio@xxxxxxxxxxxxxxx
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 4cbaba15d86e..5562eea4d0bb 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1340,6 +1340,16 @@ config SENSORS_TPS23861
This driver can also be built as a module. If so, the module
will be called tps23861.
+config SENSORS_TPS389008
+ tristate "TI TPS389008 VMON Driver"
+ depends on I2C
+ help
+ This driver provides support for voltage monitoring for the Texas
+ Instruments TPS389008, TPS389006 and TPS389004 chips.
+
+ This driver can also be built as a module. If so, the module
+ will be called tps389008.
+
config SENSORS_MENF21BMC_HWMON
tristate "MEN 14F021P00 BMC Hardware Monitoring"
depends on MFD_MENF21BMC
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index b7ef0f0562d3..6eb0b7696239 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -164,6 +164,7 @@ obj-$(CONFIG_SENSORS_MC34VR500) += mc34vr500.o
obj-$(CONFIG_SENSORS_MCP3021) += mcp3021.o
obj-$(CONFIG_SENSORS_TC654) += tc654.o
obj-$(CONFIG_SENSORS_TPS23861) += tps23861.o
+obj-$(CONFIG_SENSORS_TPS389008) += tps389008.o
obj-$(CONFIG_SENSORS_MLXREG_FAN) += mlxreg-fan.o
obj-$(CONFIG_SENSORS_MENF21BMC_HWMON) += menf21bmc_hwmon.o
obj-$(CONFIG_SENSORS_MR75203) += mr75203.o
diff --git a/drivers/hwmon/tps389008.c b/drivers/hwmon/tps389008.c
new file mode 100644
index 000000000000..6ee6c3b58747
--- /dev/null
+++ b/drivers/hwmon/tps389008.c
@@ -0,0 +1,466 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * tps389008.c Support for the TI TPS389008 Voltage Monitor
+ *
+ * Part numbers supported:
+ * TPS389006, TPS389008
+ *
+ * Author: Flaviu Nistor <flaviu.nistor@xxxxxxxxx>
+ *
+ * Datasheet and application notes:
+ * https://www.ti.com/
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/hwmon.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/module.h>
+#include <linux/regulator/consumer.h>
+
+#define TPS389008_NUM_CHANNELS 8
+#define TPS389008_1LSB 5
+#define RANGE_OFFSET 200
+#define BANK0 0
+#define BANK1 1
+/*BANK0 REGISTERS*/
+#define MON_LVL 0x40
+#define BANK_SEL 0xF0
+/*BANK1 REGISTERS
+ * use 0x100 to signal that address is part of BANK1
+ */
+#define MON_CH_EN 0x11E
+
+struct tps389008_input {
+ const char *label;
+ int vrange_mult_mv;
+ bool enabled;
+ bool disconnected;
+};
+
+struct tps389008_data {
+ struct device *hwmon;
+ struct i2c_client *client;
+ struct tps389008_input input[TPS389008_NUM_CHANNELS];
+ struct mutex dev_access_lock; /* device access lock */
+ const char *name;
+ int current_bank;
+};
+
+enum tps_chan_addr {
+ TPS_CHANNEL_0 = 0,
+ TPS_CHANNEL_1,
+ TPS_CHANNEL_2,
+ TPS_CHANNEL_3,
+ TPS_CHANNEL_4,
+ TPS_CHANNEL_5,
+ TPS_CHANNEL_6,
+ TPS_CHANNEL_7,
+ TPS_CHANNEL_8
+};
+
+static int tps389008_read_string(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, const char **str)
+{
+ struct tps389008_data *tps = dev_get_drvdata(dev);
+ int index;
+
+ index = channel - 1;
+ *str = tps->input[index].label;
+
+ return 0;
+}
+
+static umode_t tps389008_is_visible(const void *data, enum hwmon_sensor_types type,
+ u32 attr, int channel)
+{
+ const struct tps389008_data *tps = data;
+ const struct tps389008_input *input = NULL;
+ int index;
+
+ /* channel numbering starts from 1, but index from 0*/
+ index = channel - 1;
+
+ /* in0_ or disconnected channels should be ignored*/
+ if (channel == 0 ||
+ (tps->input[index].disconnected ||
+ tps->input[index].vrange_mult_mv == 0)) {
+ return 0;
+ }
+ switch (attr) {
+ case hwmon_in_input:
+ return 0444;
+ case hwmon_in_label:
+ input = &tps->input[index];
+ /* Hide label node if label is not provided */
+ return (input && input->label) ? 0444 : 0;
+ case hwmon_in_enable:
+ return 0644;
+ default:
+ return 0;
+ }
+}
+
+static int change_bank(struct tps389008_data *data, u8 bank)
+{
+ int ret;
+
+ ret = i2c_smbus_write_byte_data(data->client, BANK_SEL, bank);
+ if (ret < 0) {
+ dev_err(&data->client->dev,
+ "change to bank%d failed with error code: %d\n", bank, ret);
+ return ret;
+ }
+
+ return ret;
+}
+
+static int tps389008_write_reg(struct tps389008_data *data, u16 reg, u8 val)
+{
+ int ret;
+ u8 bank;
+
+ bank = (reg & 0x100) >> 8;
+
+ if (bank != (u8)data->current_bank) {
+ change_bank(data, bank);
+ data->current_bank = bank;
+ }
+
+ ret = i2c_smbus_write_byte_data(data->client, reg, val);
+ if (ret < 0) {
+ dev_err(&data->client->dev,
+ "I2C write failed at address: 0x%X with error code: %d\n",
+ reg, ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int tps389008_read_reg(struct tps389008_data *data, u16 reg, u16 *val)
+{
+ int read_byte;
+ u8 bank;
+
+ bank = (reg & 0x100) >> 8;
+
+ if (bank != (u8)data->current_bank) {
+ change_bank(data, bank);
+ data->current_bank = bank;
+ }
+
+ read_byte = i2c_smbus_read_byte_data(data->client, reg);
+ if (read_byte < 0) {
+ dev_err(&data->client->dev,
+ "I2C read failed at address: 0x%X with error code: %d\n", reg, read_byte);
+ return read_byte;
+ }
+
+ *val = read_byte;
+
+ return 0;
+}
+
+static int tps389008_get_in_val(struct tps389008_data *data, u16 reg, int channel, long *val)
+{
+ u16 reg_val;
+ int ret;
+ int index;
+
+ /* channel numbering starts from 1, but index from 0*/
+ index = channel - 1;
+
+ ret = tps389008_read_reg(data, reg, ®_val);
+ if (ret)
+ return ret;
+
+ *val = ((reg_val * TPS389008_1LSB + RANGE_OFFSET) * data->input[index].vrange_mult_mv);
+
+ return 0;
+}
+
+static int disable_input(struct tps389008_data *data, u8 channel)
+{
+ u16 reg_val;
+ int ret;
+ int index;
+
+ index = channel - 1;
+
+ ret = tps389008_read_reg(data, MON_CH_EN, ®_val);
+ if (ret)
+ return ret;
+
+ reg_val = reg_val & ~(1 << index);
+
+ ret = tps389008_write_reg(data, MON_CH_EN, reg_val);
+
+ return ret;
+}
+
+static int enable_input(struct tps389008_data *data, u8 channel)
+{
+ u16 reg_val;
+ int ret;
+ int index;
+
+ index = channel - 1;
+
+ ret = tps389008_read_reg(data, MON_CH_EN, ®_val);
+ if (ret)
+ return ret;
+
+ reg_val = reg_val | (1 << index);
+
+ ret = tps389008_write_reg(data, MON_CH_EN, reg_val);
+
+ return ret;
+}
+
+static int tps389008_write(struct device *dev, enum hwmon_sensor_types type, u32 attr,
+ int channel, long val)
+{
+ struct tps389008_data *data = dev_get_drvdata(dev);
+ int index;
+ int ret;
+
+ /* channel numbering starts from 1, but index from 0*/
+ index = channel - 1;
+
+ guard(mutex)(&data->dev_access_lock);
+
+ if (attr == hwmon_in_input || attr == hwmon_in_label) {
+ dev_warn(dev, "Write to READ ONLY resource\n");
+ return -EOPNOTSUPP;
+ }
+ if (attr == hwmon_in_enable) {
+ if (val == 0) {
+ data->input[index].enabled = val;
+ ret = disable_input(data, channel);
+ return ret;
+ } else if (val == 1) {
+ data->input[index].enabled = val;
+ ret = enable_input(data, channel);
+ return ret;
+ }
+ dev_err(dev, "invalid value %ld\n", val);
+ return -EINVAL;
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static int tps389008_read(struct device *dev, enum hwmon_sensor_types type, u32 attr,
+ int channel, long *val)
+{
+
+ struct tps389008_data *data = dev_get_drvdata(dev);
+ int ret;
+ int index;
+
+ /* channel numbering starts from 1, but index from 0*/
+ index = channel - 1;
+
+ guard(mutex)(&data->dev_access_lock);
+
+ switch (attr) {
+ case hwmon_in_input:
+ ret = tps389008_get_in_val(data, MON_LVL + index, channel, val);
+ if (ret)
+ dev_err(dev,
+ "Reading the ADC value for channel %d failed with error code: %d\n",
+ channel, ret);
+ break;
+ case hwmon_in_enable:
+ *val = data->input[index].enabled;
+ break;
+ default:
+ ret = -EOPNOTSUPP;
+ break;
+ }
+
+ return ret;
+}
+
+static const struct hwmon_channel_info *tps389008_info[] = {
+ HWMON_CHANNEL_INFO(in,
+ /* 0: dummy, skipped in is_visible */
+ HWMON_I_INPUT,
+ /* 1-8: input voltage Channels */
+ HWMON_I_INPUT | HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_ENABLE | HWMON_I_LABEL),
+ NULL
+};
+
+static const struct hwmon_ops tps389008_hwmon_ops = {
+ .is_visible = tps389008_is_visible,
+ .read_string = tps389008_read_string,
+ .read = tps389008_read,
+ .write = tps389008_write,
+};
+
+static const struct hwmon_chip_info tps389008_chip_info = {
+ .ops = &tps389008_hwmon_ops,
+ .info = tps389008_info,
+};
+
+
+static int tps389008_probe_child_from_dt(struct device *dev,
+ struct device_node *child,
+ struct tps389008_data *tps)
+{
+ struct tps389008_input *input;
+ u32 val;
+ int ret;
+
+ ret = of_property_read_u32(child, "reg", &val);
+ if (ret) {
+ dev_err(dev, "missing reg property of %pOFn\n", child);
+ return ret;
+ }
+ if (val < 1 || val > TPS_CHANNEL_8) {
+ dev_err(dev, "invalid reg %d of %pOFn\n", val, child);
+ return -EINVAL;
+ }
+
+ /* remember that children nodes starts from 1, but we have the input start index 0.*/
+ input = &tps->input[val-1];
+
+ ret = (int)of_property_read_bool(child, "ti,vrange-mult-4x");
+ if (ret)
+ input->vrange_mult_mv = 4;
+ else
+ input->vrange_mult_mv = 1;
+
+ ret = (int)of_property_read_bool(child, "ti,vmon-enable");
+ /* missing optional property. Default enable the channel*/
+ if (ret)
+ input->enabled = true;
+
+ /* Log the disconnected channel input */
+ if (!of_device_is_available(child)) {
+ input->disconnected = true;
+ return 0;
+ }
+
+ /* Save the connected input label if available */
+ of_property_read_string(child, "label", &input->label);
+
+ return 0;
+}
+
+static int tps389008_probe_from_dt(struct device *dev, struct tps389008_data *tps)
+{
+ const struct device_node *np = dev->of_node;
+ struct device_node *child;
+ int ret;
+
+ for_each_child_of_node(np, child) {
+ ret = tps389008_probe_child_from_dt(dev, child, tps);
+ if (ret) {
+ of_node_put(child);
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static int tps389008_probe(struct i2c_client *client)
+{
+ struct tps389008_data *data;
+ struct device *dev = &client->dev;
+ int ret, i;
+
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
+ return -EOPNOTSUPP;
+
+ data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+
+ i2c_set_clientdata(client, data);
+ mutex_init(&data->dev_access_lock);
+ data->client = client;
+
+ ret = tps389008_probe_from_dt(dev, data);
+ if (ret) {
+ dev_err(dev, "Unable to probe from device tree\n");
+ return ret;
+ }
+
+ data->current_bank = 0;
+
+ /* Enable channels if they are enabled */
+ for (i = 0; i < TPS389008_NUM_CHANNELS; i++) {
+ if (data->input[i].enabled) {
+ enable_input(data, i+1);
+ dev_dbg(dev, "VMON input %d is enabled\n", (i+1));Noise; please drop.
+ }
+ }
+
+ data->hwmon = devm_hwmon_device_register_with_info(dev, client->name,
+ data, &tps389008_chip_info,
+ NULL);
+
+ if (IS_ERR(data->hwmon))
+ return dev_err_probe(dev, PTR_ERR(data->hwmon),
+ "Failed to register hwmon device tps389008\n");
+
+ dev_info(dev, "hwmon device tps389008 probed successfully\n");
+
+ return 0;
+}
+
+static void tps389008_remove(struct i2c_client *client)
+{
+ struct tps389008_data *tps = dev_get_drvdata(&client->dev);
+
+ hwmon_device_unregister(tps->hwmon);
+
+ mutex_destroy(&tps->dev_access_lock);
+}
+
+static const struct i2c_device_id tps389008_ids[] = {
+ { "tps389008" },
+ { "tps389006" },
+ { "tps389004" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, tps389008_ids);
+
+static const struct of_device_id tps389008_of_match[] = {
+ { .compatible = "ti,tps389008", },
+ { .compatible = "ti,tps389006", },
+ { .compatible = "ti,tps389004", },
+ { }
+};
+MODULE_DEVICE_TABLE(of, tps389008_of_match);
+
+static struct i2c_driver tps389008_i2c_driver = {
+ .driver = {
+ .name = "tps389008",
+ .of_match_table = tps389008_of_match,
+ },
+ .probe = tps389008_probe,
+ .remove = tps389008_remove,
+ .id_table = tps389008_ids,
+};
+
+module_i2c_driver(tps389008_i2c_driver);
+
+MODULE_AUTHOR("Flaviu Nistor <flaviu.nistor@xxxxxxxxx>");
+MODULE_DESCRIPTION("TI TPS389008 voltage monitor driver");
+MODULE_LICENSE("GPL");