[PATCH net-next v1] net: phy: sfp: detect presence via I2C when no MOD_DEF0 GPIO

From: Greg Patrick

Date: Tue Jun 02 2026 - 19:56:40 EST


Boards that do not wire the SFP cage MOD_DEF0 signal to a GPIO (for
example the NicGiga S100-0800S-M, where the I/O expander is fully
consumed by TX_DISABLE) currently use sff_gpio_get_state(), which
unconditionally reports the module as present. An empty cage therefore
fails its probe and is parked in SFP_MOD_ERROR forever; because
SFP_F_PRESENT never deasserts there is no REMOVE event to recover the
state machine, so a module inserted after boot is never detected.

Derive presence from a throttled single-byte I2C read of the module
EEPROM instead: an ACK asserts SFP_F_PRESENT, two consecutive failures
clear it (to ride out a transient error on a live module). The existing
poll then emits SFP_E_INSERT / SFP_E_REMOVE normally, giving working
hot-plug and avoiding the boot-time -EIO spam on empty cages.

Signed-off-by: Greg Patrick <gregspatrick@xxxxxxxxxxx>
---
Posting for review per a downstream discussion (OpenWrt PR #23579) on adding
the NicGiga S100-0800S-M, an RTL9303 8x SFP+ switch that wires none of the
cage sideband signals to a software-visible GPIO. I'd value your steer before
this is carried downstream.

Open design question: this changes behaviour for *all* boards without a
MOD_DEF0 GPIO, from "always present" (sff_gpio_get_state) to I2C-probed
presence. That fixes hot-plug and the empty-cage -EIO spam on boards that
simply lack a wired MOD_ABS, but it also affects boards that intentionally
carry a permanently-fitted module and relied on the always-present path.
Would you prefer this gated behind a DT opt-in property rather than changing
the default? Happy to respin either way. The 2s re-probe interval is likewise
a first guess.

Tested on a NicGiga S100-0800S-M (RTL9303, 8x SFP+, no MOD_DEF0 GPIO):
inserting a module into a cage that was empty at boot now links within ~2s,
removal is detected, and the boot-time -EIO burst on empty cages is gone. No
behaviour change observed on cages populated at boot.

drivers/net/phy/sfp.c | 74 ++++++++++++++++++++++++++++++++++++++-----
1 file changed, 66 insertions(+), 8 deletions(-)

diff --git a/drivers/net/phy/sfp.c b/drivers/net/phy/sfp.c
index 376c705a9..fd99638e9 100644
--- a/drivers/net/phy/sfp.c
+++ b/drivers/net/phy/sfp.c
@@ -206,6 +206,11 @@ static const enum gpiod_flags gpio_flags[] = {
#define T_PROBE_RETRY_SLOW msecs_to_jiffies(5000)
#define R_PROBE_RETRY_SLOW 12

+/* Interval at which sfp_i2c_get_state() re-probes the I2C bus for module
+ * presence on boards without a MOD_DEF0 GPIO (see sfp_i2c_get_state()).
+ */
+#define T_PROBE_PRESENT msecs_to_jiffies(2000)
+
/* SFP modules appear to always have their PHY configured for bus address
* 0x56 (which with mdio-i2c, translates to a PHY address of 22).
* RollBall SFPs access phy via SFP Enhanced Digital Diagnostic Interface
@@ -249,6 +254,13 @@ struct sfp {

bool need_poll;

+ /* I2C-probed presence, for boards without a MOD_DEF0 GPIO.
+ * Access rules: st_mutex held (updated from the poll/state machine).
+ */
+ bool i2c_present;
+ u8 i2c_present_nak;
+ unsigned long i2c_present_next;
+
/* Access rules:
* state_hw_drive: st_mutex held
* state_hw_mask: st_mutex held
@@ -651,11 +663,6 @@ static unsigned int sfp_gpio_get_state(struct sfp *sfp)
return state;
}

-static unsigned int sff_gpio_get_state(struct sfp *sfp)
-{
- return sfp_gpio_get_state(sfp) | SFP_F_PRESENT;
-}
-
static void sfp_gpio_set_state(struct sfp *sfp, unsigned int state)
{
unsigned int drive;
@@ -863,6 +870,44 @@ static int sfp_read(struct sfp *sfp, bool a2, u8 addr, void *buf, size_t len)
return sfp->read(sfp, a2, addr, buf, len);
}

+/* Probe whether a module is physically present by attempting a single-byte
+ * I2C read of the EEPROM identifier (an empty cage NAKs). Used as the presence
+ * source on boards that do not wire MOD_DEF0 to a GPIO.
+ */
+static bool sfp_module_present_i2c(struct sfp *sfp)
+{
+ u8 id;
+
+ return sfp_read(sfp, false, SFP_PHYS_ID, &id, sizeof(id)) == sizeof(id);
+}
+
+/* get_state variant for boards without a MOD_DEF0 GPIO. Instead of assuming
+ * the module is always present, derive SFP_F_PRESENT from a throttled I2C
+ * probe so that hot-insertion and removal are detected. A single ACK asserts
+ * presence; two consecutive failures clear it, to ride out a transient I2C
+ * error on a live module.
+ */
+static unsigned int sfp_i2c_get_state(struct sfp *sfp)
+{
+ unsigned int state = sfp_gpio_get_state(sfp);
+
+ if (time_after_eq(jiffies, sfp->i2c_present_next)) {
+ if (sfp_module_present_i2c(sfp)) {
+ sfp->i2c_present = true;
+ sfp->i2c_present_nak = 0;
+ } else if (sfp->i2c_present && ++sfp->i2c_present_nak >= 2) {
+ sfp->i2c_present = false;
+ sfp->i2c_present_nak = 0;
+ }
+ sfp->i2c_present_next = jiffies + T_PROBE_PRESENT;
+ }
+
+ if (sfp->i2c_present)
+ state |= SFP_F_PRESENT;
+
+ return state;
+}
+
static int sfp_write(struct sfp *sfp, bool a2, u8 addr, void *buf, size_t len)
{
return sfp->write(sfp, a2, addr, buf, len);
@@ -3168,9 +3213,22 @@ static int sfp_probe(struct platform_device *pdev)
sfp->get_state = sfp_gpio_get_state;
sfp->set_state = sfp_gpio_set_state;

- /* Modules that have no detect signal are always present */
- if (!(sfp->gpio[GPIO_MODDEF0]))
- sfp->get_state = sff_gpio_get_state;
+ /* Boards with no MOD_DEF0 GPIO have no hardware presence signal. Rather
+ * than assume the module is always present (which traps an empty cage
+ * in MOD_ERROR and never detects hot-insertion), derive presence from a
+ * throttled I2C probe and poll for changes. sfp_i2c_configure() has
+ * already set i2c_max_block_size; seed i2c_block_size so the presence
+ * read does not issue a zero-length transfer before the first probe.
+ * Seed i2c_present_next to jiffies so the first probe happens
+ * immediately (a zero value would be in the past relative to the
+ * negative INITIAL_JIFFIES at boot and delay detection).
+ */
+ if (!(sfp->gpio[GPIO_MODDEF0])) {
+ sfp->get_state = sfp_i2c_get_state;
+ sfp->i2c_block_size = sfp->i2c_max_block_size;
+ sfp->i2c_present_next = jiffies;
+ sfp->need_poll = true;
+ }

device_property_read_u32(&pdev->dev, "maximum-power-milliwatt",
&sfp->max_power_mW);
--
2.53.0