[PATCH v1 1/2] hwmon: (ltc4271) new driver for LTC4271 PoE PSE controller

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Driver for Analog Devices LTC4271 PoE PSE controller with I2C interface.
The device monitors voltage, current (via shunt resistor) and controls
power for each port.

Signed-off-by: Lothar Felten <lothar.felten@xxxxxxxxx>
---
 Documentation/hwmon/index.rst   |   1 +
 Documentation/hwmon/ltc4271.rst |  65 +++++
 MAINTAINERS                     |   7 +
 drivers/hwmon/Kconfig           |  11 +
 drivers/hwmon/Makefile          |   1 +
 drivers/hwmon/ltc4271.c         | 449 ++++++++++++++++++++++++++++++++
 6 files changed, 534 insertions(+)
 create mode 100644 Documentation/hwmon/ltc4271.rst
 create mode 100644 drivers/hwmon/ltc4271.c

diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index f1fe75f59..0724b04fc 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -124,6 +124,7 @@ Hardware Monitoring Kernel Drivers
    ltc4245
    ltc4260
    ltc4261
+   ltc4271
    max127
    max15301
    max16064
diff --git a/Documentation/hwmon/ltc4271.rst b/Documentation/hwmon/ltc4271.rst
new file mode 100644
index 000000000..e65bc325b
--- /dev/null
+++ b/Documentation/hwmon/ltc4271.rst
@@ -0,0 +1,65 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+Kernel driver ltc4271
+======================
+
+Supported chips:
+  * Analog Devices LTC4271
+
+    Prefix: 'ltc4271'
+
+    Datasheet: https://www.analog.com/en/products/ltc4271.html
+
+Author: Lothar Felten <lothar.felten@xxxxxxxxx>
+
+Description
+-----------
+
+This driver supports hardware monitoring for Analog Devices LTC4271 PoE PSE.
+
+LTC4271 is a quad port IEEE802.3at PSE controller with optional I2C control
+and monitoring capabilities.
+
+This driver provides monitoring as well as enabling/disabling the four ports.
+
+Usage Notes
+-----------
+
+This driver does not probe for I2C devices. You will have to instantiate
+devices explicitly, either by adding nodes to the device tree or by loading
+the driver manually (see below).
+
+Example: the following commands will load the driver for the LTC4271 at address
+0x20 on I2C bus #3:
+
+	# modprobe ltc4271
+	# echo ltc4271 0x20 > /sys/bus/i2c/devices/i2c-3/new_device
+
+The lm-sensors tool can be use to display the current status
+
+Example:
+	# sensors
+	ltc4271-i2c-3-20
+	Adapter: SMBus I801 adapter at f040
+	Port1:        56.06 V
+	Port2:         0.00 V
+	Port3:         0.00 V
+	Port4:         0.00 V
+	Input:        55.57 V
+	Port1:        57.00 mA
+	Port2:         0.00 A
+	Port3:         0.00 A
+	Port4:         0.00 A
+
+Sysfs entries
+-------------
+
+======================= =====================================================================
+in[0-3]_input		Voltage on ports [1-4]
+in[0-3]_label		"Port[1-4]"
+in[0-3]_enable		Enable/disable ports [1-4]
+in4_input		IC input voltage
+in4_label		"Input"
+curr[1-4]_input		Current on ports [1-4]
+curr[1-4]_label		"Port[1-4]"
+======================= =====================================================================
diff --git a/MAINTAINERS b/MAINTAINERS
index c6545eb54..789742390 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12183,6 +12183,13 @@ S:	Maintained
 F:	Documentation/hwmon/ltc4261.rst
 F:	drivers/hwmon/ltc4261.c
 
+LTC4271 ANALOG DEVICES PoE PSE DRIVER
+M:	Lothar Felten <lothar.felten@xxxxxxxxx>
+L:	linux-hwmon@xxxxxxxxxxxxxxx
+S:	Maintained
+F:	Documentation/hwmon/ltc4271.rst
+F:	drivers/hwmon/ltc4271.c
+
 LTC4306 I2C MULTIPLEXER DRIVER
 M:	Michael Hennerich <michael.hennerich@xxxxxxxxxx>
 L:	linux-i2c@xxxxxxxxxxxxxxx
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 5b3b76477..8254987bc 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -995,6 +995,17 @@ config SENSORS_LTC4261
 	  This driver can also be built as a module. If so, the module will
 	  be called ltc4261.
 
+config SENSORS_LTC4271
+	tristate "Analog Devices LTC4271 PoE PSE"
+	depends on I2C
+	select REGMAP_I2C
+	help
+	  If you say yes here you get support for Analog Devices LTC4271
+	  802.3at PoE PSE chips.
+
+	  This driver can also be built as a module. If so, the module
+	  will be called ltc4271.
+
 config SENSORS_LTQ_CPUTEMP
 	bool "Lantiq cpu temperature sensor driver"
 	depends on SOC_XWAY
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 88712b503..8b50361c5 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -132,6 +132,7 @@ obj-$(CONFIG_SENSORS_LTC4222)	+= ltc4222.o
 obj-$(CONFIG_SENSORS_LTC4245)	+= ltc4245.o
 obj-$(CONFIG_SENSORS_LTC4260)	+= ltc4260.o
 obj-$(CONFIG_SENSORS_LTC4261)	+= ltc4261.o
+obj-$(CONFIG_SENSORS_LTC4271)	+= ltc4271.o
 obj-$(CONFIG_SENSORS_LTQ_CPUTEMP) += ltq-cputemp.o
 obj-$(CONFIG_SENSORS_MAX1111)	+= max1111.o
 obj-$(CONFIG_SENSORS_MAX127)	+= max127.o
diff --git a/drivers/hwmon/ltc4271.c b/drivers/hwmon/ltc4271.c
new file mode 100644
index 000000000..a95f5403c
--- /dev/null
+++ b/drivers/hwmon/ltc4271.c
@@ -0,0 +1,449 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Driver for the Analog Devices LTC4271 8/12 port PoE PSE controller
+ *
+ * The LTC4271 controls 8 ports when paired with the LTC4290 or 12 ports when
+ * paired with the LTC4270.
+ * The LTC4271 will appear as separate consecutive devices on the I2C bus
+ * controlling four ports each.
+ *
+ * Derived from the tps23861 driver by Robert Marko
+ *
+ * Copyright (C) 2023 Lothar Felten <lothar.felten@xxxxxxxxx>
+ *
+ * Datasheet: https://www.analog.com/en/products/ltc4271.html
+ */
+
+#include <linux/bitfield.h>
+#include <linux/debugfs.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/hwmon.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/regmap.h>
+
+#define ID                              0x1b
+#define FIRMWARE_REVISION               0x41
+#define VOLTAGE_CURRENT_MASK		GENMASK(13, 0)
+#define INPUT_VOLTAGE_LSB		0x2e
+#define PORT_1_CURRENT_LSB		0x30
+#define PORT_1_VOLTAGE_LSB		0x32
+#define PORT_2_CURRENT_LSB		0x34
+#define PORT_2_VOLTAGE_LSB		0x36
+#define PORT_3_CURRENT_LSB		0x38
+#define PORT_3_VOLTAGE_LSB		0x3a
+#define PORT_4_CURRENT_LSB		0x3c
+#define PORT_4_VOLTAGE_LSB		0x3e
+#define PORT_N_CURRENT_LSB_OFFSET	0x04
+#define PORT_N_VOLTAGE_LSB_OFFSET	0x04
+#define PORT_1_STATUS			0x0c
+#define PORT_2_STATUS			0x0d
+#define PORT_3_STATUS			0x0e
+#define PORT_4_STATUS			0x0f
+#define PORT_STATUS_CLASS_MASK		GENMASK(6, 4)
+#define PORT_STATUS_DETECT_MASK		GENMASK(2, 0)
+#define PORT_CLASS_UNKNOWN		0
+#define PORT_CLASS_1			1
+#define PORT_CLASS_2			2
+#define PORT_CLASS_3			3
+#define PORT_CLASS_4			4
+#define PORT_CLASS_RESERVED		5
+#define PORT_CLASS_0			6
+#define PORT_CLASS_OVERCURRENT		7
+#define PORT_DETECT_UNKNOWN		0
+#define PORT_DETECT_SHORT		1
+#define PORT_DETECT_RESERVED		2
+#define PORT_DETECT_RESISTANCE_LOW	3
+#define PORT_DETECT_RESISTANCE_OK	4
+#define PORT_DETECT_RESISTANCE_HIGH	5
+#define PORT_DETECT_OPEN_CIRCUIT	6
+#define PORT_DETECT_RESERVED_2		7
+#define PORT_DETECT_MOSFET_FAULT	8
+#define PORT_DETECT_LEGACY		9
+/* Measurement beyond clamp voltage */
+#define PORT_DETECT_CAPACITANCE_INVALID_BEYOND	10
+/* Insufficient voltage delta */
+#define PORT_DETECT_CAPACITANCE_INVALID_DELTA	11
+#define PORT_DETECT_CAPACITANCE_OUT_OF_RANGE	12
+
+#define DETECT_CLASS_RESTART		0x18
+#define POWER_ENABLE			0x19
+#define LTC4271_NUM_PORTS		4
+
+#define VOLTAGE_LSB			5835 /* 5.835 mV */
+#define SHUNT_RESISTOR_DEFAULT		250000 /* 250 mOhm */
+#define SHUNT_RESISTOR_250MOHMS		250000 /* 250 mOhm */
+#define SHUNT_RESISTOR_500MOHMS		500000 /* 500 mOhm */
+#define CURRENT_LSB_250			122070 /* 122.07 uA */
+#define CURRENT_LSB_500			61035 /* 61.035 uA */
+
+struct ltc4271_data {
+	struct regmap *regmap;
+	u32 shunt_resistor;
+	struct i2c_client *client;
+	struct dentry *debugfs_dir;
+};
+
+static struct regmap_config ltc4271_regmap_config = {
+	.reg_bits = 8,
+	.val_bits = 8,
+	.use_single_read = true,
+	.use_single_write = true,
+	.max_register = 0xed,
+};
+
+static int ltc4271_read_voltage(struct ltc4271_data *data, int channel,
+				 long *val)
+{
+	__le16 regval;
+	long raw_val;
+	int err;
+
+	if (channel < LTC4271_NUM_PORTS) {
+		err = regmap_bulk_read(data->regmap,
+				       PORT_1_VOLTAGE_LSB + channel * PORT_N_VOLTAGE_LSB_OFFSET,
+				       &regval, 2);
+	} else {
+		err = regmap_bulk_read(data->regmap,
+				       INPUT_VOLTAGE_LSB,
+				       &regval, 2);
+	}
+	if (err < 0)
+		return err;
+
+	raw_val = le16_to_cpu(regval);
+	*val = (FIELD_GET(VOLTAGE_CURRENT_MASK, raw_val) * VOLTAGE_LSB) / 1000;
+
+	return 0;
+}
+
+static int ltc4271_read_current(struct ltc4271_data *data, int channel,
+				 long *val)
+{
+	long raw_val, current_lsb;
+	__le16 regval;
+
+	int err;
+
+	if (data->shunt_resistor == SHUNT_RESISTOR_DEFAULT)
+		current_lsb = CURRENT_LSB_250;
+	else
+		current_lsb = CURRENT_LSB_500;
+
+	err = regmap_bulk_read(data->regmap,
+			       PORT_1_CURRENT_LSB + channel * PORT_N_CURRENT_LSB_OFFSET,
+			       &regval, 2);
+	if (err < 0)
+		return err;
+
+	raw_val = le16_to_cpu(regval);
+	*val = (FIELD_GET(VOLTAGE_CURRENT_MASK, raw_val) * current_lsb) / 1000000;
+
+	return 0;
+}
+
+static int ltc4271_port_disable(struct ltc4271_data *data, int channel)
+{
+	unsigned int regval = 0;
+
+	regval |= BIT(channel + 4);
+
+	return regmap_write(data->regmap, POWER_ENABLE, regval);
+}
+
+static int ltc4271_port_enable(struct ltc4271_data *data, int channel)
+{
+	unsigned int regval = 0;
+
+	regval |= BIT(channel);
+	regval |= BIT(channel + 4);
+
+	return regmap_write(data->regmap, DETECT_CLASS_RESTART, regval);
+}
+
+static umode_t ltc4271_is_visible(const void *data, enum hwmon_sensor_types type,
+				   u32 attr, int channel)
+{
+	switch (type) {
+	case hwmon_in:
+		switch (attr) {
+		case hwmon_in_input:
+		case hwmon_in_label:
+			return 0444;
+		case hwmon_in_enable:
+			return 0200;
+		}
+	case hwmon_curr:
+		switch (attr) {
+		case hwmon_curr_input:
+		case hwmon_curr_label:
+			return 0444;
+		}
+	}
+
+	return 0;
+}
+
+static int ltc4271_write(struct device *dev, enum hwmon_sensor_types type,
+			  u32 attr, int channel, long val)
+{
+	struct ltc4271_data *data = dev_get_drvdata(dev);
+
+	switch (type) {
+	case hwmon_in:
+		switch (attr) {
+		case hwmon_in_enable:
+			if (val == 0)
+				return ltc4271_port_disable(data, channel);
+			else if (val == 1)
+				return ltc4271_port_enable(data, channel);
+			else
+				return -EINVAL;
+		}
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static int ltc4271_read(struct device *dev, enum hwmon_sensor_types type,
+			 u32 attr, int channel, long *val)
+{
+	struct ltc4271_data *data = dev_get_drvdata(dev);
+
+	switch (type) {
+	case hwmon_in:
+		switch (attr) {
+		case hwmon_in_input:
+			return ltc4271_read_voltage(data, channel, val);
+		}
+	case hwmon_curr:
+		switch (attr) {
+		case hwmon_curr_input:
+			return ltc4271_read_current(data, channel, val);
+		}
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static const char * const ltc4271_port_label[] = {
+	"Port1",
+	"Port2",
+	"Port3",
+	"Port4",
+	"Input",
+};
+
+static int ltc4271_read_string(struct device *dev,
+				enum hwmon_sensor_types type,
+				u32 attr, int channel, const char **str)
+{
+	switch (type) {
+	case hwmon_in:
+	case hwmon_curr:
+		*str = ltc4271_port_label[channel];
+
+		return 0;
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static const struct hwmon_channel_info *ltc4271_info[] = {
+	HWMON_CHANNEL_INFO(in,
+			   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_LABEL),
+	HWMON_CHANNEL_INFO(curr,
+			   HWMON_C_INPUT | HWMON_C_LABEL,
+			   HWMON_C_INPUT | HWMON_C_LABEL,
+			   HWMON_C_INPUT | HWMON_C_LABEL,
+			   HWMON_C_INPUT | HWMON_C_LABEL),
+	NULL
+};
+
+static const struct hwmon_ops ltc4271_hwmon_ops = {
+	.is_visible = ltc4271_is_visible,
+	.write = ltc4271_write,
+	.read = ltc4271_read,
+	.read_string = ltc4271_read_string,
+};
+
+static const struct hwmon_chip_info ltc4271_chip_info = {
+	.ops = &ltc4271_hwmon_ops,
+	.info = ltc4271_info,
+};
+
+static const char *port_detect_status_string(uint8_t status_reg)
+{
+	switch (FIELD_GET(PORT_STATUS_DETECT_MASK, status_reg)) {
+	case PORT_DETECT_UNKNOWN:
+		return "Unknown device";
+	case PORT_DETECT_SHORT:
+		return "Short circuit";
+	case PORT_DETECT_RESISTANCE_LOW:
+		return "Too low resistance";
+	case PORT_DETECT_RESISTANCE_OK:
+		return "Valid resistance";
+	case PORT_DETECT_RESISTANCE_HIGH:
+		return "Too high resistance";
+	case PORT_DETECT_OPEN_CIRCUIT:
+		return "Open circuit";
+	case PORT_DETECT_MOSFET_FAULT:
+		return "MOSFET fault";
+	case PORT_DETECT_LEGACY:
+		return "Legacy device";
+	case PORT_DETECT_CAPACITANCE_INVALID_BEYOND:
+		return "Invalid capacitance, beyond clamp voltage";
+	case PORT_DETECT_CAPACITANCE_INVALID_DELTA:
+		return "Invalid capacitance, insufficient voltage delta";
+	case PORT_DETECT_CAPACITANCE_OUT_OF_RANGE:
+		return "Valid capacitance, outside of legacy range";
+	case PORT_DETECT_RESERVED:
+	case PORT_DETECT_RESERVED_2:
+	default:
+		return "Invalid";
+	}
+}
+
+static char *port_class_status_string(uint8_t status_reg)
+{
+	switch (FIELD_GET(PORT_STATUS_CLASS_MASK, status_reg)) {
+	case PORT_CLASS_UNKNOWN:
+		return "Unknown";
+	case PORT_CLASS_0:
+		return "0";
+	case PORT_CLASS_1:
+		return "1";
+	case PORT_CLASS_2:
+		return "2";
+	case PORT_CLASS_3:
+		return "3";
+	case PORT_CLASS_4:
+		return "4";
+	case PORT_CLASS_OVERCURRENT:
+		return "Overcurrent";
+	case PORT_CLASS_RESERVED:
+	default:
+		return "Invalid";
+	}
+}
+
+static int ltc4271_port_status_show(struct seq_file *s, void *data)
+{
+	struct ltc4271_data *priv = s->private;
+	unsigned int i, status;
+
+	for (i = 0; i < LTC4271_NUM_PORTS; i++) {
+		regmap_read(priv->regmap, PORT_1_STATUS + i, &status);
+
+		seq_printf(s, "Port: \t\t%d\n", i + 1);
+		seq_printf(s, "Detected: \t%s\n", port_detect_status_string(status));
+		seq_printf(s, "Class: \t\t%s\n", port_class_status_string(status));
+		seq_putc(s, '\n');
+	}
+
+	return 0;
+}
+
+DEFINE_SHOW_ATTRIBUTE(ltc4271_port_status);
+
+static void ltc4271_init_debugfs(struct ltc4271_data *data,
+				  struct device *hwmon_dev)
+{
+	const char *debugfs_name;
+
+	debugfs_name = devm_kasprintf(&data->client->dev, GFP_KERNEL, "%s-%s",
+				      data->client->name, dev_name(hwmon_dev));
+	if (!debugfs_name)
+		return;
+
+	data->debugfs_dir = debugfs_create_dir(debugfs_name, NULL);
+
+	debugfs_create_file("port_status",
+			    0400,
+			    data->debugfs_dir,
+			    data,
+			    &ltc4271_port_status_fops);
+}
+
+static int ltc4271_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct ltc4271_data *data;
+	struct device *hwmon_dev;
+	u32 shunt_resistor;
+
+	data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	data->client = client;
+	i2c_set_clientdata(client, data);
+
+	data->regmap = devm_regmap_init_i2c(client, &ltc4271_regmap_config);
+	if (IS_ERR(data->regmap)) {
+		dev_err(dev, "failed to allocate register map\n");
+		return PTR_ERR(data->regmap);
+	}
+
+	if (of_property_read_u32(dev->of_node, "shunt-resistor-micro-ohms", &shunt_resistor)) {
+		dev_warn(dev, "assuming default shunt resistor of 250mOhms\n");
+		data->shunt_resistor = SHUNT_RESISTOR_250MOHMS;
+	} else if ((shunt_resistor == SHUNT_RESISTOR_250MOHMS) ||
+		(shunt_resistor == SHUNT_RESISTOR_500MOHMS))
+		data->shunt_resistor = shunt_resistor;
+	else {
+		dev_err(dev, "invalid shunt resistor value: %i. supported values are 250mOhms or 500mOhms\n",
+			shunt_resistor/1000);
+		return -EINVAL;
+	}
+
+	hwmon_dev = devm_hwmon_device_register_with_info(dev, client->name,
+							 data, &ltc4271_chip_info,
+							 NULL);
+	if (IS_ERR(hwmon_dev))
+		return PTR_ERR(hwmon_dev);
+
+	ltc4271_init_debugfs(data, hwmon_dev);
+
+	return 0;
+}
+
+static void ltc4271_remove(struct i2c_client *client)
+{
+	struct ltc4271_data *data = i2c_get_clientdata(client);
+
+	debugfs_remove_recursive(data->debugfs_dir);
+}
+
+static const struct i2c_device_id ltc4271_id[] = {
+	{ "ltc4271", 0 },
+	{ }
+};
+MODULE_DEVICE_TABLE(i2c, ltc4271_id);
+
+static const struct of_device_id __maybe_unused ltc4271_of_match[] = {
+	{ .compatible = "adi,ltc4271", },
+	{ },
+};
+MODULE_DEVICE_TABLE(of, ltc4271_of_match);
+
+static struct i2c_driver ltc4271_driver = {
+	.class		= I2C_CLASS_HWMON,
+	.probe_new	= ltc4271_probe,
+	.remove		= ltc4271_remove,
+	.driver = {
+		.name		= "ltc4271",
+		.of_match_table	= of_match_ptr(ltc4271_of_match),
+	},
+	.id_table	= ltc4271_id,
+};
+module_i2c_driver(ltc4271_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Lothar Felten <lothar.felten@xxxxxxxxx>");
+MODULE_DESCRIPTION("ltc4271 PoE PSE");
-- 
2.39.2




[Index of Archives]     [LM Sensors]     [Linux Sound]     [ALSA Users]     [ALSA Devel]     [Linux Audio Users]     [Linux Media]     [Kernel]     [Gimp]     [Yosemite News]     [Linux Media]

  Powered by Linux