]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
phy: Add driver for EyeQ5 Ethernet PHY wrapper
authorThéo Lebrun <theo.lebrun@bootlin.com>
Mon, 9 Mar 2026 14:37:34 +0000 (15:37 +0100)
committerVinod Koul <vkoul@kernel.org>
Sun, 10 May 2026 12:14:21 +0000 (17:44 +0530)
EyeQ5 embeds a system-controller called OLB. It features many unrelated
registers, and some of those are registers used to configure the
integration of the RGMII/SGMII Cadence PHY used by MACB/GEM instances.

Wrap in a neat generic PHY provider, exposing two PHYs with standard
phy_init() / phy_set_mode() / phy_power_on() operations.

Reviewed-by: Luca Ceresoli <luca.ceresoli@bootlin.com>
Signed-off-by: Théo Lebrun <theo.lebrun@bootlin.com>
Reviewed-by: Vladimir Oltean <olteanv@gmail.com>
Link: https://patch.msgid.link/20260309-macb-phy-v9-1-5afd87d9db43@bootlin.com
Signed-off-by: Vinod Koul <vkoul@kernel.org>
MAINTAINERS
drivers/phy/Kconfig
drivers/phy/Makefile
drivers/phy/phy-eyeq5-eth.c [new file with mode: 0644]

index cf179372c7f0ec5e16d79cdaab892e72bd10567a..421dd786b399a61696a759b56b6b0628f023fd9a 100644 (file)
@@ -17958,6 +17958,7 @@ F:      arch/mips/boot/dts/mobileye/
 F:     arch/mips/configs/eyeq*_defconfig
 F:     arch/mips/mobileye/board-epm5.its.S
 F:     drivers/clk/clk-eyeq.c
+F:     drivers/phy/phy-eyeq5-eth.c
 F:     drivers/pinctrl/pinctrl-eyeq5.c
 F:     drivers/reset/reset-eyeq.c
 F:     include/dt-bindings/clock/mobileye,eyeq5-clk.h
index 872a7f02e1c4d23ec7921f6a27ba1170cdf3fa8e..ab96ee5858c1a9dee2aea3a896c09b397cc30c7f 100644 (file)
@@ -66,6 +66,19 @@ config PHY_CAN_TRANSCEIVER
          functional modes using gpios and sets the attribute max link
          rate, for CAN drivers.
 
+config PHY_EYEQ5_ETH
+       tristate "Ethernet PHY Driver on EyeQ5"
+       depends on OF
+       depends on MACH_EYEQ5 || COMPILE_TEST
+       select AUXILIARY_BUS
+       select GENERIC_PHY
+       default MACH_EYEQ5
+       help
+         Enable this to support the Ethernet PHY integrated on EyeQ5.
+         It supports both RGMII and SGMII. Registers are located in a
+         shared register region called OLB. If M is selected, the
+         module will be called phy-eyeq5-eth.
+
 config PHY_GOOGLE_USB
        tristate "Google Tensor SoC USB PHY driver"
        select GENERIC_PHY
index ae756fea473b70b1217c6b51c9fa132a3ec0e097..f31767745123773757e84b0b5fb85ec286c1d977 100644 (file)
@@ -9,6 +9,7 @@ obj-$(CONFIG_GENERIC_PHY)               += phy-core.o
 obj-$(CONFIG_GENERIC_PHY_MIPI_DPHY)    += phy-core-mipi-dphy.o
 obj-$(CONFIG_PHY_AIROHA_PCIE)          += phy-airoha-pcie.o
 obj-$(CONFIG_PHY_CAN_TRANSCEIVER)      += phy-can-transceiver.o
+obj-$(CONFIG_PHY_EYEQ5_ETH)            += phy-eyeq5-eth.o
 obj-$(CONFIG_PHY_GOOGLE_USB)           += phy-google-usb.o
 obj-$(CONFIG_USB_LGM_PHY)              += phy-lgm-usb.o
 obj-$(CONFIG_PHY_LPC18XX_USB_OTG)      += phy-lpc18xx-usb-otg.o
diff --git a/drivers/phy/phy-eyeq5-eth.c b/drivers/phy/phy-eyeq5-eth.c
new file mode 100644 (file)
index 0000000..c03d77c
--- /dev/null
@@ -0,0 +1,280 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/auxiliary_bus.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/gfp_types.h>
+#include <linux/init.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/phy.h>
+#include <linux/phy/phy.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+
+#define EQ5_PHY_COUNT  2
+
+#define EQ5_PHY0_GP    0x128
+#define EQ5_PHY1_GP    0x12c
+#define EQ5_PHY0_SGMII 0x134
+#define EQ5_PHY1_SGMII 0x138
+
+#define EQ5_GP_TX_SWRST_DIS    BIT(0)          // Tx SW reset
+#define EQ5_GP_TX_M_CLKE       BIT(1)          // Tx M clock enable
+#define EQ5_GP_SYS_SWRST_DIS   BIT(2)          // Sys SW reset
+#define EQ5_GP_SYS_M_CLKE      BIT(3)          // Sys clock enable
+#define EQ5_GP_SGMII_MODE      BIT(4)          // SGMII mode
+#define EQ5_GP_RGMII_DRV       GENMASK(8, 5)   // RGMII drive strength
+
+#define EQ5_SGMII_PWR_EN       BIT(0)
+#define EQ5_SGMII_RST_DIS      BIT(1)
+#define EQ5_SGMII_PLL_EN       BIT(2)
+#define EQ5_SGMII_SIG_DET_SW   BIT(3)
+#define EQ5_SGMII_PWR_STATE    BIT(4)
+#define EQ5_SGMII_PLL_ACK      BIT(18)
+#define EQ5_SGMII_PWR_STATE_ACK        GENMASK(24, 20)
+
+/*
+ * Instead of storing a phy_interface_t, we store this enum.
+ *
+ * We do not deal with RGMII timings in this generic PHY driver,
+ * it is all handled inside the net PHY.
+ */
+enum eq5_phy_submode {
+       EQ5_PHY_SUBMODE_SGMII,
+       EQ5_PHY_SUBMODE_RGMII,
+};
+
+struct eq5_phy_inst {
+       struct device           *dev;
+       struct phy              *phy;
+       void __iomem            *gp, *sgmii;
+       enum eq5_phy_submode    submode;
+       bool                    sgmii_support;
+};
+
+struct eq5_phy_private {
+       struct eq5_phy_inst     phys[EQ5_PHY_COUNT];
+};
+
+static int eq5_phy_exit(struct phy *phy)
+{
+       struct eq5_phy_inst *inst = phy_get_drvdata(phy);
+
+       writel(0, inst->gp);
+       writel(0, inst->sgmii);
+       udelay(5); /* settling time */
+       return 0;
+}
+
+static int eq5_phy_init(struct phy *phy)
+{
+       struct eq5_phy_inst *inst = phy_get_drvdata(phy);
+       u32 reg;
+
+       /*
+        * Hardware stops listening to our instructions once it is started.
+        * It must be reset to reconfigure it.
+        */
+       eq5_phy_exit(phy);
+
+       reg = EQ5_GP_TX_SWRST_DIS | EQ5_GP_TX_M_CLKE |
+             EQ5_GP_SYS_SWRST_DIS | EQ5_GP_SYS_M_CLKE |
+             FIELD_PREP(EQ5_GP_RGMII_DRV, 0x9);
+       writel(reg, inst->gp);
+
+       return 0;
+}
+
+static int eq5_phy_power_on(struct phy *phy)
+{
+       struct eq5_phy_inst *inst = phy_get_drvdata(phy);
+       u32 reg;
+
+       if (inst->submode == EQ5_PHY_SUBMODE_SGMII) {
+               writel(readl(inst->gp) | EQ5_GP_SGMII_MODE, inst->gp);
+
+               reg = EQ5_SGMII_PWR_EN | EQ5_SGMII_RST_DIS | EQ5_SGMII_PLL_EN;
+               writel(reg, inst->sgmii);
+
+               if (readl_poll_timeout(inst->sgmii, reg,
+                                      reg & EQ5_SGMII_PLL_ACK, 1, 100)) {
+                       dev_err(inst->dev, "PLL timeout\n");
+                       return -ETIMEDOUT;
+               }
+
+               reg = readl(inst->sgmii);
+               reg |= EQ5_SGMII_PWR_STATE | EQ5_SGMII_SIG_DET_SW;
+               writel(reg, inst->sgmii);
+       } else {
+               writel(readl(inst->gp) & ~EQ5_GP_SGMII_MODE, inst->gp);
+               writel(0, inst->sgmii);
+       }
+
+       return 0;
+}
+
+static int eq5_phy_power_off(struct phy *phy)
+{
+       struct eq5_phy_inst *inst = phy_get_drvdata(phy);
+
+       writel(readl(inst->gp) & ~EQ5_GP_SGMII_MODE, inst->gp);
+       writel(0, inst->sgmii);
+
+       return 0;
+}
+
+static int eq5_phy_validate(struct phy *phy, enum phy_mode mode, int submode,
+                           union phy_configure_opts *opts)
+{
+       struct eq5_phy_inst *inst = phy_get_drvdata(phy);
+
+       if (mode != PHY_MODE_ETHERNET)
+               return -EINVAL;
+
+       if (phy_interface_mode_is_rgmii(submode))
+               return 0;
+
+       if (inst->sgmii_support && submode == PHY_INTERFACE_MODE_SGMII)
+               return 0;
+
+       return -EINVAL;
+}
+
+static int eq5_phy_set_mode(struct phy *phy, enum phy_mode mode, int submode)
+{
+       struct eq5_phy_inst *inst = phy_get_drvdata(phy);
+       enum eq5_phy_submode target_submode;
+       int ret;
+
+       ret = eq5_phy_validate(phy, mode, submode, NULL);
+       if (ret)
+               return ret;
+
+       if (submode == PHY_INTERFACE_MODE_SGMII)
+               target_submode = EQ5_PHY_SUBMODE_SGMII;
+       else
+               target_submode = EQ5_PHY_SUBMODE_RGMII;
+
+       if (target_submode == inst->submode)
+               return 0;
+
+       inst->submode = target_submode;
+
+       if (phy->power_count) {
+               eq5_phy_init(phy);
+               return eq5_phy_power_on(phy);
+       }
+
+       return 0;
+}
+
+static const struct phy_ops eq5_phy_ops = {
+       .init           = eq5_phy_init,
+       .exit           = eq5_phy_exit,
+       .power_on       = eq5_phy_power_on,
+       .power_off      = eq5_phy_power_off,
+       .set_mode       = eq5_phy_set_mode,
+       .validate       = eq5_phy_validate,
+};
+
+static struct phy *eq5_phy_xlate(struct device *dev,
+                                const struct of_phandle_args *args)
+{
+       struct eq5_phy_private *priv = dev_get_drvdata(dev);
+
+       if (args->args_count != 1 || args->args[0] >= EQ5_PHY_COUNT)
+               return ERR_PTR(-EINVAL);
+
+       return priv->phys[args->args[0]].phy;
+}
+
+static int eq5_phy_probe_phy(struct device *dev, struct eq5_phy_private *priv,
+                            unsigned int index, void __iomem *base,
+                            unsigned int gp, unsigned int sgmii,
+                            bool sgmii_support)
+{
+       struct eq5_phy_inst *inst = &priv->phys[index];
+       struct phy *phy;
+
+       phy = devm_phy_create(dev, dev->of_node, &eq5_phy_ops);
+       if (IS_ERR(phy))
+               return dev_err_probe(dev, PTR_ERR(phy),
+                                    "failed to create PHY %u\n", index);
+
+       inst->dev = dev;
+       inst->phy = phy;
+       inst->gp = base + gp;
+       inst->sgmii = base + sgmii;
+       inst->sgmii_support = sgmii_support;
+       phy_set_drvdata(phy, inst);
+
+       /*
+        * Init inst->submode based on probe hardware state, allowing
+        * consumers to power us on without first setting the mode.
+        */
+       if (sgmii_support && (readl(inst->gp) & EQ5_GP_SGMII_MODE))
+               inst->submode = EQ5_PHY_SUBMODE_SGMII;
+       else
+               inst->submode = EQ5_PHY_SUBMODE_RGMII;
+
+       return 0;
+}
+
+static int eq5_phy_probe(struct auxiliary_device *adev,
+                        const struct auxiliary_device_id *id)
+{
+       struct device *dev = &adev->dev;
+       struct phy_provider *provider;
+       struct eq5_phy_private *priv;
+       void __iomem *base;
+       int ret;
+
+       priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+       if (!priv)
+               return -ENOMEM;
+
+       dev_set_drvdata(dev, priv);
+
+       base = (void __iomem *)dev_get_platdata(dev);
+
+       ret = eq5_phy_probe_phy(dev, priv, 0, base, EQ5_PHY0_GP,
+                               EQ5_PHY0_SGMII, true);
+       if (ret)
+               return ret;
+
+       ret = eq5_phy_probe_phy(dev, priv, 1, base, EQ5_PHY1_GP,
+                               EQ5_PHY1_SGMII, false);
+       if (ret)
+               return ret;
+
+       provider = devm_of_phy_provider_register(dev, eq5_phy_xlate);
+       if (IS_ERR(provider))
+               return dev_err_probe(dev, PTR_ERR(provider),
+                                    "registering provider failed\n");
+
+       return 0;
+}
+
+static const struct auxiliary_device_id eq5_phy_id_table[] = {
+       { .name = "clk_eyeq.phy" },
+       {}
+};
+MODULE_DEVICE_TABLE(auxiliary, eq5_phy_id_table);
+
+static struct auxiliary_driver eq5_phy_driver = {
+       .probe = eq5_phy_probe,
+       .id_table = eq5_phy_id_table,
+};
+module_auxiliary_driver(eq5_phy_driver);
+
+MODULE_DESCRIPTION("EyeQ5 Ethernet PHY driver");
+MODULE_AUTHOR("Théo Lebrun <theo.lebrun@bootlin.com>");
+MODULE_LICENSE("GPL");