[PATCH net-next v2 6/8] net: phy: add X-Powers AC300 EPHY control driver

Jagielski, Jedrzej jedrzej.jagielski at intel.com
Thu Aug 6 02:20:16 PDT 2026


From: James Hilliard <james.hilliard1 at gmail.com> 
Sent: Wednesday, August 5, 2026 4:27 AM

>The AC300 Fast Ethernet PHY needs a separate non-PHY Clause 22 endpoint
>to complete package setup before the link endpoint becomes usable.
>
>Enable VCC1 and the input clock, select its 24, 25 or 27 MHz rate, apply
>the SoC SID calibration value and vendor offset, configure the documented
>I/O drive strength, and perform the reset-before-clock power-up sequence.
>Validate the control and link address relationship against the package
>straps, clear a latched MDIO error, and configure the internal digital
>LDO only for the package variant which uses it.
>
>Early PHY creation uses reset-default MII. A separate operation later
>applies phydev->interface and, if already powered, changes only RMII_SEL
>before the normal PHY soft reset.
>
>Expose serialized, idempotent power and interface operations to the
>common PHY driver. Enable only the MDIO, MII and input-clock pads in this
>basic driver; dedicated LED outputs remain disabled for a later patch.
>
>Signed-off-by: James Hilliard <james.hilliard1 at gmail.com>
>---
> drivers/net/phy/Kconfig             |   9 +
> drivers/net/phy/Makefile            |   1 +
> drivers/net/phy/xpowers-ac300-ctl.c | 423 ++++++++++++++++++++++++++++++++++++
> drivers/net/phy/xpowers-acx00.h     |   2 +
> 4 files changed, 435 insertions(+)
>
>diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
>index 6119f4de880d..7535db7f3471 100644
>--- a/drivers/net/phy/Kconfig
>+++ b/drivers/net/phy/Kconfig
>@@ -484,6 +484,15 @@ config XPOWERS_AC200_PHY_CTL
> 	  address, interface mode, calibration and I/O controls through the
> 	  parent AC200 I2C regmap.
> 
>+config XPOWERS_AC300_PHY_CTL
>+	tristate "X-Powers AC300 Ethernet PHY control"
>+	depends on COMMON_CLK && NVMEM && OF_MDIO
>+	help
>+	  Enable the MDIO control driver for the Fast Ethernet PHY in the
>+	  X-Powers AC300 companion IC. It programs the PHY calibration,
>+	  interface mode, clock and I/O controls through the separate MDIO
>+	  control address.
>+
> config XILINX_GMII2RGMII
> 	tristate "Xilinx GMII2RGMII converter driver"
> 	help
>diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
>index f81854fc9f12..0c4e40c9a28a 100644
>--- a/drivers/net/phy/Makefile
>+++ b/drivers/net/phy/Makefile
>@@ -101,4 +101,5 @@ obj-$(CONFIG_STE10XP)		+= ste10Xp.o
> obj-$(CONFIG_TERANETICS_PHY)	+= teranetics.o
> obj-$(CONFIG_VITESSE_PHY)	+= vitesse.o
> obj-$(CONFIG_XPOWERS_AC200_PHY_CTL) += xpowers-ac200-ctl.o
>+obj-$(CONFIG_XPOWERS_AC300_PHY_CTL) += xpowers-ac300-ctl.o
> obj-$(CONFIG_XILINX_GMII2RGMII) += xilinx_gmii2rgmii.o
>diff --git a/drivers/net/phy/xpowers-ac300-ctl.c b/drivers/net/phy/xpowers-ac300-ctl.c
>new file mode 100644
>index 000000000000..d3ca4df8a9a3
>--- /dev/null
>+++ b/drivers/net/phy/xpowers-ac300-ctl.c
>@@ -0,0 +1,423 @@
>+// SPDX-License-Identifier: GPL-2.0-only
>+/*
>+ * X-Powers AC300 Ethernet PHY control driver
>+ *
>+ * Copyright (C) 2026 James Hilliard <james.hilliard1 at gmail.com>
>+ */
>+
>+#include <linux/bitfield.h>
>+#include <linux/clk.h>
>+#include <linux/delay.h>
>+#include <linux/mdio.h>
>+#include <linux/module.h>
>+#include <linux/mutex.h>
>+#include <linux/nvmem-consumer.h>
>+#include <linux/phy.h>
>+#include <linux/property.h>
>+#include <linux/regulator/consumer.h>
>+
>+#include "xpowers-acx00.h"
>+
>+#define AC300_EPHY_BGS_EFFUSE_OFFSET	3
>+#define AC300_SYS_CONTROL_REG			0x00
>+#define AC300_CHIP_VERSION_MASK			GENMASK(15, 12)
>+#define AC300_PACKAGE_STATUS_MASK		GENMASK(11, 8)
>+#define AC300_EPHY_CLK_SEL_MASK			GENMASK(7, 6)
>+#define AC300_EPHY_CLK_SEL_25_MHZ		FIELD_PREP(AC300_EPHY_CLK_SEL_MASK, 0)
>+#define AC300_EPHY_CLK_SEL_27_MHZ		FIELD_PREP(AC300_EPHY_CLK_SEL_MASK, 1)
>+#define AC300_EPHY_CLK_SEL_24_MHZ		FIELD_PREP(AC300_EPHY_CLK_SEL_MASK, 2)
>+#define AC300_EFUSE_CLK_ENABLE			BIT(5)
>+#define AC300_EPHY_REG_CLK_ENABLE		BIT(4)
>+#define AC300_MDIO_ERROR			BIT(3)
>+#define AC300_CLKIN_GATING_ENABLE		BIT(2)
>+#define AC300_EPHY_RESET_DEASSERT		BIT(1)
>+#define AC300_CHIP_RESET_DEASSERT		BIT(0)
>+
>+#define AC300_MASK_VERSION_REG			0x04
>+#define AC300_MASK_VERSION_MASK			GENMASK(2, 0)
>+
>+#define AC300_PACKAGE_POR_INTERNAL_DLDO		BIT(3)
>+#define AC300_PACKAGE_PHY_ADDR_MASK		GENMASK(2, 0)
>+
>+#define AC300_SYS_BIAS1_REG			0x02
>+#define AC300_INTERNAL_DLDO_ENABLE		BIT(15)
>+
>+#define AC300_SYS_IO_REG			0x05
>+#define AC300_MDIO_DRV_MASK			GENMASK(15, 14)
>+#define AC300_MII_DRV_MASK			GENMASK(11, 10)
>+#define AC300_IO_DRV_LEVEL_2			2
>+#define AC300_CLKIN_PAD_ENABLE			BIT(4)
>+#define AC300_EPHY_MII_IO_ENABLE			BIT(0)
>+
>+#define AC300_EPHY_CONFIG_REG			0x06
>+#define AC300_EPHY_BGS_EFFUSE_MASK		GENMASK(15, 12)
>+#define AC300_EPHY_RMII_SEL			BIT(11)
>+#define AC300_EPHY_SHUTDOWN			BIT(0)
>+
>+#define AC300_SYS_CONTROL_ENABLE_BITS \
>+	(AC300_EFUSE_CLK_ENABLE | AC300_EPHY_REG_CLK_ENABLE | \
>+	 AC300_CLKIN_GATING_ENABLE | AC300_EPHY_RESET_DEASSERT | \
>+	 AC300_CHIP_RESET_DEASSERT)
>+
>+#define AC300_SYS_IO_VALUE \
>+	(FIELD_PREP(AC300_MDIO_DRV_MASK, AC300_IO_DRV_LEVEL_2) | \
>+	 FIELD_PREP(AC300_MII_DRV_MASK, AC300_IO_DRV_LEVEL_2) | \
>+	 AC300_CLKIN_PAD_ENABLE | AC300_EPHY_MII_IO_ENABLE)
>+
>+struct ac300_ephy_ctl {
>+	struct acx00_ephy_control control;
>+	struct mdio_device *mdiodev;
>+	struct clk *clk;
>+	struct mutex lock; /* Serializes power sequencing and state. */
>+	u16 sys_control;
>+	u16 ephy_config;
>+	phy_interface_t interface;
>+	bool package_known;
>+	bool internal_dldo;
>+	bool powered;
>+};
>+
>+static unsigned int
>+ac300_ephy_ctl_link_addr(const struct ac300_ephy_ctl *priv)
>+{
>+	return priv->mdiodev->addr - AC300_EPHY_CONTROL_ADDR_OFFSET;
>+}
>+
>+static u16 ac300_ephy_ctl_config(const struct ac300_ephy_ctl *priv)
>+{
>+	return priv->ephy_config |
>+		(priv->interface == PHY_INTERFACE_MODE_RMII ?
>+		 AC300_EPHY_RMII_SEL : 0);
>+}
>+
>+static int ac300_ephy_ctl_power_off_locked(struct ac300_ephy_ctl *priv)
>+{
>+	int err;
>+	int ret;
>+
>+	if (!priv->powered)
>+		return 0;
>+
>+	ret = mdiodev_write(priv->mdiodev, AC300_EPHY_CONFIG_REG,
>+			    ac300_ephy_ctl_config(priv) | AC300_EPHY_SHUTDOWN);
>+	err = mdiodev_write(priv->mdiodev, AC300_SYS_IO_REG, 0);
>+	if (!ret)
>+		ret = err;
>+	err = mdiodev_write(priv->mdiodev, AC300_SYS_CONTROL_REG,
>+			    priv->package_known && !priv->internal_dldo ?
>+			    AC300_CHIP_RESET_DEASSERT : 0);
>+	if (!ret)
>+		ret = err;
>+
>+	clk_disable_unprepare(priv->clk);
>+	priv->powered = false;
>+
>+	return ret;
>+}
>+
>+static int ac300_ephy_ctl_power_off(struct acx00_ephy_control *control)
>+{
>+	struct ac300_ephy_ctl *priv =
>+		container_of(control, struct ac300_ephy_ctl, control);
>+	int ret;
>+
>+	mutex_lock(&priv->lock);
>+	ret = ac300_ephy_ctl_power_off_locked(priv);
>+	mutex_unlock(&priv->lock);
>+
>+	return ret;
>+}
>+
>+static int
>+ac300_ephy_ctl_set_interface(struct acx00_ephy_control *control,
>+			     phy_interface_t interface)
>+{
>+	struct ac300_ephy_ctl *priv =
>+		container_of(control, struct ac300_ephy_ctl, control);
>+	u16 value;
>+	int ret = 0;

RCT

>+
>+	switch (interface) {
>+	case PHY_INTERFACE_MODE_MII:
>+		value = 0;
>+		break;
>+	case PHY_INTERFACE_MODE_RMII:
>+		value = AC300_EPHY_RMII_SEL;
>+		break;
>+	default:
>+		return -EINVAL;
>+	}
>+
>+	mutex_lock(&priv->lock);
>+	if (priv->interface == interface)
>+		goto out_unlock;
>+
>+	if (priv->powered)
>+		ret = mdiodev_modify(priv->mdiodev, AC300_EPHY_CONFIG_REG,
>+				     AC300_EPHY_RMII_SEL, value);

value can be moved, same like for previous patch

>+	if (!ret)
>+		priv->interface = interface;
>+
>+out_unlock:
>+	mutex_unlock(&priv->lock);
>+
>+	return ret;
>+}
>+
>+static int ac300_ephy_ctl_power_on(struct acx00_ephy_control *control,
>+				   unsigned int phy_addr)
>+{
>+	struct ac300_ephy_ctl *priv =
>+		container_of(control, struct ac300_ephy_ctl, control);
>+	u8 package_status;
>+	u16 reset_value;
>+	int sys_control;
>+	int ret;
>+
>+	if (phy_addr != ac300_ephy_ctl_link_addr(priv))
>+		return -EINVAL;
>+
>+	mutex_lock(&priv->lock);
>+	if (priv->powered) {
>+		ret = 0;
>+		goto out_unlock;
>+	}
>+
>+	ret = clk_prepare_enable(priv->clk);
>+	if (ret)
>+		goto out_unlock;
>+	priv->powered = true;
>+
>+	/* Keep the external-supply configuration across subsequent resets. */
>+	reset_value = priv->package_known && !priv->internal_dldo ?
>+		      AC300_CHIP_RESET_DEASSERT : 0;
>+	ret = mdiodev_write(priv->mdiodev, AC300_SYS_CONTROL_REG, reset_value);
>+	if (ret)
>+		goto err_power_off;
>+
>+	/* The manual requires both resets to be released before the clocks. */
>+	ret = mdiodev_write(priv->mdiodev, AC300_SYS_CONTROL_REG,
>+			    AC300_EPHY_RESET_DEASSERT |
>+			    AC300_CHIP_RESET_DEASSERT);
>+	if (ret)
>+		goto err_power_off;
>+
>+	/* Retain the vendor clock-enable defaults, including the eFuse clock. */
>+	ret = mdiodev_write(priv->mdiodev, AC300_SYS_CONTROL_REG,
>+			    priv->sys_control);
>+	if (ret)
>+		goto err_power_off;
>+
>+	sys_control = mdiodev_read(priv->mdiodev, AC300_SYS_CONTROL_REG);
>+	if (sys_control < 0) {
>+		ret = sys_control;
>+		goto err_power_off;
>+	}
>+	if (sys_control & AC300_MDIO_ERROR) {

sys_control is checked for both and both ifs are contradictory
if else?

>+		ret = mdiodev_write(priv->mdiodev, AC300_SYS_CONTROL_REG,
>+				    priv->sys_control | AC300_MDIO_ERROR);
>+		if (ret)
>+			goto err_power_off;
>+
>+		sys_control = mdiodev_read(priv->mdiodev, AC300_SYS_CONTROL_REG);
>+		if (sys_control < 0) {
>+			ret = sys_control;
>+			goto err_power_off;
>+		}
>+		if (sys_control & AC300_MDIO_ERROR) {
>+			ret = -EIO;
>+			goto err_power_off;
>+		}
>+	}

somehow that doubling instructions with only AC300_MDIO_ERROR changed
doesn't appeal to me, could be done with goto saving some lines
purely cosmetic nothing critical

>+
>+	package_status = FIELD_GET(AC300_PACKAGE_STATUS_MASK, sys_control);
>+	if ((~package_status & AC300_PACKAGE_PHY_ADDR_MASK) !=
>+	    ac300_ephy_ctl_link_addr(priv)) {
>+		ret = -EINVAL;
>+		goto err_power_off;
>+	}
>+
>+	priv->internal_dldo = package_status & AC300_PACKAGE_POR_INTERNAL_DLDO;
>+	priv->package_known = true;
>+	ret = mdiodev_modify(priv->mdiodev, AC300_SYS_BIAS1_REG,
>+			     AC300_INTERNAL_DLDO_ENABLE,
>+			     priv->internal_dldo ?
>+			     AC300_INTERNAL_DLDO_ENABLE : 0);
>+	if (ret)
>+		goto err_power_off;
>+
>+	/* Keep the documented default drive level and leave the IRQ disabled. */
>+	ret = mdiodev_write(priv->mdiodev, AC300_SYS_IO_REG,
>+			    AC300_SYS_IO_VALUE);
>+	if (ret)
>+		goto err_power_off;
>+
>+	fsleep(10000);
>+
>+	ret = mdiodev_write(priv->mdiodev, AC300_EPHY_CONFIG_REG,
>+			    ac300_ephy_ctl_config(priv) | AC300_EPHY_SHUTDOWN);
>+	if (ret)
>+		goto err_power_off;
>+
>+	fsleep(10000);
>+
>+	ret = mdiodev_write(priv->mdiodev, AC300_EPHY_CONFIG_REG,
>+			    ac300_ephy_ctl_config(priv));
>+	if (ret)
>+		goto err_power_off;
>+
>+	goto out_unlock;
>+
>+err_power_off:
>+	ac300_ephy_ctl_power_off_locked(priv);
>+out_unlock:
>+	mutex_unlock(&priv->lock);
>+
>+	return ret;
>+}
>+
>+static int ac300_ephy_ctl_probe(struct mdio_device *mdiodev)
>+{
>+	struct device *dev = &mdiodev->dev;
>+	struct ac300_ephy_ctl *priv;
>+	unsigned long clk_rate;
>+	unsigned int phy_addr;
>+	u8 calibration;
>+	u8 bgs_effuse_code;
>+	u8 package_status;
>+	int mask_version;
>+	int sys_control;
>+	int ret;

RCT

>+
>+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
>+	if (!priv)
>+		return -ENOMEM;
>+	if (mdiodev->addr < AC300_EPHY_CONTROL_ADDR_OFFSET ||
>+	    mdiodev->addr > AC300_EPHY_CONTROL_ADDR_OFFSET +
>+			    FIELD_MAX(AC300_PACKAGE_PHY_ADDR_MASK))
>+		return dev_err_probe(dev, -EINVAL,
>+				     "control address is outside the package range\n");
>+	priv->mdiodev = mdiodev;
>+	mutex_init(&priv->lock);
>+
>+	ret = devm_regulator_get_enable(dev, "vcc1");
>+	if (ret)
>+		return dev_err_probe(dev, ret,
>+				     "failed to enable VCC1 supply\n");
>+
>+	/* Wait for the power-on reset interval specified by the manual. */
>+	fsleep(10000);
>+
>+	priv->clk = devm_clk_get(dev, NULL);
>+	if (IS_ERR(priv->clk))
>+		return dev_err_probe(dev, PTR_ERR(priv->clk),
>+				     "failed to get input clock\n");
>+
>+	ret = devm_clk_rate_exclusive_get(dev, priv->clk);
>+	if (ret)
>+		return dev_err_probe(dev, ret, "failed to lock clock rate\n");
>+
>+	clk_rate = clk_get_rate(priv->clk);
>+	switch (clk_rate) {
>+	case 24000000:

why do not provide defines also for these?

>+		priv->sys_control = AC300_EPHY_CLK_SEL_24_MHZ;
>+		break;
>+	case 25000000:
>+		priv->sys_control = AC300_EPHY_CLK_SEL_25_MHZ;
>+		break;
>+	case 27000000:
>+		priv->sys_control = AC300_EPHY_CLK_SEL_27_MHZ;
>+		break;
>+	default:
>+		return dev_err_probe(dev, -EINVAL,
>+				     "unsupported input clock rate %lu Hz\n",
>+				     clk_rate);
>+	}
>+	priv->sys_control |= AC300_SYS_CONTROL_ENABLE_BITS;
>+
>+	ret = nvmem_cell_read_u8(dev, "calibration", &calibration);
>+	if (ret)
>+		return dev_err_probe(dev, ret,
>+				     "failed to read calibration data\n");
>+
>+	/* The vendor driver supplies no transfer function beyond this offset. */
>+	bgs_effuse_code = (calibration + AC300_EPHY_BGS_EFFUSE_OFFSET) &
>+			   FIELD_MAX(AC300_EPHY_BGS_EFFUSE_MASK);
>+	priv->ephy_config =
>+		FIELD_PREP(AC300_EPHY_BGS_EFFUSE_MASK, bgs_effuse_code);
>+	/* EPHY_MODE and BIST_CLK_EN stay clear for normal operation. */
>+
>+	priv->control.power_on = ac300_ephy_ctl_power_on;
>+	priv->control.power_off = ac300_ephy_ctl_power_off;
>+	priv->control.set_interface = ac300_ephy_ctl_set_interface;
>+	/* MII is the reset default used until the MAC supplies its interface. */
>+	priv->interface = PHY_INTERFACE_MODE_MII;
>+	mdiodev_set_drvdata(mdiodev, &priv->control);
>+
>+	/* Validate the package while the control endpoint is known to respond. */
>+	phy_addr = ac300_ephy_ctl_link_addr(priv);
>+	ret = ac300_ephy_ctl_power_on(&priv->control, phy_addr);
>+	if (ret)
>+		return ret;
>+
>+	sys_control = mdiodev_read(mdiodev, AC300_SYS_CONTROL_REG);
>+	if (sys_control < 0) {
>+		ret = sys_control;
>+		goto err_disable;
>+	}
>+	package_status = FIELD_GET(AC300_PACKAGE_STATUS_MASK, sys_control);
>+
>+	mask_version = mdiodev_read(mdiodev, AC300_MASK_VERSION_REG);
>+	if (mask_version < 0) {
>+		ret = mask_version;
>+		goto err_disable;
>+	}
>+
>+	dev_info(dev, "chip version %u, mask version %u, package %#x, %s supplies, PHY %u, %lu Hz clock\n",
>+		 (unsigned int)FIELD_GET(AC300_CHIP_VERSION_MASK, sys_control),
>+		 (unsigned int)FIELD_GET(AC300_MASK_VERSION_MASK, mask_version),
>+		 package_status,
>+		 package_status & AC300_PACKAGE_POR_INTERNAL_DLDO ?
>+			"POR/internal DLDO" : "reset pin/external VDD",
>+		 ac300_ephy_ctl_link_addr(priv), clk_rate);
>+
>+	ret = ac300_ephy_ctl_power_off(&priv->control);
>+	if (ret)
>+		return dev_err_probe(dev, ret,
>+				     "failed to quiesce control block\n");
>+
>+	return 0;
>+
>+err_disable:
>+	ac300_ephy_ctl_power_off(&priv->control);
>+	return ret;
>+}
>+
>+static void ac300_ephy_ctl_remove(struct mdio_device *mdiodev)
>+{
>+	struct acx00_ephy_control *control = mdiodev_get_drvdata(mdiodev);
>+
>+	control->power_off(control);
>+}
>+
>+static const struct of_device_id ac300_ephy_ctl_of_match[] = {
>+	{ .compatible = "x-powers,ac300-ephy-ctl" },
>+	{ }
>+};
>+MODULE_DEVICE_TABLE(of, ac300_ephy_ctl_of_match);
>+
>+static struct mdio_driver ac300_ephy_ctl_driver = {
>+	.probe = ac300_ephy_ctl_probe,
>+	.remove = ac300_ephy_ctl_remove,
>+	.shutdown = ac300_ephy_ctl_remove,
>+	.mdiodrv.driver = {
>+		.name = "ac300-ephy-ctl",
>+		.of_match_table = ac300_ephy_ctl_of_match,
>+	},
>+};
>+
>+mdio_module_driver(ac300_ephy_ctl_driver);
>+
>+MODULE_AUTHOR("James Hilliard <james.hilliard1 at gmail.com>");
>+MODULE_DESCRIPTION("X-Powers AC300 Ethernet PHY control driver");
>+MODULE_LICENSE("GPL");
>diff --git a/drivers/net/phy/xpowers-acx00.h b/drivers/net/phy/xpowers-acx00.h
>index 482ba6faba6e..0af8895e4cae 100644
>--- a/drivers/net/phy/xpowers-acx00.h
>+++ b/drivers/net/phy/xpowers-acx00.h
>@@ -4,6 +4,8 @@
> 
> #include <linux/phy.h>
> 
>+#define AC300_EPHY_CONTROL_ADDR_OFFSET	16
>+
> struct acx00_ephy_control {
> 	int (*power_on)(struct acx00_ephy_control *control,
> 			unsigned int phy_addr);
>
>-- 
>2.53.0




More information about the linux-arm-kernel mailing list