- 已编辑
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