casaroli commented on code in PR #21485:
URL: https://github.com/apache/nuttx/pull/21485#discussion_r4496385316
##########
drivers/net/Make.defs:
##########
@@ -125,6 +125,28 @@ endif
include net/oa_tc6/Make.defs
+ifeq ($(CONFIG_NET_PHYLIB),y)
+ CSRCS += phy_core.c phy_access.c phy_device.c
+ CSRCS += phy_gen.c
+ DEPPATH += --dep-path net/phy
+ VPATH += :net/phy
+ CFLAGS += ${INCDIR_PREFIX}$(TOPDIR)$(DELIM)drivers$(DELIM)net$(DELIM)phy
+
+ifeq ($(CONFIG_MDIO_CLAUSE_45),y)
+ CSRCS += phy_genc45.c
+endif
+
+ifeq ($(CONFIG_NET_PHY_REALTEK_RTL8221B),y)
+ CSRCS += realtek/rtl8221b.c realtek/rtl8221b_fw.c
Review Comment:
With `CONFIG_NET_PHY_REALTEK_RTL8221B=y` the Make build stops in `make
depend`: `/bin/bash: realtek/rtl8221b.ddc: No such file or directory`. `CSRCS`
names `realtek/rtl8221b.c` relative to `drivers/`, where no `realtek/`
directory exists; `RTL9000B` (line 146) has the same problem. No defconfig
enables `NET_PHYLIB`, so CI builds none of this code.
Fix: use `CSRCS += rtl8221b.c rtl8221b_fw.c` and `CSRCS += rtl9000b.c
rtl9000b_fw.c` with `VPATH += :net/phy/realtek` and `DEPPATH += --dep-path
net/phy/realtek` outside the RTL8221B block, and add a defconfig that enables
`NET_PHYLIB` and both Realtek drivers so CI builds them.
##########
drivers/net/mdio.c:
##########
@@ -211,43 +323,204 @@ int mdio_write(FAR struct mdio_bus_s *dev, uint8_t
phyaddr, uint8_t regaddr,
* Name: mdio_reset
*
* Description:
- * Reset a PHY on the MDIO bus.
+ * Control the hardware reset line of a PHY on the MDIO bus.
*
* Input Parameters:
* dev - The MDIO bus handle.
* phyaddr - The PHY address (0-31) to reset.
+ * value - Non-zero to assert reset, zero to deassert.
*
* Returned Value:
- * Zero (OK) on success; a negated errno value on failure.
+ * Zero (OK) on success; -ENOSYS if no reset op is provided; a negated
+ * errno value on failure.
*
****************************************************************************/
-int mdio_reset(FAR struct mdio_bus_s *dev, uint8_t phyaddr)
+int mdio_reset(FAR struct mdio_bus_s *dev, uint8_t phyaddr, int value)
{
- int ret = -ENOSYS;
+ int ret;
DEBUGASSERT(dev != NULL && dev->lower != NULL);
- /* Check if the reset method is provided by the lower-half */
+ /* No reset line wired / no lower-half op: nothing to do. */
- if (dev->lower->ops->reset)
+ if (dev->lower->ops->reset == NULL)
{
- /* Take the mutex */
+ return -ENOSYS;
+ }
- ret = nxmutex_lock(&dev->lock);
- if (ret < 0)
- {
- return ret;
- }
+ ret = nxmutex_lock(&dev->lock);
+ if (ret < 0)
+ {
+ return ret;
+ }
- /* Call the lowerhalf driver's reset method */
+ /* Idempotency: skip if the requested state is already in effect. */
- ret = MDIO_RESET(dev, phyaddr);
+ if (dev->reset_state == value)
Review Comment:
`reset_state` is one value per bus, but `reset()` and `mdio_reset()` take
`phyaddr`. After `mdio_reset(bus, 0, 0)` releases the PHY at address 0,
`mdio_reset(bus, 1, 0)` returns OK without calling the lower half, so the PHY
at address 1 stays in reset and `phy_device_create(bus, 1)` fails.
`phy_disconnect()` of the second PHY has the same problem for assert.
Fix: keep the reset state per address (for example two `uint32_t` bitmaps,
"known" and "asserted", indexed by `phyaddr`), or drop the idempotency check
from `mdio_reset()`.
##########
drivers/net/netdev_upperhalf.c:
##########
@@ -1385,6 +1389,24 @@ static int netdev_upper_ioctl(FAR struct net_driver_s
*dev, int cmd,
FAR struct netdev_lowerhalf_s *lower = upper->lower;
int ret = -ENOTTY;
+#ifdef CONFIG_NET_PHYLIB
+ if (lower->phydev != NULL)
+ {
+ ret = phy_ioctl(lower->phydev, cmd, arg);
+ if (ret != -ENOTTY)
+ {
+ return ret;
+ }
+ }
+#endif
+
+#ifdef CONFIG_NETDEV_ETHTOOL_IOCTL
Review Comment:
No Kconfig defines `CONFIG_NETDEV_ETHTOOL_IOCTL`, `struct
netdev_lowerhalf_s` has no `eth_ops` member and `netdev_upper_ethtool_ioctl()`
does not exist, so this block is dead and does not compile when the option is
set. `struct ethtool_ops_s` in netdev_lowerhalf.h and `struct
ethtool_chns`/`chns2`/`sset_info`/`gstrings`/`stats` in ethtool.h are used only
by it (and `ethtool_stats.data[]` is `uint32_t`, while the comment says the
driver fills uint64 values).
Fix: remove this block, `struct ethtool_ops_s` and the structs only it uses,
or add the Kconfig option, the `eth_ops` member and
`netdev_upper_ethtool_ioctl()` in this PR.
##########
drivers/net/phy/realtek/rtl8221b.c:
##########
@@ -0,0 +1,933 @@
+/****************************************************************************
+ * drivers/net/phy/realtek/rtl8221b.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <errno.h>
+#include <debug.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include <nuttx/arch.h>
+#include <nuttx/clock.h>
+#include <nuttx/signal.h>
+#include <nuttx/net/phy.h>
+#include <nuttx/net/mii.h>
+#include <nuttx/net/realtek_phy.h>
+#include <nuttx/ethtool.h>
+
+#include "phy.h"
+#include "rtl8221b.h"
+#include "rtl8221b_fw.h"
+
+/****************************************************************************
+ * Private Function Prototypes
+ ****************************************************************************/
+
+static int rtl8221b_config_init(FAR struct phy_device_s *phydev);
+static int rtl8221b_config_aneg(FAR struct phy_device_s *phydev);
+static int rtl8221b_read_status(FAR struct phy_device_s *phydev);
+static int rtl8221b_config_intr(FAR struct phy_device_s *phydev,
+ bool enable);
+static int rtl8221b_handle_interrupt(FAR struct phy_device_s *phydev);
+
+/****************************************************************************
+ * Private Data
+ ****************************************************************************/
+
+static const struct phy_driver_s g_rtl8221b_driver =
+{
+ .name = "RTL8221B 2.5Gbps PHY",
+ .phy_id = RTL8221B_VB_CG_PHY_ID,
+ .phy_id_mask = RTL8221B_PHY_ID_MASK,
+ .config_init = rtl8221b_config_init,
+ .config_aneg = rtl8221b_config_aneg,
+ .read_status = rtl8221b_read_status,
+ .config_intr = rtl8221b_config_intr,
+ .handle_interrupt = rtl8221b_handle_interrupt,
+};
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: rtl8221b_write_ramcode
+ *
+ * Description:
+ * Write one firmware patch (ramcode) table to the PHY via MMD writes.
+ *
+ * Input Parameters:
+ * phydev - PHY device
+ * code - Ramcode table (MMD device/address/value triples) to write
+ * len - Number of entries in the ramcode table
+ *
+ * Returned Value:
+ * OK on success; a negated errno value on failure.
+ *
+ ****************************************************************************/
+
+static int rtl8221b_write_ramcode(FAR struct phy_device_s *phydev,
+ FAR const struct rtl8221b_mmd_reg_s *code,
+ size_t len)
+{
+ size_t i;
+ int ret;
+
+ for (i = 0; i < len; i++)
+ {
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, code[i].addr,
+ code[i].value);
+ if (ret < 0)
+ {
+ return ret;
+ }
+ }
+
+ return OK;
+}
+
+/****************************************************************************
+ * Name: rtl8221b_apply_patch
+ *
+ * Description:
+ * Apply the RTL8221B firmware patch (NC0/NC1/NC2/UC2/UC ramcode plus the
+ * SDS patch and GPHY parameter tuning). Mirrors the vendor reference
+ * initialization sequence, expressed purely through Clause-45 MMD
+ * register access.
+ *
+ * Input Parameters:
+ * phydev - PHY device
+ *
+ * Returned Value:
+ * OK on success; a negated errno value on failure.
+ *
+ ****************************************************************************/
+
+static int rtl8221b_apply_patch(FAR struct phy_device_s *phydev)
+{
+ int ret;
+ int val;
+ int timeout;
+
+ /* Patch request: MMD31.0xb820[4] = 1 */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_CTRL,
+ RTL_VEND2_PATCH_REQ, RTL_VEND2_PATCH_REQ);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Wait for patch ready: MMD31.0xb800[6] = 1 (bounded timeout) */
+
+ for (timeout = RTL_PATCH_READY_TIMEOUT_MS; timeout > 0; timeout--)
+ {
+ val = phy_read_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_STATUS);
+ if (val < 0)
+ {
+ return val;
+ }
+
+ if (val & RTL_VEND2_PATCH_RDY)
+ {
+ break;
+ }
+
+ nxsig_usleep(USEC_PER_MSEC);
+ }
+
+ if (timeout == 0)
+ {
+ nerr("ERROR: RTL8221B patch ready (set) timeout\n");
+ return -ETIMEDOUT;
+ }
+
+ /* Set patch_key & patch_lock */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, 0xa436, 0x8024);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, 0xa438, 0x3701);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, 0xa436, 0xb82e);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, 0xa438, 0x0001);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Enable NC & UC patch: MMD31.0xb820[7] = 1 */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_CTRL,
+ RTL_VEND2_UCODE_EN, RTL_VEND2_UCODE_EN);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Load NC ramcode banks */
+
+ ret = rtl8221b_write_ramcode(phydev, g_rtl8221b_n0_ramcode,
+ g_rtl8221b_n0_ramcode_len);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = rtl8221b_write_ramcode(phydev, g_rtl8221b_n1_ramcode,
+ g_rtl8221b_n1_ramcode_len);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = rtl8221b_write_ramcode(phydev, g_rtl8221b_n2_ramcode,
+ g_rtl8221b_n2_ramcode_len);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = rtl8221b_write_ramcode(phydev, g_rtl8221b_uc2_ramcode,
+ g_rtl8221b_uc2_ramcode_len);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Disable NC & UC patch: MMD31.0xb820[7] = 0 */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_CTRL,
+ RTL_VEND2_UCODE_EN, 0);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Load UC ramcode bank */
+
+ ret = rtl8221b_write_ramcode(phydev, g_rtl8221b_uc_ramcode,
+ g_rtl8221b_uc_ramcode_len);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* GPHY OCP 0xb896[0] = 0 */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND2, 0xb896, 0x0001, 0);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* GPHY OCP 0xb892[15:8] = 0 */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, 0xb892, 0x0000);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* GPHY OCP 0xb896[0] = 1 */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND2, 0xb896, 0x0001, 0x0001);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Clear patch_key & patch_lock */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, 0xa436, 0x0000);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, 0xa438, 0x0000);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* MMD31.0xb82e[0] = 0 */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND2, 0xb82e, 0x0001, 0);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Clear patch key address */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, 0xa436, 0x8024);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, 0xa438, 0x0000);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Release patch request: MMD31.0xb820[4] = 0 */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_CTRL,
+ RTL_VEND2_PATCH_REQ, 0);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Wait for patch ready bit to clear: MMD31.0xb800[6] = 0 (bounded) */
+
+ for (timeout = RTL_PATCH_READY_TIMEOUT_MS; timeout > 0; timeout--)
+ {
+ val = phy_read_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_STATUS);
+ if (val < 0)
+ {
+ return val;
+ }
+
+ if (!(val & RTL_VEND2_PATCH_RDY))
+ {
+ break;
+ }
+
+ nxsig_usleep(USEC_PER_MSEC);
+ }
+
+ if (timeout == 0)
+ {
+ nerr("ERROR: RTL8221B patch ready (clear) timeout\n");
+ return -ETIMEDOUT;
+ }
+
+ /* Apply GPHY tuning and OCP programming (all on VEND2). */
+
+ ret = rtl8221b_write_ramcode(phydev, g_rtl8221b_gphy_tuning,
+ g_rtl8221b_gphy_tuning_len);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* SDS patch (MMD30 / VEND1) */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND1, 0x75b5, 0xe086);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND1, 0x75b6, 0x0024);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND1, 0x75b1, 0x0013);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Update patch version marker (VEND2) */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_REG_SEL,
+ RTL_VEND2_PATCH_VER_SEL);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_REG_DATA,
+ RTL_VEND2_PATCH_VERSION);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* PHY reset & restart Nway */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PHYRST_REG,
+ RTL_VEND2_PHYRST_NWAY);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return OK;
+}
+
+/****************************************************************************
+ * Name: rtl8221b_config_serdes
+ *
+ * Description:
+ * Configure the RTL8221B SerDes interface toward the MAC as a fixed
+ * 2500BASE-X / SGMII link with auto-negotiation disabled, then wait for
+ * the SerDes link to come up. All accesses are pure Clause-45 MMD
+ * register operations (MMD30 / VEND1 and MMD31 / VEND2).
+ *
+ * Input Parameters:
+ * phydev - PHY device
+ *
+ * Returned Value:
+ * OK on success; a negated errno value on failure.
+ *
+ ****************************************************************************/
+
+static int rtl8221b_config_serdes(FAR struct phy_device_s *phydev)
+{
+ int val;
+ int ret;
+ int waited;
+
+ /* --- SerDes option set: 2500BASE-X + SGMII --- */
+
+ /* Clear MMD30.0x75f3[0] */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND1, RTL_VEND1_SDS_75F3, 0x0001, 0);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Program SerDes mode select MMD30.0x697a[5:0] */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND1, RTL_VEND1_SDS_OPTION,
+ RTL_VEND1_SDS_MODE_MASK,
+ RTL_SDS_OPTION_2500BASEX_SGMII);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* 2500BASE-X specific SerDes tuning */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND1, RTL_VEND1_SDS_CTRL1, 0x0503);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND1, RTL_VEND1_SDS_6F10, 0xd455);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND1, RTL_VEND1_SDS_6F11, 0x8020);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Toggle link state: set MMD31.0xa400[14] */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PHYRST_REG,
+ RTL_VEND2_LINK_CHG, RTL_VEND2_LINK_CHG);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Wait for the SerDes link: MMD31.0xa434[2] (bounded timeout) */
+
+ for (waited = 0; waited < RTL_SDS_LINK_TIMEOUT_MS; waited += 10)
+ {
+ val = phy_read_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PHYSR);
+ if (val < 0)
+ {
+ return val;
+ }
+
+ if (val & RTL_PHYSR_LINK_RT)
+ {
+ break;
+ }
+
+ nxsig_usleep(10 * USEC_PER_MSEC);
+ }
+
+ if (waited >= RTL_SDS_LINK_TIMEOUT_MS)
+ {
+ nwarn("WARNING: RTL8221B SerDes link not up within %dms\n",
+ RTL_SDS_LINK_TIMEOUT_MS);
+ }
+
+ /* Clear link-change bit: MMD31.0xa400[14] = 0 */
+
+ ret = phy_modify_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PHYRST_REG,
+ RTL_VEND2_LINK_CHG, 0);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* --- Disable SerDes auto-negotiation (fixed-speed link) --- */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND1, RTL_VEND1_SDS_ANEG_7588,
+ 0x0002);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND1, RTL_VEND1_SDS_ANEG_7589,
+ 0x71d0);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND1, RTL_VEND1_SDS_ANEG_7587,
+ 0x0003);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return OK;
+}
+
+/****************************************************************************
+ * Name: rtl8221b_config_init
+ *
+ * Description:
+ * Initialize RTL8221B PHY: wait for the PHY to become ready, apply the
+ * firmware patch (ramcode) when the running patch version does not match,
+ * then advertise the supported capabilities.
+ *
+ * Input Parameters:
+ * phydev - PHY device
+ *
+ * Returned Value:
+ * OK on success; a negated errno value on failure.
+ *
+ ****************************************************************************/
+
+static int rtl8221b_config_init(FAR struct phy_device_s *phydev)
+{
+ int timeout;
+ int val;
+ int ret;
+
+ /* Wait until the PHY reports ready: MMD31.0xa420[1:0] == 0x3 */
+
+ for (timeout = RTL_PHY_READY_TIMEOUT_MS; timeout > 0; timeout--)
+ {
+ val = phy_read_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PHY_READY_REG);
+ if (val < 0)
+ {
+ return val;
+ }
+
+ if (val != 0xffff && (val & 0x3) == 0x3)
+ {
+ break;
+ }
+
+ nxsig_usleep(USEC_PER_MSEC);
+ }
+
+ if (timeout == 0)
+ {
+ nerr("ERROR: RTL8221B not ready\n");
+ return -ETIMEDOUT;
+ }
+
+ /* Check whether the firmware patch is already applied (idempotent):
+ * read the running patch version from OCP 0x801e.
+ */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_REG_SEL,
+ RTL_VEND2_PATCH_VER_SEL);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ val = phy_read_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_REG_DATA);
+ if (val < 0)
+ {
+ return val;
+ }
+
+ if (val != RTL_VEND2_PATCH_VERSION)
+ {
+ /* Patch not present or stale: apply it */
+
+ ret = rtl8221b_apply_patch(phydev);
+ if (ret < 0)
+ {
+ nerr("ERROR: RTL8221B patch failed: %d\n", ret);
+ return ret;
+ }
+
+ /* Verify the patch version was written back */
+
+ ret = phy_write_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_REG_SEL,
+ RTL_VEND2_PATCH_VER_SEL);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ val = phy_read_mmd(phydev, MII_MMD_VEND2, RTL_VEND2_PATCH_REG_DATA);
+ if (val < 0)
+ {
+ return val;
+ }
+
+ if (val != RTL_VEND2_PATCH_VERSION)
+ {
+ nerr("ERROR: RTL8221B patch verification failed (0x%04x)\n", val);
+ return -EIO;
+ }
+ }
+
+ phydev->supported = SUPPORTED_10BASET_FULL |
+ SUPPORTED_100BASET_FULL |
+ SUPPORTED_1000BASET_FULL |
+ SUPPORTED_AUTONEG |
+ SUPPORTED_TP |
+ SUPPORTED_MII;
+
+ phydev->advertising = phydev->supported;
+
+ /* The PHY was (potentially) soft-reset at the end of the firmware patch,
+ * which clears any prior SerDes configuration. Re-arm the one-shot
+ * SerDes setup so it is (re)applied by read_status on the next link-up.
+ */
+
+ phydev->priv = NULL;
+
+ /* NOTE: SerDes configuration toward the MAC is intentionally NOT done
+ * here. The SerDes "set option" sequence ends by toggling the link and
+ * waiting for the real-time link bit, which only asserts once the UTP
+ * link is established. Doing it at config_init time (UTP link not yet
+ * up) would stall and leave the SerDes data path unconfigured. Instead
+ * it is performed from read_status when link-up is first observed.
+ */
+
+ ninfo("RTL8221B config_init done: supported=0x%08lx\n",
Review Comment:
`phydev->supported` is `uint32_t`, which is `unsigned int` on arm64, so the
`-Werror` build of qemu-armv8a:netnsh with this driver (debug output off, the
default) fails: `rtl8221b.c:653:9: error: format '%lx' expects argument of type
'long unsigned int', but argument 3 has type 'uint32_t' {aka 'unsigned int'}
[-Werror=format=]`. `rtl9000b.c:709` (`%lx`) fails the same way.
Fix: use `"0x%08" PRIx32` in both (with `#include <inttypes.h>`).
##########
Documentation/os/networking/phylib.rst:
##########
@@ -0,0 +1,1678 @@
+.. _phylib:
+
+.. include:: /substitutions.rst
+
+==========================================
+PHY Abstraction Layer (PHYLIB)
+==========================================
+
+This document describes the design of the PHY Abstraction Layer (PHYLIB)
+for NuttX. It covers: design background, overall architecture, core data
+structures, the state machine, the generic PHY driver, MAC driver
+integration, ethtool integration, vendor drivers, and debugging.
+
+1. Design Background
+====================
+
+1.1 Current Problems
+--------------------
+
+In the current NuttX network drivers, PHY management logic is scattered
+inside each MAC driver. Drawbacks of having no PHYLIB:
+
+#. **Large amount of duplicated code** — every MAC driver implements its
+ own MDIO read/write, PHY init and link detection; the logic is similar
+ but rewritten each time.
+#. **Changing the PHY means changing the MAC** — PHY register definitions
+ are spread across MAC drivers, so swapping a chip requires editing MAC
+ source.
+#. **Inconsistent link management** — each driver uses a different poll
+ interval, and carrier notification timing is not unified.
+#. **PHY drivers cannot be reused** — the same PHY on a different SoC has
+ its init code copied again.
+#. **Inconsistent ioctl behavior** — MII/ethtool commands are interpreted
+ by each driver on its own.
+
+1.2 Design Goals
+----------------
+
+#. Decouple PHY management from the MAC driver.
+#. Provide a standardized MDIO bus, PHY device and PHY driver framework.
+#. Support Clause 22 and Clause 45 PHYs.
+#. Support both polling and interrupt link-monitoring modes, selectable
+ by hardware capability.
+#. Expose link state uniformly through ethtool ioctl.
+#. Allow PHY drivers to be reused across platforms.
+
+.. note::
+
+ Core value: a MAC driver only needs to implement the MDIO lower-half
+ (the ``mdio_ops_s`` read/write callbacks) and call a few PHYLIB APIs to
+ complete PHY integration, without caring about the register details of
+ a specific PHY chip. The MDIO bus itself is managed by the community
+ upper-half framework (``drivers/net/mdio.c``).
+
+1.3 Provenance
+--------------
+
+This implementation is written independently based on the IEEE 802.3
+standard and chip datasheets (such as the RTL8221B). Register addresses,
+bit definitions and access timing are IEEE standard / hardware facts.
+The interface naming follows common PHYLIB conventions for readability.
+The underlying implementation (state machine, concurrency model, memory
+management) is a native NuttX design.
+
+2. Overall Architecture
+=======================
+
+The framework follows the ``netdev -> phydev -> mdio`` layering. Benefits
+of PHYLIB:
+
+#. **Zero change to swap a PHY** — only add a vendor driver file; the MAC
+ driver is untouched.
+#. **Unified state machine** — one piece of logic serves all MACs; link
+ changes are reported to the stack automatically.
+#. **PHY drivers reusable across platforms** — a vendor driver works on any
+ MAC platform.
+#. **Fast bring-up on new platforms** — implement the MDIO lower-half
+ (``mdio_ops_s``) and call a few PHYLIB APIs.
+#. **Standardized ioctl** — PHYLIB handles MII/ethtool commands uniformly.
+#. **Polling / interrupt dual mode** — polling by default; if the hardware
+ wires INTB, switching to interrupt-driven is free.
+
+3. Core Source Files
+====================
+
+.. list-table::
+ :header-rows: 1
+ :widths: 40 60
+
+ * - File
+ - Role
+ * - ``drivers/net/phy/phy.c``
+ - State machine: start/stop/poll worker, interrupt handling
+ (``phy_mac_interrupt``), suspend/resume.
+ * - ``drivers/net/phy/phy_device.c``
+ - PHY device model: create, match, connect, scan.
+ * - ``drivers/net/mdio.c``
+ - MDIO bus upper-half (community framework): ``mdio_register``,
+ ``mdio_read``/``write``, ``mdio_c45`` access, serialized by a lock.
+ * - ``drivers/net/phy/phy_core.c``
+ - Low-level helpers:
``phy_read``/``write``/``modify``/``set_bits``/``mmd``.
+ * - ``drivers/net/phy/phy_ioctl.c``
+ - MII ioctl + ethtool link-settings dispatch.
+ * - ``drivers/net/phy/genphy.c``
+ - Generic Clause 22 PHY driver (IEEE 802.3 standard registers).
+ * - ``drivers/net/phy/genphy_c45.c``
+ - Generic Clause 45 PHY driver (MMD registers).
+ * - ``drivers/net/phy/realtek/``
+ - RTL8221B vendor driver.
+ * - ``include/nuttx/net/phy.h``
+ - Public header: structures + API declarations.
+ * - ``include/nuttx/net/mdio.h``
+ - MDIO upper/lower-half contract: ``mdio_ops_s``,
+ ``mdio_lowerhalf_s``, ``mdio_register`` etc.
+
+4. Kconfig
+==========
+
+.. code-block:: kconfig
+
+ # Framework core
+ CONFIG_NET_PHYLIB=y # Enable the PHYLIB subsystem
+ # Auto-selects the two options below:
+ CONFIG_MDIO_BUS=y # MDIO bus upper-half
(drivers/net/mdio.c)
+ CONFIG_MDIO_CLAUSE_45=y # MDIO Clause 45 support (needed by
C45 PHYs)
+
+ # Tunables
+ CONFIG_NET_PHYLIB_POLL_INTERVAL=1000 # State-machine poll interval (ms)
+ CONFIG_NET_PHYLIB_MAX_DRIVERS=8 # Maximum number of PHY drivers
+ CONFIG_NET_PHYLIB_MAX_PHYS=8 # Maximum number of PHY devices
+
+5. Core Data Structures
+=======================
+
+5.1 MDIO Bus
+------------
+
+The MDIO bus uses the standard upper/lower-half layering
+(``drivers/net/mdio.c`` + ``include/nuttx/net/mdio.h``). The upper-half
+owns the bus core object (lock, read/write forwarding, lifecycle); the
+MAC driver only implements the lower-half hardware callbacks.
+
+Design points:
+
+- **Upper/lower layering**: the bus core object (including the lock) lives
+ in the upper-half ``drivers/net/mdio.c`` and is allocated dynamically by
+ ``mdio_register()``. The MAC driver only holds a ``mdio_lowerhalf_s`` and
+ the handle returned by ``mdio_register()``.
+- **Locking**: the read/write mutex is held by the upper-half, guaranteeing
+ concurrency safety between ethtool ioctl and the state machine.
+- **C45**: ``read_c45``/``write_c45`` are optional — when NULL, the
+ upper-half automatically uses the Clause 22 reg 13/14 indirect access;
+ implementing them when the hardware natively supports C45 (e.g. AURIX
+ GETH) avoids the indirect transactions. Controlled by
+ ``CONFIG_MDIO_CLAUSE_45`` (see "C45 Extension" below).
+
+Lower-half callback table ``mdio_ops_s`` (implemented by the MAC driver):
+
+.. code-block:: c
+
+ struct mdio_ops_s
+ {
+ /* Clause 22 read/write */
+ int (*read)(FAR struct mdio_lowerhalf_s *lower, uint8_t phyaddr,
+ uint8_t regaddr, FAR uint16_t *value);
+ int (*write)(FAR struct mdio_lowerhalf_s *lower, uint8_t phyaddr,
+ uint8_t regaddr, uint16_t value);
+
+ #ifdef CONFIG_MDIO_CLAUSE_45
+ /* Native Clause 45 read/write - optional; when NULL the upper-half
+ * uses reg13/14 indirect access.
+ */
+ int (*read_c45)(FAR struct mdio_lowerhalf_s *lower, uint8_t phyaddr,
+ uint8_t devad, uint16_t regaddr, FAR uint16_t *value);
+ int (*write_c45)(FAR struct mdio_lowerhalf_s *lower, uint8_t phyaddr,
+ uint8_t devad, uint16_t regaddr, uint16_t value);
+ #endif
+
+ /* PHY hardware reset line control - optional. Controls the PHY RESET_N
+ * by logic level rather than a one-shot pulse. value != 0 asserts
+ * (into reset); value == 0 deasserts (release). The lower-half handles
+ * pin polarity (active-low RESET_N handled internally) and the
+ * datasheet-specified assert/deassert settle delays; the upper-half
+ * mdio_reset() handles idempotency (skips when already at the target
+ * state).
+ */
+ int (*reset)(FAR struct mdio_lowerhalf_s *lower, uint8_t phyaddr,
+ int value);
+ };
+
+ /* Lower-half instance: allocated by the MAC driver; ops must be the
+ * first member.
+ */
+ struct mdio_lowerhalf_s
+ {
+ FAR const struct mdio_ops_s *ops;
+ };
+
+Upper-half implementation (``drivers/net/mdio.c``) and public API. The
+upper-half collapses the bus core into ``struct mdio_bus_s`` (defined
+privately inside mdio.c, not visible externally), holding only a
+"lower-half pointer + mutex":
+
+.. code-block:: c
+
+ /* Register the lower-half */
+ FAR struct mdio_bus_s *mdio_register(FAR struct mdio_lowerhalf_s *lower);
+ int mdio_unregister(FAR struct mdio_bus_s *dev);
+
+ /* Read/write (serialized by the upper-half internal lock) */
+ int mdio_read(FAR struct mdio_bus_s *dev, uint8_t phyaddr,
+ uint8_t regaddr, FAR uint16_t *value);
+ int mdio_write(FAR struct mdio_bus_s *dev, uint8_t phyaddr,
+ uint8_t regaddr, uint16_t value);
+
+ #ifdef CONFIG_MDIO_CLAUSE_45
+ int mdio_c45_read(FAR struct mdio_bus_s *dev, uint8_t phyaddr,
+ uint8_t devad, uint16_t regaddr, FAR uint16_t *value);
+ int mdio_c45_write(FAR struct mdio_bus_s *dev, uint8_t phyaddr,
+ uint8_t devad, uint16_t regaddr, uint16_t value);
+ #endif
+
+ /* PHY hardware reset line control (assert/deassert) */
+ int mdio_reset(FAR struct mdio_bus_s *dev, uint8_t phyaddr, int value);
+
+ struct mdio_bus_s
+ {
+ FAR struct mdio_lowerhalf_s *lower;
+ mutex_t lock; /* Bus mutex */
+ int8_t reset_state; /* PHY reset line: -1 unknown /
0 released / 1 in reset */
+ };
+
+C45 Extension
+~~~~~~~~~~~~~
+
+The original community upper-half implemented only Clause 22 (C45 was left
+as an empty ``#ifdef CONFIG_MDIO_CLAUSE_45``). Since the RTL8221B is a C45
+PHY, C45 was completed on top of the community framework:
+
+#. **``mdio_ops_s`` gains** two optional callbacks ``read_c45`` /
+ ``write_c45`` (see the structure above).
+#. **``mdio.c`` gains** ``mdio_c45_read`` / ``mdio_c45_write``: when the
+ lower-half provides native C45 ops they are called directly; otherwise
+ it **falls back automatically** to the Clause 22 reg13/14 indirect
+ access (MMD indirect addressing), transparent to upper layers:
+
+ .. code-block:: c
+
+ int mdio_c45_read(FAR struct mdio_bus_s *dev, uint8_t phyaddr,
+ uint8_t devad, uint16_t regaddr, FAR uint16_t *value)
+ {
+ nxmutex_lock(&dev->lock);
+
+ if (dev->lower->ops->read_c45 != NULL)
+ {
+ /* Native C45: one transaction in the lower-half */
+ int ret = dev->lower->ops->read_c45(dev->lower, phyaddr, devad,
+ regaddr, value);
+ nxmutex_unlock(&dev->lock);
+ return ret;
+ }
+
+ /* Fallback: indirect via C22 reg13(MMDCTRL)/reg14(MMDDATA) */
+ dev->lower->ops->write(dev->lower, phyaddr, MII_MMDCONTROL, devad);
+ dev->lower->ops->write(dev->lower, phyaddr, MII_MMDADDRDATA, regaddr);
+ dev->lower->ops->write(dev->lower, phyaddr, MII_MMDCONTROL,
+ devad | MII_MMDCONTROL_FUNC_NOINCR);
+ int ret = dev->lower->ops->read(dev->lower, phyaddr,
+ MII_MMDADDRDATA, value);
+ nxmutex_unlock(&dev->lock);
+ return ret;
+ }
+
+#. **Kconfig gains** a ``CONFIG_MDIO_CLAUSE_45`` switch, which
+ ``NET_PHYLIB`` auto-selects.
+
+.. note::
+
+ Meaning of the C45 fallback: when the hardware natively supports C45
+ (e.g. AURIX GETH), implementing ``read_c45``/``write_c45`` completes the
+ access in one bus transaction; a MAC without native C45 need not write
+ indirect-access logic itself, as the upper-half falls back to reg13/14
+ automatically (4 C22 transactions). The PHYLIB caller
+ (``phy_read_mmd`` -> ``mdio_c45_read``) is completely unaware.
+
+reg13/14 indirect addressing (IEEE 802.3 Clause 22 extension, the standard
+mechanism for accessing MMD registers): the C22 register space only has a
+5-bit address (reg 0-31) and cannot directly address the 16-bit MMD
+register space of C45. IEEE 802.3 uses **reg 13 (MMDCONTROL) + reg 14
+(MMDADDRDATA)** as an indirect channel, so each C45 access takes 4 steps:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 10 15 40 35
+
+ * - Step
+ - Operation
+ - reg 13 ``MMDCONTROL``
+ - reg 14 ``MMDADDRDATA``
+ * - 1
+ - C22 write
+ - devad (address mode, FUNC=00)
+ - —
+ * - 2
+ - C22 write
+ - —
+ - Write the target 16-bit MMD register address
+ * - 3
+ - C22 write
+ - devad | data mode (FUNC=01, no post-increment)
+ - —
+ * - 4
+ - C22 read / write
+ - —
+ - Read / write the 16-bit data of the target MMD register
+
+Each C45 read/write needs 4 C22 bus transactions, 4x the cost of native
+C45 (one frame of 5-bit OP + 16-bit address + 16-bit data). So implementing
+``read_c45``/``write_c45`` when the hardware supports native C45 (e.g.
+AURIX GETH) saves 75% of the bus transactions, a significant optimization.
+
+Interface provenance (community original / added by this design):
+
+.. list-table::
+ :header-rows: 1
+ :widths: 35 15 50
+
+ * - Interface / field
+ - Source
+ - Notes
+ * - ``mdio_register`` / ``mdio_unregister``
+ - community
+ - Dynamically allocate/free the bus handle, bind the lower-half.
+ * - ``mdio_read`` / ``mdio_write``
+ - community
+ - Clause 22 read/write, forward to ``lower->ops`` under the lock.
+ * - ``mdio_reset``
+ - community
+ - PHY hardware reset line control: changed to ``(lower, phyaddr,
+ value)`` level control + ``reset_state`` idempotency.
+ * - ``mdio_ops_s.read`` / ``.write`` / ``.reset``
+ - community
+ - Lower-half C22 + reset callback contract.
+ * - ``mdio_ops_s.read_c45`` / ``.write_c45``
+ - added here
+ - Lower-half native C45 callbacks (optional,
+ ``#ifdef CONFIG_MDIO_CLAUSE_45``).
+ * - ``mdio_c45_read`` / ``mdio_c45_write``
+ - added here
+ - C45 read/write: native preferred, falls back to reg13/14 indirect.
+ * - ``CONFIG_MDIO_CLAUSE_45``
+ - added here
+ - C45 switch, auto-selected by ``NET_PHYLIB``.
+
+5.2 struct phy_device_s
+-----------------------
+
+The PHY device instance, holding all runtime state:
+
+.. code-block:: c
+
+ struct phy_device_s
+ {
+ uint32_t phy_id; /* (PHYID1<<16)|PHYID2 */
+ uint8_t mdio_addr; /* MDIO address 0-31 */
+ FAR struct mdio_bus_s *bus; /* The MDIO bus it is on */
+ FAR const struct phy_driver_s *drv; /* Matched PHY driver */
+ FAR struct netdev_lowerhalf_s *attached_dev; /* Associated MAC */
+
+ /* Link state */
+ bool is_c45; /* Whether it is a Clause 45 PHY
*/
+ bool link; /* Current link state */
+ uint32_t speed; /* Current speed
(10/100/1000/2500) */
+ uint8_t duplex; /* Current duplex mode */
+ enum phy_interface_e interface; /* Interface mode
(RGMII/SGMII/...) */
+
+ /* Capability bitmaps */
+ uint32_t supported; /* Modes supported by the PHY */
+ uint32_t advertising; /* Currently advertised modes */
+ uint32_t lp_advertising; /* Link-partner advertised modes
*/
+ bool autoneg; /* Auto-negotiation enable */
+
+ /* State machine */
+ enum phy_state_e state; /* Current state */
+ struct work_s state_work; /* LPWORK poll work */
+ bool state_machine_running;
+ mutex_t lock; /* Device lock, serializes state
fields */
+
+ /* Interrupt mode (optional) */
+ int irq; /* PHY IRQ; PHY_POLL(-1) =
polling */
+ bool interrupts; /* Whether interrupt-driven now */
+
+ /* Driver private data (optional) */
+ FAR void *priv; /* PHY driver private state,
+ * kmm_free'd by the framework in
+ * phy_disconnect */
+ };
+
+Field groups:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 15 35 50
+
+ * - Group
+ - Key fields
+ - Notes
+ * - Identity
+ - ``phy_id``, ``is_c45``, ``drv``
+ - Who the PHY is, what protocol, which driver it binds to.
+ * - Link state
+ - ``link``, ``speed``, ``duplex``, ``autoneg``
+ - Refreshed every second by the state machine; the MAC configures
+ hardware accordingly.
+ * - Capability bitmaps
+ - ``supported``, ``advertising``, ``lp_advertising``
+ - Core of auto-negotiation: local advertise ∩ partner advertise =
+ final speed.
+ * - State machine
+ - ``state``, ``state_work``, ``lock``
+ - The engine driving the PHY lifecycle; ``lock`` serializes state
+ field access.
+ * - Connections
+ - ``attached_dev``, ``bus``
+ - Association with the netdev and the MDIO bus.
+
+5.3 struct phy_driver_s
+-----------------------
+
+The PHY driver callback interface:
+
+.. code-block:: c
+
+ struct phy_driver_s
+ {
+ FAR const char *name;
+ uint32_t phy_id;
+ uint32_t phy_id_mask;
+
+ CODE int (*config_init)(FAR struct phy_device_s *phydev);
+ CODE int (*config_aneg)(FAR struct phy_device_s *phydev);
+ CODE int (*read_status)(FAR struct phy_device_s *phydev);
+ CODE int (*suspend)(FAR struct phy_device_s *phydev);
+ CODE int (*resume)(FAR struct phy_device_s *phydev);
+
+ /* Interrupt support (optional; polling only if unimplemented) */
+ CODE int (*config_intr)(FAR struct phy_device_s *phydev, bool enable);
+ CODE int (*handle_interrupt)(FAR struct phy_device_s *phydev);
+ };
+
+.. note::
+
+ Key design: all callbacks are optional. An unimplemented callback
+ automatically falls back to the generic genphy implementation. A PHY
+ compliant with IEEE 802.3 works without any vendor driver.
+
+Match rule: ``(phydev->phy_id & drv->phy_id_mask) == drv->phy_id``
+
+Callback execution frequency:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 35 20 45
+
+ * - Callback
+ - Frequency
+ - Notes
+ * - ``config_init``
+ - once
+ - Executed at connect.
+ * - ``config_aneg``
+ - on demand
+ - When the user changes link settings.
+ * - ``read_status``
+ - **once per second (default)**
+ - State-machine polling, the hottest path.
+ * - ``suspend`` / ``resume``
+ - on demand
+ - On system sleep/wake.
+ * - ``config_intr`` / ``handle_interrupt``
+ - on demand (optional)
+ - Enable INTB in interrupt mode / decode and handle all interrupt
+ sources when triggered; falls back to polling if unimplemented.
+
+6. Device Lifecycle
+===================
+
+The full PHY device lifecycle: **discover -> connect -> start -> run ->
+stop -> free**.
+
+.. note::
+
+ NuttX MAC drivers usually already know the PHY address (from the board
+ defconfig or datasheet), so they call ``phy_device_create(bus,
+ known_addr)`` to create it precisely, without scanning addresses 0-31.
+ ``mdio_bus_scan()`` is kept as an optional tool for the
+ unknown-address case.
+
+6.1 Discovery and Registration
+------------------------------
+
+.. code-block:: text
+
+ MAC driver init / ifup:
+ mdio_register(&priv->mdio_lower) // register MDIO lower-half, get
bus handle
+ phy_device_create(handle, known_addr) // create directly with known
address
+ -> read PHYID1 (reg 2) / PHYID2 (reg 3) (C45 PHY tries MMD1 indirect
first)
+ -> concat: phy_id = (PHYID1 << 16) | PHYID2
+ -> reject invalid (0x0000/0xFFFF)
+ -> allocate phy_device_s from the static pool
+ -> match driver by (id & mask) -> fall back to genphy if none
+ -> phydev->bus = handle (save bus handle for later access)
+
+6.2 Connect
+-----------
+
+.. code-block:: text
+
+ phy_connect(netdev_lower, phydev)
+ -> check phydev is not in use (else -EBUSY)
+ -> set bidirectional link: phydev->attached_dev = dev, dev->phydev =
phydev
+ -> phy_device_reset(phydev, 0) // deassert, release hard reset
(idempotent)
+ -> call drv->config_init() to init the chip
+ -> transition to PHY_READY
+
+6.3 Start and Stop
+------------------
+
+.. code-block:: text
+
+ phy_start(phydev) // called by the MAC driver in ifup
+ -> check state == PHY_READY or PHY_HALTED
+ -> call drv->config_aneg() // start auto-negotiation
+ -> state = PHY_UP
+ -> if irq != PHY_POLL and config_intr present: enable interrupts
+ -> work_queue(LPWORK) schedules polling
+ - polling mode: 1s period
+ - interrupt mode: 8x period (missed-interrupt backstop only)
+
+ phy_stop(phydev) // called by the MAC driver in ifdown
+ -> if interrupts: config_intr(false) disables interrupts
+ -> work_cancel_sync(LPWORK) cancels state_work (waits for running work,
prevents UAF)
+ -> state = PHY_HALTED
+ -> netdev_lower_carrier_off() notifies the MAC
+
+6.4 Disconnect and Free
+-----------------------
+
+.. code-block:: text
+
+ phy_disconnect(phydev)
+ -> phy_stop() to ensure the state machine stopped
+ -> clear bidirectional link (attached_dev = NULL, dev->phydev = NULL)
+ -> phy_device_reset(phydev, 1) // assert hard reset to finish
+ -> state = PHY_DOWN
+
+ mdio_unregister(handle) // MAC driver frees the bus on remove
+
+7. State Machine
+================
+
+The state machine is the core engine of PHYLIB, managing the complete PHY
+lifecycle.
+
+7.1 State Definitions
+---------------------
+
+.. code-block:: c
+
+ enum phy_state_e
+ {
+ PHY_DOWN = 0, // Device/driver not ready
+ PHY_READY, // PHY ready, MAC not started
+ PHY_UP, // Auto-negotiation started, waiting for link
+ PHY_RUNNING, // Link established
+ PHY_NOLINK, // PHY working but no link
+ PHY_HALTED, // PHY stopped
+ PHY_ERROR // Unrecoverable error (e.g. MDIO lost); forces carrier
+ // off and stops polling
+ };
+
+7.2 State Descriptions
+----------------------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 12 28 30 30
+
+ * - State
+ - Meaning
+ - Entry condition
+ - Exit condition
+ * - DOWN
+ - Device/driver not ready
+ - Initial state / after disconnect
+ - phy_connect succeeds
+ * - READY
+ - PHY ready, MAC not open
+ - config_init succeeds
+ - phy_start
+ * - UP
+ - Auto-negotiation started, waiting for link
+ - phy_start called
+ - link detection result determined
+ * - RUNNING
+ - Link established, normal traffic
+ - read_status returns link=1
+ - link lost / phy_stop
+ * - NOLINK
+ - PHY working but no link
+ - read_status returns link=0
+ - link restored / phy_stop
+ * - HALTED
+ - Stopped, PHY suspended
+ - phy_stop called
+ - phy_start / disconnect
+ * - ERROR
+ - Unrecoverable error (MDIO lost etc.)
+ - read_status / handle_interrupt returns < 0
+ - phy_start (recover via stop/start or disconnect/connect)
+
+7.3 Link Monitoring: Polling vs Interrupt
+-----------------------------------------
+
+PHYLIB supports two link-state monitoring modes, decided by the
+``phy_device_s.irq`` field. Both share the same state-transition logic
+(``phy_check_link``), differing only in the trigger source.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 12 44 44
+
+ * - Dimension
+ - Polling
+ - Interrupt
+ * - Enable condition
+ - ``irq == PHY_POLL`` (default)
+ - ``irq != PHY_POLL`` and the driver implements ``config_intr``
+ * - Trigger source
+ - LPWORK timer, every ``CONFIG_NET_PHYLIB_POLL_INTERVAL`` (default 1s)
+ - PHY INTB pin interrupt, fired immediately on link change
+ * - Response latency
+ - Worst case one poll period (default 1s)
+ - Near real-time (interrupt -> work schedule latency)
+ * - CPU cost
+ - One MDIO read per period, constant
+ - Reads only on link change, zero cost when idle
+ * - Backstop
+ - Is the backstop itself
+ - Keeps 8x low-frequency polling to catch missed interrupts
+
+Interrupt sources — three kinds. The interrupt mode can be further
+subdivided by the physical source, distinguished by the value of
+``phy_device_s.irq``:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 12 22 26 20 20
+
+ * - Source
+ - irq value
+ - Where the interrupt comes from
+ - Who attaches the ISR
+ - Who calls phy_mac_interrupt
+ * - Pin source
+ - >= 0 (real IRQ number)
+ - PHY INTB pin -> SoC GPIO interrupt
+ - phylib (irq_attach)
+ - phylib's phy_interrupt ISR
+ * - MAC source
+ - PHY_MAC_INTERRUPT(-2)
+ - MAC detects link change internally (e.g. SGMII in-band)
+ - the MAC driver
+ - the MAC driver ISR
+ * - Polling
+ - PHY_POLL(-1)
+ - No interrupt, timed register reads
+ - none
+ - none (timed work backstop)
+
+.. note::
+
+ The phylib framework does not care about the physical source of the
+ interrupt; it only exposes the unified entry ``phy_mac_interrupt()``.
+ Who calls it decides the source — the pin source is called by phylib's
+ own ISR, the MAC source by the MAC driver's ISR. The IRQ number is
+ filled into ``phydev->irq`` by the MAC/board driver. All three sources
+ converge into the same ``phy_check_link``; the link decision logic
+ exists only once.
+
+irq field three-state encoding:
+
+.. code-block:: c
+
+ /* phy.h: the two sentinels of the irq field */
+ #define PHY_POLL (-1) /* no interrupt source, pure polling */
+ #define PHY_MAC_INTERRUPT (-2) /* interrupt handled by the MAC driver,
phylib does not attach */
+ /* others (>= 0) = a real, attachable IRQ number (pin source) */
+
+ /* Whether it is a "real, attachable interrupt line" - i.e. a pin source */
+ #define phy_interrupt_is_valid(p) \
+ ((p)->irq != PHY_POLL && (p)->irq != PHY_MAC_INTERRUPT)
+
+.. note::
+
+ Key constraint: ``phy_interrupt`` (the pin-source ISR) runs in hard
+ interrupt context. NuttX has no threaded IRQ, so it must not read MDIO
+ (which would block). It therefore only "masks the line with
+ up_disable_irq + posts work via phy_mac_interrupt"; the real register
+ reads (handle_interrupt / read_status) are deferred to the LPWORK
+ thread. This is the only structural difference from a threaded-handler
+ design. The masked line is re-enabled by the worker (via up_enable_irq)
+ after handle_interrupt acknowledges the source (equivalent to
+ IRQF_ONESHOT).
+
+.. note::
+
+ In one sentence: ``phy_interrupt`` is the interrupt callback attached by
+ irq_attach and called directly by hardware; ``phy_mac_interrupt`` is the
+ notification API called from inside that callback (a thin forwarding
+ layer) — it does not decode, read registers or touch link state, it only
+ moves "something happened" safely from (possibly) interrupt context to
+ the LPWORK thread, where the unified ``phy_check_link`` does the real
+ work. The MAC source has no ``phy_interrupt``; the MAC driver's own ISR
+ calls ``phy_mac_interrupt`` directly.
+
+7.4 Link Detection Flow
+-----------------------
+
+Whether triggered by polling or interrupt, everything converges to the
+shared ``phy_check_link()``: read the real-time PHY link state -> compare
+with the current ``state`` -> on an up/down transition, notify the netdev
+carrier.
+
+7.5 Concurrency Model and Locks
+-------------------------------
+
+Who accesses the PHY / MDIO bus:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 40 20 40
+
+ * - Accessor
+ - Context
+ - When triggered
+ * - PHY state machine ``phy_check_link``
+ - LPWORK thread
+ - Polling: every 1s; interrupt: INTB -> work_queue
+ * - MII ioctl (SIOCGMIIREG/SIOCSMIIREG)
+ - Application thread
+ - User space issues ioctl
+ * - ethtool ioctl
+ - Application thread
+ - User space ``ethtool eth0`` etc.
+
+Serialization mechanisms:
+
+- **MDIO bus lock** (``mdio_bus_s.lock``, held by the upper-half
+ ``mdio.c``): all ``mdio_read/write/c45_*`` run under this lock, ensuring
+ mutual exclusion between the LPWORK state machine and application threads.
+ When a MAC driver's ``mdio_ops_s`` callback is entered the upper-half
+ already holds the lock, so **the callback must not take that lock again**,
+ or it deadlocks.
+- **PHY device lock** (``phy_device_s.lock``): protects the state fields of
+ a single ``phydev`` (``state``/``link``/``speed``/``duplex``/``autoneg``/
+ ``advertising``). All paths that read-modify-write state run under this
+ lock, serializing state transitions and avoiding races between the state
+ machine and ioctl. Initialized in ``phy_device_create`` via
+ ``nxmutex_init``.
+- **The state machine itself**: each ``phydev`` has only one ``state_work``,
+ executed serially by a single LPWORK thread.
+- **Interrupt context never touches MDIO**: ``phy_mac_interrupt()`` only
+ ``work_queue()``'s a task to LPWORK (ISR-safe); the real register access
+ happens in the LPWORK thread.
+
+8. MAC Driver Integration
+=========================
+
+This section uses the real AURIX TC4x + RTL8221B driver (``aurix_enet.c``)
+as a case study to detail the changes a MAC driver needs to integrate
+PHYLIB.
+
+.. note::
+
+ Core principle: a MAC driver only needs to do two things — (1) tell
+ PHYLIB how to read/write MDIO registers; (2) start/stop the state
+ machine at ifup/ifdown. All PHY-chip-specific init, auto-negotiation and
+ link detection are taken over by PHYLIB + the PHY driver.
+
+8.1 Interfaces the MAC Driver Must Implement
+--------------------------------------------
+
+When integrating PHYLIB, the MAC driver connects to two groups of
+interfaces: the ``netdev_lowerhalf`` OPS facing the network stack (already
+present; some callbacks now forward to PHYLIB), and the MDIO lower-half
+(``mdio_ops_s`` callback table, the new core part).
+
+netdev_lowerhalf OPS (facing the stack):
+
+.. list-table::
+ :header-rows: 1
+ :widths: 30 20 50
+
+ * - OPS callback
+ - After PHYLIB
+ - Notes
+ * - ``ifup``
+ - modified
+ - Calls ``mdio_register`` + ``phy_device_create`` + ``phy_connect`` +
+ ``phy_start``.
+ * - ``ifdown``
+ - modified
+ - Calls ``phy_stop`` + ``phy_disconnect`` + ``mdio_unregister``.
+ * - ``ioctl``
+ - simplified
+ - MII/ethtool commands intercepted by PHYLIB; private register code
+ can be removed.
+ * - ``transmit`` / ``receive`` / ``txpoll``
+ - unchanged
+ - Pure data plane, unrelated to PHY management.
+
+MDIO lower-half callbacks ``mdio_ops_s`` (facing the PHY, the new core):
+
+.. list-table::
+ :header-rows: 1
+ :widths: 25 15 60
+
+ * - Callback
+ - Required / optional
+ - Notes
+ * - ``read`` / ``write``
+ - **required**
+ - Clause 22 read/write; ``mdio_register`` validates ops is non-NULL.
+ * - ``read_c45`` / ``write_c45``
+ - optional
+ - Native Clause 45; when NULL the framework uses reg13/14 indirect
+ access. Implement when hardware supports native C45 (e.g. AURIX
+ GETH) to save transactions.
+
+.. note::
+
+ Comparison: before, the MAC driver managed both the data plane and the
+ PHY control plane (init / link polling / MII ioctl / MDIO), making it
+ bloated and tightly coupled to the PHY chip; after, the MAC driver keeps
+ only the **data plane + one group of mdio_bus callbacks**, with all PHY
+ control-plane logic pushed down to PHYLIB. Swapping the PHY chip needs
+ zero MAC driver change.
+
+8.2 Change Overview
+-------------------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 10 50 40
+
+ * - Step
+ - What
+ - Where
+ * - 1
+ - Add mdio_lowerhalf + handle members to the struct
+ - ``aurix_enet_dev_s``
+ * - 2
+ - Implement the MDIO read/write callbacks
+ - New static functions
+ * - 3
+ - Register the bus + create the PHY at init
+ - ``aurix_enet_ifup`` or init
+ * - 4
+ - connect + start in ifup
+ - ``aurix_enet_ifup``
+ * - 5
+ - stop in ifdown
+ - ``aurix_enet_ifdown``
+
+8.3 Step 1: Add MDIO Lower-Half Members
+---------------------------------------
+
+.. code-block:: c
+
+ struct aurix_enet_dev_s
+ {
+ struct netdev_lowerhalf_s dev;
+ struct aurix_enet_module_config_s config_d;
+ bool ifup;
+ bool mac_ready;
+ const struct aurix_enet_config_s *config;
+ struct work_s init_work;
+
+ /* ===== New: MDIO lower-half + bus handle ===== */
+ struct mdio_lowerhalf_s mdio_lower; /* lower-half instance
(ops shell) */
+ FAR struct mdio_bus_s *mdio; /* handle from
mdio_register */
+
+ /* ... other fields unchanged ... */
+ };
+
+.. note::
+
+ The whole ``mdio_bus_s`` bus object is no longer embedded; only the
+ lower-half ``mdio_lowerhalf_s`` (a thin ops vtable shell) and the bus
+ handle pointer returned by the upper-half are kept. The bus core object
+ (including the lock) is allocated and held by the upper-half
+ ``drivers/net/mdio.c``. In multi-instance cases (BMP multi-core /
+ multi-port) each ``aurix_enet_dev_s`` has its own lower, and the callback
+ uses ``container_of`` to recover the instance.
+
+8.4 Step 2: Implement the MDIO Lower-Half Callbacks
+---------------------------------------------------
+
+Wrap the hardware MDIO controller into ``mdio_ops_s`` callbacks. Note the
+community signature: ``lower`` is the lower-half instance, the read value
+is returned via the ``value`` out-parameter, and the return is 0 / negative
+errno.
+
+.. code-block:: c
+
+ /* Clause 22 read - called via ops->read */
+ static int aurix_mdio_c22_read(FAR struct mdio_lowerhalf_s *lower,
+ uint8_t phyaddr, uint8_t regaddr,
+ FAR uint16_t *value)
+ {
+ FAR struct aurix_enet_dev_s *priv =
+ container_of(lower, struct aurix_enet_dev_s, mdio_lower);
+
+ if (IfxGeth_Eth_mdio_read_clause22(priv->config_d.geth,
+ priv->config->port,
+ phyaddr, regaddr, value))
+ {
+ return -EIO;
+ }
+
+ return OK;
+ }
+
+ /* Clause 22 write */
+ static int aurix_mdio_c22_write(FAR struct mdio_lowerhalf_s *lower,
+ uint8_t phyaddr, uint8_t regaddr,
+ uint16_t val)
+ {
+ FAR struct aurix_enet_dev_s *priv =
+ container_of(lower, struct aurix_enet_dev_s, mdio_lower);
+
+ if (IfxGeth_Eth_mdio_write_clause22(priv->config_d.geth,
+ priv->config->port,
+ phyaddr, regaddr, val))
+ {
+ return -EIO;
+ }
+
+ return OK;
+ }
+
+ /* Native Clause 45 read/write (AURIX GETH supports it, avoids reg13/14) */
+ static int aurix_mdio_c45_read(FAR struct mdio_lowerhalf_s *lower,
+ uint8_t phyaddr, uint8_t devad,
+ uint16_t regaddr, FAR uint16_t *value)
+ {
+ FAR struct aurix_enet_dev_s *priv =
+ container_of(lower, struct aurix_enet_dev_s, mdio_lower);
+
+ if (IfxGeth_Eth_mdio_read_clause45(priv->config_d.geth,
+ priv->config->port,
+ phyaddr, devad, regaddr, value))
+ {
+ return -EIO;
+ }
+
+ return OK;
+ }
+
+ static int aurix_mdio_c45_write(FAR struct mdio_lowerhalf_s *lower,
+ uint8_t phyaddr, uint8_t devad,
+ uint16_t regaddr, uint16_t val)
+ {
+ FAR struct aurix_enet_dev_s *priv =
+ container_of(lower, struct aurix_enet_dev_s, mdio_lower);
+
+ if (IfxGeth_Eth_mdio_write_clause45(priv->config_d.geth,
+ priv->config->port,
+ phyaddr, devad, regaddr, val))
+ {
+ return -EIO;
+ }
+
+ return OK;
+ }
+
+PHY hardware reset callback ``aurix_mdio_reset`` (level control):
+
+.. code-block:: c
+
+ /* PHY hard reset line (PHYRSTB) control. value!=0 asserts (drive low,
+ * into reset); value==0 deasserts (drive high, release). The reset pin
+ * is selected by GETH port (port0->P10, port1->P11). The upper-half
+ * mdio_reset() already makes it idempotent via reset_state; here we only
+ * perform the requested level flip + the datasheet assert/settle delays.
+ */
+ static int aurix_mdio_reset(FAR struct mdio_lowerhalf_s *lower,
+ uint8_t phyaddr, int value)
+ {
+ FAR struct aurix_enet_dev_s *priv =
+ container_of(lower, struct aurix_enet_dev_s, mdio_lower);
+ int pin = (priv->config->port == 0) ? 10 : 11;
+
+ IfxPort_setPinModeOutput(priv->config_d.port_p, pin,
+ IfxPort_OutputMode_pushPull,
+ IfxPort_OutputIdx_general);
+ if (value)
+ {
+ IfxPort_setPinLow(priv->config_d.port_p, pin); /* assert */
+ up_mdelay(10); /* Tgap >= 10ms */
+ }
+ else
+ {
+ IfxPort_setPinHigh(priv->config_d.port_p, pin); /* deassert */
+ up_mdelay(68); /* settle 68ms */
+ }
+
+ return OK;
+ }
+
+.. note::
+
+ PHYLIB also provides a thin wrapper ``phy_device_reset(phydev, value)``
+ (forwarding to ``mdio_reset(phydev->bus, phydev->mdio_addr, value)``),
+ called by ``phy_connect`` (deassert) / ``phy_disconnect`` (assert)
+ during the lifecycle.
+
+8.5 Step 3: Register MDIO + Create PHY at ifup
+----------------------------------------------
+
+.. note::
+
+ MDIO callback execution-context constraint: ``read``/``write``/
+ ``read_c45``/``write_c45`` are triggered by two kinds of callers — (1)
+ the PHY state machine's ``read_status`` (running in the **LPWORK
+ thread**); (2) ethtool/MII ioctl (running in an **application thread**).
+ Both are serialized by the upper-half bus lock. Therefore the callback
+ implementation may sleep/block (it is always in thread context, never
+ in interrupt/atomic context), and must not re-acquire the upper-half bus
+ lock (the framework already holds it; doing so deadlocks).
+
+.. code-block:: c
+
+ /* In aurix_enet_ifup(), after aurix_mdio_init() */
+
+ static int aurix_phylib_setup(struct aurix_enet_dev_s *priv)
+ {
+ /* MDIO lower-half ops table (file-scope static const) */
+ static const struct mdio_ops_s g_aurix_mdio_ops =
+ {
+ .read = aurix_mdio_c22_read,
+ .write = aurix_mdio_c22_write,
+ #ifdef CONFIG_MDIO_CLAUSE_45
+ .read_c45 = aurix_mdio_c45_read,
+ .write_c45 = aurix_mdio_c45_write,
+ #endif
+ .reset = aurix_mdio_reset, /* PHY hard reset line (PHYRSTB) level
control */
+ };
+
+ int ret;
+
+ /* 1. Register the vendor PHY driver (idempotent) */
+ phy_realtek_register();
+
+ /* 2. Fill the lower-half and register with the upper-half, get the
handle */
+ priv->mdio_lower.ops = &g_aurix_mdio_ops;
+
+ priv->mdio = mdio_register(&priv->mdio_lower);
+ if (priv->mdio == NULL)
+ {
+ nerr("ERROR: mdio_register failed\n");
+ return -ENODEV;
+ }
+
+ /* 3. Hard-reset the PHY (must complete before reading the PHY ID).
+ * assert->deassert forms a complete power-on pulse; the lower-half
+ * applies 10ms assert + 68ms settle delays.
+ */
+ mdio_reset(priv->mdio, ETH_PHY_ADDRESS, 1); /* assert */
+ mdio_reset(priv->mdio, ETH_PHY_ADDRESS, 0); /* deassert + settle */
+
+ /* 4. Create the PHY device (known address, read PHY ID, match RTL8221B)
*/
+ priv->phydev = phy_device_create(priv->mdio, ETH_PHY_ADDRESS);
+ if (priv->phydev == NULL)
+ {
+ nerr("ERROR: phy_device_create failed\n");
+ mdio_unregister(priv->mdio);
+ priv->mdio = NULL;
+ return -ENODEV;
+ }
+
+ /* 5. Connect the PHY to the netdev (runs config_init / firmware patch) */
+ ret = phy_connect(&priv->dev, priv->phydev);
+ if (ret < 0)
+ {
+ nerr("ERROR: phy_connect failed: %d\n", ret);
+ mdio_unregister(priv->mdio);
+ priv->mdio = NULL;
+ priv->phydev = NULL;
+ return ret;
+ }
+
+ return OK;
+ }
+
+8.6 Step 4: Start the State Machine in ifup
+-------------------------------------------
+
+.. code-block:: c
+
+ static int aurix_enet_ifup(struct netdev_lowerhalf_s *dev)
+ {
+ struct aurix_enet_dev_s *priv = (struct aurix_enet_dev_s *)dev;
+
+ /* ... hardware init (hsphy_init, geth_init, mdio_init) ... */
+
+ #ifdef CONFIG_NET_PHYLIB
+ /* Integrate PHYLIB: register bus + create PHY + connect */
+ ret = aurix_phylib_setup(priv);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ /* Start the PHY state machine - begin auto-negotiation + link polling */
+ phy_start(priv->dev.phydev);
+
+ #else
+ /* Old path: call MCAL PHY init directly (kept for compatibility) */
+ ret = aurix_realtekethphy_init(priv);
+ #endif
+
+ /* ... remaining init (DMA, interrupts, MAC enable) ... */
+ }
+
+.. note::
+
+ After ``phy_start()`` the link state is managed automatically by the
+ PHYLIB state machine. When the PHY detects link up, PHYLIB calls
+ ``netdev_lower_carrier_on()`` and the stack starts sending/receiving.
+ The MAC driver no longer judges link state manually.
+
+8.7 Step 5: Stop the State Machine in ifdown
+--------------------------------------------
+
+.. code-block:: c
+
+ static int aurix_enet_ifdown(struct netdev_lowerhalf_s *dev)
+ {
+ struct aurix_enet_dev_s *priv = (struct aurix_enet_dev_s *)dev;
+
+ #ifdef CONFIG_NET_PHYLIB
+ /* Stop the PHY state machine */
+ if (priv->dev.phydev != NULL)
+ {
+ phy_stop(priv->dev.phydev);
+ }
+ #endif
+
+ /* ... remaining cleanup (disable interrupts, stop MAC) ... */
+ }
+
+8.8 Benefits After Integration
+------------------------------
+
+#. Removes about 200 lines of PHY management code
+ (``aurix_realtekethphy_init`` + ``aurix_ethlink_up`` + link-retry logic).
+#. Gets ethtool support automatically (speed/duplex/autoneg query and set).
+#. Link changes are reported to the stack automatically, no manual
+ carrier_on/off.
+#. Swapping the PHY chip only needs a vendor driver file; zero MAC change.
+#. Unified MII ioctl behavior, no longer implemented per driver.
+
+9. Master/Slave Core Link Sync in BMP Multi-core
+================================================
+
+9.1 Background: the Shared MAC Model under BMP
+----------------------------------------------
+
+AURIX TC4x in BMP (Bare Metal Porting / AMP) mode runs an independent
+NuttX instance on each core (``CONFIG_NCPUS=6``, ``CONFIG_SMP_NCPUS=1`` per
+core), communicating through shared memory. They share the same GETH MAC
+hardware, but each core has its own ``aurix_enet_dev_s`` / netdev.
+
+.. note::
+
+ The MDIO controller and the PHY are a globally unique physical resource.
+ If 6 independent OS instances accessed MDIO / ran the PHY state machine
+ simultaneously, it would cause bus contention and state corruption.
+ Therefore a single owner must be established.
+
+Following the "control-plane / data-plane separation" principle of
+SR-IOV (the PF manages the PHY, the VFs only send/receive and query the PF
+for link), the BMP design is:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 30 20 50
+
+ * - Role
+ - Maps to
+ - Responsibility
+ * - main_core
+ - SR-IOV PF
+ - Owns MDIO / PHY exclusively, runs the phylib state machine,
+ **publishes** link state.
+ * - non-main_core
+ - SR-IOV VF
+ - Does not touch the PHY, **subscribes** to link state, reflects it to
+ its own netdev carrier.
+
+The difference: SR-IOV uses a hardware mailbox; BMP uses **shared memory**
+(``shared_data_manual``) to pass link state.
+
+Before (no phylib): there was no real master/slave link sync. main_core
+polled the PHY UTP link via the iLLD ``aurix_ethlink_up()`` (with retry)
+and did ``carrier_on``; non-main_core did an **unconditional**
+``netdev_lower_carrier_on()``, i.e. assumed carrier was always up and never
+knew the real physical link.
+
+After (phylib + shared memory): main_core's phylib state machine manages
+the link uniformly and publishes it through the shared-memory flag
+``eth_link_up``; non-main_core subscribes to that flag and **really
+follows** the physical link up/down.
+
+9.2 Shared Memory Field
+-----------------------
+
+Add a link flag to ``shared_data_manual_s`` in
+``vendor/infineon/chips/aurix/include/chip.h``:
+
+.. code-block:: c
+
+ typedef struct
+ {
+ volatile int tmadc_sync_barrier;
+ volatile int eth_sync_barrier;
+ volatile bool eth_mac_ready_flag;
+
+ /* ENET PHY link state published by the main core (which owns the PHY
+ * and runs the phylib state machine) so that non-main cores can follow
+ * the real link status instead of assuming carrier is always up.
+ */
+ volatile bool eth_link_up;
+ struct can_module_s can_module[CAN_MODULE_NUM];
+ volatile int leth_sync_barrier;
+ } shared_data_manual_s;
+
+.. note::
+
+ ``eth_link_up`` is marked ``volatile``, consistent with the existing
+ ``eth_sync_barrier``. After main_core writes it, it calls ``UP_DMB()``
+ for cross-core visibility. The shared region lives in the RPTUN shared
+ segment, visible to all cores.
+
+9.3 Writer: main_core Publishes Link State
+-------------------------------------------
+
+main_core owns the PHY; the phylib state machine manages its own carrier.
+It additionally runs a periodic ``aurix_enet_link_worker`` mirroring
+``phydev->link`` to shared memory:
+
+.. code-block:: c
+
+ static void aurix_enet_link_worker(void *arg)
+ {
+ struct aurix_enet_dev_s *priv = (struct aurix_enet_dev_s *)arg;
+
+ /* ifdown cleared priv->ifup, so the worker stops naturally */
+ if (!priv->ifup)
+ {
+ return;
+ }
+
+ if (priv->config->main_core)
+ {
+ if (priv->phydev != NULL)
+ {
+ /* Publish the current link state for other cores to read */
+ shared_data_manual.eth_link_up = priv->phydev->link;
+ UP_DMB();
+ }
+ }
+ else
+ {
+ /* non-main_core: see below */
+ }
+
+ work_queue(LPWORK, &priv->link_work, aurix_enet_link_worker,
+ priv, MSEC2TICK(CONFIG_NET_PHYLIB_POLL_INTERVAL));
+ }
+
+9.4 Reader: non-main_core Follows Link State
+--------------------------------------------
+
+non-main_core has no phydev and runs no state machine; in the same
+``link_worker`` it reads the shared flag and, using edge detection
+(``link_up_last``), updates its own carrier only on change:
+
+.. code-block:: c
+
+ else
+ {
+ /* non-main_core: follow the link state published by main_core */
+ bool link = shared_data_manual.eth_link_up;
+
+ if (link != priv->link_up_last)
+ {
+ priv->link_up_last = link;
+ if (link)
+ {
+ netdev_lower_carrier_on(&priv->dev);
+ }
+ else
+ {
+ netdev_lower_carrier_off(&priv->dev);
+ }
+ }
+ }
+
+non-main_core does ``netdev_lower_carrier_off()`` first at ifup (initial
+``link_up_last = false``), guaranteeing it starts from carrier-off
+regardless of the current link, then the worker raises it per the shared
+flag.
+
+.. note::
+
+ This mechanism is only enabled under ``CONFIG_AURIX_ENET_USE_PHY &&
+ CONFIG_NET_PHYLIB``. Single-core or non-PHYLIB cases fall back to the
+ original behavior (direct carrier_on).
+
+10. User-space PHY Control Call Chains
+======================================
+
+User space controls the PHY through ioctl system calls, in two kinds by
+cmd:
+
+- **SIOCETHTOOL**: link parameter query/set (used by the ethtool tool).
+- **SIOCGMIIREG / SIOCSMIIREG**: direct PHY register read/write.
+
+10.1 Path 1: ethtool ioctl (link parameters)
+---------------------------------------------
+
+.. code-block:: text
+
+ User: ethtool eth0 / ethtool -s eth0 speed 100
+ | ioctl(fd, SIOCETHTOOL, &ifr)
+ Socket layer: net/netdev/netdev_ioctl.c -> netdev_ioctl()
+ | find net_device by ifr_name, cmd == SIOCETHTOOL
+ | call dev->d_ioctl(dev, cmd, arg)
+ Upper half: drivers/net/netdev_upperhalf.c -> netdev_upper_ioctl()
+ | cmd == SIOCETHTOOL && lower->phydev != NULL
+ PHYLIB: phy_upper_ethtool_ioctl(lower->phydev, arg)
+ | switch(ethtool_cmd)
+ |-- ETHTOOL_GLINKSETTINGS -> phy_ethtool_get_link_ksettings()
+ |-- ETHTOOL_SLINKSETTINGS -> phy_ethtool_set_link_ksettings()
+ |-- ETHTOOL_NWAY_RST -> phy_ethtool_nway_reset()
+ |-- ETHTOOL_GLINK -> phydev->link
+ | (if PHYLIB returns -ENOTTY, fall through to the MAC ethtool_ops)
+ Upper half: netdev_upper_ethtool_ioctl(dev, arg)
+
+10.2 Path 2: MII ioctl (direct register access)
+------------------------------------------------
+
+.. code-block:: text
+
+ User: ioctl(fd, SIOCGMIIREG, &ifr)
+ | struct mii_ioctl_data_s passes phy_id and reg_num
+ Socket layer: net/netdev/netdev_ioctl.c -> netdev_ioctl()
+ | cmd == SIOCGMIIREG/SIOCSMIIREG/SIOCGMIIPHY
+ | call dev->d_ioctl(dev, cmd, arg)
+ Upper half: drivers/net/netdev_upperhalf.c -> netdev_upper_ioctl()
+ | lower->phydev != NULL
+ PHYLIB: phy_upper_mii_ioctl(lower->phydev, cmd, arg)
+ |-- SIOCGMIIPHY -> return phydev->mdio_addr
+ |-- SIOCGMIIREG -> phy_read(phydev, reg_num)
+ |-- SIOCSMIIREG -> phy_write(phydev, reg_num, val)
+ |
+ phy_core.c: phy_read() / phy_write()
+ |
+ mdio.c (upper half): mdio_read/mdio_write - locked, calls
lower->ops->read/write
+ |
+ MAC hardware MDIO controller -> PHY chip registers
+
+10.3 Command to Kernel Call Mapping
+-----------------------------------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 30 25 45
+
+ * - ethtool command
+ - Kernel CMD
+ - Final call
+ * - ``ethtool eth0``
+ - ETHTOOL_GLINKSETTINGS
+ - ``phy_ethtool_get_link_ksettings()``
+ * - ``ethtool -s eth0 speed 100``
+ - ETHTOOL_SLINKSETTINGS
+ - ``phy_ethtool_set_link_ksettings()`` -> ``drv->config_aneg()``
+ * - MII register read
+ - SIOCGMIIREG
+ - ``phy_upper_mii_ioctl()`` -> ``phy_read()`` -> ``mdio_read()``
+ * - MII register write
+ - SIOCSMIIREG
+ - ``phy_upper_mii_ioctl()`` -> ``phy_write()`` -> ``mdio_write()``
+
+.. note::
+
+ Key design: ``netdev_upper_ioctl`` first checks ``lower->phydev !=
+ NULL`` and gives the command to PHYLIB first. Only when PHYLIB returns
+ ``-ENOTTY`` (does not handle this command) does it fall through to the
+ MAC driver's own ethtool_ops or ``lower->ops->ioctl``. This guarantees
+ unified behavior with PHYLIB and backward compatibility without it.
+
+11. Debugging and Verification
+==============================
+
+11.1 ethtool Commands
+---------------------
+
+With ``CONFIG_SYSTEM_ETHTOOL=y``, in NSH:
+
+.. code-block:: text
+
+ core1> ethtool eth0
+ Settings for eth0:
+ Supported link modes:
+ 10baseT/Full 100baseT/Full 1000baseT/Full Autoneg 2500baseT/Full
+ Supports auto-negotiation: Yes
+ Speed: 1000Mb/s
+ Duplex: Full
+ Auto-negotiation: on
+ PHY Address: 2
+ Link detected: yes
+
+Supported commands:
+
+- ``ethtool eth0`` — show link settings and status
+- ``ethtool -s eth0 speed 100 duplex full`` — change link parameters
+- ``ethtool -r eth0`` — restart auto-negotiation
+
+11.2 Kernel Log
+---------------
+
+PHYLIB emits key events via ``ninfo()``/``nerr()`` (needs
+``CONFIG_DEBUG_NET_INFO=y``):
+
+.. code-block:: text
+
+ [CPU1] phylib: PHY id=0x001cc849 addr=2 drv='RTL8221B 2.5Gbps PHY' c45=1
+ [CPU1] phylib: phy_start config_aneg ret=0
+ [CPU1] phylib: PHY link up: 1000 Mbps full
+ [CPU1] phylib: PHY link down
+
+12. Performance Overhead Evaluation
+===================================
+
+To quantify the extra overhead the PHYLIB abstraction adds to the PHY
+register access path, the most common path — MII register read
+(``SIOCGMIIREG``) — was measured.
+
+Method: a start timestamp is recorded at the entry of the user-space
+``mdio`` tool and an end timestamp on the kernel-side path; the difference
+is the software-path time of that access. Timestamps use
+``perf_gettime() * 1000000 / perf_getfreq()`` in microseconds. Each path is
+sampled 100 times and averaged.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 15 70 15
+
+ * - Path
+ - Call chain
+ - Avg time
+ * - No PHYLIB
+ - ``ioctl`` -> netdev_upper_ioctl -> ``lower->ops->ioctl`` ->
+ aurix_enet_ioctl -> private MDIO read
+ - ~13 us
+ * - With PHYLIB
+ - ``ioctl`` -> netdev_upper_ioctl -> phy_upper_mii_ioctl -> phy_read
+ -> mdio_bus -> MDIO callback
+ - ~12 us
+
+.. note::
+
+ Conclusion: the PHYLIB abstraction adds almost no overhead to the MII
+ register access path. The two paths average essentially the same (13 us
+ vs 12 us), a difference on the order of 1 us. The bulk of the 13 us is
+ the user-to-kernel syscall cost of ioctl; the PHYLIB forwarding chain
+ itself is only a small part. Introducing PHYLIB does not observably
+ affect PHY register access performance.
+
+13. Framework Extensions (Implemented)
+=======================================
+
+On top of the base framework, the following capabilities were added to
+support paged PHYs and automotive single-pair Ethernet (100BASE-T1) PHYs.
+All are backward compatible (removed at compile time when
+``CONFIG_NET_PHYLIB=n``).
+
+13.1 Driver Private Data phy_device_s.priv
+------------------------------------------
+
+``struct phy_device_s`` gains a ``FAR void *priv`` pointer for a PHY driver
+to keep chip-specific state across callbacks (config_init / read_status /
+suspend / resume), such as DVDDIO voltage selection, operation state or
+patch-load flags — fields beyond IEEE 802.3 that should not go into the
+generic structure.
+
+- **Allocation**: the driver uses ``kmm_malloc()`` (usually in
+ config_init) and stores it in ``phydev->priv``.
+- **Free**: the framework ``kmm_free()``'s it in ``phy_disconnect()``.
Review Comment:
The document does not match the code: here it says the framework frees
`phydev->priv` in `phy_disconnect()`, but `phy_release_device()` does not free
it (and `rtl8221b.c` stores `(FAR void *)1` there); line 1526 gives
`PHY_SUPPORTED_100BASET1_FULL (1 << 12)`, the code has
`SUPPORTED_100BASET1_FULL (1 << 13)`; lines 522/554/1024 call
`phy_device_reset()`, the code has `phy_reset()`. `mdio.rst` still describes
`reset` as a one-shot reset and does not list `read_c45`/`write_c45`.
`realtek_phy.h:47` says registration is idempotent, but `phy_driver_register()`
takes a new slot on each call.
Fix: update phylib.rst, mdio.rst and the `rtl8221b_register()` comment to
the code.
##########
drivers/net/Kconfig:
##########
@@ -837,4 +837,60 @@ endif # NET_IGC
source "drivers/net/oa_tc6/Kconfig"
+comment "PHY Support"
+
+menuconfig NET_PHYLIB
+ bool "PHY Abstraction Layer (PHYLIB)"
+ default n
+ depends on NET
+ depends on MDIO_BUS
+ ---help---
+ Enable the PHY Abstraction Layer (PHYLIB) which provides a
+ standardized MDIO bus abstraction, PHY device model, PHY driver
+ framework, and PHY state machine. This decouples PHY management
+ from individual MAC drivers.
+
+if NET_PHYLIB
+
+config NET_PHYLIB_POLL_INTERVAL
+ int "PHY state machine poll interval (ms)"
+ default 1000
+ ---help---
+ The interval in milliseconds at which the PHY state machine
+ polls the PHY link status.
+
+config NET_PHYLIB_MAX_DRIVERS
+ int "Maximum number of PHY drivers"
+ default 8
+ ---help---
+ Maximum number of PHY drivers that can be registered
+ simultaneously.
+
+config NET_PHYLIB_MAX_PHYS
+ int "Maximum number of PHY devices"
+ default 8
+ ---help---
+ Maximum number of PHY devices that can be tracked
+ simultaneously.
+
+comment "PHY Device Drivers"
+
+config NET_PHY_REALTEK_RTL8221B
Review Comment:
`NET_PHY_REALTEK_RTL8221B` can be enabled without `MDIO_CLAUSE_45`, but
`rtl8221b.c` calls `phy_read_mmd()`, `phy_write_mmd()` and `phy_modify_mmd()`,
which `phy.h` declares only with `CONFIG_MDIO_CLAUSE_45`. That build fails:
`rtl8221b.c:102:13: error: implicit declaration of function 'phy_write_mmd'`.
Fix: add `depends on MDIO_CLAUSE_45` (or `select MDIO_CLAUSE_45`) to
`NET_PHY_REALTEK_RTL8221B`.
##########
drivers/net/phy/phy_core.c:
##########
@@ -0,0 +1,1002 @@
+/****************************************************************************
+ * drivers/net/phy/phy_core.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <errno.h>
+#include <debug.h>
+#include <inttypes.h>
+#include <string.h>
+#include <sys/types.h>
+#include <syslog.h>
+
+#include <nuttx/irq.h>
+#include <nuttx/arch.h>
+#include <nuttx/wqueue.h>
+#include <nuttx/net/phy.h>
+#include <nuttx/net/mii.h>
+#include <nuttx/net/ioctl.h>
+#include <nuttx/net/netdev_lowerhalf.h>
+#include <nuttx/ethtool.h>
+
+#include <net/if.h>
+
+#include "phy.h"
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#define PHY_POLL_TICKS MSEC2TICK(CONFIG_NET_PHYLIB_POLL_INTERVAL)
+
+/* The PHY core currently stores each link mode mask in one 32-bit word. */
+
+#define PHY_LINK_MODE_MASK_NWORDS 1
+
+/* phy_interrupt_is_valid - true only when phy_device_s.irq is a real,
+ * attachable interrupt number. Both sentinels (PHY_POLL, PHY_MAC_INTERRUPT)
+ * are negative, so a non-negative irq is a real, attachable IRQ number.
+ */
+
+#define phy_interrupt_is_valid(p) ((p)->irq >= 0)
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: phy_check_link
+ *
+ * Description:
+ * Read the current PHY status and run the link up/down state transitions,
+ * updating phydev->state. Shared by both the polling work and the
+ * interrupt work so the two paths behave identically.
+ *
+ * Carrier notification (netdev_lower_carrier_on/off) is NOT performed
+ * here: it takes the netdev d_lock and would invert the lock order used
+ * by the ioctl path (d_lock then phydev->lock), risking an AB-BA
+ * deadlock. The caller therefore compares phydev->state before and after
+ * this call and performs the carrier notification after releasing
+ * phydev->lock.
+ *
+ * Input Parameters:
+ * phydev - Pointer to the PHY device structure
+ *
+ * Returned Value:
+ * None. The new link state is reflected in phydev->state.
+ *
+ ****************************************************************************/
+
+static void phy_check_link(FAR struct phy_device_s *phydev)
+{
+ int ret;
+
+ /* In interrupt mode, service the PHY interrupt source first so INTB
+ * de-asserts and a fresh edge can be latched. Both the polling and the
+ * interrupt paths share this single state machine entry point: the
+ * interrupt path only kicks the state machine, and the driver's
+ * handle_interrupt() does the actual decode/ack.
+ *
+ * handle_interrupt - the driver reads its own vendor interrupt-status
+ * register, decodes ALL pending sources (link and non-link), services
+ * non-link diagnostic events internally, and returns:
+ * >0 : a link-relevant event is pending -> re-read link status below
+ * 0 : no link-relevant event -> skip the link re-read
+ * <0 : MDIO/HW failure (PHY unreachable) -> enter PHY_ERROR
+ */
+
+ if (phydev->irq != PHY_POLL && phydev->drv->handle_interrupt)
+ {
+ ret = phydev->drv->handle_interrupt(phydev);
+ if (ret < 0)
+ {
+ /* The PHY is unreachable (could not even read its interrupt
+ * status register). Enter the error state: force the carrier
+ * off and stop rescheduling. Recovery needs an explicit
+ * stop/start cycle.
+ */
+
+ nerr("ERROR: handle_interrupt failed: %d\n", ret);
+ phydev->state = PHY_ERROR;
+ return;
+ }
+ else if (ret == 0)
Review Comment:
In interrupt mode `phy_check_link()` returns before `read_status()` when
`handle_interrupt()` returns 0. The direct `phy_work()` call at line 829 is
there to read the initial link state, but `config_intr(true)` has just cleared
the interrupt status (RTL8221B INSR, RTL9000B GINSR), so the first run returns
here and the state stays `PHY_UP` until the next link change. With RTL9000B,
`config_aneg()` does nothing on the first start, so a link that is already up
gives no interrupt and the carrier stays off.
Fix: always call `read_status()` in the run started by `phy_start()` (for
example skip `handle_interrupt()` while `phydev->state == PHY_UP`).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]