Linux MDIO 深入分析
🖥️ Linux MDIO 深入分析
MDIO(Management Data Input/Output)是IEEE 802.3定义的一种串行管理接口,用于以太网MAC层与PHY层之间的通信。Linux内核中的MDIO子系统提供了一套完整的框架,用于管理通过MDIO总线连接的PHY设备。它将PHY设备的访问抽象化,使得驱动开发者能够以统一的方式操作不同厂商的PHY芯片。
1 MDIO 工作原理与协议基础
MDIO接口最初由IEEE 802.3标准定义,包含两根信号线:
- MDC(Management Data Clock):时钟信号,由MAC设备产生,最高频率可达2.5MHz(MDIO标准定义)或更高(后续标准扩展)。
- MDIO(Management Data Input/Output):双向数据信号,用于在MAC和PHY之间传输数据。
1.1 MDIO 协议帧结构
MDIO协议采用一种简单的基于帧的通信格式,基本帧结构如下:
+---------------+---------------+---------------+----------------+--------+--------+----------+
| PRE (32 bits) | ST (2 bits) | OP (2 bits) | PHYAD (5 bits) | REGAD (5 bits) | TA (2 bits) | DATA (16 bits) |
+---------------+---------------+---------------+----------------+--------+--------+----------+
各字段含义如下:
- PRE(Preamble):前缀域,32个连续的"1"比特,用于同步时钟。
- ST(Start):起始位,固定为"01",标识帧开始。
- OP(Operation Code):操作码,"10"表示读操作,"01"表示写操作。
- PHYAD(PHY Address):PHY芯片地址,5比特,最多支持32个PHY设备。
- REGAD(Register Address):寄存器地址,5比特,每个PHY最多32个寄存器。
- TA(Turnaround):状态转换域,读操作时第一比特为高阻态,第二比特由PHY置"0";写操作时输出"10"。
- DATA:数据域,16比特,传输的寄存器数据。
- IDLE:空闲状态,MDIO进入高阻状态,通常由上拉电阻保持高电平。
MDIO协议读写操作时序如下:
2 Linux MDIO 子系统实现机制
Linux内核中的MDIO子系统采用标准的总线-设备-驱动模型,提供了灵活且可扩展的框架来管理PHY设备。
2.1 MDIO 总线架构
MDIO子系统遵循Linux设备模型,主要包含以下组件:
2.2 核心数据结构
2.2.1 struct mii_bus
- MDIO总线结构
struct mii_bus {const char *name; // 总线名称char id[MII_BUS_ID_SIZE]; // 总线IDvoid *priv; // 私有数据int (*read)(struct mii_bus *bus, int phy_id, int regnum); // 读操作函数指针int (*write)(struct mii_bus *bus, int phy_id, int regnum, u16 val); // 写操作函数指针int phy_mask; // PHY地址掩码struct phy_device *phy_map[PHY_MAX_ADDR]; // PHY设备数组struct device dev; // 设备结构struct module *owner; // 所属模块// 其他字段省略...
};
2.2.2 struct phy_device
- PHY设备结构
struct phy_device {struct mii_bus *bus; // 所属MDIO总线int phy_id; // PHY标识符int addr; // PHY地址u32 phy_id_mask; // PHY ID掩码u32 phy_id_value; // PHY ID值struct phy_driver *drv; // 关联的PHY驱动void *priv; // 私有数据int speed; // 当前速度int duplex; // 当前双工模式int link; // 链路状态// 其他字段省略...
};
2.2.3 struct phy_driver
- PHY驱动结构
struct phy_driver {u32 phy_id; // PHY标识符char *name; // 驱动名称u32 phy_id_mask; // PHY ID掩码int (*probe)(struct phy_device *phydev); // 探测函数int (*config_init)(struct phy_device *phydev); // 配置初始化函数int (*config_aneg)(struct phy_device *phydev); // 自动协商函数int (*read_status)(struct phy_device *phydev); // 读取状态函数int (*ack_interrupt)(struct phy_device *phydev); // 中断确认函数int (*config_intr)(struct phy_device *phydev); // 配置中断函数void (*remove)(struct phy_device *phydev); // 移除函数// 其他字段省略...
};
2.3 设备驱动匹配机制
MDIO总线使用三种匹配机制来关联PHY设备和驱动:
- 设备树匹配:首先检查设备树节点的
compatible
属性是否与驱动中的of_match_table
匹配。 - 自定义匹配函数:如果驱动实现了
match_phy_device
函数,则调用该函数进行匹配。 - PHY ID匹配:最后比较PHY设备的
phy_id
与驱动中的phy_id
和phy_id_mask
是否匹配。
匹配流程的代码如下:
static int mdio_bus_match(struct device *dev, struct device_driver *drv)
{struct phy_device *phydev = to_phy_device(dev);struct phy_driver *phydrv = to_phy_driver(drv);// 1. 设备树匹配if (of_driver_match_device(dev, drv))return 1;// 2. 自定义匹配函数if (phydrv->match_phy_device)return phydrv->match_phy_device(phydev);// 3. PHY ID匹配return (phydrv->phy_id & phydrv->phy_id_mask) ==(phydev->phy_id & phydrv->phy_id_mask);
}
3 MDIO 总线驱动开发
3.1 实现MDIO总线操作
开发MDIO总线驱动主要需要实现mii_bus
结构的读写方法:
#include <linux/phy.h>
#include <linux/mii_bus.h>// MDIO总线读操作
static int my_mdio_read(struct mii_bus *bus, int phy_id, int regnum)
{struct my_private_data *priv = bus->priv;u32 value;// 1. 硬件特定的MDIO读序列// 2. 设置MDIO帧格式并发送// 3. 读取返回的数据return value;
}// MDIO总线写操作
static int my_mdio_write(struct mii_bus *bus, int phy_id, int regnum, u16 value)
{struct my_private_data *priv = bus->priv;// 1. 硬件特定的MDIO写序列// 2. 设置MDIO帧格式并发送数据return 0;
}// MDIO总线初始化
static int my_mdio_probe(struct platform_device *pdev)
{struct mii_bus *bus;struct my_private_data *priv;int ret;// 分配MDIO总线结构bus = mdiobus_alloc_size(sizeof(struct my_private_data));if (!bus)return -ENOMEM;bus->name = "my_mdio_bus";bus->read = my_mdio_read;bus->write = my_mdio_write;bus->parent = &pdev->dev;snprintf(bus->id, MII_BUS_ID_SIZE, "my-mdio");priv = bus->priv;// 初始化硬件特定数据// 注册MDIO总线ret = of_mdiobus_register(bus, pdev->dev.of_node);if (ret) {dev_err(&pdev->dev, "Cannot register MDIO bus\n");mdiobus_free(bus);return ret;}platform_set_drvdata(pdev, bus);return 0;
}
3.2 实现PHY设备驱动
PHY驱动开发需要实现phy_driver
结构的主要方法:
#include <linux/phy.h>// 读取PHY状态
static int my_phy_read_status(struct phy_device *phydev)
{int val;// 读取基本状态寄存器val = phy_read(phydev, MII_BMSR);if (val < 0)return val;// 检查链路状态if (val & BMSR_LSTATUS)phydev->link = 1;elsephydev->link = 0;// 读取更多状态信息(速度、双工模式等)// ...return 0;
}// 配置自动协商
static int my_phy_config_aneg(struct phy_device *phydev)
{// 配置自动协商广告能力// ...return genphy_config_aneg(phydev);
}// PHY驱动结构
static struct phy_driver my_phy_driver = {.phy_id = 0x01410bc0, // PHY标识符.name = "My Ethernet PHY", // 驱动名称.phy_id_mask = 0xfffffff0, // PHY ID掩码.probe = my_phy_probe, // 探测函数.config_init = my_phy_config_init, // 初始化配置.config_aneg = my_phy_config_aneg, // 自动协商.read_status = my_phy_read_status, // 读取状态.ack_interrupt = my_phy_ack_interrupt, // 中断确认.config_intr = my_phy_config_intr, // 配置中断
};// 模块初始化
static int __init my_phy_init(void)
{return phy_driver_register(&my_phy_driver);
}// 模块退出
static void __exit my_phy_exit(void)
{phy_driver_unregister(&my_phy_driver);
}module_init(my_phy_init);
module_exit(my_phy_exit);
4 用户空间访问MDIO总线
除了内核驱动,用户空间也可以直接访问MDIO总线来读写PHY寄存器。
4.1 使用ioctl系统调用
以下是一个用户空间程序,通过ioctl系统调用访问PHY寄存器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <linux/mii.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/sockios.h>
#include <linux/types.h>
#include <netinet/in.h>#define reteck(ret) \if(ret < 0){ \printf("%m! \"%s\" : line: %d\n", __func__, __LINE__); \goto lab; \}int main(int argc, char *argv[])
{if (argc < 3) {printf("Usage: %s <interface> <reg> [value]\n", argv[0]);printf("Read: %s eth0 1\n", argv[0]);printf("Write: %s eth0 0 0x1120\n", argv[0]);return -1;}int sockfd;struct mii_ioctl_data *mii = NULL;struct ifreq ifr;int ret;memset(&ifr, 0, sizeof(ifr));strncpy(ifr.ifr_name, argv[1], IFNAMSIZ - 1);// 创建套接字sockfd = socket(PF_LOCAL, SOCK_DGRAM, 0);reteck(sockfd);// 获取PHY地址ret = ioctl(sockfd, SIOCGMIIPHY, &ifr);reteck(ret);mii = (struct mii_ioctl_data*)&ifr.ifr_data;if (argc == 3) {// 读取寄存器mii->reg_num = (uint16_t)strtoul(argv[2], NULL, 0);ret = ioctl(sockfd, SIOCGMIIREG, &ifr);reteck(ret);printf("Read PHY addr: 0x%x, reg: 0x%x, value: 0x%x\n", mii->phy_id, mii->reg_num, mii->val_out);// 检查链路状态(寄存器1的第2位)if (mii->reg_num == 1 && (mii->val_out & 0x0004)) {printf("Link: UP\n");} else if (mii->reg_num == 1) {printf("Link: DOWN\n");}} else if (argc == 4) {// 写入寄存器mii->reg_num = (uint16_t)strtoul(argv[2], NULL, 0);mii->val_in = (uint16_t)strtoul(argv[3], NULL, 0);ret = ioctl(sockfd, SIOCSMIIREG, &ifr);reteck(ret);printf("Write PHY addr: 0x%x, reg: 0x%x, value: 0x%x\n", mii->phy_id, mii->reg_num, mii->val_in);}lab:close(sockfd);return 0;
}
编译和使用方法:
gcc -o mdio-tool mdio-tool.c
./mdio-tool eth0 1 # 读取寄存器1
./mdio-tool eth0 0 0x1120 # 写入值0x1120到寄存器0
4.2 使用mdio-tool工具
除了自己编写代码,还可以使用现有的mdio-tool
工具来访问MDIO总线:
# 列出MDIO总线上的设备
mdio-tool list# 读取PHY寄存器
mdio-tool read 0 0x01# 写入PHY寄存器
mdio-tool write 0 0x00 0x8000
5 调试和故障排除
MDIO子系统调试常用的方法和工具如下:
5.1 常用调试命令
命令 | 描述 | 示例 |
---|---|---|
dmesg \| grep -i mdio | 查看内核MDIO相关消息 | dmesg \| grep -i mdio |
dmesg \| grep -i phy | 查看内核PHY相关消息 | dmesg \| grep -i phy |
mdio-tool list | 列出MDIO总线上的设备 | mdio-tool list |
mdio-tool read | 读取PHY寄存器 | mdio-tool read 0 0x01 |
mdio-tool write | 写入PHY寄存器 | mdio-tool write 0 0x00 0x8000 |
cat /sys/class/net/eth0/phy_settings | 查看PHY设置 | cat /sys/class/net/eth0/phy_settings |
5.2 常见问题及解决方法
-
PHY设备未检测到
# 错误信息 davinci_mdio c000f00.mdio: phy[10]: device c000f00.mdio:0a, MDIO device at address 10 is missing.
解决方法:检查设备树配置、硬件连接和电源。
-
使用通用PHY驱动
# 错误信息 am65-cpsw-nuss c000000.ethernet eth1: PHY [c000f00.mdio:0a] driver [Generic PHY] (irq=POLL) davinci_mdio c000f00.mdio: phy[10]: device c000f00.mdio:0a, driver unknown
解决方法:确保专用PHY驱动已编译并加载到内核。
-
读取返回0xFFFF
解决方法:检查MDIO总线物理连接、时钟信号和电源。
5.3 内核调试技巧
启用内核调试选项:
# 配置内核开启MDIO/PHY调试支持
make menuconfig
# 选择: Device Drivers -> Network device support -> PHY Device support and infrastructure -> Debugging support for PHY drivers
使用动态调试:
# 启用MDIO总线调试
echo "file mdio_bus.c +p" > /sys/kernel/debug/dynamic_debug/control# 启用PHY驱动调试
echo "file phy_device.c +p" > /sys/kernel/debug/dynamic_debug/control
6 总结
Linux MDIO子系统为以太网PHY设备管理提供了完整的框架,通过标准的总线-设备-驱动模型实现了不同PHY芯片的统一访问。开发MDIO相关驱动需要理解MDIO协议、Linux设备模型和PHY芯片特性。用户空间工具和内核调试机制为MDIO总线调试和故障排除提供了有效手段。