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

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


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

>The AC200 Fast Ethernet PHY needs package registers in the parent I2C
>regmap to be configured before its Clause 22 endpoint becomes usable.
>
>Add a control driver which obtains the calibration value, applies the
>vendor offset, selects the documented 24 or 27 MHz input clock, programs
>the link PHY address and performs the required reset, clock and shutdown
>sequence. Use the optional SoC SID cell when supplied and otherwise read
>the AC200's internal calibration eFuse.
>
>Early PHY creation occurs before a MAC has attached, so use reset-default
>MII initially. A separate operation later applies phydev->interface; when
>the block is already powered it 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 MII I/O pads in this basic driver;
>dedicated LED outputs remain disabled until a later LED patch describes
>and manages them.
>
>Signed-off-by: James Hilliard <james.hilliard1 at gmail.com>
>---
> drivers/net/phy/Kconfig             |   9 ++
> drivers/net/phy/Makefile            |   1 +
> drivers/net/phy/xpowers-ac200-ctl.c | 294 ++++++++++++++++++++++++++++++++++++
> drivers/net/phy/xpowers-acx00.h     |  15 ++
> 4 files changed, 319 insertions(+)
>
>diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
>index a29d3fed8a05..6119f4de880d 100644
>--- a/drivers/net/phy/Kconfig
>+++ b/drivers/net/phy/Kconfig
>@@ -475,6 +475,15 @@ config VITESSE_PHY
> 	help
> 	  Currently supports the vsc8244
> 
>+config XPOWERS_AC200_PHY_CTL
>+	tristate "X-Powers AC200 Ethernet PHY control"
>+	depends on MFD_AC200
>+	help
>+	  Enable the control driver for the Fast Ethernet PHY function in
>+	  the X-Powers AC200 mixed-signal companion IC. It programs the PHY
>+	  address, interface mode, calibration and I/O controls through the
>+	  parent AC200 I2C regmap.
>+
> config XILINX_GMII2RGMII
> 	tristate "Xilinx GMII2RGMII converter driver"
> 	help
>diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
>index e23df5e836e9..f81854fc9f12 100644
>--- a/drivers/net/phy/Makefile
>+++ b/drivers/net/phy/Makefile
>@@ -100,4 +100,5 @@ obj-$(CONFIG_SMSC_PHY)		+= smsc.o
> 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_XILINX_GMII2RGMII) += xilinx_gmii2rgmii.o
>diff --git a/drivers/net/phy/xpowers-ac200-ctl.c b/drivers/net/phy/xpowers-ac200-ctl.c
>new file mode 100644
>index 000000000000..c69e6a8c71a4
>--- /dev/null
>+++ b/drivers/net/phy/xpowers-ac200-ctl.c
>@@ -0,0 +1,294 @@
>+// SPDX-License-Identifier: GPL-2.0-only
>+/*
>+ * X-Powers AC200 Ethernet PHY control driver
>+ *
>+ * Copyright (c) 2022 Arm Ltd. (Andre Przywara <andre.przywara at arm.com>)
>+ * Copyright (C) 2026 James Hilliard <james.hilliard1 at gmail.com>

Quite unclear for me what's Andre's role here
if he worked on that shouldn't he be mentioned in the
commit msg anyhow?

>+ */
>+
>+#include <linux/bitfield.h>
>+#include <linux/clk.h>
>+#include <linux/delay.h>
>+#include <linux/module.h>
>+#include <linux/mutex.h>
>+#include <linux/nvmem-consumer.h>
>+#include <linux/phy.h>
>+#include <linux/platform_device.h>
>+#include <linux/property.h>
>+#include <linux/regmap.h>
>+
>+#include "xpowers-acx00.h"
>+
>+#define AC200_EPHY_BPS_EFFUSE_OFFSET	3
>+
>+#define AC200_SYS_EPHY_CTL0_REG			0x0014
>+#define AC200_EPHY_RESET_DEASSERT		BIT(0)
>+#define AC200_EPHY_SYSCLK_ENABLE			BIT(1)
>+
>+#define AC200_SYS_EPHY_CTL1_REG			0x0016
>+#define AC200_EPHY_MII_IO_ENABLE			BIT(0)
>+
>+/* AC200-internal copy of the Ethernet PHY calibration eFuse. */
>+#define AC200_EFUSE_EPHY_REG			0x8004
>+
>+#define AC200_EPHY_CTL_REG			0x6000
>+#define AC200_EPHY_SHUTDOWN			BIT(0)
>+#define AC200_EPHY_CLK_SEL_24_MHZ		BIT(2)
>+#define AC200_EPHY_PHY_ADDR_MASK			GENMASK(8, 4)
>+#define AC200_EPHY_RMII_SEL			BIT(11)
>+#define AC200_EPHY_BPS_EFFUSE_MASK		GENMASK(15, 12)
>+
>+struct ac200_ephy_ctl {
>+	struct acx00_ephy_control control;
>+	struct regmap *regmap;
>+	struct mutex lock; /* Serializes power sequencing and state. */
>+	u16 ephy_ctl;
>+	unsigned int phy_addr;
>+	phy_interface_t interface;
>+	bool powered;
>+};
>+
>+static u16 ac200_ephy_ctl_config(const struct ac200_ephy_ctl *priv)
>+{
>+	return priv->ephy_ctl |
>+		(priv->interface == PHY_INTERFACE_MODE_RMII ?
>+		 AC200_EPHY_RMII_SEL : 0) |
>+		FIELD_PREP(AC200_EPHY_PHY_ADDR_MASK, priv->phy_addr);
>+}
>+
>+static int ac200_ephy_ctl_power_off_locked(struct ac200_ephy_ctl *priv)
>+{
>+	int err;
>+	int ret;
>+
>+	if (!priv->powered)
>+		return 0;
>+
>+	ret = regmap_write(priv->regmap, AC200_EPHY_CTL_REG,
>+			   ac200_ephy_ctl_config(priv) | AC200_EPHY_SHUTDOWN);

how about creating wrapper logging which register write failed?
currently there's no info at the end what's not ideal when debugging
especially as the're called in a sequence

>+	err = regmap_write(priv->regmap, AC200_SYS_EPHY_CTL1_REG, 0);
>+	if (!ret)
>+		ret = err;
>+	err = regmap_write(priv->regmap, AC200_SYS_EPHY_CTL0_REG, 0);
>+	if (!ret)
>+		ret = err;
>+
>+	priv->powered = false;
>+
>+	return ret;
>+}
>+
>+static int ac200_ephy_ctl_power_off(struct acx00_ephy_control *control)
>+{
>+	struct ac200_ephy_ctl *priv =
>+		container_of(control, struct ac200_ephy_ctl, control);
>+	int ret;
>+
>+	mutex_lock(&priv->lock);
>+	ret = ac200_ephy_ctl_power_off_locked(priv);
>+	mutex_unlock(&priv->lock);
>+
>+	return ret;
>+}
>+
>+static int
>+ac200_ephy_ctl_set_interface(struct acx00_ephy_control *control,
>+			     phy_interface_t interface)
>+{
>+	struct ac200_ephy_ctl *priv =
>+		container_of(control, struct ac200_ephy_ctl, control);
>+	u16 value;
>+	int ret = 0;
>+
>+	switch (interface) {
>+	case PHY_INTERFACE_MODE_MII:
>+		value = 0;
>+		break;
>+	case PHY_INTERFACE_MODE_RMII:
>+		value = AC200_EPHY_RMII_SEL;
>+		break;
>+	default:
>+		return -EINVAL;
>+	}

'value' can be moved into priv->powered branch since
it's used only there

>+
>+	mutex_lock(&priv->lock);
>+	if (priv->interface == interface)
>+		goto out_unlock;
>+
>+	if (priv->powered)
>+		ret = regmap_update_bits(priv->regmap, AC200_EPHY_CTL_REG,
>+					 AC200_EPHY_RMII_SEL, value);
>+	if (!ret)
>+		priv->interface = interface;
>+
>+out_unlock:
>+	mutex_unlock(&priv->lock);
>+
>+	return ret;
>+}
>+
>+static int ac200_ephy_ctl_power_on(struct acx00_ephy_control *control,
>+				   unsigned int phy_addr)
>+{
>+	struct ac200_ephy_ctl *priv =
>+		container_of(control, struct ac200_ephy_ctl, control);
>+	u16 ephy_ctl;
>+	int ret;
>+
>+	if (phy_addr > FIELD_MAX(AC200_EPHY_PHY_ADDR_MASK))
>+		return -EINVAL;
>+
>+	mutex_lock(&priv->lock);
>+	if (priv->powered && priv->phy_addr == phy_addr) {
>+		ret = 0;
>+		goto out_unlock;
>+	}
>+	if (priv->powered) {
>+		ret = ac200_ephy_ctl_power_off_locked(priv);
>+		if (ret)
>+			goto out_unlock;
>+	}
>+	priv->phy_addr = phy_addr;
>+
>+	ephy_ctl = ac200_ephy_ctl_config(priv);
>+
>+	/* Start from a disabled state before applying the configuration. */
>+	ret = regmap_write(priv->regmap, AC200_SYS_EPHY_CTL0_REG, 0);
>+	if (ret)
>+		goto err_disable;
>+
>+	ret = regmap_write(priv->regmap, AC200_SYS_EPHY_CTL1_REG,
>+			   AC200_EPHY_MII_IO_ENABLE);
>+	if (ret)
>+		goto err_disable;
>+
>+	ret = regmap_write(priv->regmap, AC200_EPHY_CTL_REG,
>+			   ephy_ctl | AC200_EPHY_SHUTDOWN);
>+	if (ret)
>+		goto err_disable;
>+
>+	ret = regmap_write(priv->regmap, AC200_SYS_EPHY_CTL0_REG,
>+			   AC200_EPHY_RESET_DEASSERT |
>+			   AC200_EPHY_SYSCLK_ENABLE);
>+	if (ret)
>+		goto err_disable;
>+

what does the value comes from?

>+	fsleep(10000);
>+
>+	ret = regmap_write(priv->regmap, AC200_EPHY_CTL_REG, ephy_ctl);
>+	if (ret)
>+		goto err_disable;
>+
>+	priv->powered = true;
>+	goto out_unlock;
>+
>+err_disable:
>+	/* Attempt every step of the shutdown sequence after a partial start. */
>+	priv->powered = true;
>+	ac200_ephy_ctl_power_off_locked(priv);
>+out_unlock:
>+	mutex_unlock(&priv->lock);
>+
>+	return ret;
>+}
>+
>+static int ac200_ephy_ctl_probe(struct platform_device *pdev)
>+{
>+	struct device *dev = &pdev->dev;
>+	struct ac200_ephy_ctl *priv;
>+	unsigned long clk_rate;
>+	unsigned int calibration;
>+	u8 nvmem_calibration;
>+	u8 bps_effuse_code;
>+	struct clk *clk;
>+	int ret;
>+
>+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
>+	if (!priv)
>+		return -ENOMEM;
>+	mutex_init(&priv->lock);
>+
>+	priv->regmap = dev_get_regmap(dev->parent, NULL);
>+	if (!priv->regmap)
>+		return dev_err_probe(dev, -EPROBE_DEFER,
>+				     "parent regmap is not ready\n");
>+
>+	if (device_property_present(dev, "nvmem-cells")) {
>+		ret = nvmem_cell_read_u8(dev, "calibration",
>+					 &nvmem_calibration);
>+		if (ret)
>+			return dev_err_probe(dev, ret,
>+					     "failed to read calibration data\n");
>+		calibration = nvmem_calibration;
>+	} else {
>+		ret = regmap_read(priv->regmap, AC200_EFUSE_EPHY_REG,
>+				  &calibration);
>+		if (ret)
>+			return dev_err_probe(dev, ret,
>+					     "failed to read on-chip calibration data\n");
>+	}
>+
>+	/* The vendor driver supplies no transfer function beyond this offset. */
>+	bps_effuse_code = (calibration + AC200_EPHY_BPS_EFFUSE_OFFSET) &
>+			   FIELD_MAX(AC200_EPHY_BPS_EFFUSE_MASK);
>+	priv->ephy_ctl =
>+		FIELD_PREP(AC200_EPHY_BPS_EFFUSE_MASK, bps_effuse_code);
>+	/* EPHY_MODE and BIST_CLK_EN stay clear for normal operation. */
>+
>+	clk = clk_get(dev->parent, NULL);
>+	if (IS_ERR(clk))
>+		return dev_err_probe(dev, PTR_ERR(clk),
>+				     "failed to get input clock\n");
>+
>+	clk_rate = clk_get_rate(clk);
>+	clk_put(clk);
>+
>+	switch (clk_rate) {
>+	case 24000000:
>+		priv->ephy_ctl |= AC200_EPHY_CLK_SEL_24_MHZ;
>+		break;
>+	case 27000000:
>+		break;
>+	default:
>+		return dev_err_probe(dev, -EINVAL,
>+				     "unsupported input clock rate %lu Hz\n",
>+				     clk_rate);
>+	}
>+
>+	priv->control.power_on = ac200_ephy_ctl_power_on;
>+	priv->control.power_off = ac200_ephy_ctl_power_off;
>+	priv->control.set_interface = ac200_ephy_ctl_set_interface;
>+	/* MII is the reset default used until the MAC supplies its interface. */
>+	priv->interface = PHY_INTERFACE_MODE_MII;
>+	platform_set_drvdata(pdev, &priv->control);
>+
>+	return 0;
>+}
>+
>+static void ac200_ephy_ctl_remove(struct platform_device *pdev)
>+{
>+	struct acx00_ephy_control *control = platform_get_drvdata(pdev);
>+
>+	control->power_off(control);
>+}
>+
>+static const struct of_device_id ac200_ephy_ctl_of_match[] = {
>+	{ .compatible = "x-powers,ac200-ephy-ctl" },
>+	{ }
>+};
>+MODULE_DEVICE_TABLE(of, ac200_ephy_ctl_of_match);
>+
>+static struct platform_driver ac200_ephy_ctl_driver = {
>+	.probe = ac200_ephy_ctl_probe,
>+	.remove = ac200_ephy_ctl_remove,
>+	.shutdown = ac200_ephy_ctl_remove,
>+	.driver = {
>+		.name = "ac200-ephy-ctl",
>+		.of_match_table = ac200_ephy_ctl_of_match,
>+	},
>+};
>+module_platform_driver(ac200_ephy_ctl_driver);
>+
>+MODULE_AUTHOR("James Hilliard <james.hilliard1 at gmail.com>");
>+MODULE_DESCRIPTION("X-Powers AC200 Ethernet PHY control driver");
>+MODULE_LICENSE("GPL");
>diff --git a/drivers/net/phy/xpowers-acx00.h b/drivers/net/phy/xpowers-acx00.h
>new file mode 100644
>index 000000000000..482ba6faba6e
>--- /dev/null
>+++ b/drivers/net/phy/xpowers-acx00.h
>@@ -0,0 +1,15 @@
>+/* SPDX-License-Identifier: GPL-2.0-only */
>+#ifndef __DRIVERS_NET_PHY_XPOWERS_ACX00_H
>+#define __DRIVERS_NET_PHY_XPOWERS_ACX00_H
>+
>+#include <linux/phy.h>
>+
>+struct acx00_ephy_control {
>+	int (*power_on)(struct acx00_ephy_control *control,
>+			unsigned int phy_addr);
>+	int (*power_off)(struct acx00_ephy_control *control);
>+	int (*set_interface)(struct acx00_ephy_control *control,
>+			     phy_interface_t interface);
>+};
>+
>+#endif
>
>-- 
>2.53.0




More information about the linux-arm-kernel mailing list