在Linux网络驱动开发中,除了net_device结构体和NAPI等软件架构,理解数据包的实际发送硬件链路同样关键。这通常涉及MAC与PHY两个核心硬件模块的协同工作。
以太网硬件通常由两个独立的部分组成:
- MAC (Media Access Control):位于数据链路层,负责以太网帧的格式处理、CRC校验、MAC地址识别以及与CPU之间的数据搬移(通常通过DMA)。MAC通常是集成在SoC内部的网络控制器,其驱动是网络功能的核心。
- PHY (Physical Layer):位于物理层,负责将数字信号调制为能在网线上传输的模拟信号,包括编码、解码、时钟恢复和自适应等。PHY通常是一颗独立的外部芯片(如RTL8211E, AR8031)。
简单来说,MAC负责组帧和逻辑控制,PHY则负责物理信号的转换。二者通过MII/RMII/RGMII等接口连接。
为了配置和监控PHY芯片,需要一个管理接口,这就是MDIO (Management Data Input/Output) 总线。它是一个简单的两线串行协议:
- MDC:管理数据时钟线。
- MDIO:管理数据输入/输出线。
MAC驱动作为主设备,通过MDIO总线读写PHY的内部寄存器,从而配置其速度(10/100/1000 Mbps)、双工模式(半双工/全双工)以及监控链路状态。
在Linux内核驱动开发中,PHY驱动被抽象为一个独立的子系统,架构清晰:
- MDIO总线驱动:通常集成在SoC的MAC驱动中,负责实现MDIO协议的底层时序,将读写请求转换为具体的MDC/MDIO信号。
- PHY核心:负责PHY设备的发现、注册、状态机管理和提供通用API。
- PHY驱动:针对特定型号的PHY芯片(如RTL8211F),实现其私有寄存器的配置逻辑。这正是驱动开发者需要编写的主要部分。
PHY驱动的编写围绕 struct phy_driver 结构体展开,开发者无需关心MDIO总线的传输细节,只需专注于芯片寄存器的操作。
PHY驱动匹配与核心结构
驱动通过一个由OUI(组织唯一标识符)和型号组成的PHY ID进行匹配。驱动核心结构体如下:
#include <linux/phy.h>
#include <linux/module.h>
/* PHY 驱动的核心结构体,封装了驱动的能力和匹配信息 */
struct phy_driver {
u32 phy_id; // 芯片的唯一 ID,通常通过读取 PHY_IDR1/PHY_IDR2 寄存器获得
u32 phy_id_mask; // 匹配掩码
char *name; // 驱动名称
// 关键回调函数
int (*probe)(struct phy_device *phydev);
int (*config_init)(struct phy_device *phydev); // 芯片初始化配置
int (*read_status)(struct phy_device *phydev); // 读取链路状态
void (*remove)(struct phy_device *phydev);
// ...
struct device_driver driver;
};
寄存器读写API
PHY核心通过MDIO总线驱动封装好了寄存器读写函数,开发者直接调用即可:
int phy_read(struct phy_device *phydev, u32 regnum);
int phy_write(struct phy_device *phydev, u32 regnum, u16 val);
PHY驱动实例:Realtek PHY配置
以下是一个简化的Realtek PHY驱动模板,展示了 config_init 函数的典型实现:
#define REALTEK_PHY_ID 0x001cc912 // 假设的 Realtek 芯片ID
// 寄存器地址定义 (部分为标准,部分为厂商特定)
#define MII_BMCR 0x00 // 基本模式控制寄存器
#define MII_BMSR 0x01 // 基本模式状态寄存器
#define RTL_SPEC_REG 0x10 // 厂商特定的寄存器
/* PHY 芯片的初始化配置 */
static int rtl_config_init(struct phy_device *phydev)
{
int ret;
// 1. 读取标准状态寄存器,了解芯片基本情况
ret = phy_read(phydev, MII_BMSR);
if (ret < 0) return ret;
dev_info(&phydev->dev, "PHY 芯片基本状态: 0x%x\n", ret);
// 2. 配置芯片特定的功能寄存器
// 例如:写入 0x0100 启用 Realtek 的某种节能模式
ret = phy_write(phydev, RTL_SPEC_REG, 0x0100);
if (ret < 0) return ret;
// 3. 调用内核通用函数,配置标准的速度/双工自动协商
ret = genphy_config_aneg(phydev);
return ret;
}
static int rtl_probe(struct phy_device *phydev)
{
// probe函数通常很简单,进行必要的初始化或信息打印
dev_info(&phydev->dev, "Realtek PHY 芯片已匹配并加载\n");
return 0;
}
static struct phy_driver rtl_phy_driver = {
.phy_id = REALTEK_PHY_ID,
.phy_id_mask = 0xfffffff0, // 掩码用于匹配同一系列芯片
.name = "Realtek-Eth-PHY",
.probe = rtl_probe,
.config_init = rtl_config_init,
.read_status = genphy_read_status, // 使用内核提供的通用状态读取函数
.driver = {
.owner = THIS_MODULE,
},
};
// 注册 PHY 驱动
module_phy_driver(rtl_phy_driver);
MODULE_LICENSE("GPL");
工作流程总结
整个Linux网络子系统中MDIO/MAC/PHY的协同工作流程如下:
- MAC驱动:初始化SoC内部网络控制器,实现
net_device的收发逻辑(DMA/NAPI),并注册其MDIO总线控制器驱动。
- PHY核心:扫描MDIO总线,读取每个PHY设备的ID寄存器(0x02和0x03),获取
phy_id。
- PHY驱动匹配:PHY核心根据获取的
phy_id,匹配已注册的struct phy_driver。
- PHY驱动工作:调用匹配成功的PHY驱动的
config_init等回调函数,通过phy_read/phy_write完成芯片特定配置。
- 数据流:网络数据包经MAC组帧后,通过MII接口发送给PHY,PHY根据配置将其转换为物理电信号发送至网络。