Re: [Intel-wired-lan] [PATCH iwl-next v5 4/4] igc: add support for forcing link speed without autonegotiation

From: Kadosh, MoriyaX

Date: Mon Jun 22 2026 - 04:20:36 EST




On 14/06/2026 10:17, Ruinskiy, Dima wrote:
On 08/05/2026 0:47, KhaiWenTan wrote:
From: Faizal Rahim <faizal.abdul.rahim@xxxxxxxxxxxxxxx>

Allow users to force 10/100 Mb/s link speed and duplex via ethtool
when autonegotiation is disabled. Previously, the driver rejected
these requests with "Force mode currently not supported.".

Forcing at 1000 Mb/s and 2500 Mb/s is not supported.

Reviewed-by: Looi Hong Aun <hong.aun.looi@xxxxxxxxx>
Signed-off-by: Faizal Rahim <faizal.abdul.rahim@xxxxxxxxxxxxxxx>
Signed-off-by: Khai Wen Tan <khai.wen.tan@xxxxxxxxxxxxxxx>
---
  drivers/net/ethernet/intel/igc/igc_base.c    |  35 ++++-
  drivers/net/ethernet/intel/igc/igc_defines.h |   9 +-
  drivers/net/ethernet/intel/igc/igc_ethtool.c | 138 ++++++++++++++-----
  drivers/net/ethernet/intel/igc/igc_hw.h      |   9 ++
  drivers/net/ethernet/intel/igc/igc_mac.c     |  12 ++
  drivers/net/ethernet/intel/igc/igc_main.c    |   2 +-
  drivers/net/ethernet/intel/igc/igc_phy.c     |  65 ++++++++-
  drivers/net/ethernet/intel/igc/igc_phy.h     |   1 +
  8 files changed, 220 insertions(+), 51 deletions(-)

diff --git a/drivers/net/ethernet/intel/igc/igc_base.c b/drivers/net/ ethernet/intel/igc/igc_base.c
index 1613b562d17c..ab9120a3127f 100644
--- a/drivers/net/ethernet/intel/igc/igc_base.c
+++ b/drivers/net/ethernet/intel/igc/igc_base.c
@@ -114,11 +114,35 @@ static s32 igc_setup_copper_link_base(struct igc_hw *hw)
      u32 ctrl;
      ctrl = rd32(IGC_CTRL);
-    ctrl |= IGC_CTRL_SLU;
-    ctrl &= ~(IGC_CTRL_FRCSPD | IGC_CTRL_FRCDPX);
-    wr32(IGC_CTRL, ctrl);
-
-    ret_val = igc_setup_copper_link(hw);
+    ctrl &= ~(IGC_CTRL_FRCSPD | IGC_CTRL_FRCDPX |
+          IGC_CTRL_SPEED_MASK | IGC_CTRL_FD);
+
+    if (hw->mac.autoneg_enabled) {
+        ctrl |= IGC_CTRL_SLU;
+        wr32(IGC_CTRL, ctrl);
+        ret_val = igc_setup_copper_link(hw);
+    } else {
+        ctrl |= IGC_CTRL_SLU | IGC_CTRL_FRCSPD | IGC_CTRL_FRCDPX;
+
+        switch (hw->mac.forced_speed_duplex) {
+        case IGC_FORCED_10H:
+            ctrl |= IGC_CTRL_SPEED_10;
+            break;
+        case IGC_FORCED_10F:
+            ctrl |= IGC_CTRL_SPEED_10 | IGC_CTRL_FD;
+            break;
+        case IGC_FORCED_100H:
+            ctrl |= IGC_CTRL_SPEED_100;
+            break;
+        case IGC_FORCED_100F:
+            ctrl |= IGC_CTRL_SPEED_100 | IGC_CTRL_FD;
+            break;
+        default:
+            return -IGC_ERR_CONFIG;
+        }
+        wr32(IGC_CTRL, ctrl);
+        ret_val = igc_setup_copper_link(hw);
+    }
      return ret_val;
  }
@@ -443,6 +467,7 @@ static const struct igc_phy_operations igc_phy_ops_base = {
      .reset            = igc_phy_hw_reset,
      .read_reg        = igc_read_phy_reg_gpy,
      .write_reg        = igc_write_phy_reg_gpy,
+    .force_speed_duplex    = igc_force_speed_duplex,
  };
  const struct igc_info igc_base_info = {
diff --git a/drivers/net/ethernet/intel/igc/igc_defines.h b/drivers/ net/ethernet/intel/igc/igc_defines.h
index 9482ab11f050..3f504751c2d9 100644
--- a/drivers/net/ethernet/intel/igc/igc_defines.h
+++ b/drivers/net/ethernet/intel/igc/igc_defines.h
@@ -129,10 +129,13 @@
  #define IGC_ERR_SWFW_SYNC        13
  /* Device Control */
+#define IGC_CTRL_FD        BIT(0)  /* Full Duplex */
  #define IGC_CTRL_RST        0x04000000  /* Global reset */
-
  #define IGC_CTRL_PHY_RST    0x80000000  /* PHY Reset */
  #define IGC_CTRL_SLU        0x00000040  /* Set link up (Force Link) */
+#define IGC_CTRL_SPEED_MASK    GENMASK(10, 8)
+#define IGC_CTRL_SPEED_10    FIELD_PREP(IGC_CTRL_SPEED_MASK, 0)
+#define IGC_CTRL_SPEED_100    FIELD_PREP(IGC_CTRL_SPEED_MASK, 1)
  #define IGC_CTRL_FRCSPD        0x00000800  /* Force Speed */
  #define IGC_CTRL_FRCDPX        0x00001000  /* Force Duplex */
  #define IGC_CTRL_VME        0x40000000  /* IEEE VLAN mode enable */
@@ -673,6 +676,10 @@
  #define IGC_GEN_POLL_TIMEOUT    1920
  /* PHY Control Register */
+#define MII_CR_SPEED_MASK    (BIT(6) | BIT(13))
+#define MII_CR_SPEED_10        0x0000    /* SSM=0, SSL=0: 10 Mb/s */
+#define MII_CR_SPEED_100    BIT(13)    /* SSM=0, SSL=1: 100 Mb/s */
+#define MII_CR_DUPLEX_EN    BIT(8)    /* 0 = Half Duplex, 1 = Full Duplex */
  #define MII_CR_RESTART_AUTO_NEG    0x0200  /* Restart auto negotiation */
  #define MII_CR_POWER_DOWN    0x0800  /* Power down */
  #define MII_CR_AUTO_NEG_EN    0x1000  /* Auto Neg Enable */
diff --git a/drivers/net/ethernet/intel/igc/igc_ethtool.c b/drivers/ net/ethernet/intel/igc/igc_ethtool.c
index cfcbf2fdad6e..b103836a895f 100644
--- a/drivers/net/ethernet/intel/igc/igc_ethtool.c
+++ b/drivers/net/ethernet/intel/igc/igc_ethtool.c
@@ -1914,44 +1914,58 @@ static int igc_ethtool_get_link_ksettings(struct net_device *netdev,
      ethtool_link_ksettings_add_link_mode(cmd, supported, TP);
      ethtool_link_ksettings_add_link_mode(cmd, advertising, TP);
-    /* advertising link modes */
-    if (hw->phy.autoneg_advertised & ADVERTISE_10_HALF)
-        ethtool_link_ksettings_add_link_mode(cmd, advertising, 10baseT_Half);
-    if (hw->phy.autoneg_advertised & ADVERTISE_10_FULL)
-        ethtool_link_ksettings_add_link_mode(cmd, advertising, 10baseT_Full);
-    if (hw->phy.autoneg_advertised & ADVERTISE_100_HALF)
-        ethtool_link_ksettings_add_link_mode(cmd, advertising, 100baseT_Half);
-    if (hw->phy.autoneg_advertised & ADVERTISE_100_FULL)
-        ethtool_link_ksettings_add_link_mode(cmd, advertising, 100baseT_Full);
-    if (hw->phy.autoneg_advertised & ADVERTISE_1000_FULL)
-        ethtool_link_ksettings_add_link_mode(cmd, advertising, 1000baseT_Full);
-    if (hw->phy.autoneg_advertised & ADVERTISE_2500_FULL)
-        ethtool_link_ksettings_add_link_mode(cmd, advertising, 2500baseT_Full);
-
      /* set autoneg settings */
      ethtool_link_ksettings_add_link_mode(cmd, supported, Autoneg);
-    ethtool_link_ksettings_add_link_mode(cmd, advertising, Autoneg);
+    if (hw->mac.autoneg_enabled) {
+        ethtool_link_ksettings_add_link_mode(cmd, advertising, Autoneg);
+        cmd->base.autoneg = AUTONEG_ENABLE;
+
+        /* advertising link modes only apply when autoneg is on */
+        if (hw->phy.autoneg_advertised & ADVERTISE_10_HALF)
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 10baseT_Half);
+        if (hw->phy.autoneg_advertised & ADVERTISE_10_FULL)
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 10baseT_Full);
+        if (hw->phy.autoneg_advertised & ADVERTISE_100_HALF)
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 100baseT_Half);
+        if (hw->phy.autoneg_advertised & ADVERTISE_100_FULL)
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 100baseT_Full);
+        if (hw->phy.autoneg_advertised & ADVERTISE_1000_FULL)
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 1000baseT_Full);
+        if (hw->phy.autoneg_advertised & ADVERTISE_2500_FULL)
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 2500baseT_Full);
+
+        /* Set pause flow control advertising */
+        switch (hw->fc.requested_mode) {
+        case igc_fc_full:
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 Pause);
+            break;
+        case igc_fc_rx_pause:
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 Pause);
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 Asym_Pause);
+            break;
+        case igc_fc_tx_pause:
+            ethtool_link_ksettings_add_link_mode(cmd, advertising,
+                                 Asym_Pause);
+            break;
+        default:
+            break;
+        }
+    } else {
+        cmd->base.autoneg = AUTONEG_DISABLE;
+    }
-    /* Set pause flow control settings */
+    /* Pause is always supported */
      ethtool_link_ksettings_add_link_mode(cmd, supported, Pause);
-    switch (hw->fc.requested_mode) {
-    case igc_fc_full:
-        ethtool_link_ksettings_add_link_mode(cmd, advertising, Pause);
-        break;
-    case igc_fc_rx_pause:
-        ethtool_link_ksettings_add_link_mode(cmd, advertising, Pause);
-        ethtool_link_ksettings_add_link_mode(cmd, advertising,
-                             Asym_Pause);
-        break;
-    case igc_fc_tx_pause:
-        ethtool_link_ksettings_add_link_mode(cmd, advertising,
-                             Asym_Pause);
-        break;
-    default:
-        break;
-    }
-
      status = pm_runtime_suspended(&adapter->pdev->dev) ?
           0 : rd32(IGC_STATUS);
@@ -1983,7 +1997,6 @@ static int igc_ethtool_get_link_ksettings(struct net_device *netdev,
          cmd->base.duplex = DUPLEX_UNKNOWN;
      }
      cmd->base.speed = speed;
-    cmd->base.autoneg = AUTONEG_ENABLE;
      /* MDI-X => 2; MDI =>1; Invalid =>0 */
      if (hw->phy.media_type == igc_media_type_copper)
@@ -2000,6 +2013,37 @@ static int igc_ethtool_get_link_ksettings(struct net_device *netdev,
      return 0;
  }
+/**
+ * igc_handle_autoneg_disabled - Configure forced speed/duplex settings
+ * @adapter: private driver structure
+ * @speed: requested speed (must be SPEED_10 or SPEED_100)
+ * @duplex: requested duplex
+ *
+ * Records forced speed/duplex when autoneg is disabled.
+ * Caller must validate speed before calling this function.
+ */
+static void igc_handle_autoneg_disabled(struct igc_adapter *adapter, u32 speed,
+                    u8 duplex)
+{
+    struct igc_mac_info *mac = &adapter->hw.mac;
+
+    switch (speed) {
+    case SPEED_10:
+        mac->forced_speed_duplex = (duplex == DUPLEX_FULL) ?
+            IGC_FORCED_10F : IGC_FORCED_10H;
+        break;
+    case SPEED_100:
+        mac->forced_speed_duplex = (duplex == DUPLEX_FULL) ?
+            IGC_FORCED_100F : IGC_FORCED_100H;
+        break;
+    default:
+        WARN_ONCE(1, "Unsupported speed %u\n", speed);
+        return;
+    }
+
+    mac->autoneg_enabled = false;
+}
+
  /**
   * igc_handle_autoneg_enabled - Configure autonegotiation advertisement
   * @adapter: private driver structure
@@ -2038,6 +2082,7 @@ static void igc_handle_autoneg_enabled(struct igc_adapter *adapter,
                            10baseT_Half))
          advertised |= ADVERTISE_10_HALF;
+    hw->mac.autoneg_enabled = true;
      hw->phy.autoneg_advertised = advertised;
      if (adapter->fc_autoneg)
          hw->fc.requested_mode = igc_fc_default;
@@ -2059,6 +2104,12 @@ igc_ethtool_set_link_ksettings(struct net_device *netdev,
          return -EINVAL;
      }
+    if (cmd->base.autoneg != AUTONEG_ENABLE &&
+        cmd->base.autoneg != AUTONEG_DISABLE) {
+        netdev_info(dev, "Unsupported autoneg setting\n");
+        return -EINVAL;
+    }
+
      /* MDI setting is only allowed when autoneg enabled because
       * some hardware doesn't allow MDI setting when speed or
       * duplex is forced.
@@ -2071,14 +2122,25 @@ igc_ethtool_set_link_ksettings(struct net_device *netdev,
          }
      }
+    if (cmd->base.autoneg == AUTONEG_DISABLE) {
+        if (cmd->base.speed != SPEED_10 && cmd->base.speed != SPEED_100) {
+            netdev_info(dev, "Unsupported speed for forced link\n");
+            return -EINVAL;
+        }
+        if (cmd->base.duplex != DUPLEX_HALF && cmd->base.duplex != DUPLEX_FULL) {
+            netdev_info(dev, "Duplex must be half or full for forced link\n");
+            return -EINVAL;
+        }
+    }
+
      while (test_and_set_bit(__IGC_RESETTING, &adapter->state))
          usleep_range(1000, 2000);
-    if (cmd->base.autoneg == AUTONEG_ENABLE) {
+    if (cmd->base.autoneg == AUTONEG_ENABLE)
          igc_handle_autoneg_enabled(adapter, cmd);
-    } else {
-        netdev_info(dev, "Force mode currently not supported\n");
-    }
+    else
+        igc_handle_autoneg_disabled(adapter, cmd->base.speed,
+                        cmd->base.duplex);
      /* MDI-X => 2; MDI => 1; Auto => 3 */
      if (cmd->base.eth_tp_mdix_ctrl) {
diff --git a/drivers/net/ethernet/intel/igc/igc_hw.h b/drivers/net/ ethernet/intel/igc/igc_hw.h
index 86ab8f566f44..62aaee55668a 100644
--- a/drivers/net/ethernet/intel/igc/igc_hw.h
+++ b/drivers/net/ethernet/intel/igc/igc_hw.h
@@ -73,6 +73,13 @@ struct igc_info {
  extern const struct igc_info igc_base_info;
+enum igc_forced_speed_duplex {
+    IGC_FORCED_10H,
+    IGC_FORCED_10F,
+    IGC_FORCED_100H,
+    IGC_FORCED_100F,
+};
+
  struct igc_mac_info {
      struct igc_mac_operations ops;
@@ -93,6 +100,8 @@ struct igc_mac_info {
      bool arc_subsystem_valid;
      bool get_link_status;
+    bool autoneg_enabled;
+    enum igc_forced_speed_duplex forced_speed_duplex;
  };
  struct igc_nvm_operations {
diff --git a/drivers/net/ethernet/intel/igc/igc_mac.c b/drivers/net/ ethernet/intel/igc/igc_mac.c
index 0a3d3f357505..d6f3f6618469 100644
--- a/drivers/net/ethernet/intel/igc/igc_mac.c
+++ b/drivers/net/ethernet/intel/igc/igc_mac.c
@@ -446,6 +446,17 @@ s32 igc_config_fc_after_link_up(struct igc_hw *hw)
      u16 speed, duplex;
      s32 ret_val = 0;
+    /* Without autoneg, flow control capability is not exchanged with the
+     * link partner. IEEE 802.3 prohibits flow control in half-duplex mode.
+     */
+    if (!hw->mac.autoneg_enabled) {
+        if (hw->mac.forced_speed_duplex == IGC_FORCED_10H ||
+            hw->mac.forced_speed_duplex == IGC_FORCED_100H)
+            hw->fc.current_mode = igc_fc_none;
+
+        goto force_fc;
+    }
+
      /* In auto-neg, we need to check and see if Auto-Neg has completed,
       * and if so, how the PHY and link partner has flow control
       * configured.
@@ -607,6 +618,7 @@ s32 igc_config_fc_after_link_up(struct igc_hw *hw)
      /* Now we call a subroutine to actually force the MAC
       * controller to use the correct flow control settings.
       */
+force_fc:
      ret_val = igc_force_mac_fc(hw);
      if (ret_val) {
          hw_dbg("Error forcing flow control settings\n");
diff --git a/drivers/net/ethernet/intel/igc/igc_main.c b/drivers/net/ ethernet/intel/igc/igc_main.c
index 72bc5128d8b8..437e1d1ef1e4 100644
--- a/drivers/net/ethernet/intel/igc/igc_main.c
+++ b/drivers/net/ethernet/intel/igc/igc_main.c
@@ -7298,7 +7298,7 @@ static int igc_probe(struct pci_dev *pdev,
      /* Initialize link properties that are user-changeable */
      adapter->fc_autoneg = true;
      hw->phy.autoneg_advertised = 0xaf;
-
+    hw->mac.autoneg_enabled = true;
      hw->fc.requested_mode = igc_fc_default;
      hw->fc.current_mode = igc_fc_default;
diff --git a/drivers/net/ethernet/intel/igc/igc_phy.c b/drivers/net/ ethernet/intel/igc/igc_phy.c
index 6c4d204aecfa..4cf737fb3b21 100644
--- a/drivers/net/ethernet/intel/igc/igc_phy.c
+++ b/drivers/net/ethernet/intel/igc/igc_phy.c
@@ -494,12 +494,20 @@ s32 igc_setup_copper_link(struct igc_hw *hw)
      s32 ret_val = 0;
      bool link;
-    /* Setup autoneg and flow control advertisement and perform
-     * autonegotiation.
-     */
-    ret_val = igc_copper_link_autoneg(hw);
-    if (ret_val)
-        goto out;
+    if (hw->mac.autoneg_enabled) {
+        /* Setup autoneg and flow control advertisement and perform
+         * autonegotiation.
+         */
+        ret_val = igc_copper_link_autoneg(hw);
+        if (ret_val)
+            goto out;
+    } else {
+        ret_val = hw->phy.ops.force_speed_duplex(hw);
+        if (ret_val) {
+            hw_dbg("Error Forcing Speed/Duplex\n");
+            goto out;
+        }
+    }
      /* Check link status. Wait up to 100 microseconds for link to become
       * valid.
@@ -778,3 +786,48 @@ u16 igc_read_phy_fw_version(struct igc_hw *hw)
      return gphy_version;
  }
+
+/**
+ * igc_force_speed_duplex - Force PHY speed and duplex settings
+ * @hw: pointer to the HW structure
+ *
+ * Programs the GPY PHY control register to disable autonegotiation
+ * and force the speed/duplex indicated by hw->mac.forced_speed_duplex.
+ */
+s32 igc_force_speed_duplex(struct igc_hw *hw)
+{
+    struct igc_phy_info *phy = &hw->phy;
+    u16 phy_ctrl;
+    s32 ret_val;
+
+    ret_val = phy->ops.read_reg(hw, PHY_CONTROL, &phy_ctrl);
+    if (ret_val)
+        return ret_val;
+
+    phy_ctrl &= ~(MII_CR_SPEED_MASK | MII_CR_DUPLEX_EN |
+              MII_CR_AUTO_NEG_EN | MII_CR_RESTART_AUTO_NEG);
+
+    switch (hw->mac.forced_speed_duplex) {
+    case IGC_FORCED_10H:
+        phy_ctrl |= MII_CR_SPEED_10;
+        break;
+    case IGC_FORCED_10F:
+        phy_ctrl |= MII_CR_SPEED_10 | MII_CR_DUPLEX_EN;
+        break;
+    case IGC_FORCED_100H:
+        phy_ctrl |= MII_CR_SPEED_100;
+        break;
+    case IGC_FORCED_100F:
+        phy_ctrl |= MII_CR_SPEED_100 | MII_CR_DUPLEX_EN;
+        break;
+    default:
+        return -IGC_ERR_CONFIG;
+    }
+
+    ret_val = phy->ops.write_reg(hw, PHY_CONTROL, phy_ctrl);
+    if (ret_val)
+        return ret_val;
+
+    hw->mac.get_link_status = true;
+    return 0;
+}
diff --git a/drivers/net/ethernet/intel/igc/igc_phy.h b/drivers/net/ ethernet/intel/igc/igc_phy.h
index 832a7e359f18..d37a89174826 100644
--- a/drivers/net/ethernet/intel/igc/igc_phy.h
+++ b/drivers/net/ethernet/intel/igc/igc_phy.h
@@ -18,5 +18,6 @@ void igc_power_down_phy_copper(struct igc_hw *hw);
  s32 igc_write_phy_reg_gpy(struct igc_hw *hw, u32 offset, u16 data);
  s32 igc_read_phy_reg_gpy(struct igc_hw *hw, u32 offset, u16 *data);
  u16 igc_read_phy_fw_version(struct igc_hw *hw);
+s32 igc_force_speed_duplex(struct igc_hw *hw);
  #endif
Reviewed-by: Dima Ruinskiy <dima.ruinskiy@xxxxxxxxx>
Tested-by: Moriya Kadosh <moriyax.kadosh@xxxxxxxxx>