Linux内核mdio子系统
为什么要学习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 | static int __init phy_init(void) |
这里注册两个通用phy驱动,客户还可以向总线注册其它phy驱动,当总线上注册mdio设备的时候,将会由总线核心通过总线的match方法来判断和设备兼容的驱动,并建立绑定关系。
mdio bus type
mdio总线是内核bus_type类型的数据结构,其名字为mdio_bus。mdio总线在内核中的定义如下:
1 | struct bus_type mdio_bus_type = { |
在mdio子系统初始化的时候,调用mdio_bus_init()函数初始化mdio总线,在mdio_bus_init函数中执行bus_register()函数向核心注册了mdio_bus_type。该段代码片段如下:
1 | int __init mdio_bus_init(void) |
mdio总线match方法:
由mdio总线定义可知,mdio总线的match回调函数为mdio_bus_match,在设备驱动模型中,总线的match方法用来判断总线上的设备是否能被总线上的驱动处理。mdio的match方法首先通过of_driver_match_device()来判断,如果在of中指定了判断条件并且成立,则直接返回真。否则调用mdio_device的bus_match方法来判断。该部分代码片段如下:
1 | static int mdio_bus_match(struct device *dev, struct device_driver *drv) |
mdio driver
通用的mdio driver对象在内核中用mdio_driver结构表示,该结构在内核源码中的定义如下:
1 | /* struct mdio_driver: Generic MDIO driver */ |
从定义可以看到,mdio driver对象继承自mdio_driver_common对象,而mdio_driver_common继承了内核设备驱动模型的device_driver对象,从而实现了mdio子系统的设备驱动模型。
linux内核提供了mdio driver的注册和注销接口:
1 | int mdio_driver_register(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 | struct mdio_device { |
其中dev成员是为了继承device对象,从而实现mdio的设备驱动模型。bus成员用来提供访问phy寄存器的方法,mii_bus会在后面详细说明。bus_match方法在mdio总线的match方法中调用,用来和mdio驱动进行匹配。device_free和device_remove用来释放和移除mdio设备。addr表示phy在该总线上的地址。
linux内核提供了一个mdio_device的创建,释放,注册,移除和复位以及mdio总线匹配接口:
1 | void mdio_device_free(struct mdio_device *mdiodev); |
当mdio_device注册到mdio设备驱动模型的时候,会和mdio总线上的mdio driver进行匹配和关联。
mdio class
当mdio总线初始化的时候,在注册mdio总线之前向核心注册了mdio总线的class,mdio总线的class在内核中定义如下:
1 | static struct class mdio_bus_class = { |
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 | void phy_driver_unregister(struct phy_driver *drv); |
通过阅读phy_driver_register函数可知,phy driver是将mdio_driver_common对象注册到了mdio的总线上。同时设置了driver的probe和remove分别为phy_probe和phy_remove。该部分代码片段如下:
1 | new_driver->mdiodrv.driver.bus = &mdio_bus_type; |
当新的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 | struct phy_device *get_phy_device(struct mii_bus *bus, int addr, bool is_c45); |
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 | dev->state = PHY_DOWN; |
phy状态机定义了如下几个状态:
1 | enum phy_state { |
其中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 | struct mii_bus { |
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 |
|
由上面的宏结构可知,其原理是通过module_init定义一个模块初始化函数,在模块初始化函数中调用phy_drivers_register接口注册phy_driver对象。
申请注册mii_bus
一般在网卡驱动的probe初始化网卡驱动的时候申请mii_bus,以smsc911x网卡驱动为例,在其probe函数中调用smsc911x_mii_init()函数来初始化mii,在该函数中申请了mii_bus并设置了read和write回调函数。
1 | pdata->mii_bus->read = smsc911x_mii_read; |
此后通过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 | if (mdiobus_register(pdata->mii_bus)) { |
开启phy状态机
一般在网卡驱动的网卡open函数中开启phy状态机。仍然以smsc911x网卡驱动为例,smsc911x驱动的netdevice对象设置的netdev_ops成员为smsc911x_netdev_ops。
1 | dev->netdev_ops = &smsc911x_netdev_ops; |
该函数集的open成员初始化为smsc911x_open:
1 | static const struct net_device_ops smsc911x_netdev_ops = { |
在smsc911x_open中调用smsc911x_mii_probe函数来创建网卡驱动和phy设备间的联系,其中调用了phy_connect_direct来连接网卡和phy。在通过smsc911x_mii_probe建立连接后,继续做其他必要初始化,之后调用phy_start启动phy:
1 | /* Bring the PHY up */ |
在phy_start函数中会调用phy_start_machine来启动phy状态机,phy_start函数实现如下:
1 | void phy_start(struct phy_device *phydev) |