[SOLVED] OEM SFP-10G-T (RTL8261BE/CE) working at 10G on BPI-R4 — kernel patch
Hardware: Banana Pi BPI-R4 (MT7988A), SFP2 port
Module: OEM SFP-10G-T — Realtek RTL8261BE-CG copper 10G SFP+
Kernel: Linux 6.12 (OpenWrt 25.12)
The Problem
The OEM SFP-10G-T module (Realtek RTL8261BE-CG) is incorrectly matched by the kernel
as a ROLLBALL module and fails to establish any link:
sfp sfp2: module OEM SFP-10G-T rev A has been found in the quirk list
sfp sfp2: probing phy device through the [MDIO_I2C_ROLLBALL] protocol
sfp sfp2: no PHY detected, 24 tries left
sfp sfp2: no PHY detected, 23 tries left
...
Root cause: The sfp_quirk_rollball_cc quirk in drivers/net/phy/sfp.c was written
for modules with a Marvell 88X3310 PHY behind a RollBall I2C-to-MDIO bridge.
The RTL8261BE (and RTL8261CE) is a pure Media Converter — it has no I2C-to-MDIO
bridge at all. Sending ROLLBALL protocol commands to it results in infinite
"no PHY detected" retries.
The RTL8261BE auto-switches its SerDes speed based on copper autoneg:
- 10G copper → 10GBASE-R SerDes
- 2.5G copper → 2500BASE-X SerDes
- 1G copper → 1000BASE-X SerDes
No MDIO access is needed or possible. The MAC just needs to use the EEPROM-declared
link mode (10gbase-r) directly.
The Fix
Instead of blindly applying the ROLLBALL quirk, we probe for the I2C-to-MDIO bridge
first. Real ROLLBALL modules respond with CMD_DONE within ~70 ms. Modules without a
bridge (RTL8261BE, RTL8261CE) never respond — the probe times out after 200 ms and
we fall back to MDIO_I2C_NONE.
This approach is safe for all existing ROLLBALL modules and fixes RTL8261BE/CE
without adding new per-chip quirks.
Patch for drivers/net/phy/sfp.c (applies on top of upstream 6.12):
--- a/drivers/net/phy/sfp.c
+++ b/drivers/net/phy/sfp.c
@@ sfp_fixup_potron / sfp_fixup_rollball_cc @@
+/* Local mirror of rollball constants from mdio-i2c.c */
+#define SFP_ROLLBALL_PHY_ADDR 0x51
+#define SFP_ROLLBALL_MDIO_PAGE 3
+#define SFP_ROLLBALL_CMD_ADDR 0x80
+#define SFP_ROLLBALL_CMD_READ 0x02
+#define SFP_ROLLBALL_CMD_DONE 0x04
+
+static int sfp_rollball_a2_write(struct sfp *sfp, u8 reg, const u8 *data, int len)
+{
+ struct i2c_msg msg;
+ u8 buf[8];
+
+ buf[0] = reg;
+ memcpy(buf + 1, data, len);
+ msg.addr = SFP_ROLLBALL_PHY_ADDR;
+ msg.flags = 0;
+ msg.len = len + 1;
+ msg.buf = buf;
+ return i2c_transfer(sfp->i2c, &msg, 1) == 1 ? 0 : -EIO;
+}
+
+static int sfp_rollball_a2_read1(struct sfp *sfp, u8 reg, u8 *val)
+{
+ struct i2c_msg msgs[2];
+
+ msgs[0].addr = SFP_ROLLBALL_PHY_ADDR;
+ msgs[0].flags = 0;
+ msgs[0].len = 1;
+ msgs[0].buf = ®
+ msgs[1].addr = SFP_ROLLBALL_PHY_ADDR;
+ msgs[1].flags = I2C_M_RD;
+ msgs[1].len = 1;
+ msgs[1].buf = val;
+ return i2c_transfer(sfp->i2c, msgs, 2) == 2 ? 0 : -EIO;
+}
+
+/* Probe for a RollBall I2C-to-MDIO bridge by sending the unlock password,
+ * switching to the MDIO page, issuing CMD_READ and polling for CMD_DONE.
+ * A real RollBall bridge asserts CMD_DONE within ~70 ms; modules without a
+ * bridge (e.g. RTL8261BE pure media converter) never assert it, so the poll
+ * times out after 200 ms. Returns true only when CMD_DONE is observed.
+ */
+static bool sfp_has_rollball_bridge(struct sfp *sfp)
+{
+ u8 pw[4] = { 0xff, 0xff, 0xff, 0xff };
+ u8 page = SFP_ROLLBALL_MDIO_PAGE;
+ u8 cmd = SFP_ROLLBALL_CMD_READ;
+ u8 saved_page = 0, res;
+ int i;
+
+ if (sfp_rollball_a2_write(sfp, SFP_VSL + 3, pw, sizeof(pw)) < 0)
+ return false;
+
+ sfp_rollball_a2_read1(sfp, SFP_PAGE, &saved_page);
+
+ if (sfp_rollball_a2_write(sfp, SFP_PAGE, &page, 1) < 0 ||
+ sfp_rollball_a2_write(sfp, SFP_ROLLBALL_CMD_ADDR, &cmd, 1) < 0)
+ goto restore;
+
+ for (i = 0; i < 10; i++) {
+ msleep(20);
+ if (sfp_rollball_a2_read1(sfp, SFP_ROLLBALL_CMD_ADDR, &res) == 0 &&
+ res == SFP_ROLLBALL_CMD_DONE) {
+ sfp_rollball_a2_write(sfp, SFP_PAGE, &saved_page, 1);
+ return true;
+ }
+ }
+
+restore:
+ sfp_rollball_a2_write(sfp, SFP_PAGE, &saved_page, 1);
+ return false;
+}
+
static void sfp_fixup_rollball_cc(struct sfp *sfp)
-{
- sfp_fixup_rollball(sfp);
-
- /* Some RollBall SFPs may have wrong (zero) extended compliance code
- * burned in EEPROM. For PHY probing we need the correct one.
- */
- sfp->id.base.extended_cc = SFF8024_ECC_10GBASE_T_SFI;
-}
+{
+ /* Probe for I2C-to-MDIO bridge: real RollBall modules assert CMD_DONE
+ * within ~70 ms; pure media converters (e.g. RTL8261BE) have no bridge
+ * and the probe times out after 200 ms -- skip PHY probing for those.
+ */
+ if (!sfp_has_rollball_bridge(sfp)) {
+ sfp->mdio_protocol = MDIO_I2C_NONE;
+ return;
+ }
+ sfp_fixup_rollball(sfp);
+ sfp->id.base.extended_cc = SFF8024_ECC_10GBASE_T_SFI;
+}
/* Add SFP-10G-T-I (industrial grade, same chip) to the quirk list */
- SFP_QUIRK_F("OEM", "SFP-10G-T", sfp_fixup_rollball_cc),
+ SFP_QUIRK_F("OEM", "SFP-10G-T-I", sfp_fixup_rollball_cc),
+ SFP_QUIRK_F("OEM", "SFP-10G-T", sfp_fixup_rollball_cc),
Results
dmesg — after patch (working)
sfp sfp2: probing phy device through the [MDIO_I2C_NONE] protocol
mtk_soc_eth 15100000.ethernet sfp-lan: Link is Up - 10Gbps/Full - flow control off
iperf3 — BPI-R4 ↔ BPI-R4 via OEM SFP-10G-T + Cat5e 2m
[SUM] 0.00-10.00 sec 11.0 GBytes 9.41 Gbits/sec 2900 sender
[SUM] 0.00-10.01 sec 10.9 GBytes 9.39 Gbits/sec receiver
9.41 Gbits/sec — 94% of 10GbE line rate.
Notes
- Tested on OpenWrt 25.12 / Linux 6.12, BPI-R4 MT7988A
- Covers
SFP-10G-TandSFP-10G-T-I(industrial grade, same chip) - Should also fix RTL8261CE — same architecture, no I2C-to-MDIO bridge
- Existing ROLLBALL modules (Marvell 88X3310) are unaffected — probe succeeds
and they continue to useMDIO_I2C_ROLLBALLas before - Multi-speed limitation: MAC is locked to 10gbase-r. If the link partner
only supports 2.5G or 1G, traffic will not pass (SerDes/MAC speed mismatch).
Suitable for 10G-only setups. Full multi-speed support is not feasible —
the RTL8261BE in Media Converter mode has no I2C-to-MDIO bridge, making
runtime SerDes speed detection impossible. - Candidate for upstream
drivers/net/phy/sfp.c