还没有学习第一篇内容的建议从第一篇开始学习会比较容易理解【以太网驱动】以太网扫盲篇一:各种网络总线 mii总线,mdio总线介绍
1. 概述
phy芯片为osi的最底层-物理层(physical layer),通过mii/gmii/rmii/sgmii/xgmii等多种媒体独立接口(介质无关接口)与数据链路层的mac芯片相连,并通过mdio接口实现对phy状态的监控、配置和管理。
phy与mac整体的大致连接框架如下(图片来源于网络):
phy的整个硬件系统组成比较复杂,phy与mac相连(也可以通过一个中间设备相连),mac与cpu相连(有集成在内部的,也有外接的方式)。
phy与mac通过mii和mdio/mdc相连,mii是走网络数据的,mdio/mdc是用来与phy的寄存器通讯的,对phy进行配置。
phy的驱动与i2c/spi的驱动一样,分为控制器驱动和设备器驱动。本节先讲控制器驱动。
2. phy的控制器驱动总述
phy的控制器驱动和spi/i2c非常类似,控制器的核心功能是实现具体的读写功能。区别在于phy的控制器读写功能的实现大致可以分为两种方式():
直接调用cpu的mdio控制器(直接调用cpu对应的寄存器)的方式;通过gpio/外围soc模拟mdio时序的方式;phy的控制器一般被描述为mdio_bus平台设备(注意:这是一个设备,等同于spi/i2c中的master设备;和总线、驱动、设备中的bus不是一个概念)。
既然是平台设备,那么设备树中必定要有可以被解析为平台设备的节点,也要有对应的平台设备驱动。与spi驱动类似,phy设备模型也是在控制器驱动的probe函数中注册的。
3. 通过gpio/外围soc模拟mdio时序的方式
3.1 控制器平台设备在设备树中的大致描述方式(不完全准确,主要描述匹配的规则)
# linux-4.9.225documentationdevicetreeindingssocfslcpm_qeetwork.txt* mdiocurrently defined compatibles: fsl,pq1-fec-mdio (reg is same as first resource of fec device) fsl,cpm2-mdio-bitbang (reg is port c registers)properties for fsl,cpm2-mdio-bitbang: fsl,mdio-pin : pin of port c controlling mdio data fsl,mdc-pin : pin of port c controlling mdio clockexample: mdio@10d40 { compatible = fsl,mpc8272ads-mdio-bitbang, fsl,mpc8272-mdio-bitbang, fsl,cpm2-mdio-bitbang; reg = ; #address-cells = ; #size-cells = ; fsl,mdio-pin = ; fsl,mdc-pin = ; # linux-4.9.225documentationdevicetreeindingsphy xxx_phy: xxx-phy@xxx { //描述控制器下挂phy设备的节点 reg = ; //phy的地址 }; };
3.2 控制器平台驱动代码走读
3.2.1 控制器平台驱动的注册
static const struct of_device_id fs_enet_mdio_bb_match[] = { { .compatible = fsl,cpm2-mdio-bitbang, //匹配平台设备的名称 }, {},};module_device_table(of, fs_enet_mdio_bb_match); static struct platform_driver fs_enet_bb_mdio_driver = { .driver = { .name = fsl-bb-mdio, .of_match_table = fs_enet_mdio_bb_match, }, .probe = fs_enet_mdio_probe, .remove = fs_enet_mdio_remove,}; module_platform_driver(fs_enet_bb_mdio_driver); //注册控制器平台设备驱动
3.2.2 控制器平台驱动的probe函数走读
/********************************************************************************************** 通过gpio/外围soc模拟mdio时序方式的mdio驱动(probe函数中完成phy设备的创建和注册)***********************************************************************************************/# linux-4.9.225driversetethernetfreescalefs_enetmii-bitbang.c fs_enet_mdio_probe(struct platform_device *ofdev)|--- bitbang = kzalloc(sizeof(struct bb_info), gfp_kernel)||--- bitbang->ctrl.ops = &bb_ops ----------------------------------------------->| static struct mdiobb_ops bb_ops = { | | .owner = this_module, | | .set_mdc = mdc,| | .set_mdio_dir = mdio_dir,| | .set_mdio_data = mdio, |-->实现为gpio的读写| | .get_mdio_data = mdio_read,| | }; | ctrl) || |--- bus = mdiobus_alloc() -----------| | struct mdiobb_ctrl *ctrl = bus->priv| || |--- bus->read = mdiobb_read -----------| | ctrl->ops->set_mdc | || |--- bus->write = mdiobb_write -----------|--mdiobb_read/mdiobb_write/mdiobb_reset函数的实现 -| ctrl->ops->set_mdio_dir |--|| |--- bus->reset = mdiobb_reset -----------| / | ctrl->ops->set_mdio_data || |--- bus->priv = ctrl ops->get_mdio_data || |--- fs_mii_bitbang_init //设置用来模拟mdc和mdio的管脚资源| |--- of_address_to_resource(np, 0, &res) //转换设备树地址并作为资源返回,设备树中指定| | | |--- snprintf(bus->id, mii_bus_id_size, %x, res.start) //把资源的起始地址设置为bus->id| || |--- data = of_get_property(np, fsl,mdio-pin, &len)| |--- mdio_pin = *data //决定控制mdio数据的端口的引脚| || |--- data = of_get_property(np, fsl,mdc-pin, &len)| |--- mdc_pin = *data //控制mdio时钟的端口引脚| || |--- bitbang->dir = ioremap(res.start, resource_size(&res)) | || |--- bitbang->dat = bitbang->dir + 4 | |--- bitbang->mdio_msk = 1
owner = owner| | | |--- bus->dev.parent = bus->parent | | | |--- bus->dev.class = &mdio_bus_class| | | |--- bus->dev.groups = null| | | |--- dev_set_name(&bus->dev, %s, bus->id) //设置总线设备的名称| | | |--- device_register(&bus->dev) //注册总线设备| | | |--- for_each_available_child_of_node(np, child) //遍历这个平台设备的子节点并为每个phy注册一个phy_device| |--- addr = of_mdio_parse_addr(&mdio->dev, child) //从子节点的reg属性中获得phy设备的地址 | | |--- of_property_read_u32(np, reg, &addr)| |--- if (addr mdio //mdiodev是最新的内核引入,较老的版本没有这个结构| | | |--- mdiodev->dev.release = phy_device_release| | | |--- mdiodev->dev.parent = &bus->dev| | | |--- mdiodev->dev.bus = &mdio_bus_type //phy设备和驱动都会挂在mdio_bus下,匹配时会调用对应的match函数 ---| | | | |--- mdiodev->bus = bus || | | |--- mdiodev->pm_ops = mdio_bus_phy_pm_ops || | | |--- mdiodev->bus_match = phy_bus_match //真正实现phy设备和驱动匹配的函数addr = addr| | | |--- mdiodev->flags = mdio_device_flag_phy| | | |--- mdiodev->device_free = phy_mdio_device_free| | | |--- diodev->device_remove = phy_mdio_device_remove| | | |--- dev->speed = speed_unknown| | | |--- dev->duplex = duplex_unknown| | | |--- dev->pause = 0| | | |--- dev->asym_pause = 0| | | |--- dev->link = 1| | | |--- dev->interface = phy_interface_mode_gmii| | | |--- dev->autoneg = autoneg_enable //默认支持自协商| | | |--- dev->is_c45 = is_c45| | | |--- dev->phy_id = phy_id| | | |--- if (c45_ids)| | | | |--- dev->c45_ids = *c45_ids| | | |--- dev->irq = bus->irq[addr]| | | |--- dev_set_name(&mdiodev->dev, phy_id_fmt, bus->id, addr) | | | |--- dev->state = phy_down //指示phy设备和驱动程序尚未准备就绪,在phy驱动的probe函数中会更改为ready| | | |--- init_delayed_work(&dev->state_queue, phy_state_machine) //phy的状态机(核心work) | | | |--- init_work(&dev->phy_queue, phy_change) //由phy_interrupt / timer调度以处理phy状态的更改| | | |--- request_module(mdio_module_prefix mdio_id_fmt, mdio_id_args(phy_id))//加载内核模块(这里没有细致研究过)| | | |--- device_initialize(&mdiodev->dev) //设备模型中的一些设备,主要是kset、kobject、ktype的设置| | | | | |--- irq_of_parse_and_map(child, 0) //将中断解析并映射到linux virq空间(未深入研究)| | |--- if (of_property_read_bool(child, broken-turn-around))//mdio总线中的ta(turnaround time)| | | |--- mdio->phy_ignore_ta_mask |= 1 mdio) //注册到mdiodev->bus,其实笔者认为这是一个虚拟的注册,仅仅是根据phy的地址在mdiodev->bus->mdio_map数组对应位置填充这个mdiodev | | | | |--- mdiodev->bus->mdio_map[mdiodev->addr] = mdiodev // 方便通过mdiodev->bus统一管理和查找,以及关联bus的读写函数,方便phy的功能配置| | | || | | |--- device_add(&phydev->mdio.dev)//注册到linux设备模型框架中| || |--- if (!scanphys) //如果从子节点的reg属性中获得phy设备的地址,scanphys=false,这里就直接返回了,因为不需要再扫描了| | |--- return 0 | |/****************************************************************************************************************** 一般来说只要设备树种指定了phy设备的reg属性,后面的流程可以自动忽略******************************************************************************************************************| |--- for_each_available_child_of_node(np, child) //自动扫描具有空reg属性的phy| |--- if (of_find_property(child, reg, null)) //跳过具有reg属性集的phy| | |--- continue| | | |--- for (addr = 0; addr dev, scan phy %s at address %i, child->name, addr) //打印扫描的phy,建议开发人员设置reg属性| || |--- if (of_mdiobus_child_is_phy(child))| |--- of_mdiobus_register_phy(mdio, child, addr) //注册phy设备| ******************************************************************************************************************/
4. 直接调用cpu的mdio控制器的方式
控制器平台设备在设备树中的大致描述方式(不完全准确,主要描述匹配的规则)
# linux4.9.225documentationdevicetreeindingspowerpcfslfman.txtexample for fman v3 internal mdio:mdio@e3120 { //描述mdio控制器驱动节点 compatible = fsl,fman-mdio; reg = ; fsl,fman-internal-mdio; tbi1: tbi-phy@8 { //描述控制器下挂phy设备的节点 reg = ; device_type = tbi-phy; }; };
控制器平台驱动的注册
# linux-4.9.225driversetethernetfreescalefsl_pq_mdio.cstatic const struct of_device_id fsl_pq_mdio_match[] = { ...... /* no kconfig option for fman support yet */ { .compatible = fsl,fman-mdio, //匹配平台设备的名称 .data = &(struct fsl_pq_mdio_data) { .mii_offset = 0, /* fman tbi operations are handled elsewhere */ }, }, ...... {},}; static struct platform_driver fsl_pq_mdio_driver = { .driver = { .name = fsl-pq_mdio, .of_match_table = fsl_pq_mdio_match, }, .probe = fsl_pq_mdio_probe, .remove = fsl_pq_mdio_remove,}; module_platform_driver(fsl_pq_mdio_driver); //注册控制器平台设备驱动
控制器平台驱动的probe函数走读
/**************************************************************************************** 直接调用cpu的mdio控制器的方式的mdio控制器驱动(probe函数中涉及phy设备的创建和注册)****************************************************************************************/# linux-4.9.225driversetethernetfreescalefsl_pq_mdio.c fsl_pq_mdio_probe(struct platform_device *pdev|--- struct fsl_pq_mdio_priv *priv|--- struct mii_bus *new_bus||--- new_bus = mdiobus_alloc_size(sizeof(*priv)) //分配结构体|--- priv = new_bus->priv|--- new_bus->name = freescale powerquicc mii bus|--- new_bus->read = &fsl_pq_mdio_read //总线的读接口|--- new_bus->write = &fsl_pq_mdio_write //总线的写接口|--- new_bus->reset = &fsl_pq_mdio_reset //总线的复位接口| |--- of_address_to_resource(np, 0, &res) //获取控制器地址资源|--- snprintf(bus->id, mii_bus_id_size, %x, res.start) //把资源的起始地址设置为bus->id | |--- of_mdiobus_register(new_bus, np)//注册mii_bus设备,并通过设备树中控制器的子节点创建phy设备,这一点与模拟方式流程相同
of_mdiobus_register的流程与第四小节一致,这里就不再列出。
5. 控制器的读写会在哪里得到调用?
在phy设备的注册中(读phy id)、phy的初始化、自协商、中断、状态、能力获取等流程中经常可以看到phy_read和phy_write两个函数(下一节要讲的phy驱动),这两个函数的实现就依赖于控制器设备mii_bus的读写。
phy_read和phy_write定义在linux-4.9.225includelinuxphy.h中,如下:
static inline int phy_read(struct phy_device *phydev, u32 regnum){ return mdiobus_read(phydev->mdio.bus, phydev->mdio.addr, regnum);} static inline int phy_write(struct phy_device *phydev, u32 regnum, u16 val){ return mdiobus_write(phydev->mdio.bus, phydev->mdio.addr, regnum, val);}
其中mdiobus_read和mdiobus_write定义在linux-4.9.225drivers etphymdio_bus.c中,如下:
/** * mdiobus_read - convenience function for reading a given mii mgmt register * @bus: the mii_bus struct * @addr: the phy address * @regnum: register number to read * * note: must not be called from interrupt context, * because the bus read/write functions may wait for an interrupt * to conclude the operation. */int mdiobus_read(struct mii_bus *bus, int addr, u32 regnum){ int retval; bug_on(in_interrupt()); mutex_lock(&bus->mdio_lock); retval = bus->read(bus, addr, regnum); mutex_unlock(&bus->mdio_lock); return retval;} /** * mdiobus_write - convenience function for writing a given mii mgmt register * @bus: the mii_bus struct * @addr: the phy address * @regnum: register number to write * @val: value to write to @regnum * * note: must not be called from interrupt context, * because the bus read/write functions may wait for an interrupt * to conclude the operation. */int mdiobus_write(struct mii_bus *bus, int addr, u32 regnum, u16 val){ int err; bug_on(in_interrupt()); mutex_lock(&bus->mdio_lock); err = bus->write(bus, addr, regnum, val); mutex_unlock(&bus->mdio_lock); return err;}
可以清楚的看到bus->read和bus->write读写接口在这里得到调用。
6. mdio_bus总线
接下来要讲的phy设备驱动是基于device、driver、bus的连接方式。其驱动涉及如下几个重要部分:
总线 - sturct mii_bus (mii stand for media independent interface)
设备 - struct phy_device
驱动 - struct phy_driver
关于phy设备的创建和注册已经在第5节的probe函数中有过详细的描述(需要注意的是:phy设备不像i2c/spi有一个board_info函数进行设备的添加,而是直接读取phy中的寄存器),本节就不再描述;
总线注册的入口函数
# linux-4.9.225driversetphyphy_device.cstatic int __init phy_init(void){ int rc; rc = mdio_bus_init(); //mdio_bus总线的注册 if (rc) return rc; rc = phy_drivers_register(genphy_driver,array_size(genphy_driver), this_module); //通用phy驱动 if (rc) mdio_bus_exit(); return rc;} subsys_initcall(phy_init);
subsys_initcall(phy_init) 这行的作用非常重要,这一行就决定了内核在启动的时候会调用该函数,注册完了之后紧接着又注册一个通用的phy驱动。
总线注册函数--- mdio_bus_init解析
# linux-4.9.225driversetphymdio_bus.cstatic struct class mdio_bus_class = { .name = mdio_bus, .dev_release = mdiobus_release,}; 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;} struct bus_type mdio_bus_type = { .name = mdio_bus, //总线名称 .match = mdio_bus_match, //用来匹配总线上设备和驱动的函数 .pm = mdio_bus_pm_ops,};export_symbol(mdio_bus_type); int __init mdio_bus_init(void){ int ret; ret = class_register(&mdio_bus_class); //注册设备类 (在linux设备模型中,我再仔细讲这个类的概念) if (!ret) { ret = bus_register(&mdio_bus_type);//总线注册 if (ret) class_unregister(&mdio_bus_class); } return ret;}
总线中的match函数解析
/** * mdio_bus_match - determine if given mdio driver supports the given * mdio device * @dev: target mdio device * @drv: given mdio driver * * description: given a mdio device, and a mdio driver, return 1 if * the driver supports the device. otherwise, return 0. this may * require calling the devices own match function, since different classes * of mdio devices have different match criteria. */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;}
7. 设备驱动的注册
在phy_init函数中不仅注册了mdio_bus总线,还注册了一个通用的phy驱动作为缺省的内核phy驱动,但是如果phy芯片的内部寄存器和802.3定义的并不一样或者需要特殊的功能配置以实现更强的功能,这就需要专有的驱动。
关于通用phy驱动的知识,网上有一大堆讲解,本节就不再重复的去描述。
对于市场上存在的主流phy品牌,一般在内核源码 drivers etphy目录下都有对应的驱动。本节主要以realtek rtl8211f为例,讲述phy的驱动,代码如下:
# linux-4.9.225driversetphyealtek.cstatic struct phy_driver realtek_drvs[] = { ...... , { .phy_id = 0x001cc916, .name = rtl8211f gigabit ethernet, .phy_id_mask = 0x001fffff, .features = phy_gbit_features, .flags = phy_has_interrupt, .config_aneg = &genphy_config_aneg, .config_init = &rtl8211f_config_init, .read_status = &genphy_read_status, .ack_interrupt = &rtl8211f_ack_interrupt, .config_intr = &rtl8211f_config_intr, .suspend = genphy_suspend, .resume = genphy_resume, },}; module_phy_driver(realtek_drvs); //注册phy驱动 static struct mdio_device_id __maybe_unused realtek_tbl[] = { { 0x001cc912, 0x001fffff }, { 0x001cc914, 0x001fffff }, { 0x001cc915, 0x001fffff }, { 0x001cc916, 0x001fffff }, { }}; module_device_table(mdio, realtek_tbl);
phy驱动的注册
1、同一品牌的phy设备有多种不同的型号,内核为了支持一次可以注册多个型号的phy的驱动,在includelinuxphy.h中提供了用于注册phy驱动的宏module_phy_driver。该宏的定义如下:
# linux-4.9.225includelinuxphy.h #define phy_module_driver(__phy_drivers, __count) static int __init phy_module_init(void) { return phy_drivers_register(__phy_drivers, __count, this_module); } #define module_phy_driver(__phy_drivers) phy_module_driver(__phy_drivers, array_size(__phy_drivers))
2、其中phy_driver_register定义如下(注意这里与老版本内核有一定的改动)
/** * phy_driver_register - register a phy_driver with the phy layer * @new_driver: new phy_driver to register * @owner: module owning this phy */int phy_driver_register(struct phy_driver *new_driver, struct module *owner){ int retval; 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; //phy设备和驱动匹配后调用的probe函数 new_driver->mdiodrv.driver.remove = phy_remove; new_driver->mdiodrv.driver.owner = owner; retval = driver_register(&new_driver->mdiodrv.driver); //向linux设备模型框架中注册device_driver驱动 if (retval) { pr_err(%s: error %d in registering driver, new_driver->name, retval); return retval; } pr_debug(%s: registered new driver, new_driver->name); return 0;} int phy_drivers_register(struct phy_driver *new_driver, int n, struct module *owner){ int i, ret = 0; for (i = 0; i 0) phy_driver_unregister(new_driver + i); break; } } return ret;}
8. 设备驱动与控制器驱动之间的关系图
世纪高通与和利时达成战略合作
微软计划收购医疗软件公司Sentillion
关于汽车360度全景影像系统工作原理研究和应用分析
大华股份携手华录集团共同推动智慧城市创新应用
电热鼓风干燥箱的产品简述及其特点介绍
PHY的控制器驱动框架分析
看到新版MacBook Pro的五大槽点,你还会买嘛?
工业互联网将为工业企业开创一个全新的时代
从2014中国(成都)电子展看测试测量行业新动向
变容二极管在电调谐器频率合成中的应用
2011年中国晶圆代工行业企业竞争分析
安全不塞耳朵的耳机,旗舰骨传导南卡runner pro2体验分享
甲醛传感器的报警功能介绍
什么原因导致手机信号变弱
华为P10与iPhone7哪个好?华为P10与iPhone7对比评测:谁才是轻便出游的拍照利器?
10.5英寸iPad Pro拆解:4GB内存 维修成本增高
CMOS掉电电路故障及主板CMOS电路常见故障
英特尔物联网规划受阻 Edison退市 放弃与树莓派的竞争
网段隔离器(NAT网关)如何解决IP地址冲突问题?
用于改善SiC MOSFET导通瞬态的电荷泵栅极驱动