为什么要学习mdio子系统,因为一个新的硬件产品在开发之初,总是遇到这样或那样的问题,这其中包括网卡的问题。大部分soc内部有mac控制器,需要和外部phy芯片建立关联。mdio就是ethnet管理phy寄存器的桥梁。了解这些驱动的核心流程,才能尽快定位和调试软硬件问题。

参考内核源码版本:linux-5.3.4

参考linux设备驱动模型:

1
https://www.chenxd.xyz/2020/02/29/Linux%E5%86%85%E6%A0%B8%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8%E6%A8%A1%E5%9E%8B/

mdio子系统

mdio设备驱动模型

mdio子系统继承了linux设备驱动模型,在mdio子系统初始化的过程中向内核设备驱动模型注册了mdio总线,mdio总线用来关联mdio设备和驱动。

mdio子系统的初始化在内核源码drivers/net/phy/phy_device.c中。

1
subsys_initcall(phy_init);

初始化流程:首先注册了mdio总线类型,然后向mdio总线上注册了两个mdio驱动genphy_c45_driver和genphy_driver,mdio总线上的驱动即为phy_driver。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int __init phy_init(void)
{
int rc;

rc = mdio_bus_init();
if (rc)
return rc;

features_init();

rc = phy_driver_register(&genphy_c45_driver, THIS_MODULE);
if (rc)
goto err_c45;

rc = phy_driver_register(&genphy_driver, THIS_MODULE);
if (rc) {
phy_driver_unregister(&genphy_c45_driver);
err_c45:
mdio_bus_exit();
}

return rc;
}

这里注册两个通用phy驱动,客户还可以向总线注册其它phy驱动,当总线上注册mdio设备的时候,将会由总线核心通过总线的match方法来判断和设备兼容的驱动,并建立绑定关系。

mdio bus type

mdio总线是内核bus_type类型的数据结构,其名字为mdio_bus。mdio总线在内核中的定义如下:

1
2
3
4
5
struct bus_type mdio_bus_type = {
.name = "mdio_bus",
.match = mdio_bus_match,
.uevent = mdio_uevent,
};

在mdio子系统初始化的时候,调用mdio_bus_init()函数初始化mdio总线,在mdio_bus_init函数中执行bus_register()函数向核心注册了mdio_bus_type。该段代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int __init mdio_bus_init(void)
{
int ret;

ret = class_register(&mdio_bus_class);
if (!ret) {
ret = bus_register(&mdio_bus_type);
if (ret)
class_unregister(&mdio_bus_class);
}

return ret;
}

mdio总线match方法:

由mdio总线定义可知,mdio总线的match回调函数为mdio_bus_match,在设备驱动模型中,总线的match方法用来判断总线上的设备是否能被总线上的驱动处理。mdio的match方法首先通过of_driver_match_device()来判断,如果在of中指定了判断条件并且成立,则直接返回真。否则调用mdio_device的bus_match方法来判断。该部分代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
static int mdio_bus_match(struct device *dev, struct device_driver *drv)
{
struct mdio_device *mdio = to_mdio_device(dev);

if (of_driver_match_device(dev, drv))
return 1;

if (mdio->bus_match)
return mdio->bus_match(dev, drv);

return 0;
}

mdio driver

通用的mdio driver对象在内核中用mdio_driver结构表示,该结构在内核源码中的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* struct mdio_driver: Generic MDIO driver */
struct mdio_driver {
struct mdio_driver_common mdiodrv;

/*
* Called during discovery. Used to set
* up device-specific structures, if any
*/
int (*probe)(struct mdio_device *mdiodev);

/* Clears up any memory if needed */
void (*remove)(struct mdio_device *mdiodev);
};

从定义可以看到,mdio driver对象继承自mdio_driver_common对象,而mdio_driver_common继承了内核设备驱动模型的device_driver对象,从而实现了mdio子系统的设备驱动模型。

linux内核提供了mdio driver的注册和注销接口:

1
2
int mdio_driver_register(struct mdio_driver *drv);
void mdio_driver_unregister(struct mdio_driver *drv);

在mdio的设备驱动模型中mdio driver的核心是mdio_driver_common,其他对象也可以通过继承mdio_driver_common对象来实现特定的mdio driver,从而和mdio子系统建立关联。比如后面说明的phy driver对象。

mdio device

mdio device在内核中用mdio_device结构体表示,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct mdio_device {
struct device dev;

struct mii_bus *bus;
char modalias[MDIO_NAME_SIZE];

int (*bus_match)(struct device *dev, struct device_driver *drv);
void (*device_free)(struct mdio_device *mdiodev);
void (*device_remove)(struct mdio_device *mdiodev);

/* Bus address of the MDIO device (0-31) */
int addr;
int flags;
struct gpio_desc *reset_gpio;
struct reset_control *reset_ctrl;
unsigned int reset_assert_delay;
unsigned int reset_deassert_delay;
};

其中dev成员是为了继承device对象,从而实现mdio的设备驱动模型。bus成员用来提供访问phy寄存器的方法,mii_bus会在后面详细说明。bus_match方法在mdio总线的match方法中调用,用来和mdio驱动进行匹配。device_free和device_remove用来释放和移除mdio设备。addr表示phy在该总线上的地址。

linux内核提供了一个mdio_device的创建,释放,注册,移除和复位以及mdio总线匹配接口:

1
2
3
4
5
6
void mdio_device_free(struct mdio_device *mdiodev);
struct mdio_device *mdio_device_create(struct mii_bus *bus, int addr);
int mdio_device_register(struct mdio_device *mdiodev);
void mdio_device_remove(struct mdio_device *mdiodev);
void mdio_device_reset(struct mdio_device *mdiodev, int value);
int mdio_device_bus_match(struct device *dev, struct device_driver *drv);

当mdio_device注册到mdio设备驱动模型的时候,会和mdio总线上的mdio driver进行匹配和关联。

mdio class

当mdio总线初始化的时候,在注册mdio总线之前向核心注册了mdio总线的class,mdio总线的class在内核中定义如下:

1
2
3
4
static struct class mdio_bus_class = {
.name = "mdio_bus",
.dev_release = mdiobus_release,
};

phy设备驱动

phy设备和驱动是mdio设备和驱动的容器。用来描述phy设备实例及其驱动。

phy driver

phy驱动在内核中用phy_driver结构定义。phy driver是mdio driver的容器。同时定义了很多可选的方法,这些方法较多,具体可参考内核源码中的include/linux/phy.h文件。

同通用的mdio_driver对象一样,phy驱动也继承自mdio_driver_common对象。即一个phy驱动关联一个mdio驱动。从而和mdio子系统关联。

在phy_driver中用phy_id成员来表示该driver对应的phy设备的id,新的phy_device加入mdio总线后,mdio会先读取该物理芯片的phyid寄存器值,然后和驱动的phy_id区域进行匹配,随之绑定phy_driver和phy_device。

内核中定义了phy driver的注册和注销接口:

1
2
3
4
5
void phy_driver_unregister(struct phy_driver *drv); 
void phy_drivers_unregister(struct phy_driver *drv, int n);
int phy_driver_register(struct phy_driver *new_driver, struct module *owner);
int phy_drivers_register(struct phy_driver *new_driver, int n,
struct module *owner);

通过阅读phy_driver_register函数可知,phy driver是将mdio_driver_common对象注册到了mdio的总线上。同时设置了driver的probe和remove分别为phy_probe和phy_remove。该部分代码片段如下:

1
2
3
new_driver->mdiodrv.driver.bus = &mdio_bus_type;
new_driver->mdiodrv.driver.probe = phy_probe;
new_driver->mdiodrv.driver.remove = phy_remove;

当新的phy device注册的时候,会注册其所包含的mdio device,内核设备驱动模型核心就会在mdio总线上为其找到合适的mdio驱动,从而使的mdio device的容器phy_device和mdio驱动的容器phy_driver之间建立关联。

当phy_driver和phy_device对象匹配后,就会调用到phy_probe函数,该函数主要工作是:

1.初始化phy device对象。

2.复位phy设备。

3.phy_probe函数是在mdio driver对象中设置的probe回调,在该回调函数中还会调用phy driver对象自己的probe函数。

4.设置phy模式(包括10/100/1000m等)。

5.初始化phy 对象的状态机为PHY_READY状态。

phy device

phy device是mdio device对象的容器。phy device对象继承了mdio device对象,用来描述一个phy设备。内核提供了如下接口用来创建,注册以及注销和销毁一个phy device对象:

1
2
3
4
struct phy_device *get_phy_device(struct mii_bus *bus, int addr, bool is_c45);
int phy_device_register(struct phy_device *phy);
void phy_device_remove(struct phy_device *phydev);
void phy_device_free(struct phy_device *phydev);

get_phy_device接口通过mii_bus和物理phy芯片通信获取phy id,并通过phy_device_create()接口创建一个phy_device对象。

当phy device对象被注册的时候,在phy_device_register()函数中注册其mdio device对象到mdio总线,内核设备驱动模型核心会在mdio总线上为其绑定满足条件的mdio driver的容器,即phy driver。并调用mdio driver的probe函数以初始化phy device对象。

phy状态机

phy device对象包含一个phy状态机,用来处理phy的状态变化。phy状态机由一个内核工作队列来维护,该工作队列在phy_device中由state_queue来表示,其状态由phy_device结构中的state来表示。

phy状态机在phy_device_create接口中初始化,其初始化状态为PHY_DOWN,在和phy驱动绑定的时候设置为PHY_READY。phy状态机初始化代码片段如下:

1
2
dev->state = PHY_DOWN;
INIT_DELAYED_WORK(&dev->state_queue, phy_state_machine);

phy状态机定义了如下几个状态:

1
2
3
4
5
6
7
8
enum phy_state {
PHY_DOWN = 0,
PHY_READY,
PHY_HALTED,
PHY_UP,
PHY_RUNNING,
PHY_NOLINK,
};

其中PHY_DOWN表示初始状态,在创建phy的时候设置该状态,并在phy_driver对象的mdio driver对象的probe中设置该对象为PHY_READY,probe应该并且只能在PHY_DOWN状态下调用。

PHY_READY表示就绪状态,此时phy已经准备好收发数据包。该状态由probe函数设置。

PHY_HALTED表示phy已经up,但是还未轮询或者中断。或者phy处于错误状态。可以由phy_start设置为PHY_UP状态。

PHY_UP表示phy和关联的设备已经就绪工作。由phy_start设置。在up状态下应该启动中断或定时器,由中断或定时器将phy状态根据phy的链接状态设置为running或者nolink。

PHY_RUNNING表示phy已经启动运行,可能正在收发数据包。在该状态下,由中断或定时器将phy状态设置为PHY_NOLINK,如果phy变成link down。可以由phy_stop设置为HALTED状态。

PHY_NOLINK表示phy已经启动,未插入网线。在该状态下由中断或定时器将phy状态设置为RUNNING,如果phy链接状态恢复正常的话。可以由phy_stop设置为HALTED状态。

phy状态机的工作流程如下:

在创建phy device对象的时候初始化为DOWN,在probe函数中设置为READY,调用phy_start设置为UP状态,在up状态中通过定时器根据phy的链接状态设置为running或者nolink,此后在running和nolink之间切换。调用phy_stop可以使其进入HALTED状态,当再次调用phy_start的时候将会再次进入up状态。

phy状态机函数为phy_state_machine()。

mdio bus

mdio bus是mdio管里phy寄存器的总线,此总线非设备驱动模型之总线。mdiobus代码在内核源码树中的位置为:drivers/net/phy/mdio_bus.c。mdiobus在内核中用mii_bus结构体描述,mii_bus定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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;
};

mdiobus结构中记录所有该mdiobus关联的mdio_device,即phy设备。同时提供了读/写/复位函数。mdiobus一般由ethernet驱动直接申请和注册。

mdiobus申请接口:mdiobus_alloc

mdiobus注册接口:__mdiobus_register

__mdiobus_register()接口主要完成以下工作:

1.初始化和注册mdiobus设备对象。

2.调用mdiobus_scan()函数创建phy_device,然后调用phy_device_register()向mdio设备驱动模型注册phy_device,并将其关联到mdiobus中。

mdio向外同时提供了mdiobus_read和mdiobus_write等读写函数。这些函数最终调用mii_bus对象中设置的read/write回调函数。

mdiobus_read和mdiobus_write等读写函数也可通过phy_read/phy_write等接口函数间接调用。

phy工作流程

phy核心的一般工作流程如下:

初始化mdio子系统

mdio子系统的初始化在mdio子系统章节已经说明。

注册phy driver

在mdio子系统初始化的时候通过phy_driver_register接口注册了两个通用的phy驱动。其他phy驱动可以通过module_phy_driver宏注册到mdio子系统核心,以dp83848phy驱动为例,其注册代码如下:

1
module_phy_driver(dp83848_driver);

module_phy_driver宏实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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))

由上面的宏结构可知,其原理是通过module_init定义一个模块初始化函数,在模块初始化函数中调用phy_drivers_register接口注册phy_driver对象。

申请注册mii_bus

一般在网卡驱动的probe初始化网卡驱动的时候申请mii_bus,以smsc911x网卡驱动为例,在其probe函数中调用smsc911x_mii_init()函数来初始化mii,在该函数中申请了mii_bus并设置了read和write回调函数。

1
2
pdata->mii_bus->read = smsc911x_mii_read;
pdata->mii_bus->write = smsc911x_mii_write;

此后通过mdiobus_read/mdiobus_write或者phy_read/phy_write接口都会调用smsc911x_mii_read和smsc911x_mii_write函数。

注册phy设备

一般动态扫描的phy设备在网卡驱动初始化的时候注册,仍然以smsc911x网卡驱动为例,在其初始化调用smsc911x_mii_init函数申请mii_bus后,调用mdiobus_register函数创建并注册phy_device对象。

1
2
3
4
if (mdiobus_register(pdata->mii_bus)) {
SMSC_WARN(pdata, probe, "Error registering mii bus");
goto err_out_free_bus_2;
}

开启phy状态机

一般在网卡驱动的网卡open函数中开启phy状态机。仍然以smsc911x网卡驱动为例,smsc911x驱动的netdevice对象设置的netdev_ops成员为smsc911x_netdev_ops。

1
dev->netdev_ops = &smsc911x_netdev_ops;

该函数集的open成员初始化为smsc911x_open:

1
2
3
4
5
6
7
8
9
10
11
12
13
static const struct net_device_ops smsc911x_netdev_ops = {
.ndo_open = smsc911x_open,
.ndo_stop = smsc911x_stop,
.ndo_start_xmit = smsc911x_hard_start_xmit,
.ndo_get_stats = smsc911x_get_stats,
.ndo_set_rx_mode = smsc911x_set_multicast_list,
.ndo_do_ioctl = smsc911x_do_ioctl,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = smsc911x_set_mac_address,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = smsc911x_poll_controller,
#endif
};

在smsc911x_open中调用smsc911x_mii_probe函数来创建网卡驱动和phy设备间的联系,其中调用了phy_connect_direct来连接网卡和phy。在通过smsc911x_mii_probe建立连接后,继续做其他必要初始化,之后调用phy_start启动phy:

1
2
/* Bring the PHY up */
phy_start(dev->phydev);

在phy_start函数中会调用phy_start_machine来启动phy状态机,phy_start函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void phy_start(struct phy_device *phydev)
{
mutex_lock(&phydev->lock);

if (phydev->state != PHY_READY && phydev->state != PHY_HALTED) {
WARN(1, "called from state %s\n",
phy_state_to_str(phydev->state));
goto out;
}

/* if phy was suspended, bring the physical link up again */
__phy_resume(phydev);

phydev->state = PHY_UP;

phy_start_machine(phydev);
out:
mutex_unlock(&phydev->lock);
}