1. 简介

在调试网口驱动的过程中发现 phy 芯片的驱动框架结构还有点复杂,不仔细研究的话还不好搞懂,另外百度到的资料也不够全面,这篇就总结梳理一下这方面的知识。

我们知道一个 phy 驱动的原理是非常简单的,一般流程如下:

  • 1、用轮询/中断的方式通过 mdio 总线读取 phy 芯片的状态。
  • 2、在 phy link 状态变化的情况下,正确配置 mac 的状态。(例如:根据 phy 自协商的速率 10/100/1000M 把 mac 配置成对应速率)

下面就以 stmmac 网口驱动为例,展示一下 phy 驱动整个调用过程。整个 phy 驱动的主要调用流程如下图所示:

在这里插入图片描述

2. phy_device

首先每个 phy 芯片会创建一个 struct phy_device 类型的设备,对应的有 struct phy_driver 类型的驱动,这两者实际上是挂载在 mdio_bus_type 总线上的。

2.1 mdio bus

mdio 总线的定义:

struct bus_type mdio_bus_type = {
	.name		= "mdio_bus",
	.dev_groups	= mdio_bus_dev_groups,
	.match		= mdio_bus_match,
	.uevent		= mdio_uevent,
};

2.2 mdio device

网口驱动在初始化 probe() 时遍历 dts 的定义创建相应struct phy_device 类型的设备:

stmmac_dvr_probe()
`-| stmmac_mdio_register()
  `-| stmmac_mdio_register()
     `-| {
       |  new_bus = mdiobus_alloc();
       |  new_bus->read = &stmmac_xgmac2_mdio_read;  // mdio 读写函数
       |  new_bus->write = &stmmac_xgmac2_mdio_write;
       | 
       |  of_mdiobus_register(new_bus, mdio_node);
       `-| of_mdiobus_register_phy(mdio, child, addr);
          `-| get_phy_device()
             `-| get_phy_c22_id(bus, addr, &phy_id);
                `-| {
                  |  phy_reg = mdiobus_read(bus, addr, MII_PHYSID1); // 通过 mdio 总线读取 phy 芯片 id
                  |  phy_reg = mdiobus_read(bus, addr, MII_PHYSID2);
                  | }
               | phy_device_create(bus, addr, phy_id, is_c45, &c45_ids);
                `-| {
                  |  mdiodev->dev.bus = &mdio_bus_type;
                  |  mdiodev->dev.type = &mdio_bus_phy_type;
                  |  mdiodev->bus_match = phy_bus_match;
                  |  INIT_DELAYED_WORK(&dev->state_queue, phy_state_machine); // 这里就是 phy device 的轮询任务
                  | }
            | of_mdiobus_phy_device_register()
             `-| phy_device_register()
                `-| device_add()

2.3 mdio driver

mdio bus 会根据 struct phy_device 的 phy id 和 struct phy_driver 进行 match,如果没有找到对应驱动会使用通用驱动 genphy_driver

static struct phy_driver genphy_driver = {
	.phy_id		= 0xffffffff,
	.phy_id_mask	= 0xffffffff,
	.name		= "Generic PHY",
	.get_features	= genphy_read_abilities,
	.suspend	= genphy_suspend,
	.resume		= genphy_resume,
	.set_loopback   = genphy_loopback,
};

genphy_driver 为例 struct phy_device 的注册过程如下:

phy_init()
`-| phy_driver_register(&genphy_driver, THIS_MODULE);
   `-| {
     |  new_driver->mdiodrv.driver.bus = &mdio_bus_type;
     |  new_driver->mdiodrv.driver.probe = phy_probe;
     |  new_driver->mdiodrv.driver.remove = phy_remove;
     | }
     | driver_register()

其中一个关键点是 mdio driver 的 probe 函数是一个通用函数 phy_probe(),match 成功时会调用它读取状态寄存器来确定 phy 芯片的能力:

phy_probe()
`-| genphy_read_abilities()
   `-| {
     |  val = phy_read(phydev, MII_BMSR); // 读取 mdio 0x01 寄存器来确定 phy 的 10/100M 能力
     |  linkmode_mod_bit(ETHTOOL_LINK_MODE_Autoneg_BIT, phydev->supported, val & BMSR_ANEGCAPABLE);
     |  linkmode_mod_bit(ETHTOOL_LINK_MODE_100baseT_Full_BIT, phydev->supported, val & BMSR_100FULL);
     |  linkmode_mod_bit(ETHTOOL_LINK_MODE_100baseT_Half_BIT, phydev->supported, val & BMSR_100HALF);
     |  linkmode_mod_bit(ETHTOOL_LINK_MODE_10baseT_Full_BIT, phydev->supported, val & BMSR_10FULL);
     |  linkmode_mod_bit(ETHTOOL_LINK_MODE_10baseT_Half_BIT, phydev->supported, val & BMSR_10HALF);
     |  if (val & BMSR_ESTATEN) {
     |   val = phy_read(phydev, MII_ESTATUS); // 读取 mdio 0x0f 寄存器来确定 phy 的 1000M 能力
     |   linkmode_mod_bit(ETHTOOL_LINK_MODE_1000baseT_Full_BIT, phydev->supported, val & ESTATUS_1000_TFULL);
     |   linkmode_mod_bit(ETHTOOL_LINK_MODE_1000baseT_Half_BIT, phydev->supported, val & ESTATUS_1000_THALF);
     |   linkmode_mod_bit(ETHTOOL_LINK_MODE_1000baseX_Full_BIT, phydev->supported, val & ESTATUS_1000_XFULL);
     |  }
     | }

2.4 poll task

上面 phy_device_create() 函数中创建了一个重要的 work phy_state_machine(),这个就是 phy_device 查询任务的主体,用来查询 phy 芯片的状态维护 phy 状态机。

在网口驱动启动时会启动这个 work:

net_device_ops->ndo_open()
`-| stmmac_open()
   `-| phylink_start()
      `-| phy_start()
         `-| phydev->state = PHY_UP;
           | phy_start_machine()
            `-| phy_trigger_machine()
               `-| phy_queue_state_machine(phydev, 0);
                  `-| mod_delayed_work(system_power_efficient_wq, &phydev->state_queue, jiffies);

phy_state_machine() 的核心逻辑如下所示:

void phy_state_machine(struct work_struct *work)
{

	old_state = phydev->state;

    /* (1) 状态机主体 */
	switch (phydev->state) {
    /* (1.1) 在 PHY_DOWN/PHY_READY 状态下不动作 */
	case PHY_DOWN:
	case PHY_READY:
		break;
    
    /* (1.2) 在 PHY_UP 状态下,表明网口被 up 起来,需要启动自协商并且查询自协商后的 link 状态
             如果自协商结果是 link up,进入 PHY_RUNNING 状态
             如果自协商结果是 link down,进入 PHY_NOLINK 状态
     */
	case PHY_UP:
		needs_aneg = true;

		break;

    /* (1.3) 在运行的过程中定时轮询 link 状态
             如果 link up,进入 PHY_RUNNING 状态
             如果 link down,进入 PHY_NOLINK 状态
     */
	case PHY_NOLINK:
	case PHY_RUNNING:
		err = phy_check_link_status(phydev);
		break;

	}

    /* (2) 如果需要,启动自协商配置 */
	if (needs_aneg)
		err = phy_start_aneg(phydev);

    /* (3) 如果 phy link 状态有变化,通知给对应网口 netdev */
	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));
		if (phydev->drv && phydev->drv->link_change_notify)
			phydev->drv->link_change_notify(phydev);
	}

    /* (4) 重新启动 work,周期为 1s */
	if (phy_polling_mode(phydev) && phy_is_started(phydev))
		phy_queue_state_machine(phydev, PHY_STATE_TIME);
}

标准的 mdio/mii 寄存器列表定义如下:

/* Generic MII registers. */
#define MII_BMCR		0x00	/* Basic mode control register */
#define MII_BMSR		0x01	/* Basic mode status register  */
#define MII_PHYSID1		0x02	/* PHYS ID 1                   */
#define MII_PHYSID2		0x03	/* PHYS ID 2                   */
#define MII_ADVERTISE		0x04	/* Advertisement control reg   */
#define MII_LPA			0x05	/* Link partner ability reg    */
#define MII_EXPANSION		0x06	/* Expansion register          */
#define MII_CTRL1000		0x09	/* 1000BASE-T control          */
#define MII_STAT1000		0x0a	/* 1000BASE-T status           */
#define	MII_MMD_CTRL		0x0d	/* MMD Access Control Register */
#define	MII_MMD_DATA		0x0e	/* MMD Access Data Register */
#define MII_ESTATUS		0x0f	/* Extended Status             */
#define MII_DCOUNTER		0x12	/* Disconnect counter          */
#define MII_FCSCOUNTER		0x13	/* False carrier counter       */
#define MII_NWAYTEST		0x14	/* N-way auto-neg test reg     */
#define MII_RERRCOUNTER		0x15	/* Receive error counter       */
#define MII_SREVISION		0x16	/* Silicon revision            */
#define MII_RESV1		0x17	/* Reserved...                 */
#define MII_LBRERROR		0x18	/* Lpback, rx, bypass error    */
#define MII_PHYADDR		0x19	/* PHY address                 */
#define MII_RESV2		0x1a	/* Reserved...                 */
#define MII_TPISTATUS		0x1b	/* TPI status for 10mbps       */
#define MII_NCONFIG		0x1c	/* Network interface config    */

2.4.1 自协商配置

具体启动 phy 自协商的代码流程如下:

phy_state_machine()
`-| phy_start_aneg()
   `-| phy_config_aneg()
      `-| genphy_config_aneg()
         `-| __genphy_config_aneg()
            `-| genphy_setup_master_slave() // (1) 如果是千兆网口,配置其 master/slave
               `-| {
                 |  phy_modify_changed(phydev, MII_CTRL1000,    // 配置 mdio 0x09 寄存器
                 |     (CTL1000_ENABLE_MASTER | CTL1000_AS_MASTER | CTL1000_PREFER_MASTER), ctl);
                 | }
              | genphy_config_advert() // (2) 设置本端的 advert 能力
               `-| {
                 |  linkmode_and(phydev->advertising, phydev->advertising, phydev->supported);
                 |  adv = linkmode_adv_to_mii_adv_t(phydev->advertising);
                 |  phy_modify_changed(phydev, MII_ADVERTISE,   // 10M/100M 能力配置到 mdio 0x04 寄存器
                 |       ADVERTISE_ALL | ADVERTISE_100BASE4 |
                 |       ADVERTISE_PAUSE_CAP | ADVERTISE_PAUSE_ASYM, adv);
                 |  if (!(bmsr & BMSR_ESTATEN)) return changed;
                 |  adv = linkmode_adv_to_mii_ctrl1000_t(phydev->advertising);
                 |  phy_modify_changed(phydev, MII_CTRL1000,    // 1000M 能力配置到 mdio 0x09 寄存器
                 |       ADVERTISE_1000FULL | ADVERTISE_1000HALF, adv);
                 | }
              | genphy_check_and_restart_aneg()
               `-| genphy_restart_aneg() // (3) 启动 phy 自协商
                  `-| {
                    |  phy_modify(phydev, MII_BMCR, BMCR_ISOLATE,   // 配置 mdio 0x00 寄存器
                    |       BMCR_ANENABLE | BMCR_ANRESTART);
                    | }

2.4.2 link 状态读取

phy link 状态读取的代码流程如下:

phy_state_machine()
`-| phy_check_link_status()
   `-| phy_read_status()    // (1) 读取 link 状态 
      `-| genphy_read_status()
         `-| {
           |  genphy_update_link(phydev);   // (1.1) 更新 link 状态
           |  if (phydev->autoneg == AUTONEG_ENABLE && old_link && phydev->link) return 0;
           |  genphy_read_master_slave(phydev); // (1.2) 如果是千兆网口,更新本端和对端的 master/slave
           |  genphy_read_lpa(phydev);  // (1.3) 更新对端(link partner) 声明的能力
           |  if (phydev->autoneg == AUTONEG_ENABLE && phydev->autoneg_complete) {
           |    phy_resolve_aneg_linkmode(phydev);  // (1.4.1) 自协商模式,解析 link 结果
           |  } else if (phydev->autoneg == AUTONEG_DISABLE) {
           |    genphy_read_status_fixed(phydev); // (1.4.2) 固定模式,解析 link 结果
           |  }
           | }
     | if (phydev->link && phydev->state != PHY_RUNNING) {  // (2) link 状态 change 事件:变成 link up
     |   phydev->state = PHY_RUNNING;
     |   phy_link_up(phydev);   // link up 事件,通知给 phylink
     | } else if (!phydev->link && phydev->state != PHY_NOLINK) {  // (3) link 状态 change 事件:变成 link down
     |   phydev->state = PHY_NOLINK;
     |   phy_link_down(phydev); // link down 事件,通知给 phylink
     | }

2.4.3 link 状态通知

phy 的 link 状态变化怎么通知给 netdev,并且让 mac 做出相应的配置改变,这个是通过一个中介 phylink 来实现的。

phy_device 把 link 状态通知给 phylink 的流程如下:

phy_link_up()/phy_link_down()
`-| phydev->phy_link_change(phydev, true/false);
   `-| phylink_phy_change()
      `-| {
        |  pl->phy_state.speed = phydev->speed;     // (1) 把 `phy_device`  状态更新给 `phylink`
        |  pl->phy_state.duplex = phydev->duplex;
        |  pl->phy_state.interface = phydev->interface;
        |  pl->phy_state.link = up;
        |  phylink_run_resolve(pl);     // (2) 通知 `phylink` 的轮询任务启动
        | }

3. phylink

在 linux 内核中,以太网 mac 会被注册成 struct net_device,phy 芯片会被注册成 struct phy_devicephy_device 的状态怎么传递给 net_device,让其在 link 状态变化时做出对应的配置改变,这个任务就落在上述的 struct phylink 中介身上。

因为 phylink 只是一个中介,所以它不会创建对应的 device,它的核心在以下的几个函数当中。

3.1 phylink_create()

网口驱动在初始化 probe() 中创建 struct net_device 的同时也创建了 struct phylink

stmmac_dvr_probe()
`-| stmmac_phy_setup()
  `-| phylink_create(&priv->phylink_config, fwnode, mode, &stmmac_phylink_mac_ops);
     `-| {
       |  INIT_WORK(&pl->resolve, phylink_resolve);     // phylink 的轮询任务
       |  pl->mac_ops = mac_ops;
       |  timer_setup(&pl->link_poll, phylink_fixed_poll, 0);
       | }

这里的 stmmac_phylink_mac_ops 就包含了 mac 对 phy link 状态变化相应的相关函数:

static const struct phylink_mac_ops stmmac_phylink_mac_ops = {
	.validate = stmmac_validate,
	.mac_pcs_get_state = stmmac_mac_pcs_get_state,
	.mac_config = stmmac_mac_config,
	.mac_an_restart = stmmac_mac_an_restart,
	.mac_link_down = stmmac_mac_link_down,
	.mac_link_up = stmmac_mac_link_up,
};

3.2 phylink_connect_phy()

在网口驱动启动时会启动时,会连接 phylinkphy_device

net_device_ops->ndo_open()
`-| stmmac_open()
   `-| stmmac_init_phy()
      `-| phylink_of_phy_connect()
         `-| phy_attach_direct()
           | phylink_bringup_phy()
            `-| {
              |  phy->phylink = pl;
              |  phy->phy_link_change = phylink_phy_change; // (1) 设置 phy_device 的通知函数
              |  timer_setup(&pl->link_poll, phylink_fixed_poll, 0);
              | }

3.3 phylink_start()

在连接完成后,会同时启动 phylinkphy_device 的轮询任务:

net_device_ops->ndo_open()
`-| stmmac_open()
   `-| phylink_start()
      `-| mod_timer(&pl->link_poll, jiffies + HZ);  // (1) 启动 `phylink` 的轮询任务
         `-| phylink_fixed_poll()
            `-| phylink_run_resolve()
               `-| queue_work(system_power_efficient_wq, &pl->resolve);
        | phy_start()   // (2) 启动 `phy_device` 的轮询任务
         `-| phydev->state = PHY_UP;
           | phy_start_machine()
            `-| phy_trigger_machine()
               `-| phy_queue_state_machine(phydev, 0);
                  `-| mod_delayed_work(system_power_efficient_wq, &phydev->state_queue, jiffies);

3.3 poll task

phylink 的轮询任务就是查询 phy_device 更新过来的 link 状态,调用 stmmac_phylink_mac_ops 相关函数来同步配置 mac:

phylink_resolve()
`-| {
  |  link_state = pl->phy_state;
  |  phylink_link_up()/phylink_link_down()
   `-| pl->mac_ops->mac_link_up()
      `-| stmmac_mac_link_up()  // (1) 配置 mac 为相应状态

参考资料

1.以太网PHY寄存器分析

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐