@@ -726,6 +726,7 @@ S: Maintained
F: Documentation/devicetree/bindings/net/dsa/airoha,an8855.yaml
F: drivers/net/dsa/an8855.c
F: drivers/net/dsa/an8855.h
+F: drivers/net/phy/air_an8855.c
AIROHA ETHERNET DRIVER
M: Lorenzo Bianconi <lorenzo@kernel.org>
@@ -79,6 +79,11 @@ config SFP
comment "MII PHY device drivers"
+config AIR_AN8855_PHY
+ tristate "Airoha AN8855 Internal Gigabit PHY"
+ help
+ Currently supports the internal Airoha AN8855 Switch PHY.
+
config AIR_EN8811H_PHY
tristate "Airoha EN8811H 2.5 Gigabit PHY"
help
@@ -35,6 +35,7 @@ obj-y += $(sfp-obj-y) $(sfp-obj-m)
obj-$(CONFIG_ADIN_PHY) += adin.o
obj-$(CONFIG_ADIN1100_PHY) += adin1100.o
+obj-$(CONFIG_AIR_AN8855_PHY) += air_an8855.o
obj-$(CONFIG_AIR_EN8811H_PHY) += air_en8811h.o
obj-$(CONFIG_AMD_PHY) += amd.o
obj-$(CONFIG_AMCC_QT2025_PHY) += qt2025.o
new file mode 100644
@@ -0,0 +1,278 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2024 Christian Marangi <ansuelsmth@gmail.com>
+ */
+
+#include <linux/phy.h>
+#include <linux/module.h>
+#include <linux/bitfield.h>
+#include <linux/nvmem-consumer.h>
+
+#define AN8855_PHY_SELECT_PAGE 0x1f
+/* Mask speculation based on page up to 0x4 */
+#define AN8855_PHY_PAGE GENMASK(2, 0)
+#define AN8855_PHY_PAGE_STANDARD FIELD_PREP_CONST(AN8855_PHY_PAGE, 0x0)
+#define AN8855_PHY_PAGE_EXTENDED_1 FIELD_PREP_CONST(AN8855_PHY_PAGE, 0x1)
+
+/* MII Registers Page 1 */
+#define AN8855_PHY_EXT_REG_14 0x14
+#define AN8855_PHY_EN_DOWN_SHFIT BIT(4)
+
+/* R50 Calibration regs in MDIO_MMD_VEND1 */
+#define AN8855_PHY_R500HM_RSEL_TX_AB 0x174
+#define AN8855_PHY_R50OHM_RSEL_TX_A_EN BIT(15)
+#define AN8855_PHY_R50OHM_RSEL_TX_A GENMASK(14, 8)
+#define AN8855_PHY_R50OHM_RSEL_TX_B_EN BIT(7)
+#define AN8855_PHY_R50OHM_RSEL_TX_B GENMASK(6, 0)
+#define AN8855_PHY_R500HM_RSEL_TX_CD 0x175
+#define AN8855_PHY_R50OHM_RSEL_TX_C_EN BIT(15)
+#define AN8855_PHY_R50OHM_RSEL_TX_C GENMASK(14, 8)
+#define AN8855_PHY_R50OHM_RSEL_TX_D_EN BIT(7)
+#define AN8855_PHY_R50OHM_RSEL_TX_D GENMASK(6, 0)
+
+#define AN8855_SWITCH_EFUSE_R50O GENMASK(30, 24)
+
+/* PHY TX PAIR DELAY SELECT Register */
+#define AN8855_PHY_TX_PAIR_DLY_SEL_GBE 0x013
+#define AN8855_PHY_CR_DA_TX_PAIR_DELKAY_SEL_A_GBE GENMASK(14, 12)
+#define AN8855_PHY_CR_DA_TX_PAIR_DELKAY_SEL_B_GBE GENMASK(10, 8)
+#define AN8855_PHY_CR_DA_TX_PAIR_DELKAY_SEL_C_GBE GENMASK(6, 4)
+#define AN8855_PHY_CR_DA_TX_PAIR_DELKAY_SEL_D_GBE GENMASK(2, 0)
+/* PHY ADC Register */
+#define AN8855_PHY_RXADC_CTRL 0x0d8
+#define AN8855_PHY_RG_AD_SAMNPLE_PHSEL_A BIT(12)
+#define AN8855_PHY_RG_AD_SAMNPLE_PHSEL_B BIT(8)
+#define AN8855_PHY_RG_AD_SAMNPLE_PHSEL_C BIT(4)
+#define AN8855_PHY_RG_AD_SAMNPLE_PHSEL_D BIT(0)
+#define AN8855_PHY_RXADC_REV_0 0x0d9
+#define AN8855_PHY_RG_AD_RESERVE0_A GENMASK(15, 8)
+#define AN8855_PHY_RG_AD_RESERVE0_B GENMASK(7, 0)
+#define AN8855_PHY_RXADC_REV_1 0x0da
+#define AN8855_PHY_RG_AD_RESERVE0_C GENMASK(15, 8)
+#define AN8855_PHY_RG_AD_RESERVE0_D GENMASK(7, 0)
+
+#define AN8855_PHY_ID 0xc0ff0410
+
+struct air_an8855_priv {
+ u8 calibration_data[4];
+};
+
+static const u8 dsa_r50ohm_table[] = {
+ 127, 127, 127, 127, 127, 127, 127, 127, 127, 127,
+ 127, 127, 127, 127, 127, 127, 127, 126, 122, 117,
+ 112, 109, 104, 101, 97, 94, 90, 88, 84, 80,
+ 78, 74, 72, 68, 66, 64, 61, 58, 56, 53,
+ 51, 48, 47, 44, 42, 40, 38, 36, 34, 32,
+ 31, 28, 27, 24, 24, 22, 20, 18, 16, 16,
+ 14, 12, 11, 9
+};
+
+static int en8855_get_r50ohm_val(struct device *dev, const char *calib_name,
+ u8 *dest)
+{
+ u32 shift_sel, val;
+ int ret;
+ int i;
+
+ ret = nvmem_cell_read_u32(dev, calib_name, &val);
+ if (ret)
+ return ret;
+
+ shift_sel = FIELD_GET(AN8855_SWITCH_EFUSE_R50O, val);
+ for (i = 0; i < ARRAY_SIZE(dsa_r50ohm_table); i++)
+ if (dsa_r50ohm_table[i] == shift_sel)
+ break;
+
+ if (i < 8 || i >= ARRAY_SIZE(dsa_r50ohm_table))
+ *dest = dsa_r50ohm_table[25];
+ else
+ *dest = dsa_r50ohm_table[i - 8];
+
+ return 0;
+}
+
+static int an8855_probe(struct phy_device *phydev)
+{
+ struct device *dev = &phydev->mdio.dev;
+ struct device_node *node = dev->of_node;
+ struct air_an8855_priv *priv;
+ int ret;
+
+ /* If we don't have a node, skip get calib */
+ if (!node)
+ return 0;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ ret = en8855_get_r50ohm_val(dev, "tx_a", &priv->calibration_data[0]);
+ if (ret)
+ return ret;
+
+ ret = en8855_get_r50ohm_val(dev, "tx_b", &priv->calibration_data[1]);
+ if (ret)
+ return ret;
+
+ ret = en8855_get_r50ohm_val(dev, "tx_c", &priv->calibration_data[2]);
+ if (ret)
+ return ret;
+
+ ret = en8855_get_r50ohm_val(dev, "tx_d", &priv->calibration_data[3]);
+ if (ret)
+ return ret;
+
+ phydev->priv = priv;
+
+ return 0;
+}
+
+static int an8855_get_downshift(struct phy_device *phydev, u8 *data)
+{
+ int saved_page;
+ int val;
+ int ret;
+
+ saved_page = phy_select_page(phydev, AN8855_PHY_PAGE_EXTENDED_1);
+ if (saved_page >= 0)
+ val = __phy_read(phydev, AN8855_PHY_EXT_REG_14);
+ ret = phy_restore_page(phydev, saved_page, val);
+ if (ret)
+ return ret;
+
+ *data = val & AN8855_PHY_EXT_REG_14 ? DOWNSHIFT_DEV_DEFAULT_COUNT :
+ DOWNSHIFT_DEV_DISABLE;
+
+ return 0;
+}
+
+static int an8855_set_downshift(struct phy_device *phydev, u8 cnt)
+{
+ int saved_page;
+ int ret;
+
+ saved_page = phy_select_page(phydev, AN8855_PHY_PAGE_EXTENDED_1);
+ if (saved_page >= 0) {
+ if (cnt != DOWNSHIFT_DEV_DISABLE)
+ ret = __phy_set_bits(phydev, AN8855_PHY_EXT_REG_14,
+ AN8855_PHY_EN_DOWN_SHFIT);
+ else
+ ret = __phy_clear_bits(phydev, AN8855_PHY_EXT_REG_14,
+ AN8855_PHY_EN_DOWN_SHFIT);
+ }
+
+ return phy_restore_page(phydev, saved_page, ret);
+}
+
+static int an8855_config_init(struct phy_device *phydev)
+{
+ struct air_an8855_priv *priv = phydev->priv;
+ int ret;
+
+ /* Enable HW auto downshift */
+ ret = an8855_set_downshift(phydev, DOWNSHIFT_DEV_DEFAULT_COUNT);
+ if (ret)
+ return ret;
+
+ /* Apply calibration values, if needed. BIT(0) signal this */
+ if (phydev->dev_flags & BIT(0)) {
+ u8 *calibration_data = priv->calibration_data;
+
+ ret = phy_modify_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_R500HM_RSEL_TX_AB,
+ AN8855_PHY_R50OHM_RSEL_TX_A | AN8855_PHY_R50OHM_RSEL_TX_B,
+ FIELD_PREP(AN8855_PHY_R50OHM_RSEL_TX_A, calibration_data[0]) |
+ FIELD_PREP(AN8855_PHY_R50OHM_RSEL_TX_B, calibration_data[1]));
+ if (ret)
+ return ret;
+ ret = phy_modify_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_R500HM_RSEL_TX_CD,
+ AN8855_PHY_R50OHM_RSEL_TX_C | AN8855_PHY_R50OHM_RSEL_TX_D,
+ FIELD_PREP(AN8855_PHY_R50OHM_RSEL_TX_C, calibration_data[2]) |
+ FIELD_PREP(AN8855_PHY_R50OHM_RSEL_TX_D, calibration_data[3]));
+ if (ret)
+ return ret;
+ }
+
+ /* Apply values to reduce signal noise */
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_TX_PAIR_DLY_SEL_GBE,
+ FIELD_PREP(AN8855_PHY_CR_DA_TX_PAIR_DELKAY_SEL_A_GBE, 0x4) |
+ FIELD_PREP(AN8855_PHY_CR_DA_TX_PAIR_DELKAY_SEL_C_GBE, 0x4));
+ if (ret)
+ return ret;
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_RXADC_CTRL,
+ AN8855_PHY_RG_AD_SAMNPLE_PHSEL_A |
+ AN8855_PHY_RG_AD_SAMNPLE_PHSEL_C);
+ if (ret)
+ return ret;
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_RXADC_REV_0,
+ FIELD_PREP(AN8855_PHY_RG_AD_RESERVE0_A, 0x1));
+ if (ret)
+ return ret;
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_RXADC_REV_1,
+ FIELD_PREP(AN8855_PHY_RG_AD_RESERVE0_C, 0x1));
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int an8855_get_tunable(struct phy_device *phydev,
+ struct ethtool_tunable *tuna, void *data)
+{
+ switch (tuna->id) {
+ case ETHTOOL_PHY_DOWNSHIFT:
+ return an8855_get_downshift(phydev, data);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int an8855_set_tunable(struct phy_device *phydev,
+ struct ethtool_tunable *tuna, const void *data)
+{
+ switch (tuna->id) {
+ case ETHTOOL_PHY_DOWNSHIFT:
+ return an8855_set_downshift(phydev, *(const u8 *)data);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int an8855_read_page(struct phy_device *phydev)
+{
+ return __phy_read(phydev, AN8855_PHY_SELECT_PAGE);
+}
+
+static int an8855_write_page(struct phy_device *phydev, int page)
+{
+ return __phy_write(phydev, AN8855_PHY_SELECT_PAGE, page);
+}
+
+static struct phy_driver an8855_driver[] = {
+{
+ PHY_ID_MATCH_EXACT(AN8855_PHY_ID),
+ .name = "Airoha AN8855 internal PHY",
+ /* PHY_GBIT_FEATURES */
+ .flags = PHY_IS_INTERNAL,
+ .probe = an8855_probe,
+ .config_init = an8855_config_init,
+ .soft_reset = genphy_soft_reset,
+ .get_tunable = an8855_get_tunable,
+ .set_tunable = an8855_set_tunable,
+ .suspend = genphy_suspend,
+ .resume = genphy_resume,
+ .read_page = an8855_read_page,
+ .write_page = an8855_write_page,
+}, };
+
+module_phy_driver(an8855_driver);
+
+static struct mdio_device_id __maybe_unused an8855_tbl[] = {
+ { PHY_ID_MATCH_EXACT(AN8855_PHY_ID) },
+ { }
+};
+
+MODULE_DEVICE_TABLE(mdio, an8855_tbl);
+
+MODULE_DESCRIPTION("Airoha AN8855 PHY driver");
+MODULE_AUTHOR("Christian Marangi <ansuelsmth@gmail.com>");
+MODULE_LICENSE("GPL");
Add support for Airoha AN8855 Internal Switch Gigabit PHY. This is a simple PHY driver to configure and calibrate the PHY for the AN8855 Switch with the use of NVMEM cells. Signed-off-by: Christian Marangi <ansuelsmth@gmail.com> --- MAINTAINERS | 1 + drivers/net/phy/Kconfig | 5 + drivers/net/phy/Makefile | 1 + drivers/net/phy/air_an8855.c | 278 +++++++++++++++++++++++++++++++++++ 4 files changed, 285 insertions(+) create mode 100644 drivers/net/phy/air_an8855.c