Linux: 以太网 PHY 驱动简析

文章目录

  • 1. 前言
  • 2. 背景
  • 3. 硬件拓扑
  • 4. 以太网卡 PHY 驱动实现
    • 4.1 MDIO 总线对象的创建和注册
    • 4.2 MDIO 总线从设的 创建注册 和 驱动注册的加载
      • 4.2.1 以太网的 PHY 设备创建和注册
      • 4.2.2 以太网的 PHY 设备驱动注册和加载
    • 4.3 绑定以太网卡的 MAC 和 PHY
    • 4.4 以太网卡 PHY 和 MAC 的协同工作
    • 4.5 以太网卡 PHY 驱动示范

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本文分析基于 linux-4.14.132 内核代码进行分析。

3. 硬件拓扑

在这里插入图片描述
上图来自 瑞昱半导体 (RealTek) 的 RTL8201F 系列网卡 PHY 芯片手册。按OSI 7层网络模型划分,网卡PHY 芯片(图中的RTL8201F)位于物理层,对应的软件层就是本文讨论的 PHY 驱动层;而 MAC 位于 数据链路层,也是通常软件上所说的网卡驱动层,它不是本文的重点,不做展开。另外,可通过 MDIO 接口对 PHY 芯片进行配置(如PHY芯片寄存器读写),而 PHY 和 MAC 通过 MII/RMII 进行数据传输
值得一提的是,Linux的网络子系统,只取了OSI 7层网络模型前4层:物理层、数据链路层、网络层(IP协议等)、传输层(TPC/UDP协议等)在内核代码注释里,我们经常看到用 L2 指代 物理层, L3 指代 网络层

4. 以太网卡 PHY 驱动实现

在现在嵌入式产品中,以太网卡的典型应用是:在 SoC 中集成一个 MAC,而在外围扩展电路中,加上一个以太网 PHY 芯片,这样和 RJ45 以及 网线一起,组成一个完整的以太网通信接口。想集成第二个以太网卡的产品,通常是加入一个 USB 以太网芯片(包含MAC),然后再加入一路以太网 PHY ,和 额外的 RJ45 和 网线一起,组成系统中的第二个以太网通信接口

4.1 MDIO 总线对象的创建和注册

Linux系统中的网卡 PHY ,统一通过 MDIO 总线管理,看一下拓扑结构:

MDIO BUS ----> PHY 0
	        |
			|- PHY 1
			|
			|- ...
			|
			 - PHY 31

PHY 通过 MDIO 接口挂接在 MDIO BUS 上(通常是 MAC 的 MDIO BUS 上:即 MII 接口),一个 MDIO BUS 上最多挂 32 个 PHY 。
在 Linux 中, MDIO BUS 用数据结构 struct mii_bus 抽象,看一下它的具体实现:

struct mii_bus {
	struct module *owner;
	const char *name;
	char id[MII_BUS_ID_SIZE];
	void *priv;
	int (*read)(struct mii_bus *bus, int addr, int regnum);
	int (*write)(struct mii_bus *bus, int addr, int regnum, u16 val);
	int (*reset)(struct mii_bus *bus);

	/*
	 * A lock to ensure that only one thing can read/write
	 * the MDIO bus at a time
	 */
	struct mutex mdio_lock;

	struct device *parent;
	enum {
		MDIOBUS_ALLOCATED = 1,
		MDIOBUS_REGISTERED,
		MDIOBUS_UNREGISTERED,
		MDIOBUS_RELEASED,
	} state;
	struct device dev;

	/* list of all PHYs on bus */
	struct mdio_device *mdio_map[PHY_MAX_ADDR];

	/* PHY addresses to be ignored when probing */
	u32 phy_mask;

	/* PHY addresses to ignore the TA/read failure */
	u32 phy_ignore_ta_mask;

	/*
	 * An array of interrupts, each PHY's interrupt at the index
	 * matching its address
	 */
	int irq[PHY_MAX_ADDR];

	/* GPIO reset pulse width in microseconds */
	int reset_delay_us;
	/* RESET GPIO descriptor pointer */
	struct gpio_desc *reset_gpiod;
};

MDIO 总线对象由 MAC 层驱动(也就是我们通常所说的网卡驱动)创建。我们以一个实际的例子来分析 MDIO 总线 和 PHY 驱动的具体实现,先看 SoC 内置以太网 MAC 设备的 DTS 配置:

emac: ethernet@1c30000 {
	compatible = "allwinner,sun8i-h3-emac";
	...
	local-mac-address = [00 00 00 00 00 00];
	...
	phy-handle = <&ext_rgmii_phy>;
	phy-mode = "rgmii"; /* 使用 1000Mbps 模式 */

	/* MAC 的 MDIO 配置 */
	mdio: mdio {
		#address-cells = <1>;
		#size-cells = <0>;
		...
		/* 挂接在 MAC 的 MDIO 总线上 PHY 的 配置 */
		int_mii_phy: ethernet-phy@1 {
			compatible = "ethernet-phy-ieee802.3-c22";
			...
		};
	};
};

来看 MDIO 总线对象的注册细节,从 MAC 驱动(也即通常说的网卡驱动)开始:

static const struct of_device_id sun8i_dwmac_match[] = {
	{ .compatible = "allwinner,sun8i-h3-emac",
		.data = &emac_variant_h3 },
	...
	{ }
};
MODULE_DEVICE_TABLE(of, sun8i_dwmac_match); 

static struct platform_driver sun8i_dwmac_driver = {
	.probe  = sun8i_dwmac_probe,
	...
	.driver = {
		.name           = "dwmac-sun8i",
		...
		.of_match_table = sun8i_dwmac_match,
	},
};
module_platform_driver(sun8i_dwmac_driver);

/* 
 * emac 驱动加载过程 
 */
static int sun8i_dwmac_probe(struct platform_device *pdev)
{
	int ret;
	
	...
	ret = stmmac_dvr_probe(&pdev->dev, plat_dat, &stmmac_res);
	...
	return ret;
}

int stmmac_dvr_probe(struct device *device,
		     struct plat_stmmacenet_data *plat_dat,
		     struct stmmac_resources *res)
{
	struct net_device *ndev = NULL;

	/* 创建网卡设备对象 */
	ndev = alloc_etherdev_mqs(sizeof(struct stmmac_priv),
				  MTL_MAX_TX_QUEUES,
				  MTL_MAX_RX_QUEUES);
	
	...
	
	stmmac_set_ethtool_ops(ndev); /* 提供对用户空间工具 ethtool 的支持 */
	...
	
	/* 设置 MAC 地址 */
	if (res->mac)
		memcpy(priv->dev->dev_addr, res->mac, ETH_ALEN);

	...

	ndev->netdev_ops = &stmmac_netdev_ops; /* 配置网卡接口 */

	...

	if (priv->hw->pcs != STMMAC_PCS_RGMII  &&
	    priv->hw->pcs != STMMAC_PCS_TBI &&
	    priv->hw->pcs != STMMAC_PCS_RTBI) {
		/* MDIO bus Registration */
		ret = stmmac_mdio_register(ndev); /* 注册 MAC 的 MDIO 总线对象 */
		...
	}

	ret = register_netdev(ndev); /* 注册网卡设备对象 */

	return ret;
}

我们这里实际已经给出了一个网卡驱动的框架,但我们这里不关注网卡驱动,MDIO 总线对象的注册过程,是我们关注的重点:

int stmmac_mdio_register(struct net_device *ndev)
{
	int err = 0;
	struct mii_bus *new_bus;
	...
	struct device_node *mdio_node = priv->plat->mdio_node;

	...
	new_bus = mdiobus_alloc(); /* 创建 MDIO 总线对象 */
	...

	if (mdio_node) {
		...
		err = of_mdiobus_register(new_bus, mdio_node);
	}  else {
		...
	}

	...
	
	return 0;
}

/* 创建 MDIO 总线对象 */
static inline struct mii_bus *mdiobus_alloc(void)
{
	return mdiobus_alloc_size(0);
}

struct mii_bus *mdiobus_alloc_size(size_t size)
{
	struct mii_bus *bus;
	size_t aligned_size = ALIGN(sizeof(*bus), NETDEV_ALIGN);
	size_t alloc_size;
	int i;

	alloc_size = sizeof(*bus);

	bus = kzalloc(alloc_size, GFP_KERNEL);
	...

	bus->state = MDIOBUS_ALLOCATED;
	...

	/* Initialise the interrupts to polling */
	/* 初始设置所有 PHY 以轮询方式处理连接事件(Link Up/Down, 自动协商, ...) */
	for (i = 0; i < PHY_MAX_ADDR; i++)
		bus->irq[i] = PHY_POLL;

	return bus;
}

/* drivers/of/of_mdio.c */
/* 注册 MDIO 总线对象 */
int of_mdiobus_register(struct mii_bus *mdio, struct device_node *np)
{
	int addr, rc;
	
	rc = mdiobus_register(mdio); /* __mdiobus_register(mdio, THIS_MODULE); */

	/* 
	 * 解析并注册 MDIO 总线后,这里还包含 MDIO 总线下挂载的 PHY 
	 * 设备的解析和注册过程,将在后面的章节进行分析。
	 * emac: ethernet@1c30000 {
	 * 		compatible = "allwinner,sun8i-h3-emac";
	 * 		...
	 * 		mdio: mdio {
	 * 			...
	 * 			// 挂接在 MAC 的 MDIO 总线上 PHY 的 配置
	 *			int_mii_phy: ethernet-phy@1 {
	 *				compatible = "ethernet-phy-ieee802.3-c22";
	 *				...
	 *			};
	 * 		};
	 * };
	 */
	/* Loop over the child nodes and register a phy_device for each phy */
	for_each_available_child_of_node(np, child) {
		...
		if (of_mdiobus_child_is_phy(child))
			rc = of_mdiobus_register_phy(mdio, child, addr); // 细节后续展开
		else
			...
		...
	}

	/* auto scan for PHYs with empty reg property */
	for_each_available_child_of_node(np, child) {
		/* Skip PHYs with reg property set */
		if (of_find_property(child, "reg", NULL))
			continue;
		
		for (addr = 0; addr < PHY_MAX_ADDR; addr++) {
			/* skip already registered PHYs */
			if (mdiobus_is_registered_device(mdio, addr))
				continue;
			
			/* be noisy to encourage people to set reg property */
			dev_info(&mdio->dev, "scan phy %s at address %i\n",
				 child->name, addr);
			
			if (of_mdiobus_child_is_phy(child)) {
				// 细节后续展开
				rc = of_mdiobus_register_phy(mdio, child, addr);
				...
			}
		}
		...
	}
	...

	return 0;
}

int __mdiobus_register(struct mii_bus *bus, struct module *owner)
{
	struct mdio_device *mdiodev;

	bus->owner = owner;
	bus->dev.parent = bus->parent;
	bus->dev.class = &mdio_bus_class;
	bus->dev.groups = NULL;
	dev_set_name(&bus->dev, "%s", bus->id);

	err = device_register(&bus->dev); /* 注册 MIDO 总线对象设备到 driver core */
	...

	/*
	 * 探测地址 0 ~ 31 ,看对应地址是否有物理设备挂接到 MDIO 总线上:
	 * 如果有,创建 MDIO 从设对象(struct phy_device) ,并将其注册到 
	 * MDIO 总线 和 driver core 。
	 */
	for (i = 0; i < PHY_MAX_ADDR; i++) {
		if ((bus->phy_mask & (1 << i)) == 0) {
			struct phy_device *phydev;

			/* 细节在后续章节展开 */
			phydev = mdiobus_scan(bus, i);
			...
		}
	}

	...

	bus->state = MDIOBUS_REGISTERED;
	pr_info("%s: probed\n", bus->name);
	return 0;
}

到此,已经完成 MAC 设备 MDIO 总线对象的 创建 和 注册,期间会伴随着挂接在 MDIO 总线上从设的扫描注册,将在接下来的章节展开。MDIO 总线对象的 创建 和 注册,涉及到两个接口:

/* 
 * MDIO 总线对象 创建、释放 接口。
 */
// include/linux/phy.h  
struct mii_bus *mdiobus_alloc_size(size_t);
static inline struct mii_bus *mdiobus_alloc(void)
{
	return mdiobus_alloc_size(0);
}

void mdiobus_free(struct mii_bus *bus);

struct mii_bus *devm_mdiobus_alloc_size(struct device *dev, int sizeof_priv);
static inline struct mii_bus *devm_mdiobus_alloc(struct device *dev)
{
	return devm_mdiobus_alloc_size(dev, 0);
}

void devm_mdiobus_free(struct device *dev, struct mii_bus *bus);

/* 
 * MDIO 总线对象 注册、注销 接口。
 */
// include/linux/phy.h 
int __mdiobus_register(struct mii_bus *bus, struct module *owner);
#define mdiobus_register(bus) __mdiobus_register(bus, THIS_MODULE)

void mdiobus_unregister(struct mii_bus *bus);

/* MDIO 总线对象 注册 接口:一些带 DTS 数据解析变种 */
// include/linux/of_mdio.h
extern int of_mdiobus_register(struct mii_bus *mdio, struct device_node *np);

4.2 MDIO 总线从设的 创建注册 和 驱动注册的加载

MDIO 总线从设,具体到我们例子中,就是以太网的 PHY 设备。

4.2.1 以太网的 PHY 设备创建和注册

上面提到,MDIO 总线对象的创建和注册过程中,会伴随对总线上从设的扫描探测,并为探测到物理设备创建设备对象并注册的过程。我们在这里展开章节 4.1 未涉及到的前述细节:

/* drivers/of/of_mdio.c */
/* 注册 MDIO 总线对象 */
int of_mdiobus_register(struct mii_bus *mdio, struct device_node *np)
{
	int addr, rc;
	
	rc = mdiobus_register(mdio); /* __mdiobus_register(mdio, THIS_MODULE); */

	/* 
	 * 解析并注册 MDIO 总线后,这里还包含 MDIO 总线下挂载的 PHY 
	 * 设备的解析和注册过程,将在后面的章节进行分析。
	 * emac: ethernet@1c30000 {
	 * 		compatible = "allwinner,sun8i-h3-emac";
	 * 		...
	 * 		mdio: mdio {
	 * 			...
	 * 			// 挂接在 MAC 的 MDIO 总线上 PHY 的 配置
	 *			int_mii_phy: ethernet-phy@1 {
	 *				compatible = "ethernet-phy-ieee802.3-c22";
	 *				...
	 *			};
	 * 		};
	 * };
	 */
	/* Loop over the child nodes and register a phy_device for each phy */
	for_each_available_child_of_node(np, child) {
		addr = of_mdio_parse_addr(&mdio->dev, child);
		...
		if (of_mdiobus_child_is_phy(child))
			rc = of_mdiobus_register_phy(mdio, child, addr);
		else
			...
		...
	}

	...
	
	return 0;
}

static bool of_mdiobus_child_is_phy(struct device_node *child)
{
	...
	
	/* 和我们例子中的 DTS 示例匹配 */
	if (of_device_is_compatible(child, "ethernet-phy-ieee802.3-c22"))
		return true;

	...

	return false;
}

static int of_mdiobus_register_phy(struct mii_bus *mdio,
				    struct device_node *child, u32 addr)
{
	struct phy_device *phy;

	if (!is_c45 && !of_get_phy_id(child, &phy_id))
		...
	else
		/* 为探测到 PHY 设备,创建 struct phy_device 设备对象 */
		phy = get_phy_device(mdio, addr, is_c45);
	
	...
	/* 简化了下此处代码 */
	phy->irq = mdio->irq[addr]; /* phy->irq = PHY_POLL; */

	...
	of_node_get(child);
	phy->mdio.dev.of_node = child;

	/* 注册 PHY 设备对象 到 对应的 MDIO 总线,同时添加 PHY 设备对象 到 driver core 。
	 * 如果 PHY 驱动已经注册,可触发驱动加载过程。
	 */ 
	rc = phy_device_register(phy);
	...

	return 0;
}

/* 探测 PHY 设备,并为探测到的设备创建 struct phy_device 设备对象 */
struct phy_device *get_phy_device(struct mii_bus *bus, int addr, bool is_c45)
{
	struct phy_c45_device_ids c45_ids = {0};
	u32 phy_id = 0;
	int r;
	
	r = get_phy_id(bus, addr, &phy_id, is_c45, &c45_ids);
	if (r) /* 没探测到 PHY 设备 */
		return ERR_PTR(r);

	/* If the phy_id is mostly Fs, there is no device there */
	if ((phy_id & 0x1fffffff) == 0x1fffffff) /* 探测设备的 PHY ID 错误 */
		return ERR_PTR(-ENODEV);
	
	/* 探测到了 PHY 设备,为其创建设备对象 struct phy_device */
	return phy_device_create(bus, addr, phy_id, is_c45, &c45_ids);
}

/* 探测 PHY 设备,实际就是读几个 PHY 寄存器,这些寄存器是 PHY 芯片规范设定的 (802.3-c22, ...) */
static int get_phy_id(struct mii_bus *bus, int addr, u32 *phy_id,
		      bool is_c45, struct phy_c45_device_ids *c45_ids)
{
	int phy_reg;

	if (is_c45)
		return get_phy_c45_ids(bus, addr, phy_id, c45_ids);

	/* Grab the bits from PHYIR1, and put them in the upper half */
	phy_reg = mdiobus_read(bus, addr, MII_PHYSID1);
	if (phy_reg < 0)
		return -EIO;

	*phy_id = (phy_reg & 0xffff) << 16;

	/* Grab the bits from PHYIR2, and put them in the lower half */
	phy_reg = mdiobus_read(bus, addr, MII_PHYSID2);
	if (phy_reg < 0)
		return -EIO;

	*phy_id |= (phy_reg & 0xffff);

	return 0;
}

/* 创建 PHY 设备对象 */
struct phy_device *phy_device_create(struct mii_bus *bus, int addr, int phy_id,
				     bool is_c45,
				     struct phy_c45_device_ids *c45_ids)
{
	struct phy_device *dev;
	struct mdio_device *mdiodev;

	dev = kzalloc(sizeof(*dev), GFP_KERNEL);

	mdiodev = &dev->mdio;
	mdiodev->dev.release = phy_device_release;
	mdiodev->dev.parent = &bus->dev;
	mdiodev->dev.bus = &mdio_bus_type;
	mdiodev->bus = bus;
	/*
	 * struct phy_device {
	 * 		struct mdio_device mdio; // 可以认为 phy_device 类设备是 mdio_device 的子类设备
	 * 		...
	 * };
	 */
	mdiodev->pm_ops = MDIO_BUS_PHY_PM_OPS; /* 理论上,MDIO 总线从设备,不一定只有 PHY ,区分下种类 */
	mdiodev->bus_match = phy_bus_match;
	mdiodev->addr = addr;
	mdiodev->flags = MDIO_DEVICE_FLAG_PHY;
	mdiodev->device_free = phy_mdio_device_free;
	mdiodev->device_remove = phy_mdio_device_remove;

	...
	dev->link = 1;
	dev->interface = PHY_INTERFACE_MODE_GMII;

	dev->autoneg = AUTONEG_ENABLE;

	...
	dev->phy_id = phy_id; /* 从物理 PHY 设备寄存器读到的 PHY ID */
	...
	dev->irq = bus->irq[addr]; /* PHY_POLL */
	dev_set_name(&mdiodev->dev, PHY_ID_FMT, bus->id, addr); /* 设置 PHY 设备对象名 */

	/* 处理 PHY 状态变化的 work : 如 link down, up, ... */
	INIT_DELAYED_WORK(&dev->state_queue, phy_state_machine);
	INIT_WORK(&dev->phy_queue, phy_change_work);

	device_initialize(&mdiodev->dev);

	return dev;
}

到此,以太网 PHY 设备的 创建 和 注册 已经完成。
必须说明的是,例子所对应的实际案例中,并没有挂接我们例子中 DTS 配置的 "ethernet-phy-ieee802.3-c22" PHY 设备,实际上挂接在 MAC 设备上 以太网 PHY 芯片,是 ReaTek 的 RTL8201F。该 PHY 设备在 MAC 驱动注册 MDIO 总线对象时,在 get_phy_device() 流程中被探测到,并为其创建了对应的设备对象。

4.2.2 以太网的 PHY 设备驱动注册和加载

说完 PHY 设备的创建注册,接下来看 PHY 设备驱动的注册和加载过程。

/* include/linux/phy.h */

#define phy_module_driver(__phy_drivers, __count)			\
static int __init phy_module_init(void)					\
{									\
	return phy_drivers_register(__phy_drivers, __count, THIS_MODULE); \
}									\
module_init(phy_module_init);						\
static void __exit phy_module_exit(void)				\
{									\
	phy_drivers_unregister(__phy_drivers, __count);			\
}									\
module_exit(phy_module_exit)

#define module_phy_driver(__phy_drivers)				\
	phy_module_driver(__phy_drivers, ARRAY_SIZE(__phy_drivers))
static int rtl8211f_config_init(struct phy_device *phydev)
{
	// 按芯片手册做适当操作
	return 0;
}

static int rtl8211f_ack_interrupt(struct phy_device *phydev)
{
	// 按芯片手册做适当操作
	return 0;
}

static int rtl8211f_config_intr(struct phy_device *phydev)
{
	// 按芯片手册做适当操作
	return 0;
}

static struct phy_driver realtek_drvs[] = {
	...
	{
		.phy_id		= 0x001cc916, /* PHY ID */
		.name		= "RTL8211F Gigabit Ethernet",
		.phy_id_mask	= 0x001fffff, /* PHY ID 掩码 */
		.features	= PHY_GBIT_FEATURES,
		.flags		= PHY_HAS_INTERRUPT,
		/* PHY 驱动接口 */
		.config_aneg	= &genphy_config_aneg,
		.config_init	= &rtl8211f_config_init,
		.read_status	= &genphy_read_status,
		.ack_interrupt	= &rtl8211f_ack_interrupt,
		.config_intr	= &rtl8211f_config_intr,
		.suspend	= genphy_suspend,
		.resume		= genphy_resume,
	},	
	...
};

module_phy_driver(realtek_drvs); /* 模块 加载、卸载 时分别 注册、注销 PHY 驱动 */

static struct mdio_device_id __maybe_unused realtek_tbl[] = {
	{ 0x001cc916, 0x001fffff }, /* PHY ID 和 PHY ID 掩码 */
	{ }
};
MODULE_DEVICE_TABLE(mdio, realtek_tbl);
int phy_drivers_register(struct phy_driver *new_driver, int n,
			 struct module *owner)
{
	int i, ret = 0;

	for (i = 0; i < n; i++) {
		ret = phy_driver_register(new_driver + i, owner);
		if (ret) {
			while (i-- > 0)
				phy_driver_unregister(new_driver + i);
			break;
		}
	}
	return ret;
}

int phy_driver_register(struct phy_driver *new_driver, struct module *owner)
{
	int retval;

	new_driver->mdiodrv.flags |= MDIO_DEVICE_IS_PHY;
	new_driver->mdiodrv.driver.name = new_driver->name;
	new_driver->mdiodrv.driver.bus = &mdio_bus_type;
	new_driver->mdiodrv.driver.probe = phy_probe;
	new_driver->mdiodrv.driver.remove = phy_remove;
	new_driver->mdiodrv.driver.owner = owner;

	/* 
	 * 注册 PHY 驱动到 driver core 。
	 * 如果 PHY 设备已经注册,可触发此驱动加载过程。
	 */
	retval = driver_register(&new_driver->mdiodrv.driver);
	...

	return retval;
}

假设 PHY 设备已经先注册到 driver core (先后关系是无所谓的,最终都会触发驱动的加载),注册 PHY 驱动将触发驱动加载过程:

phy_driver_register()
	driver_register(&new_driver->mdiodrv.driver)
		bus_add_driver(drv)
			driver_attach(drv)
				bus_for_each_dev(drv->bus, NULL, drv, __driver_attach)
					while ((dev = next_device(&i)) && !error)
						/* 循环到注册的 PHY 设备时 */
						fn(dev, data) = __driver_attach()
							/* 匹配设备和驱动 */
							driver_match_device(drv, dev)
								mdio_bus_match(dev, drv)
									phy_bus_match(dev, drv)
										 /* 按 phy_id & phy_id_mask 匹配 */
										return (phydrv->phy_id & phydrv->phy_id_mask) == (phydev->phy_id & phydrv->phy_id_mask);
							/* 匹配到设备和驱动,加载驱动 */
							driver_probe_device(drv, dev)
								really_probe(dev, drv)
									dev->driver = drv; /* 绑定设备的驱动 */
									drv->probe(dev) = phy_probe()
/*
 * 所有 PHY 驱动的公共入口,做 PHY 公共初始化,然后再由它再调用
 * 具体 PHY 驱动的 probe (如果实现了的话)。 
 */
static int phy_probe(struct device *dev)
{
	struct phy_device *phydev = to_phy_device(dev);
	struct device_driver *drv = phydev->mdio.dev.driver;
	struct phy_driver *phydrv = to_phy_driver(drv);
	int err = 0;

	phydev->drv = phydrv; /* 绑定 phy_device 和 phy_driver */

	/* PHY 中断模式最终配置 */ 
	if (!(phydrv->flags & PHY_HAS_INTERRUPT) &&
	    phy_interrupt_is_valid(phydev))
		phydev->irq = PHY_POLL;  /* 除非后面 PHY 驱动的 probe 调用改变 irq, 否则都会是 PHY_POLL 模式 */

	...

	/* PHY 功能特性配置 */ 
	phydev->supported = phydrv->features;
	of_set_phy_supported(phydev);
	phydev->advertising = phydev->supported;

	...

	/* Set the state to READY by default */
	phydev->state = PHY_READY; /* 标记 PHY 设备已经就绪 */

	if (phydev->drv->probe)
		err = phydev->drv->probe(phydev); /* PHY 驱动的 probe */

	return err;
}

到此,以太网 PHY 设备的驱动也已经加载,看起来似乎一切都已经结束了,是这样吗?事实上,一切才刚开始。因为我们还差一步,就是在软件层面绑定 MAC 和 PHY ,毕竟,只有它们在一起了,才组成一张完整的以太网卡,结局才算圆满。

4.3 绑定以太网卡的 MAC 和 PHY

在用户打开网卡是,将触发网卡设备对象的 net_device::netdev_ops->open 接口,即我们例子中的 stmmac_open()

static int stmmac_open(struct net_device *dev)
{
	if (priv->hw->pcs != STMMAC_PCS_RGMII &&
	    priv->hw->pcs != STMMAC_PCS_TBI &&
	    priv->hw->pcs != STMMAC_PCS_RTBI) {
	    /* 绑定 MAC 和 PHY */
	    ret = stmmac_init_phy(dev);
	    ...
	}
	
	...

	/* 启动 MAC 连接的 PHY */
	if (dev->phydev)
		phy_start(dev->phydev);

	/* 网卡数据处理中断 */
	ret = request_irq(dev->irq, stmmac_interrupt,
			  			IRQF_SHARED, dev->name, dev);
	
	...

	/* 启动 MAC 收发 */
	stmmac_enable_all_queues(priv);
	stmmac_start_all_queues(priv);

	return 0;
}

static int stmmac_init_phy(struct net_device *dev)
{
	struct phy_device *phydev;

	phydev = of_phy_connect(dev, priv->plat->phy_node,
					&stmmac_adjust_link, 0, interface);
	
	...

	/* 
	 * 内核日志输出如下格式信息: 
	 * "attached PHY driver [%s] (mii_bus:phy_addr=%s, irq=%s)"
	 */
	phy_attached_info(phydev);
	return 0;
}

struct phy_device *of_phy_connect(struct net_device *dev,
				  struct device_node *phy_np,
				  void (*hndlr)(struct net_device *), u32 flags,
				  phy_interface_t iface
{
	struct phy_device *phy = of_phy_find_device(phy_np);
	int ret;

	phy->dev_flags = flags;
	
	ret = phy_connect_direct(dev, phy, hndlr, iface);

	put_device(&phy->mdio.dev);
	
	return ret ? NULL : phy;
}

int phy_connect_direct(struct net_device *dev, struct phy_device *phydev,
		       void (*handler)(struct net_device *),
		       phy_interface_t interface)
{
	int rc;

	rc = phy_attach_direct(dev, phydev, phydev->dev_flags, interface);
	if (rc)
		return rc;

	/* 设定状态改变 MAC 层驱动回调改变 Link 状态, 显示 Link is Down/Up 信息等 */
	phy_prepare_link(phydev, handler); 
	/* 启动 PHY 状态机 work , 此时 PHY 才真正工作起来: phy_state_machine() */
	phy_start_machine(phydev);
	if (phydev->irq > 0) /* 没见过从中断方式工作的 PHY,都是 work 方式 */
		phy_start_interrupts(phydev);

	return 0;
}

int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
		      u32 flags, phy_interface_t interface)
{
	int err;
	
	...

	phydev->phy_link_change = phy_link_change;
	phydev->attached_dev = dev; /* 关联 net_device (MAC层) 和 phy_device (物理层) */
	dev->phydev = phydev;
	
	...
	
	phydev->dev_flags = flags;

	phydev->interface = interface; /* 设定 PHY 和 MAC 数据通信接口类型: MII, RMII, ... */

	phydev->state = PHY_READY;

	netif_carrier_off(phydev->attached_dev);

	err = phy_init_hw(phydev);

	phy_resume(phydev); /* 重启 PHY 设备 */
	phy_led_triggers_register(phydev);

	return err;
}

static void phy_prepare_link(struct phy_device *phydev,
			     void (*handler)(struct net_device *))
{
	phydev->adjust_link = handler;
}

/* 将 PHY 状态机 work 加入到 workqueue */
void phy_start_machine(struct phy_device *phydev)
{
	/* phydev->state_queue == phy_state_machine() */
	queue_delayed_work(system_power_efficient_wq, &phydev->state_queue, HZ);
}

/* 重启 PHY 状态机 work */
void phy_start(struct phy_device *phydev)
{
	switch (phydev->state) {
	...
	case PHY_READY:
		phydev->state = PHY_UP; /* 状态更新: PHY_READY -> PHY_UP */
		break;
	...
	}

	phy_trigger_machine(phydev, true);
}

void phy_trigger_machine(struct phy_device *phydev, bool sync)
{
	if (sync)
		cancel_delayed_work_sync(&phydev->state_queue);
	else
		cancel_delayed_work(&phydev->state_queue);
	/* 无延时的重启 PHY 状态机 work */
	queue_delayed_work(system_power_efficient_wq, &phydev->state_queue, 0);
}

从此, PHY 和 MAC 一起愉快的玩耍了。PHY 工作在其状态机 work 函数 phy_state_machine() 中。

4.4 以太网卡 PHY 和 MAC 的协同工作

以太网卡 PHY 管理了连接状态、和对端通信速度的自动协商等工作,作为一个网卡内外部沟通的桥梁,我们来看 PHY 是如何工作的。现在我们知道 PHY 工作在状态机 work 函数 phy_state_machine() 中:

void phy_state_machine(struct work_struct *work)
{
	struct delayed_work *dwork = to_delayed_work(work);
	struct phy_device *phydev =
			container_of(dwork, struct phy_device, state_queue);
	bool needs_aneg = false, do_suspend = false;
	enum phy_state old_state;
	int err = 0;
	int old_link;

	mutex_lock(&phydev->lock);

	old_state = phydev->state;

	/* PHY 驱动检测连接状态变化 */
	if (phydev->drv && phydev->drv->link_change_notify)
		phydev->drv->link_change_notify(phydev);

	/* PHY 工作状态机 */
	switch (phydev->state) {
	case PHY_DOWN:
	case PHY_STARTING:
	case PHY_READY:
	case PHY_PENDING:
		break;
	case PHY_UP:
		needs_aneg = true;

		phydev->link_timeout = PHY_AN_TIMEOUT;

		break;
	case PHY_AN:
		err = phy_read_status(phydev);
		if (err < 0)
			break;

		/* If the link is down, give up on negotiation for now */
		if (!phydev->link) {
			phydev->state = PHY_NOLINK;
			phy_link_down(phydev, true);
			break;
		}

		/* Check if negotiation is done.  Break if there's an error */
		err = phy_aneg_done(phydev);
		if (err < 0)
			break;

		/* If AN is done, we're running */
		if (err > 0) {
			phydev->state = PHY_RUNNING;
			phy_link_up(phydev);
		} else if (0 == phydev->link_timeout--)
			needs_aneg = true;
		break;
	case PHY_NOLINK:
		if (phy_interrupt_is_valid(phydev))
			break;

		err = phy_read_status(phydev);
		if (err)
			break;

		if (phydev->link) {
			if (AUTONEG_ENABLE == phydev->autoneg) {
				err = phy_aneg_done(phydev);
				if (err < 0)
					break;

				if (!err) {
					phydev->state = PHY_AN;
					phydev->link_timeout = PHY_AN_TIMEOUT;
					break;
				}
			}
			phydev->state = PHY_RUNNING;
			phy_link_up(phydev);
		}
		break;
	case PHY_FORCING:
		err = genphy_update_link(phydev);
		if (err)
			break;

		if (phydev->link) {
			phydev->state = PHY_RUNNING;
			phy_link_up(phydev);
		} else {
			if (0 == phydev->link_timeout--)
				needs_aneg = true;
			phy_link_down(phydev, false);
		}
		break;
	case PHY_RUNNING:
		/* Only register a CHANGE if we are polling and link changed
		 * since latest checking.
		 */
		if (phydev->irq == PHY_POLL) {
			old_link = phydev->link;
			err = phy_read_status(phydev);
			if (err)
				break;

			if (old_link != phydev->link)
				phydev->state = PHY_CHANGELINK;
		}
		/*
		 * Failsafe: check that nobody set phydev->link=0 between two
		 * poll cycles, otherwise we won't leave RUNNING state as long
		 * as link remains down.
		 */
		if (!phydev->link && phydev->state == PHY_RUNNING) {
			phydev->state = PHY_CHANGELINK;
			phydev_err(phydev, "no link in PHY_RUNNING\n");
		}
		break;
	case PHY_CHANGELINK:
		err = phy_read_status(phydev);
		if (err)
			break;

		if (phydev->link) {
			phydev->state = PHY_RUNNING;
			phy_link_up(phydev);
		} else {
			phydev->state = PHY_NOLINK;
			phy_link_down(phydev, true);
		}

		if (phy_interrupt_is_valid(phydev))
			err = phy_config_interrupt(phydev,
						   PHY_INTERRUPT_ENABLED);
		break;
	case PHY_HALTED:
		if (phydev->link) {
			phydev->link = 0;
			phy_link_down(phydev, true);
			do_suspend = true;
		}
		break;
	case PHY_RESUMING:
		if (AUTONEG_ENABLE == phydev->autoneg) {
			err = phy_aneg_done(phydev);
			if (err < 0)
				break;

			/* err > 0 if AN is done.
			 * Otherwise, it's 0, and we're  still waiting for AN
			 */
			if (err > 0) {
				err = phy_read_status(phydev);
				if (err)
					break;

				if (phydev->link) {
					phydev->state = PHY_RUNNING;
					phy_link_up(phydev);
				} else	{
					phydev->state = PHY_NOLINK;
					phy_link_down(phydev, false);
				}
			} else {
				phydev->state = PHY_AN;
				phydev->link_timeout = PHY_AN_TIMEOUT;
			}
		} else {
			err = phy_read_status(phydev);
			if (err)
				break;

			if (phydev->link) {
				phydev->state = PHY_RUNNING;
				phy_link_up(phydev);
			} else	{
				phydev->state = PHY_NOLINK;
				phy_link_down(phydev, false);
			}
		}
		break;
	}

	mutex_unlock(&phydev->lock);

	if (needs_aneg) /* 需要自动协商 */
		err = phy_start_aneg_priv(phydev, false); /* 自动协商处理 */
	else if (do_suspend)
		phy_suspend(phydev);

	if (err < 0)
		phy_error(phydev);

	if (old_state != phydev->state)
		phydev_dbg(phydev, "PHY state change %s -> %s\n",
			   phy_state_to_str(old_state),
			   phy_state_to_str(phydev->state));

	/* Only re-schedule a PHY state machine change if we are polling the
	 * PHY, if PHY_IGNORE_INTERRUPT is set, then we will be moving
	 * between states from phy_mac_interrupt()
	 */
	if (phydev->irq == PHY_POLL)
		queue_delayed_work(system_power_efficient_wq, &phydev->state_queue,
				   PHY_STATE_TIME * HZ); /* 重启 PHY 状态机 work */
}

PHY 要处理的事务较多,这里不一一列举,仅就连接状态变化来做一下分析说明:

static void phy_link_up(struct phy_device *phydev)
{
	phydev->phy_link_change(phydev, true, true); /* phy_link_change() */
	phy_led_trigger_change_speed(phydev);
}

static void phy_link_change(struct phy_device *phydev, bool up, bool do_carrier)
{
	struct net_device *netdev = phydev->attached_dev;

	if (do_carrier) {
		if (up)
			netif_carrier_on(netdev);
		else
			netif_carrier_off(netdev);
	}
	/* 通过 MAC 驱动注册的连接状态监控接口,告知其连接的变化 */
	phydev->adjust_link(netdev); /* stmmac_adjust_link() */
}

/* MAC 驱动连接状态变化时的处理接口 */
static void stmmac_adjust_link(struct net_device *dev)
{
	/* MAC 驱动针对连接状态变化的处理 ... */

	/* 
	 * 我们在内核日志最熟悉的连接状态变化日志来了。
	 * 连接拉起时:Link is Up 1000 Mbps Full Duplex, Flow Control: None
	 * 连接掉线时:Link is Down
	 */
	if (new_state && netif_msg_link(priv))
		phy_print_status(phydev);
		
	...
}

/* drivers/net/phy/phy.c */
void phy_print_status(struct phy_device *phydev)
{
	if (phydev->link) {
		netdev_info(phydev->attached_dev,
			"Link is Up - %s/%s - flow control %s\n",
			phy_speed_to_str(phydev->speed),
			phy_duplex_to_str(phydev->duplex),
			phydev->pause ? "rx/tx" : "off");
	} else	{
		netdev_info(phydev->attached_dev, "Link is Down\n");
	}
}

4.5 以太网卡 PHY 驱动示范

写一个 以太网卡 PHY 驱动很简单,驱动框架如下:

static struct phy_driver xxx_phy_drvs[] = {
	/* 根据 PHY 芯片情形实现需要的接口 */
	{
		.phy_id         = 0x00008201,
		.name           = "RTL8201CP Ethernet",
		.phy_id_mask    = 0x0000ffff,
		.features       = PHY_BASIC_FEATURES,
		.flags          = PHY_HAS_INTERRUPT,
		.config_aneg    = &genphy_config_aneg,
		.read_status    = &genphy_read_status,
		......
	}, 
};

module_phy_driver(xxx_phy_drvs);

static struct mdio_device_id __maybe_unused xxx_phy_id_tbl[] = {
	{ 0x001cc916, 0x001fffff }, 
	{ }
};

MODULE_DEVICE_TABLE(mdio, xxx_phy_id_tbl);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/1126.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2022-2023 年度广东省职业院校学生专业技能大赛中职组“网络安全”赛项竞赛任务书(样题)

2022-2023 年度广东省职业院校学生专业技能大赛中职组“网络安全”赛项竞赛任务书&#xff08;样题&#xff09; 一、竞赛时间 总计&#xff1a;210 分钟 二、竞赛阶段 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 A 模块 A-1 登录安全加固 90 分钟 200…

重构·改善既有代码的设计.03之重构手法(上)

1. 前言 之前的重构系列中&#xff0c;介绍了书中提到的重构基础&#xff0c;以及识别代码的坏味道。今天继续第三更&#xff0c;讲述那些重构手法&#xff08;上&#xff09;。看看哪些手法对你的项目能有所帮助… 2. 重新组织函数 对函数进行整理&#xff0c;使之更恰当的…

51单片机入门 -驱动 8x8 LED 点阵屏

硬件型号、软件版本、以及烧录流程 操作系统&#xff1a;Windows 10 x84-64单片机&#xff1a;STC89C52RC编译器&#xff1a;SDCC烧录软件&#xff1a;stcgal 1.6开发板&#xff1a;普中51单片机开发板A2套件&#xff08;2022&#xff09; 在 VS Code 中新建项目到烧录的过程…

[ROC-RK3568-PC] [Firefly-Android] 10min带你了解I2C的使用

&#x1f347; 博主主页&#xff1a; 【Systemcall小酒屋】&#x1f347; 博主追寻&#xff1a;热衷于用简单的案例讲述复杂的技术&#xff0c;“假传万卷书&#xff0c;真传一案例”&#xff0c;这是林群院士说过的一句话&#xff0c;另外“成就是最好的老师”&#xff0c;技术…

5.springcloud微服务架构搭建 之 《springboot集成Hystrix》

1.springcloud微服务架构搭建 之 《springboot自动装配Redis》 2.springcloud微服务架构搭建 之 《springboot集成nacos注册中心》 3.springcloud微服务架构搭建 之 《springboot自动装配ribbon》 4.springcloud微服务架构搭建 之 《springboot集成openFeign》 目录 1.项目…

C语言刷题(7)(字符串旋转问题)——“C”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰的内容依旧是复习之前的知识点&#xff0c;那么&#xff0c;就是做一道小小的题目啦&#xff0c;下面&#xff0c;让我们进入C语言的世界吧 实现一个函数&#xff0c;可以左旋字符串中的k个字符。 例如&#xff1a; A…

2023最新性能测试八股文【附答案】,软测人必备!

1. 请描述什么是性能测试、什么是负载测试、什么是压力测试&#xff1f;【参考答案】性能测试&#xff1a;性能测试是和功能测试相对应的。根据用户场景进行的单个用户操作&#xff0c;是属于功能测试领域&#xff0c;主要是验证软件是否可以满足用户的功能需求。比如&#xff…

【刷题之路Ⅱ】LeetCode 11.盛水最多的容器

【刷题之路Ⅱ】LeetCode 11.盛水最多的容器一、题目描述二、解题1、方法1——暴力法1.1、思路分析1.2、代码实现2、方法2——双指针2.1、思路分析2.2、代码实现一、题目描述 原题连接&#xff1a; 11.盛水最多的容器 题目描述&#xff1a; 给定一个长度为 n 的整数数组 height…

44岁了,我从没想过在CSDN创作2年,会有这么大收获

1998年上的大学&#xff0c;02年毕业&#xff0c;就算从工作算起&#xff0c;我也有20余年的码龄生涯了。 但正式开启博文的写作&#xff0c;却是2021年开始的&#xff0c;差不多也就写了2年的博客&#xff0c;今天我来说说我在CSDN的感受和收获。 我是真的没想到&#xff0c;…

QT串口助手开发3串口开发

系列文章目录 QT串口助手开发3串口开发 QT串口助手开发3系列文章目录一、UI界面程序的编写二、发送框程序编写一、UI界面程序的编写 根据上文的未解决问题&#xff1a;我们打开串口按钮打开后只能选择关闭串口&#xff0c;所以这个是循环的过程 上文链接 所以按钮对应的槽函数…

【C++】搜索二叉树(保姆教程)

&#x1f345;二叉树底层是堆&#xff0c;之前学习的简单二叉树不是大堆就是小堆&#xff0c;今天是二叉树进阶&#xff0c;一定要好好掌握&#xff01; 目录 ☃️1.搜索二叉树介绍 ☃️2.底层实现 ☃️3.key模型和key&#xff0c;value模型 ☃️1.搜索二叉树介绍 右>根&…

蜻蜓优化算法Python代码(详细注释)

1.代入例子&#xff0c;目标函数求最优解迭代过程&#xff1a;蜻蜓算法流程&#xff1a;蜻蜓算法&#xff08;Dragonfly Algorithm&#xff09;是一种基于种群的优化算法&#xff0c;灵感来自于蜻蜓的群集行为。该算法通过模拟蜻蜓之间的吸引力和斥力&#xff0c;以及蜻蜓的移动…

IDEA好用插件:MybatisX快速生成接口实体类mapper.xml映射文件

目录 1、在Idea中找到下载插件&#xff0c;Install&#xff0c;重启Idea 2、一个测试java文件&#xff0c;里面有com包 3、在Idea中添加数据库 --------以Oracle数据库为例 4、快速生成entity-service-mapper方法 5、查看生成的代码 6、自动生成&#xff08;增删查改&#xff0…

C++ | 对比inline内联函数和宏的不同点

文章目录一、前言二、宏的优缺点分析1、概念回顾2、宏的缺点3、宏的优点三、inline内联函数1、概念2、特性①&#xff1a;空间换时间&#x1f381;趣味杂谈&#xff1a;庞大的游戏更新包3、特性②&#xff1a;inline实现机制4、特性③&#xff1a;inline的声明与定义反汇编观察…

【链表OJ题(八)】相交链表

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;数据结构 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录链表OJ题(八)8. 相交…

【C++笔试强训】第三十二天

&#x1f387;C笔试强训 博客主页&#xff1a;一起去看日落吗分享博主的C刷题日常&#xff0c;大家一起学习博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a;夜色难免微凉&#xff0c;前方必有曙光 &#x1f31e;。 &#x1f4a6;&a…

Spring Bean实例化和初始化的过程

承接上文Spring Bean生命周期应用程序在运行过程中能否去读取当前系统的环境变量或系统属性?这里涉及到一个非常重要的接口Environment&#xff0c;System.getenv&#xff0c;System.getProperties都是获取当前系统环境变量&#xff0c;Environment接口的实现类AbstractEnviro…

浏览器前进与后退的秘密——栈 (栈的理解与实现)

文章目录前言&#xff1a;浏览器与栈的纠缠如何理解“栈”&#xff1f;如何实现一个“栈”&#xff1f;基于数组的顺序栈基于链表的链式栈解答开篇&#x1f431;‍&#x1f409;作者简介&#xff1a;大家好&#xff0c;我是黑洞晓威&#xff0c;一名大二学生&#xff0c;希望和…

每日算法题

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 作为篮球队教练&#xff0c;你需要从以下名单中选出 11 号位至 55 号位各一名球员&#xff0c;组成球队的首发阵容。 每位球员担任 11 号位至 55 号位时的评分如下…

CCF-CSP真题《202212-3 JPEG 解码》思路+python,c++满分题解

想查看其他题的真题及题解的同学可以前往查看&#xff1a;CCF-CSP真题附题解大全 试题编号&#xff1a;202212-3试题名称&#xff1a;JPEG 解码时间限制&#xff1a;1.0s内存限制&#xff1a;512.0MB问题描述&#xff1a; 问题背景 四年一度的世界杯即将画上尾声。在本…
最新文章