1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html 3)对正点原子Linux感兴趣的同学可以加群讨论:935446741 4)关注正点原子公众号,获取最新资料更新 第六十九章 Linux 网络驱动实验
网络驱动是linux里面驱动三巨头之一,linux下的网络功能非常强大,嵌入式linux中也常常用到网络功能。前面我们已经讲过了字符设备驱动和块设备驱动,本章我们就来学习一下linux里面的网络设备驱动。
69.1 嵌入式网络简介 69.1.1嵌入式下的网络硬件接口 本章节讨论的都是有线网络! 提起网络,我们一般想到的硬件就是“网卡”,“网卡”这个概念最早从电脑领域传出来,顾名思义就是能上网的卡。在电脑领域的“原始社会”,网卡是独立的硬件,如果电脑要上网就得买个网卡插上去,类似现在的显卡一样。但是大家现在观察自己的笔记本或者台式机主板会发现并没有类似显卡一样的网卡设备,原因是随着技术的不断发展,现在只需要一个芯片就可以实现有线网卡功能,因此网卡芯片都直接放到了主板上。所以大家在接触嵌入式的时候听到“网卡”这两个字,不要急着在开发板上找“卡”一样的东西。 既然现在网卡已经是通过一个芯片来完成了,那么是什么样的芯片呢?这个就要先了解一下嵌入式中的网络硬件方案。首先,嵌入式网络硬件分为两部分:MAC和PHY,大家都是通过看数据手册来判断一款SOC是否支持网络,如果一款芯片数据手册说自己支持网络,一般都是说的这款SOC内置MAC,MAC类似I2C控制器、SPI控制器一样的外设。但是光有MAC还不能直接驱动网络,还需要另外一个芯片:PHY,因此对于内置MAC的SOC,其外部必须搭配一个PHY芯片。但是有些SOC内部没有MAC,那也就没法搭配PHY芯片了,这些内部没有网络MAC的芯片如何上网呢?这里就要牵扯出常见的两个嵌入式网络硬件方案了。 1、SOC内部没有网络MAC外设 我们一般说某个SOC不支持网络,说的就是它没有网络MAC。那么这个芯片就不能上网了吗?显然不是的,既然没有内部MAC,那么可以找个外置的MAC芯片啊,不过一般这种外置的网络芯片都是MAC+PHY一体的。比如三星linux开发板里面用的最多的DM9000,因为三星的芯片基本没有内部MAC(比如S3C2440、S5PV210,4412等),所以三星的开发板都是通过外置的DM9000来完成有线网络功能的,DM9000对SOC提供了一个SRAM接口,SOC会以SRAM的方式操作DM9000。 有些外置的网络芯片更强大,内部甚至集成了硬件TCP/IP协议栈,对外提供一个SPI接口,比如W5500。这个一般用于单片机领域,单片机通过SPI接口与W5500进行通信,由于W5500内置了硬件TCP/IP协议栈,因此单片机就不需要移植负责的软件协议栈,直接通过SPI来操作W5500,简化了单片机联网方案。 这种方案的优点就是让不支持网络的SOC能够另辟蹊径,实现网络功能,但是缺点就是网络效率不高,因为一般芯片内置的MAC会有网络加速引擎,比如网络专用DMA,网络处理效率会很高。而且此类芯片网速都不快,基本就是10/100M。另外,相比PHY芯片而言,此类芯片的成本也比较高,可选择比较少。 SOC与外部MAC+PHY芯片的连接如图69.1.1.1所示:
图69.1.1.1 主控SOC与外置MAC+PHY芯片连接 2、SOC内部集成网络MAC外设 我们一般说某个SOC支持网络,说的就是他内部集成网络MAC外设,此时我们还需要外接一个网络PHY芯片。此时就有朋友会有疑问,PHY芯片不能也集成进SOC吗?笔者目前还没见过将PHY也集成到芯片里面的SOC。 一般常见的通用SOC都会集成网络MAC外设,比如STM32F4/F7/H7系列、NXP的I.MX系列,内部集成网络MAC的优点如下: ①、内部MAC外设会有专用的加速模块,比如专用的DMA,加速网速数据的处理。 ②、网速快,可以支持10/100/1000M网速。 ③、外接PHY可选择性多,成本低。 内部的MAC外设会通过MII或者RMII接口来连接外部的PHY芯片,MII/RMII接口用来传输网络数据。另外主控需要配置或读取PHY芯片,也就是读写PHY的内部寄存器,所以还需要一个控制接口,叫做MIDO,MDIO很类似IIC,也是两根线,一根数据线叫做MDIO,一根时钟线叫做MDC。 SOC内部MAC外设与外部PHY芯片的连接如图69.1.1.2所示:
图69.1.1.2 内部MAC与外部PHY之间的连接 大家在做项目的时候,如果要用到网络功能,强烈建议大家选择内部带有网络MAC外设的主控SOC!I.MX6ULL就有两个10M/100M的网络MAC外设,正点原子ALPHA开发板板载了两颗PHY芯片,型号为LAN8720。因此,本章节只讲解SOC内部MAC+外置PHY芯片这种方案。 69.1.2 MII/RMII接口 前面我们说了,内部MAC通过MII/RMII接口来与外部的PHY芯片连接,完成网络数据传输,本节我们就来学习一下什么是MII和RMII接口。 1、MII接口 MII全称是Media Independent Interface,直译过来就是介质独立接口,它是IEEE-802.3定义的以太网标准接口,MII接口用于以太网MAC连接PHY芯片,连接示意图如图69.1.2.1所示:
图69.1.2.1 MII接口 MII接口一共有16根信号线,含义如下: TX_CLK:发送时钟,如果网速为100M的话时钟频率为25MHz,10M网速的话时钟频率为2.5MHz,此时钟由PHY产生并发送给MAC。 TX_EN:发送使能信号。 TX_ER:发送错误信号,高电平有效,表示TX_ER有效期内传输的数据无效。10Mpbs网速下TX_ER不起作用。 TXD[3:0]:发送数据信号线,一共4根。 RXD[3:0]:接收数据信号线,一共4根。 RX_CLK:接收时钟信号,如果网速为100M的话时钟频率为25MHz,10M网速的话时钟频率为2.5MHz,RX_CLK也是由PHY产生的。 RX_ER:接收错误信号,高电平有效,表示RX_ER有效期内传输的数据无效。10Mpbs网速下RX_ER不起作用。 RX_DV:接收数据有效,作用类似TX_EN。 CRS:载波侦听信号。 COL:冲突检测信号。 MII接口的缺点就是所需信号线太多,这还没有算MDIO和MDC这两根管理接口的数据线,因此MII接口使用已经越来越少了。 2、RMII接口 RMII全称是Reduced Media Independent Interface,翻译过来就是精简的介质独立接口,也就是MII接口的精简版本。RMII接口只需要7根数据线,相比MII直接减少了9根,极大的方便了板子布线,RMII接口连接PHY芯片的示意图如图69.1.2.2所示:
图69.1.2.1 RMII接口 TX_EN:发送使能信号。 TXD[1:0]:发送数据信号线,一共2根。 RXD[1:0]:接收数据信号线,一共2根。 CRS_DV:相当于MII接口中的RX_DV和CRS这两个信号的混合。 REF_CLK:参考时钟,由外部时钟源提供, 频率为50MHz。这里与MII不同,MII的接收和发送时钟是独立分开的,而且都是由PHY芯片提供的。 除了MII和RMII以外,还有其他接口,比如GMII、RGMII、SMII、SMII等,关于其他接口基本都是大同小异的,这里就不做讲解了。正点原子ALPAH开发板上的两个网口都是采用RMII接口来连接MAC与外部PHY芯片。 69.1.3 MDIO接口 MDIO全称是Management Data Input/Output,直译过来就是管理数据输入输出接口,是一个简单的两线串行接口,一根MDIO数据线,一根MDC时钟线。驱动程序可以通过MDIO和MDC这两根线访问PHY芯片的任意一个寄存器。MDIO接口支持多达32个PHY。同一时刻内只能对一个PHY进行操作,那么如何区分这32个PHY芯片呢?和IIC一样,使用器件地址即可。同一MDIO接口下的所有PHY芯片,其器件地址不能冲突,必须保证唯一,具体器件地址值要查阅相应的PHY数据手册。 因此,MAC和外部PHY芯片进行连接的时候主要是MII/RMII和MDIO接口,另外可能还需要复位、中断等其他引脚。 69.1.4 RJ45接口 网络设备是通过网线连接起来的,插入网线的叫做RJ45座,如图69.1.4.1所示:
图69.1.4.1 RJ45座子 RJ45座要与PHY芯片连接在一起,但是中间需要一个网络变压器,网络变压器用于隔离以及滤波等,网络变压器也是一个芯片,外形一般如图69.1.4.2所示:
图69.1.4.2 网络变压器 但是现在很多RJ45座子内部已经集成了网络变压器,比如正点原子ALPHA开发板所使用的HR911105A就是内置网络变压器的RJ45座。内置网络变压器的RJ45座和不内置的引脚一样,但是一般不内置的RJ45座会短一点。因此,大家在画板的时候一定要考虑你所使用的RJ45座是否内置网络变压器,如果不内置的话就要自行添加网络变压器部分电路!!!同理,如果你所设计的硬件是需要内置网络变压器的RJ45座,肯定不能随便焊接一个不内置变压器的RJ45座,否则网络工作不正常! RJ45座子上一般有两个灯,一个黄色(橙色),一个绿色,绿色亮的话表示网络连接正常,黄色闪烁的话说明当前正在进行网络通信。这两个灯由PHY芯片控制,PHY芯片会有两个引脚来连接RJ45座上的这两个灯。内部MAC+外部PHY+RJ45座(内置网络变压器)就组成了一个完整的嵌入式网络接口硬件,如图69.1.4.2所示:
图69.1.4.2 嵌入式网络硬件接口示意图 69.1.5 I.MX6ULL ENET接口简介 I.MX6ULL有两个网络接口,也就是两个MAC外设,一个MAC连接一个PHY芯片形成一个完整网络接口,本节我们简单了解一下I.MX6ULL自带的ENET接口。I.MX6ULL内部自带的ENET外设其实就是一个网络MAC,支持10/100M。实现了三层网络加速,用于加速那些通用的网络协议,比如IP、TCP、UDP和ICMP等,为客户端应用程序提供加速服务。 I.MX6ULL内核集成了两个10/100Mbit/S的网络MAC,符合IEEE802.3-2002标准,MAC层支持双工、半双工局域网。MAC可编程、可以作为NIC卡或其他一些交换器件。根据IETF RFC 2819协议,MAC实现了RMON(Remote Network Monitoring)计数功能。MAC内核拥有硬件加速处理单元来提高网络性能,硬件加速单元用于处理TCP/IP、UDP、ICMP等协议。通过硬件来处理帧头等信息,效果要比用一大堆软件处理要好很多。ENET外设有一个专用的DMA,此DMA用于在ENET外设和SOC之间传输数据,并且支持可编程的增强型的缓冲描述符,用以支持IEEE 1588。 I.MX6ULL内部ENET外设主要特性如下: 1)、实现了全功能的802.3规范前导码/SFD生成、帧填充、CRC生成和检查。 2)、支持零长的前导码。 3)、支持10/100M动态配置。 4)、兼容AMD远端节点电源管理的魔术帧中断检测。 5)、可以通过如下接口无缝的连接PHY芯片: ·4bit的MII接口,频率为2.5/25MHz。 ·4bit的MII-Lite接口,也就是MII接口取消掉CRS和COL这两根线,频率也是2.5/25MHz。 ·2bit的RMII接口,频率为50MHz。 6)、MAC地址可编程。 7)、多播和单播地址过滤,降低更高层的处理负担。 8)、MDIO主接口,用于管理和配置PHY设备。 …… I.MX6ULL的ENET外设内容比较多,详细的介绍请查阅《I.MX6ULL参考手册》的“Chapter 22 10/100-Mbps Ethernet MAC(ENET)”章节。我们在编写驱动的时候其实并不需要关注ENET外设的具体内容,因为这部分驱动是SOC厂商编写的,我们重点关注的是更换PHY芯片以后哪里需要调整。 69.2 PHY芯片详解 69.2.1 PHY基础知识简介 PHY是IEEE 802.3规定的一个标准模块,前面说了,SOC可以对PHY进行配置或者读取PHY相关状态,这个就需要PHY内部寄存器去实现了。PHY芯片寄存器地址空间为5位,地址 031共32个寄存器,IEEE定义了015这16个寄存器的功能,1631这16个寄存器由厂商自行实现。也就是说不管你用的哪个厂家的PHY芯片,其中015这16个寄存器是一模一样的。仅靠这16个寄存器是完全可以驱动起PHY芯片的,至少能保证基本的网络数据通信,因此Linux内核有通用PHY驱动,按道理来讲,不管你使用的哪个厂家的PHY芯片,都可以使用Linux的这个通用PHY驱动来验证网络工作是否正常。事实上在实际开发中可能会遇到一些其他的问题导致Linux内核的通用PHY驱动工作不正常,这个时候就需要驱动开发人员去调试了。但是,随着现在的PHY芯片性能越来越强大,32个寄存器可能满足不了厂商的需求,因此很多厂商采用分页技术来扩展寄存器地址空间,以求定义更多的寄存器。这些多出来的寄存器可以用于实现厂商特有的一些技术,因此Linux内核的通用PHY驱动就无法驱动这些特色功能了,这个时候就需要PHY厂商提供相应的驱动源码了,所以大家也会在Linux内核里面看到很多具体的PHY芯片驱动源码。不管你的PHY芯片有多少特色功能,按道理来讲,Linux内核的通用PHY驱动是绝对可以让你这PHY芯片实现基本的网络通信,因此大家也不用担心更换PHY芯片以后网络驱动编写是不是会很复杂。 IEEE802.3协议英文原版已经放到了开发板光盘中,路径为4、参考资料->802.3协议英文原版_2018年.pdf,打开此文档,此文档有5600页,按照SECTION进行分类,一共8个SECTION。选中“802.3-2018_SECTION2”,找到“22.2.4 Management functions”章节,此章节对PHY的前16个寄存器功能进行了规定,如图69.2.1.1所示:
图69.2.1.1 IEEE规定的前16个寄存器 关于这16个寄存器的内容协议里面也进行了详细的讲解,这里就不分析了。我们后面会以ALPHA开发板所使用的LAN8720A这个PHY为例,详细的分析一下PHY芯片的寄存器。 69.2.2 LAN8720A详解 虽然本教程讲解的是LAN8720A这颗PHY,但是前面说了,IEEE规定了PHY的前16个寄存器的功能,因此如果你所使用的板子用的其他厂家的PHY芯片,也是可以看本小节的。 1、LAN8720A简介 LAN8720A是低功耗的10/100M单以太网PHY层芯片,可应用于机顶盒、网络打印机、嵌入式通信设备、IP电话等领域。I/O引脚电压符合IEEE802.3-2005标准。LAN8720A支持通过RMII接口与以太网MAC层通信,内置10-BASE-T/100BASE-TX全双工传输模块,支持10Mbps和100Mbps。LAN8720A可以通过自协商的方式选择与目的主机最佳的连接方式(速度和双工模式)。支持HP Auto-MDIX自动翻转功能,无需更换网线即可将连接更改为直连或交叉连接。 LAN8720A的主要特点如下: · 高性能的10/100M以太网传输模块 · 支持RMII接口以减少引脚数 · 支持全双工和半双工模式 · 两个状态LED输出 · 可以使用25M晶振以降低成本 · 支持自协商模式 · 支持HP Auto-MDIX自动翻转功能 · 支持SMI串行管理接口 · 支持MAC接口 LAN8720A功能框图如图69.2.2.1所示。
图69.2.2.1 LAN8720A功能框图 2、LAN8720A中断管理 LAN8720A的器件管理接口支持非IEEE 802.3规范的中断功能。当一个中断事件发生并且相应事件的中断位使能,LAN8720A就会在nINT(14脚)产生一个低电平有效的中断信号。LAN8720A的中断系统提供两种中断模式:主中断模式和复用中断模式。主中断模式是默认中断模式,LAN8720A上电或复位后就工作在主中断模式,当模式控制/状态寄存器(十进制地址为17)的ALTINT位为0时LAN8720A工作在主模式,当ALTINT位为1时工作在复用中断模式。正点原子的ALPHA开发板虽然讲LAN8720A的中断引脚连接到了I.MX6ULL上,但是并没有使用中断功能,关于中断的具体用法可以参考LAN8720A数据手册的29~30页。 3、PHY地址设置 MAC层通过MDIO/MDC总线对PHY进行读写操作,MDIO最多可以控制32个PHY芯片,通过不同的PHY芯片地址来对不同的PHY操作。LAN8720A通过设置RXER/PHYAD0引脚来设置其PHY地址,默认情况下为0,其地址设置如表69.2.2.1所示。 RXER/PHYAD0引脚状态 PHY地址 上拉 0X01 下拉(默认) 0X00
表69.2.2.1 LAN8720A地址设置 正点原子ALPHA开发板的ENET1网络的LAN8720A上的RXER/PHYAD0引脚为默认状态(原理图上有个10K下拉,但是没有焊接),因此ENET1上的LAN8720A地址为0。ENET2网络上的LAN8720A上的RXER/PHYAD0引脚接了个10K上拉电阻,因此ENET2上的LAN8720A地址为1。 4、nINT/REFCLKO配置 nINTSEL引脚(2号引脚)用于设置nINT/REFCLKO引脚(14号引脚)的功能。nINTSEL配置如表69.2.2.2所示。。 nINTSEL引脚值 模式 nINT/REFCLKO引脚功能 nINTSEL= 0 REF_CLK Out模式 nINT/REFCLKO作为REF_CLK时钟源 nINTSEL = 1(默认) REF_CLK In模式 nINT/REFCLKO作为中断引脚
表69.2.2.2 nINTSEL配置 对于正点原子的ALPHA开发板的两个LAN8720A而言,全都工作在默认的REF_CLK In模式下。当LAN8720A工作在REF_CLK In模式时,50MHz的外部时钟信号应接到LAN8720的XTAL1/CKIN引脚(5号引脚)上,如图69.2.2.3所示:
图69.2.2.3 REF_CLK连接外部50MHz时钟信号 为了降低成本,LAN8720A可以从外部的25MHz的晶振中产生REF_CLK时钟。到要使用此功能时应工作在REF_CLK Out模式时。当工作在REF_CLO Out模式时REF_CLK的时钟源如图69.2.2.4所示。
图69.2.2.4 REF_CLK Out模式时的REF_CLK时钟源 前面说了,正点原子的ALPHA开发板工作在REF_CLK In模式下,因此需要外部50MHz时钟信号,I.MX6ULL有专用的网络时钟引脚,因此ALPHA开发板是通过I.MX6ULL的ENET1_REF_CLK和ENET2_REF_CLK这两个网络时钟引脚来为LAN8720A提供50MHz的时钟。 5、LAN8720A内部寄存器 LAN8720A的前16个寄存器满足IEEE的要求,在这里我们只介绍几个常用的寄存器,首先是BCR(Basic Control Rgsister)寄存器,地址为0,BCR寄存器各位如表69.2.2.3所示。 位 描述 类型
表 69.2.2.3 BCR寄存器 我们说的配置PHY芯片,重点就是配置BCR寄存器,由于LAN8720A是个10/100M的PHY,因此表69.2.2.3中没有体现出1000M相关的配置。但是10/100M相关的配置是和IEEE的规定完全相符的,大家可以选择一个其他的10/100M的PHY芯片对比看一下,比如NXP官方EVK开发板使用的KSZ8081。 接下来看一下BSR(Basic Status Register)寄存器,地址为1。此寄存器为PHY的状态寄存器,通过此寄存器可以获取到PHY芯片的工作状态,BSR寄存器各位如表69.2.2.4所示:
表69.2.2.4 BSR寄存器 从表69.2.2.4可以看出,和IEEE标准规定相比,LAN8720A的BSR寄存器少了几个位,这个没关系的,不管什么PHY芯片,只要它实现了的位和IEEE规定相符就行。通过读取BSR寄存器的值我们可以得到当前的连接速度、双工状态和连接状态等。 接下来看一下LAN8720A的PHY ID寄存器1和ID寄存器2,地址为2和3,后面就成为寄存器2和寄存器3。这两个寄存器都是PHY的ID寄存器。IEEE规定寄存器2和寄存器3为PHY的ID寄存器,这两个寄存器组成一个32位的唯一ID值。IEEE规定了一叫做OUI的ID组成方式,全称是Organizationally Unique Identifier,OUI一共32位,分为三部分:22位的ID+6位厂商型号ID+4位厂商版本ID,组成如图69.2.2.5所示:
图69.2.2.5 OUI组成方式
表69.2.2.5 PHY ID寄存器2 ID寄存器3如表69.2.2.6所示:
表69.2.2.6 PHY ID寄存器3 最后来看一下LAN8720A的特殊控制/状态寄存器,此寄存器地址为31,寄存器内容是LAN8720A厂商自定义的,此寄存器的各个位如表69.2.2.7所示:
表69.2.2.7 LAN8720A特殊控制/状态寄存器 特殊控制/状态寄存器中我们关心的是bit2~bit4这三位,因为通过这3位来确定连接的状态和速度,关于LAN8720A这个PHY就讲解到这里。 69.3 Linux内核网络驱动框架 69.3.1 net_device结构体 Linux内核使用net_device结构体表示一个具体的网络设备,net_device是整个网络驱动的灵魂。网络驱动的核心就是初始化net_device结构体中的各个成员变量,然后将初始化完成以后的net_device注册到Linux内核中。net_device结构体定义在include/linux/netdevice.h中,net_device是一个庞大的结构体,内容如下(有缩减):
示例代码69.3.1.1 net_device结构体
1 struct net_device {
2 char name[IFNAMSIZ];
3 struct hlist_node name_hlist;
4 char *ifalias;
5 /*
6 * I/O specific fields
7 * FIXME: Merge these and struct ifmap into one
8 */
9 unsigned long mem_end;
10 unsigned long mem_start;
11 unsigned long base_addr;
12 int irq;
13
14 atomic_t carrier_changes;
15
16 /*
17 * Some hardware also needs these fields (state,dev_list,
18 * napi_list,unreg_list,close_list) but they are not
19 * part of the usual set specified in Space.c.
20 */
21
22 unsigned long state;
23
24 struct list_head dev_list;
25 struct list_head napi_list;
26 struct list_head unreg_list;
27 struct list_head close_list;
......
60 const struct net_device_ops *netdev_ops;
61 const struct ethtool_ops *ethtool_ops;
62 #ifdef CONFIG_NET_SWITCHDEV
63 const struct swdev_ops *swdev_ops;
64 #endif
65
66 const struct header_ops *header_ops;
67
68 unsigned int flags;
......
77 unsigned char if_port;
78 unsigned char dma;
79
80 unsigned int mtu;
81 unsigned short type;
82 unsigned short hard_header_len;
83
84 unsigned short needed_headroom;
85 unsigned short needed_tailroom;
86
87 /* Interface address info. */
88 unsigned char perm_addr[MAX_ADDR_LEN];
89 unsigned char addr_assign_type;
90 unsigned char addr_len;
......
130 /*
131 * Cache lines mostly used on receive path (including eth_type_trans())
132 */
133 unsigned long last_rx;
134
135 /* Interface address info used in eth_type_trans() */
136 unsigned char *dev_addr;
137
138
139 #ifdef CONFIG_SYSFS
140 struct netdev_rx_queue *_rx;
141
142 unsigned int num_rx_queues;
143 unsigned int real_num_rx_queues;
144
145 #endif
......
158 /*
159 * Cache lines mostly used on transmit path
160 */
161 struct netdev_queue *_tx ____cacheline_aligned_in_smp;
162 unsigned int num_tx_queues;
163 unsigned int real_num_tx_queues;
164 struct Qdisc *qdisc;
165 unsigned long tx_queue_len;
166 spinlock_t tx_global_lock;
167 int watchdog_timeo;
......
173 /* These may be needed for future network-power-down code. */
174
175 /*
176 * trans_start here is expensive for high speed devices on SMP,
177 * please use netdev_queue->trans_start instead.
178 */
179 unsigned long trans_start;
......
248 struct phy_device *phydev;
249 struct lock_class_key *qdisc_tx_busylock;
250 };
下面介绍一些关键的成员变量,如下:
第2行:name是网络设备的名字。
第9行:mem_end是共享内存结束地址。
第10行:mem_start是共享内存起始地址。
第11行:base_addr是网络设备I/O地址。
第12行:irq是网络设备的中断号。
第24行:dev_list是全局网络设备列表。
第25行:napi_list是napi网络设备的列表入口。
第26行:unreg_list是注销(unregister)的网络设备列表入口。
第27行:close_list是关闭的网络设备列表入口。
第60行:netdev_ops是网络设备的操作集函数,包含了一系列的网络设备操作回调函数,类似字符设备中的file_operations,稍后会讲解netdev_ops结构体。
第61行:ethtool_ops是网络管理工具相关函数集,用户空间网络管理工具会调用此结构体中的相关函数获取网卡状态或者配置网卡。
第66行:header_ops是头部的相关操作函数集,比如创建、解析、缓冲等。
第68行:flags是网络接口标志,标志类型定义在include/uapi/linux/if.h文件中,为一个枚举类型,内容如下:
示例代码69.3.1.2 网络标志类型
1 enum net_device_flags {
2 IFF_UP = 1dev, "ipg");
86 if (IS_ERR(fep->clk_ipg)) {
87 ret = PTR_ERR(fep->clk_ipg);
88 goto failed_clk;
89 }
90
91 fep->clk_ahb = devm_clk_get(&pdev->dev, "ahb");
92 if (IS_ERR(fep->clk_ahb)) {
93 ret = PTR_ERR(fep->clk_ahb);
94 goto failed_clk;
95 }
96
97 fep->itr_clk_rate = clk_get_rate(fep->clk_ahb);
98
99 /* enet_out is optional, depends on board */
100 fep->clk_enet_out = devm_clk_get(&pdev->dev, "enet_out");
101 if (IS_ERR(fep->clk_enet_out))
102 fep->clk_enet_out = NULL;
103
104 fep->ptp_clk_on = false;
105 mutex_init(&fep->ptp_clk_mutex);
106
107 /* clk_ref is optional, depends on board */
108 fep->clk_ref = devm_clk_get(&pdev->dev, "enet_clk_ref");
109 if (IS_ERR(fep->clk_ref))
110 fep->clk_ref = NULL;
111
112 fep->bufdesc_ex = fep->quirks & FEC_QUIRK_HAS_BUFDESC_EX;
113 fep->clk_ptp = devm_clk_get(&pdev->dev, "ptp");
114 if (IS_ERR(fep->clk_ptp)) {
115 fep->clk_ptp = NULL;
116 fep->bufdesc_ex = false;
117 }
118
119 pm_runtime_enable(&pdev->dev);
120 ret = fec_enet_clk_enable(ndev, true);
121 if (ret)
122 goto failed_clk;
123
124 fep->reg_phy = devm_regulator_get(&pdev->dev, "phy");
125 if (!IS_ERR(fep->reg_phy)) {
126 ret = regulator_enable(fep->reg_phy);
127 if (ret) {
128 dev_err(&pdev->dev,
129 "Failed to enable phy regulator: %d\n", ret);
130 goto failed_regulator;
131 }
132 } else {
133 fep->reg_phy = NULL;
134 }
135
136 fec_reset_phy(pdev);
137
138 if (fep->bufdesc_ex)
139 fec_ptp_init(pdev);
140
141 ret = fec_enet_init(ndev);
142 if (ret)
143 goto failed_init;
144
145 for (i = 0; i name, ndev);
155 if (ret)
156 goto failed_irq;
157
158 fep->irq[i] = irq;
159 }
160
161 ret = of_property_read_u32(np, "fsl,wakeup_irq", &irq);
162 if (!ret && irq wake_irq = fep->irq[irq];
164 else
165 fep->wake_irq = fep->irq[0];
166
167 init_completion(&fep->mdio_done);
168 ret = fec_enet_mii_init(pdev);
169 if (ret)
170 goto failed_mii_init;
171
172 /* Carrier starts down, phylib will bring it up */
173 netif_carrier_off(ndev);
174 fec_enet_clk_enable(ndev, false);
175 pinctrl_pm_select_sleep_state(&pdev->dev);
176
177 ret = register_netdev(ndev);
178 if (ret)
179 goto failed_register;
180
181 device_init_wakeup(&ndev->dev, fep->wol_flag &
182 FEC_WOL_HAS_MAGIC_PACKET);
183
184 if (fep->bufdesc_ex && fep->ptp_clock)
185 netdev_info(ndev, "registered PHC device %d\n", fep->dev_id);
186
187 fep->rx_copybreak = COPYBREAK_DEFAULT;
188 INIT_WORK(&fep->tx_timeout_work, fec_enet_timeout_work);
189 return 0;
......
206 return ret;
207 }
第14行,使用fec_enet_get_queue_num函数来获取设备树中的“fsl,num-tx-queues”和“fsl,num-rx-queues”这两个属性值,也就是发送队列和接收队列的大小,设备树中这两个属性都设置为1。
第17行,使用alloc_etherdev_mqs函数申请net_device。
第25行,获取net_device中私有数据内存首地址,net_device中的私有数据用来存放I.MX6ULL网络设备结构体,此结构体为fec_enet_private。
第30行,接下来所有以“fep->”开头的代码行就是初始化网络设备结构体各个成员变量,结构体类型为fec_enet_privatede,这个结构体是NXP自己定义的。
第45行,获取设备树中I.MX6ULL网络外设(ENET)相关寄存器起始地址,ENET1的寄存器起始地址0X02188000,ENET2的寄存器起始地址0X20B4000。 第46行,对第45行获取到的地址做虚拟地址转换,转换后的ENET虚拟寄存器起始地址保存在fep的hwp成员中。 第57行,使用fec_enet_of_parse_stop_mode函数解析设备树中关于ENET的停止模式属性值,属性名字为“stop-mode”,我们没有用到。 第59行,从设备树查找“fsl,magic-packet”属性是否存在,如果存在的话就说明有魔术包,有魔术包的话就将fep的wol_flag成员与FEC_WOL_HAS_MAGIC_PACKET进行或运算,也就是在wol_flag中做登记,登记支持魔术包。 第62行,获取“phy-handle”属性的值,phy-handle属性指定了I.MX6ULL网络外设所对应获取PHY的设备节点。在设备树的fec1和fec2两个节点中phy-handle属性值分别为: phy-handle = ; phy-handle = ; 而ethphy0和ethphy1都定义在mdio子节点下,内容如下所示: mdio { #address-cells = ; #size-cells = ;
ethphy0: ethernet-phy@0 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = ;
};
ethphy1: ethernet-phy@1 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = ;
};
}; 可以看出ethphy0和ethphy1都是与MDIO相关的,而MDIO接口是配置PHY芯片的,通过一个MDIO接口可以配置多个PHY芯片,不同的PHY芯片通过不同的地址进行区别。正点原子ALPHA开发板中ENET的PHY地址为0X00,ENET2的PHY地址为0X01。这两个PHY地址要通过设备树告诉Linux系统,下面两行代码@后面的数值就是PHY地址: ethphy0: ethernet-phy@2 ethphy1: ethernet-phy@1 并且ethphy0和ethphy1节点中的reg属性也是PHY地址,如果我们要更换其他的网络PHY芯片,第一步就是要修改设备树中的PHY地址。 第74行,获取PHY工作模式,函数of_get_phy_mode会读取属性phy-mode的值,” phy-mode”中保存了PHY的工作方式,即PHY是RMII还是MII,IMX6ULL中的PHY工作在RMII模式,设备树描述如下所示: 第85、91、100、108和113行,分别获取时钟ipg、ahb、enet_out、enet_clk_ref和ptp,对应结构体fec_enet_private有如下成员函数 struct clk *clk_ipg; struct clk *clk_ahb; struct clk *clk_ref; struct clk *clk_enet_out; struct clk *clk_ptp; 第120行,使能时钟。 第136行,调用函数fec_reset_phy 复位PHY。 第141行,调用函数fec_enet_init()初始化enet,此函数会分配队列、申请dma、设置MAC地址,初始化net_device的netdev_ops和ethtool_ops成员,如图69.4.2.1所示:
图69.4.2.1 设置netdev_ops和ethtool_ops 从图69.4.2.1可以看出,net_device的netdev_ops和ethtool_ops成变量分别初始化成了fec_netdev_ops和fec_enet_ethtool_ops。fec_enet_init函数还会调用netif_napi_add来设置poll函数,说明NXP官方编写的此网络驱动是NAPI兼容驱动,如图69.4.2.2所示:
图69.4.2.2 netif_napi_add函数 从图69.4.2.2可以看出,通过netif_napi_add函数向网卡添加了一个napi示例,使用NAPI驱动要提供一个poll函数来轮询处理接收数据,此处的poll函数为fec_enet_rx_napi,后面分析网络数据接收处理流程的时候详细讲解此函数。 最后,fec_enet_init函数会设置IMX6ULL网络外设相关硬件寄存器。 第146行,从设备树中获取中断号。 第153行,申请中断,中断处理函数为fec_enet_interrupt,重点!后面会分析此函数。 第161行,从设备树中获取属性“fsl,wakeup_irq”的值,也就是唤醒中断, 第167行,初始化完成量completion,用于一个执行单元等待另一个执行单元执行完某事。 第168行,函数fec_enet_mii_init完成MII/RMII接口的初始化,此函数重点是图69.4.2.3中的两行代码:
图69.4.2.3 mdio读写函数 mii_bus下的read和write这两个成员变量分别是读/写PHY寄存器的操作函数,这里设置为fec_enet_mdio_read和fec_enet_mdio_write,这两个函数就是I.MX系列SOC读写PHY内部寄存器的函数,读取或者配置PHY寄存器都会通过这两个MDIO总线函数完成。fec_enet_mii_init函数最终会向Linux内核注册MIDO总线,相关代码如下所示:
示例代码69.4.2.2 fec_enet_mii_init函数注册mdio总线
1 node = of_get_child_by_name(pdev->dev.of_node, "mdio");
2 if (node) {
3 err = of_mdiobus_register(fep->mii_bus, node);
4 of_node_put(node);
5 } else {
6 err = mdiobus_register(fep->mii_bus);
7 }
示例代码代码69.4.2.2中第1行就是从设备树中获取mdio节点,如果节点存在的话就会通过of_mdiobus_register或者mdiobus_register来向内核注册MDIO总线,如果采用设备树的话就使用of_mdiobus_register来注册MDIO总线,否则就使用mdiobus_register函数。
继续回到示例代码69.4.2.1,接着分析fec_probe函数。 第173行,先调用函数netif_carrier_off通知内核,先关闭链路,phylib会打开。 第174行,调用函数fec_enet_clk_enable使能网络相关时钟。 第177行,调用函数register_netdev注册net_device! 2、MDIO总线注册 MDIO我们讲了很多次了,就是用来管理PHY芯片的,分为MDIO和MDC两根线,Linux内核专门为MDIO准备一个总线,叫做MDIO总线,采用mii_bus结构体表示,定义在include/linux/phy.h文件中,mii_bus结构体如下所示:
示例代码69.4.2.3 mii_bus结构体
1 struct mii_bus {
2 const char *name;
3 char id[MII_BUS_ID_SIZE];
4 void *priv;
5 int (*read)(struct mii_bus *bus, int phy_id, int regnum);
6 int (*write)(struct mii_bus *bus, int phy_id, int regnum,
u16 val);
7 int (*reset)(struct mii_bus *bus);
8
9 /*
10 * A lock to ensure that only one thing can read/write
11 * the MDIO bus at a time
12 */
13 struct mutex mdio_lock;
14
15 struct device *parent;
16 enum {
17 MDIOBUS_ALLOCATED = 1,
18 MDIOBUS_REGISTERED,
19 MDIOBUS_UNREGISTERED,
20 MDIOBUS_RELEASED,
21 } state;
22 struct device dev;
23
24 /* list of all PHYs on bus */
25 struct phy_device *phy_map[PHY_MAX_ADDR];
26
27 /* PHY addresses to be ignored when probing */
28 u32 phy_mask;
29
30 /*
31 * Pointer to an array of interrupts, each PHY's
32 * interrupt at the index matching its address
33 */
34 int *irq;
35 };
重点是第5、6两行的read和write函数,这两个函数就是读/些PHY芯片的操作函数,不同的SOC其MDIO主控部分是不一样的,因此需要驱动编写人员去编写。我们前面在分析fec_probe函数的时候已经讲过了,fec_probe函数会调用fec_enet_mii_init函数完成MII接口的初始化,其中就包括初始化mii_bus下的read和write这两个函数。最终通过of_mdiobus_register或者mdiobus_register函数将初始化以后的mii_bus注册到Linux内核,of_mdiobus_register函数其实最终也是调用的mdiobus_register函数来完成mii_bus注册的。of_mdiobus_register函数内容如下(限于篇幅,有省略):
示例代码69.4.2.4 of_mdiobus_register函数
1 int of_mdiobus_register(struct mii_bus *mdio, struct device_node *np)
2 {
3 struct device_node *child;
4 const __be32 *paddr;
5 bool scanphys = false;
6 int addr, rc, i;
7
8 /* Mask out all PHYs from auto probing. Instead the PHYs listed
9 * in the device tree are populated after the bus has been
*registered */
10 mdio->phy_mask = ~0;
11
12 /* Clear all the IRQ properties */
13 if (mdio->irq)
14 for (i=0; iirq[i] = PHY_POLL;
16
17 mdio->dev.of_node = np;
18
19 /* Register the MDIO bus */
20 rc = mdiobus_register(mdio);
21 if (rc)
22 return rc;
23
24 /* Loop over the child nodes and register a phy_device for each
one */
25 for_each_available_child_of_node(np, child) {
26 addr = of_mdio_parse_addr(&mdio->dev, child);
27 if (addr 0) {
21 phy->irq = rc;
22 if (mdio->irq)
23 mdio->irq[addr] = rc;
24 } else {
25 if (mdio->irq)
26 phy->irq = mdio->irq[addr];
27 }
28
29 /* Associate the OF node with the device structure so it
30 * can be looked up later */
31 of_node_get(child);
32 phy->dev.of_node = child;
33
34 /* All data is now stored in the phy struct;
35 * register it */
36 rc = phy_device_register(phy);
37 if (rc) {
38 phy_device_free(phy);
39 of_node_put(child);
40 return 1;
41 }
42
43 dev_dbg(&mdio->dev, "registered phy %s at address %i\n",
44 child->name, addr);
45
46 return 0;
47 }
第9行,使用函数of_device_is_compatible检查PHY节点的compatible属性是否为“ethernet-phy-ieee802.3-c45”,如果是的话要做其他的处理,本章节我们设置的compatible属性为“ethernet-phy-ieee802.3-c22”。
第15行,调用get_phy_device函数获取PHY设备,此函数里面会调用phy_device_create来创建一个phy_device设备并返回。 第19行,获取PHY芯片的中断信息,本章节并未用到。 第36行,调用phy_device_register函数向Linux内核注册PHY设备。 从上面的分析可以看出,向Linux内核注册MDIO总线的时候也会同时向Linux内核注册PHY设备,流程如图69.4.2.3所示:
图69.4.2.3 MDIO总线注册流程 注册MIDO总线的时候会从设备树中查找PHY设备,然后通过phy_device_register函数向内核注册PHY设备,接下来我们就来学习一下PHY子系统。 3、fec_drv_remove函数简析 卸载I.MX6ULL网络驱动的时候fec_drv_remove函数就会执行,函数内容如下所示:
示例代码69.4.2.6 fec_drv_remove函数
1 static int fec_drv_remove(struct platform_device *pdev)
2 {
3 struct net_device *ndev = platform_get_drvdata(pdev);
4 struct fec_enet_private *fep = netdev_priv(ndev);
5
6 cancel_delayed_work_sync(&fep->time_keep);
7 cancel_work_sync(&fep->tx_timeout_work);
8 unregister_netdev(ndev);
9 fec_enet_mii_remove(fep);
10 if (fep->reg_phy)
11 regulator_disable(fep->reg_phy);
12 if (fep->ptp_clock)
13 ptp_clock_unregister(fep->ptp_clock);
14 of_node_put(fep->phy_node);
15 free_netdev(ndev);
16
17 return 0;
18 }
第8行,调用unregister_netdev函数注销前面注册的net_device。
第9行,调用fec_enet_mii_remove函数来移除掉MDIO总线相关的内容,此函数会调用mdiobus_unregister来注销掉mii_bus,并且通过函数mdiobus_free释放掉mii_bus
第15行,调用free_netdev函数释放掉前面申请的net_device。
69.4.3 fec_netdev_ops操作集 fec_probe函数设置了网卡驱动的net_dev_ops操作集为fec_netdev_ops,fec_netdev_ops内容如下:
示例代码69.4.3.1 fec_netdev_ops操作集
1 static const struct net_device_ops fec_netdev_ops = {
2 .ndo_open = fec_enet_open,
3 .ndo_stop = fec_enet_close,
4 .ndo_start_xmit = fec_enet_start_xmit,
5 .ndo_select_queue = fec_enet_select_queue,
6 .ndo_set_rx_mode = set_multicast_list,
7 .ndo_change_mtu = eth_change_mtu,
8 .ndo_validate_addr = eth_validate_addr,
9 .ndo_tx_timeout = fec_timeout,
10 .ndo_set_mac_address = fec_set_mac_address,
11 .ndo_do_ioctl = fec_enet_ioctl,
12 #ifdef CONFIG_NET_POLL_CONTROLLER
13 .ndo_poll_controller = fec_poll_controller,
14 #endif
15 .ndo_set_features = fec_set_features,
16 };
1、fec_enet_open函数简析
打开一个网卡的时候fec_enet_open函数就会执行,函数源码如下所示(限于篇幅原因,有省略):
示例代码69.4.3.2 fec_enet_open函数
1 static int fec_enet_open(struct net_device *ndev)
2 {
3 struct fec_enet_private *fep = netdev_priv(ndev);
4 const struct platform_device_id *id_entry =
5 platform_get_device_id(fep->pdev);
6 int ret;
7
8 pinctrl_pm_select_default_state(&fep->pdev->dev);
9 ret = fec_enet_clk_enable(ndev, true);
10 if (ret)
11 return ret;
12
13 /* I should reset the ring buffers here, but I don't yet know
14 * a simple way to do that.
15 */
16
17 ret = fec_enet_alloc_buffers(ndev);
18 if (ret)
19 goto err_enet_alloc;
20
21 /* Init MAC prior to mii bus probe */
22 fec_restart(ndev);
23
24 /* Probe and connect to PHY when open the interface */
25 ret = fec_enet_mii_probe(ndev);
26 if (ret)
27 goto err_enet_mii_probe;
28
29 napi_enable(&fep->napi);
30 phy_start(fep->phy_dev);
31 netif_tx_start_all_queues(ndev);
32
......
47
48 return 0;
49
50 err_enet_mii_probe:
51 fec_enet_free_buffers(ndev);
52 err_enet_alloc:
53 fep->miibus_up_failed = true;
54 if (!fep->mii_bus_share)
55 pinctrl_pm_select_sleep_state(&fep->pdev->dev);
56 return ret;
57 }
第9行,调用fec_enet_clk_enable函数使能enet时钟。 第17行,调用fec_enet_alloc_buffers函数申请环形缓冲区buffer,此函数里面会调用fec_enet_alloc_rxq_buffers和fec_enet_alloc_txq_buffers这两个函数分别实现发送队列和接收队列缓冲区的申请。 第22行,重启网络,一般连接状态改变、传输超时或者配置网络的时候都会调用fec_restart函数。 第25行,打开网卡的时候调用fec_enet_mii_probe函数来探测并连接对应的PHY设备。 第29行,调用napi_enable函数使能NAPI调度。 第30行,调用phy_start函数开启PHY设备。 第31行,调用netif_tx_start_all_queues函数来激活发送队列。 2、fec_enet_close函数简析 关闭网卡的时候fec_enet_close函数就会执行,函数内容如下:
示例代码69.4.3.3 fec_enet_close函数
1 static int fec_enet_close(struct net_device *ndev)
2 {
3 struct fec_enet_private *fep = netdev_priv(ndev);
4
5 phy_stop(fep->phy_dev);
6
7 if (netif_device_present(ndev)) {
8 napi_disable(&fep->napi);
9 netif_tx_disable(ndev);
10 fec_stop(ndev);
11 }
12
13 phy_disconnect(fep->phy_dev);
14 fep->phy_dev = NULL;
15
16 fec_enet_clk_enable(ndev, false);
17 pm_qos_remove_request(&fep->pm_qos_req);
18 pinctrl_pm_select_sleep_state(&fep->pdev->dev);
19 pm_runtime_put_sync_suspend(ndev->dev.parent);
20 fec_enet_free_buffers(ndev);
21
22 return 0;
23 }
第5行,调用phy_stop函数停止PHY设备。
第8行,调用napi_disable函数关闭NAPI调度。
第9行,调用netif_tx_disable函数关闭NAPI的发送队列。
第10行,调用fec_stop函数关闭I.MX6ULL的ENET外设。
第13行,调用phy_disconnect函数断开与PHY设备的连接。
第16行,调用fec_enet_clk_enable函数关闭ENET外设时钟。
第20行,调用fec_enet_free_buffers函数释放发送和接收的环形缓冲区内存。
3、fec_enet_start_xmit函数简析 I.MX6ULL的网络数据发送是通过fec_enet_start_xmit函数来完成的,这个函数将上层传递过来的sk_buff中的数据通过硬件发送出去,函数源码如下:
示例代码69.4.3.4 fec_enet_start_xmit函数
1 static netdev_tx_t fec_enet_start_xmit(struct sk_buff *skb,
struct net_device *ndev)
2 {
3 struct fec_enet_private *fep = netdev_priv(ndev);
4 int entries_free;
5 unsigned short queue;
6 struct fec_enet_priv_tx_q *txq;
7 struct netdev_queue *nq;
8 int ret;
9
10 queue = skb_get_queue_mapping(skb);
11 txq = fep->tx_queue[queue];
12 nq = netdev_get_tx_queue(ndev, queue);
13
14 if (skb_is_gso(skb))
15 ret = fec_enet_txq_submit_tso(txq, skb, ndev);
16 else
17 ret = fec_enet_txq_submit_skb(txq, skb, ndev);
18 if (ret)
19 return ret;
20
21 entries_free = fec_enet_get_free_txdesc_num(fep, txq);
22 if (entries_free tx_stop_threshold)
23 netif_tx_stop_queue(nq);
24
25 return NETDEV_TX_OK;
26 }
此函数的参数第一个参数skb就是上层应用传递下来的要发送的网络数据,第二个参数ndev就是要发送数据的设备。
第14行,判断skb是否为GSO(Generic Segmentation Offload),如果是GSO的话就通过fec_enet_txq_submit_tso函数发送,如果不是的话就通过fec_enet_txq_submit_skb发送。这里简单讲一下TSO和GSO:
TSO:全称是TCP Segmentation Offload,利用网卡对大数据包进行自动分段处理,降低CPU负载。
GSO:全称是Generic Segmentation Offload,在发送数据之前先检查一下网卡是否支持TSO,如果支持的话就让网卡分段,不过不支持的话就由协议栈进行分段处理,分段处理完成以后再交给网卡去发送。
第21行,通过fec_enet_get_free_txdesc_num函数获取剩余的发送描述符数量。
第23行,如果剩余的发送描述符的数量小于设置的阈值(tx_stop_threshold)的话就调用函数netif_tx_stop_queu来暂停发送,通过暂停发送来通知应用层停止向网络发送skb,发送中断中会重新开启的。
4、fec_enet_interrupt中断服务函数简析 前面说了I.MX6ULL的网络数据接收采用NAPI框架,所以肯定要用到中断。fec_probe函数会初始化网络中断,中断服务函数为fec_enet_interrupt,函数内容如下:
示例代码69.4.3.5 fec_enet_interrupt函数
1 static irqreturn_t fec_enet_interrupt(int irq, void *dev_id)
2 {
3 struct net_device *ndev = dev_id;
4 struct fec_enet_private *fep = netdev_priv(ndev);
5 uint int_events;
6 irqreturn_t ret = IRQ_NONE;
7
8 int_events = readl(fep->hwp + FEC_IEVENT);
9 writel(int_events, fep->hwp + FEC_IEVENT);
10 fec_enet_collect_events(fep, int_events);
11
12 if ((fep->work_tx || fep->work_rx) && fep->link) {
13 ret = IRQ_HANDLED;
14
15 if (napi_schedule_prep(&fep->napi)) {
16 /* Disable the NAPI interrupts */
17 writel(FEC_ENET_MII, fep->hwp + FEC_IMASK);
18 __napi_schedule(&fep->napi);
19 }
20 }
21
22 if (int_events & FEC_ENET_MII) {
23 ret = IRQ_HANDLED;
24 complete(&fep->mdio_done);
25 }
26
27 if (fep->ptp_clock)
28 fec_ptp_check_pps_event(fep);
29
30 return ret;
31 }
可以看出中断服务函数非常短!而且也没有见到有关数据接收的处理过程,那是因为I.MX6ULL的网络驱动使用了NAPI,具体的网络数据收发是在NAPI的poll函数中完成的,中断里面只需要进行napi调度即可,这个就是中断的上半部和下半部处理机制。
第8行,读取NENT的中断状态寄存器EIR,获取中断状态, 第9行,将第8行获取到的中断状态值又写入EIR寄存器,用于清除中断状态寄存器。 第10行,调用fec_enet_collect_events函数统计中断信息,也就是统计都发生了哪些中断。fep中成员变量work_tx和work_rx的bit0、bit1和bit2用来做不同的标记,work_rx的bit2表示接收到数据帧,work_tx的bit2表示发送完数据帧。 第15行,调用napi_schedule_prep函数检查NAPI是否可以进行调度。 第17行,如果使能了相关中断就要先关闭这些中断,向EIMR寄存器的bit23写1即可关闭相关中断。 第18行,调用__napi_schedule函数来启动NAPI调度,这个时候napi的poll函数就会执行,在本网络驱动中就是fec_enet_rx_napi函数。 5、fec_enet_interrupt中断服务函数简析 fec_enet_init函数初始化网络的时候会调用netif_napi_add来设置NAPI的poll函数为fec_enet_rx_napi,函数内容如下:
示例代码69.4.3.6 fec_enet_rx_napi轮询函数
1 static int fec_enet_rx_napi(struct napi_struct *napi, int budget)
2 {
3 struct net_device *ndev = napi->dev;
4 struct fec_enet_private *fep = netdev_priv(ndev);
5 int pkts;
6
7 pkts = fec_enet_rx(ndev, budget);
8
9 fec_enet_tx(ndev);
10
11 if (pkts hwp + FEC_IMASK);
14 }
15 return pkts;
16 }
第7行,调用fec_enet_rx函数进行真正的数据接收。
第9行,调用fec_enet_tx函数进行数据发送。
第12行,调用napi_complete函数来宣布一次轮询结束,
第13行,设置ENET的EIMR寄存器,重新使能中断。
69.4.3 Linux内核PHY子系统与MDIO总线简析 上一小节在讲解MDIO总线的时候讲过,注册MDIO总线的时候也会向内核注册PHY设备,本节我们就来简单了解一下PHY子系统。PHY子系统就是用于PHY设备相关内容的,分为PHY设备和PHY驱动,和platform总线一样,PHY子系统也是一个设备、总线和驱动模型。 1、PHY设备 首先看一下PHY设备,Linux内核使用phy_device结构体来表示PHY设备,结构体定义在include/linux/phy.h,结构体内容如下:
示例代码69.4.3.1 phy_device结构体
1 struct phy_device {
2 /* Information about the PHY type */
3 /* And management functions */
4 struct phy_driver *drv; /* PHY设备驱动 */
5 struct mii_bus *bus; /* 对应的MII总线 */
6 struct device dev; /* 设备文件 */
7 u32 phy_id; /* PHY ID */
8
9 struct phy_c45_device_ids c45_ids;
10 bool is_c45;
11 bool is_internal;
12 bool has_fixups;
13 bool suspended;
14
15 enum phy_state state; /* PHY状态 */
16 u32 dev_flags;
17 phy_interface_t interface; /* PHY接口 */
18
19 /* Bus address of the PHY (0-31) */
20 int addr; /* PHY地址(0~31) */
21
22 /*
23 * forced speed & duplex (no autoneg)
24 * partner speed & duplex & pause (autoneg)
25 */
26 int speed; /* 速度 */
27 int duplex; /* 双共模式 */
28 int pause;
29 int asym_pause;
30
31 /* The most recently read link state */
32 int link;
33
34 /* Enabled Interrupts */
35 u32 interrupts; /* 中断使能标志 */
36
37 /* Union of PHY and Attached devices' supported modes */
38 /* See mii.h for more info */
39 u32 supported;
40 u32 advertising;
41 u32 lp_advertising;
42 int autoneg;
43 int link_timeout;
44
45 /*
46 * Interrupt number for this PHY
47 * -1 means no interrupt
48 */
49 int irq; /* 中断号 */
50
51 /* private data pointer */
52 /* For use by PHYs to maintain extra state */
53 void *priv; /* 私有数据 */
54
55 /* Interrupt and Polling infrastructure */
56 struct work_struct phy_queue;
57 struct delayed_work state_queue;
58 atomic_t irq_disable;
59 struct mutex lock;
60 struct net_device *attached_dev; /* PHY芯片对应的网络设备 */
61 void (*adjust_link)(struct net_device *dev);
62 };
一个PHY设备对应一个phy_device实例,然后需要向Linux内核注册这个实例。使用phy_device_register函数完成PHY设备的注册,函数原型如下:
int phy_device_register(struct phy_device *phy) 函数参数和返回值含义如下: phy:需要注册的PHY设备。 返回值:0 成功,负值 失败。 PHY设备的注册过程一般是先调用get_phy_device函数获取PHY设备,此函数内容如下:
示例代码69.4.3.2 get_phy_device函数
1 struct phy_device *get_phy_device(struct mii_bus *bus, int addr,
bool is_c45)
2 {
3 struct phy_c45_device_ids c45_ids = {0};
4 u32 phy_id = 0;
5 int r;
6
7 r = get_phy_id(bus, addr, &phy_id, is_c45, &c45_ids);
8 if (r)
9 return ERR_PTR(r);
10
11 /* If the phy_id is mostly Fs, there is no device there */
12 if ((phy_id & 0x1fffffff) == 0x1fffffff)
13 return NULL;
14
15 return phy_device_create(bus, addr, phy_id, is_c45, &c45_ids);
16 }
第7行,调用get_phy_id函数获取PHY ID,也就是读取PHY芯片的那两个ID寄存器,得到PHY芯片ID信息。
第15行,调用phy_device_create函数创建phy_device,此函数先申请phy_device内存,然后初始化phy_device的各个结构体成员,最终返回创建好的phy_device。phy_device_register函数注册的就是这个创建好的phy_device。
2、PHY驱动
PHY驱动使用结构体phy_driver表示,结构体也定义在include/linux/phy.h文件中,结构体内容如下(为了缩小篇幅,省略了注释部分):
示例代码69.4.3.3 phy_driver结构体
1 struct phy_driver {
2 u32 phy_id; /* PHY ID */
3 char *name;
4 unsigned int phy_id_mask; /* PHY ID掩码 */
5 u32 features;
6 u32 flags;
7 const void *driver_data;
8
9 int (*soft_reset)(struct phy_device *phydev);
10 int (*config_init)(struct phy_device *phydev);
11 int (*probe)(struct phy_device *phydev);
12 int (*suspend)(struct phy_device *phydev);
13 int (*resume)(struct phy_device *phydev);
14 int (*config_aneg)(struct phy_device *phydev);
15 int (*aneg_done)(struct phy_device *phydev);
16 int (*read_status)(struct phy_device *phydev);
17 int (*ack_interrupt)(struct phy_device *phydev);
18 int (*config_intr)(struct phy_device *phydev);
19 int (*did_interrupt)(struct phy_device *phydev);
20 void (*remove)(struct phy_device *phydev);
21 int (*match_phy_device)(struct phy_device *phydev);
22 int (*ts_info)(struct phy_device *phydev,
struct ethtool_ts_info *ti);
23 int (*hwtstamp)(struct phy_device *phydev, struct ifreq *ifr);
24 bool (*rxtstamp)(struct phy_device *dev, struct sk_buff *skb,
int type);
25 void (*txtstamp)(struct phy_device *dev, struct sk_buff *skb,
int type);
26 int (*set_wol)(struct phy_device *dev,
struct ethtool_wolinfo *wol);
27 void (*get_wol)(struct phy_device *dev,
struct ethtool_wolinfo *wol);
28 void (*link_change_notify)(struct phy_device *dev);
29 int (*read_mmd_indirect)(struct phy_device *dev, int ptrad,
30 int devnum, int regnum);
31 void (*write_mmd_indirect)(struct phy_device *dev, int ptrad,
32 int devnum, int regnum, u32 val);
33 int (*module_info)(struct phy_device *dev,
34 struct ethtool_modinfo *modinfo);
35 int (*module_eeprom)(struct phy_device *dev,
36 struct ethtool_eeprom *ee, u8 *data);
37
38 struct device_driver driver;
39 };
可以看出,phy_driver重点是大量的函数,编写PHY驱动的主要工作就是实现这些函数,但是不一定全部实现,稍后我们会简单分析一下Linux内核通用PHY驱动。
①、注册PHY驱动
phy_driver结构体初始化完成以后,就需要向Linux内核注册,PHY驱动的注册使用phy_driver_register函数,注册phy驱动时候会设置驱动的总线为mdio_bus_type,也就是MDIO总线,关于MDIO总线稍后会讲解,函数原型如下:
int phy_driver_register(struct phy_driver *new_driver) 函数参数和返回值含义如下: new_driver:需要注册的PHY驱动。 返回值:0 成功,负值 失败。 ②、连续注册多个PHY驱动 一个厂家会生产多种PHY芯片,这些PHY芯片内部差别一般不大,如果一个个的去注册驱动将会导致一堆重复的驱动文件,因此Linux内核提供了一个连续注册多个PHY驱动的函数phy_drivers_register。首先准备一个phy_driver数组,一个数组元素就表示一个PHY芯片的驱动,然后调用phy_drivers_register一次性注册整个数组中的所有驱动,函数原型如下: int phy_drivers_register(struct phy_driver *new_driver, int n) 函数参数和返回值含义如下: new_driver:需要注册的多个PHY驱动数组。 n:要注册的驱动数量。 返回值:0 成功,负值 失败。 ②、卸载PHY驱动 卸载PHY驱动的话使用phy_driver_unregister函数,函数原型如下: void phy_driver_unregister(struct phy_driver *drv) 函数参数和返回值含义如下: new_driver:需要卸载的PHY驱动。 返回值:无。 3、MDIO总线 前面说了,PHY子系统也是遵循设备、总线、驱动模型的,设备和驱动就是phy_device和phy_driver。总线就是MDIO总线,因为PHY芯片是通过MIDO接口来管理的,MDIO总线最主要的工作就是匹配PHY设备和PHY驱动。在文件drivers/net/phy/mdio_bus.c中有如下定义:
示例代码69.4.3.4 mdio总线
1 struct bus_type mdio_bus_type = {
2 .name = "mdio_bus",
3 .match = mdio_bus_match,
4 .pm = MDIO_BUS_PM_OPS,
5 .dev_groups = mdio_dev_groups,
6 };
示例代码69.4.3.4定义了一个名为“mdio_bus_type”的总线,这个就是MDIO总线,总线的名字为“mdio_bus”,重点是总线的匹配函数为mdio_bus_match。此函数内容如下:
示例代码69.4.3.5 mdio_bus_match匹配函数
1 static int mdio_bus_match(struct device *dev,
struct device_driver *drv)
2 {
3 struct phy_device *phydev = to_phy_device(dev);
4 struct phy_driver *phydrv = to_phy_driver(drv);
5
6 if (of_driver_match_device(dev, drv))
7 return 1;
8
9 if (phydrv->match_phy_device)
10 return phydrv->match_phy_device(phydev);
11
12 return (phydrv->phy_id & phydrv->phy_id_mask) ==
13 (phydev->phy_id & phydrv->phy_id_mask);
14 }
第6行,采用设备树的话先尝试使用of_driver_match_device来对设备和驱动进行匹配,也就是检查compatible属性值与匹配表of_match_table里面的内容是否一致。但是对于本章教程而言,并不是通过of_driver_match_device来完成PHY驱动和设备匹配的。
第9、10行,检查PHY驱动有没有提供匹配函数match_phy_device,如果有的话就直接调用PHY驱动提供的匹配函数完成与设备的匹配。
第12、13行,如果上面两个匹配方法都无效的话就使用最后一种,phy_driver里面有两个成员变量phy_id和phy_id_mask,表示此驱动所匹配的PHY芯片ID以及ID掩码,PHY驱动编写人员需要给这两个成员变量赋值。phy_device也有一个phy_id成员变量,表示此PHY芯片的ID,phy_device里面的phy_id是在注册PHY设备的时候调用get_phy_id函数直接读取PHY芯片内部ID寄存器得到的!很明显PHY驱动和PHY设备中的ID要一样,这样才能匹配起来。所以最后一种方法就是对比PHY驱动和PHY设备中的phy_id是否一致,这里需要与PHY驱动里面的phy_id_mask进行与运算,如果结果一致的话就说明驱动和设备匹配。
如果PHY设备和PHY驱动匹配,那么就使用指定的PHY驱动,如果不匹配的话就使用Linux内核自带的通用PHY驱动。
3、通用PHY驱动
前面多次提到Linux内核已经集成了通用PHY驱动,通用PHY驱动名字为“Generic PHY”,打开drivers/net/phy/phy_device.c,找到phy_init函数,内容如下:
示例代码69.4.3.6 phy_init函数
1 static int __init phy_init(void)
2 {
3 int rc;
4
5 rc = mdio_bus_init();
6 if (rc)
7 return rc;
8
9 rc = phy_drivers_register(genphy_driver,
10 ARRAY_SIZE(genphy_driver));
11 if (rc)
12 mdio_bus_exit();
13
14 return rc;
15 }
phy_init是整个PHY子系统的入口函数,第9行会调用phy_drivers_register函数向内核直接注册一个通用PHY驱动:genphy_driver,也就是通用PHY驱动,也就是说Linux系统启动以后默认就已经存在了通用PHY驱动。
genphy_driver是一个数组,有两个数组元素,表示有两个通用的PHY驱动,一个是针对10/100/1000M网络的,一个是针对10G网络的。genphy_driver定义在drivers/net/phy/phy_device.c里面,内容如下:
示例代码69.4.3.7 通用PHY驱动
1 static struct phy_driver genphy_driver[] = {
2 {
3 .phy_id = 0xffffffff,
4 .phy_id_mask = 0xffffffff,
5 .name = "Generic PHY",
6 .soft_reset = genphy_soft_reset,
7 .config_init = genphy_config_init,
8 .features = PHY_GBIT_FEATURES | SUPPORTED_MII |
9 SUPPORTED_AUI | SUPPORTED_FIBRE |
10 SUPPORTED_BNC,
11 .config_aneg = genphy_config_aneg,
12 .aneg_done = genphy_aneg_done,
13 .read_status = genphy_read_status,
14 .suspend = genphy_suspend,
15 .resume = genphy_resume,
16 .driver = { .owner = THIS_MODULE, },
17 }, {
18 .phy_id = 0xffffffff,
19 .phy_id_mask = 0xffffffff,
20 .name = "Generic 10G PHY",
21 .soft_reset = gen10g_soft_reset,
22 .config_init = gen10g_config_init,
23 .features = 0,
24 .config_aneg = gen10g_config_aneg,
25 .read_status = gen10g_read_status,
26 .suspend = gen10g_suspend,
27 .resume = gen10g_resume,
28 .driver = {.owner = THIS_MODULE, },
29 } };
genphy_driver数组有两个元素,genphy_driver[0]为10/100/1000M的PHY驱动,名字为“Generic PHY”,genphy_driver[1]为10G的PHY驱动,名字为“Generic 10G PHY”。注意,很多另外编写的PHY驱动也会用到通用PHY驱动的一些函数,比如正点原子ALPHA开发板所用的LAN8720A是SMSC公司的产品,此公司针对自家的所有PHY芯片编写了一个驱动文件smsc.c,这驱动文件里面用到了大量的通用PHY驱动相关函数。
4、LAN8720A驱动
最后我们来看一下LAN8720A的Linux驱动,LAN8720A的驱动文件为drivers/net/phy/smsc.c,这个文件是SMSC针对自家的一些PHY芯片编写的驱动文件,其中就包含了LAN8720A这个PHY芯片。默认情况下,LAN8720A这个驱动是没有打开的,我们需要配置linux内核,打开此驱动选项,配置路径如下:
-> Device Drivers -> Network device support -> PHY Device support and infrastructure -> Drivers for SMSC PHYs 配置界面如图69.4.3.1所示:
图69.4.3.1 使能LAN8720A驱动 选中图69.4.3.1中的“Drivers for SMSC PHYs”,然后编译内核即可,这个在37.4.3小节讲解linux移植的时候都已经说过了。 打开smsc.c,找到如下所示内容(限于篇幅,有删减):
示例代码69.4.3.8 通用PHY驱动
1 static struct phy_driver smsc_phy_driver[] = {
2 {
3 .phy_id = 0x0007c0a0, /* OUI=0x00800f, Model#=0x0a */
4 .phy_id_mask = 0xfffffff0,
5 .name = "SMSC LAN83C185",
......
24 .driver = { .owner = THIS_MODULE, }
25 }, {
26 .phy_id = 0x0007c0b0, /* OUI=0x00800f, Model#=0x0b */
27 .phy_id_mask = 0xfffffff0,
28 .name = "SMSC LAN8187",
......
47 .driver = { .owner = THIS_MODULE, }
48 }, {
49 .phy_id = 0x0007c0c0, /* OUI=0x00800f, Model#=0x0c */
50 .phy_id_mask = 0xfffffff0,
51 .name = "SMSC LAN8700",
......
70 .driver = { .owner = THIS_MODULE, }
71 }, {
72 .phy_id = 0x0007c0d0, /* OUI=0x00800f, Model#=0x0d */
73 .phy_id_mask = 0xfffffff0,
74 .name = "SMSC LAN911x Internal PHY",
......
92 .driver = { .owner = THIS_MODULE, }
93 }, {
94 .phy_id = 0x0007c0f0, /* OUI=0x00800f, Model#=0x0f */
95 .phy_id_mask = 0xfffffff0,
96 .name = "SMSC LAN8710/LAN8720",
97
98 .features = (PHY_BASIC_FEATURES | SUPPORTED_Pause
99 | SUPPORTED_Asym_Pause),
100 .flags = PHY_HAS_INTERRUPT | PHY_HAS_MAGICANEG,
101
102 /* basic functions */
103 .config_aneg = genphy_config_aneg,
104 .read_status = lan87xx_read_status,
105 .config_init = smsc_phy_config_init,
106 .soft_reset = smsc_phy_reset,
107
108 /* IRQ related */
109 .ack_interrupt = smsc_phy_ack_interrupt,
110 .config_intr = smsc_phy_config_intr,
111
112 .suspend = genphy_suspend,
113 .resume = genphy_resume,
114
115 .driver = { .owner = THIS_MODULE, }
116 } };
117
118 module_phy_driver(smsc_phy_driver);
从示例代码69.4.3.8可以看出,smsc_phy_driver还是支持了不少SMSC家的PHY芯片,比如LAN83C185、LAN8187、LAN8700等等,当然了,肯定也包括了LAN8720系列,第93~116行就是LAN8710/LAN8720系列PHY驱动。
第94行,PHY ID为0X0007C0F0。
第95行,PHY的ID掩码为0XFFFFFFF0,也就是前28位有效,在进行匹配的时候只需要比较前28位,第4位不用比较。
第74行,驱动名字为“SMSC LAN8710/LAN8720”,系统启动以后,打开网卡就会提示当前PHY驱动名字为“SMSC LAN8710/LAN8720”。 最后,第118行使用module_phy_driver(本质是一个宏)来完成smsc_phy_driver的注册。 此驱动里面的成员函数有一些是SMSC自己编写的,有一些是直接用的通用PHY驱动的,比如第103行的genphy_config_aneg、第112行的genphy_suspend等。 69.5 网络驱动实验测试 69.5.1 LAN8720 PHY驱动测试 首先肯定是驱动修改,这个已经在37.4.3小节做了详细的讲解,参考修改即可。系统启动以后就会打印出当前PHY驱动名字为“SMSC LAN8710/LAN8720”,如图69.5.1.1所示:
图69.5.1.1 网络PHY驱动信息 从图69.5.1.1可以看出,此时PHY驱动使用的是“SMSC LAN8710/8720”,当我们使用ifconfig命令打开网卡的时候也会提示当前PHY驱动名字。至于网络的测试就很简单了,大家可以ping一下主机或者ubuntu的地址,如果能ping通就说明网络工作正常。 69.5.2 通用PHY驱动测试 前面我们说了,按道理来讲通用PHY驱动肯定是可以驱动起任何的PHY芯片的,但是有时候社会就是这样的不讲道理,笔者一开始就是使用通用PHY驱动来驱动LAN8720,结果现实给年轻的笔者上了一课,失败了!经过搜索发现,在I.MX6ULL下使用LAN8720的话需要ENET1_TX_CLK和ENET2_TX_CLK这两个引脚的配置,也就是需要修改fec_main.c文件中的fec_probe函数,具体修改内容参看37.4.3小节。修改完成以后就可以尝试使用通用PHY驱动,不保证会成功!首先配置Linux内核,关闭自带的LAN8720驱动即可,然后重新编译linux内核,使用新的linux内核启动即可。 系统启动以后就会使用通用PHY驱动,会输出如图69.5.1.2所示信息:
图69.5.2.1 通用PHY驱动信息 从图69.5.2.1可以看出,此时网卡驱动名字为“Generic PHY”,说明使用的是通用PHY驱动。 69.5.3 DHCP功能配置 我们以前做实验的时候,开发板的IP地址都是手动设置的,但是自己设置IP地址很容易和网络中其他设备的IP地址冲突,最好的办法就是让路由器自动分配IP地址,通过路由器分配到的IP地址肯定是在本网络中独一无二的。我们需要通过udhcpc命令来实现从路由器动态申请IP地址,udhcpc命令已经集成到了busybox中,所以不需要我们另外移植。 另外,我们还需要一个文件,否则直接使用udhcpc申请IP地址会失败。在busybox源码中找到examples/udhcp/simple.script,将其拷贝到开发板的/usr/share/udhcpc目录下(如果没有的话请自行创建此目录),拷贝完成以后将根文件系统下的simple.script并且重命名为default.script,命令如下: cd busybox-1.29.0/examples/udhcp cp simple.script /home/zuozhongkai/linux/nfs/rootfs/usr/share/udhcpc/default.script //拷贝并重命名 完成以后就可以使用udhcpc来给指定的网卡申请IP地址了,通过“-i”参数来指定给哪个网卡申请IP地址,“-i”参数后面紧跟要申请IP地址的网卡名字。比如,这里我们以正点原子的ALPHA开发板为例,给eth1这个网卡申请IP地址,命令如下: ifconfig eth1 up //打开eth1网卡 udhcpc -i eth1 //为eth1网卡申请IP地址 申请过程如图69.5.3.1所示:
图69.5.3.1 udhcpc申请IP地址过程 从图69.5.3.1可以看出,eth1申请到的IP地址是192.168.1.156,并且也修改了/etc/resolv.conf文件中DNS服务器地址。可以输入“ifconfig”命令来查看eth1网卡的详细信息,这里就不演示了。 69.6 单网卡使用 有时候我们在实际的产品开发中由于I.MX6ULL引脚数量的限制,用到的一些外设引脚和网卡冲突了,但是我们的产品又不需要两个网卡,有一个就够了。这个时候就需要修改linux内核,实现一个网卡的使用,另外一个网卡的IO用作其他的外设。 69.6.1 只使用ENET2网卡 如果只使用ENET2网卡的话修改起来比较简单,打开设备树imx6ull-alientek-emmc.dts,ENET1网卡对应的节点名字为fec1,ENET2网卡对应的节点名字为fec2。我们需要关闭ENET1网卡,首先找到fec1节点,然后将其中的status属性改为“disabled”即可,修改完成以后的fec1节点内容如下
示例代码69.6.1.1 fec1节点信息
1 &fec1 {
2 pinctrl-names = "default";
3 pinctrl-0 = ;
5 phy-mode = "rmii";
6 phy-handle = ;
7 phy-reset-gpios = ;
8 phy-reset-duration = ;
9 status = "disabled";
10 };
第9行,将status改为“disabled”。
修改完成以后重新编译设备树,使用新的设备树启动内核。内核启动以后输入如下命令查看当前系统中所有网卡:
ifconfig -a //查看所有网卡 结果如图69.6.1.1所示:
图69.6.1.1 当前系统网卡信息 从图69.6.1.1可以看出,此时只要一个eth0网卡,并没有eth1,说明fec1被我们关闭了,接下来大家就可以将fec1网卡对应的IO复用为其他外设功能了。 69.6.2 只使用ENET1网卡 如果只使用ENET1网卡的话就稍微复杂一点了,不是简单的将fec2节点下的status改为“disabled”,按照如下步骤来修改设备树imx6ull-alientek-emmc.dts: 1、屏蔽或删除掉fec2节点内容 首先,屏蔽或删除掉imx6ull-alientek-emmc.dts文件中ENET2网卡对应的fec2节点内容,如图69.6.2.1所示:
图69.6.2.1 屏蔽掉fec2节点内容 2、修改ENET1对应的fec1节点信息。 接下来需要fec1节点进行修改,重点是在fec1节点下添加mdio子节点,修改后的fec1节点内容如下所示:
示例代码69.6.1.2 修改后的fec1节点
1 &fec1 {
2 pinctrl-names = "default";
3 pinctrl-0 = ;
5 phy-mode = "rmii";
6 phy-handle = ;
7 phy-reset-gpios = ;
8 phy-reset-duration = ;
9 status = "okay";
10
11 mdio {
12 #address-cells = ;
13 #size-cells = ;
14
15 ethphy0: ethernet-phy@0 {
16 compatible = "ethernet-phy-ieee802.3-c22";
17 reg = ;
18 };
19 };
20 };
重点是在fec1中添加第11~19行的mdio子节点,而且仅仅留下ENET1对应的PHY子节点。
3、屏蔽或删除掉ENET2对应的pinctrl节点
屏蔽后删除掉ENET2对应的pinctrl_enet2,如图69.6.2.2所示:
图69.6.2.2 屏蔽掉pinctrl_enet2节点 4、在ENET1网卡对应的pinctrl节点中添加MDIO和MDC引脚配置 默认情况下GPIO1_IO07和GPIO1_IO06复用为了ENET2的MDC和MDIO,因此这里我们需要重新将GPIO1_IO07和GPIO1_IO06这两个IO复用为ENET1的MDC和DMIO,修改后的pinctrl_enet1内容如下所示:
//示例代码69.6.1.3 修改后的pinctrl_enet1节点
1 pinctrl_enet1: enet1grp {
2 fsl,pins = ;
14 };
第3行,将GPIO1_IO07复用为ENET1_MDC引脚。
第4行,将GPIO1_IO06复用为ENET1_MDIO引脚。
至此,设备树修改完成,重新编译设备树,然后用新的设备树启动系统。启动以后输入如下命令查看开发板所有网卡信息:
ifconfig -a 结果如图69.6.2.3所示:
图69.6.2.3 开发板网卡信息 从图69.6.2.3可以看出,此时整个系统只有一个eth0网卡,注意!这里的eth0网卡就是ENET1,不要和前面的混淆了,以为eth0是ENET2网卡名字。 至此,关于Linux的网络驱动就讲到这里,整体比较复杂,但是实际使用起来确实非常简单的,尤其是对这种内置MAC+外置PHY的网络方案而言,几乎不需要我们修改驱动,因为内核已经继承了通用PHY驱动了。