27. Linux 网络PHY驱动

PHY(Physical Layer Device,物理层设备)是网络通信的物理层组件,主要用于处理与物理传输介质(如铜线、光纤等)之间的信号交互。在计算机网络中,PHY 负责将数据转换为适合通过物理介质传输的信号,同时将接收到的物理信号解码为数据。具体来说,PHY 是网络通信协议的物理层实现部分,它与 MAC(Media Access Control,媒体访问控制)层一起工作,完成数据链路层的功能。

27.1 PHY的作用

PHY 在网络中有多种关键作用,它的核心功能是将数字数据和模拟信号之间进行双向转换。具体可以分为以下几大功能:

1.) 数据编码与解码

  • PHY 负责将来自 MAC 层的数字信号(比如以太网帧中的比特流)编码为可以通过物理介质传输的模拟信号。

  • 典型的编码方法有 Manchester 编码、NRZ(Non-Return to Zero)编码 等,它们是将二进制数据转换为电气信号的不同方式。

  • 在接收端,PHY 负责解码接收到的物理信号,恢复出原始的数字数据,并将其传递给 MAC 层。

2.) 信号调制与解调

  • 在发送端,PHY 负责将编码后的数字信号调制为适合在物理介质上传输的模拟信号。这包括电气信号的电压/电流调制(用于铜缆)或者光信号调制(用于光纤)。

  • 在接收端,PHY 负责将接收到的模拟信号进行解调,提取出信号中的数字信息。

3.) 链路检测与维护

  • PHY 设备可以检测链路状态,确定物理连接的质量、速度、以及双工模式(全双工或半双工)。

  • 当检测到链路状态变化(比如网络电缆被拔掉或插入,或者网速变化),PHY 会通知 MAC 层,以便进行相应的处理。

4.) 自动协商(Auto-Negotiation)

  • 在支持自动协商的网络中,PHY 会负责与对端设备进行通信,确定最适合的链路参数。这包括链路速度(如 10Mbps、100Mbps、1Gbps 等)和双工模式(全双工或半双工)。

  • 自动协商可以在连接两台设备时自动选择最佳的传输速度和模式,确保链路的高效性和兼容性。

5.) 媒介独立接口(MII, GMII, RGMII 等)

  • PHY 通常与 MAC 层通过特定的媒介独立接口(MII, GMII, RGMII, SGMII 等)进行通信,这些接口定义了在物理层与 MAC 层之间传输数据的电气信号和时序。

  • 例如,MII(Media Independent Interface)是常见的 10/100Mbps 以太网接口标准,RGMII(Reduced Gigabit Media Independent Interface)是更高速的千兆以太网接口。

6.) 时钟恢复

  • PHY 负责在从物理介质中接收到的信号中提取时钟信息。因为传输的信号中不仅包含数据,还隐含着时钟信息,PHY 必须通过特定算法(如 PLL, Phase-Locked Loop)恢复出时钟信号,以确保数据的同步。

7.) 电气特性处理

  • PHY 负责控制电气信号的强度、形状等电气特性,以便在不同的物理介质(如电缆、光纤等)上实现数据传输。

  • 它还管理信号的发送和接收之间的切换,并根据网络条件调整信号强度。

27.2 PHY 与 MAC 的协作

PHY 和 MAC 协作完成以太网通信的整个过程。PHY 主要处理物理层的信号,而 MAC 处理数据链路层的帧。它们之间通过标准的媒介独立接口(MII、RGMII、SGMII 等)进行通信。

+-----------------+                            +---------------------+
|                 |         RGMII/SGMII        |                     |
|      MAC        |<-------------------------->|        PHY          |
|                 |    bit stream: 10110101    |                     |
+-----------------+                            +---------------------+

以下是二者之间的工作流程:

1.) 数据发送过程

  • MAC 层负责将上层协议(如 IP 或 ARP)的数据帧封装成以太网帧。

  • 以太网帧通过 MII 或其他接口发送给 PHY。

  • PHY 对帧进行编码、调制,将数据转换为物理信号,并通过网线(如 RJ45 铜缆)发送出去。

2.) 数据接收过程

  • 当收到来自物理介质的信号时,PHY 进行解调和解码,恢复出以太网帧。

  • PHY 通过 MII 或其他接口将解码后的帧传递给 MAC 层。

  • MAC 处理该帧,提取出数据并传递给上层协议栈(如 IP 层)。

27.3 Linux PHY驱动

这里我们来移植YT8521的PHY驱动, 首先需要设置DTS。

27.3.1 DTS配置

PHY 设备通常是在 mdio(Management Data Input/Output)节点下配置的,这是因为 PHY 设备通过 MDIO 总线与主设备(例如以太网控制器)通信。MDIO 是一种用于管理和控制以太网 PHY 的串行接口,它允许网络控制器(MAC)读取和写入 PHY 寄存器,因此 PHY 必须挂在 MDIO 总线上。

&gmac1 {
        /* Use rgmii-rxid mode to disable rx delay inside Soc */
        phy-mode = "rgmii-rxid";
        clock_in_out = "input";

        snps,reset-gpio = <&gpio3 RK_PB7 GPIO_ACTIVE_LOW>;
        snps,reset-active-low;
        /* Reset time is 20ms, 100ms for rtl8211f */
        snps,reset-delays-us = <0 20000 100000>;

        pinctrl-names = "default";
        pinctrl-0 = <&gmac1_miim
                     &gmac1_tx_bus2
                     &gmac1_rx_bus2
                     &gmac1_rgmii_clk
                     &gmac1_rgmii_bus
                         &gmac1_clkinout>;

        tx_delay = <0x44>;
        /*rx_delay = <0x0>; */

        phy-handle = <&rgmii_phy1>;
        status = "okay";
};

&mdio1 {
        rgmii_phy1: phy@1 {
                compatible = "ethernet-phy-ieee802.3-c22";
                reg = <0x0>;
        };
};

每一个PHY在MDIO总线上面都有唯一的地址, PHY 设备通过 phy-handle 与对应的网络控制器(MAC)关联,上面gmac里有指定PHY是rgmii_phy1, 而rgmii_phy1定义在mdio节点里, phy的地址是1。

27.3.2 驱动代码

1.) 注册phy driver

YT8521的 Linux 驱动代码在: kernel/drivers/net/phy/motorcomm.c, 其中向内核注册phy driver部分代码如下:

static struct phy_driver ytphy_drvs[] = {
	{
                .phy_id         = PHY_ID_YT8521,
		.name           = "YT8521 Ethernet",
		.phy_id_mask    = MOTORCOMM_PHY_ID_MASK,
		.features       = PHY_GBIT_FEATURES,
		.flags          = PHY_POLL,
#if (KERNEL_VERSION(4, 0, 0) > LINUX_VERSION_CODE)
#else
		.soft_reset     = yt8521_soft_reset,
#endif
		.config_aneg    = genphy_config_aneg,
#if (KERNEL_VERSION(3, 14, 79) < LINUX_VERSION_CODE)
		.aneg_done      = yt8521_aneg_done,
#endif
		.config_init    = yt8521_config_init,
		.read_status    = yt8521_read_status,
		.suspend        = yt8521_suspend,
		.resume         = yt8521_resume,
#if (YTPHY_WOL_FEATURE_ENABLE)
		.get_wol        = &ytphy_wol_feature_get,
		.set_wol        = &ytphy_wol_feature_set,
#endif
        }
};
static int ytphy_drivers_register(struct phy_driver *phy_drvs, int size)
{
	int i, j;
	int ret;

	for (i = 0; i < size; i++) {
		ret = phy_driver_register(&phy_drvs[i]);
		if (ret)
			goto err;
	}

	return 0;

err:
		for (j = 0; j < i; j++)
			phy_driver_unregister(&phy_drvs[j]);

	return ret;
}

static void ytphy_drivers_unregister(struct phy_driver *phy_drvs, int size)
{
	int i;

	for (i = 0; i < size; i++)
		phy_driver_unregister(&phy_drvs[i]);
}
static int __init ytphy_init(void)
{
	return ytphy_drivers_register(ytphy_drvs, ARRAY_SIZE(ytphy_drvs));
}

static void __exit ytphy_exit(void)
{
	ytphy_drivers_unregister(ytphy_drvs, ARRAY_SIZE(ytphy_drvs));
}

module_init(ytphy_init);
module_exit(ytphy_exit);

phy_driver_register 是 Linux 内核中用于注册 PHY 驱动的 API,负责将定义好的 phy_driver 结构体与内核的 PHY 子系统关联,允许内核识别和使用该驱动来管理特定的物理层设备(PHY)。通过这个 API,内核可以知道哪些驱动与哪些 PHY 设备兼容,并能在设备初始化时找到对应的驱动。

这里也就是告诉内核PHY ID是PHY_ID_YT8521使用此驱动, 由于在DTS mdio节点里有定义phy设备, 因此当MDIO总线初始化时, 会透过phy地址读取phy ID, 再依据此ID查找带ID已经注册的phy驱动。

2.) 报告link status

static int yt8521_read_status(struct phy_device *phydev)
{
	int ret;
	volatile int val;
	volatile int yt8521_fiber_latch_val;
	volatile int yt8521_fiber_curr_val;
	volatile int link;
	int link_utp = 0, link_fiber = 0;

	if (YT8521_PHY_MODE_CURR != YT8521_PHY_MODE_FIBER) {
		/* reading UTP */
		ret = ytphy_write_ext(phydev, 0xa000, 0);
		if (ret < 0)
			return ret;

		val = phy_read(phydev, REG_PHY_SPEC_STATUS);
		if (val < 0)
			return val;

		link = val & (BIT(YT8521_LINK_STATUS_BIT));
		if (link) {
			link_utp = 1;
			yt8521_adjust_status(phydev, val, 1);
		} else {
			link_utp = 0;
		}
	} //(YT8521_PHY_MODE_CURR != YT8521_PHY_MODE_FIBER)

	if (YT8521_PHY_MODE_CURR != YT8521_PHY_MODE_UTP) {
		/* reading Fiber */
		ret = ytphy_write_ext(phydev, 0xa000, 2);
		if (ret < 0)
			return ret;

		val = phy_read(phydev, REG_PHY_SPEC_STATUS);
		if (val < 0)
			return val;

		//note: below debug information is used to check multiple PHy ports.

		/* for fiber, from 1000m to 100m, there is not link down from 0x11,
		 * and check reg 1 to identify such case this is important for Linux
		 * kernel for that, missing linkdown event will cause problem.
		 */
		yt8521_fiber_latch_val = phy_read(phydev, MII_BMSR);
		yt8521_fiber_curr_val = phy_read(phydev, MII_BMSR);
		link = val & (BIT(YT8521_LINK_STATUS_BIT));
		if (link && yt8521_fiber_latch_val != yt8521_fiber_curr_val) {
			link = 0;
#if (KERNEL_VERSION(4, 5, 0) > LINUX_VERSION_CODE)            
			netdev_info(phydev->attached_dev, "%s, phy addr: %d, fiber link down detect, latch = %04x, curr = %04x\n",
				__func__, phydev->addr, yt8521_fiber_latch_val, yt8521_fiber_curr_val);
#else
			netdev_info(phydev->attached_dev, "%s, phy addr: %d, fiber link down detect, latch = %04x, curr = %04x\n",
				__func__, phydev->mdio.addr, yt8521_fiber_latch_val, yt8521_fiber_curr_val);
#endif
		}

		if (link) {
			link_fiber = 1;
			yt8521_adjust_status(phydev, val, 0);
		} else {
			link_fiber = 0;
		}
	} //(YT8521_PHY_MODE_CURR != YT8521_PHY_MODE_UTP)

	if (link_utp || link_fiber) {
		if (phydev->link == 0)
#if (KERNEL_VERSION(4, 5, 0) > LINUX_VERSION_CODE)
			netdev_info(phydev->attached_dev, "%s, phy addr: %d, link up, media: %s, mii reg 0x11 = 0x%x\n",
				__func__, phydev->addr, (link_utp && link_fiber) ? "UNKONWN MEDIA" : (link_utp ? "UTP" : "Fiber"), (unsigned int)val);
#else
			netdev_info(phydev->attached_dev, "%s, phy addr: %d, link up, media: %s, mii reg 0x11 = 0x%x\n",
				__func__, phydev->mdio.addr, (link_utp && link_fiber) ? "UNKONWN MEDIA" : (link_utp ? "UTP" : "Fiber"), (unsigned int)val);
#endif
		phydev->link = 1;
	} else {
		if (phydev->link == 1)
#if (KERNEL_VERSION(4, 5, 0) > LINUX_VERSION_CODE)
			netdev_info(phydev->attached_dev, "%s, phy addr: %d, link down\n", __func__, phydev->addr);
#else
			netdev_info(phydev->attached_dev, "%s, phy addr: %d, link down\n", __func__, phydev->mdio.addr);
#endif
		phydev->link = 0;
	}

	if (YT8521_PHY_MODE_CURR != YT8521_PHY_MODE_FIBER) {    //utp or combo
		if (link_fiber)
			ytphy_write_ext(phydev, 0xa000, 2);
		if (link_utp)
			ytphy_write_ext(phydev, 0xa000, 0);
	}
	return 0;
}

3.) 编译

修改kernel/drivers/net/phy/Makefile, 在最后一行添加如下即可:

obj-y  += motorcomm.o