使用zynq7000系列SoC开发以太网功能比较简单,xilinx提供了完整的驱动程序,只需配置设备树中与phy相关的信息即可,本篇文章的重点并非讲解以太网驱动程序本身,而是通过对以太网驱动程序代码的分析,探索linux phy子系统,包括了mdio总线,phy设备、phy驱动等,同时也能加深对linux设备模型总线、设备和驱动的理解。
phy芯片和以太网控制器之间通过mdio总线进行控制信息的传输,phy芯片挂接在mdio总线上,并且一个mdio总线上可同时挂接多个phy芯片,通过phy地址进行区分。可以看出,linux phy子系统典型地对应linux的总线、设备、驱动模型。
和大多数总线结构一样,mdio总线结构定义为mdio_bus_type:
/drivers/net/phy/mdio_bus.c
struct bus_type mdio_bus_type = {
.name = "mdio_bus",
.match = mdio_bus_match,
.uevent = mdio_uevent,
};
设备结构体包括mdio_device、phy_device和mii_bus,其中,mdio_device代表挂接在mdio总线上的mdio设备,phy_device是对mdio_device的进一步封装,专门代表phy设备(属于mdio设备的一种)。mii_bus是代表mdio总线本身的设备,是总线上mdio设备的父设备(parent)。
/include/linux/mdio.h
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;
};
/include/linux/phy.h
struct phy_device {
struct mdio_device mdio;
/* Information about the PHY type */
/* And management functions */
struct phy_driver *drv;
u32 phy_id;
struct phy_c45_device_ids c45_ids;
unsigned is_c45:1;
unsigned is_internal:1;
unsigned is_pseudo_fixed_link:1;
unsigned is_gigabit_capable:1;
unsigned has_fixups:1;
unsigned suspended:1;
unsigned sysfs_links:1;
unsigned loopback_enabled:1;
/*其余部分......*/
};
/include/linux/phy.h
/*
* The Bus class for PHYs. Devices which provide access to
* PHYs should register using this structure
*/
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_driver和phy_driver,其关系和mdio_device与phy_device的关系类似。
/include/linux/mdio.h
/* struct mdio_driver_common: Common to all MDIO drivers */
struct mdio_driver_common {
struct device_driver driver;
int flags;
};
/* 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);
};
/include/linux/phy.h
struct phy_driver {
struct mdio_driver_common mdiodrv;
u32 phy_id;
char *name;
u32 phy_id_mask;
const unsigned long * const features;
u32 flags;
const void *driver_data;
/*
* Called to issue a PHY software reset
*/
int (*soft_reset)(struct phy_device *phydev);
/*
* Called to initialize the PHY,
* including after a reset
*/
int (*config_init)(struct phy_device *phydev);
/*
* Called during discovery. Used to set
* up device-specific structures, if any
*/
int (*probe)(struct phy_device *phydev);
/*其余部分......*/
};
总线和通用phy驱动的注册主要在phy_init函数中进行,可以看出这是个系统初始化函数:
/drivers/net/phy/phy_device.c
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;
}
int phy_driver_register(struct phy_driver *new_driver, struct module *owner)
{
int retval;
/* Either the features are hard coded, or dynamically
* determined. It cannot be both.
*/
if (WARN_ON(new_driver->features && new_driver->get_features)) {
pr_err("%s: features and get_features must not both be set\n",
new_driver->name);
return -EINVAL;
}
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;
retval = driver_register(&new_driver->mdiodrv.driver);
if (retval) {
pr_err("%s: Error %d in registering driver\n",
new_driver->name, retval);
return retval;
}
pr_debug("%s: Registered new driver\n", new_driver->name);
return 0;
}
/drivers/net/phy/mdio_bus.c
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;
}
可见总线和驱动的注册最终都调用了linux设备模型中的bus_register函数和driver_register函数。
phy子系统设备包括mii_bus设备和phy_device设备,是在以太网控制器驱动注册后进行注册的。xilinx提供的macb以太网控制器驱动遵循典型的网络设备驱动架构,主要是构建net_device结构体并调用register_netdev函数注册。之后便会进入phy芯片的探测和注册流程,主要在macb_mii_init函数中进行。流程如下:
mdiobus_alloc分配了mii_bus结构体,如前所述,mii_bus结构体代表mdio总线本身,是总线上挂接的mido设备的父设备,其本身又从属于macb控制器,因此它的parent是对应macb的net_device结构体。在该结构体中还提供了mdio总线的读写方法。
of_mdiobus_register注册mii_bus结构体并扫描phy设备。
mdiobus_register最终会调用device_register将mii_bus设备注册进设备模型,之后获取设备树中记录的phy节点信息并调用of_mdiobus_register_phy探测并创建phy_device设备。
get_phy_device调用get_phy_id读取phy芯片的id号,然后调用phy_device_create。phy_device_create函数分配了phy_device结构体并对其进行初始化。
/drivers/net/phy/phy_device.c
struct phy_device *phy_device_create(struct mii_bus *bus, int addr, u32 phy_id,
bool is_c45,
struct phy_c45_device_ids *c45_ids)
{
struct phy_device *dev;
struct mdio_device *mdiodev;
int ret = 0;
/* We allocate the device, and initialize the default values */
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return ERR_PTR(-ENOMEM);
mdiodev = &dev->mdio;
mdiodev->dev.parent = &bus->dev;
mdiodev->dev.bus = &mdio_bus_type;
mdiodev->dev.type = &mdio_bus_phy_type;
mdiodev->bus = bus;
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;
/*......*/
mutex_init(&dev->lock);
INIT_DELAYED_WORK(&dev->state_queue, phy_state_machine);
/*......*/
if (!ret) {
device_initialize(&mdiodev->dev);
} else {
kfree(dev);
dev = ERR_PTR(ret);
}
return dev;
可见,phy_device_create中设置了phy_device设备的父设备为mii_bus设备,设置phy_device设备对应的总线为mdio_bus_type。
值得注意的是,创建phy_device设备后,of_mdiobus_register_phy将phy_device中device结构体的of_node成员设置为设备树中的phy节点,以便后续可以通过设备树节点直接查找到phy_device结构体。
/drivers/of/of_mdio.c
static int of_mdiobus_register_phy(struct mii_bus *mdio,
struct device_node *child, u32 addr)
{
/*......*/
phy = get_phy_device(mdio, addr, is_c45);
/*......*/
/* Associate the OF node with the device structure so it
* can be looked up later */
of_node_get(child);
phy->mdio.dev.of_node = child;
phy->mdio.dev.fwnode = of_fwnode_handle(child);
/*......*/
/* All data is now stored in the phy struct;
* register it */
rc = phy_device_register(phy);
/*......*/
}
然后,在phy_device_register中首先调用mdiobus_register_device将该phy_device添加进mii_bus的mdio_map数组中,最终调用device_add将phy_device注册进驱动模型并初始化该phy_device的phy状态机。
在linux设备模型中,device_add是一个重要的函数,它会调用kobject_add将device注册进sysfs,然后调用bus_add_device和bus_probe_device。bus_add_device将设备挂接到其总线(这里即mdio_bus_type)的设备列表中。bus_probe_device将遍历总线上已注册的所有驱动程序,调用总线的match函数检查驱动是否能和该设备匹配。若匹配则将设备和驱动程序绑定,然后调用驱动的probe函数。在笔者所做的项目中,使用的phy芯片并不能在这一步匹配上。
macb_mii_probe初始化和启动phy硬件,其主要调用了of_phy_connect函数。而of_phy_connect函数在通过设备树节点信息获取对应的phy_device设备后最终调用了phy_attach_direct函数。
phy_attach_direct判断phy_device是否绑定了phy驱动,如果没有的话则将通用phy驱动genphy_driver作为phy_device的驱动,然后调用genphy_driver的probe函数,即phy_probe函数进行初始化处理,随后调用device_bind_driver将设备和驱动正式绑定。最后调用phy_init_hw和phy_resume初始化和唤醒Phy硬件使其开始工作。至此,phy设备的注册就告一段落了。
通过对linux phy子系统的结构体和流程进行分析可以发现,linxu phy子系统是一个典型的总线、设备和驱动架构,总线和phy驱动在系统初始化时注册,而设备则是在以太网控制器驱动注册函数中根据设备树的信息进行创建和注册。注册后设备模型会调用总线的match进行设备和驱动的匹配,若未能匹配成功则使用通用phy驱动。
源码:linux-5.5.2和xilinx-linux-master