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.) 信号调制与解调
3.) 链路检测与维护
4.) 自动协商(Auto-Negotiation)
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.) 电气特性处理
27.2 PHY 与 MAC 的协作
PHY 和 MAC 协作完成以太网通信的整个过程。PHY 主要处理物理层的信号,而 MAC 处理数据链路层的帧。它们之间通过标准的媒介独立接口(MII、RGMII、SGMII 等)进行通信。
+-----------------+ +---------------------+
| | RGMII/SGMII | |
| MAC |<-------------------------->| PHY |
| | bit stream: 10110101 | |
+-----------------+ +---------------------+
以下是二者之间的工作流程:
1.) 数据发送过程
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