0 环境
内核:经过xilinx基于zynq平台定制的4.4.0系内核;
硬件:zynq芯片,其中mac contorller是使用Cadence的IP核,phy芯片使用提marvell的1116R芯片;
设备树:如下表所示,定义了phy芯片与mac controller;
ethernet@e000b000 { compatible = "cdns,zynq-gem","cdns,gem"; reg = <0xe000b000 0x1000>; status = "okay"; interrupts = <0x0 0x16 0x4>; clocks = <0x1 0x1e 0x1 0x1e 0x1 0xd>; clock-names = "pclk", "hclk", "tx_clk"; #address-cells = <0x1>; #size-cells = <0x0>; phy-mode = "rgmii-id"; phy-handle = <0x7>; pinctrl-names = "default"; pinctrl-0 = <0x8>; ethernet-phy@17 { reg = <0x17>; linux,phandle = <0x7>; phandle = <0x7>; }; }; |
1 MDIO总线设备模型
事实上,我认为网卡驱动程序可以分为两层,一层专注于与数据的收发,另外一层则是MDIO,对PHY芯片的控制信息。对于MDIO相信阅读完《网卡驱动之01硬件及协议介绍》已经不陌生。本文主要分析的是MDIO总线驱动。
MDIO驱动模型,套用了常规的总线驱动设备模型。此处的“总线”即指MDIO总线,“驱动”则是由PHY相关芯片的驱动程序注册,而“设备”则是由MacController完成触发。
MDIO总线的定义在~/drivers/net/phy/mdio_bus.c
struct bus_type mdio_bus_type = { .name = "mdio_bus", .match = mdio_bus_match, .pm = MDIO_BUS_PM_OPS, .dev_groups = mdio_dev_groups, }; |
驱动端的注册在调用int phy_driver_register(struct phy_driver *new_driver)函数时触发,该函数定义在~/drivers/net/phy/phy_device.c中。
设备端的注册则在int phy_device_register(struct phy_device *phydev)函数完成,该函数定义在~/drviers/net/phy_device.c中。
2 驱动程序内核文件结构分布
根据上述分析其实也不难分析到对内核文件分布特点,事实上分析驱动源码的组织结构对我们处理其它的芯片驱动非常有益。大致总结如下。
首先,~/drivers/net/phy 目录下内核提供了phy芯片访问通用的代码,包括了phy_device.c、phy.c和marvell.c通用代码,其中MDIO专注于维护总线,phy_device.c则是专注于为mac 和 phy芯片提供接口,而phy.c则是提供以USB为介质的接口函数。
其次,关于mdio_bus.c,如果MACcontroller中有MDIO接口那么使用的是该文件,但如果没有MDIO接口,那么可能需要使用GPIO模拟,那么使用的是mdio-gpio.c。还有在net/目录下的mdio.c暂时没有发现什么用处。
然后,对于phy芯片毫无疑问,是存储在~/drivrs/net/phy目录下,例如我们使用的是marvell phy芯片,所以编译进内核的是~/drivrs/net/phy/marvell.c。
最后,mac controller驱动,因为使用的是以太网协议,所有有关此协议的mac驱动都存放在~/drivers/net/ethernet/下,在net目录下还有一些其它网络协议,如PPP,则相关mac驱动存储在~/drivers/net/ppp目录下。另外,由于我们使用的是cadence,所以对应的驱动代码都在~/drivers/net/ethernet/cadence中的macb.c文件。
3 源码追踪
先放弃内核完成的部分(比如MDIO总线驱动),而从硬件模块角度出发。显然,有两个硬件MacController和PHY芯片。
如上所述遵循的是总线-设备-驱动模型,那么随意从设备或者驱动分析都是可以的(因为模型中,任何一端无论是设备还是驱动加载时,都会去另外一端寻找匹配的支持)。下面就先以Mac Controller端分析。
MacController芯片端的驱动是Cadence,是~/drivers/net/ethernet/cadence中的macb.c文件。分析驱动肯定从入口函数分析。
1)设备树中.compatible属性触发了macb_probe的调用。
static struct platform_driver macb_driver = { .probe = macb_probe, .remove = macb_remove, .driver = { .name = "macb", .of_match_table = of_match_ptr(macb_dt_ids), .pm = &macb_pm_ops, }, }; |
static const struct of_device_id macb_dt_ids[] = { { .compatible = "cdns,at32ap7000-macb" }, { .compatible = "cdns,at91sam9260-macb" }, { .compatible = "cdns,macb" }, { .compatible = "cdns,pc302-gem", .data = &pc302gem_config }, { .compatible = "cdns,gem", .data = &pc302gem_config }, { .compatible = "atmel,sama5d3-gem", .data = &sama5d3_config }, { .compatible = "atmel,sama5d4-gem", .data = &sama5d4_config }, { .compatible = "cdns,zynqmp-gem", .data = &zynqmp_config}, { /* sentinel */ } }; |
ethernet@e000b000 { compatible = "cdns,zynq-gem","cdns,gem"; |
2)mac_probe函数,正如之前所述maccontroller的功能除了“向下”与phy芯片建立MDIO的控制数据的交换,还需要“向上”完成网卡的本职工作,即数据流的收发。因此mac_probe的代码也可以划分为两部分,在此只关注与PHY芯片的交互,MII协议(MDIO可以算是MII的一个组成部分),因为在该函数中并无mdio类似的关键词,但有mii的关键词,所以相信会在mii相关处理函数中完成了驱动挂接。
static int macb_probe(struct platform_device *pdev) { //设置mac controller相关的代码仅针对mac controller 不涉及与PHY芯片的通信 //准确地说上述代码是与NET DEVICE 相关,不是MAC相关 err = register_netdev(dev); err = macb_mii_init(bp); } |
3)macb_mii_init函数,如上所述mdio是属于mii的一个组成部分,所以macb_mii_init函数中也分为两部分,mii相关的工作和MDIO相关的工作。显然需要继续追踪的是of_mdiobus_register函数,该函数应该会完成MDIO总线的注册。(由此可知,总线虽然定义了(mdio_bus.c),但是没有实例化,是在此提出注册完成初始化)
int macb_mii_init(struct macb *bp) { //mii 相关 …… //mdio 相关 np = bp->pdev->dev.of_node; if (np) { //因为设备树中已经有相关节点了,所以进入该分支 /* try dt phy registration */ err = of_mdiobus_register(bp->mii_bus, np);
/* fallback to standard phy registration if no phy were found during dt phy registration */ if (!err && !phy_find_first(bp->mii_bus)) { for (i = 0; i < PHY_MAX_ADDR; i++) { struct phy_device *phydev; phydev = mdiobus_scan(bp->mii_bus, i); if (IS_ERR(phydev)) { err = PTR_ERR(phydev); break; } } } } err = macb_mii_probe(bp->dev); } |
4)of_mdiobus_register函数,这里需要注意mii_bus中的phy_mask属性,该属性对应位如果置1,那么会屏蔽掉对该PHY地址的扫描工作。而且这里有两个需要继续追踪,一个是mdiobus_register函数,还有一个是of_mdiobus_register_phy函数。
nt of_mdiobus_register(struct mii_bus *mdio, struct device_node *np) { /* Mask out all PHYs from auto probing. Instead the PHYs listed in * the device tree are populated after the bus has been registered */ mdio->phy_mask = ~0;//之所以将这点单独列出是因为phy_mask为1表示不扫描该地址挂接的PHY芯片 //否则,在mdiobus_register中会扫描所有地址,这样我们的设备树中节点就无意义了
/* Register the MDIO bus */ rc = mdiobus_register(mdio); //此处并不会注册PHY芯片
/* Loop over the child nodes and register a phy_device for each one */ //这里会根据子节点即dts中的ethernet-phy中进行注册 for_each_available_child_of_node(np, child) { addr = of_mdio_parse_addr(&mdio->dev, child); if (addr < 0) { scanphys = true; continue; }
rc = of_mdiobus_register_phy(mdio, child, addr); if (rc) continue; } //显然scanphys仍然为false,返回从而避免了auto scan if (!scanphys) return 0; //auto scan 相关代码 return 0; } |
5.1)mdiobus_register函数,完成MDIO总线的初始化工作,同时将dev挂接到MDIO总线的设备端,此时dev并没有初始化。因为mac conroller会作为mdio总线的设备端,所以加载了驱动当然也需要挂接,只不过因为没有初始化所以不会执行初始化工作。
另外在注册完MDIO总线时,会扫描总线上是否挂有phy设备,但是由于在4)中将mask全为1,所以会屏蔽掉扫描的操作。
5.2)of_mdiobus_register_phy,根据设备树中相关的定义,ethernet节点下还挂接了phy节点,所以在4)中的扫描子节点时会发现0x17的phy节点,0x17即为phy addr。从而进入of_mdiobus_register_phy 函数。详细分析如下注释
static int of_mdiobus_register_phy(struct mii_bus *mdio, struct device_node *child, u32 addr) { is_c45 = of_device_is_compatible(child, "ethernet-phy-ieee802.3-c45"); //设备树中无ethernet-phy-ieee802.3-c45属性且通过of_get_phy_id返回错误码负值 //进入else分支 if (!is_c45 && !of_get_phy_id(child, &phy_id)) phy = phy_device_create(mdio, addr, phy_id, 0, NULL); else //同时如果跟踪进去会发现在get_phy_device中创建了phy_device(phy_device_create) //phy_device_create中最关键的是使用的时侯确定该设备需要挂接的总线是MDIO_BUS_TYPE //但并没有真正的挂接 phy = get_phy_device(mdio, addr, is_c45); /* Associate the OF node with the device structure so it * can be looked up later */ //phy->dev.of_node指向ethernet-phy@17节点 of_node_get(child); phy->dev.of_node = child;
/* All data is now stored in the phy struct; * register it */ //向刚才已经注册的MDIO总线上注册PHY设备 //显然待会儿肯定会注册PHY驱动 rc = phy_device_register(phy); //至此完成了MDIO总线,以及从设备树中扫描到的PHY节点的作为设备端注册至MDIO总线上 } |
6)get_phy_device 初始化并且构造phy结构体,将作为mdio总线的设备端加入到总线中。下面贴出了初始化代码,非常容易理解,在此就不多解释。需要注意的是,从此phy_device特性是phy_addr是0x17(因为在5)中是解析到了子节点且将reg参数作为addr去扫描的,而硬件上设定了phy芯片的addr就是0x17,所以是有回应的);phy_id这是ieee定义的必须在每个phy芯片中的addr为2和3的寄存器中定义phy_id标识phy芯片,所以在创建设备后的初始化中也会读取该ID(注意区分phy_id和addr区别后者是由硬件设计决定的);bus_type为刚初始化的&mdio_bus_type。
struct phy_device *get_phy_device(struct mii_bus *bus, int addr, bool is_c45) { r = get_phy_id(bus, addr, &phy_id, is_c45, &c45_ids); return phy_device_create(bus, addr, phy_id, is_c45, &c45_ids); } 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) { dev->dev.bus = &mdio_bus_type; //MDIO_BUS_TYPE 待会儿在驱动端你也会发现,bustype是mdio_bus_type dev_set_name(&dev->dev, PHY_ID_FMT, bus->id, addr);
device_initialize(&dev->dev); return dev; } |
7)phy_device_register,6)中完成的是phy芯片的初始化工作,而此处则是挂接到mdio总线上。
至此,完成了MDIO总线和设备端的初始化和注册工作。
接下来分析phy芯片的注册挂接以及匹配的过程。根据硬件和2中所述,phy芯片完成MDIO总线的驱动端注册,且是在marvell.c(~/drivers/net/phy/marvell.c)中定义。但是没有module_init的初始化函数,而是定义了一堆如下的结构体。
最后仔细分析,发现在于module_phy_driver宏定义。跟踪进去即可发现它完成了驱动端的注册,相对简单在此就不赘述。
static struct phy_driver marvell_drivers[] = { { .phy_id = MARVELL_PHY_ID_88E1101, .phy_id_mask = MARVELL_PHY_ID_MASK, .name = "Marvell 88E1101", .features = PHY_GBIT_FEATURES, .flags = PHY_HAS_INTERRUPT, .config_aneg = &marvell_config_aneg, .read_status = &genphy_read_status, .ack_interrupt = &marvell_ack_interrupt, .config_intr = &marvell_config_intr, .resume = &genphy_resume, .suspend = &genphy_suspend, .driver = { .owner = THIS_MODULE }, }, ... }; module_phy_driver(marvell_drivers); |
#define module_phy_driver(__phy_drivers) \ phy_module_driver(__phy_drivers, ARRAY_SIZE(__phy_drivers)) #define phy_module_driver(__phy_drivers, __count) \ static int __init phy_module_init(void) \ { \ return phy_drivers_register(__phy_drivers, __count); \ } int phy_drivers_register(struct phy_driver *new_driver, int n) { ret = phy_driver_register(new_driver + i); }
int phy_driver_register(struct phy_driver *new_driver) { new_driver->driver.name = new_driver->name; new_driver->driver.bus = &mdio_bus_type; new_driver->driver.probe = phy_probe; new_driver->driver.remove = phy_remove; retval = driver_register(&new_driver->driver); } |
可以很清楚的发现,这是一个高度抽象化的框架,首先,内核定义了一系列需要用到的操作函数作为phy_driver的结构体成员,这样各phy芯片厂商可以根据自身芯片的特点去实现各成员函数。然后,phy_device.c负责调用各个接口函数的操作。
换句话说,内核实现了ieee定义的mdio标准,完成MDIO的通信任务,而各大phy芯片厂商必然满足该标准,所以内核的代码可以复用所有的phy芯片,但这是流程化的操作,而具体访问的地址是多少,则由phy芯片厂商定义,所以内核定义的某些成员函数由芯片厂商作为驱动程序去实例化。这样,大大减轻了phy芯片驱动开发的工作量。
最后,简单说下匹配的问题。匹配函数在mdio_bus.c中定义,显然我们定义的设备树只会进入最后的分支。从上述分析中可以知道在初始化phy_device中,已经从芯片中读取到了ID值是0x01410e40(#define MARVELL_PHY_ID_88E1116R 0x01410e40),所以和驱动(marvell.c)中定义phy_driver结构体匹配上,完成匹配工作。
//至此,总线MDIO,设备mac-controller ,驱动marvell.c phy.c //由于都是mdio_bus_type 所以任何一次加载都会互相扫描 //查看match函数至此完成了互相关联 static int mdio_bus_match(struct device *dev, struct device_driver *drv) { struct phy_device *phydev = to_phy_device(dev); struct phy_driver *phydrv = to_phy_driver(drv); if (of_driver_match_device(dev, drv)) return 1; if (phydrv->match_phy_device) return phydrv->match_phy_device(phydev); //进入的是最后一个分支 return (phydrv->phy_id & phydrv->phy_id_mask) == (phydev->phy_id & phydrv->phy_id_mask); } |
总结
通过上述分析,相信即使对于其它内核版本也是有相当的帮助的。其实内核驱动都遵循一套:分配à设置à注册à调用的套路。