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 | 267 +++++++++++++++++++++++++++++++++++
4 files changed, 274 insertions(+)
create mode 100644 drivers/net/phy/air_an8855.c
diff --git a/MAINTAINERS b/MAINTAINERS
index c42431a09b84..25490fcab5a2 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -725,6 +725,7 @@ F: drivers/mfd/airoha-an8855.c
F: drivers/net/dsa/an8855.c
F: drivers/net/dsa/an8855.h
F: drivers/net/mdio/mdio-an8855.c
+F: drivers/net/phy/air_an8855.c
F: drivers/nvmem/an8855-efuse.c
AIROHA ETHERNET DRIVER
diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
index 15828f4710a9..9b4a7c12b138 100644
--- a/drivers/net/phy/Kconfig
+++ b/drivers/net/phy/Kconfig
@@ -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
diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
index e6145153e837..38ca7432780e 100644
--- a/drivers/net/phy/Makefile
+++ b/drivers/net/phy/Makefile
@@ -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
diff --git a/drivers/net/phy/air_an8855.c b/drivers/net/phy/air_an8855.c
new file mode 100644
index 000000000000..914cd729404b
--- /dev/null
+++ b/drivers/net/phy/air_an8855.c
@@ -0,0 +1,267 @@
+// 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
+#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_SHIFT 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
+
+#define AN8855_PHY_FLAGS_EN_CALIBRATION BIT(0)
+
+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 calib */
+ if (!node)
+ return 0;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ phydev->priv = priv;
+
+ return 0;
+}
+
+static int an8855_get_downshift(struct phy_device *phydev, u8 *data)
+{
+ int val;
+
+ val = phy_read_paged(phydev, AN8855_PHY_PAGE_EXTENDED_1, AN8855_PHY_EXT_REG_14);
+ if (val < 0)
+ return val;
+
+ *data = val & AN8855_PHY_EN_DOWN_SHIFT ? DOWNSHIFT_DEV_DEFAULT_COUNT :
+ DOWNSHIFT_DEV_DISABLE;
+
+ return 0;
+}
+
+static int an8855_set_downshift(struct phy_device *phydev, u8 cnt)
+{
+ u16 ds = cnt != DOWNSHIFT_DEV_DISABLE ? AN8855_PHY_EN_DOWN_SHIFT : 0;
+
+ return phy_modify_paged(phydev, AN8855_PHY_PAGE_EXTENDED_1,
+ AN8855_PHY_EXT_REG_14, AN8855_PHY_EN_DOWN_SHIFT,
+ ds);
+}
+
+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.
+ * AN8855_PHY_FLAGS_EN_CALIBRATION signal this.
+ */
+ if (priv && phydev->dev_flags & AN8855_PHY_FLAGS_EN_CALIBRATION) {
+ u8 *calibration_data = priv->calibration_data;
+
+ ret = en8855_get_r50ohm_val(dev, "tx_a", &calibration_data[0]);
+ if (ret)
+ return ret;
+
+ ret = en8855_get_r50ohm_val(dev, "tx_b", &calibration_data[1]);
+ if (ret)
+ return ret;
+
+ ret = en8855_get_r50ohm_val(dev, "tx_c", &calibration_data[2]);
+ if (ret)
+ return ret;
+
+ ret = en8855_get_r50ohm_val(dev, "tx_d", &calibration_data[3]);
+ if (ret)
+ return ret;
+
+ 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");
--
2.45.2
Hi Christian,
kernel test robot noticed the following build errors:
[auto build test ERROR on net-next/main]
url: https://github.com/intel-lab-lkp/linux/commits/Christian-Marangi/dt-bindings-nvmem-Document-support-for-Airoha-AN8855-Switch-EFUSE/20241208-082533
base: net-next/main
patch link: https://lore.kernel.org/r/20241208002105.18074-10-ansuelsmth%40gmail.com
patch subject: [net-next PATCH v10 9/9] net: phy: Add Airoha AN8855 Internal Switch Gigabit PHY
config: sh-allmodconfig (https://download.01.org/0day-ci/archive/20241208/202412081155.xp97LlzV-lkp@intel.com/config)
compiler: sh4-linux-gcc (GCC) 14.2.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20241208/202412081155.xp97LlzV-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202412081155.xp97LlzV-lkp@intel.com/
All error/warnings (new ones prefixed by >>):
drivers/net/phy/air_an8855.c: In function 'an8855_probe':
>> drivers/net/phy/air_an8855.c:100:13: warning: unused variable 'ret' [-Wunused-variable]
100 | int ret;
| ^~~
drivers/net/phy/air_an8855.c: In function 'an8855_config_init':
>> drivers/net/phy/air_an8855.c:154:45: error: 'dev' undeclared (first use in this function); did you mean 'cdev'?
154 | ret = en8855_get_r50ohm_val(dev, "tx_a", &calibration_data[0]);
| ^~~
| cdev
drivers/net/phy/air_an8855.c:154:45: note: each undeclared identifier is reported only once for each function it appears in
vim +154 drivers/net/phy/air_an8855.c
94
95 static int an8855_probe(struct phy_device *phydev)
96 {
97 struct device *dev = &phydev->mdio.dev;
98 struct device_node *node = dev->of_node;
99 struct air_an8855_priv *priv;
> 100 int ret;
101
102 /* If we don't have a node, skip calib */
103 if (!node)
104 return 0;
105
106 priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
107 if (!priv)
108 return -ENOMEM;
109
110 phydev->priv = priv;
111
112 return 0;
113 }
114
115 static int an8855_get_downshift(struct phy_device *phydev, u8 *data)
116 {
117 int val;
118
119 val = phy_read_paged(phydev, AN8855_PHY_PAGE_EXTENDED_1, AN8855_PHY_EXT_REG_14);
120 if (val < 0)
121 return val;
122
123 *data = val & AN8855_PHY_EN_DOWN_SHIFT ? DOWNSHIFT_DEV_DEFAULT_COUNT :
124 DOWNSHIFT_DEV_DISABLE;
125
126 return 0;
127 }
128
129 static int an8855_set_downshift(struct phy_device *phydev, u8 cnt)
130 {
131 u16 ds = cnt != DOWNSHIFT_DEV_DISABLE ? AN8855_PHY_EN_DOWN_SHIFT : 0;
132
133 return phy_modify_paged(phydev, AN8855_PHY_PAGE_EXTENDED_1,
134 AN8855_PHY_EXT_REG_14, AN8855_PHY_EN_DOWN_SHIFT,
135 ds);
136 }
137
138 static int an8855_config_init(struct phy_device *phydev)
139 {
140 struct air_an8855_priv *priv = phydev->priv;
141 int ret;
142
143 /* Enable HW auto downshift */
144 ret = an8855_set_downshift(phydev, DOWNSHIFT_DEV_DEFAULT_COUNT);
145 if (ret)
146 return ret;
147
148 /* Apply calibration values, if needed.
149 * AN8855_PHY_FLAGS_EN_CALIBRATION signal this.
150 */
151 if (priv && phydev->dev_flags & AN8855_PHY_FLAGS_EN_CALIBRATION) {
152 u8 *calibration_data = priv->calibration_data;
153
> 154 ret = en8855_get_r50ohm_val(dev, "tx_a", &calibration_data[0]);
155 if (ret)
156 return ret;
157
158 ret = en8855_get_r50ohm_val(dev, "tx_b", &calibration_data[1]);
159 if (ret)
160 return ret;
161
162 ret = en8855_get_r50ohm_val(dev, "tx_c", &calibration_data[2]);
163 if (ret)
164 return ret;
165
166 ret = en8855_get_r50ohm_val(dev, "tx_d", &calibration_data[3]);
167 if (ret)
168 return ret;
169
170 ret = phy_modify_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_R500HM_RSEL_TX_AB,
171 AN8855_PHY_R50OHM_RSEL_TX_A | AN8855_PHY_R50OHM_RSEL_TX_B,
172 FIELD_PREP(AN8855_PHY_R50OHM_RSEL_TX_A, calibration_data[0]) |
173 FIELD_PREP(AN8855_PHY_R50OHM_RSEL_TX_B, calibration_data[1]));
174 if (ret)
175 return ret;
176 ret = phy_modify_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_R500HM_RSEL_TX_CD,
177 AN8855_PHY_R50OHM_RSEL_TX_C | AN8855_PHY_R50OHM_RSEL_TX_D,
178 FIELD_PREP(AN8855_PHY_R50OHM_RSEL_TX_C, calibration_data[2]) |
179 FIELD_PREP(AN8855_PHY_R50OHM_RSEL_TX_D, calibration_data[3]));
180 if (ret)
181 return ret;
182 }
183
184 /* Apply values to reduce signal noise */
185 ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_TX_PAIR_DLY_SEL_GBE,
186 FIELD_PREP(AN8855_PHY_CR_DA_TX_PAIR_DELKAY_SEL_A_GBE, 0x4) |
187 FIELD_PREP(AN8855_PHY_CR_DA_TX_PAIR_DELKAY_SEL_C_GBE, 0x4));
188 if (ret)
189 return ret;
190 ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_RXADC_CTRL,
191 AN8855_PHY_RG_AD_SAMNPLE_PHSEL_A |
192 AN8855_PHY_RG_AD_SAMNPLE_PHSEL_C);
193 if (ret)
194 return ret;
195 ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_RXADC_REV_0,
196 FIELD_PREP(AN8855_PHY_RG_AD_RESERVE0_A, 0x1));
197 if (ret)
198 return ret;
199 ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, AN8855_PHY_RXADC_REV_1,
200 FIELD_PREP(AN8855_PHY_RG_AD_RESERVE0_C, 0x1));
201 if (ret)
202 return ret;
203
204 return 0;
205 }
206
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
© 2016 - 2025 Red Hat, Inc.