summaryrefslogtreecommitdiff
path: root/drivers/net/phy
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-22 15:58:00 -1000
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-22 15:58:00 -1000
commit734bef548a96e8774d6eef5dfa2026c6edc4e80b (patch)
treece2520b2602ce3b82f9842490d21836f9838d441 /drivers/net/phy
parent864c07de0c7dcbca58b61625dfa2e461ae63b242 (diff)
parent1a2c35c49cc7c6be860038b8442b52424a8f123c (diff)
downloadlinux-next-stable.tar.gz
linux-next-stable.zip
Merge tag 'net-next-7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-nextstable
Pull networking updates from Paolo Abeni: "This includes a significant number of low-priority fixes - especially in the NIC drivers area - that we deferred from the last few RCs in the previous cycle, which can't be easily outlined in the synopsis below. Core & protocols: - Continue the work on removing rtnl_lock dependencies: - per-netns ARP and ND neighbour tables, with the default settings now configurable per netns (inheritance from init_net is controlled by the new net.core.neigh_inherit_init_net sysctl) - RCU-protected configuration and rtnl_lock-less fill_info() for hsr, netkit, sit, vlan and vxlan devices - per-netns device unregistration for ip tunnel and gre devices - use the netdev instance lock instead of rtnl_lock when reading speed and duplex via sysfs - batch the flushing of uncached routes at device unregistration time, relieving rtnl_lock contention on netns dismantle. - Add hardware pacing offload support, admin-controlled via a new rtnetlink attribute, with sch_fq integration (implemented in idpf). - Re-segment BIG TCP GSO packets into GSO packets fitting the egress device limits instead of plain MSS-sized segments, improving throughput for mixed BIG TCP / non-BIG TCP paths by up to 70%. - Generate RSS keys that spread flows over all the RX queues; the previous random keys could leave some queues unused. - Optimize the bridge VLAN forwarding paths: cache the port-VLAN in fdb entries and consider only VLAN members when flooding, for ~10% more Mpps in the fdb forwarding path. - A few TCP/IP performance improvements aimed at efficient cache usage and reducing contention. - Add the TCP ROCCET congestion control, a CUBIC variant aimed at avoiding bufferbloat in cellular networks. - Continue the conversion of getsockopt callbacks to sockopt_t. - Add an ethtool command to list the front-facing ports of an interface, including SFP cages and modules, using the phy_port representation. - Enable BIG TCP support for bareudp tunnels. - Remove the IPV6_ADDRFORM socket option and disallow buggy TCP state transitions (listen() + shutdown() + connect() and connect() + connect(AF_UNSPEC) + listen()). - Remove most BUG_ON()s from core packet processing, replacing them with error paths. Wireless: - Latency improvements, including lockless station get/dump and beacon updates, plus RTNL reduction and locking cleanups. - Infrastructure for non-station TX/RX with MLO. - NAN (Wi-Fi Aware) Instant Communication support and other NAN improvements. - Control frame Integrity Protocol (CIP) and protected TWT support. - Improvements for AP regulatory checks. Netfilter: - Disable br_netfilter in user namespaces and schedule it for removal in 2027. - Restrict the raw table in nft_compat to the specific family, hook and priority. - Prepare the flowtable for mixed IPv4-over-IPv6 tunneling (and vice versa). - Use GFP_KERNEL_ACCOUNT for most netfilter allocations, for better integration with memcg. - Allow discovering conntrack zone filtering support in ctnetlink dumps, and fix nfnetlink_hook handling of large dumps. Deletions: - Move the LLC type 2 sockets out of tree. There are no known users and the code attracts a never-ending stream of LLM-driven fixes. Device drivers: - New drivers: - Meta Platforms NIC (mpnic), the second-generation NIC from Meta - initial skeleton of the Nebula-matrix NICs driver (nbl) - X-Powers AC200/AC300 Fast Ethernet PHYs - Ethernet high-speed NICs: - Intel: - i40e: XDP metadata support for RX hash and VLAN tag - ice: expose the E825 TSPLL state via the DPLL subsystem - nVidia/Mellanox: - move the data direct infrastructure from IB to core, in preparation for netdev support. - reduce the memory used per offloaded TC rule - Meta (fbnic): - support large io_uring zero-copy RX buffers - Microsoft vNIC (mana): - add BQL support - Ethernet NICs consumer, and embedded: - Airoha: - LRO offload support - Cadence (macb): - context swapping for ringparam and MTU changes, avoiding buffer loss under memory pressure - Mucse (rnpgbe): - TX/RX and link status support - NXP (enetc): - SR-IOV improvements and VF support for ENETC v4 - Qualcomm (ppe): - IPQ5424 support - Realtek PCI (r8169): - phylink support - RSS support for RTL8127 - Renesas (ravb): - gPTP support for R-Car Gen4 - Synopsys (stmmac): - XDP multi-buffer support on the TX side - tc mqprio hw queue priority offload - support for RK3568 SGMII, Allwinner H616 EMAC1, UltraRISC DP1000 and Qualcomm sa8255p - Ethernet switches: - Mediatek (mt7530): - fast age and change conduit support - Microchip (ksz): - support for KSZ8995XA/KS8995XA - Microchip (lan966x): - PCIe FDMA support, ~20x throughput increase in PCIe endpoint - Microchip (sparx5): - Time Aware Shaper (taprio) offload - Motorcomm (yt921x): - LED and SerDes PCS support - Realtek (rtl8365mb): - EEE support - Ethernet PHYs: - Broadcom: - enable jumbo frames on BCM54xx - Realtek: - support for RTL8261CE_CG and RTL8261D_VM - MDIO support for RTL838x and RTL839x - WiFi drivers: - Broadcom (brcmfmac): - FT/OKC roaming offload - Intel (iwlwifi): - UHR sniffer support - support for newer UEFI/BIOS tables - MediaTek (mt76): - mt7925 NAN improvements - offchannel/scan improvements and MLO fixes - Morse Micro (mm81x): - monitor mode support - Qualcomm (ath12k): - IPQ5332 platform infrastructure - Realtek (rtw88): - RTL8723BS support - SDIO TX performance improvements - Realtek (rtw89): - P2P_DEVICE support - BT-coex updates for RTL8922D - Bluetooth: - move H:4 reassembly into the Bluetooth core - add generic support for vendor packets - MGMT: add security level changed event - Bluetooth drivers: - btusb: support Bulk Serialization Mode - Intel: support Draco and Titan Lake, allow unsigned firmware - Mediatek: MT7928 support - Qualcomm: QCC2072 support - Broadcom: Synaptics 4384 support" * tag 'net-next-7.4' of git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next: (2126 commits) Bluetooth: btusb: disable by default Qualcomm multi-subsystem tipc: fix broadcast sender hang on last acker departure tipc: fix use-after-free of broadcast reception link at node release r8169: clear TX LPI on RTL8168h at link up after phylink conversion tun: Require CAP_NET_ADMIN to attach to an ownerless device net/sched/sch_cake: drop dead nla_nest_end() error check net/sched/sch_cake: widen buffer_used to u64 net/sched/sch_cake: reduce parent backlog when clearing a tin net/sched/sch_cake: serialize reconfiguration with the datapath tg3: Add Datto SIRIS 5 1U Server to restart AER quirk table net/mlx5e: Stop asserting for RTNL when closing channels net: shrink the payload-only part of skb dataref to one bit net: wwan: iosm: validate msg_head before indexing msg_ring net: bgmac: rate limit RX error messages selftests: net: veth: test XDP_TX to a peer with GRO enabled veth: schedule the peer's NAPI after XDP_TX when only GRO is enabled pppoe: delete session hash entries by pointer match net: lan969x: don't disable hardirqs in fdma tx clear net: sparx5/lan969x: do not clobber the PSFP cycle timer update period net: lan969x: fix DSM calendar for empty taxis ...
Diffstat (limited to 'drivers/net/phy')
-rw-r--r--drivers/net/phy/Kconfig14
-rw-r--r--drivers/net/phy/Makefile3
-rw-r--r--drivers/net/phy/air_en8811h.c84
-rw-r--r--drivers/net/phy/broadcom.c7
-rw-r--r--drivers/net/phy/dp83848.c3
-rw-r--r--drivers/net/phy/dp83867.c111
-rw-r--r--drivers/net/phy/dp83td510.c72
-rw-r--r--drivers/net/phy/marvell.c102
-rw-r--r--drivers/net/phy/mediatek/mtk-phy-lib.c26
-rw-r--r--drivers/net/phy/micrel.c2
-rw-r--r--drivers/net/phy/microchip_t1.c18
-rw-r--r--drivers/net/phy/motorcomm.c228
-rw-r--r--drivers/net/phy/mxl-gpy.c2
-rw-r--r--drivers/net/phy/nxp-c45-tja11xx.c2
-rw-r--r--drivers/net/phy/phy-caps.h2
-rw-r--r--drivers/net/phy/phy_caps.c26
-rw-r--r--drivers/net/phy/phy_device.c565
-rw-r--r--drivers/net/phy/phy_fixup.c98
-rw-r--r--drivers/net/phy/phy_led_triggers.c11
-rw-r--r--drivers/net/phy/phy_link_topology.c101
-rw-r--r--drivers/net/phy/phylib-internal.h1
-rw-r--r--drivers/net/phy/phylink.c313
-rw-r--r--drivers/net/phy/qcom/at803x.c98
-rw-r--r--drivers/net/phy/qcom/qca808x.c15
-rw-r--r--drivers/net/phy/qcom/qca83xx.c58
-rw-r--r--drivers/net/phy/realtek/realtek_main.c213
-rw-r--r--drivers/net/phy/sfp.c23
-rw-r--r--drivers/net/phy/xpowers/Makefile3
-rw-r--r--drivers/net/phy/xpowers/ac200.c314
-rw-r--r--drivers/net/phy/xpowers/ac300.c387
-rw-r--r--drivers/net/phy/xpowers/acx00.c633
-rw-r--r--drivers/net/phy/xpowers/acx00.h28
32 files changed, 3091 insertions, 472 deletions
diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
index b4ef927fd4a6..30e9d22c8d1a 100644
--- a/drivers/net/phy/Kconfig
+++ b/drivers/net/phy/Kconfig
@@ -363,7 +363,8 @@ config MOTORCOMM_PHY
tristate "Motorcomm PHYs"
help
Enables support for Motorcomm network PHYs.
- Currently supports YT85xx Gigabit Ethernet PHYs.
+ Currently supports YT85xx Gigabit Ethernet PHYs, YT861x PHYs
+ integrated in YT921x switches, and YT8821 2.5Gbps PHYs.
config NATIONAL_PHY
tristate "National Semiconductor PHYs"
@@ -484,6 +485,17 @@ config VITESSE_PHY
help
Currently supports the vsc8244
+config XPOWERS_ACX00_PHY
+ tristate "X-Powers AC200/AC300 Ethernet PHY"
+ depends on COMMON_CLK && NVMEM && OF_MDIO && REGULATOR
+ depends on I2C || !I2C
+ select PHY_PACKAGE
+ help
+ Enable the Fast Ethernet PHY driver shared by the X-Powers AC200
+ and AC300 companion ICs. The driver configures their common MDIO PHY
+ registers and the package-specific AC200 I2C or AC300 MDIO control
+ registers.
+
config XILINX_GMII2RGMII
tristate "Xilinx GMII2RGMII converter driver"
help
diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
index 25c4a3c2429f..579066a638f0 100644
--- a/drivers/net/phy/Makefile
+++ b/drivers/net/phy/Makefile
@@ -8,7 +8,7 @@ libphy-y := phy.o phy-c45.o phy-core.o phy_device.o \
ifdef CONFIG_PHYLIB
# built-in whenever PHYLIB is built-in or module
-obj-y += stubs.o
+obj-y += stubs.o phy_fixup.o
endif
libphy-$(CONFIG_SWPHY) += swphy.o
@@ -101,4 +101,5 @@ obj-$(CONFIG_SMSC_PHY) += smsc.o
obj-$(CONFIG_STE10XP) += ste10Xp.o
obj-$(CONFIG_TERANETICS_PHY) += teranetics.o
obj-$(CONFIG_VITESSE_PHY) += vitesse.o
+obj-$(CONFIG_XPOWERS_ACX00_PHY) += xpowers/
obj-$(CONFIG_XILINX_GMII2RGMII) += xilinx_gmii2rgmii.o
diff --git a/drivers/net/phy/air_en8811h.c b/drivers/net/phy/air_en8811h.c
index 0eeb7b9a4e26..a03d63dd4ac9 100644
--- a/drivers/net/phy/air_en8811h.c
+++ b/drivers/net/phy/air_en8811h.c
@@ -145,10 +145,22 @@
#define AN8811HB_GPIO_OUTPUT 0x5cf8b8
#define AN8811HB_GPIO_OUTPUT_345 (BIT(3) | BIT(4) | BIT(5))
+#define AN8811HB_GPIO_OUTPUT_0115 (BIT(0) | BIT(1) | BIT(15))
+
+#define AN8811HB_GPIO_SEL1 0x5cf8bc
+#define AN8811HB_GPIO_SEL1_0_MASK GENMASK(3, 0)
+#define AN8811HB_GPIO_SEL1_1_MASK GENMASK(7, 4)
+#define AN8811HB_GPIO_SEL1_0 BIT(0)
+#define AN8811HB_GPIO_SEL1_1 0
+
+#define AN8811HB_GPIO_SEL2 0x5cf8c0
+#define AN8811HB_GPIO_SEL2_15_MASK GENMASK(31, 28)
+#define AN8811HB_GPIO_SEL2_15 BIT(29)
#define AN8811HB_HWTRAP1 0x5cf910
#define AN8811HB_HWTRAP2 0x5cf914
#define AN8811HB_HWTRAP2_CKO BIT(28)
+#define AN8811HB_HWTRAP2_PKG GENMASK(14, 12)
#define AN8811HB_CLK_DRV 0x5cf9e4
#define AN8811HB_CLK_DRV_CKO_MASK GENMASK(14, 12)
@@ -202,6 +214,7 @@ struct en8811h_priv {
struct phy_device *phydev;
unsigned int cko_is_enabled;
struct mdio_device *pbusdev;
+ bool is_an8811hbcn;
};
enum {
@@ -312,6 +325,12 @@ static int air_write_buf(struct phy_device *phydev, u32 address,
int saved_page;
int ret = 0;
+ if (fw->size % 4) {
+ phydev_err(phydev, "firmware size %zu is not a multiple of 4\n",
+ fw->size);
+ return -EINVAL;
+ }
+
saved_page = phy_select_page(phydev, AIR_PHY_PAGE_EXTENDED_4);
if (saved_page >= 0) {
@@ -932,7 +951,7 @@ static const struct clk_ops an8811hb_clk_ops = {
static int an8811hb_clk_provider_setup(struct device *dev, struct clk_hw *hw)
{
- struct clk_init_data init;
+ struct clk_init_data init = {};
int ret;
if (!IS_ENABLED(CONFIG_COMMON_CLK))
@@ -1031,7 +1050,7 @@ static const struct clk_ops en8811h_clk_ops = {
static int en8811h_clk_provider_setup(struct device *dev, struct clk_hw *hw)
{
- struct clk_init_data init;
+ struct clk_init_data init = {};
int ret;
if (!IS_ENABLED(CONFIG_COMMON_CLK))
@@ -1071,10 +1090,49 @@ static int en8811h_leds_setup(struct phy_device *phydev)
return ret;
}
+static int an8811hb_led_gpio_setup(struct phy_device *phydev)
+{
+ struct en8811h_priv *priv = phydev->priv;
+ int ret;
+
+ if (priv->is_an8811hbcn) {
+ /* AN8811HBCN: LED GPIOs are 0, 1, 15 */
+ ret = air_phy_buckpbus_reg_modify(phydev, AN8811HB_GPIO_OUTPUT,
+ AN8811HB_GPIO_OUTPUT_0115,
+ AN8811HB_GPIO_OUTPUT_0115);
+ if (ret < 0)
+ return ret;
+
+ ret = air_phy_buckpbus_reg_modify(phydev, AN8811HB_GPIO_SEL1,
+ AN8811HB_GPIO_SEL1_0_MASK |
+ AN8811HB_GPIO_SEL1_1_MASK,
+ AN8811HB_GPIO_SEL1_0 |
+ AN8811HB_GPIO_SEL1_1);
+ if (ret < 0)
+ return ret;
+
+ ret = air_phy_buckpbus_reg_modify(phydev, AN8811HB_GPIO_SEL2,
+ AN8811HB_GPIO_SEL2_15_MASK,
+ AN8811HB_GPIO_SEL2_15);
+ if (ret < 0)
+ return ret;
+ } else {
+ /* AN8811HBN: LED GPIOs are 3, 4, 5 */
+ ret = air_phy_buckpbus_reg_modify(phydev, AN8811HB_GPIO_OUTPUT,
+ AN8811HB_GPIO_OUTPUT_345,
+ AN8811HB_GPIO_OUTPUT_345);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
static int an8811hb_probe(struct phy_device *phydev)
{
struct mdio_device *mdiodev;
struct en8811h_priv *priv;
+ u32 reg_val;
int ret;
priv = devm_kzalloc(&phydev->mdio.dev, sizeof(struct en8811h_priv),
@@ -1115,6 +1173,12 @@ static int an8811hb_probe(struct phy_device *phydev)
/* MDIO_DEVS1/2 empty, so set mmds_present bits here */
phydev->c45_ids.mmds_present |= MDIO_DEVS_PMAPMD | MDIO_DEVS_AN;
+ /* Detect package variant */
+ ret = air_phy_buckpbus_reg_read(phydev, AN8811HB_HWTRAP2, &reg_val);
+ if (ret < 0)
+ goto err_dev_create;
+ priv->is_an8811hbcn = FIELD_GET(AN8811HB_HWTRAP2_PKG, reg_val);
+
ret = en8811h_leds_setup(phydev);
if (ret < 0)
goto err_dev_create;
@@ -1125,13 +1189,6 @@ static int an8811hb_probe(struct phy_device *phydev)
if (ret)
goto err_dev_create;
- /* Configure led gpio pins as output */
- ret = air_phy_buckpbus_reg_modify(phydev, AN8811HB_GPIO_OUTPUT,
- AN8811HB_GPIO_OUTPUT_345,
- AN8811HB_GPIO_OUTPUT_345);
- if (ret < 0)
- goto err_dev_create;
-
return 0;
err_dev_create:
@@ -1265,9 +1322,16 @@ static int an8811hb_config_init(struct phy_device *phydev)
ret = air_leds_init(phydev, EN8811H_LED_COUNT, AIR_PHY_LED_DUR,
AIR_LED_MODE_USER_DEFINE);
- if (ret < 0)
+ if (ret < 0) {
phydev_err(phydev, "Failed to initialize leds: %d\n", ret);
+ return ret;
+ }
+ ret = an8811hb_led_gpio_setup(phydev);
+ if (ret < 0) {
+ phydev_err(phydev, "Failed to set up LED GPIO: %d\n", ret);
+ return ret;
+ }
return ret;
}
diff --git a/drivers/net/phy/broadcom.c b/drivers/net/phy/broadcom.c
index d1a4edb34ad2..b515e3875504 100644
--- a/drivers/net/phy/broadcom.c
+++ b/drivers/net/phy/broadcom.c
@@ -537,6 +537,13 @@ static int bcm54xx_config_init(struct phy_device *phydev)
bcm54xx_ptp_config_init(phydev);
+ /* Transmit is limited to 4.5 KB without it, see the BCM54210PE
+ * datasheet section 5.4.12.14.
+ */
+ err = bcm_phy_enable_jumbo(phydev);
+ if (err < 0)
+ return err;
+
/* Acknowledge any left over interrupt and charge the device for
* wake-up.
*/
diff --git a/drivers/net/phy/dp83848.c b/drivers/net/phy/dp83848.c
index d88b1999d596..e65651ac567a 100644
--- a/drivers/net/phy/dp83848.c
+++ b/drivers/net/phy/dp83848.c
@@ -117,6 +117,9 @@ static int dp83848_config_init(struct phy_device *phydev)
* we check initial value of BMCR Auto negotiation enable bit
*/
val = phy_read(phydev, MII_BMCR);
+ if (val < 0)
+ return val;
+
if (!(val & BMCR_ANENABLE))
phydev->autoneg = AUTONEG_DISABLE;
diff --git a/drivers/net/phy/dp83867.c b/drivers/net/phy/dp83867.c
index 61a941aa02d9..a3cd5cb11ea0 100644
--- a/drivers/net/phy/dp83867.c
+++ b/drivers/net/phy/dp83867.c
@@ -8,6 +8,7 @@
#include <linux/kernel.h>
#include <linux/mii.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/of.h>
#include <linux/phy.h>
#include <linux/delay.h>
@@ -196,6 +197,16 @@ struct dp83867_private {
bool set_clk_output;
u32 clk_output_sel;
bool sgmii_ref_clk_en;
+
+ /* Shadow of the LED registers, replayed after a soft reset. led_lock
+ * serializes the shadow and its replay against the LED callbacks,
+ * because dp83867_config_init() does the replay off phydev->lock.
+ */
+ struct mutex led_lock;
+ u16 ledcr1;
+ u16 ledcr1_mask;
+ u16 ledcr2;
+ u16 ledcr2_mask;
};
static int dp83867_ack_interrupt(struct phy_device *phydev)
@@ -722,6 +733,7 @@ static int dp83867_resume(struct phy_device *phydev)
static int dp83867_probe(struct phy_device *phydev)
{
struct dp83867_private *dp83867;
+ int ret;
dp83867 = devm_kzalloc(&phydev->mdio.dev, sizeof(*dp83867),
GFP_KERNEL);
@@ -730,9 +742,60 @@ static int dp83867_probe(struct phy_device *phydev)
phydev->priv = dp83867;
+ ret = devm_mutex_init(&phydev->mdio.dev, &dp83867->led_lock);
+ if (ret)
+ return ret;
+
return dp83867_of_init(phydev);
}
+/* Update an LED register and mirror the change into the shadow, so that
+ * dp83867_config_init() can replay it after a soft reset. Caller must hold
+ * dp83867->led_lock.
+ */
+static int __dp83867_led_modify(struct phy_device *phydev, u32 reg,
+ u16 mask, u16 val)
+{
+ struct dp83867_private *dp83867 = phydev->priv;
+ int ret;
+
+ ret = phy_modify(phydev, reg, mask, val);
+ if (ret)
+ return ret;
+
+ if (reg == DP83867_LEDCR1) {
+ dp83867->ledcr1 = (dp83867->ledcr1 & ~mask) | (val & mask);
+ dp83867->ledcr1_mask |= mask;
+ } else if (reg == DP83867_LEDCR2) {
+ dp83867->ledcr2 = (dp83867->ledcr2 & ~mask) | (val & mask);
+ dp83867->ledcr2_mask |= mask;
+ } else {
+ WARN_ON_ONCE(1);
+ }
+
+ return 0;
+}
+
+/* Restore the LED registers the driver has programmed, cleared by the soft
+ * reset in dp83867_phy_reset().
+ */
+static int dp83867_led_restore(struct phy_device *phydev)
+{
+ struct dp83867_private *dp83867 = phydev->priv;
+ int ret = 0;
+
+ mutex_lock(&dp83867->led_lock);
+ if (dp83867->ledcr1_mask)
+ ret = phy_modify(phydev, DP83867_LEDCR1,
+ dp83867->ledcr1_mask, dp83867->ledcr1);
+ if (!ret && dp83867->ledcr2_mask)
+ ret = phy_modify(phydev, DP83867_LEDCR2,
+ dp83867->ledcr2_mask, dp83867->ledcr2);
+ mutex_unlock(&dp83867->led_lock);
+
+ return ret;
+}
+
static int dp83867_config_init(struct phy_device *phydev)
{
struct dp83867_private *dp83867 = phydev->priv;
@@ -896,6 +959,10 @@ static int dp83867_config_init(struct phy_device *phydev)
mask, val);
}
+ ret = dp83867_led_restore(phydev);
+ if (ret)
+ phydev_warn(phydev, "failed to restore LED config: %d\n", ret);
+
return 0;
}
@@ -1004,7 +1071,9 @@ static int
dp83867_led_brightness_set(struct phy_device *phydev,
u8 index, enum led_brightness brightness)
{
- u32 val;
+ struct dp83867_private *dp83867 = phydev->priv;
+ u16 val;
+ int ret;
if (index >= DP83867_LED_COUNT)
return -EINVAL;
@@ -1015,10 +1084,13 @@ dp83867_led_brightness_set(struct phy_device *phydev,
if (brightness)
val |= DP83867_LED_DRV_VAL(index);
- return phy_modify(phydev, DP83867_LEDCR2,
- DP83867_LED_DRV_VAL(index) |
- DP83867_LED_DRV_EN(index),
- val);
+ mutex_lock(&dp83867->led_lock);
+ ret = __dp83867_led_modify(phydev, DP83867_LEDCR2,
+ DP83867_LED_DRV_VAL(index) |
+ DP83867_LED_DRV_EN(index), val);
+ mutex_unlock(&dp83867->led_lock);
+
+ return ret;
}
static int dp83867_led_mode(u8 index, unsigned long rules)
@@ -1069,18 +1141,24 @@ static int dp83867_led_hw_is_supported(struct phy_device *phydev, u8 index,
static int dp83867_led_hw_control_set(struct phy_device *phydev, u8 index,
unsigned long rules)
{
+ struct dp83867_private *dp83867 = phydev->priv;
int mode, ret;
mode = dp83867_led_mode(index, rules);
if (mode < 0)
return mode;
- ret = phy_modify(phydev, DP83867_LEDCR1, DP83867_LED_FN_MASK(index),
- DP83867_LED_FN(index, mode));
- if (ret)
- return ret;
-
- return phy_modify(phydev, DP83867_LEDCR2, DP83867_LED_DRV_EN(index), 0);
+ mutex_lock(&dp83867->led_lock);
+ ret = __dp83867_led_modify(phydev, DP83867_LEDCR1,
+ DP83867_LED_FN_MASK(index),
+ DP83867_LED_FN(index, mode));
+ if (!ret)
+ ret = __dp83867_led_modify(phydev, DP83867_LEDCR2,
+ DP83867_LED_DRV_EN(index) |
+ DP83867_LED_DRV_VAL(index), 0);
+ mutex_unlock(&dp83867->led_lock);
+
+ return ret;
}
static int dp83867_led_hw_control_get(struct phy_device *phydev, u8 index,
@@ -1141,9 +1219,11 @@ static int dp83867_led_hw_control_get(struct phy_device *phydev, u8 index,
static int dp83867_led_polarity_set(struct phy_device *phydev, int index,
unsigned long modes)
{
+ struct dp83867_private *dp83867 = phydev->priv;
/* Default active high */
u16 polarity = DP83867_LED_POLARITY(index);
u32 mode;
+ int ret;
for_each_set_bit(mode, &modes, __PHY_LED_MODES_NUM) {
switch (mode) {
@@ -1157,8 +1237,13 @@ static int dp83867_led_polarity_set(struct phy_device *phydev, int index,
return -EINVAL;
}
}
- return phy_modify(phydev, DP83867_LEDCR2,
- DP83867_LED_POLARITY(index), polarity);
+
+ mutex_lock(&dp83867->led_lock);
+ ret = __dp83867_led_modify(phydev, DP83867_LEDCR2,
+ DP83867_LED_POLARITY(index), polarity);
+ mutex_unlock(&dp83867->led_lock);
+
+ return ret;
}
static unsigned int dp83867_inband_caps(struct phy_device *phydev,
diff --git a/drivers/net/phy/dp83td510.c b/drivers/net/phy/dp83td510.c
index 9e9a41bf6457..31c139bef076 100644
--- a/drivers/net/phy/dp83td510.c
+++ b/drivers/net/phy/dp83td510.c
@@ -30,6 +30,12 @@
#define DP83TD510E_INT1_LINK BIT(13)
#define DP83TD510E_INT1_LINK_EN BIT(5)
+#define DP83TD510E_RCSR 0x17
+#define DP83TD510E_RMII_MODE_EN BIT(5)
+#define DP83TD510E_RGMII_MODE_EN BIT(9)
+#define DP83TD510E_TX_CLK_SHIFT BIT(11)
+#define DP83TD510E_RX_CLK_SHIFT BIT(12)
+
#define DP83TD510E_CTRL 0x1f
#define DP83TD510E_CTRL_HW_RESET BIT(15)
#define DP83TD510E_CTRL_SW_RESET BIT(14)
@@ -649,6 +655,71 @@ static int dp83td510_config_aneg(struct phy_device *phydev)
return genphy_c45_check_and_restart_aneg(phydev, changed);
}
+static bool dp83td510_config_rgmii_rx_delay(struct phy_device *phydev)
+{
+ return phydev->interface == PHY_INTERFACE_MODE_RGMII_ID ||
+ phydev->interface == PHY_INTERFACE_MODE_RGMII_RXID;
+}
+
+static bool dp83td510_config_rgmii_tx_delay(struct phy_device *phydev)
+{
+ return phydev->interface == PHY_INTERFACE_MODE_RGMII_ID ||
+ phydev->interface == PHY_INTERFACE_MODE_RGMII_TXID;
+}
+
+static int dp83td510_config_init(struct phy_device *phydev)
+{
+ int rgmii_delay = 0;
+ bool rx_int_delay;
+ bool tx_int_delay;
+ int ret;
+
+ if (phy_interface_is_rgmii(phydev)) {
+ rx_int_delay = dp83td510_config_rgmii_rx_delay(phydev);
+ if (rx_int_delay)
+ rgmii_delay |= DP83TD510E_RX_CLK_SHIFT;
+
+ tx_int_delay = dp83td510_config_rgmii_tx_delay(phydev);
+
+ if (tx_int_delay)
+ rgmii_delay |= DP83TD510E_TX_CLK_SHIFT;
+
+ ret = phy_modify_mmd(phydev, MDIO_MMD_VEND2, DP83TD510E_RCSR,
+ DP83TD510E_RX_CLK_SHIFT |
+ DP83TD510E_TX_CLK_SHIFT,
+ rgmii_delay);
+ if (ret)
+ return ret;
+
+ ret = phy_set_bits_mmd(phydev, MDIO_MMD_VEND2,
+ DP83TD510E_RCSR, DP83TD510E_RGMII_MODE_EN);
+
+ if (ret)
+ return ret;
+
+ } else if (phydev->interface == PHY_INTERFACE_MODE_RMII) {
+ /* set RMII_MODE_EN, clear RGMII_MODE_EN. We need to clear RGMII
+ * here because otherwise, RMII/MII configuration would be
+ * ignored as RGMII would be selected.
+ */
+ ret = phy_modify_mmd(phydev, MDIO_MMD_VEND2, DP83TD510E_RCSR,
+ DP83TD510E_RMII_MODE_EN |
+ DP83TD510E_RGMII_MODE_EN,
+ DP83TD510E_RMII_MODE_EN);
+ if (ret)
+ return ret;
+ } else {
+ /* may be MII, which is supported, or something else. Just
+ * return success to keep the old behavior and not break
+ * anything. Configuration may have been done by straps so
+ * it's better to not touch it.
+ */
+ ret = 0;
+ }
+
+ return ret;
+}
+
static int dp83td510_get_sqi(struct phy_device *phydev)
{
int sqi, ret;
@@ -942,6 +1013,7 @@ static struct phy_driver dp83td510_driver[] = {
.name = "TI DP83TD510E",
.flags = PHY_POLL_CABLE_TEST,
+ .config_init = dp83td510_config_init,
.probe = dp83td510_probe,
.config_aneg = dp83td510_config_aneg,
.read_status = dp83td510_read_status,
diff --git a/drivers/net/phy/marvell.c b/drivers/net/phy/marvell.c
index f71cffa88406..2c688d661297 100644
--- a/drivers/net/phy/marvell.c
+++ b/drivers/net/phy/marvell.c
@@ -55,6 +55,8 @@
#define MII_M1011_IMASK 0x12
#define MII_M1011_IMASK_INIT 0x6400
#define MII_M1011_IMASK_CLEAR 0x0000
+/* WOL Event Interrupt Enable, 88E1318S and 88E1510 only */
+#define MII_M1011_IMASK_WOL BIT(7)
#define MII_M1011_PHY_SCR 0x10
#define MII_M1011_PHY_SCR_DOWNSHIFT_EN BIT(11)
@@ -139,11 +141,6 @@
#define MII_88E1318S_PHY_MSCR1_REG 16
#define MII_88E1318S_PHY_MSCR1_PAD_ODD BIT(6)
-/* Copper Specific Interrupt Enable Register */
-#define MII_88E1318S_PHY_CSIER 0x12
-/* WOL Event Interrupt Enable */
-#define MII_88E1318S_PHY_CSIER_WOL_EIE BIT(7)
-
#define MII_88E1318S_PHY_LED_FUNC 0x10
#define MII_88E1318S_PHY_LED_FUNC_OFF (0x8)
#define MII_88E1318S_PHY_LED_FUNC_ON (0x9)
@@ -354,6 +351,7 @@ struct marvell_priv {
u32 step;
s8 pair;
u8 vct_phase;
+ bool wol_enabled;
};
static int marvell_read_page(struct phy_device *phydev)
@@ -407,7 +405,8 @@ static int marvell_config_intr(struct phy_device *phydev)
return err;
}
-static irqreturn_t marvell_handle_interrupt(struct phy_device *phydev)
+static irqreturn_t __marvell_handle_interrupt(struct phy_device *phydev,
+ u16 mask)
{
int irq_status;
@@ -417,7 +416,7 @@ static irqreturn_t marvell_handle_interrupt(struct phy_device *phydev)
return IRQ_NONE;
}
- if (!(irq_status & MII_M1011_IMASK_INIT))
+ if (!(irq_status & mask))
return IRQ_NONE;
phy_trigger_machine(phydev);
@@ -425,6 +424,57 @@ static irqreturn_t marvell_handle_interrupt(struct phy_device *phydev)
return IRQ_HANDLED;
}
+static irqreturn_t marvell_handle_interrupt(struct phy_device *phydev)
+{
+ return __marvell_handle_interrupt(phydev, MII_M1011_IMASK_INIT);
+}
+
+/* WOL_EIE also routes WoL events to INTn. Keep it set while WoL is enabled,
+ * unless INTn is the PHY interrupt and that interrupt is being disabled:
+ * nothing would read IEVENT after free_irq() to release the line.
+ */
+static u16 m88e1318_wol_imask(struct phy_device *phydev)
+{
+ struct marvell_priv *priv = phydev->priv;
+
+ if (priv->wol_enabled &&
+ (phydev->interrupts == PHY_INTERRUPT_ENABLED ||
+ !phy_interrupt_is_valid(phydev)))
+ return MII_M1011_IMASK_WOL;
+
+ return 0;
+}
+
+static int m88e1318_config_intr(struct phy_device *phydev)
+{
+ bool enable = phydev->interrupts == PHY_INTERRUPT_ENABLED;
+ u16 imask;
+ int err;
+
+ if (enable) {
+ err = marvell_ack_interrupt(phydev);
+ if (err)
+ return err;
+ }
+
+ /* Serialise against m88e1318_set_wol() updating wol_enabled */
+ phy_lock_mdio_bus(phydev);
+ imask = enable ? MII_M1011_IMASK_INIT : MII_M1011_IMASK_CLEAR;
+ imask |= m88e1318_wol_imask(phydev);
+ err = __phy_write(phydev, MII_M1011_IMASK, imask);
+ phy_unlock_mdio_bus(phydev);
+ if (err || enable)
+ return err;
+
+ return marvell_ack_interrupt(phydev);
+}
+
+static irqreturn_t m88e1318_handle_interrupt(struct phy_device *phydev)
+{
+ return __marvell_handle_interrupt(phydev, MII_M1011_IMASK_INIT |
+ MII_M1011_IMASK_WOL);
+}
+
static int marvell_set_polarity(struct phy_device *phydev, int polarity)
{
u16 val;
@@ -1969,18 +2019,27 @@ static void m88e1318_get_wol(struct phy_device *phydev,
static int m88e1318_set_wol(struct phy_device *phydev,
struct ethtool_wolinfo *wol)
{
+ struct marvell_priv *priv = phydev->priv;
int err = 0, oldpage;
oldpage = phy_save_page(phydev);
if (oldpage < 0)
goto error;
- if (wol->wolopts & (WAKE_MAGIC | WAKE_PHY)) {
- /* Explicitly switch to page 0x00, just to be sure */
- err = marvell_write_page(phydev, MII_MARVELL_COPPER_PAGE);
- if (err < 0)
- goto error;
+ priv->wol_enabled = !!(wol->wolopts & (WAKE_MAGIC | WAKE_PHY));
+
+ /* Explicitly switch to page 0x00, just to be sure */
+ err = marvell_write_page(phydev, MII_MARVELL_COPPER_PAGE);
+ if (err < 0)
+ goto error;
+
+ /* Update the WOL interrupt enable */
+ err = __phy_modify(phydev, MII_M1011_IMASK, MII_M1011_IMASK_WOL,
+ m88e1318_wol_imask(phydev));
+ if (err < 0)
+ goto error;
+ if (priv->wol_enabled) {
/* If WOL event happened once, the LED[2] interrupt pin
* will not be cleared unless we reading the interrupt status
* register. If interrupts are in use, the normal interrupt
@@ -1990,12 +2049,6 @@ static int m88e1318_set_wol(struct phy_device *phydev,
if (!phy_interrupt_is_valid(phydev))
__phy_read(phydev, MII_M1011_IEVENT);
- /* Enable the WOL interrupt */
- err = __phy_set_bits(phydev, MII_88E1318S_PHY_CSIER,
- MII_88E1318S_PHY_CSIER_WOL_EIE);
- if (err < 0)
- goto error;
-
err = marvell_write_page(phydev, MII_MARVELL_LED_PAGE);
if (err < 0)
goto error;
@@ -3817,8 +3870,8 @@ static struct phy_driver marvell_drivers[] = {
.config_init = m88e1318_config_init,
.config_aneg = m88e1318_config_aneg,
.read_status = marvell_read_status,
- .config_intr = marvell_config_intr,
- .handle_interrupt = marvell_handle_interrupt,
+ .config_intr = m88e1318_config_intr,
+ .handle_interrupt = m88e1318_handle_interrupt,
.get_wol = m88e1318_get_wol,
.set_wol = m88e1318_set_wol,
.resume = genphy_resume,
@@ -3913,6 +3966,11 @@ static struct phy_driver marvell_drivers[] = {
.get_stats = marvell_get_stats,
.get_tunable = m88e1011_get_tunable,
.set_tunable = m88e1011_set_tunable,
+ .led_brightness_set = m88e1318_led_brightness_set,
+ .led_blink_set = m88e1318_led_blink_set,
+ .led_hw_is_supported = m88e1318_led_hw_is_supported,
+ .led_hw_control_set = m88e1318_led_hw_control_set,
+ .led_hw_control_get = m88e1318_led_hw_control_get,
},
{
.phy_id = MARVELL_PHY_ID_88E1510,
@@ -3925,8 +3983,8 @@ static struct phy_driver marvell_drivers[] = {
.config_init = m88e1510_config_init,
.config_aneg = m88e1510_config_aneg,
.read_status = marvell_read_status,
- .config_intr = marvell_config_intr,
- .handle_interrupt = marvell_handle_interrupt,
+ .config_intr = m88e1318_config_intr,
+ .handle_interrupt = m88e1318_handle_interrupt,
.get_wol = m88e1318_get_wol,
.set_wol = m88e1318_set_wol,
.resume = m88e1510_resume,
diff --git a/drivers/net/phy/mediatek/mtk-phy-lib.c b/drivers/net/phy/mediatek/mtk-phy-lib.c
index 608072fbfde9..4c4eca870bde 100644
--- a/drivers/net/phy/mediatek/mtk-phy-lib.c
+++ b/drivers/net/phy/mediatek/mtk-phy-lib.c
@@ -136,22 +136,15 @@ int mtk_phy_led_hw_ctrl_get(struct phy_device *phydev, u8 index,
if (blink < 0)
return -EIO;
- if ((on & (on_set | MTK_PHY_LED_ON_FDX |
- MTK_PHY_LED_ON_HDX | MTK_PHY_LED_ON_LINKDOWN)) ||
- (blink & (rx_blink_set | tx_blink_set)))
- set_bit(bit_netdev, &priv->led_state);
- else
- clear_bit(bit_netdev, &priv->led_state);
+ assign_bit(bit_netdev, &priv->led_state,
+ (on & (on_set | MTK_PHY_LED_ON_FDX | MTK_PHY_LED_ON_HDX |
+ MTK_PHY_LED_ON_LINKDOWN)) ||
+ (blink & (rx_blink_set | tx_blink_set)));
- if (on & MTK_PHY_LED_ON_FORCE_ON)
- set_bit(bit_on, &priv->led_state);
- else
- clear_bit(bit_on, &priv->led_state);
+ assign_bit(bit_on, &priv->led_state, on & MTK_PHY_LED_ON_FORCE_ON);
- if (blink & MTK_PHY_LED_BLINK_FORCE_BLINK)
- set_bit(bit_blink, &priv->led_state);
- else
- clear_bit(bit_blink, &priv->led_state);
+ assign_bit(bit_blink, &priv->led_state,
+ blink & MTK_PHY_LED_BLINK_FORCE_BLINK);
if (!rules)
return 0;
@@ -254,10 +247,7 @@ int mtk_phy_led_hw_ctrl_set(struct phy_device *phydev, u8 index,
}
}
- if (blink || on)
- set_bit(bit_netdev, &priv->led_state);
- else
- clear_bit(bit_netdev, &priv->led_state);
+ assign_bit(bit_netdev, &priv->led_state, blink || on);
ret = phy_modify_mmd(phydev, MDIO_MMD_VEND2, index ?
MTK_PHY_LED1_ON_CTRL : MTK_PHY_LED0_ON_CTRL,
diff --git a/drivers/net/phy/micrel.c b/drivers/net/phy/micrel.c
index 5c8461db7b4b..d0b1ad21667b 100644
--- a/drivers/net/phy/micrel.c
+++ b/drivers/net/phy/micrel.c
@@ -2791,7 +2791,7 @@ static int kszphy_probe(struct phy_device *phydev)
if (type)
priv->rmii_ref_clk_sel = type->has_rmii_ref_clk_sel;
- rmii_ref_clk_sel_25_mhz = of_property_read_bool(np,
+ rmii_ref_clk_sel_25_mhz = of_property_present(np,
"micrel,rmii-reference-clock-select-25-mhz");
if (rate > 24500000 && rate < 25500000) {
diff --git a/drivers/net/phy/microchip_t1.c b/drivers/net/phy/microchip_t1.c
index 3292b2235c8f..e38d20bf6aa4 100644
--- a/drivers/net/phy/microchip_t1.c
+++ b/drivers/net/phy/microchip_t1.c
@@ -1285,14 +1285,16 @@ static int lan887x_phy_init(struct phy_device *phydev)
if (IS_ERR(priv->clock))
return PTR_ERR(priv->clock);
- /* Enable pin mux for EVT */
- phy_modify_mmd(phydev, MDIO_MMD_VEND1,
- LAN887X_MX_CHIP_TOP_REG_CONTROL1,
- LAN887X_MX_CHIP_TOP_REG_CONTROL1_EVT_EN,
- LAN887X_MX_CHIP_TOP_REG_CONTROL1_EVT_EN);
-
- /* Initialize pin numbers specific to PEROUT */
- priv->clock->event_pin = 3;
+ if (priv->clock) {
+ /* Enable pin mux for EVT */
+ phy_modify_mmd(phydev, MDIO_MMD_VEND1,
+ LAN887X_MX_CHIP_TOP_REG_CONTROL1,
+ LAN887X_MX_CHIP_TOP_REG_CONTROL1_EVT_EN,
+ LAN887X_MX_CHIP_TOP_REG_CONTROL1_EVT_EN);
+
+ /* Initialize pin numbers specific to PEROUT */
+ priv->clock->event_pin = 3;
+ }
priv->init_done = true;
}
diff --git a/drivers/net/phy/motorcomm.c b/drivers/net/phy/motorcomm.c
index c5a2cda8d31b..463e1ea3efc9 100644
--- a/drivers/net/phy/motorcomm.c
+++ b/drivers/net/phy/motorcomm.c
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0+
/*
- * Motorcomm 8511/8521/8522/8531/8531S/8821 PHY driver.
+ * Motorcomm 8511/8521/8522/8531/8531S/861x/8821 PHY driver.
*
* Author: Peter Geis <pgwipeout@gmail.com>
* Author: Frank <Frank.Sae@motor-comm.com>
@@ -8,6 +8,7 @@
#include <linux/clk.h>
#include <linux/etherdevice.h>
+#include <linux/ethtool_netlink.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/phy.h>
@@ -18,6 +19,8 @@
#define PHY_ID_YT8522 0x4f51e928
#define PHY_ID_YT8531 0x4f51e91b
#define PHY_ID_YT8531S 0x4f51e91a
+/* Live captured from YT921x switch internal MDIO bus */
+#define PHY_ID_INT861X 0x01e04281
#define PHY_ID_YT8821 0x4f51ea19
/* YT8521/YT8531S/YT8821 Register Overview
* UTP Register space | FIBER Register space
@@ -144,6 +147,11 @@
#define YT8521_CLOCK_GATING_REG 0xC
#define YT8521_CGR_RX_CLK_EN BIT(12)
+/* Analog front-end control register 3 */
+#define YT8531S_EXT_AFE_CTRL3 0x12
+/* Analog front-end DAC clock enable */
+#define YT8531S_AFE_CTRL3_CLKDAC_AON BIT(13)
+
#define YT8521_EXTREG_SLEEP_CONTROL1_REG 0x27
#define YT8521_ESC1R_SLEEP_SW BIT(15)
#define YT8521_ESC1R_PLLON_SLP BIT(14)
@@ -293,6 +301,19 @@
#define YT8531_SCR_CLK_SRC_REF_25M 4
#define YT8531_SCR_CLK_SRC_SSC_25M 5
+/* TDR (cable diagnostic) test control */
+#define YT861X_TDR_CTRL_REG 0x80
+#define YT861X_TDR_CTRL_START BIT(0)
+
+#define YT861X_TDR_STATUS_REG 0x84
+#define YT861X_TDR_STATUS_BUSY BIT(15)
+#define YT861X_TDR_STATUS_PAIR_OK 0
+#define YT861X_TDR_STATUS_PAIR_UNKNOWN 1
+#define YT861X_TDR_STATUS_PAIR_SHORT 2
+#define YT861X_TDR_STATUS_PAIR_OPEN 3
+
+#define YT861X_TDR_PAIR_LENGTH_REG(n) (0x87 + (n)) /* in cm */
+
#define YT8821_SDS_EXT_CSR_CTRL_REG 0x23
#define YT8821_SDS_EXT_CSR_VCO_LDO_EN BIT(15)
#define YT8821_SDS_EXT_CSR_VCO_BIAS_LPF_EN BIT(8)
@@ -1680,27 +1701,16 @@ static int yt8521_resume(struct phy_device *phydev)
return yt8521_modify_utp_fiber_bmcr(phydev, BMCR_PDOWN, 0);
}
-/**
- * yt8521_config_init() - called to initialize the PHY
- * @phydev: a pointer to a &struct phy_device
- *
- * returns 0 or negative errno code
- */
-static int yt8521_config_init(struct phy_device *phydev)
+static int __yt8521_config_init(struct phy_device *phydev)
{
struct device *dev = &phydev->mdio.dev;
- int old_page;
int ret = 0;
- old_page = phy_select_page(phydev, YT8521_RSSR_UTP_SPACE);
- if (old_page < 0)
- goto err_restore_page;
-
/* set rgmii delay mode */
if (phydev->interface != PHY_INTERFACE_MODE_SGMII) {
ret = ytphy_rgmii_clk_delay_config(phydev);
if (ret < 0)
- goto err_restore_page;
+ return ret;
}
if (device_property_read_bool(dev, "motorcomm,auto-sleep-disabled")) {
@@ -1708,20 +1718,58 @@ static int yt8521_config_init(struct phy_device *phydev)
ret = ytphy_modify_ext(phydev, YT8521_EXTREG_SLEEP_CONTROL1_REG,
YT8521_ESC1R_SLEEP_SW, 0);
if (ret < 0)
- goto err_restore_page;
+ return ret;
}
- if (device_property_read_bool(dev, "motorcomm,keep-pll-enabled")) {
+ if (device_property_read_bool(dev, "motorcomm,keep-pll-enabled"))
/* enable RXC clock when no wire plug */
- ret = ytphy_modify_ext(phydev, YT8521_CLOCK_GATING_REG,
- YT8521_CGR_RX_CLK_EN, 0);
- if (ret < 0)
+ return ytphy_modify_ext(phydev, YT8521_CLOCK_GATING_REG,
+ YT8521_CGR_RX_CLK_EN, 0);
+
+ return 0;
+}
+
+/**
+ * yt8521_config_init() - called to initialize the PHY
+ * @phydev: a pointer to a &struct phy_device
+ *
+ * returns 0 or negative errno code
+ */
+static int yt8521_config_init(struct phy_device *phydev)
+{
+ int old_page, ret = 0;
+
+ old_page = phy_select_page(phydev, YT8521_RSSR_UTP_SPACE);
+ if (old_page < 0)
+ goto err_restore_page;
+
+ ret = __yt8521_config_init(phydev);
+
+err_restore_page:
+ return phy_restore_page(phydev, old_page, ret);
+}
+
+static int yt8531s_config_init(struct phy_device *phydev)
+{
+ int old_page, ret = 0;
+
+ old_page = phy_select_page(phydev, YT8521_RSSR_UTP_SPACE);
+ if (old_page < 0)
+ goto err_restore_page;
+
+ ret = __yt8521_config_init(phydev);
+ if (ret)
+ goto err_restore_page;
+
+ if (phy_interface_is_rgmii(phydev)) {
+ ret = yt8531_set_ds(phydev);
+ if (ret)
goto err_restore_page;
}
- if (phy_interface_is_rgmii(phydev) &&
- phydev_id_compare(phydev, PHY_ID_YT8531S))
- ret = yt8531_set_ds(phydev);
+ if (phydev->interface == PHY_INTERFACE_MODE_GMII)
+ ret = ytphy_modify_ext(phydev, YT8531S_EXT_AFE_CTRL3,
+ 0, YT8531S_AFE_CTRL3_CLKDAC_AON);
err_restore_page:
return phy_restore_page(phydev, old_page, ret);
@@ -2507,6 +2555,126 @@ static int yt8521_get_features(struct phy_device *phydev)
}
/**
+ * yt861x_config_init() - apply the auto sleep setting to the PHY
+ * @phydev: a pointer to a &struct phy_device
+ *
+ * Auto sleep is enabled by default and a BMCR soft reset does not restore it,
+ * so apply the setting from config_init. It can be disabled with the
+ * "motorcomm,auto-sleep-disabled" device property.
+ *
+ * Returns: 0 or negative errno code
+ */
+static int yt861x_config_init(struct phy_device *phydev)
+{
+ bool sleep;
+
+ sleep = !device_property_read_bool(&phydev->mdio.dev,
+ "motorcomm,auto-sleep-disabled");
+
+ return ytphy_modify_ext_with_lock(phydev,
+ YT8521_EXTREG_SLEEP_CONTROL1_REG,
+ YT8521_ESC1R_SLEEP_SW,
+ sleep ? YT8521_ESC1R_SLEEP_SW : 0);
+}
+
+/**
+ * yt861x_cable_test_start() - start a cable diagnostic (TDR) test
+ * @phydev: a pointer to a &struct phy_device
+ *
+ * Returns: 0 or negative errno code
+ */
+static int yt861x_cable_test_start(struct phy_device *phydev)
+{
+ int ret;
+
+ /* auto sleep would abort the TDR test */
+ ret = ytphy_modify_ext_with_lock(phydev,
+ YT8521_EXTREG_SLEEP_CONTROL1_REG,
+ YT8521_ESC1R_SLEEP_SW, 0);
+ if (ret)
+ return ret;
+
+ ret = ytphy_write_ext_with_lock(phydev, YT861X_TDR_CTRL_REG,
+ YT861X_TDR_CTRL_START);
+ if (ret) {
+ int err = yt861x_config_init(phydev);
+
+ if (err)
+ phydev_warn(phydev,
+ "Failed to restore auto sleep: %d\n",
+ err);
+ }
+
+ return ret;
+}
+
+/**
+ * yt861x_cable_test_get_status() - report cable diagnostic test results
+ * @phydev: a pointer to a &struct phy_device
+ * @finished: set to true when the test is complete
+ *
+ * Returns: 0 or negative errno code
+ */
+static int yt861x_cable_test_get_status(struct phy_device *phydev,
+ bool *finished)
+{
+ int status;
+ int ret;
+
+ *finished = false;
+
+ status = ytphy_read_ext_with_lock(phydev, YT861X_TDR_STATUS_REG);
+ if (status < 0)
+ return status;
+
+ if (status & YT861X_TDR_STATUS_BUSY)
+ return 0;
+
+ for (int pair = ETHTOOL_A_CABLE_PAIR_A; pair <= ETHTOOL_A_CABLE_PAIR_D;
+ pair++) {
+ u8 code;
+
+ switch ((status >> (2 * pair)) & 0x3) {
+ case YT861X_TDR_STATUS_PAIR_OK:
+ code = ETHTOOL_A_CABLE_RESULT_CODE_OK;
+ break;
+ case YT861X_TDR_STATUS_PAIR_SHORT:
+ code = ETHTOOL_A_CABLE_RESULT_CODE_SAME_SHORT;
+ break;
+ case YT861X_TDR_STATUS_PAIR_OPEN:
+ code = ETHTOOL_A_CABLE_RESULT_CODE_OPEN;
+ break;
+ default:
+ code = ETHTOOL_A_CABLE_RESULT_CODE_UNSPEC;
+ }
+
+ ethnl_cable_test_result(phydev, pair, code);
+
+ if (code != ETHTOOL_A_CABLE_RESULT_CODE_OK &&
+ code != ETHTOOL_A_CABLE_RESULT_CODE_UNSPEC) {
+ ret = ytphy_read_ext_with_lock(phydev,
+ YT861X_TDR_PAIR_LENGTH_REG(pair));
+ if (ret >= 0)
+ ethnl_cable_test_fault_length(phydev, pair,
+ ret);
+ }
+ }
+
+ /* the reset below does not restore auto sleep by itself */
+ ret = yt861x_config_init(phydev);
+ if (ret)
+ return ret;
+
+ ret = genphy_soft_reset(phydev);
+ if (ret)
+ return ret;
+
+ *finished = true;
+
+ return 0;
+}
+
+/**
* yt8821_get_features - read mmd register to get 2.5G capability
* @phydev: target phy_device struct
*
@@ -3135,13 +3303,24 @@ static struct phy_driver motorcomm_phy_drvs[] = {
.set_wol = ytphy_set_wol,
.config_aneg = yt8521_config_aneg,
.aneg_done = yt8521_aneg_done,
- .config_init = yt8521_config_init,
+ .config_init = yt8531s_config_init,
.read_status = yt8521_read_status,
.soft_reset = yt8521_soft_reset,
.suspend = yt8521_suspend,
.resume = yt8521_resume,
},
{
+ PHY_ID_MATCH_EXACT(PHY_ID_INT861X),
+ .name = "YT861x Internal Gigabit Ethernet",
+ .flags = PHY_IS_INTERNAL,
+ .config_init = yt861x_config_init,
+ .soft_reset = genphy_soft_reset,
+ .suspend = genphy_suspend,
+ .resume = genphy_resume,
+ .cable_test_start = yt861x_cable_test_start,
+ .cable_test_get_status = yt861x_cable_test_get_status,
+ },
+ {
PHY_ID_MATCH_EXACT(PHY_ID_YT8821),
.name = "YT8821 2.5Gbps PHY",
.get_features = yt8821_get_features,
@@ -3162,7 +3341,7 @@ static struct phy_driver motorcomm_phy_drvs[] = {
module_phy_driver(motorcomm_phy_drvs);
-MODULE_DESCRIPTION("Motorcomm 8511/8521/8531/8531S/8821 PHY driver");
+MODULE_DESCRIPTION("Motorcomm 8511/8521/8531/8531S/861x/8821 PHY driver");
MODULE_AUTHOR("Peter Geis");
MODULE_AUTHOR("Frank");
MODULE_LICENSE("GPL");
@@ -3173,6 +3352,7 @@ static const struct mdio_device_id __maybe_unused motorcomm_tbl[] = {
{ PHY_ID_MATCH_EXACT(PHY_ID_YT8522) },
{ PHY_ID_MATCH_EXACT(PHY_ID_YT8531) },
{ PHY_ID_MATCH_EXACT(PHY_ID_YT8531S) },
+ { PHY_ID_MATCH_EXACT(PHY_ID_INT861X) },
{ PHY_ID_MATCH_EXACT(PHY_ID_YT8821) },
{ /* sentinel */ }
};
diff --git a/drivers/net/phy/mxl-gpy.c b/drivers/net/phy/mxl-gpy.c
index 0da2d4e9d854..089a1a9e5bc6 100644
--- a/drivers/net/phy/mxl-gpy.c
+++ b/drivers/net/phy/mxl-gpy.c
@@ -1405,7 +1405,7 @@ static struct phy_driver gpy_drivers[] = {
PHY_ID_MATCH_MODEL(PHY_ID_MXL86211C),
.name = "Maxlinear Ethernet MxL86211C",
.get_features = genphy_c45_pma_read_abilities,
- .config_init = gpy_config_init,
+ .config_init = gpy21x_config_init,
.probe = gpy_probe,
.inband_caps = gpy_inband_caps,
.config_inband = gpy_config_inband,
diff --git a/drivers/net/phy/nxp-c45-tja11xx.c b/drivers/net/phy/nxp-c45-tja11xx.c
index f526528d2e32..8df152f6671f 100644
--- a/drivers/net/phy/nxp-c45-tja11xx.c
+++ b/drivers/net/phy/nxp-c45-tja11xx.c
@@ -2180,7 +2180,7 @@ module_phy_driver(nxp_c45_driver);
static const struct mdio_device_id __maybe_unused nxp_c45_tbl[] = {
{ PHY_ID_MATCH_MODEL(PHY_ID_TJA_1103) },
{ PHY_ID_MATCH_MODEL(PHY_ID_TJA_1120) },
- { /*sentinel*/ },
+ { /*sentinel*/ }
};
MODULE_DEVICE_TABLE(mdio, nxp_c45_tbl);
diff --git a/drivers/net/phy/phy-caps.h b/drivers/net/phy/phy-caps.h
index 421088e6f6e8..ec3d39a0ae06 100644
--- a/drivers/net/phy/phy-caps.h
+++ b/drivers/net/phy/phy-caps.h
@@ -66,5 +66,7 @@ void phy_caps_medium_get_supported(unsigned long *supported,
enum ethtool_link_medium medium,
int lanes);
u32 phy_caps_mediums_from_linkmodes(unsigned long *linkmodes);
+void phy_caps_linkmode_filter_ifaces(unsigned long *to, const unsigned long *from,
+ const unsigned long *interfaces);
#endif /* __PHY_CAPS_H */
diff --git a/drivers/net/phy/phy_caps.c b/drivers/net/phy/phy_caps.c
index 942d43191561..558e4df4d63c 100644
--- a/drivers/net/phy/phy_caps.c
+++ b/drivers/net/phy/phy_caps.c
@@ -445,3 +445,29 @@ u32 phy_caps_mediums_from_linkmodes(unsigned long *linkmodes)
return mediums;
}
EXPORT_SYMBOL_GPL(phy_caps_mediums_from_linkmodes);
+
+/**
+ * phy_caps_linkmode_filter_ifaces() - Filter linkmodes with an interface list
+ * @to: Stores the filtered linkmodes
+ * @from: Linkmodes to filter
+ * @interfaces: Bitfield of phy_interface_t that we use for filtering
+ *
+ * Filter the provided linkmodes, only to keep the ones we can possibly achieve
+ * when using any of the provided MII interfaces.
+ */
+void phy_caps_linkmode_filter_ifaces(unsigned long *to,
+ const unsigned long *from,
+ const unsigned long *interfaces)
+{
+ __ETHTOOL_DECLARE_LINK_MODE_MASK(ifaces_supported) = {};
+ unsigned int ifaces_caps = 0;
+ phy_interface_t interface;
+
+ for_each_set_bit(interface, interfaces, PHY_INTERFACE_MODE_MAX)
+ ifaces_caps |= phy_caps_from_interface(interface);
+
+ phy_caps_linkmodes(ifaces_caps, ifaces_supported);
+
+ linkmode_and(to, from, ifaces_supported);
+}
+EXPORT_SYMBOL_GPL(phy_caps_linkmode_filter_ifaces);
diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c
index 94b2e85e00a3..5d4508bcfbe9 100644
--- a/drivers/net/phy/phy_device.c
+++ b/drivers/net/phy/phy_device.c
@@ -49,14 +49,6 @@ MODULE_DESCRIPTION("PHY library");
MODULE_AUTHOR("Andy Fleming");
MODULE_LICENSE("GPL");
-struct phy_fixup {
- struct list_head list;
- char bus_id[MII_BUS_ID_SIZE + 3];
- u32 phy_uid;
- u32 phy_uid_mask;
- int (*run)(struct phy_device *phydev);
-};
-
static struct phy_driver genphy_c45_driver = {
.phy_id = 0xffffffff,
.phy_id_mask = 0xffffffff,
@@ -237,9 +229,6 @@ static void phy_mdio_device_remove(struct mdio_device *mdiodev)
static struct phy_driver genphy_driver;
-static LIST_HEAD(phy_fixup_list);
-static DEFINE_MUTEX(phy_fixup_lock);
-
static bool phy_drv_wol_enabled(struct phy_device *phydev)
{
struct ethtool_wolinfo wol = { .cmd = ETHTOOL_GWOL };
@@ -428,85 +417,6 @@ static SIMPLE_DEV_PM_OPS(mdio_bus_phy_pm_ops, mdio_bus_phy_suspend,
mdio_bus_phy_resume);
/**
- * phy_register_fixup - creates a new phy_fixup and adds it to the list
- * @bus_id: A string which matches phydev->mdio.dev.bus_id (or NULL)
- * @phy_uid: Used to match against phydev->phy_id (the UID of the PHY)
- * @phy_uid_mask: Applied to phydev->phy_id and fixup->phy_uid before
- * comparison (or 0 to disable id-based matching)
- * @run: The actual code to be run when a matching PHY is found
- */
-static int phy_register_fixup(const char *bus_id, u32 phy_uid, u32 phy_uid_mask,
- int (*run)(struct phy_device *))
-{
- struct phy_fixup *fixup = kzalloc_obj(*fixup);
-
- if (!fixup)
- return -ENOMEM;
-
- if (bus_id)
- strscpy(fixup->bus_id, bus_id, sizeof(fixup->bus_id));
- fixup->phy_uid = phy_uid;
- fixup->phy_uid_mask = phy_uid_mask;
- fixup->run = run;
-
- mutex_lock(&phy_fixup_lock);
- list_add_tail(&fixup->list, &phy_fixup_list);
- mutex_unlock(&phy_fixup_lock);
-
- return 0;
-}
-
-/* Registers a fixup to be run on any PHY with the UID in phy_uid */
-int phy_register_fixup_for_uid(u32 phy_uid, u32 phy_uid_mask,
- int (*run)(struct phy_device *))
-{
- return phy_register_fixup(NULL, phy_uid, phy_uid_mask, run);
-}
-EXPORT_SYMBOL(phy_register_fixup_for_uid);
-
-/* Registers a fixup to be run on the PHY with id string bus_id */
-int phy_register_fixup_for_id(const char *bus_id,
- int (*run)(struct phy_device *))
-{
- return phy_register_fixup(bus_id, 0, 0, run);
-}
-EXPORT_SYMBOL(phy_register_fixup_for_id);
-
-static bool phy_needs_fixup(struct phy_device *phydev, struct phy_fixup *fixup)
-{
- if (!strcmp(fixup->bus_id, phydev_name(phydev)))
- return true;
-
- if (fixup->phy_uid_mask &&
- phy_id_compare(phydev->phy_id, fixup->phy_uid, fixup->phy_uid_mask))
- return true;
-
- return false;
-}
-
-/* Runs any matching fixups for this phydev */
-static int phy_scan_fixups(struct phy_device *phydev)
-{
- struct phy_fixup *fixup;
-
- mutex_lock(&phy_fixup_lock);
- list_for_each_entry(fixup, &phy_fixup_list, list) {
- if (phy_needs_fixup(phydev, fixup)) {
- int err = fixup->run(phydev);
-
- if (err < 0) {
- mutex_unlock(&phy_fixup_lock);
- return err;
- }
- phydev->has_fixups = true;
- }
- }
- mutex_unlock(&phy_fixup_lock);
-
- return 0;
-}
-
-/**
* genphy_match_phy_device - match a PHY device with a PHY driver
* @phydev: target phy_device struct
* @phydrv: target phy_driver struct
@@ -1490,11 +1400,22 @@ static int phy_sfp_connect_phy(void *upstream, struct phy_device *phy)
{
struct phy_device *phydev = upstream;
struct net_device *dev = phydev->attached_dev;
+ int ret;
- if (dev)
- return phy_link_topo_add_phy(dev, phy, PHY_UPSTREAM_PHY, phydev);
+ phydev->has_sfp_mod_phy = true;
+ phy_set_upstream_port(phy, phydev->sfp_cage_port);
- return 0;
+ /* If we aren't attached to a netdev, we can't add the SFP PHY to its
+ * topology.
+ */
+ if (!dev)
+ return 0;
+
+ ret = phy_link_topo_add_phy(dev, phy, PHY_UPSTREAM_PHY, phydev);
+ if (ret)
+ phydev->has_sfp_mod_phy = false;
+
+ return ret;
}
/**
@@ -1512,8 +1433,12 @@ static void phy_sfp_disconnect_phy(void *upstream, struct phy_device *phy)
struct phy_device *phydev = upstream;
struct net_device *dev = phydev->attached_dev;
+ phydev->has_sfp_mod_phy = false;
+
if (dev)
phy_link_topo_del_phy(dev, phy);
+
+ phy_set_upstream_port(phy, NULL);
}
/**
@@ -1617,6 +1542,80 @@ static void phy_sfp_link_down(void *upstream)
port->ops->link_down(port);
}
+static int phy_add_sfp_mod_port(struct phy_device *phydev)
+{
+ DECLARE_PHY_INTERFACE_MASK(interfaces);
+ const struct sfp_module_caps *caps;
+ struct phy_port *port;
+ int ret = 0;
+
+ /* Create mod port */
+ port = phy_port_alloc();
+ if (!port)
+ return -ENOMEM;
+
+ port->active = true;
+
+ caps = sfp_get_module_caps(phydev->sfp_bus);
+
+ phy_interface_and(interfaces, phydev->sfp_cage_port->interfaces,
+ caps->interfaces);
+ phy_caps_linkmode_filter_ifaces(port->supported, caps->link_modes,
+ interfaces);
+
+ if (phydev->attached_dev) {
+ ret = phy_link_topo_add_port(phydev->attached_dev, port);
+ if (ret) {
+ phy_port_destroy(port);
+ return ret;
+ }
+ }
+
+ /* we don't use phy_add_port() here as the module port isn't a direct
+ * interface from the PHY, but rather an extension to the sfp-bus, that
+ * is already represented by its own phy_port
+ */
+ phydev->mod_port = port;
+
+ port->upstream_port = phydev->sfp_cage_port;
+
+ return 0;
+}
+
+static void phy_del_sfp_mod_port(struct phy_device *phydev)
+{
+ if (!phydev->mod_port)
+ return;
+
+ if (phydev->attached_dev)
+ phy_link_topo_del_port(phydev->attached_dev, phydev->mod_port);
+
+ phy_port_destroy(phydev->mod_port);
+ phydev->mod_port = NULL;
+}
+
+static int phy_sfp_module_start(void *upstream)
+{
+ struct phy_device *phydev = upstream;
+
+ /* If there's a downstream SFP module, and it doesn't contain a PHY
+ * device, let's create a phy_port to represent that module.
+ */
+ if (!phydev->has_sfp_mod_phy)
+ return phy_add_sfp_mod_port(phydev);
+
+ return 0;
+}
+
+static void phy_sfp_module_stop(void *upstream)
+{
+ struct phy_device *phydev = upstream;
+
+ /* Called upon module removal or upstream removal */
+ if (!phydev->has_sfp_mod_phy)
+ phy_del_sfp_mod_port(phydev);
+}
+
static const struct sfp_upstream_ops sfp_phydev_ops = {
.attach = phy_sfp_attach,
.detach = phy_sfp_detach,
@@ -1626,6 +1625,8 @@ static const struct sfp_upstream_ops sfp_phydev_ops = {
.link_down = phy_sfp_link_down,
.connect_phy = phy_sfp_connect_phy,
.disconnect_phy = phy_sfp_disconnect_phy,
+ .module_start = phy_sfp_module_start,
+ .module_stop = phy_sfp_module_stop,
};
static int phy_add_port(struct phy_device *phydev, struct phy_port *port)
@@ -1670,16 +1671,19 @@ static void phy_del_port(struct phy_device *phydev, struct phy_port *port)
list_del(&port->head);
+ if (phydev->attached_dev)
+ phy_link_topo_del_port(phydev->attached_dev, port);
+
phydev->n_ports--;
}
-static int phy_setup_sfp_port(struct phy_device *phydev)
+static struct phy_port *phy_setup_sfp_port(struct phy_device *phydev)
{
struct phy_port *port = phy_port_alloc();
int ret;
if (!port)
- return -ENOMEM;
+ return ERR_PTR(-ENOMEM);
port->parent_type = PHY_PORT_PHY;
port->phy = phydev;
@@ -1694,10 +1698,12 @@ static int phy_setup_sfp_port(struct phy_device *phydev)
* when attaching the port to the phydev.
*/
ret = phy_add_port(phydev, port);
- if (ret)
+ if (ret) {
phy_port_destroy(port);
+ return ERR_PTR(ret);
+ }
- return ret;
+ return port;
}
/**
@@ -1706,27 +1712,75 @@ static int phy_setup_sfp_port(struct phy_device *phydev)
*/
static int phy_sfp_probe(struct phy_device *phydev)
{
+ struct phy_port *port;
struct sfp_bus *bus;
- int ret = 0;
- if (phydev->mdio.dev.fwnode) {
- bus = sfp_bus_find_fwnode(phydev->mdio.dev.fwnode);
- if (IS_ERR(bus))
- return PTR_ERR(bus);
+ if (!phydev->mdio.dev.fwnode)
+ return 0;
- phydev->sfp_bus = bus;
+ bus = sfp_bus_find_fwnode(phydev->mdio.dev.fwnode);
+ if (IS_ERR(bus))
+ return PTR_ERR(bus);
- ret = sfp_bus_add_upstream(bus, phydev, &sfp_phydev_ops);
- sfp_bus_put(bus);
+ phydev->sfp_bus = bus;
- if (ret)
+ if (bus) {
+ port = phy_setup_sfp_port(phydev);
+ if (IS_ERR(port)) {
+ sfp_bus_put(bus);
phydev->sfp_bus = NULL;
+ return PTR_ERR(port);
+ }
+
+ phydev->sfp_cage_port = port;
}
- if (!ret && phydev->sfp_bus)
- ret = phy_setup_sfp_port(phydev);
+ return 0;
+}
- return ret;
+static int phy_sfp_add_upstream(struct phy_device *phydev)
+{
+ struct sfp_bus *bus = phydev->sfp_bus;
+ int ret;
+
+ ret = sfp_bus_add_upstream(bus, phydev, &sfp_phydev_ops);
+ if (ret) {
+ sfp_bus_put(bus);
+ phydev->sfp_bus = NULL;
+ phydev->sfp_cage_port = NULL;
+ return ret;
+ }
+
+ /* sfp_bus_add_upstream() grabs a ref to the sfp bus on success, it's
+ * safe to release it now.
+ */
+ sfp_bus_put(bus);
+
+ return 0;
+}
+
+/**
+ * phy_sfp_release - release resources set up by phy_sfp_probe()
+ * @phydev: the PHY device
+ *
+ * Release the SFP resources set up by a successful phy_sfp_probe(). Unregister
+ * the upstream before destroying its phy_port, so SFP upstream callbacks cannot
+ * race with port destruction.
+ */
+static void phy_sfp_release(struct phy_device *phydev)
+{
+ struct phy_port *port, *tmp;
+
+ sfp_bus_del_upstream(phydev->sfp_bus);
+ phydev->sfp_bus = NULL;
+
+ list_for_each_entry_safe(port, tmp, &phydev->ports, head) {
+ if (!port->is_sfp)
+ continue;
+
+ phy_del_port(phydev, port);
+ phy_port_destroy(port);
+ }
}
static bool phy_drv_supports_irq(const struct phy_driver *phydrv)
@@ -1735,6 +1789,108 @@ static bool phy_drv_supports_irq(const struct phy_driver *phydrv)
}
/**
+ * phy_detach_internal - detach a PHY device from its network device
+ * @phydev: target phy_device struct
+ * @notify_bus: whether to notify the MDIO bus about the PHY detach
+ *
+ * This detaches the phy device from its network device and the phy
+ * driver, and drops the reference count taken in phy_attach_direct().
+ */
+static void phy_detach_internal(struct phy_device *phydev, bool notify_bus)
+{
+ struct net_device *dev = phydev->attached_dev;
+ struct module *ndev_owner = NULL;
+ struct mii_bus *bus;
+
+ if (phydev->devlink) {
+ device_link_del(phydev->devlink);
+ phydev->devlink = NULL;
+ }
+
+ if (phydev->sysfs_links) {
+ if (dev)
+ sysfs_remove_link(&dev->dev.kobj, "phydev");
+ sysfs_remove_link(&phydev->mdio.dev.kobj, "attached_dev");
+ }
+
+ if (!phydev->attached_dev)
+ sysfs_remove_file(&phydev->mdio.dev.kobj,
+ &dev_attr_phy_standalone.attr);
+
+ phy_suspend(phydev);
+
+ if (notify_bus && phydev->mdio.bus->notify_phy_detach)
+ phydev->mdio.bus->notify_phy_detach(phydev);
+
+ if (dev) {
+ struct hwtstamp_provider *hwprov;
+
+ /* hwprov may technically be protected by ops lock but
+ * not for devices with a phydev, see phy_link_topo_add_phy()
+ */
+ hwprov = rtnl_dereference(dev->hwprov);
+ /* Disable timestamp if it is the one selected */
+ if (hwprov && hwprov->phydev == phydev) {
+ rcu_assign_pointer(dev->hwprov, NULL);
+ kfree_rcu(hwprov, rcu_head);
+ }
+
+ phydev->attached_dev->phydev = NULL;
+ phydev->attached_dev = NULL;
+ phy_link_topo_del_phy(dev, phydev);
+ if (phydev->mod_port)
+ phy_link_topo_del_port(dev, phydev->mod_port);
+ }
+
+ phydev->phy_link_change = NULL;
+ phydev->phylink = NULL;
+
+ module_put(phydev->drv_owner);
+ phydev->drv_owner = NULL;
+
+ /* If the device had no specific driver before (i.e. - it
+ * was using the generic driver), we unbind the device
+ * from the generic driver so that there's a chance a
+ * real driver could be loaded
+ */
+ if (phydev->is_genphy_driven) {
+ /* The release below lets phy_probe() write this field. */
+ phydev->irq = phydev->mdio.bus->irq[phydev->mdio.addr];
+ device_release_driver(&phydev->mdio.dev);
+ phydev->is_genphy_driven = 0;
+ }
+
+ /* Assert the reset signal */
+ phy_device_reset(phydev, 1);
+
+ /*
+ * The phydev might go away on the put_device() below, so avoid
+ * a use-after-free bug by reading the underlying bus first.
+ */
+ bus = phydev->mdio.bus;
+
+ put_device(&phydev->mdio.dev);
+ if (dev)
+ ndev_owner = dev->dev.parent->driver->owner;
+ if (ndev_owner != bus->owner)
+ module_put(bus->owner);
+}
+
+/**
+ * phy_detach - detach a PHY device from its network device
+ * @phydev: target phy_device struct
+ *
+ * This detaches the phy device from its network device and the phy
+ * driver, and drops the reference count taken in phy_attach_direct().
+ */
+void phy_detach(struct phy_device *phydev)
+{
+ /* cleanup including bus notification */
+ phy_detach_internal(phydev, true);
+}
+EXPORT_SYMBOL(phy_detach);
+
+/**
* phy_attach_direct - attach a network device to a given PHY device pointer
* @dev: network device to attach
* @phydev: Pointer to phy_device to attach
@@ -1755,6 +1911,8 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
struct mii_bus *bus = phydev->mdio.bus;
struct device *d = &phydev->mdio.dev;
struct module *ndev_owner = NULL;
+ struct module *drv_owner;
+ int irq = phydev->irq;
int err;
/* For Ethernet device drivers that register their own MDIO bus, we
@@ -1771,6 +1929,13 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
get_device(d);
+ /* Set by every attach, with or without a netdev */
+ if (phydev->phy_link_change) {
+ phydev_err(phydev, "PHY already attached\n");
+ err = -EBUSY;
+ goto error_put_device;
+ }
+
/* Assume that if there is no driver, that it doesn't
* exist, and we should use the genphy driver.
*/
@@ -1783,11 +1948,13 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
phydev->is_genphy_driven = 1;
}
- if (!try_module_get(d->driver->owner)) {
+ drv_owner = d->driver->owner;
+ if (!try_module_get(drv_owner)) {
phydev_err(phydev, "failed to get the device driver module\n");
err = -EIO;
goto error_put_device;
}
+ phydev->drv_owner = drv_owner;
if (phydev->is_genphy_driven) {
err = d->driver->probe(d);
@@ -1798,12 +1965,6 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
goto error_module_put;
}
- if (phydev->attached_dev) {
- dev_err(&dev->dev, "PHY already attached\n");
- err = -EBUSY;
- goto error;
- }
-
phydev->phy_link_change = phy_link_change;
if (dev) {
phydev->attached_dev = dev;
@@ -1815,6 +1976,12 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
err = phy_link_topo_add_phy(dev, phydev, PHY_UPSTREAM_MAC, dev);
if (err)
goto error;
+
+ if (phydev->mod_port) {
+ err = phy_link_topo_add_port(dev, phydev->mod_port);
+ if (err)
+ goto error;
+ }
}
/* Some Ethernet drivers try to connect to a PHY device before
@@ -1876,6 +2043,12 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
if (err)
goto error;
+ if (phydev->mdio.bus->notify_phy_attach) {
+ err = phydev->mdio.bus->notify_phy_attach(phydev);
+ if (err)
+ goto error;
+ }
+
phy_resume(phydev);
/**
@@ -1890,12 +2063,14 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
return err;
error:
- /* phy_detach() does all of the cleanup below */
- phy_detach(phydev);
+ /* phy_detach_internal() does all of the cleanup below */
+ phy_detach_internal(phydev, false);
return err;
error_module_put:
- module_put(d->driver->owner);
+ module_put(phydev->drv_owner);
+ phydev->drv_owner = NULL;
+ phydev->irq = irq;
phydev->is_genphy_driven = 0;
d->driver = NULL;
error_put_device:
@@ -1906,86 +2081,6 @@ error_put_device:
}
EXPORT_SYMBOL(phy_attach_direct);
-/**
- * phy_detach - detach a PHY device from its network device
- * @phydev: target phy_device struct
- *
- * This detaches the phy device from its network device and the phy
- * driver, and drops the reference count taken in phy_attach_direct().
- */
-void phy_detach(struct phy_device *phydev)
-{
- struct net_device *dev = phydev->attached_dev;
- struct module *ndev_owner = NULL;
- struct mii_bus *bus;
-
- if (phydev->devlink) {
- device_link_del(phydev->devlink);
- phydev->devlink = NULL;
- }
-
- if (phydev->sysfs_links) {
- if (dev)
- sysfs_remove_link(&dev->dev.kobj, "phydev");
- sysfs_remove_link(&phydev->mdio.dev.kobj, "attached_dev");
- }
-
- if (!phydev->attached_dev)
- sysfs_remove_file(&phydev->mdio.dev.kobj,
- &dev_attr_phy_standalone.attr);
-
- phy_suspend(phydev);
- if (dev) {
- struct hwtstamp_provider *hwprov;
-
- /* hwprov may technically be protected by ops lock but
- * not for devices with a phydev, see phy_link_topo_add_phy()
- */
- hwprov = rtnl_dereference(dev->hwprov);
- /* Disable timestamp if it is the one selected */
- if (hwprov && hwprov->phydev == phydev) {
- rcu_assign_pointer(dev->hwprov, NULL);
- kfree_rcu(hwprov, rcu_head);
- }
-
- phydev->attached_dev->phydev = NULL;
- phydev->attached_dev = NULL;
- phy_link_topo_del_phy(dev, phydev);
- }
-
- phydev->phy_link_change = NULL;
- phydev->phylink = NULL;
-
- if (phydev->mdio.dev.driver)
- module_put(phydev->mdio.dev.driver->owner);
-
- /* If the device had no specific driver before (i.e. - it
- * was using the generic driver), we unbind the device
- * from the generic driver so that there's a chance a
- * real driver could be loaded
- */
- if (phydev->is_genphy_driven) {
- device_release_driver(&phydev->mdio.dev);
- phydev->is_genphy_driven = 0;
- }
-
- /* Assert the reset signal */
- phy_device_reset(phydev, 1);
-
- /*
- * The phydev might go away on the put_device() below, so avoid
- * a use-after-free bug by reading the underlying bus first.
- */
- bus = phydev->mdio.bus;
-
- put_device(&phydev->mdio.dev);
- if (dev)
- ndev_owner = dev->dev.parent->driver->owner;
- if (ndev_owner != bus->owner)
- module_put(bus->owner);
-}
-EXPORT_SYMBOL(phy_detach);
-
int phy_suspend(struct phy_device *phydev)
{
struct net_device *netdev = phydev->attached_dev;
@@ -3454,6 +3549,7 @@ static int phy_default_setup_single_port(struct phy_device *phydev)
{
struct phy_port *port = phy_port_alloc();
unsigned long mode;
+ int ret;
if (!port)
return -ENOMEM;
@@ -3480,9 +3576,11 @@ static int phy_default_setup_single_port(struct phy_device *phydev)
port->pairs = max_t(int, port->pairs,
ethtool_linkmode_n_pairs(mode));
- phy_add_port(phydev, port);
+ ret = phy_add_port(phydev, port);
+ if (ret)
+ phy_port_destroy(port);
- return 0;
+ return ret;
}
static int of_phy_ports(struct phy_device *phydev)
@@ -3547,13 +3645,13 @@ static int phy_setup_ports(struct phy_device *phydev)
if (!phydev->is_genphy_driven) {
ret = phy_sfp_probe(phydev);
if (ret)
- goto out;
+ goto err_ports;
}
if (phydev->n_ports < phydev->max_n_ports) {
ret = phy_default_setup_single_port(phydev);
if (ret)
- goto out;
+ goto err_sfp;
}
linkmode_zero(ports_supported);
@@ -3578,9 +3676,15 @@ static int phy_setup_ports(struct phy_device *phydev)
break;
}
+ ret = phy_sfp_add_upstream(phydev);
+ if (ret)
+ goto err_sfp;
+
return 0;
-out:
+err_sfp:
+ phy_sfp_release(phydev);
+err_ports:
phy_cleanup_ports(phydev);
return ret;
}
@@ -3605,6 +3709,25 @@ struct phy_port *phy_get_sfp_port(struct phy_device *phydev)
EXPORT_SYMBOL_GPL(phy_get_sfp_port);
/**
+ * phy_set_upstream_port() - Sets the phy_port controlling the MII this PHY is
+ * attached to.
+ * @phydev: pointer to the PHY device we set the upstream of.
+ * @port: The phy_port upstream of this PHY, can be NULL.
+ */
+void phy_set_upstream_port(struct phy_device *phydev, struct phy_port *port)
+{
+ struct phy_port *local_port;
+
+ ASSERT_RTNL();
+
+ phydev->upstream_port = port;
+
+ phy_for_each_port(phydev, local_port)
+ local_port->upstream_port = port;
+}
+EXPORT_SYMBOL_GPL(phy_set_upstream_port);
+
+/**
* fwnode_mdio_find_device - Given a fwnode, find the mdio_device
* @fwnode: pointer to the mdio_device's fwnode
*
@@ -3706,7 +3829,7 @@ static int phy_probe(struct device *dev)
if (phydev->drv->probe) {
err = phydev->drv->probe(phydev);
if (err)
- goto out;
+ goto out_reset;
}
phy_disable_interrupts(phydev);
@@ -3727,7 +3850,7 @@ static int phy_probe(struct device *dev)
err = genphy_read_abilities(phydev);
if (err)
- goto out;
+ goto out_remove;
if (!linkmode_test_bit(ETHTOOL_LINK_MODE_Autoneg_BIT,
phydev->supported))
@@ -3744,7 +3867,7 @@ static int phy_probe(struct device *dev)
err = phy_setup_ports(phydev);
if (err)
- goto out;
+ goto out_remove;
phy_advertise_supported(phydev);
@@ -3753,7 +3876,7 @@ static int phy_probe(struct device *dev)
*/
err = genphy_c45_read_eee_adv(phydev, phydev->advertising_eee);
if (err)
- goto out;
+ goto out_sfp_release;
/* Get the EEE modes we want to prohibit. */
of_set_phy_eee_broken(phydev);
@@ -3793,9 +3916,6 @@ static int phy_probe(struct device *dev)
phydev->supported);
}
- /* Set the state to READY by default */
- phydev->state = PHY_READY;
-
/* Register the PHY LED triggers */
if (!phydev->is_on_sfp_module)
phy_led_triggers_register(phydev);
@@ -3806,20 +3926,27 @@ static int phy_probe(struct device *dev)
if (IS_ENABLED(CONFIG_PHYLIB_LEDS) && !phy_driver_is_genphy(phydev)) {
err = of_phy_leds(phydev);
if (err)
- goto out;
+ goto out_unreg_led_triggers;
}
+ /* Set the state to READY by default */
+ phydev->state = PHY_READY;
+
return 0;
-out:
- sfp_bus_del_upstream(phydev->sfp_bus);
- phydev->sfp_bus = NULL;
+out_unreg_led_triggers:
+ if (!phydev->is_on_sfp_module)
+ phy_led_triggers_unregister(phydev);
+out_sfp_release:
+ phy_sfp_release(phydev);
phy_cleanup_ports(phydev);
- if (!phydev->is_on_sfp_module)
- phy_led_triggers_unregister(phydev);
+out_remove:
+ if (phydev->drv->remove)
+ phydev->drv->remove(phydev);
+out_reset:
/* Re-assert the reset signal on error */
phy_device_reset(phydev, 1);
@@ -3840,9 +3967,7 @@ static int phy_remove(struct device *dev)
phydev->state = PHY_DOWN;
- sfp_bus_del_upstream(phydev->sfp_bus);
- phydev->sfp_bus = NULL;
-
+ phy_sfp_release(phydev);
phy_cleanup_ports(phydev);
if (phydev->drv && phydev->drv->remove)
diff --git a/drivers/net/phy/phy_fixup.c b/drivers/net/phy/phy_fixup.c
new file mode 100644
index 000000000000..75ed9b142ccc
--- /dev/null
+++ b/drivers/net/phy/phy_fixup.c
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * PHY fixup support
+ */
+
+#include <linux/list.h>
+#include <linux/phy.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "phylib-internal.h"
+
+static struct list_head phy_fixup_list __ro_after_init =
+ LIST_HEAD_INIT(phy_fixup_list);
+
+struct phy_fixup {
+ struct list_head list;
+ char bus_id[MII_BUS_ID_SIZE + 3];
+ u32 phy_uid;
+ u32 phy_uid_mask;
+ int (*run)(struct phy_device *phydev);
+};
+
+/**
+ * phy_register_fixup - creates a new phy_fixup and adds it to the list
+ * @bus_id: A string which matches phydev->mdio.dev.bus_id (or NULL)
+ * @phy_uid: Used to match against phydev->phy_id (the UID of the PHY)
+ * @phy_uid_mask: Applied to phydev->phy_id and fixup->phy_uid before
+ * comparison (or 0 to disable id-based matching)
+ * @run: The actual code to be run when a matching PHY is found
+ */
+static void __init phy_register_fixup(const char *bus_id, u32 phy_uid,
+ u32 phy_uid_mask,
+ int (*run)(struct phy_device *))
+{
+ struct phy_fixup *fixup = kzalloc_obj(*fixup);
+
+ if (!fixup)
+ return;
+
+ if (bus_id)
+ strscpy(fixup->bus_id, bus_id);
+ fixup->phy_uid = phy_uid;
+ fixup->phy_uid_mask = phy_uid_mask;
+ fixup->run = run;
+
+ list_add_tail(&fixup->list, &phy_fixup_list);
+}
+
+/* Registers a fixup to be run on any PHY with the UID in phy_uid */
+void __init phy_register_fixup_for_uid(u32 phy_uid, u32 phy_uid_mask,
+ int (*run)(struct phy_device *))
+{
+ phy_register_fixup(NULL, phy_uid, phy_uid_mask, run);
+}
+
+/* Registers a fixup to be run on the PHY with id string bus_id */
+void __init phy_register_fixup_for_id(const char *bus_id,
+ int (*run)(struct phy_device *))
+{
+ phy_register_fixup(bus_id, 0, 0, run);
+}
+
+static bool phy_needs_fixup(struct phy_device *phydev, struct phy_fixup *fixup)
+{
+ if (!strcmp(fixup->bus_id, phydev_name(phydev)))
+ return true;
+
+ if (fixup->phy_uid_mask &&
+ phy_id_compare(phydev->phy_id, fixup->phy_uid, fixup->phy_uid_mask))
+ return true;
+
+ return false;
+}
+
+/**
+ * phy_scan_fixups - runs any matching fixups for this phydev
+ * @phydev: the phydev to search and run fixups for
+ * Returns: 0 or an errno
+ */
+int phy_scan_fixups(struct phy_device *phydev)
+{
+ struct phy_fixup *fixup;
+
+ list_for_each_entry(fixup, &phy_fixup_list, list) {
+ if (phy_needs_fixup(phydev, fixup)) {
+ int err = fixup->run(phydev);
+
+ if (err < 0)
+ return err;
+
+ phydev->has_fixups = true;
+ }
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(phy_scan_fixups);
diff --git a/drivers/net/phy/phy_led_triggers.c b/drivers/net/phy/phy_led_triggers.c
index 4eb7716bb9d6..ff6e518395be 100644
--- a/drivers/net/phy/phy_led_triggers.c
+++ b/drivers/net/phy/phy_led_triggers.c
@@ -126,6 +126,7 @@ out_unreg:
while (i--)
phy_led_trigger_unregister(&phy->phy_led_triggers[i]);
kfree(phy->phy_led_triggers);
+ phy->phy_led_triggers = NULL;
out_unreg_link:
phy_led_trigger_unregister(phy->led_link_trigger);
out_free_link:
@@ -141,10 +142,12 @@ void phy_led_triggers_unregister(struct phy_device *phy)
{
int i;
- for (i = 0; i < phy->phy_num_led_triggers; i++)
- phy_led_trigger_unregister(&phy->phy_led_triggers[i]);
- kfree(phy->phy_led_triggers);
- phy->phy_led_triggers = NULL;
+ if (phy->phy_led_triggers) {
+ for (i = 0; i < phy->phy_num_led_triggers; i++)
+ phy_led_trigger_unregister(&phy->phy_led_triggers[i]);
+ kfree(phy->phy_led_triggers);
+ phy->phy_led_triggers = NULL;
+ }
if (phy->led_link_trigger) {
phy_led_trigger_unregister(phy->led_link_trigger);
diff --git a/drivers/net/phy/phy_link_topology.c b/drivers/net/phy/phy_link_topology.c
index 4134de7ae313..214ccce23cf6 100644
--- a/drivers/net/phy/phy_link_topology.c
+++ b/drivers/net/phy/phy_link_topology.c
@@ -7,6 +7,7 @@
*/
#include <linux/phy_link_topology.h>
+#include <linux/phy_port.h>
#include <linux/phy.h>
#include <linux/rtnetlink.h>
#include <linux/xarray.h>
@@ -23,17 +24,89 @@ static int netdev_alloc_phy_link_topology(struct net_device *dev)
xa_init_flags(&topo->phys, XA_FLAGS_ALLOC1);
topo->next_phy_index = 1;
+ xa_init_flags(&topo->ports, XA_FLAGS_ALLOC1);
+ topo->next_port_index = 1;
+
dev->link_topo = topo;
return 0;
}
+static struct phy_link_topology *phy_link_topo_get_or_alloc(struct net_device *dev)
+{
+ int ret;
+
+ if (dev->link_topo)
+ return dev->link_topo;
+
+ /* The topology is allocated the first time we add an object to it.
+ * It is freed alongside the netdev. It can be called on multiple
+ * contexts:
+ * - It can be called from .probe() : No rtnl, no netdev_lock
+ * - .ndo_open() : rtnl and possibly netdev_lock
+ * - SFP state machine : rtnl held or not
+ *
+ * However, we can't really have races :
+ * - If we have a PHY, phy_link_topo_add_phy() will always run first
+ * and trigger the alloc. Only then the ports can be added through
+ * phylib or sfp.
+ * - If we don't, the SFP port for the cage is registered first, and
+ * only then other ports/PHYs can be registered.
+ */
+ ret = netdev_alloc_phy_link_topology(dev);
+ if (ret)
+ return ERR_PTR(ret);
+
+ return dev->link_topo;
+}
+
+int phy_link_topo_add_port(struct net_device *dev, struct phy_port *port)
+{
+ struct phy_link_topology *topo;
+ int ret;
+
+ /* Ports can now be queried without rtnl for ops-locked devices, which
+ * we don't support now as port retrieval is done under rtnl.
+ * We don't have phy_port enabled devices yet, let's make sure
+ * we are loudly warned about that when it happens.
+ */
+ if (WARN_ON_ONCE(dev->netdev_ops && netdev_need_ops_lock(dev)))
+ return -EOPNOTSUPP;
+
+ topo = phy_link_topo_get_or_alloc(dev);
+ if (IS_ERR(topo))
+ return PTR_ERR(topo);
+
+ /* Attempt to re-use a previously allocated port_id */
+ if (port->id)
+ ret = xa_insert(&topo->ports, port->id, port, GFP_KERNEL);
+ else
+ ret = xa_alloc_cyclic(&topo->ports, &port->id, port,
+ xa_limit_32b, &topo->next_port_index,
+ GFP_KERNEL);
+
+ return ret < 0 ? ret : 0;
+}
+EXPORT_SYMBOL_GPL(phy_link_topo_add_port);
+
+void phy_link_topo_del_port(struct net_device *dev, struct phy_port *port)
+{
+ struct phy_link_topology *topo = dev->link_topo;
+
+ if (!topo)
+ return;
+
+ xa_erase(&topo->ports, port->id);
+}
+EXPORT_SYMBOL_GPL(phy_link_topo_del_port);
+
int phy_link_topo_add_phy(struct net_device *dev,
struct phy_device *phy,
enum phy_upstream upt, void *upstream)
{
- struct phy_link_topology *topo = dev->link_topo;
+ struct phy_link_topology *topo;
struct phy_device_node *pdn;
+ struct phy_port *port;
int ret;
/* ethtool ops may run without rtnl_lock, and rtnl_lock is what
@@ -45,13 +118,9 @@ int phy_link_topo_add_phy(struct net_device *dev,
if (WARN_ON_ONCE(netdev_need_ops_lock(dev)))
return -EOPNOTSUPP;
- if (!topo) {
- ret = netdev_alloc_phy_link_topology(dev);
- if (ret)
- return ret;
-
- topo = dev->link_topo;
- }
+ topo = phy_link_topo_get_or_alloc(dev);
+ if (IS_ERR(topo))
+ return PTR_ERR(topo);
pdn = kzalloc_obj(*pdn);
if (!pdn)
@@ -86,8 +155,20 @@ int phy_link_topo_add_phy(struct net_device *dev,
if (ret < 0)
goto err;
+ /* Add all the PHY's ports to the topology */
+ list_for_each_entry(port, &phy->ports, head) {
+ ret = phy_link_topo_add_port(dev, port);
+ if (ret)
+ goto del_ports;
+ }
+
return 0;
+del_ports:
+ list_for_each_entry_continue_reverse(port, &phy->ports, head)
+ phy_link_topo_del_port(dev, port);
+
+ xa_erase(&topo->phys, phy->phyindex);
err:
kfree(pdn);
return ret;
@@ -99,10 +180,14 @@ void phy_link_topo_del_phy(struct net_device *dev,
{
struct phy_link_topology *topo = dev->link_topo;
struct phy_device_node *pdn;
+ struct phy_port *port;
if (!topo)
return;
+ list_for_each_entry(port, &phy->ports, head)
+ phy_link_topo_del_port(dev, port);
+
pdn = xa_erase(&topo->phys, phy->phyindex);
/* We delete the PHY from the topology, however we don't re-set the
diff --git a/drivers/net/phy/phylib-internal.h b/drivers/net/phy/phylib-internal.h
index 664ed7faa518..38ee294f3c96 100644
--- a/drivers/net/phy/phylib-internal.h
+++ b/drivers/net/phy/phylib-internal.h
@@ -23,6 +23,7 @@ void of_set_phy_eee_broken(struct phy_device *phydev);
void of_set_phy_timing_role(struct phy_device *phydev);
int phy_speed_down_core(struct phy_device *phydev);
void phy_check_downshift(struct phy_device *phydev);
+int phy_scan_fixups(struct phy_device *phydev);
int mdiobus_register_device(struct mdio_device *mdiodev);
int mdiobus_unregister_device(struct mdio_device *mdiodev);
diff --git a/drivers/net/phy/phylink.c b/drivers/net/phy/phylink.c
index 1bbcf46c8356..e885da1c2831 100644
--- a/drivers/net/phy/phylink.c
+++ b/drivers/net/phy/phylink.c
@@ -14,6 +14,8 @@
#include <linux/of_mdio.h>
#include <linux/phy.h>
#include <linux/phy_fixed.h>
+#include <linux/phy_link_topology.h>
+#include <linux/phy_port.h>
#include <linux/phylink.h>
#include <linux/rtnetlink.h>
#include <linux/spinlock.h>
@@ -93,6 +95,8 @@ struct phylink {
DECLARE_PHY_INTERFACE_MASK(sfp_interfaces);
__ETHTOOL_DECLARE_LINK_MODE_MASK(sfp_support);
u8 sfp_port;
+ struct phy_port *sfp_cage_port;
+ struct phy_port *mod_port;
struct eee_config eee_cfg;
@@ -1767,6 +1771,57 @@ static void phylink_fixed_poll(struct timer_list *t)
static const struct sfp_upstream_ops sfp_phylink_ops;
+static int phylink_create_sfp_cage_port(struct phylink *pl)
+{
+ struct phy_port *port;
+ int ret = 0;
+
+ if (!pl->netdev || !pl->sfp_bus)
+ return 0;
+
+ port = phy_port_alloc();
+ if (!port)
+ return -ENOMEM;
+
+ port->is_sfp = true;
+ port->is_mii = true;
+ port->active = true;
+
+ phy_interface_and(port->interfaces, pl->config->supported_interfaces,
+ phylink_sfp_interfaces);
+ phy_port_update_supported(port);
+
+ rtnl_lock();
+ ret = phy_link_topo_add_port(pl->netdev, port);
+ rtnl_unlock();
+ if (ret)
+ goto out_destroy_port;
+
+ pl->sfp_cage_port = port;
+
+ return 0;
+
+out_destroy_port:
+ phy_port_destroy(port);
+ pl->sfp_cage_port = NULL;
+ return ret;
+}
+
+static void phylink_destroy_sfp_cage_port(struct phylink *pl)
+{
+ if (pl->netdev && pl->sfp_cage_port) {
+ /* The port may still be accessed under RTNL */
+ rtnl_lock();
+ phy_link_topo_del_port(pl->netdev, pl->sfp_cage_port);
+ rtnl_unlock();
+ }
+
+ if (pl->sfp_cage_port)
+ phy_port_destroy(pl->sfp_cage_port);
+
+ pl->sfp_cage_port = NULL;
+}
+
static int phylink_register_sfp(struct phylink *pl,
const struct fwnode_handle *fwnode)
{
@@ -1784,9 +1839,18 @@ static int phylink_register_sfp(struct phylink *pl,
pl->sfp_bus = bus;
+ ret = phylink_create_sfp_cage_port(pl);
+ if (ret) {
+ sfp_bus_put(bus);
+ return ret;
+ }
+
ret = sfp_bus_add_upstream(bus, pl, &sfp_phylink_ops);
sfp_bus_put(bus);
+ if (ret)
+ phylink_destroy_sfp_cage_port(pl);
+
return ret;
}
@@ -1832,6 +1896,126 @@ int phylink_set_fixed_link(struct phylink *pl,
EXPORT_SYMBOL_GPL(phylink_set_fixed_link);
/**
+ * phylink_update_pause_state() - Update the phylink pause frame configuration
+ * @pl: a pointer to a &struct phylink instance
+ * @pause_state: bitmask indicating the new pause state
+ *
+ * Update the MAC pause frame (flow control) state for the phylink instance.
+ */
+static void phylink_update_pause_state(struct phylink *pl, int pause_state)
+{
+ struct phylink_link_state *config = &pl->link_config;
+ bool tx_pause = !!(pause_state & MLO_PAUSE_TX);
+ bool rx_pause = !!(pause_state & MLO_PAUSE_RX);
+ bool manual_changed;
+
+ mutex_lock(&pl->state_mutex);
+
+ /*
+ * See the comments for linkmode_set_pause(), wrt the deficiencies
+ * with the current implementation. A solution to this issue would
+ * be:
+ * ethtool Local device
+ * rx tx Pause AsymDir
+ * 0 0 0 0
+ * 1 0 1 1
+ * 0 1 0 1
+ * 1 1 1 1
+ * and then use the ethtool rx/tx enablement status to mask the
+ * rx/tx pause resolution.
+ */
+ linkmode_set_pause(config->advertising, tx_pause,
+ rx_pause);
+
+ manual_changed = (config->pause ^ pause_state) & MLO_PAUSE_AN ||
+ (!(pause_state & MLO_PAUSE_AN) &&
+ (config->pause ^ pause_state) & MLO_PAUSE_TXRX_MASK);
+
+ config->pause = pause_state;
+
+ /* Update our in-band advertisement, triggering a renegotiation if
+ * the advertisement changed.
+ */
+ if (!pl->phydev)
+ phylink_change_inband_advert(pl);
+
+ mutex_unlock(&pl->state_mutex);
+
+ /* If we have a PHY, a change of the pause frame advertisement will
+ * cause phylib to renegotiate (if AN is enabled) which will in turn
+ * call our phylink_phy_change() and trigger a resolve. Note that
+ * we can't hold our state mutex while calling phy_set_asym_pause().
+ */
+ if (pl->phydev)
+ phy_set_asym_pause(pl->phydev, rx_pause, tx_pause);
+
+ /* If the manual pause settings changed, make sure we trigger a
+ * resolve to update their state; we can not guarantee that the
+ * link will cycle.
+ */
+ if (manual_changed) {
+ pl->link_failed = true;
+ phylink_run_resolve(pl);
+ }
+}
+
+/**
+ * phylink_update_mac_pause_capabilities() - Dynamically update MAC pause
+ * @pl: a pointer to a &struct phylink returned from phylink_create()
+ * @mac_pause: the new MAC pause capabilities mask
+ *
+ * This function allows a MAC driver to dynamically change its pause state,
+ * such as losing/gaining Pause frame support based on MTU size.
+ * It recalculates supported link modes and triggers renegotiation if needed.
+ */
+void phylink_update_mac_pause_capabilities(struct phylink *pl, unsigned long mac_pause)
+{
+ struct phylink_link_state *config = &pl->link_config;
+ unsigned long old_pause;
+ int pause_state;
+
+ ASSERT_RTNL();
+
+ if (mac_pause & ~(MAC_SYM_PAUSE | MAC_ASYM_PAUSE)) {
+ phylink_err(pl, "Attempted to dynamically change non-pause MAC capabilities\n");
+ return;
+ }
+
+ old_pause = pl->config->mac_capabilities & (MAC_SYM_PAUSE | MAC_ASYM_PAUSE);
+ if (old_pause == mac_pause)
+ return;
+
+ mutex_lock(&pl->state_mutex);
+
+ pl->config->mac_capabilities &= ~(MAC_SYM_PAUSE | MAC_ASYM_PAUSE);
+ pl->config->mac_capabilities |= mac_pause;
+
+ phylink_set(pl->supported, Pause);
+ phylink_set(pl->supported, Asym_Pause);
+
+ if (pl->phydev)
+ linkmode_and(pl->supported, pl->supported, pl->phydev->supported);
+ else if (pl->sfp_bus)
+ linkmode_and(pl->supported, pl->supported, pl->sfp_support);
+
+ phylink_validate(pl, pl->supported, config);
+
+ pause_state = config->pause;
+
+ if (!phylink_test(pl->supported, Pause)) {
+ pause_state &= ~(MLO_PAUSE_RX | MLO_PAUSE_TX);
+ } else if (!phylink_test(pl->supported, Asym_Pause)) {
+ if ((pause_state & MLO_PAUSE_RX) ^ (pause_state & MLO_PAUSE_TX))
+ pause_state &= ~(MLO_PAUSE_RX | MLO_PAUSE_TX);
+ }
+
+ mutex_unlock(&pl->state_mutex);
+
+ phylink_update_pause_state(pl, pause_state);
+}
+EXPORT_SYMBOL_GPL(phylink_update_mac_pause_capabilities);
+
+/**
* phylink_create() - create a phylink instance
* @config: a pointer to the target &struct phylink_config
* @fwnode: a pointer to a &struct fwnode_handle describing the network
@@ -1949,6 +2133,7 @@ EXPORT_SYMBOL_GPL(phylink_create);
void phylink_destroy(struct phylink *pl)
{
sfp_bus_del_upstream(pl->sfp_bus);
+ phylink_destroy_sfp_cage_port(pl);
if (pl->link_gpio)
gpiod_put(pl->link_gpio);
@@ -3204,8 +3389,6 @@ EXPORT_SYMBOL_GPL(phylink_ethtool_get_pauseparam);
int phylink_ethtool_set_pauseparam(struct phylink *pl,
struct ethtool_pauseparam *pause)
{
- struct phylink_link_state *config = &pl->link_config;
- bool manual_changed;
int pause_state;
ASSERT_RTNL();
@@ -3229,54 +3412,7 @@ int phylink_ethtool_set_pauseparam(struct phylink *pl,
if (pause->tx_pause)
pause_state |= MLO_PAUSE_TX;
- mutex_lock(&pl->state_mutex);
- /*
- * See the comments for linkmode_set_pause(), wrt the deficiencies
- * with the current implementation. A solution to this issue would
- * be:
- * ethtool Local device
- * rx tx Pause AsymDir
- * 0 0 0 0
- * 1 0 1 1
- * 0 1 0 1
- * 1 1 1 1
- * and then use the ethtool rx/tx enablement status to mask the
- * rx/tx pause resolution.
- */
- linkmode_set_pause(config->advertising, pause->tx_pause,
- pause->rx_pause);
-
- manual_changed = (config->pause ^ pause_state) & MLO_PAUSE_AN ||
- (!(pause_state & MLO_PAUSE_AN) &&
- (config->pause ^ pause_state) & MLO_PAUSE_TXRX_MASK);
-
- config->pause = pause_state;
-
- /* Update our in-band advertisement, triggering a renegotiation if
- * the advertisement changed.
- */
- if (!pl->phydev)
- phylink_change_inband_advert(pl);
-
- mutex_unlock(&pl->state_mutex);
-
- /* If we have a PHY, a change of the pause frame advertisement will
- * cause phylib to renegotiate (if AN is enabled) which will in turn
- * call our phylink_phy_change() and trigger a resolve. Note that
- * we can't hold our state mutex while calling phy_set_asym_pause().
- */
- if (pl->phydev)
- phy_set_asym_pause(pl->phydev, pause->rx_pause,
- pause->tx_pause);
-
- /* If the manual pause settings changed, make sure we trigger a
- * resolve to update their state; we can not guarantee that the
- * link will cycle.
- */
- if (manual_changed) {
- pl->link_failed = true;
- phylink_run_resolve(pl);
- }
+ phylink_update_pause_state(pl, pause_state);
return 0;
}
@@ -3884,14 +4020,70 @@ static void phylink_sfp_module_remove(void *upstream)
phy_interface_zero(pl->sfp_interfaces);
}
+static int phylink_add_sfp_mod_port(struct phylink *pl)
+{
+ DECLARE_PHY_INTERFACE_MASK(interfaces);
+ const struct sfp_module_caps *caps;
+ struct phy_port *port;
+ int ret = 0;
+
+ if (!pl->sfp_cage_port)
+ return 0;
+
+ /* Create mod port */
+ port = phy_port_alloc();
+ if (!port)
+ return -ENOMEM;
+
+ port->active = true;
+
+ caps = sfp_get_module_caps(pl->sfp_bus);
+
+ phy_interface_and(interfaces, pl->sfp_cage_port->interfaces,
+ caps->interfaces);
+ phy_caps_linkmode_filter_ifaces(port->supported, caps->link_modes,
+ interfaces);
+
+ if (pl->netdev) {
+ ret = phy_link_topo_add_port(pl->netdev, port);
+ if (ret) {
+ phy_port_destroy(port);
+ return ret;
+ }
+ }
+
+ port->upstream_port = pl->sfp_cage_port;
+
+ pl->mod_port = port;
+
+ return 0;
+}
+
+static void phylink_del_sfp_mod_port(struct phylink *pl)
+{
+ if (!pl->mod_port)
+ return;
+
+ if (pl->netdev)
+ phy_link_topo_del_port(pl->netdev, pl->mod_port);
+
+ phy_port_destroy(pl->mod_port);
+ pl->mod_port = NULL;
+}
+
static int phylink_sfp_module_start(void *upstream)
{
struct phylink *pl = upstream;
+ int ret;
/* If this SFP module has a PHY, start the PHY now. */
if (pl->phydev) {
phy_start(pl->phydev);
return 0;
+ } else {
+ ret = phylink_add_sfp_mod_port(pl);
+ if (ret)
+ return ret;
}
/* If the module may have a PHY but we didn't detect one we
@@ -3900,7 +4092,16 @@ static int phylink_sfp_module_start(void *upstream)
if (!pl->sfp_may_have_phy)
return 0;
- return phylink_sfp_config_optical(pl);
+ ret = phylink_sfp_config_optical(pl);
+ if (ret)
+ goto del_mod_port;
+
+ return 0;
+
+del_mod_port:
+ phylink_del_sfp_mod_port(pl);
+
+ return ret;
}
static void phylink_sfp_module_stop(void *upstream)
@@ -3910,6 +4111,8 @@ static void phylink_sfp_module_stop(void *upstream)
/* If this SFP module has a PHY, stop it. */
if (pl->phydev)
phy_stop(pl->phydev);
+ else
+ phylink_del_sfp_mod_port(pl);
}
static void phylink_sfp_link_down(void *upstream)
@@ -3954,6 +4157,8 @@ static int phylink_sfp_connect_phy(void *upstream, struct phy_device *phy)
phy_interface_and(phy->host_interfaces, phylink_sfp_interfaces,
pl->config->supported_interfaces);
+ phy_set_upstream_port(phy, pl->sfp_cage_port);
+
/* Do the initial configuration */
return phylink_sfp_config_phy(pl, phy);
}
@@ -3962,6 +4167,7 @@ static void phylink_sfp_disconnect_phy(void *upstream,
struct phy_device *phydev)
{
phylink_disconnect_phy(upstream);
+ phy_set_upstream_port(phydev, NULL);
}
static const struct sfp_upstream_ops sfp_phylink_ops = {
@@ -4376,6 +4582,11 @@ void phylink_mii_c45_pcs_get_state(struct mdio_device *pcs,
state->duplex = DUPLEX_FULL;
break;
+ case PHY_INTERFACE_MODE_25GBASER:
+ state->speed = SPEED_25000;
+ state->duplex = DUPLEX_FULL;
+ break;
+
default:
break;
}
diff --git a/drivers/net/phy/qcom/at803x.c b/drivers/net/phy/qcom/at803x.c
index cacbadf1f484..09105f1e3737 100644
--- a/drivers/net/phy/qcom/at803x.c
+++ b/drivers/net/phy/qcom/at803x.c
@@ -1019,10 +1019,10 @@ static int ipq5018_cable_test_start(struct phy_device *phydev)
return 0;
}
-static int ipq5018_config_init(struct phy_device *phydev)
+static int ipq5018_analog_init(struct phy_device *phydev)
{
struct ipq5018_priv *priv = phydev->priv;
- u16 val;
+ int val, ret;
/*
* set LDO efuse: first temporarily store ANA_DAC_FILTER value from
@@ -1030,39 +1030,66 @@ static int ipq5018_config_init(struct phy_device *phydev)
* is written to
*/
val = at803x_debug_reg_read(phydev, IPQ5018_PHY_DEBUG_ANA_DAC_FILTER);
- at803x_debug_reg_mask(phydev, IPQ5018_PHY_DEBUG_ANA_LDO_EFUSE,
- IPQ5018_PHY_DEBUG_ANA_LDO_EFUSE_MASK,
- IPQ5018_PHY_DEBUG_ANA_LDO_EFUSE_DEFAULT);
- at803x_debug_reg_write(phydev, IPQ5018_PHY_DEBUG_ANA_DAC_FILTER, val);
+ if (val < 0)
+ return val;
+
+ ret = at803x_debug_reg_mask(phydev, IPQ5018_PHY_DEBUG_ANA_LDO_EFUSE,
+ IPQ5018_PHY_DEBUG_ANA_LDO_EFUSE_MASK,
+ IPQ5018_PHY_DEBUG_ANA_LDO_EFUSE_DEFAULT);
+ if (ret)
+ return ret;
+
+ ret = at803x_debug_reg_write(phydev, IPQ5018_PHY_DEBUG_ANA_DAC_FILTER,
+ val);
+ if (ret)
+ return ret;
/* set 8023AZ EEE TX and RX timer values */
- phy_write_mmd(phydev, MDIO_MMD_PCS, IPQ5018_PHY_PCS_EEE_TX_TIMER,
- IPQ5018_PHY_PCS_EEE_TX_TIMER_VAL);
- phy_write_mmd(phydev, MDIO_MMD_PCS, IPQ5018_PHY_PCS_EEE_RX_TIMER,
- IPQ5018_PHY_PCS_EEE_RX_TIMER_VAL);
+ ret = phy_write_mmd(phydev, MDIO_MMD_PCS, IPQ5018_PHY_PCS_EEE_TX_TIMER,
+ IPQ5018_PHY_PCS_EEE_TX_TIMER_VAL);
+ if (ret)
+ return ret;
+
+ ret = phy_write_mmd(phydev, MDIO_MMD_PCS, IPQ5018_PHY_PCS_EEE_RX_TIMER,
+ IPQ5018_PHY_PCS_EEE_RX_TIMER_VAL);
+ if (ret)
+ return ret;
/* set MSE threshold values */
- phy_write_mmd(phydev, MDIO_MMD_PMAPMD, IPQ5018_PHY_MMD1_MSE_THRESH1,
- IPQ5018_PHY_MMD1_MSE_THRESH1_VAL);
- phy_write_mmd(phydev, MDIO_MMD_PMAPMD, IPQ5018_PHY_MMD1_MSE_THRESH2,
- IPQ5018_PHY_MMD1_MSE_THRESH2_VAL);
+ ret = phy_write_mmd(phydev, MDIO_MMD_PMAPMD,
+ IPQ5018_PHY_MMD1_MSE_THRESH1,
+ IPQ5018_PHY_MMD1_MSE_THRESH1_VAL);
+ if (ret)
+ return ret;
+
+ ret = phy_write_mmd(phydev, MDIO_MMD_PMAPMD,
+ IPQ5018_PHY_MMD1_MSE_THRESH2,
+ IPQ5018_PHY_MMD1_MSE_THRESH2_VAL);
+ if (ret)
+ return ret;
/* PHY DAC values are optional and only set in a PHY to PHY link architecture */
- if (priv->set_short_cable_dac) {
- /* setting MDAC (Multi-level Digital-to-Analog Converter) in MMD1 */
- phy_modify_mmd(phydev, MDIO_MMD_PMAPMD, IPQ5018_PHY_MMD1_MDAC,
- IPQ5018_PHY_DAC_MASK,
- FIELD_PREP(IPQ5018_PHY_DAC_MASK,
- IPQ5018_PHY_MMD1_MDAC_VAL));
-
- /* setting EDAC (Error-detection and Correction) in debug register */
- at803x_debug_reg_mask(phydev, IPQ5018_PHY_DEBUG_EDAC,
- IPQ5018_PHY_DAC_MASK,
- FIELD_PREP(IPQ5018_PHY_DAC_MASK,
- IPQ5018_PHY_DEBUG_EDAC_VAL));
- }
+ if (!priv->set_short_cable_dac)
+ return 0;
- return 0;
+ /* setting MDAC (Multi-level Digital-to-Analog Converter) in MMD1 */
+ ret = phy_modify_mmd(phydev, MDIO_MMD_PMAPMD, IPQ5018_PHY_MMD1_MDAC,
+ IPQ5018_PHY_DAC_MASK,
+ FIELD_PREP(IPQ5018_PHY_DAC_MASK,
+ IPQ5018_PHY_MMD1_MDAC_VAL));
+ if (ret)
+ return ret;
+
+ /* setting EDAC (Error-detection and Correction) in debug register */
+ return at803x_debug_reg_mask(phydev, IPQ5018_PHY_DEBUG_EDAC,
+ IPQ5018_PHY_DAC_MASK,
+ FIELD_PREP(IPQ5018_PHY_DAC_MASK,
+ IPQ5018_PHY_DEBUG_EDAC_VAL));
+}
+
+static int ipq5018_config_init(struct phy_device *phydev)
+{
+ return ipq5018_analog_init(phydev);
}
static void ipq5018_link_change_notify(struct phy_device *phydev)
@@ -1111,6 +1138,21 @@ static int ipq5018_probe(struct phy_device *phydev)
phydev->priv = priv;
+ /*
+ * The PHY starts autonegotiation as soon as it leaves reset. Apply the
+ * analog settings now instead of waiting for config_init() at attach
+ * time, and restart autonegotiation so that it uses them.
+ */
+ ret = ipq5018_analog_init(phydev);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to apply analog settings\n");
+
+ ret = genphy_restart_aneg(phydev);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to restart autonegotiation\n");
+
return 0;
}
diff --git a/drivers/net/phy/qcom/qca808x.c b/drivers/net/phy/qcom/qca808x.c
index 8eb51b1a006c..bab26e4c6140 100644
--- a/drivers/net/phy/qcom/qca808x.c
+++ b/drivers/net/phy/qcom/qca808x.c
@@ -87,6 +87,9 @@
#define QCA8081_PHY_ID 0x004dd101
+/* led_polarity_mode otherwise holds a PHY_LED_ACTIVE_* value */
+#define QCA808X_PHY_LED_UNSET -1
+
MODULE_DESCRIPTION("Qualcomm Atheros QCA808X PHY driver");
MODULE_AUTHOR("Matus Ujhelyi");
MODULE_LICENSE("GPL");
@@ -187,8 +190,7 @@ static int qca808x_probe(struct phy_device *phydev)
if (!priv)
return -ENOMEM;
- /* Init LED polarity mode to -1 */
- priv->led_polarity_mode = -1;
+ priv->led_polarity_mode = QCA808X_PHY_LED_UNSET;
phydev->priv = priv;
@@ -200,8 +202,8 @@ static int qca808x_config_init(struct phy_device *phydev)
struct qca808x_priv *priv = phydev->priv;
int ret;
- /* Default to LED Active High if active-low not in DT */
- if (priv->led_polarity_mode == -1) {
+ /* Set LED Active High unless active-low was requested in DT */
+ if (priv->led_polarity_mode != PHY_LED_ACTIVE_LOW) {
ret = phy_set_bits_mmd(phydev, MDIO_MMD_AN,
QCA808X_MMD7_LED_POLARITY_CTRL,
QCA808X_LED_ACTIVE_HIGH);
@@ -603,6 +605,9 @@ static int qca808x_led_polarity_set(struct phy_device *phydev, int index,
case PHY_LED_ACTIVE_LOW:
active_low = true;
break;
+ case PHY_LED_ACTIVE_HIGH:
+ active_low = false;
+ break;
default:
return -EINVAL;
}
@@ -612,7 +617,7 @@ static int qca808x_led_polarity_set(struct phy_device *phydev, int index,
* To detect this, check if last requested polarity mode
* match the new one.
*/
- if (priv->led_polarity_mode >= 0 &&
+ if (priv->led_polarity_mode != QCA808X_PHY_LED_UNSET &&
priv->led_polarity_mode != active_low) {
phydev_err(phydev, "PHY polarity is global. Mismatched polarity on different LED\n");
return -EINVAL;
diff --git a/drivers/net/phy/qcom/qca83xx.c b/drivers/net/phy/qcom/qca83xx.c
index bc70ed8efd86..2c4d085bacdb 100644
--- a/drivers/net/phy/qcom/qca83xx.c
+++ b/drivers/net/phy/qcom/qca83xx.c
@@ -92,6 +92,63 @@ static int qca83xx_probe(struct phy_device *phydev)
return 0;
}
+static int qca8337_read_status(struct phy_device *phydev)
+{
+ struct at803x_ss_mask ss_mask = {
+ .speed_mask = AT803X_SS_SPEED_MASK,
+ .speed_shift = __bf_shf(AT803X_SS_SPEED_MASK),
+ };
+ int ret, old_link = phydev->link;
+
+ ret = genphy_read_status(phydev);
+ if (ret)
+ return ret;
+
+ /* Keep the state resolved at link-up while the link stays up */
+ if (phydev->autoneg == AUTONEG_ENABLE && old_link && phydev->link)
+ goto out;
+
+ phydev->mdix = ETH_TP_MDI_INVALID;
+
+ /* genphy_read_status() derives speed and duplex from the
+ * advertisements, which is wrong after a SmartSpeed downshift.
+ * Use what the PHY resolved instead.
+ */
+ if (phydev->autoneg == AUTONEG_ENABLE && phydev->autoneg_complete &&
+ phydev->link) {
+ phydev->speed = SPEED_UNKNOWN;
+ phydev->duplex = DUPLEX_UNKNOWN;
+ phydev->pause = false;
+ phydev->asym_pause = false;
+
+ ret = at803x_read_specific_status(phydev, ss_mask);
+ if (ret)
+ return ret;
+
+ /* Report the link down until speed and duplex are resolved,
+ * so that the next poll reads them again. Also clear what
+ * genphy_read_status() read for an up link, as the
+ * PHY_HALTED path of the state machine does.
+ */
+ if (phydev->speed == SPEED_UNKNOWN ||
+ phydev->duplex == DUPLEX_UNKNOWN) {
+ phydev->link = 0;
+ phydev->duplex = DUPLEX_UNKNOWN;
+ phydev->mdix = ETH_TP_MDI_INVALID;
+ phydev->master_slave_state = MASTER_SLAVE_STATE_UNKNOWN;
+ linkmode_zero(phydev->lp_advertising);
+ } else {
+ phy_resolve_aneg_pause(phydev);
+ }
+ }
+
+out:
+ /* MDI-X configuration is not implemented */
+ phydev->mdix_ctrl = ETH_TP_MDI_INVALID;
+
+ return 0;
+}
+
static int qca83xx_config_init(struct phy_device *phydev)
{
u8 switch_revision;
@@ -220,6 +277,7 @@ static struct phy_driver qca83xx_driver[] = {
.flags = PHY_IS_INTERNAL,
.config_init = qca83xx_config_init,
.soft_reset = genphy_soft_reset,
+ .read_status = qca8337_read_status,
.get_sset_count = qca83xx_get_sset_count,
.get_strings = qca83xx_get_strings,
.get_stats = qca83xx_get_stats,
diff --git a/drivers/net/phy/realtek/realtek_main.c b/drivers/net/phy/realtek/realtek_main.c
index 177b62a7b2d1..4927ee838de2 100644
--- a/drivers/net/phy/realtek/realtek_main.c
+++ b/drivers/net/phy/realtek/realtek_main.c
@@ -19,7 +19,6 @@
#include <linux/delay.h>
#include <linux/clk.h>
#include <linux/string_choices.h>
-#include <net/phy/realtek_phy.h>
#include "../phylib.h"
#include "realtek.h"
@@ -240,6 +239,16 @@
#define RTL_PHYSR_MASTER BIT(11)
#define RTL_PHYSR_SPEED_MASK (RTL_PHYSR_SPEEDL | RTL_PHYSR_SPEEDH)
+/* MDI-X control and status of the RTL8365MB-VC internal PHY. The resolved
+ * MDI-X state is reported in RTL_PHYSR; the configuration lives in a
+ * chip-specific control register. Both are only known to apply to this model.
+ * On this chip a set select/resolved bit means MDI, not MDI-X (measured).
+ */
+#define RTL8365MB_VC_PHYCR1 0x18
+#define RTL8365MB_VC_PHYCR1_MDIX_FORCE BIT(9)
+#define RTL8365MB_VC_PHYCR1_MDI BIT(8)
+#define RTL8365MB_VC_PHYSR_MDI BIT(1)
+
#define RTL_MDIO_PCS_EEE_ABLE 0xa5c4
#define RTL_MDIO_AN_EEE_ADV 0xa5d0
#define RTL_MDIO_AN_EEE_LPABLE 0xa5d2
@@ -258,6 +267,8 @@
#define RTL_8251B 0x001cc862
#define RTL_8261C 0x001cc890
#define RTL_8261C_CG 0x001cc898
+#define RTL_8261CE_CG 0x001cc899
+#define RTL_8261D_VM 0x001cc89a
#define RTL8261C_CE_MODEL 0x00
#define RTL8261D_MODEL 0x81
@@ -286,11 +297,15 @@
#define FW_MAIN_MAGIC 0x52544C38
#define FW_SUB_MAGIC_8261C 0x32363143
+#define FW_SUB_MAGIC_8261D 0x32363144
#define RTL8261X_POLL_TIMEOUT_MS 100
#define RTL8261X_MAX_MMD_DEV 31
#define RTL8261C_CE_FW_NAME "rtl_nic/rtl8261c.bin"
+#define RTL8261D_FW_NAME "rtl_nic/rtl8261d.bin"
+
MODULE_FIRMWARE(RTL8261C_CE_FW_NAME);
+MODULE_FIRMWARE(RTL8261D_FW_NAME);
enum rtl8261x_fw_op {
OP_WRITE = 0x00, /* Write */
@@ -342,6 +357,7 @@ struct rtl821x_priv {
struct rtl8261x_priv {
const char *fw_name;
+ u32 fw_sub_magic;
bool fw_loaded;
};
@@ -411,10 +427,13 @@ static int rtl8261x_probe(struct phy_device *phydev)
switch (sub_phy_id) {
case RTL8261C_CE_MODEL:
priv->fw_name = RTL8261C_CE_FW_NAME;
+ priv->fw_sub_magic = FW_SUB_MAGIC_8261C;
phydev_info(phydev, "RTL8261C detected (sub_id 0x%02x)\n", sub_phy_id);
break;
case RTL8261D_MODEL:
+ priv->fw_name = RTL8261D_FW_NAME;
+ priv->fw_sub_magic = FW_SUB_MAGIC_8261D;
phydev_info(phydev, "RTL8261D detected (sub_id 0x%02x)\n", sub_phy_id);
break;
@@ -473,6 +492,7 @@ static int rtl8261x_read_status(struct phy_device *phydev)
static int rtl8261x_verify_firmware(struct phy_device *phydev, const struct firmware *fw)
{
+ struct rtl8261x_priv *priv = phydev->priv;
const struct rtl8261x_fw_header *hdr;
u32 main_magic, sub_magic;
u32 calc_crc, file_crc;
@@ -493,7 +513,7 @@ static int rtl8261x_verify_firmware(struct phy_device *phydev, const struct firm
}
sub_magic = le32_to_cpu(hdr->sub_magic);
- if (sub_magic != FW_SUB_MAGIC_8261C) {
+ if (sub_magic != priv->fw_sub_magic) {
phydev_err(phydev, "Invalid sub magic: 0x%08x\n", sub_magic);
return -EINVAL;
}
@@ -549,8 +569,12 @@ static int rtl8261x_fw_execute_entry(struct phy_device *phydev,
switch (entry->type) {
case OP_WRITE:
- ret = phy_modify_mmd(phydev, dev, addr,
- GENMASK(msb, lsb), (value << lsb) & GENMASK(msb, lsb));
+ if (msb != 15 || lsb != 0)
+ ret = phy_modify_mmd(phydev, dev, addr, GENMASK(msb, lsb),
+ (value << lsb) & GENMASK(msb, lsb));
+ else
+ ret = phy_write_mmd(phydev, dev, addr, value);
+
if (ret)
return ret;
break;
@@ -1375,7 +1399,7 @@ static int rtl821x_resume(struct phy_device *phydev)
msleep(20);
- return 0;
+ return rtl8211f_config_rgmii_delay(phydev);
}
static int rtl8211f_resume(struct phy_device *phydev)
@@ -2696,10 +2720,35 @@ exit:
return ret;
}
-static int rtl8224_mdi_config_order(struct phy_device *phydev)
+static int rtl8224_get_base_addr(struct phy_device *phydev, u32 *base_addr)
+{
+ struct device *dev = &phydev->mdio.dev;
+ struct device_node *np;
+ int ret;
+
+ *base_addr = phydev->mdio.addr & ~3;
+ np = of_get_parent(dev->of_node);
+ if (of_node_name_eq(np, "ethernet-phy-package")) {
+ ret = of_property_read_u32(np, "reg", base_addr);
+ of_node_put(np);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Missing or invalid PHY package reg\n");
+
+ if (phydev->mdio.addr < *base_addr ||
+ phydev->mdio.addr - *base_addr >= 4)
+ return dev_err_probe(dev, -EINVAL,
+ "PHY address outside RTL8224 package\n");
+ } else {
+ of_node_put(np);
+ }
+
+ return 0;
+}
+
+static int rtl8224_mdi_config_order(struct phy_device *phydev, u8 port_offset)
{
struct device_node *np = phydev->mdio.dev.of_node;
- u8 port_offset = phydev->mdio.addr & 3;
u32 order = 0;
int ret;
@@ -2721,10 +2770,10 @@ static int rtl8224_mdi_config_order(struct phy_device *phydev)
order ? BIT(port_offset) : 0);
}
-static int rtl8224_mdi_config_polarity(struct phy_device *phydev)
+static int rtl8224_mdi_config_polarity(struct phy_device *phydev, u8 port_offset)
{
struct device_node *np = phydev->mdio.dev.of_node;
- u8 offset = (phydev->mdio.addr & 3) * 4;
+ u8 offset = port_offset * 4;
u32 polarity = 0;
int ret;
@@ -2748,20 +2797,33 @@ static int rtl8224_mdi_config_polarity(struct phy_device *phydev)
static int rtl8224_config_init(struct phy_device *phydev)
{
+ u32 base_addr;
+ u8 port_offset;
int ret;
- ret = rtl8224_mdi_config_order(phydev);
+ ret = rtl8224_get_base_addr(phydev, &base_addr);
if (ret)
return ret;
- return rtl8224_mdi_config_polarity(phydev);
+ port_offset = phydev->mdio.addr - base_addr;
+ ret = rtl8224_mdi_config_order(phydev, port_offset);
+ if (ret)
+ return ret;
+
+ return rtl8224_mdi_config_polarity(phydev, port_offset);
}
static int rtl8224_probe(struct phy_device *phydev)
{
- /* Chip exposes 4 ports, join all of them in the same package */
- return devm_phy_package_join(&phydev->mdio.dev, phydev,
- phydev->mdio.addr & ~3, 0);
+ struct device *dev = &phydev->mdio.dev;
+ u32 base_addr;
+ int ret;
+
+ ret = rtl8224_get_base_addr(phydev, &base_addr);
+ if (ret)
+ return ret;
+
+ return devm_phy_package_join(dev, phydev, base_addr, 0);
}
static bool rtlgen_supports_2_5gbps(struct phy_device *phydev)
@@ -3044,43 +3106,72 @@ static irqreturn_t rtl8221b_handle_interrupt(struct phy_device *phydev)
return IRQ_HANDLED;
}
-static int rtlgen_sfp_get_features(struct phy_device *phydev)
+static int rtl8365mb_config_mdix(struct phy_device *phydev)
{
- linkmode_set_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT,
- phydev->supported);
-
- /* set default mode */
- phydev->speed = SPEED_10000;
- phydev->duplex = DUPLEX_FULL;
+ u16 val;
- phydev->port = PORT_FIBRE;
+ switch (phydev->mdix_ctrl) {
+ case ETH_TP_MDI:
+ val = RTL8365MB_VC_PHYCR1_MDIX_FORCE |
+ RTL8365MB_VC_PHYCR1_MDI;
+ break;
+ case ETH_TP_MDI_X:
+ val = RTL8365MB_VC_PHYCR1_MDIX_FORCE;
+ break;
+ case ETH_TP_MDI_AUTO:
+ val = 0;
+ break;
+ default:
+ /* Leave the hardware configuration alone until user space
+ * asks for a specific mode.
+ */
+ return 0;
+ }
- return 0;
+ return phy_modify_changed(phydev, RTL8365MB_VC_PHYCR1,
+ RTL8365MB_VC_PHYCR1_MDIX_FORCE |
+ RTL8365MB_VC_PHYCR1_MDI, val);
}
-static int rtlgen_sfp_read_status(struct phy_device *phydev)
+static int rtl8365mb_config_aneg(struct phy_device *phydev)
{
- int val, err;
+ int ret;
- err = genphy_update_link(phydev);
- if (err)
- return err;
+ ret = rtl8365mb_config_mdix(phydev);
+ if (ret < 0)
+ return ret;
- if (!phydev->link)
- return 0;
+ /* The pair assignment is only evaluated while the link is brought up,
+ * so renegotiate if the crossover configuration changed.
+ */
+ return __genphy_config_aneg(phydev, ret);
+}
- val = phy_read(phydev, RTL_PHYSR);
- if (val < 0)
- return val;
+static int rtl8365mb_read_status(struct phy_device *phydev)
+{
+ int ret;
- rtlgen_decode_physr(phydev, val);
+ ret = phy_read(phydev, RTL8365MB_VC_PHYCR1);
+ if (ret < 0)
+ return ret;
- return 0;
-}
+ if (ret & RTL8365MB_VC_PHYCR1_MDIX_FORCE) {
+ if (ret & RTL8365MB_VC_PHYCR1_MDI)
+ phydev->mdix_ctrl = ETH_TP_MDI;
+ else
+ phydev->mdix_ctrl = ETH_TP_MDI_X;
+ } else {
+ phydev->mdix_ctrl = ETH_TP_MDI_AUTO;
+ }
-static int rtlgen_sfp_config_aneg(struct phy_device *phydev)
-{
- return 0;
+ ret = phy_read(phydev, RTL_PHYSR);
+ if (ret < 0)
+ return ret;
+
+ phydev->mdix = (ret & RTL8365MB_VC_PHYSR_MDI) ? ETH_TP_MDI :
+ ETH_TP_MDI_X;
+
+ return genphy_read_status(phydev);
}
static struct phy_driver realtek_drvs[] = {
@@ -3333,20 +3424,6 @@ static struct phy_driver realtek_drvs[] = {
.read_mmd = rtl822x_read_mmd,
.write_mmd = rtl822x_write_mmd,
}, {
- PHY_ID_MATCH_EXACT(PHY_ID_RTL_DUMMY_SFP),
- .name = "Realtek SFP PHY Mode",
- .flags = PHY_IS_INTERNAL,
- .probe = rtl822x_probe,
- .get_features = rtlgen_sfp_get_features,
- .config_aneg = rtlgen_sfp_config_aneg,
- .read_status = rtlgen_sfp_read_status,
- .suspend = genphy_suspend,
- .resume = rtlgen_resume,
- .read_page = rtl821x_read_page,
- .write_page = rtl821x_write_page,
- .read_mmd = rtl822x_read_mmd,
- .write_mmd = rtl822x_write_mmd,
- }, {
PHY_ID_MATCH_EXACT(0x001ccad0),
.name = "RTL8224 2.5Gbps PHY",
.flags = PHY_POLL_CABLE_TEST,
@@ -3388,9 +3465,13 @@ static struct phy_driver realtek_drvs[] = {
}, {
PHY_ID_MATCH_EXACT(0x001cc942),
.name = "RTL8365MB-VC Gigabit Ethernet",
+ .read_mmd = genphy_read_mmd_c45,
+ .write_mmd = genphy_write_mmd_c45,
/* Interrupt handling analogous to RTL8366RB */
.config_intr = genphy_no_config_intr,
.handle_interrupt = genphy_handle_interrupt_no_ack,
+ .config_aneg = rtl8365mb_config_aneg,
+ .read_status = rtl8365mb_read_status,
.suspend = genphy_suspend,
.resume = genphy_resume,
}, {
@@ -3413,6 +3494,32 @@ static struct phy_driver realtek_drvs[] = {
.soft_reset = genphy_c45_pma_soft_reset,
.suspend = genphy_c45_pma_suspend,
.resume = genphy_c45_pma_resume,
+ }, {
+ PHY_ID_MATCH_EXACT(RTL_8261CE_CG),
+ .name = "Realtek RTL8261CE 10Gbps PHY",
+ .probe = rtl8261x_probe,
+ .config_init = rtl8261x_config_init,
+ .get_features = rtl8261x_get_features,
+ .config_aneg = rtl8261x_config_aneg,
+ .read_status = rtl8261x_read_status,
+ .config_intr = rtl8261x_config_intr,
+ .handle_interrupt = rtl8261x_handle_interrupt,
+ .soft_reset = genphy_c45_pma_soft_reset,
+ .suspend = genphy_c45_pma_suspend,
+ .resume = genphy_c45_pma_resume,
+ }, {
+ PHY_ID_MATCH_EXACT(RTL_8261D_VM),
+ .name = "Realtek RTL8261D_VM 10Gbps PHY",
+ .probe = rtl8261x_probe,
+ .config_init = rtl8261x_config_init,
+ .get_features = rtl8261x_get_features,
+ .config_aneg = rtl8261x_config_aneg,
+ .read_status = rtl8261x_read_status,
+ .config_intr = rtl8261x_config_intr,
+ .handle_interrupt = rtl8261x_handle_interrupt,
+ .soft_reset = genphy_c45_pma_soft_reset,
+ .suspend = genphy_c45_pma_suspend,
+ .resume = genphy_c45_pma_resume,
},
};
diff --git a/drivers/net/phy/sfp.c b/drivers/net/phy/sfp.c
index 2ec91466acdf..85e57a16bb52 100644
--- a/drivers/net/phy/sfp.c
+++ b/drivers/net/phy/sfp.c
@@ -432,6 +432,19 @@ static void sfp_fixup_rollball_wait4s(struct sfp *sfp)
sfp->module_t_wait = msecs_to_jiffies(4000);
}
+static void sfp_fixup_xikestor_2_5g(struct sfp *sfp)
+{
+ sfp_fixup_rollball(sfp);
+
+ /* This module does not immediately respond to Rollball commands. Add
+ * a small delay to avoid unnecessary PHY access attempts. Note that
+ * the delay should not be too long, as the RTL8221B-VB-CG PHY inside
+ * breaks when reading some registers from MMD 30 if it has already
+ * established a link (which takes about 4 seconds after reset).
+ */
+ sfp->module_t_wait = msecs_to_jiffies(1000);
+}
+
static void sfp_fixup_fs_10gt(struct sfp *sfp)
{
sfp_fixup_10gbaset_30m(sfp);
@@ -581,17 +594,17 @@ static const struct sfp_quirk sfp_quirks[] = {
// can operate at 2500base-X, but reports 1000BASE-LX / 1300MBd in its
// EEPROM
SFP_QUIRK("Hisense-Leox", "LXT-010S-H", sfp_quirk_2500basex,
- sfp_fixup_ignore_tx_fault),
+ sfp_fixup_ignore_tx_fault_and_los),
// Hisense ZNID-GPON-2311NA can operate at 2500base-X, but reports
// 1000BASE-LX / 1300MBd in its EEPROM
SFP_QUIRK("Hisense", "ZNID-GPON-2311NA", sfp_quirk_2500basex,
- sfp_fixup_ignore_tx_fault),
+ sfp_fixup_ignore_tx_fault_and_los),
// HSGQ HSGQ-XPON-Stick can operate at 2500base-X, but reports
// 1000BASE-LX / 1300MBd in its EEPROM
SFP_QUIRK("HSGQ", "HSGQ-XPON-Stick", sfp_quirk_2500basex,
- sfp_fixup_ignore_tx_fault),
+ sfp_fixup_ignore_tx_fault_and_los),
// Lantech 8330-262D-E and 8330-265D can operate at 2500base-X, but
// incorrectly report 2500MBd NRZ in their EEPROM.
@@ -632,10 +645,14 @@ static const struct sfp_quirk sfp_quirks[] = {
// match the part as a prefix.
SFP_QUIRK_F_PREFIX("OEM", "XGSPONST2001", sfp_fixup_potron),
+ SFP_QUIRK_S("TP-LINK", "TL-SM410U", sfp_quirk_oem_2_5g),
+
SFP_QUIRK_F("Turris", "RTSFP-2.5G", sfp_fixup_rollball),
SFP_QUIRK_F("Turris", "RTSFP-10", sfp_fixup_rollball),
SFP_QUIRK_F("Turris", "RTSFP-10G", sfp_fixup_rollball),
+ SFP_QUIRK_F("XikeStor", "SKT-2.5G-100M", sfp_fixup_xikestor_2_5g),
+
SFP_QUIRK_S("ZOERAX", "SFP-2.5G-T", sfp_quirk_oem_2_5g),
};
diff --git a/drivers/net/phy/xpowers/Makefile b/drivers/net/phy/xpowers/Makefile
new file mode 100644
index 000000000000..ae977a795832
--- /dev/null
+++ b/drivers/net/phy/xpowers/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+xpowers-acx00-y := acx00.o ac200.o ac300.o
+obj-$(CONFIG_XPOWERS_ACX00_PHY) += xpowers-acx00.o
diff --git a/drivers/net/phy/xpowers/ac200.c b/drivers/net/phy/xpowers/ac200.c
new file mode 100644
index 000000000000..afdd2a3dfbe4
--- /dev/null
+++ b/drivers/net/phy/xpowers/ac200.c
@@ -0,0 +1,314 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * X-Powers AC200 Ethernet PHY package backend
+ *
+ * Copyright (c) 2022 Arm Ltd. (Andre Przywara <andre.przywara@arm.com>)
+ * Copyright (C) 2026 James Hilliard <james.hilliard1@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/i2c.h>
+#include <linux/of.h>
+#include <linux/phy.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/slab.h>
+
+#include "acx00.h"
+
+#define AC200_EPHY_BPS_EFFUSE_OFFSET 3
+#define AC200_EPHY_CLK_RATE_24_MHZ 24000000
+#define AC200_EPHY_CLK_RATE_27_MHZ 27000000
+
+#define AC200_SYS_EPHY_CTL0_REG 0x0014
+#define AC200_EPHY_RESET_DEASSERT BIT(0)
+#define AC200_EPHY_SYSCLK_ENABLE BIT(1)
+
+#define AC200_SYS_EPHY_CTL1_REG 0x0016
+#define AC200_EPHY_MII_IO_ENABLE BIT(0)
+
+/* AC200-internal copy of the Ethernet PHY calibration eFuse. */
+#define AC200_EFUSE_EPHY_REG 0x8004
+
+#define AC200_EPHY_CTL_REG 0x6000
+#define AC200_EPHY_SHUTDOWN BIT(0)
+#define AC200_EPHY_CLK_SEL_24_MHZ BIT(2)
+#define AC200_EPHY_PHY_ADDR_MASK GENMASK(8, 4)
+#define AC200_EPHY_RMII_SEL BIT(11)
+#define AC200_EPHY_BPS_EFFUSE_MASK GENMASK(15, 12)
+
+struct ac200_ephy_ctl {
+ struct acx00_ephy_control control;
+ struct regmap *regmap;
+ struct regulator *vcc;
+ struct device *dev;
+ u16 ephy_ctl;
+ unsigned int phy_addr;
+ phy_interface_t interface;
+ bool supply_enabled;
+ bool powered;
+};
+
+static u16 ac200_ephy_ctl_config(const struct ac200_ephy_ctl *priv)
+{
+ return priv->ephy_ctl |
+ (priv->interface == PHY_INTERFACE_MODE_RMII ?
+ AC200_EPHY_RMII_SEL : 0) |
+ FIELD_PREP(AC200_EPHY_PHY_ADDR_MASK, priv->phy_addr);
+}
+
+static int ac200_ephy_ctl_write(struct ac200_ephy_ctl *priv,
+ unsigned int reg, u16 value)
+{
+ int ret;
+
+ ret = regmap_write(priv->regmap, reg, value);
+ if (ret)
+ dev_err(priv->dev, "failed to write register %#x: %pe\n",
+ reg, ERR_PTR(ret));
+
+ return ret;
+}
+
+static int ac200_ephy_ctl_disable(struct ac200_ephy_ctl *priv)
+{
+ int err;
+ int ret = 0;
+
+ if (priv->powered) {
+ ret = ac200_ephy_ctl_write(priv, AC200_EPHY_CTL_REG,
+ ac200_ephy_ctl_config(priv) |
+ AC200_EPHY_SHUTDOWN);
+ err = ac200_ephy_ctl_write(priv, AC200_SYS_EPHY_CTL1_REG, 0);
+ if (!ret)
+ ret = err;
+ err = ac200_ephy_ctl_write(priv, AC200_SYS_EPHY_CTL0_REG, 0);
+ if (!ret)
+ ret = err;
+
+ priv->powered = false;
+ }
+
+ if (priv->supply_enabled) {
+ err = regulator_disable(priv->vcc);
+ if (!err)
+ priv->supply_enabled = false;
+ else if (!ret)
+ ret = err;
+ }
+
+ return ret;
+}
+
+static int ac200_ephy_ctl_power_off(struct acx00_ephy_control *control)
+{
+ struct ac200_ephy_ctl *priv =
+ container_of(control, struct ac200_ephy_ctl, control);
+
+ return ac200_ephy_ctl_disable(priv);
+}
+
+static int
+ac200_ephy_ctl_set_interface(struct acx00_ephy_control *control,
+ phy_interface_t interface)
+{
+ struct ac200_ephy_ctl *priv =
+ container_of(control, struct ac200_ephy_ctl, control);
+ int ret = 0;
+
+ if (interface != PHY_INTERFACE_MODE_MII &&
+ interface != PHY_INTERFACE_MODE_RMII)
+ return -EINVAL;
+
+ if (priv->interface == interface)
+ return 0;
+
+ if (priv->powered)
+ ret = regmap_update_bits(priv->regmap, AC200_EPHY_CTL_REG,
+ AC200_EPHY_RMII_SEL,
+ interface == PHY_INTERFACE_MODE_RMII ?
+ AC200_EPHY_RMII_SEL : 0);
+ if (!ret)
+ priv->interface = interface;
+
+ return ret;
+}
+
+static int ac200_ephy_ctl_power_on(struct acx00_ephy_control *control,
+ unsigned int phy_addr)
+{
+ struct ac200_ephy_ctl *priv =
+ container_of(control, struct ac200_ephy_ctl, control);
+ u16 ephy_ctl;
+ int ret;
+
+ if (phy_addr > FIELD_MAX(AC200_EPHY_PHY_ADDR_MASK))
+ return -EINVAL;
+
+ if (priv->powered && priv->phy_addr == phy_addr)
+ return 0;
+
+ if (priv->powered) {
+ ret = ac200_ephy_ctl_disable(priv);
+ if (ret)
+ return ret;
+ }
+ priv->phy_addr = phy_addr;
+
+ if (!priv->supply_enabled) {
+ ret = regulator_enable(priv->vcc);
+ if (ret)
+ return ret;
+ priv->supply_enabled = true;
+ }
+
+ ephy_ctl = ac200_ephy_ctl_config(priv);
+
+ /*
+ * Start from a disabled state before applying the configuration. AC200
+ * specifies no additional delay between these controls. The companion
+ * MFD provider has completed its input-clock startup delay before
+ * publishing the regmap.
+ */
+ ret = ac200_ephy_ctl_write(priv, AC200_SYS_EPHY_CTL0_REG, 0);
+ if (ret)
+ goto err_disable;
+
+ ret = ac200_ephy_ctl_write(priv, AC200_SYS_EPHY_CTL1_REG,
+ AC200_EPHY_MII_IO_ENABLE);
+ if (ret)
+ goto err_disable;
+
+ ret = ac200_ephy_ctl_write(priv, AC200_EPHY_CTL_REG,
+ ephy_ctl | AC200_EPHY_SHUTDOWN);
+ if (ret)
+ goto err_disable;
+
+ ret = ac200_ephy_ctl_write(priv, AC200_SYS_EPHY_CTL0_REG,
+ AC200_EPHY_RESET_DEASSERT |
+ AC200_EPHY_SYSCLK_ENABLE);
+ if (ret)
+ goto err_disable;
+
+ ret = ac200_ephy_ctl_write(priv, AC200_EPHY_CTL_REG, ephy_ctl);
+ if (ret)
+ goto err_disable;
+
+ priv->powered = true;
+ return 0;
+
+err_disable:
+ /* Attempt every step of the shutdown sequence after a partial start. */
+ priv->powered = true;
+ ac200_ephy_ctl_disable(priv);
+
+ return ret;
+}
+
+struct acx00_ephy_control *
+ac200_ephy_ctl_create(struct phy_device *phydev,
+ struct device_node *package_node,
+ bool has_calibration, u8 calibration)
+{
+ struct device *dev = &phydev->mdio.dev;
+ unsigned int internal_calibration;
+ struct device_node *ac200_node;
+ struct ac200_ephy_ctl *priv;
+ struct i2c_client *client;
+ unsigned long clk_rate;
+ u8 bps_effuse_code;
+ struct clk *clk;
+ int ret;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return ERR_PTR(-ENOMEM);
+ priv->dev = dev;
+ priv->vcc = devm_of_regulator_get(dev, package_node, "vcc");
+ if (IS_ERR(priv->vcc))
+ return ERR_PTR(dev_err_probe(dev, PTR_ERR(priv->vcc),
+ "failed to get package supply\n"));
+
+ ac200_node = of_parse_phandle(package_node, "x-powers,ac200", 0);
+ if (!ac200_node)
+ return ERR_PTR(dev_err_probe(dev, -EINVAL,
+ "missing x-powers,ac200 reference\n"));
+ client = of_find_i2c_device_by_node(ac200_node);
+ of_node_put(ac200_node);
+ if (!client) {
+ ret = IS_ENABLED(CONFIG_I2C) ? -EPROBE_DEFER : -ENODEV;
+ return ERR_PTR(dev_err_probe(dev, ret,
+ "AC200 device is not registered\n"));
+ }
+
+ if (!device_link_add(dev, &client->dev,
+ DL_FLAG_AUTOREMOVE_CONSUMER)) {
+ ret = dev_err_probe(dev, -EINVAL,
+ "failed to link AC200 device\n");
+ goto out_put_client;
+ }
+
+ device_lock(&client->dev);
+ if (device_is_bound(&client->dev))
+ priv->regmap = dev_get_regmap(&client->dev, NULL);
+ device_unlock(&client->dev);
+ if (!priv->regmap) {
+ ret = dev_err_probe(dev, -EPROBE_DEFER,
+ "AC200 driver is not ready\n");
+ goto out_put_client;
+ }
+
+ if (!has_calibration) {
+ ret = regmap_read(priv->regmap, AC200_EFUSE_EPHY_REG,
+ &internal_calibration);
+ if (ret)
+ goto out_error;
+ calibration = internal_calibration;
+ }
+
+ /* The vendor driver supplies no transfer function beyond this offset. */
+ bps_effuse_code = (calibration + AC200_EPHY_BPS_EFFUSE_OFFSET) &
+ FIELD_MAX(AC200_EPHY_BPS_EFFUSE_MASK);
+ priv->ephy_ctl =
+ FIELD_PREP(AC200_EPHY_BPS_EFFUSE_MASK, bps_effuse_code);
+ /* EPHY_MODE and BIST_CLK_EN stay clear for normal operation. */
+
+ clk = clk_get(&client->dev, NULL);
+ if (IS_ERR(clk)) {
+ ret = PTR_ERR(clk);
+ goto out_error;
+ }
+
+ /* The bound AC200 provider enables and exclusively pins this rate. */
+ clk_rate = clk_get_rate(clk);
+ clk_put(clk);
+
+ switch (clk_rate) {
+ case AC200_EPHY_CLK_RATE_24_MHZ:
+ priv->ephy_ctl |= AC200_EPHY_CLK_SEL_24_MHZ;
+ break;
+ case AC200_EPHY_CLK_RATE_27_MHZ:
+ break;
+ default:
+ ret = dev_err_probe(dev, -EINVAL,
+ "unsupported AC200 clock rate %lu Hz\n",
+ clk_rate);
+ goto out_put_client;
+ }
+
+ priv->control.power_on = ac200_ephy_ctl_power_on;
+ priv->control.power_off = ac200_ephy_ctl_power_off;
+ priv->control.set_interface = ac200_ephy_ctl_set_interface;
+ /* MII is the reset default used until the MAC supplies its interface. */
+ priv->interface = PHY_INTERFACE_MODE_MII;
+ put_device(&client->dev);
+
+ return &priv->control;
+
+out_error:
+ ret = dev_err_probe(dev, ret, "failed to initialize AC200 control\n");
+out_put_client:
+ put_device(&client->dev);
+ return ERR_PTR(ret);
+}
diff --git a/drivers/net/phy/xpowers/ac300.c b/drivers/net/phy/xpowers/ac300.c
new file mode 100644
index 000000000000..aa560432a14c
--- /dev/null
+++ b/drivers/net/phy/xpowers/ac300.c
@@ -0,0 +1,387 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * X-Powers AC300 Ethernet PHY package backend
+ *
+ * Copyright (C) 2026 James Hilliard <james.hilliard1@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/of.h>
+#include <linux/phy.h>
+#include <linux/regulator/consumer.h>
+
+#include "../phylib.h"
+#include "acx00.h"
+
+#define AC300_EPHY_BGS_EFFUSE_OFFSET 3
+#define AC300_EPHY_CLK_RATE_24_MHZ 24000000
+#define AC300_EPHY_CLK_RATE_25_MHZ 25000000
+#define AC300_EPHY_CLK_RATE_27_MHZ 27000000
+#define AC300_SYS_CONTROL_REG 0x00
+#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_CLKIN_GATING_ENABLE BIT(2)
+#define AC300_EPHY_RESET_DEASSERT BIT(1)
+#define AC300_CHIP_RESET_DEASSERT BIT(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_MII_DRV_MASK GENMASK(11, 10)
+#define AC300_IO_DRV_LEVEL_2 2
+#define AC300_CLKIN_PAD_ENABLE BIT(4)
+#define AC300_EPHY_MII_IO_ENABLE BIT(0)
+
+#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_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_VALUE \
+ (FIELD_PREP(AC300_MDIO_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 phy_device *phydev;
+ struct regulator *vcc;
+ struct clk *clk;
+ u16 sys_control;
+ u16 ephy_config;
+ phy_interface_t interface;
+ bool package_known;
+ bool internal_dldo;
+ bool supply_enabled;
+ bool powered;
+};
+
+static unsigned int
+ac300_ephy_ctl_link_addr(const struct ac300_ephy_ctl *priv)
+{
+ return priv->phydev->mdio.addr;
+}
+
+static int ac300_ephy_ctl_read(struct ac300_ephy_ctl *priv, u32 regnum)
+{
+ int ret;
+
+ phy_lock_mdio_bus(priv->phydev);
+ ret = __phy_package_read(priv->phydev,
+ AC300_EPHY_CONTROL_ADDR_OFFSET, regnum);
+ phy_unlock_mdio_bus(priv->phydev);
+
+ return ret;
+}
+
+static int ac300_ephy_ctl_write(struct ac300_ephy_ctl *priv, u32 regnum,
+ u16 val)
+{
+ int ret;
+
+ phy_lock_mdio_bus(priv->phydev);
+ ret = __phy_package_write(priv->phydev,
+ AC300_EPHY_CONTROL_ADDR_OFFSET, regnum, val);
+ phy_unlock_mdio_bus(priv->phydev);
+
+ return ret;
+}
+
+static int ac300_ephy_ctl_modify(struct ac300_ephy_ctl *priv, u32 regnum,
+ u16 mask, u16 set)
+{
+ int ret;
+
+ phy_lock_mdio_bus(priv->phydev);
+ ret = __phy_package_read(priv->phydev,
+ AC300_EPHY_CONTROL_ADDR_OFFSET, regnum);
+ if (ret >= 0) {
+ u16 val = (ret & ~mask) | (set & mask);
+
+ ret = val == ret ? 0 :
+ __phy_package_write(priv->phydev,
+ AC300_EPHY_CONTROL_ADDR_OFFSET,
+ regnum, val);
+ }
+ phy_unlock_mdio_bus(priv->phydev);
+
+ return ret;
+}
+
+static u16 ac300_ephy_ctl_config(const struct ac300_ephy_ctl *priv)
+{
+ return priv->ephy_config |
+ (priv->interface == PHY_INTERFACE_MODE_RMII ?
+ AC300_EPHY_RMII_SEL : 0);
+}
+
+static u16 ac300_ephy_ctl_reset_value(const struct ac300_ephy_ctl *priv)
+{
+ /*
+ * A chip reset restores DLDOEN to one. Until PKG_STATUS has identified
+ * the supply arrangement, preserve any external-VDD setup left by the
+ * bootloader by asserting only the EPHY reset.
+ */
+ return !priv->package_known || !priv->internal_dldo ?
+ AC300_CHIP_RESET_DEASSERT : 0;
+}
+
+static int ac300_ephy_ctl_disable(struct ac300_ephy_ctl *priv)
+{
+ int err;
+ int ret = 0;
+
+ if (priv->powered) {
+ ret = ac300_ephy_ctl_write(priv, AC300_EPHY_CONFIG_REG,
+ ac300_ephy_ctl_config(priv) |
+ AC300_EPHY_SHUTDOWN);
+ err = ac300_ephy_ctl_write(priv, AC300_SYS_IO_REG, 0);
+ if (!ret)
+ ret = err;
+ err = ac300_ephy_ctl_write(priv, AC300_SYS_CONTROL_REG,
+ ac300_ephy_ctl_reset_value(priv));
+ if (!ret)
+ ret = err;
+
+ clk_disable_unprepare(priv->clk);
+ priv->powered = false;
+ }
+
+ if (priv->supply_enabled) {
+ err = regulator_disable(priv->vcc);
+ if (!err)
+ priv->supply_enabled = false;
+ else if (!ret)
+ ret = err;
+ }
+
+ 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);
+
+ return ac300_ephy_ctl_disable(priv);
+}
+
+static int
+ac300_ephy_ctl_set_interface(struct acx00_ephy_control *control,
+ phy_interface_t interface)
+{
+ struct ac300_ephy_ctl *priv =
+ container_of(control, struct ac300_ephy_ctl, control);
+ int ret = 0;
+
+ if (interface != PHY_INTERFACE_MODE_MII &&
+ interface != PHY_INTERFACE_MODE_RMII)
+ return -EINVAL;
+
+ if (priv->interface == interface)
+ return 0;
+
+ if (priv->powered)
+ ret = ac300_ephy_ctl_modify(priv, AC300_EPHY_CONFIG_REG,
+ AC300_EPHY_RMII_SEL,
+ interface == PHY_INTERFACE_MODE_RMII ?
+ AC300_EPHY_RMII_SEL : 0);
+ if (!ret)
+ priv->interface = interface;
+
+ 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;
+ u16 reset_value;
+ int sys_control;
+ int ret;
+
+ if (phy_addr != ac300_ephy_ctl_link_addr(priv))
+ return -EINVAL;
+
+ if (priv->powered)
+ return 0;
+
+ if (!priv->supply_enabled) {
+ ret = regulator_enable(priv->vcc);
+ if (ret)
+ return ret;
+ priv->supply_enabled = true;
+
+ /* Wait for the power-on reset interval specified by the manual. */
+ fsleep(10000);
+ }
+
+ ret = clk_prepare_enable(priv->clk);
+ if (ret)
+ goto err_power_off;
+ priv->powered = true;
+
+ /* Keep the external-supply configuration across chip resets. */
+ reset_value = ac300_ephy_ctl_reset_value(priv);
+ ret = ac300_ephy_ctl_write(priv, AC300_SYS_CONTROL_REG, reset_value);
+ if (ret)
+ goto err_power_off;
+
+ /* The manual requires both resets to be released before the clocks. */
+ ret = ac300_ephy_ctl_write(priv, 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 = ac300_ephy_ctl_write(priv, AC300_SYS_CONTROL_REG,
+ priv->sys_control);
+ if (ret)
+ goto err_power_off;
+
+ sys_control = ac300_ephy_ctl_read(priv, AC300_SYS_CONTROL_REG);
+ if (sys_control < 0) {
+ ret = sys_control;
+ 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 = ac300_ephy_ctl_modify(priv, 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 = ac300_ephy_ctl_write(priv, AC300_SYS_IO_REG,
+ AC300_SYS_IO_VALUE);
+ if (ret)
+ goto err_power_off;
+
+ fsleep(10000);
+
+ ret = ac300_ephy_ctl_write(priv, AC300_EPHY_CONFIG_REG,
+ ac300_ephy_ctl_config(priv) |
+ AC300_EPHY_SHUTDOWN);
+ if (ret)
+ goto err_power_off;
+
+ fsleep(10000);
+
+ ret = ac300_ephy_ctl_write(priv, AC300_EPHY_CONFIG_REG,
+ ac300_ephy_ctl_config(priv));
+ if (ret)
+ goto err_power_off;
+
+ return 0;
+
+err_power_off:
+ ac300_ephy_ctl_disable(priv);
+
+ return ret;
+}
+
+static void ac300_ephy_clk_put(void *data)
+{
+ clk_put(data);
+}
+
+struct acx00_ephy_control *
+ac300_ephy_ctl_create(struct phy_device *phydev,
+ struct device_node *package_node, u8 calibration)
+{
+ struct device *dev = &phydev->mdio.dev;
+ struct ac300_ephy_ctl *priv;
+ unsigned long clk_rate;
+ u8 bgs_effuse_code;
+ int ret;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return ERR_PTR(-ENOMEM);
+ if (phydev->mdio.addr > FIELD_MAX(AC300_PACKAGE_PHY_ADDR_MASK))
+ return ERR_PTR(dev_err_probe(dev, -EINVAL,
+ "link address is outside the package range\n"));
+ priv->phydev = phydev;
+
+ priv->vcc = devm_of_regulator_get(dev, package_node, "vcc");
+ if (IS_ERR(priv->vcc))
+ return ERR_PTR(dev_err_probe(dev, PTR_ERR(priv->vcc),
+ "failed to get package supply\n"));
+
+ priv->clk = of_clk_get(package_node, 0);
+ if (IS_ERR(priv->clk))
+ return ERR_PTR(dev_err_probe(dev, PTR_ERR(priv->clk),
+ "failed to get input clock\n"));
+
+ ret = devm_add_action_or_reset(dev, ac300_ephy_clk_put, priv->clk);
+ if (ret)
+ return ERR_PTR(ret);
+
+ ret = devm_clk_rate_exclusive_get(dev, priv->clk);
+ if (ret)
+ return ERR_PTR(dev_err_probe(dev, ret,
+ "failed to lock clock rate\n"));
+
+ clk_rate = clk_get_rate(priv->clk);
+ switch (clk_rate) {
+ case AC300_EPHY_CLK_RATE_24_MHZ:
+ priv->sys_control = AC300_EPHY_CLK_SEL_24_MHZ;
+ break;
+ case AC300_EPHY_CLK_RATE_25_MHZ:
+ priv->sys_control = AC300_EPHY_CLK_SEL_25_MHZ;
+ break;
+ case AC300_EPHY_CLK_RATE_27_MHZ:
+ priv->sys_control = AC300_EPHY_CLK_SEL_27_MHZ;
+ break;
+ default:
+ return ERR_PTR(dev_err_probe(dev, -EINVAL,
+ "unsupported input clock rate %lu Hz\n",
+ clk_rate));
+ }
+ priv->sys_control |= AC300_SYS_CONTROL_ENABLE_BITS;
+
+ /* 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);
+ priv->ephy_config =
+ FIELD_PREP(AC300_EPHY_BGS_EFFUSE_MASK, bgs_effuse_code);
+ /* EPHY_MODE and BIST_CLK_EN stay clear for normal operation. */
+
+ priv->control.power_on = ac300_ephy_ctl_power_on;
+ priv->control.power_off = ac300_ephy_ctl_power_off;
+ priv->control.set_interface = ac300_ephy_ctl_set_interface;
+ /* MII is the reset default used until the MAC supplies its interface. */
+ priv->interface = PHY_INTERFACE_MODE_MII;
+
+ return &priv->control;
+}
diff --git a/drivers/net/phy/xpowers/acx00.c b/drivers/net/phy/xpowers/acx00.c
new file mode 100644
index 000000000000..d19ba6d8069a
--- /dev/null
+++ b/drivers/net/phy/xpowers/acx00.c
@@ -0,0 +1,633 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * X-Powers AC200/AC300 Ethernet PHY driver
+ *
+ * Copyright (C) 2019 Jernej Skrabec <jernej.skrabec@gmail.com>
+ * Copyright (C) 2026 James Hilliard <james.hilliard1@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/device.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/nvmem-consumer.h>
+#include <linux/of.h>
+#include <linux/phy.h>
+#include <linux/slab.h>
+
+#include "../phylib.h"
+#include "acx00.h"
+
+#define ACX00_EPHY_ID 0x00441400
+
+#define ACX00_EPHY_CONFIG_CALIBRATION_MASK GENMASK(3, 0)
+#define ACX00_EPHY_CONFIG_VARIANT_AC300 BIT(8)
+#define ACX00_EPHY_CONFIG_CALIBRATION_LOW BIT(9)
+
+#define ACX00_PAGE_SELECT_REG 0x1f
+#define ACX00_PAGE_SELECT_MASK GENMASK(12, 8)
+#define ACX00_PAGE_0 0
+#define ACX00_PAGE_1 1
+#define ACX00_PAGE_2 2
+#define ACX00_PAGE_6 6
+#define ACX00_PAGE_8 8
+
+#define ACX00_PAGE0_GLOBAL_CONFIG_REG 0x13
+#define ACX00_PAGE0_XMII_RX_CLOCK_INVERT BIT(12)
+#define ACX00_PAGE0_MDI_MODE_MASK GENMASK(1, 0)
+#define ACX00_PAGE0_MDI_MODE_AUTO 2
+
+#define ACX00_PAGE1_APS_CONTROL_REG 0x12
+#define ACX00_PAGE1_APS_DISABLED_4S_VALUE 0x4824
+#define ACX00_PAGE1_UAPS_CONTROL_REG 0x13
+#define ACX00_PAGE1_UAPS_ENABLE BIT(15)
+#define ACX00_PAGE1_INTELLIGENT_EEE_CONTROL_REG 0x17
+#define ACX00_PAGE1_INTELLIGENT_EEE_ENABLE BIT(3)
+
+#define ACX00_PAGE2_TX_DATA_CONTROL_REG 0x18
+#define ACX00_PAGE2_10BT_FIR_SELECT_MASK GENMASK(14, 12)
+#define ACX00_PAGE2_10BT_FIR_SELECT_DEFAULT 0
+
+#define ACX00_PAGE6_ADC_CONTROL_REG 0x10
+#define ACX00_PAGE6_ADC_CONTROL_LOW_CAL_VALUE 0x5523
+#define ACX00_PAGE6_AFE_RX_CONTROL_REG 0x13
+#define ACX00_PAGE6_AFE_RX_CONTROL_VALUE 0xf000
+#define ACX00_PAGE6_AFE_EQ_RX_DETECT_CONTROL_REG 0x14
+#define AC200_PAGE6_AFE_EQ_RX_DETECT_VALUE 0x708f
+#define AC300_PAGE6_AFE_EQ_RX_DETECT_VALUE 0x708b
+#define ACX00_PAGE6_AFE_EQ_RX_DETECT_LOW_CAL_VALUE 0x7809
+#define ACX00_PAGE6_TX_LEVEL_REG 0x15
+#define ACX00_PAGE6_TX_LEVEL_100M_MASK GENMASK(15, 8)
+#define ACX00_PAGE6_TX_LEVEL_10M_MASK GENMASK(7, 0)
+#define ACX00_PAGE6_TX_LEVEL_VALUE(_100m, _10m) \
+ (FIELD_PREP(ACX00_PAGE6_TX_LEVEL_100M_MASK, (_100m)) | \
+ FIELD_PREP(ACX00_PAGE6_TX_LEVEL_10M_MASK, (_10m)))
+#define ACX00_PAGE6_TX_LEVEL_DEFAULT_VALUE \
+ ACX00_PAGE6_TX_LEVEL_VALUE(0x15, 0x30)
+#define ACX00_PAGE6_TX_LEVEL_LOW_CAL_VALUE \
+ ACX00_PAGE6_TX_LEVEL_VALUE(0x35, 0x33)
+
+#define ACX00_PAGE8_AFE_CONTROL_REG 0x18
+#define ACX00_PAGE8_AFE_CONTROL_VALUE 0x00bc
+#define ACX00_PAGE8_AUTO_CAL_CONTROL_REG 0x1d
+#define ACX00_PAGE8_AUTO_CAL_TX_LEVEL_ADJUST_BYPASS BIT(11)
+#define ACX00_PAGE8_AUTO_CAL_LOW_CAL_OPAQUE_BITS 0x0044
+#define ACX00_PAGE8_AUTO_CAL_LOW_VALUE \
+ (ACX00_PAGE8_AUTO_CAL_TX_LEVEL_ADJUST_BYPASS | \
+ ACX00_PAGE8_AUTO_CAL_LOW_CAL_OPAQUE_BITS)
+
+/*
+ * Another integration of this exact-ID PHY documents its digital vendor
+ * register map, which also matches the observed ACx00 reset values. The ACx00
+ * analog-page field encodings remain unpublished, so keep those as opaque
+ * vendor initialization values instead of inventing bit definitions.
+ */
+
+struct acx00_ephy_priv {
+ struct phy_device *phydev;
+ struct acx00_ephy_control *control;
+ /* Serializes package state and multi-register power sequences. */
+ struct mutex state_lock;
+ bool is_ac300;
+ bool use_low_calibration_tuning;
+ bool xmii_rx_clock_inverted;
+};
+
+static int acx00_ephy_read_page(struct phy_device *phydev)
+{
+ int ret;
+
+ ret = __phy_read(phydev, ACX00_PAGE_SELECT_REG);
+ if (ret < 0)
+ return ret;
+
+ return FIELD_GET(ACX00_PAGE_SELECT_MASK, ret);
+}
+
+static int acx00_ephy_write_page(struct phy_device *phydev, int page)
+{
+ return __phy_write(phydev, ACX00_PAGE_SELECT_REG,
+ FIELD_PREP(ACX00_PAGE_SELECT_MASK, page));
+}
+
+static int acx00_ephy_control_power_on(struct acx00_ephy_priv *priv)
+{
+ lockdep_assert_held(&priv->state_lock);
+
+ return priv->control->power_on(priv->control,
+ priv->phydev->mdio.addr);
+}
+
+static int acx00_ephy_control_power_off(struct acx00_ephy_priv *priv)
+{
+ lockdep_assert_held(&priv->state_lock);
+
+ return priv->control->power_off(priv->control);
+}
+
+static int acx00_ephy_set_interface(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+
+ lockdep_assert_held(&priv->state_lock);
+
+ if (phydev->interface == PHY_INTERFACE_MODE_NA)
+ return 0;
+ if (phydev->interface != PHY_INTERFACE_MODE_MII &&
+ phydev->interface != PHY_INTERFACE_MODE_RMII)
+ return -EINVAL;
+
+ return priv->control->set_interface(priv->control,
+ phydev->interface);
+}
+
+static void acx00_ephy_control_power_off_warn(struct acx00_ephy_priv *priv)
+{
+ int ret;
+
+ ret = acx00_ephy_control_power_off(priv);
+ if (ret)
+ phydev_warn(priv->phydev,
+ "failed to power off control block: %pe\n",
+ ERR_PTR(ret));
+}
+
+static void acx00_ephy_control_release(void *data)
+{
+ struct acx00_ephy_priv *priv = data;
+
+ mutex_lock(&priv->state_lock);
+ acx00_ephy_control_power_off_warn(priv);
+ mutex_unlock(&priv->state_lock);
+}
+
+static int acx00_ephy_read_nvmem_u16(struct device_node *node,
+ const char *name, size_t min_len,
+ u16 *value)
+{
+ struct nvmem_cell *cell;
+ size_t i;
+ size_t len;
+ u8 *buf;
+ u16 val = 0;
+
+ cell = of_nvmem_cell_get(node, name);
+ if (IS_ERR(cell))
+ return PTR_ERR(cell);
+
+ buf = nvmem_cell_read(cell, &len);
+ nvmem_cell_put(cell);
+ if (IS_ERR(buf))
+ return PTR_ERR(buf);
+ if (len < min_len || len > sizeof(*value)) {
+ kfree(buf);
+ return len > sizeof(*value) ? -ERANGE : -EINVAL;
+ }
+
+ for (i = 0; i < len; i++)
+ val |= (u16)buf[i] << (8 * i);
+ kfree(buf);
+ *value = val;
+
+ return 0;
+}
+
+static int acx00_ephy_init_package(struct phy_device *phydev,
+ struct acx00_ephy_priv *priv)
+{
+ struct device *dev = &phydev->mdio.dev;
+ struct device_node *package_node;
+ bool selectable;
+ bool fixed_ac300;
+ bool has_configuration;
+ u8 calibration;
+ u32 base_addr;
+ u16 configuration = 0;
+ int ret;
+
+ package_node = of_get_parent(dev->of_node);
+ if (!package_node)
+ return -EINVAL;
+ if (!of_node_name_eq(package_node, "ethernet-phy-package")) {
+ ret = dev_err_probe(dev, -EINVAL,
+ "PHY is not in an Ethernet PHY package\n");
+ goto out_put_node;
+ }
+
+ ret = of_property_read_u32(package_node, "reg", &base_addr);
+ if (ret || base_addr != phydev->mdio.addr) {
+ ret = dev_err_probe(dev, -EINVAL,
+ "package and link PHY addresses differ\n");
+ goto out_put_node;
+ }
+
+ selectable = of_device_is_compatible(package_node,
+ "x-powers,acx00-ephy-package");
+ fixed_ac300 = of_device_is_compatible(package_node,
+ "x-powers,ac300-ephy-package");
+ if (!selectable && !fixed_ac300 &&
+ !of_device_is_compatible(package_node,
+ "x-powers,ac200-ephy-package")) {
+ ret = dev_err_probe(dev, -EINVAL,
+ "unsupported Ethernet PHY package\n");
+ goto out_put_node;
+ }
+
+ ret = devm_of_phy_package_join(dev, phydev, 0);
+ if (ret)
+ goto out_put_node;
+
+ has_configuration =
+ of_property_match_string(package_node, "nvmem-cell-names",
+ "configuration") >= 0;
+ if (has_configuration) {
+ ret = acx00_ephy_read_nvmem_u16(package_node, "configuration",
+ selectable || fixed_ac300 ? 2 : 1,
+ &configuration);
+ if (ret) {
+ ret = dev_err_probe(dev, ret,
+ "failed to read package configuration\n");
+ goto out_put_node;
+ }
+ } else if (selectable || fixed_ac300) {
+ ret = dev_err_probe(dev, -EINVAL,
+ "package configuration is required\n");
+ goto out_put_node;
+ }
+
+ if (selectable) {
+ priv->is_ac300 =
+ configuration & ACX00_EPHY_CONFIG_VARIANT_AC300;
+ } else {
+ priv->is_ac300 = fixed_ac300;
+ if (has_configuration &&
+ !!(configuration & ACX00_EPHY_CONFIG_VARIANT_AC300) !=
+ priv->is_ac300) {
+ ret = dev_err_probe(dev, -EINVAL,
+ "configuration does not match package\n");
+ goto out_put_node;
+ }
+ }
+
+ calibration = FIELD_GET(ACX00_EPHY_CONFIG_CALIBRATION_MASK,
+ configuration);
+
+ priv->use_low_calibration_tuning =
+ priv->is_ac300 &&
+ !!(configuration & ACX00_EPHY_CONFIG_CALIBRATION_LOW);
+ priv->xmii_rx_clock_inverted =
+ of_property_read_bool(package_node,
+ "x-powers,xmii-rx-clock-inverted");
+
+ if (priv->is_ac300)
+ priv->control =
+ ac300_ephy_ctl_create(phydev, package_node,
+ calibration);
+ else
+ priv->control =
+ ac200_ephy_ctl_create(phydev, package_node,
+ has_configuration,
+ calibration);
+ ret = PTR_ERR_OR_ZERO(priv->control);
+
+out_put_node:
+ of_node_put(package_node);
+ return ret;
+}
+
+static int acx00_ephy_disable_autonomous_eee(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+ int ret;
+
+ mutex_lock(&priv->state_lock);
+ ret = phy_modify_paged(phydev, ACX00_PAGE_1,
+ ACX00_PAGE1_INTELLIGENT_EEE_CONTROL_REG,
+ ACX00_PAGE1_INTELLIGENT_EEE_ENABLE, 0);
+ mutex_unlock(&priv->state_lock);
+
+ return ret;
+}
+
+static int acx00_ephy_probe(struct phy_device *phydev)
+{
+ struct device *dev = &phydev->mdio.dev;
+ struct acx00_ephy_priv *priv;
+ int ret;
+
+ if (!dev->of_node)
+ return -ENODEV;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->phydev = phydev;
+ mutex_init(&priv->state_lock);
+ ret = acx00_ephy_init_package(phydev, priv);
+ if (ret)
+ return ret;
+
+ phydev->priv = priv;
+ ret = devm_add_action_or_reset(dev, acx00_ephy_control_release, priv);
+ if (ret)
+ return ret;
+
+ mutex_lock(&priv->state_lock);
+ ret = acx00_ephy_control_power_on(priv);
+ mutex_unlock(&priv->state_lock);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to power on control block\n");
+
+ return 0;
+}
+
+static int acx00_ephy_soft_reset(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+ int ret;
+
+ mutex_lock(&priv->state_lock);
+
+ ret = acx00_ephy_set_interface(phydev);
+ if (ret)
+ goto out_power_off;
+
+ ret = acx00_ephy_control_power_on(priv);
+ if (ret)
+ goto out_power_off;
+
+ /* ACx00 can acknowledge reset in power-down without restarting. */
+ ret = genphy_resume(phydev);
+ if (ret)
+ goto out_power_off;
+
+ ret = genphy_soft_reset(phydev);
+ if (ret)
+ goto out_power_off;
+
+ goto out_unlock;
+
+out_power_off:
+ acx00_ephy_control_power_off_warn(priv);
+out_unlock:
+ mutex_unlock(&priv->state_lock);
+
+ return ret;
+}
+
+static int __acx00_ephy_config_init(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+ u16 afe_eq_rx_detect = priv->is_ac300 ?
+ AC300_PAGE6_AFE_EQ_RX_DETECT_VALUE :
+ AC200_PAGE6_AFE_EQ_RX_DETECT_VALUE;
+ u16 tx_level_value = ACX00_PAGE6_TX_LEVEL_DEFAULT_VALUE;
+ u16 global_config;
+ int oldpage;
+ int ret;
+
+ lockdep_assert_held(&priv->state_lock);
+
+ global_config = FIELD_PREP(ACX00_PAGE0_MDI_MODE_MASK,
+ ACX00_PAGE0_MDI_MODE_AUTO);
+ if (priv->xmii_rx_clock_inverted)
+ global_config |= ACX00_PAGE0_XMII_RX_CLOCK_INVERT;
+
+ ret = phy_modify_paged(phydev, ACX00_PAGE_0,
+ ACX00_PAGE0_GLOBAL_CONFIG_REG,
+ ACX00_PAGE0_XMII_RX_CLOCK_INVERT |
+ ACX00_PAGE0_MDI_MODE_MASK, global_config);
+ if (ret)
+ return ret;
+
+ if (priv->is_ac300 && priv->use_low_calibration_tuning) {
+ afe_eq_rx_detect =
+ ACX00_PAGE6_AFE_EQ_RX_DETECT_LOW_CAL_VALUE;
+ tx_level_value = ACX00_PAGE6_TX_LEVEL_LOW_CAL_VALUE;
+ }
+
+ oldpage = phy_select_page(phydev, ACX00_PAGE_1);
+ if (oldpage < 0)
+ return phy_restore_page(phydev, oldpage, oldpage);
+
+ ret = __phy_write(phydev, ACX00_PAGE1_APS_CONTROL_REG,
+ ACX00_PAGE1_APS_DISABLED_4S_VALUE);
+ if (ret)
+ goto out_restore_page;
+ ret = __phy_modify(phydev, ACX00_PAGE1_UAPS_CONTROL_REG,
+ ACX00_PAGE1_UAPS_ENABLE, 0);
+ if (ret)
+ goto out_restore_page;
+ ret = __phy_modify(phydev, ACX00_PAGE1_INTELLIGENT_EEE_CONTROL_REG,
+ ACX00_PAGE1_INTELLIGENT_EEE_ENABLE, 0);
+ if (ret)
+ goto out_restore_page;
+
+ ret = acx00_ephy_write_page(phydev, ACX00_PAGE_2);
+ if (ret)
+ goto out_restore_page;
+ ret = __phy_modify(phydev, ACX00_PAGE2_TX_DATA_CONTROL_REG,
+ ACX00_PAGE2_10BT_FIR_SELECT_MASK,
+ FIELD_PREP(ACX00_PAGE2_10BT_FIR_SELECT_MASK,
+ ACX00_PAGE2_10BT_FIR_SELECT_DEFAULT));
+ if (ret)
+ goto out_restore_page;
+
+ ret = acx00_ephy_write_page(phydev, ACX00_PAGE_6);
+ if (ret)
+ goto out_restore_page;
+ ret = __phy_write(phydev, ACX00_PAGE6_AFE_EQ_RX_DETECT_CONTROL_REG,
+ afe_eq_rx_detect);
+ if (ret)
+ goto out_restore_page;
+ ret = __phy_write(phydev, ACX00_PAGE6_AFE_RX_CONTROL_REG,
+ ACX00_PAGE6_AFE_RX_CONTROL_VALUE);
+ if (ret)
+ goto out_restore_page;
+ if (priv->is_ac300 && priv->use_low_calibration_tuning) {
+ ret = __phy_write(phydev, ACX00_PAGE6_ADC_CONTROL_REG,
+ ACX00_PAGE6_ADC_CONTROL_LOW_CAL_VALUE);
+ if (ret)
+ goto out_restore_page;
+ }
+ ret = __phy_write(phydev, ACX00_PAGE6_TX_LEVEL_REG, tx_level_value);
+ if (ret)
+ goto out_restore_page;
+
+ ret = acx00_ephy_write_page(phydev, ACX00_PAGE_8);
+ if (ret)
+ goto out_restore_page;
+ if (priv->is_ac300 && priv->use_low_calibration_tuning) {
+ ret = __phy_write(phydev, ACX00_PAGE8_AUTO_CAL_CONTROL_REG,
+ ACX00_PAGE8_AUTO_CAL_LOW_VALUE);
+ if (ret)
+ goto out_restore_page;
+ }
+ ret = __phy_write(phydev, ACX00_PAGE8_AFE_CONTROL_REG,
+ ACX00_PAGE8_AFE_CONTROL_VALUE);
+
+out_restore_page:
+ ret = phy_restore_page(phydev, oldpage, ret);
+ if (ret)
+ return ret;
+
+ /* Restore the standard EEE policy retained by phylib across resets. */
+ ret = genphy_c45_an_config_eee_aneg(phydev);
+ if (ret <= 0)
+ return ret;
+
+ return genphy_restart_aneg(phydev);
+}
+
+static int acx00_ephy_config_init(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+ int ret;
+
+ mutex_lock(&priv->state_lock);
+ ret = __acx00_ephy_config_init(phydev);
+ if (ret)
+ acx00_ephy_control_power_off_warn(priv);
+ mutex_unlock(&priv->state_lock);
+
+ return ret;
+}
+
+static int __acx00_ephy_power_on_and_resume(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+ int ret;
+
+ lockdep_assert_held(&phydev->lock);
+ lockdep_assert_held(&priv->state_lock);
+
+ ret = acx00_ephy_set_interface(phydev);
+ if (ret)
+ goto out_power_off;
+
+ ret = acx00_ephy_control_power_on(priv);
+ if (ret)
+ goto out_power_off;
+
+ ret = genphy_resume(phydev);
+ if (ret)
+ goto out_power_off;
+
+ /* Powering off the control block loses the vendor-page state. */
+ ret = __acx00_ephy_config_init(phydev);
+ if (ret)
+ goto out_power_off;
+
+ return 0;
+
+out_power_off:
+ acx00_ephy_control_power_off_warn(priv);
+
+ return ret;
+}
+
+static int acx00_ephy_resume(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+ int ret;
+
+ mutex_lock(&priv->state_lock);
+ ret = __acx00_ephy_power_on_and_resume(phydev);
+ mutex_unlock(&priv->state_lock);
+
+ return ret;
+}
+
+static int acx00_ephy_suspend(struct phy_device *phydev)
+{
+ struct acx00_ephy_priv *priv = phydev->priv;
+ int resume_ret;
+ int ret;
+
+ /* The suspend callback is invoked without the phylib lock held. */
+ mutex_lock(&phydev->lock);
+ mutex_lock(&priv->state_lock);
+
+ ret = genphy_suspend(phydev);
+ if (ret)
+ goto out_unlock;
+
+ ret = acx00_ephy_control_power_off(priv);
+ if (ret) {
+ resume_ret = __acx00_ephy_power_on_and_resume(phydev);
+ if (!resume_ret) {
+ /*
+ * The package reset also loses the standard link settings.
+ * A failed suspend is not followed by resume, so restore
+ * advertisement or forced speed/duplex before returning.
+ */
+ resume_ret = genphy_config_aneg(phydev);
+ if (resume_ret)
+ acx00_ephy_control_power_off_warn(priv);
+ }
+ if (resume_ret)
+ phydev_warn(phydev,
+ "failed to recover from suspend error: %pe\n",
+ ERR_PTR(resume_ret));
+ }
+
+out_unlock:
+ mutex_unlock(&priv->state_lock);
+ mutex_unlock(&phydev->lock);
+
+ return ret;
+}
+
+static int acx00_ephy_match_phy_device(struct phy_device *phydev,
+ const struct phy_driver *phydrv)
+{
+ struct device_node *node = phydev->mdio.dev.of_node;
+ struct device_node *package_node;
+ bool match;
+
+ if (!genphy_match_phy_device(phydev, phydrv) || !node)
+ return 0;
+
+ /* RK630 reports the same PHY ID, so also match the package identity. */
+ package_node = of_get_parent(node);
+ if (!package_node)
+ return 0;
+ match = of_device_is_compatible(package_node,
+ "x-powers,ac200-ephy-package") ||
+ of_device_is_compatible(package_node,
+ "x-powers,ac300-ephy-package") ||
+ of_device_is_compatible(package_node,
+ "x-powers,acx00-ephy-package");
+ of_node_put(package_node);
+
+ return match;
+}
+
+static struct phy_driver acx00_ephy_driver[] = {
+ {
+ PHY_ID_MATCH_MODEL(ACX00_EPHY_ID),
+ .name = "X-Powers AC200/AC300 EPHY",
+ .match_phy_device = acx00_ephy_match_phy_device,
+ .probe = acx00_ephy_probe,
+ .read_page = acx00_ephy_read_page,
+ .write_page = acx00_ephy_write_page,
+ .soft_reset = acx00_ephy_soft_reset,
+ .config_init = acx00_ephy_config_init,
+ .disable_autonomous_eee = acx00_ephy_disable_autonomous_eee,
+ .suspend = acx00_ephy_suspend,
+ .resume = acx00_ephy_resume,
+ },
+};
+module_phy_driver(acx00_ephy_driver);
+
+static const struct mdio_device_id __maybe_unused acx00_ephy_tbl[] = {
+ { PHY_ID_MATCH_MODEL(ACX00_EPHY_ID) },
+ { }
+};
+MODULE_DEVICE_TABLE(mdio, acx00_ephy_tbl);
+
+MODULE_AUTHOR("James Hilliard <james.hilliard1@gmail.com>");
+MODULE_DESCRIPTION("X-Powers AC200/AC300 Ethernet PHY driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/phy/xpowers/acx00.h b/drivers/net/phy/xpowers/acx00.h
new file mode 100644
index 000000000000..7df6084c7a77
--- /dev/null
+++ b/drivers/net/phy/xpowers/acx00.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __DRIVERS_NET_PHY_XPOWERS_ACX00_H
+#define __DRIVERS_NET_PHY_XPOWERS_ACX00_H
+
+#include <linux/err.h>
+#include <linux/phy.h>
+
+#define AC300_EPHY_CONTROL_ADDR_OFFSET 16
+
+struct device_node;
+
+struct acx00_ephy_control {
+ int (*power_on)(struct acx00_ephy_control *control,
+ unsigned int phy_addr);
+ int (*power_off)(struct acx00_ephy_control *control);
+ int (*set_interface)(struct acx00_ephy_control *control,
+ phy_interface_t interface);
+};
+
+struct acx00_ephy_control *
+ac200_ephy_ctl_create(struct phy_device *phydev,
+ struct device_node *package_node,
+ bool has_calibration, u8 calibration);
+struct acx00_ephy_control *
+ac300_ephy_ctl_create(struct phy_device *phydev,
+ struct device_node *package_node, u8 calibration);
+
+#endif