* [PATCH net-next] net: phy: Add JLSemi JL21xx Gigabit Ethernet PHY driver
@ 2026-10-16 16:15 Pufan Jin
2026-10-16 17:14 ` Andrew Lunn
2026-10-20 17:15 ` netdev-bot+sashiko
0 siblings, 2 replies; 4+ messages in thread
From: Pufan Jin @ 2026-10-16 16:15 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Heiner Kallweit,
Jakub Kicinski, Paolo Abeni, Russell King
Cc: linux-kernel, netdev
Add a new PHY driver for JLSemi (Jinglue Semiconductor (Shanghai)
Co., Ltd.) Gigabit Ethernet PHYs, specifically supporting the JL2101,
JL2121, and generic JL21xx models.
The driver implements:
- Paged register access via register 0x1f
- RGMII TX and RX internal delay configuration with Device Tree
support via phy_get_internal_delay()
- Link downshift and resolved speed, duplex, and master/slave mode
reporting from vendor-specific status registers
- Interrupt handling for link state change and PME wakeup events
- Hardware LED control via the Linux LED class / netdev triggers
- Wake-on-LAN (WoL) with magic packet matching
- Autonomous EEE disable to prevent interoperability packet drops
- Power management (suspend / resume)
Link: https://www.jlsemi.com/
Signed-off-by: Pufan Jin <cyan2254650260@gmail.com>
---
MAINTAINERS | 6 +
drivers/net/phy/Kconfig | 9 +
drivers/net/phy/Makefile | 1 +
drivers/net/phy/jlsemi.c | 551 +++++++++++++++++++++++++++++++++++++++
4 files changed, 567 insertions(+)
create mode 100644 drivers/net/phy/jlsemi.c
diff --git a/MAINTAINERS b/MAINTAINERS
index f53047aa8..4f738a573 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13976,6 +13976,12 @@ T: git https://github.com/kleikamp/linux-shaggy.git
F: Documentation/admin-guide/jfs.rst
F: fs/jfs/
+JLSEMI PHY DRIVER
+M: Pufan Jin <cyan2254650260@gmail.com>
+L: netdev@vger.kernel.org
+S: Maintained
+F: drivers/net/phy/jlsemi.c
+
JME NETWORK DRIVER
M: Guo-Fu Tseng <cooldavid@cooldavid.org>
L: netdev@vger.kernel.org
diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
index b4ef927fd..535a48084 100644
--- a/drivers/net/phy/Kconfig
+++ b/drivers/net/phy/Kconfig
@@ -270,6 +270,15 @@ config INTEL_XWAY_PHY
PEF 7061, PEF 7071 and PEF 7072 or integrated into the Intel
SoCs xRX200, xRX300, xRX330, xRX350 and xRX550.
+config JLSEMI_PHY
+ tristate "JLSemi Ethernet PHYs"
+ help
+ Supports the JLSemi Gigabit Ethernet PHYs, including the
+ JL2121 and JL2101 models found on embedded system carrier boards
+ and single-board computers.
+
+ If unsure, say N.
+
config LSI_ET1011C_PHY
tristate "LSI ET1011C PHY"
help
diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
index 25c4a3c24..2d7758e86 100644
--- a/drivers/net/phy/Makefile
+++ b/drivers/net/phy/Makefile
@@ -66,6 +66,7 @@ obj-$(CONFIG_DP83TG720_PHY) += dp83tg720.o
obj-$(CONFIG_FIXED_PHY) += fixed_phy.o
obj-$(CONFIG_ICPLUS_PHY) += icplus.o
obj-$(CONFIG_INTEL_XWAY_PHY) += intel-xway.o
+obj-$(CONFIG_JLSEMI_PHY) += jlsemi.o
obj-$(CONFIG_LSI_ET1011C_PHY) += et1011c.o
obj-$(CONFIG_LXT_PHY) += lxt.o
obj-$(CONFIG_MARVELL_10G_PHY) += marvell10g.o
diff --git a/drivers/net/phy/jlsemi.c b/drivers/net/phy/jlsemi.c
new file mode 100644
index 000000000..c3acff48d
--- /dev/null
+++ b/drivers/net/phy/jlsemi.c
@@ -0,0 +1,551 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Driver for JLSemi Gigabit Ethernet PHYs (JL2121 / JL2101)
+ *
+ * Copyright (C) 2026 Pufan Jin <cyan2254650260@gmail.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/ethtool.h>
+#include <linux/ethtool_netlink.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/phy.h>
+
+#define PHY_ID_JL2101 0x937c4032
+#define PHY_ID_JL2121 0x937c4030
+#define PHY_ID_JL21XX 0x937c0000
+#define JLSEMI_PHY_ID_MASK 0xffff0000
+
+#define JLSEMI_PAGE_SELECT 0x1f
+
+/* Status page and register */
+#define JLSEMI_STATUS_PAGE 0xa43
+#define JLSEMI_PHYSR 0x1a
+#define JLSEMI_PHYSR_DUPLEX BIT(3)
+#define JLSEMI_PHYSR_SPEED_MASK GENMASK(5, 4)
+#define JLSEMI_PHYSR_SPEED_1000 0x20
+#define JLSEMI_PHYSR_SPEED_100 0x10
+#define JLSEMI_PHYSR_SPEED_10 0x00
+#define JLSEMI_PHYSR_MASTER BIT(11)
+
+/* RGMII delay page and registers */
+#define JLSEMI_RGMII_PAGE 0xd08
+#define JLSEMI_TXCR 0x11
+#define JLSEMI_TX_DELAY BIT(8)
+#define JLSEMI_RXCR 0x15
+#define JLSEMI_RX_DELAY BIT(3)
+
+/* Power and EEE configuration */
+#define JLSEMI_PHYCR2 0x19
+#define JLSEMI_PHYCR2_PHY_EEE_ENABLE BIT(5)
+
+/* Interrupt page and registers */
+#define JLSEMI_INSR 0x1d
+#define JLSEMI_INER_PAGE 0xa42
+#define JLSEMI_INER 0x12
+#define JLSEMI_INER_LINK_STATUS BIT(4)
+#define JLSEMI_INER_PME BIT(7)
+
+/* Wake-on-LAN settings */
+#define JLSEMI_WOL_PAGE 0xd8a
+#define JLSEMI_WOL_SETTINGS_EVENTS 16
+#define JLSEMI_WOL_EVENT_MAGIC BIT(12)
+#define JLSEMI_WOL_RST_RMSQ 17
+#define JLSEMI_WOL_RG_RSTB BIT(15)
+#define JLSEMI_WOL_RMSQ 0x1fff
+
+/* Physical MAC address for WoL */
+#define JLSEMI_PHYSICAL_ADDR_PAGE 0xd8c
+#define JLSEMI_PHYSICAL_ADDR_WORD0 16
+#define JLSEMI_PHYSICAL_ADDR_WORD1 17
+#define JLSEMI_PHYSICAL_ADDR_WORD2 18
+
+/* LED control */
+#define JLSEMI_LED_COUNT 3
+#define JLSEMI_LEDCR_PAGE 0xd04
+#define JLSEMI_LEDCR 0x10
+#define JLSEMI_LEDCR_MODE BIT(15)
+#define JLSEMI_LEDCR_ACT_TXRX BIT(4)
+#define JLSEMI_LEDCR_LINK_1000 BIT(3)
+#define JLSEMI_LEDCR_LINK_100 BIT(1)
+#define JLSEMI_LEDCR_LINK_10 BIT(0)
+#define JLSEMI_LEDCR_MASK GENMASK(4, 0)
+#define JLSEMI_LEDCR_SHIFT 5
+
+struct jlsemi_priv {
+ u16 iner;
+};
+
+static int jlsemi_read_page(struct phy_device *phydev)
+{
+ return __phy_read(phydev, JLSEMI_PAGE_SELECT);
+}
+
+static int jlsemi_write_page(struct phy_device *phydev, int page)
+{
+ return __phy_write(phydev, JLSEMI_PAGE_SELECT, page);
+}
+
+static int jlsemi_probe(struct phy_device *phydev)
+{
+ struct device *dev = &phydev->mdio.dev;
+ struct jlsemi_priv *priv;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ phydev->priv = priv;
+
+ return 0;
+}
+
+static int jlsemi_config_rgmii_delay(struct phy_device *phydev)
+{
+ u16 val_txdly = 0, val_rxdly = 0;
+ s32 rx_delay = -1, tx_delay = -1;
+ int ret;
+
+ if (!phy_interface_is_rgmii(phydev))
+ return 0;
+
+ if (phydev->interface != PHY_INTERFACE_MODE_RGMII_TXID)
+ rx_delay = phy_get_internal_delay(phydev, NULL, 0, true);
+
+ if (phydev->interface != PHY_INTERFACE_MODE_RGMII_RXID)
+ tx_delay = phy_get_internal_delay(phydev, NULL, 0, false);
+
+ switch (phydev->interface) {
+ case PHY_INTERFACE_MODE_RGMII:
+ val_txdly = 0;
+ val_rxdly = 0;
+ break;
+ case PHY_INTERFACE_MODE_RGMII_ID:
+ val_txdly = JLSEMI_TX_DELAY;
+ val_rxdly = JLSEMI_RX_DELAY;
+ break;
+ case PHY_INTERFACE_MODE_RGMII_RXID:
+ val_txdly = 0;
+ val_rxdly = JLSEMI_RX_DELAY;
+ break;
+ case PHY_INTERFACE_MODE_RGMII_TXID:
+ val_txdly = JLSEMI_TX_DELAY;
+ val_rxdly = 0;
+ break;
+ default:
+ return 0;
+ }
+
+ if (rx_delay >= 0)
+ val_rxdly = rx_delay > 0 ? JLSEMI_RX_DELAY : 0;
+ if (tx_delay >= 0)
+ val_txdly = tx_delay > 0 ? JLSEMI_TX_DELAY : 0;
+
+ ret = phy_modify_paged_changed(phydev, JLSEMI_RGMII_PAGE,
+ JLSEMI_TXCR, JLSEMI_TX_DELAY, val_txdly);
+ if (ret < 0)
+ return ret;
+
+ return phy_modify_paged_changed(phydev, JLSEMI_RGMII_PAGE,
+ JLSEMI_RXCR, JLSEMI_RX_DELAY,
+ val_rxdly);
+}
+
+static int jlsemi_disable_autonomous_eee(struct phy_device *phydev)
+{
+ return phy_modify(phydev, JLSEMI_PHYCR2,
+ JLSEMI_PHYCR2_PHY_EEE_ENABLE, 0);
+}
+
+static int jlsemi_config_init(struct phy_device *phydev)
+{
+ int ret;
+
+ ret = jlsemi_disable_autonomous_eee(phydev);
+ if (ret < 0)
+ return ret;
+
+ if (phy_interface_is_rgmii(phydev)) {
+ ret = jlsemi_config_rgmii_delay(phydev);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int jlsemi_ack_interrupt(struct phy_device *phydev)
+{
+ int err = phy_read(phydev, JLSEMI_INSR);
+
+ return (err < 0) ? err : 0;
+}
+
+static int jlsemi_config_intr(struct phy_device *phydev)
+{
+ struct jlsemi_priv *priv = phydev->priv;
+ int err;
+ u16 val;
+
+ if (phydev->interrupts == PHY_INTERRUPT_ENABLED) {
+ err = jlsemi_ack_interrupt(phydev);
+ if (err)
+ return err;
+
+ val = JLSEMI_INER_LINK_STATUS;
+ err = phy_write_paged(phydev, JLSEMI_INER_PAGE, JLSEMI_INER, val);
+ if (!err)
+ priv->iner = val;
+ } else {
+ val = 0;
+ err = phy_write_paged(phydev, JLSEMI_INER_PAGE, JLSEMI_INER, val);
+ if (!err)
+ priv->iner = val;
+
+ err = jlsemi_ack_interrupt(phydev);
+ }
+
+ return err;
+}
+
+static irqreturn_t jlsemi_handle_interrupt(struct phy_device *phydev)
+{
+ int irq_status;
+
+ irq_status = phy_read(phydev, JLSEMI_INSR);
+ if (irq_status < 0) {
+ phy_error(phydev);
+ return IRQ_NONE;
+ }
+
+ if (irq_status & JLSEMI_INER_LINK_STATUS) {
+ phy_trigger_machine(phydev);
+ return IRQ_HANDLED;
+ }
+
+ if (irq_status & JLSEMI_INER_PME) {
+ pm_wakeup_event(&phydev->mdio.dev, 0);
+ return IRQ_HANDLED;
+ }
+
+ return IRQ_NONE;
+}
+
+static int jlsemi_read_status(struct phy_device *phydev)
+{
+ int err, val;
+
+ err = genphy_read_status(phydev);
+ if (err < 0 || !phydev->link)
+ return err;
+
+ val = phy_read_paged(phydev, JLSEMI_STATUS_PAGE, JLSEMI_PHYSR);
+ if (val < 0)
+ return val;
+
+ if (val & JLSEMI_PHYSR_DUPLEX)
+ phydev->duplex = DUPLEX_FULL;
+ else
+ phydev->duplex = DUPLEX_HALF;
+
+ switch (val & JLSEMI_PHYSR_SPEED_MASK) {
+ case JLSEMI_PHYSR_SPEED_1000:
+ phydev->speed = SPEED_1000;
+ break;
+ case JLSEMI_PHYSR_SPEED_100:
+ phydev->speed = SPEED_100;
+ break;
+ case JLSEMI_PHYSR_SPEED_10:
+ phydev->speed = SPEED_10;
+ break;
+ default:
+ phydev->speed = SPEED_UNKNOWN;
+ break;
+ }
+
+ if (phydev->speed >= SPEED_1000) {
+ if (val & JLSEMI_PHYSR_MASTER)
+ phydev->master_slave_state = MASTER_SLAVE_STATE_MASTER;
+ else
+ phydev->master_slave_state = MASTER_SLAVE_STATE_SLAVE;
+ } else {
+ phydev->master_slave_state = MASTER_SLAVE_STATE_UNSUPPORTED;
+ }
+
+ return 0;
+}
+
+static void jlsemi_get_wol(struct phy_device *phydev, struct ethtool_wolinfo *wol)
+{
+ int val;
+
+ wol->supported = WAKE_MAGIC;
+ wol->wolopts = 0;
+
+ val = phy_read_paged(phydev, JLSEMI_WOL_PAGE, JLSEMI_WOL_SETTINGS_EVENTS);
+ if (val < 0)
+ return;
+
+ if (val & JLSEMI_WOL_EVENT_MAGIC)
+ wol->wolopts |= WAKE_MAGIC;
+}
+
+static int jlsemi_set_wol(struct phy_device *phydev, struct ethtool_wolinfo *wol)
+{
+ const u8 *mac_addr;
+ int oldpage, ret;
+
+ if (wol->wolopts & ~WAKE_MAGIC)
+ return -EINVAL;
+
+ if (wol->wolopts & WAKE_MAGIC) {
+ if (!phydev->attached_dev)
+ return -EINVAL;
+
+ mac_addr = phydev->attached_dev->dev_addr;
+
+ oldpage = phy_select_page(phydev, JLSEMI_PHYSICAL_ADDR_PAGE);
+ if (oldpage < 0)
+ return oldpage;
+
+ __phy_write(phydev, JLSEMI_PHYSICAL_ADDR_WORD0,
+ mac_addr[1] << 8 | mac_addr[0]);
+ __phy_write(phydev, JLSEMI_PHYSICAL_ADDR_WORD1,
+ mac_addr[3] << 8 | mac_addr[2]);
+ __phy_write(phydev, JLSEMI_PHYSICAL_ADDR_WORD2,
+ mac_addr[5] << 8 | mac_addr[4]);
+
+ ret = __phy_write(phydev, JLSEMI_PAGE_SELECT, JLSEMI_WOL_PAGE);
+ if (ret < 0)
+ goto restore;
+
+ __phy_write(phydev, JLSEMI_WOL_SETTINGS_EVENTS,
+ JLSEMI_WOL_EVENT_MAGIC);
+ __phy_write(phydev, JLSEMI_WOL_RST_RMSQ,
+ JLSEMI_WOL_RMSQ);
+restore:
+ phy_restore_page(phydev, oldpage, ret);
+ } else {
+ oldpage = phy_select_page(phydev, JLSEMI_WOL_PAGE);
+ if (oldpage < 0)
+ return oldpage;
+
+ __phy_write(phydev, JLSEMI_WOL_SETTINGS_EVENTS, 0);
+ __phy_clear_bits(phydev, JLSEMI_WOL_RST_RMSQ,
+ JLSEMI_WOL_RG_RSTB);
+
+ phy_restore_page(phydev, oldpage, 0);
+ }
+
+ device_set_wakeup_enable(&phydev->mdio.dev, !!(wol->wolopts & WAKE_MAGIC));
+
+ return 0;
+}
+
+static int jlsemi_suspend(struct phy_device *phydev)
+{
+ int ret;
+
+ if (device_may_wakeup(&phydev->mdio.dev)) {
+ ret = phy_write_paged(phydev, JLSEMI_INER_PAGE, JLSEMI_INER,
+ JLSEMI_INER_PME);
+ if (ret < 0)
+ return ret;
+
+ jlsemi_ack_interrupt(phydev);
+ return 0;
+ }
+
+ return genphy_suspend(phydev);
+}
+
+static int jlsemi_resume(struct phy_device *phydev)
+{
+ struct jlsemi_priv *priv = phydev->priv;
+ int ret;
+
+ ret = genphy_resume(phydev);
+ if (ret < 0)
+ return ret;
+
+ return phy_write_paged(phydev, JLSEMI_INER_PAGE, JLSEMI_INER,
+ priv ? priv->iner : 0);
+}
+
+static int jlsemi_led_hw_is_supported(struct phy_device *phydev, u8 index,
+ unsigned long rules)
+{
+ const unsigned long mask = BIT(TRIGGER_NETDEV_LINK) |
+ BIT(TRIGGER_NETDEV_LINK_10) |
+ BIT(TRIGGER_NETDEV_LINK_100) |
+ BIT(TRIGGER_NETDEV_LINK_1000) |
+ BIT(TRIGGER_NETDEV_RX) |
+ BIT(TRIGGER_NETDEV_TX);
+
+ if (index >= JLSEMI_LED_COUNT)
+ return -EINVAL;
+
+ if (rules & ~mask)
+ return -EOPNOTSUPP;
+
+ if (!(rules & BIT(TRIGGER_NETDEV_RX)) ^ !(rules & BIT(TRIGGER_NETDEV_TX)))
+ return -EOPNOTSUPP;
+
+ return 0;
+}
+
+static int jlsemi_led_hw_control_get(struct phy_device *phydev, u8 index,
+ unsigned long *rules)
+{
+ int val;
+
+ if (index >= JLSEMI_LED_COUNT)
+ return -EINVAL;
+
+ val = phy_read_paged(phydev, JLSEMI_LEDCR_PAGE, JLSEMI_LEDCR);
+ if (val < 0)
+ return val;
+
+ val >>= JLSEMI_LEDCR_SHIFT * index;
+ val &= JLSEMI_LEDCR_MASK;
+
+ if (val & JLSEMI_LEDCR_LINK_10)
+ __set_bit(TRIGGER_NETDEV_LINK_10, rules);
+
+ if (val & JLSEMI_LEDCR_LINK_100)
+ __set_bit(TRIGGER_NETDEV_LINK_100, rules);
+
+ if (val & JLSEMI_LEDCR_LINK_1000)
+ __set_bit(TRIGGER_NETDEV_LINK_1000, rules);
+
+ if ((val & JLSEMI_LEDCR_LINK_10) &&
+ (val & JLSEMI_LEDCR_LINK_100) &&
+ (val & JLSEMI_LEDCR_LINK_1000)) {
+ __set_bit(TRIGGER_NETDEV_LINK, rules);
+ }
+
+ if (val & JLSEMI_LEDCR_ACT_TXRX) {
+ __set_bit(TRIGGER_NETDEV_RX, rules);
+ __set_bit(TRIGGER_NETDEV_TX, rules);
+ }
+
+ return 0;
+}
+
+static int jlsemi_led_hw_control_set(struct phy_device *phydev, u8 index,
+ unsigned long rules)
+{
+ const u16 mask = JLSEMI_LEDCR_MASK << (JLSEMI_LEDCR_SHIFT * index);
+ u16 reg = 0;
+
+ if (index >= JLSEMI_LED_COUNT)
+ return -EINVAL;
+
+ if (test_bit(TRIGGER_NETDEV_LINK, &rules) ||
+ test_bit(TRIGGER_NETDEV_LINK_10, &rules)) {
+ reg |= JLSEMI_LEDCR_LINK_10;
+ }
+
+ if (test_bit(TRIGGER_NETDEV_LINK, &rules) ||
+ test_bit(TRIGGER_NETDEV_LINK_100, &rules)) {
+ reg |= JLSEMI_LEDCR_LINK_100;
+ }
+
+ if (test_bit(TRIGGER_NETDEV_LINK, &rules) ||
+ test_bit(TRIGGER_NETDEV_LINK_1000, &rules)) {
+ reg |= JLSEMI_LEDCR_LINK_1000;
+ }
+
+ if (test_bit(TRIGGER_NETDEV_RX, &rules) ||
+ test_bit(TRIGGER_NETDEV_TX, &rules)) {
+ reg |= JLSEMI_LEDCR_ACT_TXRX;
+ }
+
+ reg <<= JLSEMI_LEDCR_SHIFT * index;
+ reg |= JLSEMI_LEDCR_MODE;
+
+ return phy_modify_paged(phydev, JLSEMI_LEDCR_PAGE, JLSEMI_LEDCR,
+ mask, reg);
+}
+
+static struct phy_driver jlsemi_drivers[] = {
+ {
+ PHY_ID_MATCH_EXACT(PHY_ID_JL2101),
+ .name = "JLSemi JL2101 Gigabit Ethernet",
+ .features = PHY_GBIT_FEATURES,
+ .flags = PHY_ALWAYS_CALL_SUSPEND,
+ .probe = jlsemi_probe,
+ .soft_reset = genphy_soft_reset,
+ .config_init = jlsemi_config_init,
+ .read_status = jlsemi_read_status,
+ .config_intr = jlsemi_config_intr,
+ .handle_interrupt = jlsemi_handle_interrupt,
+ .set_wol = jlsemi_set_wol,
+ .get_wol = jlsemi_get_wol,
+ .suspend = jlsemi_suspend,
+ .resume = jlsemi_resume,
+ .read_page = jlsemi_read_page,
+ .write_page = jlsemi_write_page,
+ .led_hw_is_supported = jlsemi_led_hw_is_supported,
+ .led_hw_control_get = jlsemi_led_hw_control_get,
+ .led_hw_control_set = jlsemi_led_hw_control_set,
+ .disable_autonomous_eee = jlsemi_disable_autonomous_eee,
+ },
+ {
+ PHY_ID_MATCH_MODEL(PHY_ID_JL2121),
+ .name = "JLSemi JL2121 Gigabit Ethernet",
+ .features = PHY_GBIT_FEATURES,
+ .flags = PHY_ALWAYS_CALL_SUSPEND,
+ .probe = jlsemi_probe,
+ .soft_reset = genphy_soft_reset,
+ .config_init = jlsemi_config_init,
+ .read_status = jlsemi_read_status,
+ .config_intr = jlsemi_config_intr,
+ .handle_interrupt = jlsemi_handle_interrupt,
+ .set_wol = jlsemi_set_wol,
+ .get_wol = jlsemi_get_wol,
+ .suspend = jlsemi_suspend,
+ .resume = jlsemi_resume,
+ .read_page = jlsemi_read_page,
+ .write_page = jlsemi_write_page,
+ .led_hw_is_supported = jlsemi_led_hw_is_supported,
+ .led_hw_control_get = jlsemi_led_hw_control_get,
+ .led_hw_control_set = jlsemi_led_hw_control_set,
+ .disable_autonomous_eee = jlsemi_disable_autonomous_eee,
+ },
+ {
+ .phy_id = PHY_ID_JL21XX,
+ .phy_id_mask = JLSEMI_PHY_ID_MASK,
+ .name = "JLSemi Generic Gigabit Ethernet",
+ .features = PHY_GBIT_FEATURES,
+ .flags = PHY_ALWAYS_CALL_SUSPEND,
+ .probe = jlsemi_probe,
+ .soft_reset = genphy_soft_reset,
+ .config_init = jlsemi_config_init,
+ .read_status = jlsemi_read_status,
+ .config_intr = jlsemi_config_intr,
+ .handle_interrupt = jlsemi_handle_interrupt,
+ .suspend = jlsemi_suspend,
+ .resume = jlsemi_resume,
+ .read_page = jlsemi_read_page,
+ .write_page = jlsemi_write_page,
+ .disable_autonomous_eee = jlsemi_disable_autonomous_eee,
+ },
+};
+
+module_phy_driver(jlsemi_drivers);
+
+static const struct mdio_device_id __maybe_unused jlsemi_tbl[] = {
+ { PHY_ID_MATCH_EXACT(PHY_ID_JL2101) },
+ { PHY_ID_MATCH_MODEL(PHY_ID_JL2121) },
+ { PHY_ID_JL21XX, JLSEMI_PHY_ID_MASK },
+ { /* sentinel */ }
+};
+
+MODULE_DEVICE_TABLE(mdio, jlsemi_tbl);
+
+MODULE_DESCRIPTION("JLSemi Ethernet PHY driver");
+MODULE_AUTHOR("Pufan Jin <cyan2254650260@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.56.0
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH net-next] net: phy: Add JLSemi JL21xx Gigabit Ethernet PHY driver
2026-10-16 16:15 [PATCH net-next] net: phy: Add JLSemi JL21xx Gigabit Ethernet PHY driver Pufan Jin
@ 2026-10-16 17:14 ` Andrew Lunn
2026-10-20 17:15 ` netdev-bot+sashiko
1 sibling, 0 replies; 4+ messages in thread
From: Andrew Lunn @ 2026-10-16 17:14 UTC (permalink / raw)
To: Pufan Jin
Cc: David S. Miller, Eric Dumazet, Heiner Kallweit, Jakub Kicinski,
Paolo Abeni, Russell King, linux-kernel, netdev
On Sat, Oct 17, 2026 at 12:15:43AM +0800, Pufan Jin wrote:
> Add a new PHY driver for JLSemi (Jinglue Semiconductor (Shanghai)
> Co., Ltd.) Gigabit Ethernet PHYs, specifically supporting the JL2101,
> JL2121, and generic JL21xx models.
>
> The driver implements:
> - Paged register access via register 0x1f
> - RGMII TX and RX internal delay configuration with Device Tree
> support via phy_get_internal_delay()
> - Link downshift and resolved speed, duplex, and master/slave mode
> reporting from vendor-specific status registers
> - Interrupt handling for link state change and PME wakeup events
> - Hardware LED control via the Linux LED class / netdev triggers
> - Wake-on-LAN (WoL) with magic packet matching
> - Autonomous EEE disable to prevent interoperability packet drops
> - Power management (suspend / resume)
>
> Link: https://www.jlsemi.com/
> Signed-off-by: Pufan Jin <cyan2254650260@gmail.com>
> ---
> MAINTAINERS | 6 +
> drivers/net/phy/Kconfig | 9 +
> drivers/net/phy/Makefile | 1 +
> drivers/net/phy/jlsemi.c | 551 +++++++++++++++++++++++++++++++++++++++
> 4 files changed, 567 insertions(+)
> create mode 100644 drivers/net/phy/jlsemi.c
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index f53047aa8..4f738a573 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -13976,6 +13976,12 @@ T: git https://github.com/kleikamp/linux-shaggy.git
> F: Documentation/admin-guide/jfs.rst
> F: fs/jfs/
>
> +JLSEMI PHY DRIVER
> +M: Pufan Jin <cyan2254650260@gmail.com>
> +L: netdev@vger.kernel.org
> +S: Maintained
> +F: drivers/net/phy/jlsemi.c
> +
> JME NETWORK DRIVER
> M: Guo-Fu Tseng <cooldavid@cooldavid.org>
> L: netdev@vger.kernel.org
> diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
> index b4ef927fd..535a48084 100644
> --- a/drivers/net/phy/Kconfig
> +++ b/drivers/net/phy/Kconfig
> @@ -270,6 +270,15 @@ config INTEL_XWAY_PHY
> PEF 7061, PEF 7071 and PEF 7072 or integrated into the Intel
> SoCs xRX200, xRX300, xRX330, xRX350 and xRX550.
>
> +config JLSEMI_PHY
> + tristate "JLSemi Ethernet PHYs"
> + help
> + Supports the JLSemi Gigabit Ethernet PHYs, including the
> + JL2121 and JL2101 models found on embedded system carrier boards
> + and single-board computers.
> +
> + If unsure, say N.
> +
> config LSI_ET1011C_PHY
> tristate "LSI ET1011C PHY"
> help
> diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
> index 25c4a3c24..2d7758e86 100644
> --- a/drivers/net/phy/Makefile
> +++ b/drivers/net/phy/Makefile
> @@ -66,6 +66,7 @@ obj-$(CONFIG_DP83TG720_PHY) += dp83tg720.o
> obj-$(CONFIG_FIXED_PHY) += fixed_phy.o
> obj-$(CONFIG_ICPLUS_PHY) += icplus.o
> obj-$(CONFIG_INTEL_XWAY_PHY) += intel-xway.o
> +obj-$(CONFIG_JLSEMI_PHY) += jlsemi.o
> obj-$(CONFIG_LSI_ET1011C_PHY) += et1011c.o
> obj-$(CONFIG_LXT_PHY) += lxt.o
> obj-$(CONFIG_MARVELL_10G_PHY) += marvell10g.o
> diff --git a/drivers/net/phy/jlsemi.c b/drivers/net/phy/jlsemi.c
> new file mode 100644
> index 000000000..c3acff48d
> --- /dev/null
> +++ b/drivers/net/phy/jlsemi.c
> @@ -0,0 +1,551 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/*
> + * Driver for JLSemi Gigabit Ethernet PHYs (JL2121 / JL2101)
> + *
> + * Copyright (C) 2026 Pufan Jin <cyan2254650260@gmail.com>
> + */
> +
> +#include <linux/bitfield.h>
> +#include <linux/ethtool.h>
> +#include <linux/ethtool_netlink.h>
> +#include <linux/leds.h>
> +#include <linux/module.h>
> +#include <linux/netdevice.h>
> +#include <linux/phy.h>
> +
> +#define PHY_ID_JL2101 0x937c4032
> +#define PHY_ID_JL2121 0x937c4030
> +#define PHY_ID_JL21XX 0x937c0000
> +#define JLSEMI_PHY_ID_MASK 0xffff0000
> +
> +#define JLSEMI_PAGE_SELECT 0x1f
> +
> +/* Status page and register */
> +#define JLSEMI_STATUS_PAGE 0xa43
> +#define JLSEMI_PHYSR 0x1a
> +#define JLSEMI_PHYSR_DUPLEX BIT(3)
> +#define JLSEMI_PHYSR_SPEED_MASK GENMASK(5, 4)
> +#define JLSEMI_PHYSR_SPEED_1000 0x20
> +#define JLSEMI_PHYSR_SPEED_100 0x10
> +#define JLSEMI_PHYSR_SPEED_10 0x00
> +#define JLSEMI_PHYSR_MASTER BIT(11)
> +
> +/* RGMII delay page and registers */
> +#define JLSEMI_RGMII_PAGE 0xd08
> +#define JLSEMI_TXCR 0x11
> +#define JLSEMI_TX_DELAY BIT(8)
> +#define JLSEMI_RXCR 0x15
> +#define JLSEMI_RX_DELAY BIT(3)
> +
> +/* Power and EEE configuration */
> +#define JLSEMI_PHYCR2 0x19
> +#define JLSEMI_PHYCR2_PHY_EEE_ENABLE BIT(5)
> +
> +/* Interrupt page and registers */
> +#define JLSEMI_INSR 0x1d
> +#define JLSEMI_INER_PAGE 0xa42
> +#define JLSEMI_INER 0x12
> +#define JLSEMI_INER_LINK_STATUS BIT(4)
> +#define JLSEMI_INER_PME BIT(7)
> +
> +/* Wake-on-LAN settings */
> +#define JLSEMI_WOL_PAGE 0xd8a
> +#define JLSEMI_WOL_SETTINGS_EVENTS 16
> +#define JLSEMI_WOL_EVENT_MAGIC BIT(12)
> +#define JLSEMI_WOL_RST_RMSQ 17
> +#define JLSEMI_WOL_RG_RSTB BIT(15)
> +#define JLSEMI_WOL_RMSQ 0x1fff
> +
> +/* Physical MAC address for WoL */
> +#define JLSEMI_PHYSICAL_ADDR_PAGE 0xd8c
> +#define JLSEMI_PHYSICAL_ADDR_WORD0 16
> +#define JLSEMI_PHYSICAL_ADDR_WORD1 17
> +#define JLSEMI_PHYSICAL_ADDR_WORD2 18
> +
> +/* LED control */
> +#define JLSEMI_LED_COUNT 3
> +#define JLSEMI_LEDCR_PAGE 0xd04
> +#define JLSEMI_LEDCR 0x10
> +#define JLSEMI_LEDCR_MODE BIT(15)
> +#define JLSEMI_LEDCR_ACT_TXRX BIT(4)
> +#define JLSEMI_LEDCR_LINK_1000 BIT(3)
> +#define JLSEMI_LEDCR_LINK_100 BIT(1)
> +#define JLSEMI_LEDCR_LINK_10 BIT(0)
> +#define JLSEMI_LEDCR_MASK GENMASK(4, 0)
> +#define JLSEMI_LEDCR_SHIFT 5
> +
> +struct jlsemi_priv {
> + u16 iner;
> +};
> +
> +static int jlsemi_read_page(struct phy_device *phydev)
> +{
> + return __phy_read(phydev, JLSEMI_PAGE_SELECT);
> +}
> +
> +static int jlsemi_write_page(struct phy_device *phydev, int page)
> +{
> + return __phy_write(phydev, JLSEMI_PAGE_SELECT, page);
> +}
> +
> +static int jlsemi_probe(struct phy_device *phydev)
> +{
> + struct device *dev = &phydev->mdio.dev;
> + struct jlsemi_priv *priv;
> +
> + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> + if (!priv)
> + return -ENOMEM;
> +
> + phydev->priv = priv;
> +
> + return 0;
> +}
> +
> +static int jlsemi_config_rgmii_delay(struct phy_device *phydev)
> +{
> + u16 val_txdly = 0, val_rxdly = 0;
> + s32 rx_delay = -1, tx_delay = -1;
> + int ret;
> +
> + if (!phy_interface_is_rgmii(phydev))
> + return 0;
...
> +static int jlsemi_config_init(struct phy_device *phydev)
> +{
> + int ret;
> +
> + ret = jlsemi_disable_autonomous_eee(phydev);
> + if (ret < 0)
> + return ret;
> +
> + if (phy_interface_is_rgmii(phydev)) {
> + ret = jlsemi_config_rgmii_delay(phydev);
> + if (ret < 0)
> + return ret;
> + }
No need to test for RGMII twice.
> +static int jlsemi_read_status(struct phy_device *phydev)
> +{
> + if (phydev->speed >= SPEED_1000) {
The > can be dropped. It should not happen.
Humm, actually. Should this be SPEED_100?
> +static void jlsemi_get_wol(struct phy_device *phydev, struct ethtool_wolinfo *wol)
> +{
> + int val;
> +
> + wol->supported = WAKE_MAGIC;
> + wol->wolopts = 0;
> +
> + val = phy_read_paged(phydev, JLSEMI_WOL_PAGE, JLSEMI_WOL_SETTINGS_EVENTS);
> + if (val < 0)
> + return;
> +
> + if (val & JLSEMI_WOL_EVENT_MAGIC)
> + wol->wolopts |= WAKE_MAGIC;
I think you need something like:
if (!device_can_wakeup(&dev->mdio.dev)) {
wol->supported = 0;
return;
}
in order to determine if the PHY really can wake the system.
Look at realtek_main.c and
commit b826bf795564ddef6402cf2cb522ae035bd117ae
Author: Russell King (Oracle) <rmk+kernel@armlinux.org.uk>
Date: Wed Aug 13 11:04:45 2025 +0100
net: phy: realtek: fix RTL8211F wake-on-lan support
> +static struct phy_driver jlsemi_drivers[] = {
> + {
> + PHY_ID_MATCH_EXACT(PHY_ID_JL2101),
> + .name = "JLSemi JL2101 Gigabit Ethernet",
> + .features = PHY_GBIT_FEATURES,
Should not be needed, if the device describes itself correctly.
> + {
> + .phy_id = PHY_ID_JL21XX,
> + .phy_id_mask = JLSEMI_PHY_ID_MASK,
PHY_ID_MATCH_VENDOR()
> +
> +static const struct mdio_device_id __maybe_unused jlsemi_tbl[] = {
> + { PHY_ID_MATCH_EXACT(PHY_ID_JL2101) },
> + { PHY_ID_MATCH_MODEL(PHY_ID_JL2121) },
> + { PHY_ID_JL21XX, JLSEMI_PHY_ID_MASK },
> + { /* sentinel */ }
PHY_ID_MATCH_VENDOR() should work here as well.
Andrew
---
pw-bot: cr
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH net-next] net: phy: Add JLSemi JL21xx Gigabit Ethernet PHY driver
2026-10-16 16:15 [PATCH net-next] net: phy: Add JLSemi JL21xx Gigabit Ethernet PHY driver Pufan Jin
2026-10-16 17:14 ` Andrew Lunn
@ 2026-10-20 17:15 ` netdev-bot+sashiko
2026-10-20 17:39 ` Andrew Lunn
1 sibling, 1 reply; 4+ messages in thread
From: netdev-bot+sashiko @ 2026-10-20 17:15 UTC (permalink / raw)
To: cyan2254650260
Cc: andrew, davem, edumazet, hkallweit1, kuba, pabeni, linux,
linux-kernel, netdev
Thank you for your contribution! Sashiko AI review found 10 potential
issue(s) to consider.
Critical: 0 · High: 1 · Medium: 6 · Low: 3
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] jlsemi_set_wol() can leave the MDIO bus mutex held permanently.
- [Medium] Wake-on-LAN cannot work, yet the driver tells userspace it is
enabled.
- [Medium] When WoL is enabled, jlsemi_set_wol() writes JLSEMI_WOL_RMSQ
(0x1fff) to page 0xd8a reg 17.
- [Medium] jlsemi_config_init() always calls
jlsemi_disable_autonomous_eee() (clears PHYCR2 bit 5) on every…
- [Medium] When the LED's three link-speed bits are all set,
jlsemi_led_hw_control_get() returns TRIGGER_NETDEV_LINK_10, LINK_100…
- [Medium] jlsemi_set_wol() ignores every hardware-programming error and
always returns 0.
- [Medium] In the interrupt-disable branch of jlsemi_config_intr(), `err
= phy_write_paged(phydev, JLSEMI_INER_PAGE, JLSEMI_INER, 0)` is never…
- [Low] The register map and most of the logic in
drivers/net/phy/jlsemi.c are a near-verbatim copy of the RTL8211F…
- [Low] jlsemi_config_rgmii_delay() handles rx-/tx-internal-delay-ps
inconsistently.
- [Low] jlsemi_handle_interrupt() reads INSR (0x1d) once.
--
net: phy: Add JLSemi JL21xx Gigabit Ethernet PHY driver
This commit adds a new phylib driver for the JLSemi JL2101, JL2121 and
generic JL21xx Gigabit PHYs. It covers paged register access, RGMII delay
configuration, vendor status register parsing, interrupts, hardware LED
control, magic packet Wake-on-LAN, autonomous EEE disable and
suspend/resume.
Link: https://www.jlsemi.com/
> diff --git a/drivers/net/phy/jlsemi.c b/drivers/net/phy/jlsemi.c
> new file mode 100644
> index 0000000000000..c3acff48d4618
> --- /dev/null
> +++ b/drivers/net/phy/jlsemi.c
> @@ -0,0 +1,551 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/*
> + * Driver for JLSemi Gigabit Ethernet PHYs (JL2121 / JL2101)
> + *
> + * Copyright (C) 2026 Pufan Jin <cyan2254650260@gmail.com>
> + */
[ ... ]
> +/* Wake-on-LAN settings */
> +#define JLSEMI_WOL_PAGE 0xd8a
> +#define JLSEMI_WOL_SETTINGS_EVENTS 16
> +#define JLSEMI_WOL_EVENT_MAGIC BIT(12)
> +#define JLSEMI_WOL_RST_RMSQ 17
> +#define JLSEMI_WOL_RG_RSTB BIT(15)
> +#define JLSEMI_WOL_RMSQ 0x1fff
[Severity: Low]
This isn't a bug, but the register map looks identical to the RTL8211F
support in drivers/net/phy/realtek/realtek_main.c. That covers PHYCR2 0x19
bit 5, INSR 0x1d, INER page 0xa42 reg 0x12, RGMII page 0xd08 regs
0x11/0x15, WoL page 0xd8a regs 16/17 with RG_RSTB BIT(15) and RMSQ 0x1fff,
MAC page 0xd8c, and LEDCR page 0xd04 reg 0x10 with 5-bit per-LED fields.
jlsemi_led_hw_is_supported() and jlsemi_led_hw_control_get() also follow
rtl8211x_led_hw_is_supported() and rtl8211f_led_hw_control_get() closely.
The same goes for the set_wol, suspend and resume flows.
Neither the commit message nor the file header mentions this origin. The
header lists a single copyright holder, while realtek_main.c has others.
Some later fixes in the realtek code also didn't come across:
- wakeup-source and wake IRQ setup in probe
- disabling PME events at probe
- the RG_RSTB pulse in suspend
- device_can_wakeup() gating in get_wol/set_wol
- the msleep(20) after resume
If these parts are register compatible with RTL8211F, could the IDs go into
the realtek driver, or could the code be shared, instead of keeping a copy?
[ ... ]
> +static int jlsemi_probe(struct phy_device *phydev)
> +{
> + struct device *dev = &phydev->mdio.dev;
> + struct jlsemi_priv *priv;
> +
> + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> + if (!priv)
> + return -ENOMEM;
> +
> + phydev->priv = priv;
> +
> + return 0;
> +}
[Severity: Medium]
Can Wake-on-LAN work with this driver? Nothing here marks the mdio device
as wakeup capable. There is no device_set_wakeup_capable() or
device_init_wakeup(), no wakeup-source handling and no
devm_pm_set_wake_irq(). Compare rtl8211f_probe():
drivers/net/phy/realtek/realtek_main.c:rtl8211f_probe() {
...
if (device_property_read_bool(dev, "wakeup-source") &&
phy_interrupt_is_valid(phydev)) {
device_set_wakeup_capable(dev, true);
devm_pm_set_wake_irq(dev, phydev->irq);
}
...
}
Because dev->power.can_wakeup stays false, device_may_wakeup() is always
false. device_set_wakeup_enable() in jlsemi_set_wol() returns -EINVAL. That
error is ignored and set_wol returns 0, and jlsemi_get_wol() still reports
WAKE_MAGIC as supported and enabled.
On suspend, phy_may_wakeup() falls back to the driver's WoL state, so
phydev->wol_enabled becomes true. PHY_ALWAYS_CALL_SUSPEND then skips the
core's -EBUSY guard:
drivers/net/phy/phy_device.c:phy_suspend() {
...
phydev->wol_enabled = phy_may_wakeup(phydev) ||
(netdev && netdev->ethtool->wol_enabled);
/* If the device has WOL enabled, we cannot suspend the PHY */
if (phydev->wol_enabled && !(phydrv->flags & PHY_ALWAYS_CALL_SUSPEND))
return -EBUSY;
...
}
jlsemi_suspend() checks only device_may_wakeup(). It skips the INER=PME
setup and calls genphy_suspend(), which sets BMCR_PDOWN.
Doesn't this power down the PHY while magic packet matching is armed? And
since phydev->wol_enabled is never checked, doesn't this also take the
copper link down when the MAC provides WoL?
For comparison, rtl821x_suspend() checks !phydev->wol_enabled before
calling genphy_suspend(). rtl8211f_get_wol() reports no support and
rtl8211f_set_wol() returns -EOPNOTSUPP when !device_can_wakeup().
> +
> +static int jlsemi_config_rgmii_delay(struct phy_device *phydev)
> +{
[ ... ]
> + if (phydev->interface != PHY_INTERFACE_MODE_RGMII_TXID)
> + rx_delay = phy_get_internal_delay(phydev, NULL, 0, true);
> +
> + if (phydev->interface != PHY_INTERFACE_MODE_RGMII_RXID)
> + tx_delay = phy_get_internal_delay(phydev, NULL, 0, false);
[ ... ]
> + if (rx_delay >= 0)
> + val_rxdly = rx_delay > 0 ? JLSEMI_RX_DELAY : 0;
> + if (tx_delay >= 0)
> + val_txdly = tx_delay > 0 ? JLSEMI_TX_DELAY : 0;
[Severity: Low]
With a NULL table and size 0, phy_get_internal_delay() returns the raw
picosecond value whenever rx-internal-delay-ps or tx-internal-delay-ps is
present. Any positive value is then mapped to the single fixed delay bit.
So a DT value like <100> or <500> silently enables the full fixed delay,
and unsupported values are never rejected. Would passing a delay table let
phy_get_internal_delay() validate the value or pick the nearest supported
one?
Also, the RX property is never read for rgmii-txid, and the TX property is
never read for rgmii-rxid. ethernet-phy.yaml says "If this property is
present then the PHY applies the RX delay". Is it intended that the
property is ignored in those modes?
[ ... ]
> +static int jlsemi_config_init(struct phy_device *phydev)
> +{
> + int ret;
> +
> + ret = jlsemi_disable_autonomous_eee(phydev);
> + if (ret < 0)
> + return ret;
[Severity: Medium]
Should this be called unconditionally from config_init? The same function
is also registered as .disable_autonomous_eee. phylib calls that op only
from phy_support_eee(), when the MAC says it handles LPI, and then sets
phydev->autonomous_eee_disabled. phy_init_hw() reapplies the disable only
when that flag is set.
Calling it here turns off autonomous EEE on every phy_init_hw(). That
includes MACs that never call phy_support_eee().
phy_probe() leaves EEE advertised by default. Couldn't such a MAC
negotiate EEE and then get LPI signalling it doesn't handle, or at least
lose the power savings of autonomous mode?
For comparison, rtl8211f_disable_autonomous_eee() is wired only through
the ops table and isn't called from config_init.
The commit message says:
- Autonomous EEE disable to prevent interoperability packet drops
Is there more detail on which packet drops this refers to?
[ ... ]
> +static int jlsemi_config_intr(struct phy_device *phydev)
> +{
[ ... ]
> + } else {
> + val = 0;
> + err = phy_write_paged(phydev, JLSEMI_INER_PAGE, JLSEMI_INER, val);
> + if (!err)
> + priv->iner = val;
> +
> + err = jlsemi_ack_interrupt(phydev);
> + }
[Severity: Medium]
Is the result of the INER write lost here? If phy_write_paged() fails and
the INSR read succeeds, err is overwritten with 0 and jlsemi_config_intr()
reports success.
phy_disable_interrupts() and phy_init_hw() would then assume the interrupt
sources are masked, while INER may still have LINK_STATUS enabled. Could
the PHY keep asserting its IRQ line after phylib has stopped handling it?
[ ... ]
> +static irqreturn_t jlsemi_handle_interrupt(struct phy_device *phydev)
> +{
> + int irq_status;
> +
> + irq_status = phy_read(phydev, JLSEMI_INSR);
> + if (irq_status < 0) {
> + phy_error(phydev);
> + return IRQ_NONE;
> + }
> +
> + if (irq_status & JLSEMI_INER_LINK_STATUS) {
> + phy_trigger_machine(phydev);
> + return IRQ_HANDLED;
> + }
[Severity: Low]
The INSR read is also what acknowledges the interrupt, since that read is
all jlsemi_ack_interrupt() does. What happens if LINK_STATUS and PME are
both latched in the same read?
The early return means pm_wakeup_event() is never called for a PME that
the hardware has already cleared. One example is a PME latched during
suspend plus a link change around resume, after jlsemi_resume() restores
INER.
Could both bits be handled from the single read?
> +
> + if (irq_status & JLSEMI_INER_PME) {
> + pm_wakeup_event(&phydev->mdio.dev, 0);
> + return IRQ_HANDLED;
> + }
[ ... ]
> +static int jlsemi_set_wol(struct phy_device *phydev, struct ethtool_wolinfo *wol)
> +{
[ ... ]
> + oldpage = phy_select_page(phydev, JLSEMI_PHYSICAL_ADDR_PAGE);
> + if (oldpage < 0)
> + return oldpage;
[Severity: High]
Does this return leave the MDIO bus lock held? phy_select_page() takes the
lock in phy_save_page() before it reads the page register, and it returns
errors with the lock still held:
drivers/net/phy/phy-core.c:phy_select_page() {
oldpage = ret = phy_save_page(phydev);
if (ret < 0)
return ret;
if (oldpage != page) {
ret = __phy_write_page(phydev, page);
if (ret < 0)
return ret;
}
...
}
Its kernel-doc says phy_restore_page() must always be called afterwards,
whether or not it succeeded. phy_restore_page() is also what unlocks the
bus.
If the page read or write fails with an MDIO error, this early return
skips the unlock. Every later access on that bus would then block forever,
including the state machine, the threaded PHY IRQ and other PHYs on the
same bus. The disable branch below has the same early return.
> +
> + __phy_write(phydev, JLSEMI_PHYSICAL_ADDR_WORD0,
> + mac_addr[1] << 8 | mac_addr[0]);
> + __phy_write(phydev, JLSEMI_PHYSICAL_ADDR_WORD1,
> + mac_addr[3] << 8 | mac_addr[2]);
> + __phy_write(phydev, JLSEMI_PHYSICAL_ADDR_WORD2,
> + mac_addr[5] << 8 | mac_addr[4]);
> +
> + ret = __phy_write(phydev, JLSEMI_PAGE_SELECT, JLSEMI_WOL_PAGE);
> + if (ret < 0)
> + goto restore;
> +
> + __phy_write(phydev, JLSEMI_WOL_SETTINGS_EVENTS,
> + JLSEMI_WOL_EVENT_MAGIC);
> + __phy_write(phydev, JLSEMI_WOL_RST_RMSQ,
> + JLSEMI_WOL_RMSQ);
> +restore:
> + phy_restore_page(phydev, oldpage, ret);
> + } else {
> + oldpage = phy_select_page(phydev, JLSEMI_WOL_PAGE);
> + if (oldpage < 0)
> + return oldpage;
> +
> + __phy_write(phydev, JLSEMI_WOL_SETTINGS_EVENTS, 0);
> + __phy_clear_bits(phydev, JLSEMI_WOL_RST_RMSQ,
> + JLSEMI_WOL_RG_RSTB);
> +
> + phy_restore_page(phydev, oldpage, 0);
> + }
> +
> + device_set_wakeup_enable(&phydev->mdio.dev, !!(wol->wolopts & WAKE_MAGIC));
> +
> + return 0;
> +}
[Severity: Medium]
Can jlsemi_set_wol() return anything other than 0 once the page has been
selected? The three MAC address writes and the SETTINGS_EVENTS and RST_RMSQ
writes are all unchecked.
If the switch to JLSEMI_WOL_PAGE fails, the code jumps to restore with
ret < 0. The return value of phy_restore_page() is discarded, though, and
the function still calls device_set_wakeup_enable() and returns 0.
In the disable branch, the __phy_write() and __phy_clear_bits() results
are ignored. phy_restore_page() gets a hard-coded 0 and its result is
discarded too.
phy_ethtool_set_wol() passes that 0 back to userspace. If the page restore
fails, register 0x1f is left on page 0xd8a or 0xd8c. Wouldn't later
unpaged accesses, such as INSR 0x1d in the IRQ handler or PHYCR2 0x19, hit
the wrong registers?
The realtek version at least returns the result of phy_restore_page().
> +
> +static int jlsemi_suspend(struct phy_device *phydev)
> +{
> + int ret;
> +
> + if (device_may_wakeup(&phydev->mdio.dev)) {
> + ret = phy_write_paged(phydev, JLSEMI_INER_PAGE, JLSEMI_INER,
> + JLSEMI_INER_PME);
> + if (ret < 0)
> + return ret;
> +
> + jlsemi_ack_interrupt(phydev);
> + return 0;
> + }
[Severity: Medium]
Is the WoL block ever taken out of reset? jlsemi_set_wol() writes
JLSEMI_WOL_RMSQ (0x1fff) to page 0xd8a reg 17, and that clears RG_RSTB.
For the same RTL8211F register, the realtek driver's comment says "Set the
maximum packet size, and assert WoL reset". Nothing in this driver sets
RG_RSTB again.
rtl8211f_suspend() pulses RG_RSTB low and then high after enabling the PME
interrupt:
drivers/net/phy/realtek/realtek_main.c:rtl8211f_suspend() {
...
/* Reset the WoL to ensure that an event is picked up.
* Unless we do this, even if we receive another packet,
* we may not have a PME interrupt raised.
*/
ret = phy_read_paged(phydev, RTL8211F_WOL_PAGE,
RTL8211F_WOL_RST_RMSQ);
...
wol_rst = ret & ~RTL8211F_WOL_RG_RSTB;
ret = phy_write_paged(phydev, RTL8211F_WOL_PAGE,
RTL8211F_WOL_RST_RMSQ, wol_rst);
...
wol_rst |= RTL8211F_WOL_RG_RSTB;
ret = phy_write_paged(phydev, RTL8211F_WOL_PAGE,
RTL8211F_WOL_RST_RMSQ, wol_rst);
...
}
jlsemi_suspend() has the INER=PME write and the INSR read, but not the
pulse.
If the JLSemi part has the same RG_RSTB semantics, wouldn't the magic
packet matcher stay in reset? PME would then never be raised, even after
the wakeup capability problem is fixed.
[ ... ]
> +static int jlsemi_led_hw_control_get(struct phy_device *phydev, u8 index,
> + unsigned long *rules)
> +{
[ ... ]
> + if ((val & JLSEMI_LEDCR_LINK_10) &&
> + (val & JLSEMI_LEDCR_LINK_100) &&
> + (val & JLSEMI_LEDCR_LINK_1000)) {
> + __set_bit(TRIGGER_NETDEV_LINK, rules);
> + }
[Severity: Medium]
When all three speed bits are set, this returns LINK_10, LINK_100 and
LINK_1000 plus TRIGGER_NETDEV_LINK. That is also the hardware state that
jlsemi_led_hw_control_set() writes for a plain link rule.
netdev_trig_activate() copies the bitmap straight into trigger_data->mode.
netdev_led_attr_store() then returns -EINVAL for any write that leaves
LINK set together with a LINK_xx bit:
drivers/leds/trigger/ledtrig-netdev.c:netdev_led_attr_store() {
...
if (test_bit(TRIGGER_NETDEV_LINK, &mode) &&
(test_bit(TRIGGER_NETDEV_LINK_10, &mode) ||
test_bit(TRIGGER_NETDEV_LINK_100, &mode) ||
test_bit(TRIGGER_NETDEV_LINK_1000, &mode) ||
...
}
Wouldn't toggling rx, tx, link_10 or link_1000 from sysfs then fail with
-EINVAL until link is cleared first? Should this return either the
aggregate LINK rule or the individual speed rules, but not both?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261016161543.444356-1-cyan2254650260%40gmail.com
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH net-next] net: phy: Add JLSemi JL21xx Gigabit Ethernet PHY driver
2026-10-20 17:15 ` netdev-bot+sashiko
@ 2026-10-20 17:39 ` Andrew Lunn
0 siblings, 0 replies; 4+ messages in thread
From: Andrew Lunn @ 2026-10-20 17:39 UTC (permalink / raw)
To: netdev-bot+sashiko
Cc: cyan2254650260, davem, edumazet, hkallweit1, kuba, pabeni, linux,
linux-kernel, netdev
On Tue, Oct 20, 2026 at 05:15:23PM +0000, netdev-bot+sashiko@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 10 potential
> issue(s) to consider.
>
> Critical: 0 · High: 1 · Medium: 6 · Low: 3
>
> If you believe none of the issues are legitimate, please reply to each
> issue of Medium severity or higher and explain why the AI is wrong.
> If any of them are legitimate and you plan to respin, please let patchwork
> know by including "pw-bot: cr" as a separate line at the end of your reply
> (one such reply per series is enough).
>
> - [High] jlsemi_set_wol() can leave the MDIO bus mutex held permanently.
> - [Medium] Wake-on-LAN cannot work, yet the driver tells userspace it is
> enabled.
> - [Medium] When WoL is enabled, jlsemi_set_wol() writes JLSEMI_WOL_RMSQ
> (0x1fff) to page 0xd8a reg 17.
> - [Medium] jlsemi_config_init() always calls
> jlsemi_disable_autonomous_eee() (clears PHYCR2 bit 5) on every…
> - [Medium] When the LED's three link-speed bits are all set,
> jlsemi_led_hw_control_get() returns TRIGGER_NETDEV_LINK_10, LINK_100…
> - [Medium] jlsemi_set_wol() ignores every hardware-programming error and
> always returns 0.
> - [Medium] In the interrupt-disable branch of jlsemi_config_intr(), `err
> = phy_write_paged(phydev, JLSEMI_INER_PAGE, JLSEMI_INER, 0)` is never…
> - [Low] The register map and most of the logic in
> drivers/net/phy/jlsemi.c are a near-verbatim copy of the RTL8211F…
> - [Low] jlsemi_config_rgmii_delay() handles rx-/tx-internal-delay-ps
> inconsistently.
> - [Low] jlsemi_handle_interrupt() reads INSR (0x1d) once.
>
> --
>
> net: phy: Add JLSemi JL21xx Gigabit Ethernet PHY driver
>
> This commit adds a new phylib driver for the JLSemi JL2101, JL2121 and
> generic JL21xx Gigabit PHYs. It covers paged register access, RGMII delay
> configuration, vendor status register parsing, interrupts, hardware LED
> control, magic packet Wake-on-LAN, autonomous EEE disable and
> suspend/resume.
>
> Link: https://www.jlsemi.com/
>
> > diff --git a/drivers/net/phy/jlsemi.c b/drivers/net/phy/jlsemi.c
> > new file mode 100644
> > index 0000000000000..c3acff48d4618
> > --- /dev/null
> > +++ b/drivers/net/phy/jlsemi.c
> > @@ -0,0 +1,551 @@
> > +// SPDX-License-Identifier: GPL-2.0+
> > +/*
> > + * Driver for JLSemi Gigabit Ethernet PHYs (JL2121 / JL2101)
> > + *
> > + * Copyright (C) 2026 Pufan Jin <cyan2254650260@gmail.com>
> > + */
>
> [ ... ]
>
> > +/* Wake-on-LAN settings */
> > +#define JLSEMI_WOL_PAGE 0xd8a
> > +#define JLSEMI_WOL_SETTINGS_EVENTS 16
> > +#define JLSEMI_WOL_EVENT_MAGIC BIT(12)
> > +#define JLSEMI_WOL_RST_RMSQ 17
> > +#define JLSEMI_WOL_RG_RSTB BIT(15)
> > +#define JLSEMI_WOL_RMSQ 0x1fff
>
> [Severity: Low]
> This isn't a bug, but the register map looks identical to the RTL8211F
> support in drivers/net/phy/realtek/realtek_main.c. That covers PHYCR2 0x19
> bit 5, INSR 0x1d, INER page 0xa42 reg 0x12, RGMII page 0xd08 regs
> 0x11/0x15, WoL page 0xd8a regs 16/17 with RG_RSTB BIT(15) and RMSQ 0x1fff,
> MAC page 0xd8c, and LEDCR page 0xd04 reg 0x10 with 5-bit per-LED fields.
>
> jlsemi_led_hw_is_supported() and jlsemi_led_hw_control_get() also follow
> rtl8211x_led_hw_is_supported() and rtl8211f_led_hw_control_get() closely.
> The same goes for the set_wol, suspend and resume flows.
Interesting. Please see if this really is register compatible. If it
is, please extend the RTL8211F to cover this device.
Andrew
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-20 17:40 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-16 16:15 [PATCH net-next] net: phy: Add JLSemi JL21xx Gigabit Ethernet PHY driver Pufan Jin
2026-10-16 17:14 ` Andrew Lunn
2026-10-20 17:15 ` netdev-bot+sashiko
2026-10-20 17:39 ` Andrew Lunn
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®