diff mbox series

[net-next,3/3] net: phy: bcm54140: add hwmon support

Message ID 20200417192858.6997-3-michael@walle.cc (mailing list archive)
State Changes Requested
Headers show
Series [net-next,1/3] net: phy: broadcom: add helper to write/read RDB registers | expand

Commit Message

Michael Walle April 17, 2020, 7:28 p.m. UTC
The PHY supports monitoring its die temperature as well as two analog
voltages. Add support for it.

Signed-off-by: Michael Walle <michael@walle.cc>
---
 Documentation/hwmon/bcm54140.rst |  45 ++++
 Documentation/hwmon/index.rst    |   1 +
 drivers/net/phy/bcm54140.c       | 377 +++++++++++++++++++++++++++++++
 3 files changed, 423 insertions(+)
 create mode 100644 Documentation/hwmon/bcm54140.rst

Comments

Andrew Lunn April 17, 2020, 7:50 p.m. UTC | #1
> +/* Check if one PHY has already done the init of the parts common to all PHYs
> + * in the Quad PHY package.
> + */
> +static bool bcm54140_is_pkg_init(struct phy_device *phydev)
> +{
> +	struct mdio_device **map = phydev->mdio.bus->mdio_map;
> +	struct bcm54140_phy_priv *priv;
> +	struct phy_device *phy;
> +	int i, addr;
> +
> +	/* Quad PHY */
> +	for (i = 0; i < 4; i++) {
> +		priv = phydev->priv;
> +		addr = priv->base_addr + i;
> +
> +		if (!map[addr])
> +			continue;
> +
> +		phy = container_of(map[addr], struct phy_device, mdio);

I don't particularly like a PHY driver having knowledge of the mdio
bus core. Please add a helper in the core to get you the phydev for a
particular address.

There is also the question of locking. What happens if the PHY devices
is unbound while you have an instance of its phydev? What happens if
the base PHY is unbound? Are the three others then unusable?

I think we need to take a step back and look at how we handle quad
PHYs in general. The VSC8584 has many of the same issues.

    Andrew
Michael Walle April 17, 2020, 7:53 p.m. UTC | #2
Am 2020-04-17 21:50, schrieb Andrew Lunn:
>> +/* Check if one PHY has already done the init of the parts common to 
>> all PHYs
>> + * in the Quad PHY package.
>> + */
>> +static bool bcm54140_is_pkg_init(struct phy_device *phydev)
>> +{
>> +	struct mdio_device **map = phydev->mdio.bus->mdio_map;
>> +	struct bcm54140_phy_priv *priv;
>> +	struct phy_device *phy;
>> +	int i, addr;
>> +
>> +	/* Quad PHY */
>> +	for (i = 0; i < 4; i++) {
>> +		priv = phydev->priv;
>> +		addr = priv->base_addr + i;
>> +
>> +		if (!map[addr])
>> +			continue;
>> +
>> +		phy = container_of(map[addr], struct phy_device, mdio);
> 
> I don't particularly like a PHY driver having knowledge of the mdio
> bus core. Please add a helper in the core to get you the phydev for a
> particular address.
> 
> There is also the question of locking. What happens if the PHY devices
> is unbound while you have an instance of its phydev? What happens if
> the base PHY is unbound? Are the three others then unusable?
> 
> I think we need to take a step back and look at how we handle quad
> PHYs in general. The VSC8584 has many of the same issues.

Correct, and this function was actually stolen from there ;) This was
actually stolen from the mscc PHY ;)

-michael
Andrew Lunn April 17, 2020, 8:13 p.m. UTC | #3
> Correct, and this function was actually stolen from there ;) This was
> actually stolen from the mscc PHY ;)

Which in itself indicates it is time to make it a helper :-)

      Andrew
Michael Walle April 17, 2020, 9:08 p.m. UTC | #4
Am 2020-04-17 22:13, schrieb Andrew Lunn:
>> Correct, and this function was actually stolen from there ;) This was
>> actually stolen from the mscc PHY ;)
> 
> Which in itself indicates it is time to make it a helper :-)

Sure, do you have any suggestions?

-michael
Andrew Lunn April 17, 2020, 9:28 p.m. UTC | #5
On Fri, Apr 17, 2020 at 11:08:56PM +0200, Michael Walle wrote:
> Am 2020-04-17 22:13, schrieb Andrew Lunn:
> > > Correct, and this function was actually stolen from there ;) This was
> > > actually stolen from the mscc PHY ;)
> > 
> > Which in itself indicates it is time to make it a helper :-)
> 
> Sure, do you have any suggestions?

mdiobus_get_phy() does the bit i was complaining about, the mdiobus
internal knowledge.

	Andrew
Guenter Roeck April 18, 2020, 3:09 a.m. UTC | #6
On 4/17/20 12:28 PM, Michael Walle wrote:
> The PHY supports monitoring its die temperature as well as two analog
> voltages. Add support for it.
> 
> Signed-off-by: Michael Walle <michael@walle.cc>

This will probably fail to compile if HWMON is not enabled or if it is built
as module and this driver is built into the kernel.

> ---
>  Documentation/hwmon/bcm54140.rst |  45 ++++
>  Documentation/hwmon/index.rst    |   1 +
>  drivers/net/phy/bcm54140.c       | 377 +++++++++++++++++++++++++++++++
>  3 files changed, 423 insertions(+)
>  create mode 100644 Documentation/hwmon/bcm54140.rst
> 
> diff --git a/Documentation/hwmon/bcm54140.rst b/Documentation/hwmon/bcm54140.rst
> new file mode 100644
> index 000000000000..bc6ea4b45966
> --- /dev/null
> +++ b/Documentation/hwmon/bcm54140.rst
> @@ -0,0 +1,45 @@
> +.. SPDX-License-Identifier: GPL-2.0-only
> +
> +Broadcom BCM54140 Quad SGMII/QSGMII PHY
> +=======================================
> +
> +Supported chips:
> +
> +   * Broadcom BCM54140
> +
> +     Datasheet: not public
> +
> +Author: Michael Walle <michael@walle.cc>
> +
> +Description
> +-----------
> +
> +The Broadcom BCM54140 is a Quad SGMII/QSGMII PHY which supports monitoring
> +its die temperature as well as two analog voltages.
> +
> +The AVDDL is a 1.0V analogue voltage, the AVDDH is a 3.3V analogue voltage.
> +Both voltages and the temperature are measured in a round-robin fashion.
> +
> +Sysfs entries
> +-------------
> +
> +The following attributes are supported.
> +
> +======================= ========================================================
> +in0_label		"AVDDL"
> +in0_input		Measured AVDDL voltage.
> +in0_min			Minimum AVDDL voltage.
> +in0_max			Maximum AVDDL voltage.
> +in0_alarm		AVDDL voltage alarm.
> +
> +in1_label		"AVDDH"
> +in1_input		Measured AVDDH voltage.
> +in1_min			Minimum AVDDH voltage.
> +in1_max			Maximum AVDDH voltage.
> +in1_alarm		AVDDH voltage alarm.
> +
> +temp1_input		Die temperature.
> +temp1_min		Minimum die temperature.
> +temp1_max		Maximum die temperature.
> +temp1_alarm		Die temperature alarm.
> +======================= ========================================================
> diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
> index f022583f96f6..19ad0846736d 100644
> --- a/Documentation/hwmon/index.rst
> +++ b/Documentation/hwmon/index.rst
> @@ -42,6 +42,7 @@ Hardware Monitoring Kernel Drivers
>     asb100
>     asc7621
>     aspeed-pwm-tacho
> +   bcm54140
>     bel-pfe
>     coretemp
>     da9052
> diff --git a/drivers/net/phy/bcm54140.c b/drivers/net/phy/bcm54140.c
> index 97465491b41b..97c364ce05e3 100644
> --- a/drivers/net/phy/bcm54140.c
> +++ b/drivers/net/phy/bcm54140.c
> @@ -6,6 +6,7 @@
>  
>  #include <linux/bitfield.h>
>  #include <linux/brcmphy.h>
> +#include <linux/hwmon.h>
>  #include <linux/module.h>
>  #include <linux/phy.h>
>  
> @@ -50,6 +51,54 @@
>  #define  BCM54140_RDB_TOP_IMR_PORT1	BIT(5)
>  #define  BCM54140_RDB_TOP_IMR_PORT2	BIT(6)
>  #define  BCM54140_RDB_TOP_IMR_PORT3	BIT(7)
> +#define BCM54140_RDB_MON_CTRL		0x831	/* monitor control */
> +#define  BCM54140_RDB_MON_CTRL_V_MODE	BIT(3)	/* voltage mode */
> +#define  BCM54140_RDB_MON_CTRL_SEL_MASK	GENMASK(2, 1)
> +#define  BCM54140_RDB_MON_CTRL_SEL_TEMP	0	/* meassure temperature */
> +#define  BCM54140_RDB_MON_CTRL_SEL_1V0	1	/* meassure AVDDL 1.0V */
> +#define  BCM54140_RDB_MON_CTRL_SEL_3V3	2	/* meassure AVDDH 3.3V */
> +#define  BCM54140_RDB_MON_CTRL_SEL_RR	3	/* meassure all round-robin */
> +#define  BCM54140_RDB_MON_CTRL_PWR_DOWN	BIT(0)	/* power-down monitor */
> +#define BCM54140_RDB_MON_TEMP_VAL	0x832	/* temperature value */
> +#define BCM54140_RDB_MON_TEMP_MAX	0x833	/* temperature high thresh */
> +#define BCM54140_RDB_MON_TEMP_MIN	0x834	/* temperature low thresh */
> +#define  BCM54140_RDB_MON_TEMP_DATA_MASK GENMASK(9, 0)
> +#define BCM54140_RDB_MON_1V0_VAL	0x835	/* AVDDL 1.0V value */
> +#define BCM54140_RDB_MON_1V0_MAX	0x836	/* AVDDL 1.0V high thresh */
> +#define BCM54140_RDB_MON_1V0_MIN	0x837	/* AVDDL 1.0V low thresh */
> +#define  BCM54140_RDB_MON_1V0_DATA_MASK	GENMASK(10, 0)
> +#define BCM54140_RDB_MON_3V3_VAL	0x838	/* AVDDH 3.3V value */
> +#define BCM54140_RDB_MON_3V3_MAX	0x839	/* AVDDH 3.3V high thresh */
> +#define BCM54140_RDB_MON_3V3_MIN	0x83a	/* AVDDH 3.3V low thresh */
> +#define  BCM54140_RDB_MON_3V3_DATA_MASK	GENMASK(11, 0)
> +#define BCM54140_RDB_MON_ISR		0x83b	/* interrupt status */
> +#define  BCM54140_RDB_MON_ISR_3V3	BIT(2)	/* AVDDH 3.3V alarm */
> +#define  BCM54140_RDB_MON_ISR_1V0	BIT(1)	/* AVDDL 1.0V alarm */
> +#define  BCM54140_RDB_MON_ISR_TEMP	BIT(0)	/* temperature alarm */
> +
> +/* According to the datasheet the formula is:
> + *   T = 413.35 - (0.49055 * bits[9:0])
> + */
> +#define BCM54140_HWMON_TO_TEMP(v) (413350L - (v) * 491)
> +#define BCM54140_HWMON_FROM_TEMP(v) DIV_ROUND_CLOSEST_ULL(413350L - (v), 491)
> +
> +/* According to the datasheet the formula is:
> + *   U = bits[11:0] / 1024 * 220 / 0.2
> + *
> + * Normalized:
> + *   U = bits[11:0] / 4096 * 2514
> + */
> +#define BCM54140_HWMON_TO_IN_1V0(v) ((v) * 2514 >> 11)
> +#define BCM54140_HWMON_FROM_IN_1V0(v) DIV_ROUND_CLOSEST_ULL(((v) << 11), 2514)
> +
> +/* According to the datasheet the formula is:
> + *   U = bits[10:0] / 1024 * 880 / 0.7
> + *
> + * Normalized:
> + *   U = bits[10:0] / 2048 * 4400
> + */
> +#define BCM54140_HWMON_TO_IN_3V3(v) ((v) * 4400 >> 12)
> +#define BCM54140_HWMON_FROM_IN_3V3(v) DIV_ROUND_CLOSEST_ULL(((v) << 12), 4400)
>  
>  #define BCM54140_DEFAULT_DOWNSHIFT 5
>  #define BCM54140_MAX_DOWNSHIFT 9
> @@ -57,6 +106,258 @@
>  struct bcm54140_phy_priv {
>  	int port;
>  	int base_addr;
> +	bool pkg_init;
> +	u16 alarm;
> +};
> +
> +static umode_t bcm54140_hwmon_is_visible(const void *data,
> +					 enum hwmon_sensor_types type,
> +					 u32 attr, int channel)
> +{
> +	switch (type) {
> +	case hwmon_in:
> +		switch (attr) {
> +		case hwmon_in_min:
> +		case hwmon_in_max:
> +			return 0644;
> +		case hwmon_in_label:
> +		case hwmon_in_input:
> +		case hwmon_in_alarm:
> +			return 0444;
> +		default:
> +			return 0;
> +		}
> +	case hwmon_temp:
> +		switch (attr) {
> +		case hwmon_temp_min:
> +		case hwmon_temp_max:
> +			return 0644;
> +		case hwmon_temp_input:
> +		case hwmon_temp_alarm:
> +			return 0444;
> +		default:
> +			return 0;
> +		}
> +	default:
> +		return 0;
> +	}
> +}
> +
> +static int bcm54140_hwmon_read_alarm(struct device *dev, unsigned int bit,
> +				     long *val)
> +{
> +	struct phy_device *phydev = dev_get_drvdata(dev);
> +	struct bcm54140_phy_priv *priv = phydev->priv;
> +	u16 tmp;
> +
> +	/* latch any alarm bits */
> +	tmp = bcm_phy_read_rdb(phydev, BCM54140_RDB_MON_ISR);
> +	if (tmp < 0)
> +		return tmp;
> +	priv->alarm |= tmp;
> +
> +	*val = !!(priv->alarm & bit);
> +	priv->alarm &= ~bit;> +
> +	return 0;
> +}
> +
> +static int bcm54140_hwmon_read_temp(struct device *dev, u32 attr,
> +				    int channel, long *val)
> +{
> +	struct phy_device *phydev = dev_get_drvdata(dev);
> +	u16 reg, tmp;
> +
> +	switch (attr) {
> +	case hwmon_temp_input:
> +		reg = BCM54140_RDB_MON_TEMP_VAL;
> +		break;
> +	case hwmon_temp_min:
> +		reg = BCM54140_RDB_MON_TEMP_MIN;
> +		break;
> +	case hwmon_temp_max:
> +		reg = BCM54140_RDB_MON_TEMP_MAX;
> +		break;
> +	case hwmon_temp_alarm:
> +		return bcm54140_hwmon_read_alarm(dev,
> +						 BCM54140_RDB_MON_ISR_TEMP,
> +						 val);
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +
> +	tmp = bcm_phy_read_rdb(phydev, reg);
> +	if (tmp < 0)
> +		return tmp;
> +
> +	*val = BCM54140_HWMON_TO_TEMP(tmp & BCM54140_RDB_MON_TEMP_DATA_MASK);
> +
> +	return 0;
> +}
> +
> +static int bcm54140_hwmon_read_in(struct device *dev, u32 attr,
> +				  int channel, long *val)
> +{
> +	struct phy_device *phydev = dev_get_drvdata(dev);
> +	u16 mask = (!channel) ? BCM54140_RDB_MON_1V0_DATA_MASK
> +			      : BCM54140_RDB_MON_3V3_DATA_MASK;
> +	u16 bit, reg, tmp;
> +
> +	switch (attr) {
> +	case hwmon_in_input:
> +		reg = (!channel) ? BCM54140_RDB_MON_1V0_VAL
> +				 : BCM54140_RDB_MON_3V3_VAL;

I am personally neither a friend of unnecessary () nor of
unnecessary negations. Why not the following ?

		reg = channel ? BCM54140_RDB_MON_3V3_VAL :
				BCM54140_RDB_MON_1V0_VAL;

Another option would be to read all those values from a set of
defines, given the expressions are repeated several times.

> +		break;
> +	case hwmon_in_min:
> +		reg = (!channel) ? BCM54140_RDB_MON_1V0_MIN
> +				 : BCM54140_RDB_MON_3V3_MIN;
> +		break;
> +	case hwmon_in_max:
> +		reg = (!channel) ? BCM54140_RDB_MON_1V0_MAX
> +				 : BCM54140_RDB_MON_3V3_MAX;
> +		break;
> +	case hwmon_in_alarm:
> +		bit = (!channel) ? BCM54140_RDB_MON_ISR_1V0
> +				 : BCM54140_RDB_MON_ISR_3V3;
> +		return bcm54140_hwmon_read_alarm(dev, bit, val);
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +
> +	tmp = bcm_phy_read_rdb(phydev, reg);
> +	if (tmp < 0)
> +		return tmp;
> +
> +	if (!channel)
> +		*val = BCM54140_HWMON_TO_IN_1V0(tmp & mask);
> +	else
> +		*val = BCM54140_HWMON_TO_IN_3V3(tmp & mask);
> +
> +	return 0;
> +}
> +
> +static int bcm54140_hwmon_read(struct device *dev,
> +			       enum hwmon_sensor_types type, u32 attr,
> +			       int channel, long *val)
> +{
> +	switch (type) {
> +	case hwmon_temp:
> +		return bcm54140_hwmon_read_temp(dev, attr, channel, val);
> +	case hwmon_in:
> +		return bcm54140_hwmon_read_in(dev, attr, channel, val);
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +}
> +
> +static const char *const bcm54140_hwmon_in_labels[] = {
> +	"AVDDL",
> +	"AVDDH",
> +};
> +
> +static int bcm54140_hwmon_read_string(struct device *dev,
> +				      enum hwmon_sensor_types type, u32 attr,
> +				      int channel, const char **str)
> +{
> +	switch (type) {
> +	case hwmon_in:
> +		switch (attr) {
> +		case hwmon_in_label:
> +			*str = bcm54140_hwmon_in_labels[channel];
> +			return 0;
> +		default:
> +			return -EOPNOTSUPP;
> +		}
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +}
> +
> +static int bcm54140_hwmon_write_temp(struct device *dev, u32 attr,
> +				     int channel, long val)
> +{
> +	struct phy_device *phydev = dev_get_drvdata(dev);
> +	u16 reg;
> +
> +	switch (attr) {
> +	case hwmon_temp_min:
> +		reg = BCM54140_RDB_MON_TEMP_MIN;
> +		break;
> +	case hwmon_temp_max:
> +		reg = BCM54140_RDB_MON_TEMP_MAX;
> +		break;
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +
> +	return bcm_phy_modify_rdb(phydev, reg, BCM54140_RDB_MON_TEMP_DATA_MASK,
> +				  BCM54140_HWMON_FROM_TEMP(val));
> +}
> +
> +static int bcm54140_hwmon_write_in(struct device *dev, u32 attr,
> +				   int channel, long val)
> +{
> +	struct phy_device *phydev = dev_get_drvdata(dev);
> +	u16 mask = (!channel) ? BCM54140_RDB_MON_1V0_DATA_MASK
> +			      : BCM54140_RDB_MON_3V3_DATA_MASK;
> +	unsigned long raw = (!channel) ?  BCM54140_HWMON_FROM_IN_1V0(val)
> +				       :  BCM54140_HWMON_FROM_IN_3V3(val);
> +	u16 reg;
> +
> +	raw = clamp_val(raw, 0, mask);
> +
> +	switch (attr) {
> +	case hwmon_in_min:
> +		reg = (!channel) ? BCM54140_RDB_MON_1V0_MIN
> +				 : BCM54140_RDB_MON_3V3_MIN;
> +		break;
> +	case hwmon_in_max:
> +		reg = (!channel) ? BCM54140_RDB_MON_1V0_MAX
> +				 : BCM54140_RDB_MON_3V3_MAX;
> +		break;
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +
> +	return bcm_phy_modify_rdb(phydev, reg, mask, raw);
> +}
> +
> +static int bcm54140_hwmon_write(struct device *dev,
> +				enum hwmon_sensor_types type, u32 attr,
> +				int channel, long val)
> +{
> +	switch (type) {
> +	case hwmon_temp:
> +		return bcm54140_hwmon_write_temp(dev, attr, channel, val);
> +	case hwmon_in:
> +		return bcm54140_hwmon_write_in(dev, attr, channel, val);
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +}
> +
> +static const struct hwmon_channel_info *bcm54140_hwmon_info[] = {
> +	HWMON_CHANNEL_INFO(temp,
> +			   HWMON_T_INPUT | HWMON_T_MIN | HWMON_T_MAX |
> +			   HWMON_T_ALARM),
> +	HWMON_CHANNEL_INFO(in,
> +			   HWMON_I_INPUT | HWMON_I_MIN | HWMON_I_MAX |
> +			   HWMON_I_ALARM | HWMON_I_LABEL,
> +			   HWMON_I_INPUT | HWMON_I_MIN | HWMON_I_MAX |
> +			   HWMON_I_ALARM | HWMON_I_LABEL),
> +	NULL
> +};
> +
> +static const struct hwmon_ops bcm54140_hwmon_ops = {
> +	.is_visible = bcm54140_hwmon_is_visible,
> +	.read = bcm54140_hwmon_read,
> +	.read_string = bcm54140_hwmon_read_string,
> +	.write = bcm54140_hwmon_write,
> +};
> +
> +static const struct hwmon_chip_info bcm54140_chip_info = {
> +	.ops = &bcm54140_hwmon_ops,
> +	.info = bcm54140_hwmon_info,
>  };
>  
>  static int bcm54140_phy_base_read_rdb(struct phy_device *phydev, u16 rdb)
> @@ -203,6 +504,74 @@ static int bcm54140_get_base_addr_and_port(struct phy_device *phydev)
>  	return 0;
>  }
>  
> +/* Check if one PHY has already done the init of the parts common to all PHYs
> + * in the Quad PHY package.
> + */
> +static bool bcm54140_is_pkg_init(struct phy_device *phydev)
> +{
> +	struct mdio_device **map = phydev->mdio.bus->mdio_map;
> +	struct bcm54140_phy_priv *priv;
> +	struct phy_device *phy;
> +	int i, addr;
> +
> +	/* Quad PHY */
> +	for (i = 0; i < 4; i++) {
> +		priv = phydev->priv;
> +		addr = priv->base_addr + i;
> +
> +		if (!map[addr])
> +			continue;
> +
> +		phy = container_of(map[addr], struct phy_device, mdio);
> +
> +		if ((phy->phy_id & phydev->drv->phy_id_mask) !=
> +		    (phydev->drv->phy_id & phydev->drv->phy_id_mask))
> +			continue;
> +
> +		priv = phy->priv;
> +
> +		if (priv && priv->pkg_init)
> +			return true;
> +	}
> +
> +	return false;
> +}
> +
> +static int bcm54140_enable_monitoring(struct phy_device *phydev)
> +{
> +	u16 mask, set;
> +
> +	/* 3.3V voltage mode */
> +	set = BCM54140_RDB_MON_CTRL_V_MODE;
> +
> +	/* select round-robin */
> +	mask = BCM54140_RDB_MON_CTRL_SEL_MASK;
> +	set |= FIELD_PREP(BCM54140_RDB_MON_CTRL_SEL_MASK,
> +			  BCM54140_RDB_MON_CTRL_SEL_RR);
> +
> +	/* remove power-down bit */
> +	mask |= BCM54140_RDB_MON_CTRL_PWR_DOWN;
> +
> +	return bcm_phy_modify_rdb(phydev, BCM54140_RDB_MON_CTRL, mask, set);
> +}
> +
> +static int bcm54140_phy_probe_once(struct phy_device *phydev)
> +{
> +	struct device *hwmon;
> +	int ret;
> +
> +	/* enable hardware monitoring */
> +	ret = bcm54140_enable_monitoring(phydev);
> +	if (ret)
> +		return ret;
> +
> +	hwmon = devm_hwmon_device_register_with_info(&phydev->mdio.dev,
> +						     "BCM54140", phydev,
> +						     &bcm54140_chip_info,
> +						     NULL);
> +	return PTR_ERR_OR_ZERO(hwmon);
> +}
> +
>  static int bcm54140_phy_probe(struct phy_device *phydev)
>  {
>  	struct bcm54140_phy_priv *priv;
> @@ -218,6 +587,14 @@ static int bcm54140_phy_probe(struct phy_device *phydev)
>  	if (ret)
>  		return ret;
>  
> +	if (!bcm54140_is_pkg_init(phydev)) {
> +		ret = bcm54140_phy_probe_once(phydev);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	priv->pkg_init = true;
> +
>  	dev_info(&phydev->mdio.dev,
>  		 "probed (port %d, base PHY address %d)\n",
>  		 priv->port, priv->base_addr);
>
Michael Walle April 19, 2020, 10:29 a.m. UTC | #7
Am 2020-04-17 23:28, schrieb Andrew Lunn:
> On Fri, Apr 17, 2020 at 11:08:56PM +0200, Michael Walle wrote:
>> Am 2020-04-17 22:13, schrieb Andrew Lunn:
>> > > Correct, and this function was actually stolen from there ;) This was
>> > > actually stolen from the mscc PHY ;)
>> >
>> > Which in itself indicates it is time to make it a helper :-)
>> 
>> Sure, do you have any suggestions?
> 
> mdiobus_get_phy() does the bit i was complaining about, the mdiobus
> internal knowledge.

But that doesn't address your other comment.

> There is also the question of locking. What happens if the PHY devices
> is unbound while you have an instance of its phydev?

Is there any lock one could take to avoid that?

> What happens if the base PHY is unbound? Are the three others then
> unusable?

In my case, this would mean the hwmon device is also removed. I don't
see any other way to do it right now. I guess it would be better to
have the hwmon device registered to some kind of parent device.

> I think we need to take a step back and look at how we handle quad
> PHYs in general. The VSC8584 has many of the same issues.

For the BCM54140 there are three different functions:
  (1) PHY functions accessible by the PHYs own address (ie PHY
      status/control)
  (2) PHY functions but only accessible by the global registers (ie
      interrupt enables per PHY of the shared interrupt pin)
  (3) global functions (like sensors, global configuration)

(1) is already supported in the current PHY framework. (2) and (3)
need the "hack" which uses mdiobus_read/write() with the base
address.

-michael
Andrew Lunn April 19, 2020, 4:29 p.m. UTC | #8
On Sun, Apr 19, 2020 at 12:29:23PM +0200, Michael Walle wrote:
> Am 2020-04-17 23:28, schrieb Andrew Lunn:
> > On Fri, Apr 17, 2020 at 11:08:56PM +0200, Michael Walle wrote:
> > > Am 2020-04-17 22:13, schrieb Andrew Lunn:
> > > > > Correct, and this function was actually stolen from there ;) This was
> > > > > actually stolen from the mscc PHY ;)
> > > >
> > > > Which in itself indicates it is time to make it a helper :-)
> > > 
> > > Sure, do you have any suggestions?
> > 
> > mdiobus_get_phy() does the bit i was complaining about, the mdiobus
> > internal knowledge.
> 
> But that doesn't address your other comment.

Yes, you are right. But i don't think you can easily generalize the
rest. It needs knowledge of the driver private structure to reference
pkg_init. You would have to move that into phy_device.

> 
> > There is also the question of locking. What happens if the PHY devices
> > is unbound while you have an instance of its phydev?
> 
> Is there any lock one could take to avoid that?

phy_attach_direct() does a get_device(). That at least means the
struct device will not go away. I don't know the code well enough to
know if that will also stop the phy_device structure from being freed.
We might need mdiobus_get_phy() to also do a get_device(), and add a
mdiobus_put_phy() which does a put_device().

> > What happens if the base PHY is unbound? Are the three others then
> > unusable?
> 
> In my case, this would mean the hwmon device is also removed. I don't
> see any other way to do it right now. I guess it would be better to
> have the hwmon device registered to some kind of parent device.

The phydev structure might go away. But the hardware is still
there. You can access it via address on the bus. What you have to be
careful of is using the phydev for a different phy.

> For the BCM54140 there are three different functions:
>  (1) PHY functions accessible by the PHYs own address (ie PHY
>      status/control)
>  (2) PHY functions but only accessible by the global registers (ie
>      interrupt enables per PHY of the shared interrupt pin)
>  (3) global functions (like sensors, global configuration)
> 
> (1) is already supported in the current PHY framework. (2) and (3)
> need the "hack" which uses mdiobus_read/write() with the base
> address.

Is the _is_pkg_init() function the only place you need to access some
other phy_device structure.

Maybe we need a phydev->shared structure, which all PHYs in one
package share? Get the core to do reference counting on the structure?
Add helpers phy_read_shared(), phy_write_shared(), etc, which does
MDIO accesses on the base device, taking care of the locking. pkg_init
is a member of this shared structure. And have a void * priv in shared
for shared driver private data?

Just "thinking out loud"

    Andrew
Michael Walle April 19, 2020, 4:47 p.m. UTC | #9
Am 2020-04-19 18:29, schrieb Andrew Lunn:
> On Sun, Apr 19, 2020 at 12:29:23PM +0200, Michael Walle wrote:
>> Am 2020-04-17 23:28, schrieb Andrew Lunn:
>> > On Fri, Apr 17, 2020 at 11:08:56PM +0200, Michael Walle wrote:
>> > > Am 2020-04-17 22:13, schrieb Andrew Lunn:
>> > > > > Correct, and this function was actually stolen from there ;) This was
>> > > > > actually stolen from the mscc PHY ;)
>> > > >
>> > > > Which in itself indicates it is time to make it a helper :-)
>> > >
>> > > Sure, do you have any suggestions?
>> >
>> > mdiobus_get_phy() does the bit i was complaining about, the mdiobus
>> > internal knowledge.
>> 
>> But that doesn't address your other comment.
> 
> Yes, you are right. But i don't think you can easily generalize the
> rest. It needs knowledge of the driver private structure to reference
> pkg_init. You would have to move that into phy_device.
> 
>> 
>> > There is also the question of locking. What happens if the PHY devices
>> > is unbound while you have an instance of its phydev?
>> 
>> Is there any lock one could take to avoid that?
> 
> phy_attach_direct() does a get_device(). That at least means the
> struct device will not go away. I don't know the code well enough to
> know if that will also stop the phy_device structure from being freed.
> We might need mdiobus_get_phy() to also do a get_device(), and add a
> mdiobus_put_phy() which does a put_device().
> 
>> > What happens if the base PHY is unbound? Are the three others then
>> > unusable?
>> 
>> In my case, this would mean the hwmon device is also removed. I don't
>> see any other way to do it right now. I guess it would be better to
>> have the hwmon device registered to some kind of parent device.
> 
> The phydev structure might go away. But the hardware is still
> there. You can access it via address on the bus. What you have to be
> careful of is using the phydev for a different phy.

But the hwmon is registered to the device of the PHY which might be
unbound. So it will also be removed, correct? FWIW I don't think that
is likely to happen in my case ;)

> 
>> For the BCM54140 there are three different functions:
>>  (1) PHY functions accessible by the PHYs own address (ie PHY
>>      status/control)
>>  (2) PHY functions but only accessible by the global registers (ie
>>      interrupt enables per PHY of the shared interrupt pin)
>>  (3) global functions (like sensors, global configuration)
>> 
>> (1) is already supported in the current PHY framework. (2) and (3)
>> need the "hack" which uses mdiobus_read/write() with the base
>> address.
> 
> Is the _is_pkg_init() function the only place you need to access some
> other phy_device structure.

yes.

> Maybe we need a phydev->shared structure, which all PHYs in one
> package share?

That came to my mind too. But how could the PHY core find out which
shared structure belongs to which phydev? I guess the phydev have to
find out, but then how does it tell the PHY core that it wants such
a shared structure. Have the (base) PHY address as an identifier?

> Get the core to do reference counting on the structure?
> Add helpers phy_read_shared(), phy_write_shared(), etc, which does
> MDIO accesses on the base device, taking care of the locking.

The "base" access is another thing, I guess, which has nothing to do
with the shared structure. Also I presume not every PHY has the base
address as some global register access. Eg. this PHY also have
"base + 4" (or depending on the configuration base + 3, that is the
last PHY of the four) as a special register access.

> pkg_init
> is a member of this shared structure. And have a void * priv in shared
> for shared driver private data?

if you have a void *priv, why would you need pkg_init, which is an
implementation detail of the phydev. I guess it is enough to just have
a void *shared (I don't know about the locking for now).

-michael
Andrew Lunn April 19, 2020, 5:05 p.m. UTC | #10
> > Maybe we need a phydev->shared structure, which all PHYs in one
> > package share?
> 
> That came to my mind too. But how could the PHY core find out which
> shared structure belongs to which phydev? I guess the phydev have to
> find out, but then how does it tell the PHY core that it wants such
> a shared structure. Have the (base) PHY address as an identifier?

Yes. I was thinking along those lines.

phy_package_join(phydev, base)

If this is the first call with that value of base, allocate the
structure, set the ref count to 1, and set phydev->shared to point to
it. For subsequent calls, increment the reference count, and set
phydev->shared.

phy_package_leave(phydev)

Decrement the reference count, and set phydev->shared to NULL. If the
reference count goes to 0, free the structure.

> > Get the core to do reference counting on the structure?
> > Add helpers phy_read_shared(), phy_write_shared(), etc, which does
> > MDIO accesses on the base device, taking care of the locking.
> 
> The "base" access is another thing, I guess, which has nothing to do
> with the shared structure.

I'm making the assumption that all global addresses are at the base
address. If we don't want to make that assumption, we need the change
the API above so you pass a cookie, and all PHYs need to use the same
cookie to identify the package.

Maybe base is the wrong name, since MSCC can have the base as the high
address of the four, not the low?

Still just thinking aloud....

       Andrew
Russell King (Oracle) April 19, 2020, 5:12 p.m. UTC | #11
On Sun, Apr 19, 2020 at 06:29:28PM +0200, Andrew Lunn wrote:
> On Sun, Apr 19, 2020 at 12:29:23PM +0200, Michael Walle wrote:
> > Am 2020-04-17 23:28, schrieb Andrew Lunn:
> > > On Fri, Apr 17, 2020 at 11:08:56PM +0200, Michael Walle wrote:
> > > > Am 2020-04-17 22:13, schrieb Andrew Lunn:
> > > > > > Correct, and this function was actually stolen from there ;) This was
> > > > > > actually stolen from the mscc PHY ;)
> > > > >
> > > > > Which in itself indicates it is time to make it a helper :-)
> > > > 
> > > > Sure, do you have any suggestions?
> > > 
> > > mdiobus_get_phy() does the bit i was complaining about, the mdiobus
> > > internal knowledge.
> > 
> > But that doesn't address your other comment.
> 
> Yes, you are right. But i don't think you can easily generalize the
> rest. It needs knowledge of the driver private structure to reference
> pkg_init. You would have to move that into phy_device.
> 
> > 
> > > There is also the question of locking. What happens if the PHY devices
> > > is unbound while you have an instance of its phydev?
> > 
> > Is there any lock one could take to avoid that?
> 
> phy_attach_direct() does a get_device(). That at least means the
> struct device will not go away. I don't know the code well enough to
> know if that will also stop the phy_device structure from being freed.

Well, struct device is embedded in struct mdio_device, which in turn
is embedded in struct phy_device. So, if struct device can't go away
because its refcount is held, the same is true of the structs
embedding it.
Michael Walle April 19, 2020, 9:31 p.m. UTC | #12
Am 2020-04-19 19:05, schrieb Andrew Lunn:
>> > Maybe we need a phydev->shared structure, which all PHYs in one
>> > package share?
>> 
>> That came to my mind too. But how could the PHY core find out which
>> shared structure belongs to which phydev? I guess the phydev have to
>> find out, but then how does it tell the PHY core that it wants such
>> a shared structure. Have the (base) PHY address as an identifier?
> 
> Yes. I was thinking along those lines.
> 
> phy_package_join(phydev, base)
> 
> If this is the first call with that value of base, allocate the
> structure, set the ref count to 1, and set phydev->shared to point to
> it. For subsequent calls, increment the reference count, and set
> phydev->shared.
> 
> phy_package_leave(phydev)
> 
> Decrement the reference count, and set phydev->shared to NULL. If the
> reference count goes to 0, free the structure.
> 
>> > Get the core to do reference counting on the structure?
>> > Add helpers phy_read_shared(), phy_write_shared(), etc, which does
>> > MDIO accesses on the base device, taking care of the locking.
>> 
>> The "base" access is another thing, I guess, which has nothing to do
>> with the shared structure.
> 
> I'm making the assumption that all global addresses are at the base
> address.

But what does that have to do with the shared structure? I don't think
you have to "bundle" the shared structure with the "access the global
registers" method. The phy drivers just have to know some common key,
which can be anything arbitrary, correct? So we can say its the
lowest address, but it could also be any other address, as long as
each PHY driver instance can deduce the same key.

> If we don't want to make that assumption, we need the change
> the API above so you pass a cookie, and all PHYs need to use the same
> cookie to identify the package.

whats the difference between a PHY address and a cookie, given that the
phy core doesn't actually use the phy address for anything.

-michael

> Maybe base is the wrong name, since MSCC can have the base as the high
> address of the four, not the low?
> 
> Still just thinking aloud....
> 
>        Andrew
Andrew Lunn April 19, 2020, 9:55 p.m. UTC | #13
> But what does that have to do with the shared structure? I don't think
> you have to "bundle" the shared structure with the "access the global
> registers" method.

We don't need to. But it would be a good way to clean up code which
locks the mdio bus, does a register access on some other device, and
then unlocks the bus.

As a general rule of thumb, it is better to have the core do the
locking, rather than the driver. Driver writers don't always think
about locking, so it is better to give driver writers safe APIs to
use.

	Andrew
Michael Walle April 20, 2020, 3:10 p.m. UTC | #14
Hi Andrew,

Am 2020-04-19 23:55, schrieb Andrew Lunn:
>> But what does that have to do with the shared structure? I don't think
>> you have to "bundle" the shared structure with the "access the global
>> registers" method.
> 
> We don't need to. But it would be a good way to clean up code which
> locks the mdio bus, does a register access on some other device, and
> then unlocks the bus.

I'd like do an RFC for that. But how should I proceed with the original
patch series? Should I send an updated version; you didn't reply to the
LED stuff. That is the last remark for now.

> As a general rule of thumb, it is better to have the core do the
> locking, rather than the driver. Driver writers don't always think
> about locking, so it is better to give driver writers safe APIs to
> use.

Ok I see, but what locking do you have in mind? We could have something
like

__phy_package_write(struct phy_device *dev, u32 regnum, u16 val)
{
   return __mdiobus_write(phydev->mdio.bus, phydev->shared->addr,
                          regnum, val);
}

and its phy_package_write() equivalent. But that would just be
convenience functions, nothing where you actually help the user with
locking. Am I missing something?

>>> Get the core to do reference counting on the structure?
>>> Add helpers phy_read_shared(), phy_write_shared(), etc, which does
>>> MDIO accesses on the base device, taking care of the locking.
>>> 
>> The "base" access is another thing, I guess, which has nothing to do
>> with the shared structure.
>> 
> I'm making the assumption that all global addresses are at the base
> address. If we don't want to make that assumption, we need the change
> the API above so you pass a cookie, and all PHYs need to use the same
> cookie to identify the package.

how would a phy driver deduce a common cookie? And how would that be a
difference to using a PHY address.

> Maybe base is the wrong name, since MSCC can have the base as the high
> address of the four, not the low?

I'd say it might be any of the four addresses as long as it is the same
across the PHYs in the same package. And in that case you can also have
the phy_package_read/write() functions.

-michael
Andrew Lunn April 20, 2020, 3:36 p.m. UTC | #15
> Ok I see, but what locking do you have in mind? We could have something
> like
> 
> __phy_package_write(struct phy_device *dev, u32 regnum, u16 val)
> {
>   return __mdiobus_write(phydev->mdio.bus, phydev->shared->addr,
>                          regnum, val);
> }
> 
> and its phy_package_write() equivalent. But that would just be
> convenience functions, nothing where you actually help the user with
> locking. Am I missing something?

In general, drivers should not be using __foo functions. We want
drivers to make use of phy_package_write() which would do the bus
locking. Look at a typical PHY driver. There is no locking what so
ever. Just lots of phy_read() and phy write(). The locking is done by
the core and so should be correct.

> > > > Get the core to do reference counting on the structure?
> > > > Add helpers phy_read_shared(), phy_write_shared(), etc, which does
> > > > MDIO accesses on the base device, taking care of the locking.
> > > > 
> > > The "base" access is another thing, I guess, which has nothing to do
> > > with the shared structure.
> > > 
> > I'm making the assumption that all global addresses are at the base
> > address. If we don't want to make that assumption, we need the change
> > the API above so you pass a cookie, and all PHYs need to use the same
> > cookie to identify the package.
> 
> how would a phy driver deduce a common cookie? And how would that be a
> difference to using a PHY address.

For a cookie, i don't care how the driver decides on the cookie. The
core never uses it, other than comparing cookies to combine individual
PHYs into a package. It could be a PHY address. It could be the PHY
address where the global registers are. Or it could be anything else.

> > Maybe base is the wrong name, since MSCC can have the base as the high
> > address of the four, not the low?
> 
> I'd say it might be any of the four addresses as long as it is the same
> across the PHYs in the same package. And in that case you can also have
> the phy_package_read/write() functions.

Yes. That is the semantics which is think is most useful. But then we
don't have a cookie, the value has real significance, and we need to
document what is should mean.

     Andrew
Michael Walle April 20, 2020, 4:11 p.m. UTC | #16
Am 2020-04-20 17:36, schrieb Andrew Lunn:
>> Ok I see, but what locking do you have in mind? We could have 
>> something
>> like
>> 
>> __phy_package_write(struct phy_device *dev, u32 regnum, u16 val)
>> {
>>   return __mdiobus_write(phydev->mdio.bus, phydev->shared->addr,
>>                          regnum, val);
>> }
>> 
>> and its phy_package_write() equivalent. But that would just be
>> convenience functions, nothing where you actually help the user with
>> locking. Am I missing something?
> 
> In general, drivers should not be using __foo functions. We want
> drivers to make use of phy_package_write() which would do the bus
> locking. Look at a typical PHY driver. There is no locking what so
> ever. Just lots of phy_read() and phy write(). The locking is done by
> the core and so should be correct.

Ok, but for example the BCM54140 uses indirect register access and thus
need to lock the mdio bus itself in which case I need the __funcs.

>> > > > Get the core to do reference counting on the structure?
>> > > > Add helpers phy_read_shared(), phy_write_shared(), etc, which does
>> > > > MDIO accesses on the base device, taking care of the locking.
>> > > >
>> > > The "base" access is another thing, I guess, which has nothing to do
>> > > with the shared structure.
>> > >
>> > I'm making the assumption that all global addresses are at the base
>> > address. If we don't want to make that assumption, we need the change
>> > the API above so you pass a cookie, and all PHYs need to use the same
>> > cookie to identify the package.
>> 
>> how would a phy driver deduce a common cookie? And how would that be a
>> difference to using a PHY address.
> 
> For a cookie, i don't care how the driver decides on the cookie. The
> core never uses it, other than comparing cookies to combine individual
> PHYs into a package. It could be a PHY address. It could be the PHY
> address where the global registers are. Or it could be anything else.
> 
>> > Maybe base is the wrong name, since MSCC can have the base as the high
>> > address of the four, not the low?
>> 
>> I'd say it might be any of the four addresses as long as it is the 
>> same
>> across the PHYs in the same package. And in that case you can also 
>> have
>> the phy_package_read/write() functions.
> 
> Yes. That is the semantics which is think is most useful. But then we
> don't have a cookie, the value has real significance, and we need to
> document what is should mean.

Agreed.

I will post a RFC shortly.

-michael
Russell King (Oracle) April 20, 2020, 5:20 p.m. UTC | #17
On Mon, Apr 20, 2020 at 05:10:19PM +0200, Michael Walle wrote:
> Hi Andrew,
> 
> Am 2020-04-19 23:55, schrieb Andrew Lunn:
> > > But what does that have to do with the shared structure? I don't think
> > > you have to "bundle" the shared structure with the "access the global
> > > registers" method.
> > 
> > We don't need to. But it would be a good way to clean up code which
> > locks the mdio bus, does a register access on some other device, and
> > then unlocks the bus.
> 
> I'd like do an RFC for that. But how should I proceed with the original
> patch series? Should I send an updated version; you didn't reply to the
> LED stuff. That is the last remark for now.

The LED stuff is something that there isn't a solution for at the
moment.  There's been talk about coming up with some generic way
to describe the PHY LED configuration in DT, but given that almost
every PHY has quite different ways to configure LEDs, I fear such
a task is virtually impossible.

Very few PHYs under Linux have their LEDs operating "correctly" or
in a meaningful or sensible way because of this, and it's been this
way for years.
diff mbox series

Patch

diff --git a/Documentation/hwmon/bcm54140.rst b/Documentation/hwmon/bcm54140.rst
new file mode 100644
index 000000000000..bc6ea4b45966
--- /dev/null
+++ b/Documentation/hwmon/bcm54140.rst
@@ -0,0 +1,45 @@ 
+.. SPDX-License-Identifier: GPL-2.0-only
+
+Broadcom BCM54140 Quad SGMII/QSGMII PHY
+=======================================
+
+Supported chips:
+
+   * Broadcom BCM54140
+
+     Datasheet: not public
+
+Author: Michael Walle <michael@walle.cc>
+
+Description
+-----------
+
+The Broadcom BCM54140 is a Quad SGMII/QSGMII PHY which supports monitoring
+its die temperature as well as two analog voltages.
+
+The AVDDL is a 1.0V analogue voltage, the AVDDH is a 3.3V analogue voltage.
+Both voltages and the temperature are measured in a round-robin fashion.
+
+Sysfs entries
+-------------
+
+The following attributes are supported.
+
+======================= ========================================================
+in0_label		"AVDDL"
+in0_input		Measured AVDDL voltage.
+in0_min			Minimum AVDDL voltage.
+in0_max			Maximum AVDDL voltage.
+in0_alarm		AVDDL voltage alarm.
+
+in1_label		"AVDDH"
+in1_input		Measured AVDDH voltage.
+in1_min			Minimum AVDDH voltage.
+in1_max			Maximum AVDDH voltage.
+in1_alarm		AVDDH voltage alarm.
+
+temp1_input		Die temperature.
+temp1_min		Minimum die temperature.
+temp1_max		Maximum die temperature.
+temp1_alarm		Die temperature alarm.
+======================= ========================================================
diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index f022583f96f6..19ad0846736d 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -42,6 +42,7 @@  Hardware Monitoring Kernel Drivers
    asb100
    asc7621
    aspeed-pwm-tacho
+   bcm54140
    bel-pfe
    coretemp
    da9052
diff --git a/drivers/net/phy/bcm54140.c b/drivers/net/phy/bcm54140.c
index 97465491b41b..97c364ce05e3 100644
--- a/drivers/net/phy/bcm54140.c
+++ b/drivers/net/phy/bcm54140.c
@@ -6,6 +6,7 @@ 
 
 #include <linux/bitfield.h>
 #include <linux/brcmphy.h>
+#include <linux/hwmon.h>
 #include <linux/module.h>
 #include <linux/phy.h>
 
@@ -50,6 +51,54 @@ 
 #define  BCM54140_RDB_TOP_IMR_PORT1	BIT(5)
 #define  BCM54140_RDB_TOP_IMR_PORT2	BIT(6)
 #define  BCM54140_RDB_TOP_IMR_PORT3	BIT(7)
+#define BCM54140_RDB_MON_CTRL		0x831	/* monitor control */
+#define  BCM54140_RDB_MON_CTRL_V_MODE	BIT(3)	/* voltage mode */
+#define  BCM54140_RDB_MON_CTRL_SEL_MASK	GENMASK(2, 1)
+#define  BCM54140_RDB_MON_CTRL_SEL_TEMP	0	/* meassure temperature */
+#define  BCM54140_RDB_MON_CTRL_SEL_1V0	1	/* meassure AVDDL 1.0V */
+#define  BCM54140_RDB_MON_CTRL_SEL_3V3	2	/* meassure AVDDH 3.3V */
+#define  BCM54140_RDB_MON_CTRL_SEL_RR	3	/* meassure all round-robin */
+#define  BCM54140_RDB_MON_CTRL_PWR_DOWN	BIT(0)	/* power-down monitor */
+#define BCM54140_RDB_MON_TEMP_VAL	0x832	/* temperature value */
+#define BCM54140_RDB_MON_TEMP_MAX	0x833	/* temperature high thresh */
+#define BCM54140_RDB_MON_TEMP_MIN	0x834	/* temperature low thresh */
+#define  BCM54140_RDB_MON_TEMP_DATA_MASK GENMASK(9, 0)
+#define BCM54140_RDB_MON_1V0_VAL	0x835	/* AVDDL 1.0V value */
+#define BCM54140_RDB_MON_1V0_MAX	0x836	/* AVDDL 1.0V high thresh */
+#define BCM54140_RDB_MON_1V0_MIN	0x837	/* AVDDL 1.0V low thresh */
+#define  BCM54140_RDB_MON_1V0_DATA_MASK	GENMASK(10, 0)
+#define BCM54140_RDB_MON_3V3_VAL	0x838	/* AVDDH 3.3V value */
+#define BCM54140_RDB_MON_3V3_MAX	0x839	/* AVDDH 3.3V high thresh */
+#define BCM54140_RDB_MON_3V3_MIN	0x83a	/* AVDDH 3.3V low thresh */
+#define  BCM54140_RDB_MON_3V3_DATA_MASK	GENMASK(11, 0)
+#define BCM54140_RDB_MON_ISR		0x83b	/* interrupt status */
+#define  BCM54140_RDB_MON_ISR_3V3	BIT(2)	/* AVDDH 3.3V alarm */
+#define  BCM54140_RDB_MON_ISR_1V0	BIT(1)	/* AVDDL 1.0V alarm */
+#define  BCM54140_RDB_MON_ISR_TEMP	BIT(0)	/* temperature alarm */
+
+/* According to the datasheet the formula is:
+ *   T = 413.35 - (0.49055 * bits[9:0])
+ */
+#define BCM54140_HWMON_TO_TEMP(v) (413350L - (v) * 491)
+#define BCM54140_HWMON_FROM_TEMP(v) DIV_ROUND_CLOSEST_ULL(413350L - (v), 491)
+
+/* According to the datasheet the formula is:
+ *   U = bits[11:0] / 1024 * 220 / 0.2
+ *
+ * Normalized:
+ *   U = bits[11:0] / 4096 * 2514
+ */
+#define BCM54140_HWMON_TO_IN_1V0(v) ((v) * 2514 >> 11)
+#define BCM54140_HWMON_FROM_IN_1V0(v) DIV_ROUND_CLOSEST_ULL(((v) << 11), 2514)
+
+/* According to the datasheet the formula is:
+ *   U = bits[10:0] / 1024 * 880 / 0.7
+ *
+ * Normalized:
+ *   U = bits[10:0] / 2048 * 4400
+ */
+#define BCM54140_HWMON_TO_IN_3V3(v) ((v) * 4400 >> 12)
+#define BCM54140_HWMON_FROM_IN_3V3(v) DIV_ROUND_CLOSEST_ULL(((v) << 12), 4400)
 
 #define BCM54140_DEFAULT_DOWNSHIFT 5
 #define BCM54140_MAX_DOWNSHIFT 9
@@ -57,6 +106,258 @@ 
 struct bcm54140_phy_priv {
 	int port;
 	int base_addr;
+	bool pkg_init;
+	u16 alarm;
+};
+
+static umode_t bcm54140_hwmon_is_visible(const void *data,
+					 enum hwmon_sensor_types type,
+					 u32 attr, int channel)
+{
+	switch (type) {
+	case hwmon_in:
+		switch (attr) {
+		case hwmon_in_min:
+		case hwmon_in_max:
+			return 0644;
+		case hwmon_in_label:
+		case hwmon_in_input:
+		case hwmon_in_alarm:
+			return 0444;
+		default:
+			return 0;
+		}
+	case hwmon_temp:
+		switch (attr) {
+		case hwmon_temp_min:
+		case hwmon_temp_max:
+			return 0644;
+		case hwmon_temp_input:
+		case hwmon_temp_alarm:
+			return 0444;
+		default:
+			return 0;
+		}
+	default:
+		return 0;
+	}
+}
+
+static int bcm54140_hwmon_read_alarm(struct device *dev, unsigned int bit,
+				     long *val)
+{
+	struct phy_device *phydev = dev_get_drvdata(dev);
+	struct bcm54140_phy_priv *priv = phydev->priv;
+	u16 tmp;
+
+	/* latch any alarm bits */
+	tmp = bcm_phy_read_rdb(phydev, BCM54140_RDB_MON_ISR);
+	if (tmp < 0)
+		return tmp;
+	priv->alarm |= tmp;
+
+	*val = !!(priv->alarm & bit);
+	priv->alarm &= ~bit;
+
+	return 0;
+}
+
+static int bcm54140_hwmon_read_temp(struct device *dev, u32 attr,
+				    int channel, long *val)
+{
+	struct phy_device *phydev = dev_get_drvdata(dev);
+	u16 reg, tmp;
+
+	switch (attr) {
+	case hwmon_temp_input:
+		reg = BCM54140_RDB_MON_TEMP_VAL;
+		break;
+	case hwmon_temp_min:
+		reg = BCM54140_RDB_MON_TEMP_MIN;
+		break;
+	case hwmon_temp_max:
+		reg = BCM54140_RDB_MON_TEMP_MAX;
+		break;
+	case hwmon_temp_alarm:
+		return bcm54140_hwmon_read_alarm(dev,
+						 BCM54140_RDB_MON_ISR_TEMP,
+						 val);
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	tmp = bcm_phy_read_rdb(phydev, reg);
+	if (tmp < 0)
+		return tmp;
+
+	*val = BCM54140_HWMON_TO_TEMP(tmp & BCM54140_RDB_MON_TEMP_DATA_MASK);
+
+	return 0;
+}
+
+static int bcm54140_hwmon_read_in(struct device *dev, u32 attr,
+				  int channel, long *val)
+{
+	struct phy_device *phydev = dev_get_drvdata(dev);
+	u16 mask = (!channel) ? BCM54140_RDB_MON_1V0_DATA_MASK
+			      : BCM54140_RDB_MON_3V3_DATA_MASK;
+	u16 bit, reg, tmp;
+
+	switch (attr) {
+	case hwmon_in_input:
+		reg = (!channel) ? BCM54140_RDB_MON_1V0_VAL
+				 : BCM54140_RDB_MON_3V3_VAL;
+		break;
+	case hwmon_in_min:
+		reg = (!channel) ? BCM54140_RDB_MON_1V0_MIN
+				 : BCM54140_RDB_MON_3V3_MIN;
+		break;
+	case hwmon_in_max:
+		reg = (!channel) ? BCM54140_RDB_MON_1V0_MAX
+				 : BCM54140_RDB_MON_3V3_MAX;
+		break;
+	case hwmon_in_alarm:
+		bit = (!channel) ? BCM54140_RDB_MON_ISR_1V0
+				 : BCM54140_RDB_MON_ISR_3V3;
+		return bcm54140_hwmon_read_alarm(dev, bit, val);
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	tmp = bcm_phy_read_rdb(phydev, reg);
+	if (tmp < 0)
+		return tmp;
+
+	if (!channel)
+		*val = BCM54140_HWMON_TO_IN_1V0(tmp & mask);
+	else
+		*val = BCM54140_HWMON_TO_IN_3V3(tmp & mask);
+
+	return 0;
+}
+
+static int bcm54140_hwmon_read(struct device *dev,
+			       enum hwmon_sensor_types type, u32 attr,
+			       int channel, long *val)
+{
+	switch (type) {
+	case hwmon_temp:
+		return bcm54140_hwmon_read_temp(dev, attr, channel, val);
+	case hwmon_in:
+		return bcm54140_hwmon_read_in(dev, attr, channel, val);
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static const char *const bcm54140_hwmon_in_labels[] = {
+	"AVDDL",
+	"AVDDH",
+};
+
+static int bcm54140_hwmon_read_string(struct device *dev,
+				      enum hwmon_sensor_types type, u32 attr,
+				      int channel, const char **str)
+{
+	switch (type) {
+	case hwmon_in:
+		switch (attr) {
+		case hwmon_in_label:
+			*str = bcm54140_hwmon_in_labels[channel];
+			return 0;
+		default:
+			return -EOPNOTSUPP;
+		}
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int bcm54140_hwmon_write_temp(struct device *dev, u32 attr,
+				     int channel, long val)
+{
+	struct phy_device *phydev = dev_get_drvdata(dev);
+	u16 reg;
+
+	switch (attr) {
+	case hwmon_temp_min:
+		reg = BCM54140_RDB_MON_TEMP_MIN;
+		break;
+	case hwmon_temp_max:
+		reg = BCM54140_RDB_MON_TEMP_MAX;
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	return bcm_phy_modify_rdb(phydev, reg, BCM54140_RDB_MON_TEMP_DATA_MASK,
+				  BCM54140_HWMON_FROM_TEMP(val));
+}
+
+static int bcm54140_hwmon_write_in(struct device *dev, u32 attr,
+				   int channel, long val)
+{
+	struct phy_device *phydev = dev_get_drvdata(dev);
+	u16 mask = (!channel) ? BCM54140_RDB_MON_1V0_DATA_MASK
+			      : BCM54140_RDB_MON_3V3_DATA_MASK;
+	unsigned long raw = (!channel) ?  BCM54140_HWMON_FROM_IN_1V0(val)
+				       :  BCM54140_HWMON_FROM_IN_3V3(val);
+	u16 reg;
+
+	raw = clamp_val(raw, 0, mask);
+
+	switch (attr) {
+	case hwmon_in_min:
+		reg = (!channel) ? BCM54140_RDB_MON_1V0_MIN
+				 : BCM54140_RDB_MON_3V3_MIN;
+		break;
+	case hwmon_in_max:
+		reg = (!channel) ? BCM54140_RDB_MON_1V0_MAX
+				 : BCM54140_RDB_MON_3V3_MAX;
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	return bcm_phy_modify_rdb(phydev, reg, mask, raw);
+}
+
+static int bcm54140_hwmon_write(struct device *dev,
+				enum hwmon_sensor_types type, u32 attr,
+				int channel, long val)
+{
+	switch (type) {
+	case hwmon_temp:
+		return bcm54140_hwmon_write_temp(dev, attr, channel, val);
+	case hwmon_in:
+		return bcm54140_hwmon_write_in(dev, attr, channel, val);
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static const struct hwmon_channel_info *bcm54140_hwmon_info[] = {
+	HWMON_CHANNEL_INFO(temp,
+			   HWMON_T_INPUT | HWMON_T_MIN | HWMON_T_MAX |
+			   HWMON_T_ALARM),
+	HWMON_CHANNEL_INFO(in,
+			   HWMON_I_INPUT | HWMON_I_MIN | HWMON_I_MAX |
+			   HWMON_I_ALARM | HWMON_I_LABEL,
+			   HWMON_I_INPUT | HWMON_I_MIN | HWMON_I_MAX |
+			   HWMON_I_ALARM | HWMON_I_LABEL),
+	NULL
+};
+
+static const struct hwmon_ops bcm54140_hwmon_ops = {
+	.is_visible = bcm54140_hwmon_is_visible,
+	.read = bcm54140_hwmon_read,
+	.read_string = bcm54140_hwmon_read_string,
+	.write = bcm54140_hwmon_write,
+};
+
+static const struct hwmon_chip_info bcm54140_chip_info = {
+	.ops = &bcm54140_hwmon_ops,
+	.info = bcm54140_hwmon_info,
 };
 
 static int bcm54140_phy_base_read_rdb(struct phy_device *phydev, u16 rdb)
@@ -203,6 +504,74 @@  static int bcm54140_get_base_addr_and_port(struct phy_device *phydev)
 	return 0;
 }
 
+/* Check if one PHY has already done the init of the parts common to all PHYs
+ * in the Quad PHY package.
+ */
+static bool bcm54140_is_pkg_init(struct phy_device *phydev)
+{
+	struct mdio_device **map = phydev->mdio.bus->mdio_map;
+	struct bcm54140_phy_priv *priv;
+	struct phy_device *phy;
+	int i, addr;
+
+	/* Quad PHY */
+	for (i = 0; i < 4; i++) {
+		priv = phydev->priv;
+		addr = priv->base_addr + i;
+
+		if (!map[addr])
+			continue;
+
+		phy = container_of(map[addr], struct phy_device, mdio);
+
+		if ((phy->phy_id & phydev->drv->phy_id_mask) !=
+		    (phydev->drv->phy_id & phydev->drv->phy_id_mask))
+			continue;
+
+		priv = phy->priv;
+
+		if (priv && priv->pkg_init)
+			return true;
+	}
+
+	return false;
+}
+
+static int bcm54140_enable_monitoring(struct phy_device *phydev)
+{
+	u16 mask, set;
+
+	/* 3.3V voltage mode */
+	set = BCM54140_RDB_MON_CTRL_V_MODE;
+
+	/* select round-robin */
+	mask = BCM54140_RDB_MON_CTRL_SEL_MASK;
+	set |= FIELD_PREP(BCM54140_RDB_MON_CTRL_SEL_MASK,
+			  BCM54140_RDB_MON_CTRL_SEL_RR);
+
+	/* remove power-down bit */
+	mask |= BCM54140_RDB_MON_CTRL_PWR_DOWN;
+
+	return bcm_phy_modify_rdb(phydev, BCM54140_RDB_MON_CTRL, mask, set);
+}
+
+static int bcm54140_phy_probe_once(struct phy_device *phydev)
+{
+	struct device *hwmon;
+	int ret;
+
+	/* enable hardware monitoring */
+	ret = bcm54140_enable_monitoring(phydev);
+	if (ret)
+		return ret;
+
+	hwmon = devm_hwmon_device_register_with_info(&phydev->mdio.dev,
+						     "BCM54140", phydev,
+						     &bcm54140_chip_info,
+						     NULL);
+	return PTR_ERR_OR_ZERO(hwmon);
+}
+
 static int bcm54140_phy_probe(struct phy_device *phydev)
 {
 	struct bcm54140_phy_priv *priv;
@@ -218,6 +587,14 @@  static int bcm54140_phy_probe(struct phy_device *phydev)
 	if (ret)
 		return ret;
 
+	if (!bcm54140_is_pkg_init(phydev)) {
+		ret = bcm54140_phy_probe_once(phydev);
+		if (ret)
+			return ret;
+	}
+
+	priv->pkg_init = true;
+
 	dev_info(&phydev->mdio.dev,
 		 "probed (port %d, base PHY address %d)\n",
 		 priv->port, priv->base_addr);