@@ -478,6 +478,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
@@ -100,4 +100,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
new file mode 100644
@@ -0,0 +1,454 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * X-Powers AC300 Ethernet PHY control driver
+ *
+ * Copyright (C) 2026 James Hilliard <james.hilliard1@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_LED_DRV_MASK GENMASK(13, 12)
+#define AC300_MII_DRV_MASK GENMASK(11, 10)
+#define AC300_IO_DRV_LEVEL_2 2
+#define AC300_CLKIN_PAD_ENABLE BIT(4)
+#define AC300_DUPLEX_LED_IO_ENABLE BIT(3)
+#define AC300_SPEED_LED_IO_ENABLE BIT(2)
+#define AC300_LINK_LED_IO_ENABLE BIT(1)
+#define AC300_EPHY_MII_IO_ENABLE BIT(0)
+#define AC300_LED_IO_ENABLE_MASK \
+ (AC300_DUPLEX_LED_IO_ENABLE | AC300_SPEED_LED_IO_ENABLE | \
+ AC300_LINK_LED_IO_ENABLE)
+
+#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_LED_ACTIVE_LOW BIT(1)
+#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_BASE_VALUE \
+ (FIELD_PREP(AC300_MDIO_DRV_MASK, AC300_IO_DRV_LEVEL_2) | \
+ FIELD_PREP(AC300_LED_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;
+ u16 led_io_enable;
+ 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 int
+ac300_ephy_ctl_set_led_outputs(struct acx00_ephy_control *control,
+ unsigned long outputs)
+{
+ struct ac300_ephy_ctl *priv =
+ container_of(control, struct ac300_ephy_ctl, control);
+ u16 led_io_enable;
+ int ret = 0;
+
+ if (outputs & ~GENMASK(ACX00_EPHY_LED_COUNT - 1, 0))
+ return -EINVAL;
+
+ led_io_enable = (outputs << 1) & AC300_LED_IO_ENABLE_MASK;
+
+ mutex_lock(&priv->lock);
+ if (priv->powered)
+ ret = mdiodev_modify(priv->mdiodev, AC300_SYS_IO_REG,
+ AC300_LED_IO_ENABLE_MASK, led_io_enable);
+ if (!ret)
+ priv->led_io_enable = led_io_enable;
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static int
+ac300_ephy_ctl_set_led_polarity(struct acx00_ephy_control *control,
+ bool active_low)
+{
+ struct ac300_ephy_ctl *priv =
+ container_of(control, struct ac300_ephy_ctl, control);
+ u16 value = active_low ? AC300_EPHY_LED_ACTIVE_LOW : 0;
+ int ret = 0;
+
+ mutex_lock(&priv->lock);
+ if (priv->powered)
+ ret = mdiodev_modify(priv->mdiodev, AC300_EPHY_CONFIG_REG,
+ AC300_EPHY_LED_ACTIVE_LOW, value);
+ if (!ret) {
+ priv->ephy_config &= ~AC300_EPHY_LED_ACTIVE_LOW;
+ priv->ephy_config |= value;
+ }
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+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,
+ priv->ephy_config | 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_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;
+ int sys_control;
+ u16 reset_value;
+ 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) {
+ 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;
+ }
+ }
+
+ 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_BASE_VALUE | priv->led_io_enable);
+ if (ret)
+ goto err_power_off;
+
+ fsleep(10000);
+
+ ret = mdiodev_write(priv->mdiodev, AC300_EPHY_CONFIG_REG,
+ priv->ephy_config | AC300_EPHY_SHUTDOWN);
+ if (ret)
+ goto err_power_off;
+
+ fsleep(10000);
+
+ ret = mdiodev_write(priv->mdiodev, AC300_EPHY_CONFIG_REG,
+ priv->ephy_config);
+ 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;
+ phy_interface_t interface;
+ u8 calibration;
+ u8 bgs_effuse_code;
+ u8 package_status;
+ u16 ephy_config;
+ struct clk *clk;
+ int mask_version;
+ int sys_control;
+ unsigned long clk_rate;
+ int ret;
+
+ 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);
+
+ clk = devm_clk_get(dev, NULL);
+ if (IS_ERR(clk))
+ return dev_err_probe(dev, PTR_ERR(clk),
+ "failed to get input clock\n");
+ priv->clk = clk;
+
+ ret = devm_clk_rate_exclusive_get(dev, clk);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to lock clock rate\n");
+
+ clk_rate = clk_get_rate(clk);
+ switch (clk_rate) {
+ case 24000000:
+ 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");
+
+ ret = device_get_phy_mode(dev);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "failed to read PHY mode\n");
+ interface = ret;
+
+ /* 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);
+ ephy_config = FIELD_PREP(AC300_EPHY_BGS_EFFUSE_MASK, bgs_effuse_code) |
+ AC300_EPHY_LED_ACTIVE_LOW;
+ /* EPHY_MODE and BIST_CLK_EN stay clear for normal operation. */
+
+ switch (interface) {
+ case PHY_INTERFACE_MODE_MII:
+ break;
+ case PHY_INTERFACE_MODE_RMII:
+ ephy_config |= AC300_EPHY_RMII_SEL;
+ break;
+ default:
+ return dev_err_probe(dev, -EINVAL,
+ "unsupported PHY mode %s\n",
+ phy_modes(interface));
+ }
+
+ priv->control.power_on = ac300_ephy_ctl_power_on;
+ priv->control.power_off = ac300_ephy_ctl_power_off;
+ priv->control.set_led_outputs = ac300_ephy_ctl_set_led_outputs;
+ priv->control.set_led_polarity = ac300_ephy_ctl_set_led_polarity;
+ priv->control.interface = interface;
+ priv->ephy_config = ephy_config;
+ priv->led_io_enable = AC300_LED_IO_ENABLE_MASK;
+ mdiodev_set_drvdata(mdiodev, &priv->control);
+
+ ret = ac300_ephy_ctl_power_on(&priv->control,
+ ac300_ephy_ctl_link_addr(priv));
+ if (ret)
+ goto err_disable;
+
+ 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);
+
+ 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@gmail.com>");
+MODULE_DESCRIPTION("X-Powers AC300 Ethernet PHY control driver");
+MODULE_LICENSE("GPL");
@@ -4,6 +4,8 @@
#include <linux/phy.h>
+#define AC300_EPHY_CONTROL_ADDR_OFFSET 16
+
enum acx00_ephy_led {
ACX00_EPHY_LED_LINK_ACTIVITY,
ACX00_EPHY_LED_SPEED,