diff mbox series

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

Message ID 20230811083222.15978-1-lothar.felten@gmail.com (mailing list archive)
State Changes Requested
Headers show
Series [v1,1/2] hwmon: (ltc4271) new driver for LTC4271 PoE PSE controller | expand

Commit Message

Lothar Felten Aug. 11, 2023, 8:32 a.m. UTC
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@gmail.com>
---
 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

Comments

Guenter Roeck Aug. 11, 2023, 1:18 p.m. UTC | #1
On 8/11/23 01:32, Lothar Felten wrote:
> 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@gmail.com>
> ---
>   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@gmail.com>
> +
> +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
> +-------------
> +
> +======================= =====================================================================

Did you pass this through "make htmldocs" ? It looks like that
should generate errors.

> +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]

Suspected attribute abuse. The enable attribute is intended to enable/disable
monitoring, not the port.

> +in4_input		IC input voltage
> +in4_label		"Input"
> +curr[1-4]_input		Current on ports [1-4]
> +curr[1-4]_label		"Port[1-4]"

I don't think it is a good idea to use the same labels for current and voltage.

> +======================= =====================================================================
> 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@gmail.com>
> +L:	linux-hwmon@vger.kernel.org
> +S:	Maintained
> +F:	Documentation/hwmon/ltc4271.rst
> +F:	drivers/hwmon/ltc4271.c
> +
>   LTC4306 I2C MULTIPLEXER DRIVER
>   M:	Michael Hennerich <michael.hennerich@analog.com>
>   L:	linux-i2c@vger.kernel.org
> 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.

It would be much better to register the additional sets of channels
with i2c_new_dummy_device() and manage it from a single device
instance. See pmbus/max16601.c for an example on how to do that.

> + *
> + * Derived from the tps23861 driver by Robert Marko
> + *
> + * Copyright (C) 2023 Lothar Felten <lothar.felten@gmail.com>
> + *
> + * Datasheet: https://www.analog.com/en/products/ltc4271.html
> + */
> +
> +#include <linux/bitfield.h>
> +#include <linux/debugfs.h>
> +#include <linux/hwmon-sysfs.h>

Is this really needed ? What for ?

> +#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

Consider something like

#define VOLTAGE_REG(n)	(0x2e + ((n) * 4)
#define CURRENT_REG(n)	(0x30 + ((n) * 4)

> +#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

Consider

#define PORT_STATUS(n)	(0x0c + (n))

> +#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 */

Add note explaining that value is in uV (e.g., 5,835 uV).
Specifying the value in uV and documenting it in mV is misleading.
Same everywhere below.

> +#define SHUNT_RESISTOR_DEFAULT		250000 /* 250 mOhm */
> +#define SHUNT_RESISTOR_250MOHMS		250000 /* 250 mOhm */
> +#define SHUNT_RESISTOR_500MOHMS		500000 /* 500 mOhm */

FWIW, this is even more misleading. The define says "MOHMS"
but the values are in uOhm.

> +#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;

Drop. See note below.

> +	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);
> +	}

It would be much better to model the input port as the first port.

> +	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;
> +

Consider storing current_lsb in struct ltc4271_data
do avoid the conditional.

> +	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);

Bit values need explanation.

> +
> +	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);
> +}

As mentioned above, suspected attribute abuse. The programming manual is not
available to the public. I requested access. but who knows if Analog will grant it.
Please note that I won't be able to accept this unless I have confirmation
that this controls the sensor(s), not the port.

> +
> +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;

This must be readable.

default: missing.

> +		}
> +	case hwmon_curr:
> +		switch (attr) {
> +		case hwmon_curr_input:
> +		case hwmon_curr_label:
> +			return 0444;

default missing (doesn't gcc complain about that ?)

> +		}

default: missing.

> +	}
> +
> +	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

else after return is pointless.

> +				return -EINVAL;
> +		}

default: missing.

> +	}
> +
> +	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);
> +		}

and again and everywhere.

> +	}
> +
> +	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";
> +	}

That makes me wonder if alarm attributes would be appropriate.

> +}
> +
> +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));

pass client as argument.

> +	if (!debugfs_name)
> +		return;
> +

Unnecessary (and undesirable) check.

Also, consider creating a single root directory for all LTC4271s in the system.
As it is, there will be lots of root directories. Look for various other drivers
to see how they implement that.

> +	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");

s/allocate/initialize/

Also consider using dev_err_probe().

> +		return PTR_ERR(data->regmap);
> +	}
> +
> +	if (of_property_read_u32(dev->of_node, "shunt-resistor-micro-ohms", &shunt_resistor)) {

Use device_property_read_u32().

> +		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;

Please refrain from unnecessary () in if statements.

> +	else {
> +		dev_err(dev, "invalid shunt resistor value: %i. supported values are 250mOhms or 500mOhms\n",
> +			shunt_resistor/1000);

dev_err_probe()

> +		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,

s/probe_new/probe/

> +	.remove		= ltc4271_remove,
> +	.driver = {
> +		.name		= "ltc4271",
> +		.of_match_table	= of_match_ptr(ltc4271_of_match),

Drop of_match_ptr() and _maybe_unused.

> +	},
> +	.id_table	= ltc4271_id,
> +};
> +module_i2c_driver(ltc4271_driver);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Lothar Felten <lothar.felten@gmail.com>");
> +MODULE_DESCRIPTION("ltc4271 PoE PSE");
diff mbox series

Patch

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@gmail.com>
+
+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@gmail.com>
+L:	linux-hwmon@vger.kernel.org
+S:	Maintained
+F:	Documentation/hwmon/ltc4271.rst
+F:	drivers/hwmon/ltc4271.c
+
 LTC4306 I2C MULTIPLEXER DRIVER
 M:	Michael Hennerich <michael.hennerich@analog.com>
 L:	linux-i2c@vger.kernel.org
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@gmail.com>
+ *
+ * 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@gmail.com>");
+MODULE_DESCRIPTION("ltc4271 PoE PSE");