// SPDX-License-Identifier: GPL-2.0+
/* Broadcom BCM54140 Quad SGMII/QSGMII Copper/Fiber Gigabit PHY
 *
 * Copyright (c) 2020 Michael Walle <michael@walle.cc>
 */

#include <linux/bitfield.h>
#include <linux/brcmphy.h>
#include <linux/hwmon.h>
#include <linux/module.h>
#include <linux/phy.h>

#include "bcm-phy-lib.h"

/* RDB per-port registers
 */
#define BCM54140_RDB_ISR		0x00a	/* interrupt status */
#define BCM54140_RDB_IMR		0x00b	/* interrupt mask */
#define  BCM54140_RDB_INT_LINK		BIT(1)	/* link status changed */
#define  BCM54140_RDB_INT_SPEED		BIT(2)	/* link speed change */
#define  BCM54140_RDB_INT_DUPLEX	BIT(3)	/* duplex mode changed */
#define BCM54140_RDB_SPARE1		0x012	/* spare control 1 */
#define  BCM54140_RDB_SPARE1_LSLM	BIT(2)	/* link speed LED mode */
#define BCM54140_RDB_SPARE2		0x014	/* spare control 2 */
#define  BCM54140_RDB_SPARE2_WS_RTRY_DIS BIT(8) /* wirespeed retry disable */
#define  BCM54140_RDB_SPARE2_WS_RTRY_LIMIT GENMASK(4, 2) /* retry limit */
#define BCM54140_RDB_SPARE3		0x015	/* spare control 3 */
#define  BCM54140_RDB_SPARE3_BIT0	BIT(0)
#define BCM54140_RDB_LED_CTRL		0x019	/* LED control */
#define  BCM54140_RDB_LED_CTRL_ACTLINK0	BIT(4)
#define  BCM54140_RDB_LED_CTRL_ACTLINK1	BIT(8)
#define BCM54140_RDB_C_APWR		0x01a	/* auto power down control */
#define  BCM54140_RDB_C_APWR_SINGLE_PULSE	BIT(8)	/* single pulse */
#define  BCM54140_RDB_C_APWR_APD_MODE_DIS	0 /* ADP disable */
#define  BCM54140_RDB_C_APWR_APD_MODE_EN	1 /* ADP enable */
#define  BCM54140_RDB_C_APWR_APD_MODE_DIS2	2 /* ADP disable */
#define  BCM54140_RDB_C_APWR_APD_MODE_EN_ANEG	3 /* ADP enable w/ aneg */
#define  BCM54140_RDB_C_APWR_APD_MODE_MASK	GENMASK(6, 5)
#define  BCM54140_RDB_C_APWR_SLP_TIM_MASK BIT(4)/* sleep timer */
#define  BCM54140_RDB_C_APWR_SLP_TIM_2_7 0	/* 2.7s */
#define  BCM54140_RDB_C_APWR_SLP_TIM_5_4 1	/* 5.4s */
#define BCM54140_RDB_C_PWR		0x02a	/* copper power control */
#define  BCM54140_RDB_C_PWR_ISOLATE	BIT(5)	/* super isolate mode */
#define BCM54140_RDB_C_MISC_CTRL	0x02f	/* misc copper control */
#define  BCM54140_RDB_C_MISC_CTRL_WS_EN BIT(4)	/* wirespeed enable */

/* RDB global registers
 */
#define BCM54140_RDB_TOP_IMR		0x82d	/* interrupt mask */
#define  BCM54140_RDB_TOP_IMR_PORT0	BIT(4)
#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_HWMON_TO_IN(ch, v) ((ch) ? BCM54140_HWMON_TO_IN_3V3(v) \
					  : BCM54140_HWMON_TO_IN_1V0(v))
#define BCM54140_HWMON_FROM_IN(ch, v) ((ch) ? BCM54140_HWMON_FROM_IN_3V3(v) \
					    : BCM54140_HWMON_FROM_IN_1V0(v))
#define BCM54140_HWMON_IN_MASK(ch) ((ch) ? BCM54140_RDB_MON_3V3_DATA_MASK \
					 : BCM54140_RDB_MON_1V0_DATA_MASK)
#define BCM54140_HWMON_IN_VAL_REG(ch) ((ch) ? BCM54140_RDB_MON_3V3_VAL \
					    : BCM54140_RDB_MON_1V0_VAL)
#define BCM54140_HWMON_IN_MIN_REG(ch) ((ch) ? BCM54140_RDB_MON_3V3_MIN \
					    : BCM54140_RDB_MON_1V0_MIN)
#define BCM54140_HWMON_IN_MAX_REG(ch) ((ch) ? BCM54140_RDB_MON_3V3_MAX \
					    : BCM54140_RDB_MON_1V0_MAX)
#define BCM54140_HWMON_IN_ALARM_BIT(ch) ((ch) ? BCM54140_RDB_MON_ISR_3V3 \
					      : BCM54140_RDB_MON_ISR_1V0)

/* This PHY has two different PHY IDs depening on its MODE_SEL pin. This
 * pin choses between 4x SGMII and QSGMII mode:
 *   AE02_5009 4x SGMII
 *   AE02_5019 QSGMII
 */
#define BCM54140_PHY_ID_MASK	0xffffffe8

#define BCM54140_PHY_ID_REV(phy_id)	((phy_id) & 0x7)
#define BCM54140_REV_B0			1

#define BCM54140_DEFAULT_DOWNSHIFT 5
#define BCM54140_MAX_DOWNSHIFT 9

struct bcm54140_priv {
	int port;
	int base_addr;
#if IS_ENABLED(CONFIG_HWMON)
	/* protect the alarm bits */
	struct mutex alarm_lock;
	u16 alarm;
#endif
};

#if IS_ENABLED(CONFIG_HWMON)
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_priv *priv = phydev->priv;
	int tmp, ret = 0;

	mutex_lock(&priv->alarm_lock);

	/* latch any alarm bits */
	tmp = bcm_phy_read_rdb(phydev, BCM54140_RDB_MON_ISR);
	if (tmp < 0) {
		ret = tmp;
		goto out;
	}
	priv->alarm |= tmp;

	*val = !!(priv->alarm & bit);
	priv->alarm &= ~bit;

out:
	mutex_unlock(&priv->alarm_lock);
	return ret;
}

static int bcm54140_hwmon_read_temp(struct device *dev, u32 attr, long *val)
{
	struct phy_device *phydev = dev_get_drvdata(dev);
	u16 reg;
	int 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 bit, reg;
	int tmp;

	switch (attr) {
	case hwmon_in_input:
		reg = BCM54140_HWMON_IN_VAL_REG(channel);
		break;
	case hwmon_in_min:
		reg = BCM54140_HWMON_IN_MIN_REG(channel);
		break;
	case hwmon_in_max:
		reg = BCM54140_HWMON_IN_MAX_REG(channel);
		break;
	case hwmon_in_alarm:
		bit = BCM54140_HWMON_IN_ALARM_BIT(channel);
		return bcm54140_hwmon_read_alarm(dev, bit, val);
	default:
		return -EOPNOTSUPP;
	}

	tmp = bcm_phy_read_rdb(phydev, reg);
	if (tmp < 0)
		return tmp;

	tmp &= BCM54140_HWMON_IN_MASK(channel);
	*val = BCM54140_HWMON_TO_IN(channel, tmp);

	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, 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 mask = BCM54140_RDB_MON_TEMP_DATA_MASK;
	u16 reg;

	val = clamp_val(val, BCM54140_HWMON_TO_TEMP(mask),
			BCM54140_HWMON_TO_TEMP(0));

	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, 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 = BCM54140_HWMON_IN_MASK(channel);
	u16 reg;

	val = clamp_val(val, 0, BCM54140_HWMON_TO_IN(channel, mask));

	switch (attr) {
	case hwmon_in_min:
		reg = BCM54140_HWMON_IN_MIN_REG(channel);
		break;
	case hwmon_in_max:
		reg = BCM54140_HWMON_IN_MAX_REG(channel);
		break;
	default:
		return -EOPNOTSUPP;
	}

	return bcm_phy_modify_rdb(phydev, reg, mask,
				  BCM54140_HWMON_FROM_IN(channel, val));
}

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_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_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);
}
#endif

static int bcm54140_base_read_rdb(struct phy_device *phydev, u16 rdb)
{
	int ret;

	phy_lock_mdio_bus(phydev);
	ret = __phy_package_write(phydev, MII_BCM54XX_RDB_ADDR, rdb);
	if (ret < 0)
		goto out;

	ret = __phy_package_read(phydev, MII_BCM54XX_RDB_DATA);

out:
	phy_unlock_mdio_bus(phydev);
	return ret;
}

static int bcm54140_base_write_rdb(struct phy_device *phydev,
				   u16 rdb, u16 val)
{
	int ret;

	phy_lock_mdio_bus(phydev);
	ret = __phy_package_write(phydev, MII_BCM54XX_RDB_ADDR, rdb);
	if (ret < 0)
		goto out;

	ret = __phy_package_write(phydev, MII_BCM54XX_RDB_DATA, val);

out:
	phy_unlock_mdio_bus(phydev);
	return ret;
}

/* Under some circumstances a core PLL may not lock, this will then prevent
 * a successful link establishment. Restart the PLL after the voltages are
 * stable to workaround this issue.
 */
static int bcm54140_b0_workaround(struct phy_device *phydev)
{
	int spare3;
	int ret;

	spare3 = bcm_phy_read_rdb(phydev, BCM54140_RDB_SPARE3);
	if (spare3 < 0)
		return spare3;

	spare3 &= ~BCM54140_RDB_SPARE3_BIT0;

	ret = bcm_phy_write_rdb(phydev, BCM54140_RDB_SPARE3, spare3);
	if (ret)
		return ret;

	ret = phy_modify(phydev, MII_BMCR, 0, BMCR_PDOWN);
	if (ret)
		return ret;

	ret = phy_modify(phydev, MII_BMCR, BMCR_PDOWN, 0);
	if (ret)
		return ret;

	spare3 |= BCM54140_RDB_SPARE3_BIT0;

	return bcm_phy_write_rdb(phydev, BCM54140_RDB_SPARE3, spare3);
}

/* The BCM54140 is a quad PHY where only the first port has access to the
 * global register. Thus we need to find out its PHY address.
 *
 */
static int bcm54140_get_base_addr_and_port(struct phy_device *phydev)
{
	struct bcm54140_priv *priv = phydev->priv;
	struct mii_bus *bus = phydev->mdio.bus;
	int addr, min_addr, max_addr;
	int step = 1;
	u32 phy_id;
	int tmp;

	min_addr = phydev->mdio.addr;
	max_addr = phydev->mdio.addr;
	addr = phydev->mdio.addr;

	/* We scan forward and backwards and look for PHYs which have the
	 * same phy_id like we do. Step 1 will scan forward, step 2
	 * backwards. Once we are finished, we have a min_addr and
	 * max_addr which resembles the range of PHY addresses of the same
	 * type of PHY. There is one caveat; there may be many PHYs of
	 * the same type, but we know that each PHY takes exactly 4
	 * consecutive addresses. Therefore we can deduce our offset
	 * to the base address of this quad PHY.
	 */

	while (1) {
		if (step == 3) {
			break;
		} else if (step == 1) {
			max_addr = addr;
			addr++;
		} else {
			min_addr = addr;
			addr--;
		}

		if (addr < 0 || addr >= PHY_MAX_ADDR) {
			addr = phydev->mdio.addr;
			step++;
			continue;
		}

		/* read the PHY id */
		tmp = mdiobus_read(bus, addr, MII_PHYSID1);
		if (tmp < 0)
			return tmp;
		phy_id = tmp << 16;
		tmp = mdiobus_read(bus, addr, MII_PHYSID2);
		if (tmp < 0)
			return tmp;
		phy_id |= tmp;

		/* see if it is still the same PHY */
		if ((phy_id & phydev->drv->phy_id_mask) !=
		    (phydev->drv->phy_id & phydev->drv->phy_id_mask)) {
			addr = phydev->mdio.addr;
			step++;
		}
	}

	/* The range we get should be a multiple of four. Please note that both
	 * the min_addr and max_addr are inclusive. So we have to add one if we
	 * subtract them.
	 */
	if ((max_addr - min_addr + 1) % 4) {
		dev_err(&phydev->mdio.dev,
			"Detected Quad PHY IDs %d..%d doesn't make sense.\n",
			min_addr, max_addr);
		return -EINVAL;
	}

	priv->port = (phydev->mdio.addr - min_addr) % 4;
	priv->base_addr = phydev->mdio.addr - priv->port;

	return 0;
}

static int bcm54140_probe(struct phy_device *phydev)
{
	struct bcm54140_priv *priv;
	int ret;

	priv = devm_kzalloc(&phydev->mdio.dev, sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;

	phydev->priv = priv;

	ret = bcm54140_get_base_addr_and_port(phydev);
	if (ret)
		return ret;

	devm_phy_package_join(&phydev->mdio.dev, phydev, priv->base_addr, 0);

#if IS_ENABLED(CONFIG_HWMON)
	mutex_init(&priv->alarm_lock);

	if (phy_package_init_once(phydev)) {
		ret = bcm54140_probe_once(phydev);
		if (ret)
			return ret;
	}
#endif

	phydev_dbg(phydev, "probed (port %d, base PHY address %d)\n",
		   priv->port, priv->base_addr);

	return 0;
}

static int bcm54140_config_init(struct phy_device *phydev)
{
	u16 reg = 0xffff;
	int ret;

	/* Apply hardware errata */
	if (BCM54140_PHY_ID_REV(phydev->phy_id) == BCM54140_REV_B0) {
		ret = bcm54140_b0_workaround(phydev);
		if (ret)
			return ret;
	}

	/* Unmask events we are interested in. */
	reg &= ~(BCM54140_RDB_INT_DUPLEX |
		 BCM54140_RDB_INT_SPEED |
		 BCM54140_RDB_INT_LINK);
	ret = bcm_phy_write_rdb(phydev, BCM54140_RDB_IMR, reg);
	if (ret)
		return ret;

	/* LED1=LINKSPD[1], LED2=LINKSPD[2], LED3=LINK/ACTIVITY */
	ret = bcm_phy_modify_rdb(phydev, BCM54140_RDB_SPARE1,
				 0, BCM54140_RDB_SPARE1_LSLM);
	if (ret)
		return ret;

	ret = bcm_phy_modify_rdb(phydev, BCM54140_RDB_LED_CTRL,
				 0, BCM54140_RDB_LED_CTRL_ACTLINK0);
	if (ret)
		return ret;

	/* disable super isolate mode */
	return bcm_phy_modify_rdb(phydev, BCM54140_RDB_C_PWR,
				  BCM54140_RDB_C_PWR_ISOLATE, 0);
}

static irqreturn_t bcm54140_handle_interrupt(struct phy_device *phydev)
{
	int irq_status, irq_mask;

	irq_status = bcm_phy_read_rdb(phydev, BCM54140_RDB_ISR);
	if (irq_status < 0) {
		phy_error(phydev);
		return IRQ_NONE;
	}

	irq_mask = bcm_phy_read_rdb(phydev, BCM54140_RDB_IMR);
	if (irq_mask < 0) {
		phy_error(phydev);
		return IRQ_NONE;
	}
	irq_mask = ~irq_mask;

	if (!(irq_status & irq_mask))
		return IRQ_NONE;

	phy_trigger_machine(phydev);

	return IRQ_HANDLED;
}

static int bcm54140_ack_intr(struct phy_device *phydev)
{
	int reg;

	/* clear pending interrupts */
	reg = bcm_phy_read_rdb(phydev, BCM54140_RDB_ISR);
	if (reg < 0)
		return reg;

	return 0;
}

static int bcm54140_config_intr(struct phy_device *phydev)
{
	struct bcm54140_priv *priv = phydev->priv;
	static const u16 port_to_imr_bit[] = {
		BCM54140_RDB_TOP_IMR_PORT0, BCM54140_RDB_TOP_IMR_PORT1,
		BCM54140_RDB_TOP_IMR_PORT2, BCM54140_RDB_TOP_IMR_PORT3,
	};
	int reg, err;

	if (priv->port >= ARRAY_SIZE(port_to_imr_bit))
		return -EINVAL;

	reg = bcm54140_base_read_rdb(phydev, BCM54140_RDB_TOP_IMR);
	if (reg < 0)
		return reg;

	if (phydev->interrupts == PHY_INTERRUPT_ENABLED) {
		err = bcm54140_ack_intr(phydev);
		if (err)
			return err;

		reg &= ~port_to_imr_bit[priv->port];
		err = bcm54140_base_write_rdb(phydev, BCM54140_RDB_TOP_IMR, reg);
	} else {
		reg |= port_to_imr_bit[priv->port];
		err = bcm54140_base_write_rdb(phydev, BCM54140_RDB_TOP_IMR, reg);
		if (err)
			return err;

		err = bcm54140_ack_intr(phydev);
	}

	return err;
}

static int bcm54140_get_downshift(struct phy_device *phydev, u8 *data)
{
	int val;

	val = bcm_phy_read_rdb(phydev, BCM54140_RDB_C_MISC_CTRL);
	if (val < 0)
		return val;

	if (!(val & BCM54140_RDB_C_MISC_CTRL_WS_EN)) {
		*data = DOWNSHIFT_DEV_DISABLE;
		return 0;
	}

	val = bcm_phy_read_rdb(phydev, BCM54140_RDB_SPARE2);
	if (val < 0)
		return val;

	if (val & BCM54140_RDB_SPARE2_WS_RTRY_DIS)
		*data = 1;
	else
		*data = FIELD_GET(BCM54140_RDB_SPARE2_WS_RTRY_LIMIT, val) + 2;

	return 0;
}

static int bcm54140_set_downshift(struct phy_device *phydev, u8 cnt)
{
	u16 mask, set;
	int ret;

	if (cnt > BCM54140_MAX_DOWNSHIFT && cnt != DOWNSHIFT_DEV_DEFAULT_COUNT)
		return -EINVAL;

	if (!cnt)
		return bcm_phy_modify_rdb(phydev, BCM54140_RDB_C_MISC_CTRL,
					  BCM54140_RDB_C_MISC_CTRL_WS_EN, 0);

	if (cnt == DOWNSHIFT_DEV_DEFAULT_COUNT)
		cnt = BCM54140_DEFAULT_DOWNSHIFT;

	if (cnt == 1) {
		mask = 0;
		set = BCM54140_RDB_SPARE2_WS_RTRY_DIS;
	} else {
		mask = BCM54140_RDB_SPARE2_WS_RTRY_DIS;
		mask |= BCM54140_RDB_SPARE2_WS_RTRY_LIMIT;
		set = FIELD_PREP(BCM54140_RDB_SPARE2_WS_RTRY_LIMIT, cnt - 2);
	}
	ret = bcm_phy_modify_rdb(phydev, BCM54140_RDB_SPARE2,
				 mask, set);
	if (ret)
		return ret;

	return bcm_phy_modify_rdb(phydev, BCM54140_RDB_C_MISC_CTRL,
				  0, BCM54140_RDB_C_MISC_CTRL_WS_EN);
}

static int bcm54140_get_edpd(struct phy_device *phydev, u16 *tx_interval)
{
	int val;

	val = bcm_phy_read_rdb(phydev, BCM54140_RDB_C_APWR);
	if (val < 0)
		return val;

	switch (FIELD_GET(BCM54140_RDB_C_APWR_APD_MODE_MASK, val)) {
	case BCM54140_RDB_C_APWR_APD_MODE_DIS:
	case BCM54140_RDB_C_APWR_APD_MODE_DIS2:
		*tx_interval = ETHTOOL_PHY_EDPD_DISABLE;
		break;
	case BCM54140_RDB_C_APWR_APD_MODE_EN:
	case BCM54140_RDB_C_APWR_APD_MODE_EN_ANEG:
		switch (FIELD_GET(BCM54140_RDB_C_APWR_SLP_TIM_MASK, val)) {
		case BCM54140_RDB_C_APWR_SLP_TIM_2_7:
			*tx_interval = 2700;
			break;
		case BCM54140_RDB_C_APWR_SLP_TIM_5_4:
			*tx_interval = 5400;
			break;
		}
	}

	return 0;
}

static int bcm54140_set_edpd(struct phy_device *phydev, u16 tx_interval)
{
	u16 mask, set;

	mask = BCM54140_RDB_C_APWR_APD_MODE_MASK;
	if (tx_interval == ETHTOOL_PHY_EDPD_DISABLE)
		set = FIELD_PREP(BCM54140_RDB_C_APWR_APD_MODE_MASK,
				 BCM54140_RDB_C_APWR_APD_MODE_DIS);
	else
		set = FIELD_PREP(BCM54140_RDB_C_APWR_APD_MODE_MASK,
				 BCM54140_RDB_C_APWR_APD_MODE_EN_ANEG);

	/* enable single pulse mode */
	set |= BCM54140_RDB_C_APWR_SINGLE_PULSE;

	/* set sleep timer */
	mask |= BCM54140_RDB_C_APWR_SLP_TIM_MASK;
	switch (tx_interval) {
	case ETHTOOL_PHY_EDPD_DFLT_TX_MSECS:
	case ETHTOOL_PHY_EDPD_DISABLE:
	case 2700:
		set |= BCM54140_RDB_C_APWR_SLP_TIM_2_7;
		break;
	case 5400:
		set |= BCM54140_RDB_C_APWR_SLP_TIM_5_4;
		break;
	default:
		return -EINVAL;
	}

	return bcm_phy_modify_rdb(phydev, BCM54140_RDB_C_APWR, mask, set);
}

static int bcm54140_get_tunable(struct phy_device *phydev,
				struct ethtool_tunable *tuna, void *data)
{
	switch (tuna->id) {
	case ETHTOOL_PHY_DOWNSHIFT:
		return bcm54140_get_downshift(phydev, data);
	case ETHTOOL_PHY_EDPD:
		return bcm54140_get_edpd(phydev, data);
	default:
		return -EOPNOTSUPP;
	}
}

static int bcm54140_set_tunable(struct phy_device *phydev,
				struct ethtool_tunable *tuna, const void *data)
{
	switch (tuna->id) {
	case ETHTOOL_PHY_DOWNSHIFT:
		return bcm54140_set_downshift(phydev, *(const u8 *)data);
	case ETHTOOL_PHY_EDPD:
		return bcm54140_set_edpd(phydev, *(const u16 *)data);
	default:
		return -EOPNOTSUPP;
	}
}

static struct phy_driver bcm54140_drivers[] = {
	{
		.phy_id         = PHY_ID_BCM54140,
		.phy_id_mask    = BCM54140_PHY_ID_MASK,
		.name           = "Broadcom BCM54140",
		.flags		= PHY_POLL_CABLE_TEST,
		.features       = PHY_GBIT_FEATURES,
		.config_init    = bcm54140_config_init,
		.handle_interrupt = bcm54140_handle_interrupt,
		.config_intr    = bcm54140_config_intr,
		.probe		= bcm54140_probe,
		.suspend	= genphy_suspend,
		.resume		= genphy_resume,
		.soft_reset	= genphy_soft_reset,
		.get_tunable	= bcm54140_get_tunable,
		.set_tunable	= bcm54140_set_tunable,
		.cable_test_start = bcm_phy_cable_test_start_rdb,
		.cable_test_get_status = bcm_phy_cable_test_get_status_rdb,
	},
};
module_phy_driver(bcm54140_drivers);

static struct mdio_device_id __maybe_unused bcm54140_tbl[] = {
	{ PHY_ID_BCM54140, BCM54140_PHY_ID_MASK },
	{ }
};

MODULE_AUTHOR("Michael Walle");
MODULE_DESCRIPTION("Broadcom BCM54140 PHY driver");
MODULE_DEVICE_TABLE(mdio, bcm54140_tbl);
MODULE_LICENSE("GPL");
