| // SPDX-License-Identifier: GPL-2.0 |
| /* Airoha AN7583 MDIO interface driver |
| * |
| * Copyright (C) 2025 Christian Marangi <ansuelsmth@gmail.com> |
| */ |
| |
| #include <linux/clk.h> |
| #include <linux/delay.h> |
| #include <linux/kernel.h> |
| #include <linux/mfd/syscon.h> |
| #include <linux/module.h> |
| #include <linux/of_mdio.h> |
| #include <linux/of_address.h> |
| #include <linux/platform_device.h> |
| #include <linux/regmap.h> |
| #include <linux/reset.h> |
| |
| /* MII address register definitions */ |
| #define AN7583_MII_BUSY BIT(31) |
| #define AN7583_MII_RDY BIT(30) /* RO signal BUS is ready */ |
| #define AN7583_MII_CL22_REG_ADDR GENMASK(29, 25) |
| #define AN7583_MII_CL45_DEV_ADDR AN7583_MII_CL22_REG_ADDR |
| #define AN7583_MII_PHY_ADDR GENMASK(24, 20) |
| #define AN7583_MII_CMD GENMASK(19, 18) |
| #define AN7583_MII_CMD_CL22_WRITE FIELD_PREP_CONST(AN7583_MII_CMD, 0x1) |
| #define AN7583_MII_CMD_CL22_READ FIELD_PREP_CONST(AN7583_MII_CMD, 0x2) |
| #define AN7583_MII_CMD_CL45_ADDR FIELD_PREP_CONST(AN7583_MII_CMD, 0x0) |
| #define AN7583_MII_CMD_CL45_WRITE FIELD_PREP_CONST(AN7583_MII_CMD, 0x1) |
| #define AN7583_MII_CMD_CL45_POSTREAD_INCADDR FIELD_PREP_CONST(AN7583_MII_CMD, 0x2) |
| #define AN7583_MII_CMD_CL45_READ FIELD_PREP_CONST(AN7583_MII_CMD, 0x3) |
| #define AN7583_MII_ST GENMASK(17, 16) |
| #define AN7583_MII_ST_CL45 FIELD_PREP_CONST(AN7583_MII_ST, 0x0) |
| #define AN7583_MII_ST_CL22 FIELD_PREP_CONST(AN7583_MII_ST, 0x1) |
| #define AN7583_MII_RWDATA GENMASK(15, 0) |
| #define AN7583_MII_CL45_REG_ADDR AN7583_MII_RWDATA |
| |
| #define AN7583_MII_MDIO_DELAY_USEC 100 |
| #define AN7583_MII_MDIO_RETRY_MSEC 100 |
| |
| struct airoha_mdio_data { |
| u32 base_addr; |
| struct regmap *regmap; |
| struct clk *clk; |
| struct reset_control *reset; |
| }; |
| |
| static int airoha_mdio_wait_busy(struct airoha_mdio_data *priv) |
| { |
| u32 busy; |
| |
| return regmap_read_poll_timeout(priv->regmap, priv->base_addr, busy, |
| !(busy & AN7583_MII_BUSY), |
| AN7583_MII_MDIO_DELAY_USEC, |
| AN7583_MII_MDIO_RETRY_MSEC * USEC_PER_MSEC); |
| } |
| |
| static void airoha_mdio_reset(struct airoha_mdio_data *priv) |
| { |
| /* There seems to be Hardware bug where AN7583_MII_RWDATA |
| * is not wiped in the context of unconnected PHY and the |
| * previous read value is returned. |
| * |
| * Example: (only one PHY on the BUS at 0x1f) |
| * - read at 0x1f report at 0x2 0x7500 |
| * - read at 0x0 report 0x7500 on every address |
| * |
| * To workaround this, we reset the Mdio BUS at every read |
| * to have consistent values on read operation. |
| */ |
| reset_control_assert(priv->reset); |
| reset_control_deassert(priv->reset); |
| } |
| |
| static int airoha_mdio_read(struct mii_bus *bus, int addr, int regnum) |
| { |
| struct airoha_mdio_data *priv = bus->priv; |
| u32 val; |
| int ret; |
| |
| airoha_mdio_reset(priv); |
| |
| val = AN7583_MII_BUSY | AN7583_MII_ST_CL22 | |
| AN7583_MII_CMD_CL22_READ; |
| val |= FIELD_PREP(AN7583_MII_PHY_ADDR, addr); |
| val |= FIELD_PREP(AN7583_MII_CL22_REG_ADDR, regnum); |
| |
| ret = regmap_write(priv->regmap, priv->base_addr, val); |
| if (ret) |
| return ret; |
| |
| ret = airoha_mdio_wait_busy(priv); |
| if (ret) |
| return ret; |
| |
| ret = regmap_read(priv->regmap, priv->base_addr, &val); |
| if (ret) |
| return ret; |
| |
| return FIELD_GET(AN7583_MII_RWDATA, val); |
| } |
| |
| static int airoha_mdio_write(struct mii_bus *bus, int addr, int regnum, |
| u16 value) |
| { |
| struct airoha_mdio_data *priv = bus->priv; |
| u32 val; |
| int ret; |
| |
| val = AN7583_MII_BUSY | AN7583_MII_ST_CL22 | |
| AN7583_MII_CMD_CL22_WRITE; |
| val |= FIELD_PREP(AN7583_MII_PHY_ADDR, addr); |
| val |= FIELD_PREP(AN7583_MII_CL22_REG_ADDR, regnum); |
| val |= FIELD_PREP(AN7583_MII_RWDATA, value); |
| |
| ret = regmap_write(priv->regmap, priv->base_addr, val); |
| if (ret) |
| return ret; |
| |
| ret = airoha_mdio_wait_busy(priv); |
| |
| return ret; |
| } |
| |
| static int airoha_mdio_cl45_read(struct mii_bus *bus, int addr, int devnum, |
| int regnum) |
| { |
| struct airoha_mdio_data *priv = bus->priv; |
| u32 val; |
| int ret; |
| |
| airoha_mdio_reset(priv); |
| |
| val = AN7583_MII_BUSY | AN7583_MII_ST_CL45 | |
| AN7583_MII_CMD_CL45_ADDR; |
| val |= FIELD_PREP(AN7583_MII_PHY_ADDR, addr); |
| val |= FIELD_PREP(AN7583_MII_CL45_DEV_ADDR, devnum); |
| val |= FIELD_PREP(AN7583_MII_CL45_REG_ADDR, regnum); |
| |
| ret = regmap_write(priv->regmap, priv->base_addr, val); |
| if (ret) |
| return ret; |
| |
| ret = airoha_mdio_wait_busy(priv); |
| if (ret) |
| return ret; |
| |
| val = AN7583_MII_BUSY | AN7583_MII_ST_CL45 | |
| AN7583_MII_CMD_CL45_READ; |
| val |= FIELD_PREP(AN7583_MII_PHY_ADDR, addr); |
| val |= FIELD_PREP(AN7583_MII_CL45_DEV_ADDR, devnum); |
| |
| ret = regmap_write(priv->regmap, priv->base_addr, val); |
| if (ret) |
| return ret; |
| |
| ret = airoha_mdio_wait_busy(priv); |
| if (ret) |
| return ret; |
| |
| ret = regmap_read(priv->regmap, priv->base_addr, &val); |
| if (ret) |
| return ret; |
| |
| return FIELD_GET(AN7583_MII_RWDATA, val); |
| } |
| |
| static int airoha_mdio_cl45_write(struct mii_bus *bus, int addr, int devnum, |
| int regnum, u16 value) |
| { |
| struct airoha_mdio_data *priv = bus->priv; |
| u32 val; |
| int ret; |
| |
| val = AN7583_MII_BUSY | AN7583_MII_ST_CL45 | |
| AN7583_MII_CMD_CL45_ADDR; |
| val |= FIELD_PREP(AN7583_MII_PHY_ADDR, addr); |
| val |= FIELD_PREP(AN7583_MII_CL45_DEV_ADDR, devnum); |
| val |= FIELD_PREP(AN7583_MII_CL45_REG_ADDR, regnum); |
| |
| ret = regmap_write(priv->regmap, priv->base_addr, val); |
| if (ret) |
| return ret; |
| |
| ret = airoha_mdio_wait_busy(priv); |
| if (ret) |
| return ret; |
| |
| val = AN7583_MII_BUSY | AN7583_MII_ST_CL45 | |
| AN7583_MII_CMD_CL45_WRITE; |
| val |= FIELD_PREP(AN7583_MII_PHY_ADDR, addr); |
| val |= FIELD_PREP(AN7583_MII_CL45_DEV_ADDR, devnum); |
| val |= FIELD_PREP(AN7583_MII_RWDATA, value); |
| |
| ret = regmap_write(priv->regmap, priv->base_addr, val); |
| if (ret) |
| return ret; |
| |
| ret = airoha_mdio_wait_busy(priv); |
| |
| return ret; |
| } |
| |
| static int airoha_mdio_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct airoha_mdio_data *priv; |
| struct mii_bus *bus; |
| u32 addr, freq; |
| int ret; |
| |
| ret = of_property_read_u32(dev->of_node, "reg", &addr); |
| if (ret) |
| return ret; |
| |
| bus = devm_mdiobus_alloc_size(dev, sizeof(*priv)); |
| if (!bus) |
| return -ENOMEM; |
| |
| priv = bus->priv; |
| priv->base_addr = addr; |
| priv->regmap = device_node_to_regmap(dev->parent->of_node); |
| |
| priv->clk = devm_clk_get_enabled(dev, NULL); |
| if (IS_ERR(priv->clk)) |
| return PTR_ERR(priv->clk); |
| |
| priv->reset = devm_reset_control_get_exclusive(dev, NULL); |
| if (IS_ERR(priv->reset)) |
| return PTR_ERR(priv->reset); |
| |
| reset_control_deassert(priv->reset); |
| |
| bus->name = "airoha_mdio_bus"; |
| snprintf(bus->id, MII_BUS_ID_SIZE, "%s-mii", dev_name(dev)); |
| bus->parent = dev; |
| bus->read = airoha_mdio_read; |
| bus->write = airoha_mdio_write; |
| bus->read_c45 = airoha_mdio_cl45_read; |
| bus->write_c45 = airoha_mdio_cl45_write; |
| |
| /* Check if a custom frequency is defined in DT or default to 2.5 MHz */ |
| if (of_property_read_u32(dev->of_node, "clock-frequency", &freq)) |
| freq = 2500000; |
| |
| ret = clk_set_rate(priv->clk, freq); |
| if (ret) |
| return ret; |
| |
| ret = devm_of_mdiobus_register(dev, bus, dev->of_node); |
| if (ret) { |
| reset_control_assert(priv->reset); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static const struct of_device_id airoha_mdio_dt_ids[] = { |
| { .compatible = "airoha,an7583-mdio" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, airoha_mdio_dt_ids); |
| |
| static struct platform_driver airoha_mdio_driver = { |
| .probe = airoha_mdio_probe, |
| .driver = { |
| .name = "airoha-mdio", |
| .of_match_table = airoha_mdio_dt_ids, |
| }, |
| }; |
| |
| module_platform_driver(airoha_mdio_driver); |
| |
| MODULE_DESCRIPTION("Airoha AN7583 MDIO interface driver"); |
| MODULE_AUTHOR("Christian Marangi <ansuelsmth@gmail.com>"); |
| MODULE_LICENSE("GPL"); |