1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban
第四十章 Linux网络驱动实验
网络驱动是linux里面驱动三巨头之一,linux下的网络功能非常强大,嵌入式linux中也常常用到网络功能。前面我们已经讲过了字符设备驱动和块设备驱动,本章我们就来学习一下linux里面的网络设备驱动。
40.1 嵌入式网络简介
40.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芯片的连接如图40.1.1.1所示:
图40.1.1.1 主控SOC与外置MAC+PHY芯片连接
2、SOC内部集成网络MAC外设
我们一般说某个SoC支持网络,说的就是他内部集成网络MAC外设,此时我们还需要外接一个网络PHY芯片。此时就有朋友会有疑问,PHY芯片不能也集成进SoC吗?笔者目前还没见过将PHY也集成到芯片里面的SoC。
一般常见的通用SoC都会集成网络MAC外设,比如RK系列、NXP的I.MX系列以及STM32MP1系列,内部集成网络MAC的优点如下:
①、内部MAC外设会有专用的加速模块,比如专用的DMA,加速网速数据的处理。
②、网速快,可以支持10/100/1000M网速。
③、外接PHY可选择性多,成本低。
内部的MAC外设会通过相应的接口来连接外部PHY芯片,根据数据传输模式不同,大致可以分为以下两类:
①、MII/RMII接口:支持10Mbit/s和100Mbit/s数据传输模式;
②、GMII/RGMII接口:支持10Mbit/s、100Mbit/s以及1000Mbit/s数据传输模式。
从这里可以知道,MII/RMII接口最大传输速率为100Mbit/s,而GMII/RGMII接口最大传输速率可达1000Mbit/s;所以笔者一般把MII/RMII称为百兆以太网接口,而把GMII/RGMII称为千兆以太网接口。
关于这两组接口更加详细的内容会在后面给大家进行介绍,MII/RMII或GMII/RGMII接口是用来传输网络数据的,另外主控SoC需要配置或读取PHY芯片,也就是读写PHY的内部寄存器,所以还需要一个控制接口,叫做MIDO,MDIO很类似IIC,也是两根线,一根数据线叫做MDIO,一根时钟线叫做MDC。
SoC内部MAC外设与外部PHY芯片的连接如图40.1.1.2、图40.1.1.3所示:
图40.1.1.2 内部MAC与外部PHY之间的连接(千兆以太网)
图40.1.1.3 内部MAC与外部PHY之间的连接(百兆以太网)
大家在做项目的时候,如果要用到网络功能,强烈建议大家选择内部带有网络MAC外设的主控SOC!比如RK3568就有两颗10M/100M/1000M的网络MAC外设,正点原子的ATK-DLRK3568开发板板载了两颗PHY芯片,PHY芯片型号为 YT8531C,Pin to Pin 兼容 RTL8211F,因此在我们的开发板上 YT8531C 和 RTL8211F 可以随意互换。
因此,本章节只讲解SOC内部MAC+外置PHY芯片这种方案。
40.1.2 MII/RMII、GMII/RGMII接口
前面我们说了,内部MAC通过MII/RMII接口或者GMII/RGMII接口来与外部的PHY芯片连接,完成网络数据传输,本节我们就来学习一下什么是MII/GMII、GMII/RGMII接口。
1、MII接口
MII全称是Media Independent Interface,直译过来就是介质独立接口,它是IEEE-802.3定义的以太网标准接口,MII接口用于以太网MAC连接PHY芯片,连接示意图如图40.1.2.1所示:
图40.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芯片的示意图如图40.1.2.2所示:
图40.1.2.2 RMII接口
TX_EN:发送使能信号。
TXD[1:0]:发送数据信号线,一共2根。
RXD[1:0]:接收数据信号线,一共2根。
CRS_DV:相当于MII接口中的RX_DV和CRS这两个信号的混合。
REF_CLK:参考时钟,由外部时钟源提供,频率为50MHz。这里与MII不同,MII的接收和发送时钟是独立分开的,而且都是由PHY芯片提供的。
3、GMII接口
GMII(Gigabit Media Independant Interface),千兆MII接口。GMII采用8位接口数据,工作时钟125MHz,因此传输速率可达1000Mbps;同时兼容MII所规定的10/100Mbps工作方式。GMII接口数据结构符合IEEE以太网标准,该接口定义见IEEE 802.3-2000。信号定义如下:
图40.1.2.3 GMII接口
GTX_CLK:1000M工作模式下的发送时钟(125MHz)。
TX_EN:发送使能信号。
TX_ER:发送错误信号,高电平有效,表示TX_ER有效期内传输的数据无效。
TXD[7:0]:发送数据信号线,一共8根。
RXD[7:0]:接收数据信号线,一共8根。
RX_CLK:接收时钟信号。
RX_ER:接收错误信号,高电平有效,表示RX_ER有效期内传输的数据无效。
RX_DV:接收数据有效,作用类似TX_EN。
CRS:载波侦听信号。
COL:冲突检测信号。
与MII接口相比,GMII的数据宽度由4位变为8位,GMII接口中的控制信号如TX_ER、TX_EN、RX_ER、RX_DV、CRS和COL的作用同MII接口中的一样,发送参考时钟GTX_CLK和接收参考时钟RX_CLK的频率均为125MHz(在1000Mbps工作模式下)。
在实际应用中,绝大多数GMII接口都是兼容MII接口的,所以,一般的GMII接口都有两个发送参考时钟:TX_CLK和GTX_CLK(两者的方向是不一样的,前面已经说过了),在用作MII模式时,使用TX_CLK和8根数据线中的4根。
4、RGMII接口
RGMII(Reduced Gigabit Media Independant Interface),精简版GMII接口。将接口信号线数量从24根减少到14根(COL/CRS端口状态指示信号,这里没有画出),时钟频率仍旧为125MHz,TX/RX数据宽度从8为变为4位,为了保持1000Mbps的传输速率不变,RGMII接口在时钟的上升沿和下降沿都采样数据,在参考时钟的上升沿发送GMII接口中的TXD[3:0]/RXD[3:0],在参考时钟的下降沿发送GMII接口中的TXD[7:4]/RXD[7:4]。RGMII同时也兼容100Mbps和10Mbps两种速率,此时参考时钟速率分别为25MHz和2.5MHz。
TX_EN信号线上传送TX_EN和TX_ER两种信息,在TX_CLK的上升沿发送TX_EN,下降沿发送TX_ER;同样的,RX_DV信号线上也传送RX_DV和RX_ER两种信息,在RX_CLK的上升沿发送RX_DV,下降沿发送RX_ER。
RGMII接口定义如下所示:
图40.1.2.4 RGMII接口
关于这些接口定义相关的内容我们就讲到这里了,除了上面说到4种接口以外,还有其他接口,比如SMII、SSMII和SGMII等,关于其他接口基本都是大同小异的,这里就不做讲解了。正点原子的ATK-DLRK3568开发板上的网口是采用RGMII接口来连接MAC与外部PHY芯片。
40.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(百兆网)或GMII/RGMII(千兆网)和MDIO接口,另外可能还需要复位、中断等其他引脚。
40.1.4 RJ45接口
网络设备是通过网线连接起来的,插入网线的叫做RJ45座,如图40.1.4.1所示:
图40.1.4.1 RJ45座子
RJ45座要与PHY芯片连接在一起,但是中间需要一个网络变压器,网络编译器用于隔离以及滤波等,网络变压器也是一个芯片,外形一般如图40.1.4.2所示:
图40.1.4.2 网络变压器
但是现在很多RJ45座子内部已经集成了网络变压器,比如正点原子的ATK-DLRK3568开发板所使用的ATK911130A就是内置网络变压器的RJ45座。内置网络变压器的RJ45座和不内置的引脚一样,但是一般不内置的RJ45座会短一点。因此,大家在画板的时候一定要考虑你所使用的RJ45座是否内置网络变压器,如果不内置的话就要自行添加网络变压器部分电路!同理,如果你所设计的硬件是需要内置网络变压器的RJ45座,肯定不能随便焊接一个不内置变压器的RJ45座,否则网络工作不正常!
RJ45座子上一般有两个灯,一个黄色(橙色),一个绿色,一般绿色亮的话表示网络连接正常,黄色闪烁的话说明当前正在进行网络通信,当然了有时候两个灯的状态会反过来,大家以实际为准。这两个灯由PHY芯片控制,对于千M网络PHY芯片,一般PHY芯片会有3个LED灯引脚,多了一个千M网络状态指示灯。PHY芯片会通过这几个LED灯引脚来连接RJ45座上的这两个灯(千M PHY会有3个LED灯引脚,一般硬件设计人员会自行选择将其中的哪两个连接到RJ45座上)。由于正点原子ATK-DLRK3568开发板采用的千M网络PHY,所以后面只讲千M网络。内部MAC+外部PHY+RJ45座(内置网络变压器)就组成了一个完整的嵌入式网络接口硬件,如图40.1.4.3所示:
图40.1.4.3 嵌入式网络硬件接口示意图
40.1.5 RK3568 GMAC接口简介
RK3568内核集成了两个10M/100M/1000M的网络MAC,符合IEEE802.3-2002标准,MAC层支持全双工或者半双工模式下运行。MAC可编程,有直接存储器接口的专用DMA,将数据格式化符合IEEE802.3-2002标准的数据包,并把这些数据传输到以太网的物理接口中(PHY),还可以把数据包从RXFIFO移动到微处理器的存储器中。
RK3568内部ENET外设主要特性如下:
①、支持全工和半双工操作。
②、全双工流控制操作(IEEE 802.3X 暂停包和优先级流控制)
③、报头和帧起始数据(SFD)在发送模式下自动插入、在接收中自动删除。
④、可逐帧控制CRC和pad自动生成
⑤、可编程数据包长度,支持标准以太网数据包或高达16KB的巨型以太网数据包
⑥、可编程数据包间隙
⑦、两组FIFO:一个具有可编程阈值功能的4096字节发送FIFO和一个具有可配置阈值功能的4096字节接收FIFO。
……
RK3568的GMAC外设内容比较多,详细的介绍请查阅开发板光盘->03、核心板资料->核心板板载芯片资料->Rockchip RK3568 TRM Part2 V1.1-20210301(RK3568参考手册 2).pdf。我们在编写驱动的时候其实并不需要关注GMAC控制器外设的具体内容,因为这部分驱动是SoC厂商写的,我们重点关注的是更换PHY芯片以后哪里需要调整。
40.2 PHY芯片详解
40.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 协议英文原版已经放到了开发板光盘中,路径为:06、参考资料802.3 协议英文原版_2018年.pdf 打开此文档,此文档有5600页,按照SECTION进行分类,一共8个SECTION。选中“802.3-2018_SECTION2”,找到“22.2.4 Management functions”章节,此章节对PHY的前16个寄存器功能进行了规定,如图40.2.1.1所示:
图40.2.1.1 IEEE规定的前16个寄存器
关于这16个寄存器的内容协议里面也进行了详细的讲解,这里就不分析了。我们后面会以正点原子ATK-DLRK3568开发板所使用的YT8531C(RTL8211F)这个PHY为例,详细的分析一下PHY芯片的寄存器。
40.2.2 YT8531C详解
YT8531C这颗PHY芯片是由裕太微电子股份有限公司的,它的数据手册已经放到开发板资料包里,路径为:开发板A盘-基础资料07、硬件资料02、底板板载芯片资料 YT8531(D)H_YT8531(D)C_YT8531P_Datasheet_v1.00.pdf。
前面说了,PHY芯片的前16个寄存器都是一样的,而且只需要使用这前16个寄存器基本就可以驱动起来PHY芯片,这里就不详细讲解了。
1、YT8531C简介
YT8531C是高度集成的以太网收发器,符合10Base-T、100Base-TX和1000Base-T IEEE 802.3标准。它提供了所有通过CAT.5 UTP电缆收发以太网数据包所需的必要物理层功能。
YT8531C使用最新的DSP技术和模拟前端(AFE),可以通过UTP电缆进行高速数据传输和接收。RTL8211F中实现了交叉检测和自动校正、极性校正、自适应均衡、串扰消除、回声消除、定时恢复和错误校正等功能,以提供10Mbps,100Mbps或1000Mbps的强大收发功能。
MAC和PHY之间的数据传输是通过RGMII接口进行的,YT8531C支持RGMII的1.5V信号。
YT8531C的主要特点如下:
·兼容1000Base-T IEEE 802.3ab标准。
·兼容100Base-TX IEEE 802.3u标准。
·兼容10Base-T IEEE 802.3标准。
·支持GMII、RGMII接口。
·支持IEEE 802.3az-2010(节能以太网)。
·内置LAN唤醒(WOL)。
·支持中断、并行检测、交叉检测、自动校正、自动极性校正。
·支持120m的1000Base-T的CAT.5类电缆。
·支持RGMII的1.5V信号。
……
YT8531C千兆PHY的系统应用场景如下:
·数字电视(DTV)。
·媒体访问单元(MAU)。
·通讯和网络提升板(CNR)。
·游戏机。
·打印机和办公机器。
·DVD播放机和刻录机。
·以太网集线器、交换机。
YT8531C功能框图如下所示:
图40.2.2.1 YT8531C详解
2、节能以太网(EEE)
YT8531C支持IEEE 802.3az-2010(也称为节能以太网(EEE)),速率为10Mbps、100Mbps或1000Mbps时它提供了一个协议,可根据链路利用率协调进出较低功耗级别(低功耗空闲模式)的转换,当没有数据包传输时,系统进入低功耗空闲模式以降低功耗,一旦需要发送数据包,系统将返回正常模式,并且无需更改链接状态和丢弃/破坏帧即可进行此操作。
为了节省功率,当系统处于低功耗空闲模式时,大多数电路功能都被禁用;但是,低功耗空闲模式的过渡时间保持足够小,以使得对上层协议和应用程序透明。
3、中断管理
每当YT8531C检测到介质状态发生变化时,它就会将中断引脚(INTB)拉低发出中断事件。SoC MAC端会感应到状态更改,并通过MDC/MDIO接口访问相关的寄存器。
一旦MAC通过MDC/MDIO读取了这些状态寄存器,就会将INTB置为无效。所以不需要通过轮训的方式去反复查询状态寄存器的变化。
4、PHY地址设置
MAC层通过MDIO/MDC总线对PHY进行读写操作,MDIO最多可以控制32个PHY芯片,通过不同的PHY芯片地址来对不同的PHY操作。YT8531C通过配置PHYAD[2:0]这三个引脚来设置PHY地址,通常情况下都是设置为地址0X00,配置如下图40.2.2.2所示。
图40.2.2.2 YT8531C PHY地址设置
ATK-RLDK3568开发板上的YT8531C的地址为0X00。
芯片YT8511C还有很多设置,这边就不一一列出了,大家直接查看芯片原理图就知道了。
40.3 Linux内核网络驱动框架
40.3.1 new_device结构体
Linux内核使用net_device结构体表示一个具体的网络设备,net_device是整个网络驱动的灵魂。网络驱动的核心就是初始化net_device结构体中的各个成员变量,然后将初始化完成以后的net_device注册到Linux内核中。net_device结构体定义在include/linux/netdevice.h中,net_device是一个庞大的结构体,内容如下(有缩减):
示例代码40.3.1.1 net_device结构体
1 struct net_device {
2 char name[IFNAMSIZ];
3 struct hlist_node name_hlist;
4 struct dev_ifalias __rcu *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;
......
21
22 struct list_head dev_list;
23 struct list_head napi_list;
24 struct list_head unreg_list;
25 struct list_head close_list;
26 struct list_head ptype_all;
27 struct list_head ptype_specific;
......
59 const struct net_device_ops *netdev_ops;
60 const struct ethtool_ops *ethtool_ops;
......
79 const struct header_ops *header_ops;
80
81 unsigned int flags;
82 unsigned int priv_flags;
83
84 unsigned short gflags;
85 unsigned short padded;
86
87 unsigned char operstate;
88 unsigned char link_mode;
89
90 unsigned char if_port;
91 unsigned char dma;
......
98 unsigned int mtu;
99 unsigned int min_mtu;
100 unsigned int max_mtu;
101 unsigned short type;
102 unsigned short hard_header_len;
103 unsigned char min_header_len;
104
105 unsigned short needed_headroom;
106 unsigned short needed_tailroom;
107
108 /* Interface address info. */
109 unsigned char perm_addr[MAX_ADDR_LEN];
110 unsigned char addr_assign_type;
111 unsigned char addr_len;
112 unsigned char upper_level;
......
163 unsigned char *dev_addr;
164
165 struct netdev_rx_queue *_rx;
166 unsigned int num_rx_queues;
167 unsigned int real_num_rx_queues;
......
191 struct netdev_queue *_tx ____cacheline_aligned_in_smp;
192 unsigned int num_tx_queues;
193 unsigned int real_num_tx_queues;
......
264 /* for setting kernel sock attribute on TCP connection setup */
265#define GSO_MAX_SIZE 65536
266 unsigned int gso_max_size;
267#define GSO_MAX_SEGS 65535
268 u16 gso_max_segs;
269
270#ifdef CONFIG_DCB
271 const struct dcbnl_rtnl_ops *dcbnl_ops;
272#endif
273 s16 num_tc;
274 struct netdev_tc_txq tc_to_txq[TC_MAX_QUEUE];
275 u8 prio_tc_map[TC_BITMASK + 1];
276
277#if IS_ENABLED(CONFIG_FCOE)
278 unsigned int fcoe_ddp_xid;
279#endif
280#if IS_ENABLED(CONFIG_CGROUP_NET_PRIO)
281 struct netprio_map __rcu *priomap;
282#endif
283 struct phy_device *phydev;
284 struct sfp_bus *sfp_bus;
......
299};
下面介绍一些关键的成员变量,如下:
第2行:name是网络设备的名字。
第9行:mem_end是共享内存结束地址。
第10行:mem_start是共享内存起始地址。
第11行:base_addr是网络设备I/O地址。
第12行:irq是网络设备的中断号。
第22行:dev_list是全局网络设备列表。
第23行:napi_list是napi网络设备的列表入口。
第24行:unreg_list是注销(unregister)的网络设备列表入口。
第25行:close_list是关闭的网络设备列表入口。
第59行:netdev_ops是网络设备的操作集函数,包含了一系列的网络设备操作回调函数,类似字符设备中的file_operations,稍后会讲解netdev_ops结构体。
第60行:ethtool_ops是网络管理工具相关函数集,用户空间网络管理工具会调用此结构体中的相关函数获取网卡状态或者配置网卡。
第79行:header_ops是头部的相关操作函数集,比如创建、解析、缓冲等。
第81行:flags是网络接口标志,标志类型定义在include/uapi/linux/if.h文件中,为一个枚举类型,内容如下:
示例代码40.3.1.2 网络标志类型
81 enum net_device_flags {
82 /* for compatibility with glibc net/if.h */
83 #if __UAPI_DEF_IF_NET_DEVICE_FLAGS
84 IFF_UP = 1<<0, /* sysfs */
85 IFF_BROADCAST = 1<<1, /* volatile */
86 IFF_DEBUG = 1<<2, /* sysfs */
87 IFF_LOOPBACK = 1<<3, /* volatile */
88 IFF_POINTOPOINT = 1<<4, /* volatile */
89 IFF_NOTRAILERS = 1<<5, /* sysfs */
90 IFF_RUNNING = 1<<6, /* volatile */
91 IFF_NOARP = 1<<7, /* sysfs */
92 IFF_PROMISC = 1<<8, /* sysfs */
93 IFF_ALLMULTI = 1<<9, /* sysfs */
94 IFF_MASTER = 1<<10, /* volatile */
95 IFF_SLAVE = 1<<11, /* volatile */
96 IFF_MULTICAST = 1<<12, /* sysfs */
97 IFF_PORTSEL = 1<<13, /* sysfs */
98 IFF_AUTOMEDIA = 1<<14, /* sysfs */
99 IFF_DYNAMIC = 1<<15, /* sysfs */
100 #endif /* __UAPI_DEF_IF_NET_DEVICE_FLAGS */
101 #if __UAPI_DEF_IF_NET_DEVICE_FLAGS_LOWER_UP_DORMANT_ECHO
102 IFF_LOWER_UP = 1<<16, /* volatile */
103 IFF_DORMANT = 1<<17, /* volatile */
104 IFF_ECHO = 1<<18, /* volatile */
105 #endif /* __UAPI_DEF_IF_NET_DEVICE_FLAGS_LOWER_UP_DORMANT_ECHO */
106 };
继续回到示例代码40.3.1.1中,接着看net_device结构体。
第90行:if_port指定接口的端口类型,如果设备支持多端口的话就通过if_port来指定所使用的端口类型。可选的端口类型定义在include/uapi/linux/netdevice.h中,为一个枚举类型,如下所示:
示例代码40.3.1.3 端口类型
49 enum {
50 IF_PORT_UNKNOWN = 0,
51 IF_PORT_10BASE2,
52 IF_PORT_10BASET,
53 IF_PORT_AUI,
54 IF_PORT_100BASET,
55 IF_PORT_100BASETX,
56 IF_PORT_100BASEFX
57 };
第91行:dma 是网络设备所使用的 DMA 通道,不是所有的设备都会用到 DMA。
第98行:mtu 是网络最大传输单元,为 1500。
第101行:type 用于指定 ARP 模块的类型,以太网的 ARP 接口为 ARPHRD_ETHER,Linux内核所支持的 ARP 协议定义在 include/uapi/linux/if_arp.h 中,大家自行查阅。
第109行:perm_addr是永久的硬件地址,如果某个网卡设备有永久的硬件地址,那么就会填充perm_addr。
第111行:addr_len是硬件地址长度。
第163行:dev_addr也是硬件地址,是当前分配的MAC地址,可以通过软件修改。
第165行:_rx是接收队列。
第166行:num_rx_queues是接收队列数量,在调用register_netdev注册网络设备的时候会分配指定数量的接收队列。
第167行:real_num_rx_queues是当前活动的队列数量。
第191行:_tx是发送队列。
第192行:num_tx_queues是发送队列数量,通过alloc_netdev_mq函数分配指定数量的发送队列。
第193行:real_num_tx_queues是当前有效的发送队列数量。
第283行:phydev是对应的PHY设备。
1、申请net_device
编写网络驱动的时候首先要申请net_device,使用alloc_netdev函数来申请net_device,这是一个宏,宏定义如下:
#define alloc_netdev(sizeof_priv, name, name_assign_type, setup)
alloc_netdev_mqs(sizeof_priv, name, name_assign_type, setup, 1, 1)
可以看出alloc_netdev的本质是alloc_netdev_mqs函数,此函数原型如下
struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
unsigned char name_assign_type,
void (*setup)(struct net_device *),
unsigned int txqs, unsigned int rxqs)
函数参数和返回值含义如下:
sizeof_priv:私有数据块大小。
name:设备名字。
name_assign_type:设备名字的来源。
setup:回调函数,初始化设备的设备后调用此函数。
txqs:分配的发送队列数量。
rxqs:分配的接收队列数量。
返回值:如果申请成功的话就返回申请到的net_device指针,失败的话就返回NULL。
事实上网络设备有多种,大家不要以为就只有以太网一种。Linux内核支持的网络接口有很多,比如光纤分布式数据接口(FDDI)、以太网设备(Ethernet)、红外数据接口(InDA)、高性能并行接口(HPPI)、CAN网络等。内核针对不同的网络设备在alloc_netdev的基础上提供了一层封装,比如我们本章讲解的以太网,针对以太网封装的net_device申请函数是alloc_etherdev和alloc_etherdev_mq,这也是一个宏,内容如下:
#define alloc_etherdev(sizeof_priv) alloc_etherdev_mq(sizeof_priv, 1)
#define alloc_etherdev_mq(sizeof_priv, count) alloc_etherdev_mqs(sizeof_priv, count, count)
可以看出,alloc_etherdev最终依靠的是alloc_etherdev_mqs函数,此函数就是对alloc_netdev_mqs的简单封装,函数内容如下:
示例代码40.3.1.4 alloc_etherdev_mqs函数
411 struct net_device *alloc_etherdev_mqs(int sizeof_priv,
unsigned int txqs,
412 unsigned int rxqs)
413 {
414 return alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_UNKNOWN,
415 ether_setup, txqs, rxqs);
416 }
第414行调用alloc_netdev_mqs来申请net_device,注意这里设置网卡的名字为“eth%d”,这是格式化字符串,大家进入开发板的linux系统以后看到的“eth0”、“eth1”这样的网卡名字就是从这里来的。同样的,这里设置了以太网的setup函数为ether_setup,不同的网络设备其setup函数不同,比如CAN网络里面setup函数就是can_setup。
ether_setup函数会对net_device做初步的初始化,函数内容如下所示:
示例代码40.3.1.5 ether_setup函数
377 void ether_setup(struct net_device *dev)
378 {
379 dev->header_ops = ð_header_ops;
380 dev->type = ARPHRD_ETHER;
381 dev->hard_header_len = ETH_HLEN;
382 dev->min_header_len = ETH_HLEN;
383 dev->mtu = ETH_DATA_LEN;
384 dev->min_mtu = ETH_MIN_MTU;
385 dev->max_mtu = ETH_DATA_LEN;
386 dev->addr_len = ETH_ALEN;
387 dev->tx_queue_len = DEFAULT_TX_QUEUE_LEN;
388 dev->flags = IFF_BROADCAST|IFF_MULTICAST;
389 dev->priv_flags |= IFF_TX_SKB_SHARING;
390
391 eth_broadcast_addr(dev->broadcast);
392
393 }
关于 net_device 的申请就讲解到这里,对于网络设备而言,使用 alloc_etherdev 或alloc_etherdev_mqs 来申请 net_device。
2、删除net_device
当我们注销网络驱动的时候需要释放掉前面已经申请到的net_device,释放函数为free_netdev,函数原型如下:
void free_netdev(struct net_device *dev)
函数参数和返回值含义如下:
dev:要释放掉的net_device指针。
返回值:无。
3、注册net_device
net_device申请并初始化完成以后就需要向内核注册net_device,要用到函数register_netdev,函数原型如下:
int register_netdev(struct net_device *dev)
函数参数和返回值含义如下:
dev:要注册的net_device指针。
返回值:0 注册成功,负值 注册失败。
3、注销net_device
既然有注册,那么必然有注销,注销net_device使用函数unregister_netdev,函数原型如下:
void unregister_netdev(struct net_device *dev)
函数参数和返回值含义如下:
dev:要注销的net_device指针。
返回值:无。
40.3.2 net_device_ops结构体
net_device有个非常重要的成员变量:netdev_ops,为net_device_ops结构体指针类型,这就是网络设备的操作集,net_device_ops结构体定义在include/linux/netdevice.h文件中,net_device_ops结构体里面都是一些以“ndo_”开头的函数,这些函数就需要网络驱动人员去实现,不需要全部都实现,根据实际驱动情况实现其中一小部分即可。结构体内容如下所示(结构体成员变量比较多,这里有缩减):
示例代码40.3.2.1 net_device_ops结构体内容
1 struct net_device_ops {
2 int (*ndo_init)(struct net_device *dev);
3 void (*ndo_uninit)(struct net_device *dev);
4 int (*ndo_open)(struct net_device *dev);
5 int (*ndo_stop)(struct net_device *dev);
6 netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb,
7 struct net_device *dev);
8 netdev_features_t (*ndo_features_check)(struct sk_buff *skb,
9 struct net_device *dev,
10 netdev_features_t features);
11 u16 (*ndo_select_queue)(struct net_device *dev,
12 struct sk_buff *skb,
13 struct net_device *sb_dev,
14 select_queue_fallback_t fallback);
15 void (*ndo_change_rx_flags)(struct net_device *dev,
16 int flags);
17 void (*ndo_set_rx_mode)(struct net_device *dev);
18 int (*ndo_set_mac_address)(struct net_device *dev,
19 void *addr);
20 int (*ndo_validate_addr)(struct net_device *dev);
21 int (*ndo_do_ioctl)(struct net_device *dev,
22 struct ifreq *ifr, int cmd);
23 int (*ndo_set_config)(struct net_device *dev,
24 struct ifmap *map);
25 int (*ndo_change_mtu)(struct net_device *dev,
26 int new_mtu);
27 int (*ndo_neigh_setup)(struct net_device *dev,
28 struct neigh_parms *);
29 void (*ndo_tx_timeout) (struct net_device *dev);
30
......
43 #ifdef CONFIG_NET_POLL_CONTROLLER
44 void (*ndo_poll_controller)(struct net_device *dev);
45 int (*ndo_netpoll_setup)(struct net_device *dev,
46 struct netpoll_info *info);
47 void (*ndo_netpoll_cleanup)(struct net_device *dev);
48 #endif
......
114 int (*ndo_add_slave)(struct net_device *dev,
115 struct net_device *slave_dev,
116 struct netlink_ext_ack *extack);
117 int (*ndo_del_slave)(struct net_device *dev,
118 struct net_device *slave_dev);
119 netdev_features_t (*ndo_fix_features)(struct net_device *dev,
120 netdev_features_t features);
121 int (*ndo_set_features)(struct net_device *dev,
......
198};
第2行:ndo_init函数,当第一次注册网络设备的时候此函数会执行,设备可以在此函数中做一些需要推后初始化的内容,不过一般驱动中不使用此函数,虚拟网络设备可能会使用。
第3行:ndo_uninit函数,卸载网络设备的时候此函数会执行。
第4行:ndo_open函数,打开网络设备的时候此函数会执行,网络驱动程序需要实现此函数,非常重要!以RK3568系列SoC网络驱动为例,会在此函数中做如下工作:
·初始化接收缓冲区。
·如果使用NAPI的话要使能NAPI模块,通过napi_enable函数来使能。
·RK3568 MAC控制器硬件相关初始化。
·开启PHY。
·……
第1253行:ndo_stop函数,关闭网络设备的时候此函数会执行,网络驱动程序也需要实现此函数。以RK3568系列SoC网络驱动为例,会在此函数中做如下工作:
·停止PHY。
·停止NAPI功能。
·停止发送功能。
·关闭MAC。
·断开PHY连接。
·释放数据缓冲区。
·……
第6行:ndo_start_xmit函数,当需要发送数据的时候此函数就会执行,此函数有一个参数为sk_buff结构体指针,sk_buff结构体在Linux的网络驱动中非常重要,sk_buff保存了上层传递给网络驱动层的数据。也就是说,要发送出去的数据都存在了sk_buff中,关于sk_buff稍后会做详细的讲解。如果发送成功的话此函数返回NETDEV_TX_OK,如果发送失败了就返回NETDEV_TX_BUSY,如果发送失败了我们就需要停止队列。
第11行:do_select_queue函数,当设备支持多传输队列的时候选择使用哪个队列。
第17行:ndo_set_rx_mode函数,此函数用于改变地址过滤列表,根据net_device的flags成员变量来设置SoC的网络外设寄存器。比如flags可能为IFF_PROMISC、IFF_ALLMULTI或IFF_MULTICAST,分别表示混杂模式、单播模式或多播模式。
第18行:ndo_set_mac_address函数,此函数用于修改网卡的MAC地址,设置net_device的dev_addr成员变量,并且将MAC地址写入到网络外设的硬件寄存器中。
第20行:ndo_validate_addr函数,验证MAC地址是否合法,也即是验证net_device的dev_addr中的MAC地址是否合法,直接调用is_valid_ether_addr函数。
第21行:ndo_do_ioctl函数,用户程序调用ioctl的时候此函数就会执行,比如PHY芯片相关的命令操作,一般会直接调用phy_mii_ioctl函数。
第25行:ndo_change_mtu函数,更改MTU大小。
第29行:ndo_tx_timeout函数,当发送超时的时候产生会执行,一般都是网络出问题了导致发送超时。一般可能会重启MAC和PHY,重新开始数据发送等。
第44行:ndo_poll_controller函数,使用查询方式来处理网卡数据的收发。
第121行:ndo_set_features函数,修改net_device的features属性,设置相应的硬件属性。
40.3.3 sk_buff结构体
网络是分层的,对于应用层而言不用关心具体的底层是如何工作的,只需要按照协议将要发送或接收的数据打包好即可。打包好以后都通过dev_queue_xmit函数将数据发送出去,接收数据的话使用netif_rx函数即可,我们依次来看一下这两个函数。
1、dev_queue_xmit函数
此函数用于将网络数据发送出去,函数定义在include/linux/netdevice.h中,函数原型如下:
int dev_queue_xmit(struct sk_buff *skb)
函数参数和返回值含义如下:
skb:要发送的数据,这是一个sk_buff结构体指针,sk_buff是Linux网络驱动中一个非常重要的结构体,网络数据就是以sk_buff保存的,各个协议层在sk_buff中添加自己的协议头,最终由底层驱动将sk_buff中的数据发送出去。网络数据的接收过程恰好相反,网络底层驱动将接收到的原始数据打包成sk_buff,然后发送给上层协议,上层会取掉相应的头部,然后将最终的数据发送给用户。
返回值:0,发送成功;负值,发送失败。
dev_queue_xmit函数太长,这里就不详细的分析了,dev_queue_xmit函数最终是通过net_device_ops操作集里面的ndo_start_xmit函数来完成最终发送了,ndo_start_xmit就是网络驱动编写人员去实现的
2、netif_rx函数
上层接收数据的话使用netif_rx函数,但是最原始的网络数据一般是通过轮询、中断或NAPI的方式来接收。netif_rx函数定义在net/core/dev.c中,函数原型如下:
int netif_rx(struct sk_buff *skb)
函数参数和返回值含义如下:
skb:保存接收数据的sk_buff。
返回值:NET_RX_SUCCESS,成功;NET_RX_DROP,数据包丢弃。
我们重点来看一下sk_buff这个结构体,sk_buff是Linux网络重要的数据结构,用于管理接收或发送数据包,sk_buff结构体定义在include/linux/skbuff.h中,结构体内容如下(由于结构体比较大,为了缩小篇幅只列出部分重要的内容):
示例代码40.3.3.1 sk_buff结构体
1 struct sk_buff {
2 union {
3 struct {
4 /* These two members must be first. */
5 struct sk_buff *next;
6 struct sk_buff *prev;
7
8 union {
9 struct net_device *dev;
......
15 };
16 };
17 struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */
18 struct list_head list;
19 };
20
21 union {
22 struct sock *sk;
23 int ip_defrag_offset;
24 };
25
26 union {
27 ktime_t tstamp;
28 u64 skb_mstamp;
29 };
......
36 char cb[48] __aligned(8);
37
38 union {
39 struct {
40 unsigned long _skb_refdst;
41 void (*destructor)(struct sk_buff *skb);
42 };
43 struct list_head tcp_tsorted_anchor;
44 };
......
53 unsigned int len,
54 data_len;
55 __u16 mac_len,
56 hdr_len;
......
176
177 __be16 protocol;
178 __u16 transport_header;
179 __u16 network_header;
180 __u16 mac_header;
181
182 /* private: */
183 __u32 headers_end[0];
184 /* public: */
185
186 ANDROID_KABI_RESERVE(1);
187 ANDROID_KABI_RESERVE(2);
188
189 /* These elements must be at the end, see alloc_skb() for details. */
190 sk_buff_data_t tail;
191 sk_buff_data_t end;
192 unsigned char *head,
193 *data;
194 unsigned int truesize;
195 refcount_t users;
196};
第4~6行:next和prev分别指向下一个和前一个sk_buff,构成一个双向链表。
第9行:dev表示当前sk_buff从哪个设备接收到或者发出的。
第22行:sk表示当前sk_buff所属的Socket。
第27行:tstamp表示数据包接收时或准备发送时的时间戳。
第36行:cb为控制缓冲区,不管哪个层都可以自由使用此缓冲区,用于放置私有数据。
第41行:destructor函数,当释放缓冲区的时候可以在此函数里面完成某些动作。
第53行:len为实际的数据长度,包括主缓冲区中数据长度和分片中的数据长度。data_len为数据长度,只计算分片中数据的长度。
第55行:mac_len为连接层头部长度,也就是MAC头的长度。
第177行:protocol协议。
第178行:transport_header为传输层头部。
第179行:network_header为网络层头部
第180行:mac_header为链接层头部。
第190行:tail指向实际数据的尾部。
第191行:end指向缓冲区的尾部。
第192行:head指向缓冲区的头部,data指向实际数据的头部。data和tail指向实际数据的头部和尾部,head和end指向缓冲区的头部和尾部。结构如图40.3.3.1所示:
图40.3.3.1 sk_buff数据区结构示意图
针对sk_buff内核提供了一系列的操作与管理函数,我们简单看一些常见的API函数
1、分配sk_buff
要使用sk_buff必须先分配,首先来看一下alloc_skb这个函数,此函数定义在include/linux/skbuff.h中,函数原型如下:
static inline struct sk_buff *alloc_skb(unsigned int size,
gfp_t priority)
函数参数和返回值含义如下:
size:要分配的大小,也就是skb数据段大小。
priority:为GFP MASK宏,比如GFP_KERNEL、GFP_ATOMIC等。
返回值:分配成功的话就返回申请到的sk_buff首地址,失败的话就返回NULL。
在网络设备驱动中常常使用netdev_alloc_skb来为某个设备申请一个用于接收的skb_buff,此函数也定义在include/linux/skbuff.h中,函数原型如下:
static inline struct sk_buff *netdev_alloc_skb(struct net_device *dev,
unsigned int length)
函数参数和返回值含义如下:
dev:要给哪个设备分配sk_buff。
length:要分配的大小。
返回值:分配成功的话就返回申请到的sk_buff首地址,失败的话就返回NULL。
2、释放sk_buff
当使用完成以后就要释放掉sk_buff,释放函数可以使用kfree_skb,函数定义在include/linux/skbuff.c中,函数原型如下:
void kfree_skb(struct sk_buff *skb)
函数参数和返回值含义如下:
skb:要释放的sk_buff。
返回值:无。
对于网络设备而言最好使用如下所示释放函数:
void dev_kfree_skb (struct sk_buff *skb)
函数只要一个参数skb,就是要释放的sk_buff。
3、skb_put、skb_push、sbk_pull和skb_reserve
这四个函数用于变更sk_buff,先来看一下skb_put函数,此函数用于在尾部扩展skb_buff的数据区,也就将skb_buff的tail后移n个字节,从而导致skb_buff的len增加n个字节,原型如下:
unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
函数参数和返回值含义如下:
skb:要操作的sk_buff。
len:要增加多少个字节。
返回值:扩展出来的那一段数据区首地址。
skb_put操作之前和操作之后的数据区如图40.3.3.2所示:
图40.3.3.2 skb_put函数操作前后对比
skb_push函数用于在头部扩展skb_buff的数据区,函数原型如下所示:
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
函数参数和返回值含义如下:
skb:要操作的sk_buff。
len:要增加多少个字节。
返回值:扩展完成以后新的数据区首地址。
skb_push操作之前和操作之后的数据区如图40.3.3.3所示:
图40.3.3.3 skb_push函数操作前后对比
sbk_pull函数用于从sk_buff的数据区起始位置删除数据,函数原型如下所示:
unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)
函数参数和返回值含义如下:
skb:要操作的sk_buff。
len:要删除的字节数。
返回值:删除以后新的数据区首地址。
skb_pull操作之前和操作之后的数据区如40.3.3.4所示:
图40.3.3.4 skb_pull函数操作前后对比
sbk_reserve函数用于调整缓冲区的头部大小,方法很简单,将skb_buff的data和tail同时后移n个字节即可,函数原型如下所示:
static inline void skb_reserve(struct sk_buff *skb, int len)
函数参数和返回值含义如下:
skb:要操作的sk_buff。
len:要增加的缓冲区头部大小。
返回值:无。
40.3.4 网络NAPI处理机制
如果玩过单片机的话应该都知道,像IIC、SPI、网络等这些通信接口,接收数据有两种方法:轮询或中断。Linux里面的网络数据接收也有轮询和中断两种,中断的好处就是响应快,数据量小的时候处理及时,速度快,但是一旦当数据量大,而且都是短帧的时候会导致中断频繁发生,消耗大量的CPU处理时间在中断自身处理上。轮询恰好相反,响应没有中断及时,但是在处理大量数据的时候不需要消耗过多的CPU处理时间。Linux在这两个处理方式的基础上提出了另外一种网络数据接收的处理方法:NAPI(New API),NAPI是一种高效的网络处理技术。NAPI的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用POLL的方法来轮询处理数据。这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。
关于NAPI详细的处理过程本章节不讨论,本章节就简单讲解一下如何在驱动中使用NAPI,Linux内核使用结构体napi_struct表示NAPI,在使用NAPI之前要先初始化一个napi_struct实例。
1、初始化NAPI
首先要初始化一个napi_struct实例,使用netif_napi_add函数,此函数定义在net/core/dev.c中,函数原型如下:
void netif_napi_add(struct net_device *dev,
struct napi_struct *napi,
int (*poll)(struct napi_struct *, int),
int weight)
函数参数和返回值含义如下:
dev:每个NAPI必须关联一个网络设备,此参数指定NAPI要关联的网络设备。
napi:要初始化的NAPI实例。
poll:NAPI所使用的轮询函数,非常重要,一般在此轮询函数中完成网络数据接收的工作。
weight:NAPI默认权重(weight),一般为NAPI_POLL_WEIGHT。
返回值:无。
2、删除NAPI
如果要删除NAPI,使用netif_napi_del函数即可,函数原型如下:
void netif_napi_del(struct napi_struct *napi)
函数参数和返回值含义如下:
napi:要删除的NAPI。
返回值:无。
3、使能NAPI
初始化完NAPI以后,必须使能才能使用,使用函数napi_enable,函数原型如下:
void napi_enable(struct napi_struct *n)
函数参数和返回值含义如下:
n:要使能的NAPI。
返回值:无。
4、关闭NAPI
关闭NAPI使用napi_disable函数即可,函数原型如下:
void napi_disable(struct napi_struct *n)
函数参数和返回值含义如下:
n:要关闭的NAPI。
返回值:无。
5、检查NAPI是否可以进行调度
使用napi_schedule_prep函数检查NAPI是否可以进行调度,函数原型如下:
inline bool napi_schedule_prep(struct napi_struct *n)
函数参数和返回值含义如下:
n:要检查的NAPI。
返回值:如果可以调度就返回真,如果不可调度就返回假。
6、NAPI调度
如果可以调度的话就进行调度,使用__napi_schedule函数完成NAPI调度,函数原型如下:
void __napi_schedule(struct napi_struct *n)
函数参数和返回值含义如下:
n:要调度的NAPI。
返回值:无。
我们也可以使用napi_schedule函数来一次完成napi_schedule_prep和__napi_schedule这两个函数的工作,napi_schedule函数内容如下所示:
示例代码40.3.4.1 napi_schedule函数代码
442 static inline void napi_schedule(struct napi_struct *n)
443 {
444 if (napi_schedule_prep(n))
445 __napi_schedule(n);
446 }
从示例代码52.3.4.1可以看出,napi_schedule函数就是对napi_schedule_prep和__napi_schedule的简单封装,一次完成判断和调度。
7、NAPI处理完成
NAPI处理完成以后需要调用napi_complete函数来标记NAPI处理完成,函数原型如下:
inline void napi_complete(struct napi_struct *n)
函数参数和返回值含义如下:
n:处理完成的NAPI。
返回值:无。
40.4 原理图介绍
开发板原理图ENET部分如下:
图40.4.1 ETH1原理图
图40.4.2 ETH0原理图
40.5 RK3568网络驱动简介
40.5.1 RK3568网络外设设备树
上一小节我们对Linux的网络框架进行了一个简单的介绍,本节我们就来简单分析一下RK3568的网络驱动源码。我们肯定是先分析设备树,RK3568系统SOC的网络绑定文档为Documentation/devicetree/bindings/net/rockchip-dwmac.txt,此绑定文档描述了RK3568网络设备节点的要求。此外还有一份文档Documentation/devicetree/bindings/net/ethernet.txt,该文档描述了网络设备节点的一些通用属性。
在内核源码目录arch/arm64/boot/dts/rockchip/rk3568.dtsi设备树文件中,定义了网络设备节点,如下所示:
示例代码 40.5.1.1 eth1 节点
1 gmac0: ethernet@fe2a0000 {
2 compatible = “rockchip,rk3568-gmac”, “snps,dwmac-4.20a”;
3 reg = <0x0 0xfe2a0000 0x0 0x10000>;
4 interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>,
5 <GIC_SPI 24 IRQ_TYPE_LEVEL_HIGH>;
6 interrupt-names = “macirq”, “eth_wake_irq”;
7 rockchip,grf = <&grf>;
8 clocks = <&cru SCLK_GMAC0>, <&cru SCLK_GMAC0_RX_TX>,
9 <&cru SCLK_GMAC0_RX_TX>, <&cru CLK_MAC0_REFOUT>,
10 <&cru ACLK_GMAC0>, <&cru PCLK_GMAC0>,
11 <&cru SCLK_GMAC0_RX_TX>, <&cru CLK_GMAC0_PTP_REF>,
12 <&cru PCLK_XPCS>;
13 clock-names = “stmmaceth”, “mac_clk_rx”,
14 “mac_clk_tx”, “clk_mac_refout”,
15 “aclk_mac”, “pclk_mac”,
16 “clk_mac_speed”, “ptp_ref”,
17 “pclk_xpcs”;
18 resets = <&cru SRST_A_GMAC0>;
19 reset-names = “stmmaceth”;
20
21 snps,mixed-burst;
22 snps,tso;
23
24 snps,axi-config = <&gmac0_stmmac_axi_setup>;
25 snps,mtl-rx-config = <&gmac0_mtl_rx_setup>;
26 snps,mtl-tx-config = <&gmac0_mtl_tx_setup>;
27 status = “disabled”;
28
29 mdio0: mdio {
30 compatible = “snps,dwmac-mdio”;
31 #address-cells = <0x1>;
32 #size-cells = <0x0>;
33 };
34
35 gmac0_stmmac_axi_setup: stmmac-axi-config {
36 snps,wr_osr_lmt = <4>;
37 snps,rd_osr_lmt = <8>;
38 snps,blen = <0 0 0 0 16 8 4>;
39 };
40
41 gmac0_mtl_rx_setup: rx-queues-config {
42 snps,rx-queues-to-use = <1>;
43 queue0 {};
44 };
45
46 gmac0_mtl_tx_setup: tx-queues-config {
47 snps,tx-queues-to-use = <1>;
48 queue0 {};
49 };
50 };
第2行,compatible属性有两个分别为:“rockchip,rk3568-gmac"和"snps,dwmac-4.20a”。在内核源码目录里搜索这两个属性值,我们就能找到drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c文件。dwmac-rk.c就是RK3568的网络驱动源码,后面再分析。
第3行,reg属性值表示网络寄存器的地址和长度。
第21行,snps,mixed-burst属性,表示DMA使用混合突发模式。
第22行,snps,tso属性,表示启动tso功能,tso全称为TCP Segment Offload,也就是利用网卡的少量处理能力,降低CPU发送数据包负载的技术,需要网卡硬件及驱动支持。
示例代码40.5.1.1是瑞芯微官方编写的,我们还需要根据实际情况添加或者修改一些属性。打开arch/arm64/boot/dts/rockchip/rk3568-atk-evb1-ddr4-v10.dtsi,找到如下内容:
示例代码 40.5.1.2 rk3568-atk-evb1-ddr4-v10.dtsi
1 &gmac0 {
2 phy-mode = “rgmii”;
3 clock_in_out = “output”;
4
5 snps,reset-gpio = <&gpio2 RK_PD3 GPIO_ACTIVE_LOW>;
6 snps,reset-active-low;
7 /* Reset time is 20ms, 100ms for rtl8211f */
8 snps,reset-delays-us = <0 20000 100000>;
9
10 assigned-clocks = <&cru SCLK_GMAC0_RX_TX>, <&cru SCLK_GMAC0>;
11 assigned-clock-parents = <&cru SCLK_GMAC0_RGMII_SPEED>,
<&cru CLK_MAC0_2TOP>;
12 assigned-clock-rates = <0>, <125000000>;
13
14 pinctrl-names = “default”;
15 pinctrl-0 = <&gmac0_miim
16 &gmac0_tx_bus2
17 &gmac0_rx_bus2
18 &gmac0_rgmii_clk
19 &gmac0_rgmii_bus>;
20
21 tx_delay = <0x3c>;
22 rx_delay = <0x2f>;
23
24 phy-handle = <&rgmii_phy0>;
25 status = “okay”;
26};
从示例代码40.5.1.2中可以看到,gmac0就是我们要追加的内容,接下来我们看看这些追加属性表示什么意思。
第2行,phy-mode表示支持RMII和RGMII模式。
第5行,snps,reset-gpio表示PHY复位的GPIO。
第6行,snps,reset-active-low表示复位PHY的GPIO低电平有效。
第10行,assigned-clocks表示MAC的时钟源。
第11行,assinged-clock-parents表示MAC父时钟由cru SCLK_GMAC0_RGMII_SPEED和cru CLK_MAC0_2TOP提供。
第24行,phy-handle表示连接到此网络设备的PHY芯片句柄。
第25行,使用gmac0功能。
以上是RK3568的ETH0的设备节点描述,关于ETH1的节点描述这里就不重复描述了,原理与ETH0是一样的。
40.5.2 RK3568网络驱动源码简析
1、重要的结构体
每个驱动都会定义自己的结构体,要了解驱动的注册过程,就先从自定义结构体看起。前面我们说了驱动文件为:drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c,打开dwmac-rk.c文件,找到如下内容:
示例代码 40.5.2.1 rk_priv_data 结构体
1 struct rk_priv_data {
2 struct platform_device *pdev;
3 int phy_iface;
4 int bus_id;
5 struct regulator *regulator;
6 bool suspended;
7 const struct rk_gmac_ops *ops;
8
9 bool clk_enabled;
10 bool clock_input;
11 bool integrated_phy;
12
13 struct clk *clk_mac;
14 struct clk *gmac_clkin;
15 struct clk *mac_clk_rx;
16 struct clk *mac_clk_tx;
17 struct clk *clk_mac_ref;
18 struct clk *clk_mac_refout;
19 struct clk *clk_mac_speed;
20 struct clk *aclk_mac;
21 struct clk *pclk_mac;
22 struct clk *clk_phy;
23 struct clk *pclk_xpcs;
24
25 struct reset_control *phy_reset;
26
27 int tx_delay;
28 int rx_delay;
29
30 struct regmap *grf;
31 struct regmap *xpcs;
32};
示例代码40.5.2.1中,主要是第7行,ops成员,表示网络相关的操作集,驱动人员要实现的,瑞芯微官方已经写好了;其他属性成员都是时钟相关的,肯定是通过设备树配置时钟。接下来了解一下rk_gmac_ops结构体代码如下所示:
示例代码 40.5.2.2 rk_gmac_ops 结构体
1 struct rk_gmac_ops {
2 void (*set_to_rgmii)(struct rk_priv_data *bsp_priv,
3 int tx_delay, int rx_delay);
4 void (*set_to_rmii)(struct rk_priv_data *bsp_priv);
5 void (*set_to_sgmii)(struct rk_priv_data *bsp_priv);
6 void (*set_to_qsgmii)(struct rk_priv_data *bsp_priv);
7 void (*set_rgmii_speed)(struct rk_priv_data *bsp_priv, int speed);
8 void (*set_rmii_speed)(struct rk_priv_data *bsp_priv, int speed);
9 void (*set_sgmii_speed)(struct rk_priv_data *bsp_priv, int speed);
10 void (*integrated_phy_powerup)(struct rk_priv_data bsp_priv);
11};
大家都知道单片机要使用网络的时候,需要初始化网络相关的寄存器,rk_gmac_ops就是根据设备树参数去设置网络相关的寄存器,后面会分析此结构体。
plat_stmmacenet_data结构体很重要,将网络设备注册到内核的时候需要使用到此结构体。此结构体定义在 include/linux/stmmac.h,plat_stmmacenet_data 结构体原型如下示例代码所示:
示例代码 40.5.2.3 plat_stmmacenet_data 结构体
1 struct plat_stmmacenet_data {
2 int bus_id; / 网络的ID /
3 int phy_addr; / PHY地址 */
4 int interface;
5 phy_interface_t phy_interface;
6 struct stmmac_mdio_bus_data *mdio_bus_data;
7 struct device_node phy_node; / PHY的句柄 */
8 struct device_node *phylink_node;
9 struct device_node *mdio_node;
10 struct stmmac_dma_cfg *dma_cfg;
11 struct stmmac_est *est;
12 struct stmmac_fpe_cfg *fpe_cfg;
13 struct stmmac_safety_feature_cfg safety_feat_cfg;
14 int clk_csr;
15 int has_gmac;
16 int enh_desc;
17 int tx_coe;
18 int rx_coe;
19 int bugged_jumbo;
20 int pmt;
21 int force_sf_dma_mode;
22 int force_thresh_dma_mode;
23 int riwt_off;
24 int max_speed; / 支持最大的速率 */
25 …
示例代码 40.5.2.3 中的内容比较多,有省略,只列出了我们驱动要用到的一些参数。rk_priv_data和rk_gmac_ops这两个结构体主要是用初始化网络,plat_stmmacenet_data结构体主要用于注册网络设备。
2、rk_gmac_probe函数
RK3568网络驱动主要分为两部分:RK3568网络外设MAC驱动以及PHY芯片驱动,MAC驱动是瑞芯微编写的,PHY芯片有通用驱动文件,有些PHY芯片厂商还会针对自己的芯片编写对应的PHY驱动。总体来说,SOC内置网络MAC+外置PHY芯片这种方案是不需要编写什么驱动,基本可以直接使用。但是为了学习,我们还是要简单分析一下具体的网络驱动编写过程。打开dwmac-rk.c,找到如下所示内容:
示例代码 40.5.2.4网络平台驱动匹配表
1 static const struct of_device_id rk_gmac_dwmac_match[] = {
…
32#ifdef CONFIG_CPU_RK3568
33 { .compatible = “rockchip,rk3568-gmac”, .data = &rk3568_ops },
34#endif
35#ifdef CONFIG_CPU_RV110X
36 { .compatible = “rockchip,rv1108-gmac”, .data = &rv1108_ops },
37#endif
38#ifdef CONFIG_CPU_RV1126
39 { .compatible = “rockchip,rv1126-gmac”, .data = &rv1126_ops },
40#endif
41 { }
42};
43 MODULE_DEVICE_TABLE(of, rk_gmac_dwmac_match);
44
45 static struct platform_driver rk_gmac_dwmac_driver = {
46 .probe = rk_gmac_probe,
47 .remove = rk_gmac_remove,
48 .driver = {
49 .name = “rk_gmac-dwmac”,
50 .pm = &rk_gmac_pm_ops,
51 .of_match_table = rk_gmac_dwmac_match,
52 },
53};
54 module_platform_driver(rk_gmac_dwmac_driver);
第33行,匹配表包括“rockchip,rk3368-gmac”,因此设备树和驱动匹配上,当匹配成功以后。第46行的rk_gmac_probe函数就会执行,我们简单分析一下rk_gmac_porbe函数,函数内容如下:
示例代码40.5.2.5 rk_gmac_probe函数
1 static int rk_gmac_probe(struct platform_device *pdev)
2 {
3 struct plat_stmmacenet_data *plat_dat;
4 struct stmmac_resources stmmac_res;
5 const struct rk_gmac_ops *data;
6 int ret;
7
8 data = of_device_get_match_data(&pdev->dev);
9 if (!data) {
10 dev_err(&pdev->dev, “no of match data provided\n”);
11 return -EINVAL;
12 }
13
14 ret = stmmac_get_platform_resources(pdev, &stmmac_res);
15 if (ret)
16 return ret;
17
18 plat_dat = stmmac_probe_config_dt(pdev, &stmmac_res.mac);
19 if (IS_ERR(plat_dat))
20 return PTR_ERR(plat_dat);
21
22 if (!of_device_is_compatible(pdev->dev.of_node, “snps,dwmac-4.20a”))
23 plat_dat->has_gmac = true;
24
25 plat_dat->fix_mac_speed = rk_fix_speed;
26 plat_dat->get_eth_addr = rk_get_eth_addr;
27
28 plat_dat->bsp_priv = rk_gmac_setup(pdev, plat_dat, data);
29 if (IS_ERR(plat_dat->bsp_priv)) {
30 ret = PTR_ERR(plat_dat->bsp_priv);
31 goto err_remove_config_dt;
32 }
33
34 ret = rk_gmac_clk_init(plat_dat);
35 if (ret)
36 goto err_remove_config_dt;
37
38 ret = rk_gmac_powerup(plat_dat->bsp_priv);
39 if (ret)
40 goto err_remove_config_dt;
41
42 ret = stmmac_dvr_probe(&pdev->dev, plat_dat, &stmmac_res);
43 if (ret)
44 goto err_gmac_powerdown;
45
46 ret = dwmac_rk_create_loopback_sysfs(&pdev->dev);
47 if (ret)
48 goto err_gmac_powerdown;
49
50 return 0;
51
52 err_gmac_powerdown:
53 rk_gmac_powerdown(plat_dat->bsp_priv);
54 err_remove_config_dt:
55 stmmac_remove_config_dt(pdev, plat_dat);
56
57 return ret;
58 }
第14行,使用stmmac_get_platform_resources函数获取设备树上的资源,保存到 struct stmmac_resources类型的结构体里。
第18行,plat_dat变量指针类型为plat_stmmacenet_data结构体,plat_dat结构体主要是保存网络设备的参数,根据plat_dat的参数将网络设备注册进内核。stmmac_probe_config_dt函数作用是根据设备树上的属性值去填充plat_dat各个成员。stmmac_probe_config_dt函数内容如下示例代码所示(有省略):
示例代码40.5.2.6 stmmac_probe_config_dt 函数
1 struct plat_stmmacenet_data *
2 stmmac_probe_config_dt(struct platform_device *pdev,
const char **mac)
3 {
4 struct device_node *np = pdev->dev.of_node;
5 struct plat_stmmacenet_data *plat;
6 struct stmmac_dma_cfg *dma_cfg;
7 int rc;
8
9 plat = devm_kzalloc(&pdev->dev, sizeof(*plat), GFP_KERNEL);
10 if (!plat)
11 return ERR_PTR(-ENOMEM);
12
13 mac = of_get_mac_address(np);
14 plat->interface = of_get_phy_mode(np);
15
16 / Get max speed of operation from device tree /
17 if (of_property_read_u32(np, “max-speed”, &plat->max_speed))
18 plat->max_speed = -1;
19
20 plat->bus_id = of_alias_get_id(np, “ethernet”);
21 if (plat->bus_id < 0)
22 plat->bus_id = 0;
23
24 / Default to phy auto-detection */
25 plat->phy_addr = -1;
…
34 rc = stmmac_dt_phy(plat, np, &pdev->dev);
…
192 return plat;
193
194 error_hw_init:
195 clk_disable_unprepare(plat->pclk);
196 error_pclk_get:
197 clk_disable_unprepare(plat->stmmac_clk);
198
199 return ERR_PTR(-EPROBE_DEFER);
200}
stmmac_probe_config_dt函数会新建一个plat_dat指针变量,从设备树获取数据初始化palt_dat里的成员,返回plat_dat的指针地址。
第13行,使用of_get_mac_address函数从设备树获取MAC地址,这里我们没有定义。
第14行,使用of_get_phy_mode函数从设备树获取接口模式,获取phy-mode属性值。这里属性值为“rgmii-id”。
第20行,获取网络的别名。
第25行,设置plat结构体里的phy_addr成员,phy_addr设置为“-1”,phy_addr表示PHY的地址。设置为-1表示自动检测,一个 MDIO接口最多可以连接32个PHY芯片,设置为-1就是自动遍历所有的PHY地址,看看哪个地址上有PHY芯片,然后将对应的PHY地址重新赋值给phy_addr。
第34行,使用stmmac_dt_phy函数去获取“mdio”节点中的信息。
我们继续返回示例代码40.5.2.4中的进行解释。
第376行,给dwmac结构体开空间。
第28行,rk_gmac_setup函数主要是给rk_priv_data结构体分配空间,并把获取到的data值赋值给rk_priv_data结构体里面ops成员,我们初始化网络的时候,可以调用ops就能操作到网络相关的寄存器。
第42行,使用stmmac_dvr_probe函数主要是负责网络注册,同时完成GMII/RGMII接口初始化。
stmmac_dvr_probe函数调用netif_napi_add函数来设置poll函数,如图40.5.2.7所示:
图40.5.2.7 设置poll函数
从图40.5.2.7可以知道,通过netif_napi_add函数向网卡加了一个napi示例,使用NAPI驱动要提供一个poll函数来轮询处理接收数据。
继续分析stmmac_dvr_probe函数,此函数会调用stmmac_mdio_register函数来向内核注册MDIO总线,stmmac_mdio_register函数重点是图40.5.2.8中的两行代码:
图40.5.2.8 mdio读写函数
new_bus下的read和write这两个成员变量分别是读/写PHY寄存器的操作函数,这设置为stmmac_mdio_read和stmmac_mdio_write,这两个函数就是读写PHY内部寄存器的函数。读写PHY寄存器都会通过这两个MDIO总线函数完成。stmmac_mdio_register函数最终会向Linux内核注册MDIO总线,相关代码如下所示:
示例代码40.5.2.9 stmmac_mdio_register函数注册mdio总线
1 err = of_mdiobus_register(new_bus, mdio_node);
2 if (err != 0) {
3 dev_err(dev, “Cannot register the MDIO bus\n”);
4 goto bus_register_fail;
5 }
6
7 if (priv->plat->phy_node || mdio_node)
8 goto bus_register_done;
9
10 found = 0;
11 for (addr = 0; addr < max_addr; addr++) {
12 struct phy_device *phydev = mdiobus_get_phy(new_bus, addr);
13
14 if (!phydev)
15 continue;
16
17 if (!mdio_bus_data->irqs &&
18 (mdio_bus_data->probed_phy_irq > 0)) {
19 new_bus->irq[addr] = mdio_bus_data->probed_phy_irq;
20 phydev->irq = mdio_bus_data->probed_phy_irq;
21 }
22
23 if (priv->plat->phy_addr == -1)
24 priv->plat->phy_addr = addr;
25
26 phy_attached_info(phydev);
27 found = 1;
28 }
第1行,通过of_mdiobus_register向内核注册MDIO总线,如果设备树定义了mdio节点和PHY句柄还会注册PHY设备。
第7行,判断PHY设备是否注册成功,没有就遍历所有的PHY地址找到对应的PHY设备。
第10~28行,如果设备树里没有mdio节点和PHY句柄,通过遍历所有的PHY地址,找到硬件所对应的PHY设备。
最后stmmac_dvr_probe函数调用register_netdev函数进行网络设备注册。
3、MDIO总线注册
MDIO我们讲了很多次了,就是用来管理PHY芯片,分为MDIO和MDC两根线,Linux内核专门为MDIO准备一个总线,叫做MDIO总线,采用mii_bus结构体表示,定义在include/linux/phy.h文件中,mii_bus结构体如下所示(限于篇幅,有省略):
示例代码40.5.2.10 mii_bus结构体
1 struct mii_bus {
2 struct module *owner;
3 const char *name;
3 char id[MII_BUS_ID_SIZE];
4 void *priv;
5 int (*read)(struct mii_bus *bus, int addr, int regnum);
5 int (*write)(struct mii_bus *bus, int addr, int regnum,
u16 val);
6 int (*reset)(struct mii_bus *bus);
…
44 };
重点是第5、6两行的read和write函数,这两个函数就是读/写 PHY芯片的操作函数,不同的SoC其MDIO主控部分是不一样的,因此需要驱动编写人员去编写。我们前面在分析stmmac_dvr_probe函数的时候已经讲过了,stmmac_dvr_probe函数会调用stmmac_mdio_register函数完成MII接口的初始化,其中就包括初始化mii_bus下的read和write这两个函数。最终通过of_mdiobus_register或者mdiobus_register函数将初始化以后的mii_bus注册到Linux内核,of_mdiobus_register函数其实也是调用的mdiobus_register函数来完成mii_bus注册的。我们先看下of_mdiobus_register函数,如下示例代码所示:
示例代码40.5.2.11 of_mdiobus_register函数
1 int of_mdiobus_register(struct mii_bus *mdio, struct device_node *np)
2 {
3 struct device_node child;
4 bool scanphys = false;
5 int addr, rc;
6
7 if (!np)
8 return mdiobus_register(mdio);
9
10 / Do not continue if the node is disabled /
11 if (!of_device_is_available(np))
12 return -ENODEV;
13
14 / Mask out all PHYs from auto probing. Instead the PHYs listed in
15 * the device tree are populated after the bus has been registered /
16 mdio->phy_mask = ~0;
17
18 mdio->dev.of_node = np;
19 mdio->dev.fwnode = of_fwnode_handle(np);
20
21 / Get bus level PHY reset GPIO details /
22 mdio->reset_delay_us = DEFAULT_GPIO_RESET_DELAY;
23 of_property_read_u32(np, “reset-delay-us”, &mdio->reset_delay_us);
24
25 / Register the MDIO bus /
26 rc = mdiobus_register(mdio);
27 if (rc)
28 return rc;
29
30 / Loop over the child nodes and register a phy_device for each phy */
31 for_each_available_child_of_node(np, child) {
32 addr = of_mdio_parse_addr(&mdio->dev, child);
33 if (addr < 0) {
34 scanphys = true;
35 continue;
36 }
37
38 if (of_mdiobus_child_is_phy(child))
39 rc = of_mdiobus_register_phy(mdio, child, addr);
40 else
41 rc = of_mdiobus_register_device(mdio, child, addr);
42
43 if (rc == -ENODEV)
44 dev_err(&mdio->dev,
45 “MDIO device at address %d is missing.\n”,
46 addr);
47 else if (rc)
48 goto unregister;
49 }
…
88}
of_mdiobus_register函数主要是注册mii总线(mii_bus),然后遍历所有的PHY地址,当找到对应的PHY芯片以后就会创建PHY设备,最后将这个PHY设备注册到内核中。使用mdiobus_scan函数进行注册PHY设备。
第8行,设备节点不存在的时候此行代码执行,本实验设备节点肯定存在,所以之类不会运行。
第26行,当设备树中mdio节点存在,就会将相关属性信息赋值给mdio参数,然后调用mdiobus_register向内核注册此mii总线。
第31行,轮询mdio节点下的所有phy子节点。
第39行,如果找到一个PHY子节点,就说明找到了一个PHY芯片,那么就调用of_mdiobus_register_phy函数向内核注册此PHY设备。
接下来简单分析一下of_mdiobus_register_phy函数,看看如何向Linux内核注册PHY设备的,of_mdiobus_register_phy函数内容如下所示:
示例代码40.5.2.12 of_mdiobus_register_phy函数
1 static int of_mdiobus_register_phy(struct mii_bus *mdio,
2 struct device_node *child, u32 addr)
3 {
4 struct phy_device phy;
5 bool is_c45;
6 int rc;
7 u32 phy_id;
8
9 is_c45 = of_device_is_compatible(child,
10 “ethernet-phy-ieee802.3-c45”);
11
12 if (!is_c45 && !of_get_phy_id(child, &phy_id))
13 phy = phy_device_create(mdio, addr, phy_id, 0, NULL);
14 else
15 phy = get_phy_device(mdio, addr, is_c45);
16 if (IS_ERR(phy))
17 return PTR_ERR(phy);
18
19 rc = of_irq_get(child, 0);
20 if (rc == -EPROBE_DEFER) {
21 phy_device_free(phy);
22 return rc;
23 }
24 if (rc > 0) {
25 phy->irq = rc;
26 mdio->irq[addr] = rc;
27 } else {
28 phy->irq = mdio->irq[addr];
29 }
30
31 if (of_property_read_bool(child, “broken-turn-around”))
32 mdio->phy_ignore_ta_mask |= 1 << addr;
33
34 of_property_read_u32(child, “reset-assert-us”,
35 &phy->mdio.reset_assert_delay);
36 of_property_read_u32(child, “reset-deassert-us”,
37 &phy->mdio.reset_deassert_delay);
38
39 / Associate the OF node with the device structure so it
40 * can be looked up later /
41 of_node_get(child);
42 phy->mdio.dev.of_node = child;
43 phy->mdio.dev.fwnode = of_fwnode_handle(child);
44
45 / All data is now stored in the phy struct;
46 * register it */
47 rc = phy_device_register(phy);
48 if (rc) {
49 phy_device_free(phy);
50 of_node_put(child);
51 return rc;
52 }
53
54 dev_dbg(&mdio->dev, “registered phy %pOFn at address %i\n”,
55 child, addr);
56 return 0;
57 }
第9行,使用函数of_device_is_compatible检查PHY节点的compatible属性是否为“ethernet-phy-ieee802.3-c45”,如果是的话要做其他的处理,这里没有设置此属性。
第15行,调用get_phy_device函数获取PHY设备,此函数里面会调用phy_device_create来创建一个phy_device设备并返回。
第19行,获取PHY芯片的中断信息,本章节并未用到。
第47行,调用phy_device_register函数向Linux内核注册PHY设备。
从上面的分析可以看出,向Linux内核注册MDIO总线的时候也会同时向Linux内核注册PHY设备,流程如图40.5.2.13所示:
图40.5.2.13 MDIO总线注册流程
注册MDIO总线的时候会先从设备树中查找PHY设备,没有就会遍历所有的PHY设备,然后通过phy_device_register函数向内核注册PHY设备。
4、rk_gmac_remove函数简析
示例代码40.5.2.14 rk_gmac_remove函数
1 static int rk_gmac_remove(struct platform_device *pdev)
2 {
3 struct rk_priv_data *bsp_priv = get_stmmac_bsp_priv(&pdev->dev);
4 int ret = stmmac_dvr_remove(&pdev->dev);
5
6 rk_gmac_powerdown(bsp_priv);
7 dwmac_rk_remove_loopback_sysfs(&pdev->dev);
8
9 return ret;
10}
第4行调用了stmmac_dvr_remove函数进行卸载,此函数内容如下所示:
示例代码40.5.2.15 rk_gmac_remove函数
1 int stmmac_dvr_remove(struct device *dev)
2 {
3 struct net_device *ndev = dev_get_drvdata(dev);
4 struct stmmac_priv *priv = netdev_priv(ndev);
5
6 netdev_info(priv->dev, “%s: removing driver”, func);
7
8 #ifdef CONFIG_DEBUG_FS
9 stmmac_exit_fs(ndev);
10#endif
11 stmmac_stop_all_dma(priv);
12
13 stmmac_mac_set(priv, priv->ioaddr, false);
14 netif_carrier_off(ndev);
15 unregister_netdev(ndev);
16 if (priv->plat->stmmac_rst)
17 reset_control_assert(priv->plat->stmmac_rst);
18 clk_disable_unprepare(priv->plat->pclk);
19 clk_disable_unprepare(priv->plat->stmmac_clk);
20 if (priv->hw->pcs != STMMAC_PCS_RGMII &&
21 priv->hw->pcs != STMMAC_PCS_TBI &&
22 priv->hw->pcs != STMMAC_PCS_RTBI)
23 stmmac_mdio_unregister(ndev);
24 destroy_workqueue(priv->wq);
25 mutex_destroy(&priv->lock);
26 free_netdev(ndev);
27
28 return 0;
29}
第11行,调用stmmac_stop_all_dma函数结束所有的DMA。
第15行,调用unregister_netdev函数注销前面注册的net_device。
第23行,调用stmmac_mdio_unregister函数来移除掉MDIO总线相关的内容,此函数会调用mdiobus_unregister来注销掉mii_bus,并且通过函数mdiobus_free释放掉mii_bus。
第24行,释放工作队列。
40.5.3 stmmac_dvr_probe函数简析
stmmac_dvr_probe函数设置了网卡驱动的net_dev_ops操作集为stmmac_netdev_ops,stmmac_netdev_ops内容如下:
示例代码40.5.3.1 stmmac_netdev_ops操作集
1 static const struct net_device_ops stmmac_netdev_ops = {
2 .ndo_open = stmmac_open,
3 .ndo_start_xmit = stmmac_xmit,
4 .ndo_stop = stmmac_release,
5 .ndo_change_mtu = stmmac_change_mtu,
6 .ndo_fix_features = stmmac_fix_features,
7 .ndo_set_features = stmmac_set_features,
8 .ndo_set_rx_mode = stmmac_set_rx_mode,
9 .ndo_tx_timeout = stmmac_tx_timeout,
10 .ndo_do_ioctl = stmmac_ioctl,
11 .ndo_setup_tc = stmmac_setup_tc,
12 .ndo_select_queue = stmmac_select_queue,
13 #ifdef CONFIG_NET_POLL_CONTROLLER
14 .ndo_poll_controller = stmmac_poll_controller,
15 #endif
16 .ndo_set_mac_address = stmmac_set_mac_address,
17 };
1、stmmac_open函数简析
打开一个网卡的时候stmmac_open函数就会执行,函数源码如下所示(限于篇幅原因,有省略):
示例代码40.5.3.2 stmmac_open函数
1 static int stmmac_open(struct net_device *dev)
2 {
3 struct stmmac_priv priv = netdev_priv(dev);
4 int bfsize = 0;
5 u32 chan;
6 int ret;
7
8 if (priv->hw->pcs != STMMAC_PCS_RGMII &&
9 priv->hw->pcs != STMMAC_PCS_TBI &&
10 priv->hw->pcs != STMMAC_PCS_RTBI) {
11 ret = stmmac_init_phy(dev);
12 if (ret) {
13 netdev_err(priv->dev,
14 “%s: Cannot attach to PHY (error: %d)\n”,
15 func, ret);
16 return ret;
17 }
18 }
…
36 ret = alloc_dma_desc_resources(priv);
37 if (ret < 0) {
38 netdev_err(priv->dev, “%s: DMA descriptors allocation
failed\n”,
39 func);
40 goto dma_desc_error;
41 }
42
43 ret = init_dma_desc_rings(dev, GFP_KERNEL);
44 if (ret < 0) {
45 netdev_err(priv->dev, “%s: DMA descriptors initialization
failed\n”,
46 func);
47 goto init_error;
48 }
49
50 ret = stmmac_hw_setup(dev, true);
51 if (ret < 0) {
52 netdev_err(priv->dev, “%s: Hw setup failed\n”, func);
53 goto init_error;
54 }
55
56 stmmac_init_coalesce(priv);
57
58 phylink_start(priv->phylink);
59
60 / Request the IRQ lines /
61 ret = request_irq(dev->irq, stmmac_interrupt,
62 IRQF_SHARED, dev->name, dev);
63 if (unlikely(ret < 0)) {
64 netdev_err(priv->dev,
65 “%s: ERROR: allocating the IRQ %d (error: %d)\n”,
66 func, dev->irq, ret);
67 goto irq_error;
68 }
69
70 / Request the Wake IRQ in case of another line is used for WoL /
71 if (priv->wol_irq != dev->irq) {
72 ret = request_irq(priv->wol_irq, stmmac_interrupt,
73 IRQF_SHARED, dev->name, dev);
74 if (unlikely(ret < 0)) {
75 netdev_err(priv->dev,
76 “%s: ERROR: allocating the WoL IRQ %d (%d)\n”,
77 func, priv->wol_irq, ret);
78 goto wolirq_error;
79 }
80 }
81
82 / Request the IRQ lines */
83 if (priv->lpi_irq > 0) {
84 ret = request_irq(priv->lpi_irq, stmmac_interrupt,
IRQF_SHARED,
85 dev->name, dev);
86 if (unlikely(ret < 0)) {
87 netdev_err(priv->dev,
88 “%s: ERROR: allocating the LPI IRQ %d (%d)\n”,
89 func, priv->lpi_irq, ret);
90 goto lpiirq_error;
91 }
92 }
93
94 stmmac_enable_all_queues(priv);
95 stmmac_start_all_queues(priv);
96
97 return 0;
98 };
第11行,看名字就知道了,初始化phy设备。
第36行,分配DMA资源,分为TX和RX两个DMA资源。
第43行,初始化TX和RX DMA描述符,并且分配socket缓冲区。
第50行,设置mac为可用状态,配置mac核心寄存器,然后DMA数据准备接收和发送。
第58行,启动phylink。
第60~92行,申请中断,中断函数为stmmac_interrupt,重点,后面会解析。
第94~95行,使能队列和开启队列。
2、stmmac_release函数简析
关闭网卡的时候stmmac_release函数就会执行,函数内容如下:
示例代码40.5.3.3 stmmac_release函数
1 static int stmmac_release(struct net_device *dev)
2 {
3 struct stmmac_priv priv = netdev_priv(dev);
4 u32 chan;
5
6 / Stop and disconnect the PHY /
7 if (dev->phydev) {
8 phy_stop(dev->phydev);
9 phy_disconnect(dev->phydev);
10 }
11
12 stmmac_disable_all_queues(priv);
13
14 for (chan = 0; chan < priv->plat->tx_queues_to_use; chan++)
15 del_timer_sync(&priv->tx_queue[chan].txtimer);
16
17 / Free the IRQ lines /
18 free_irq(dev->irq, dev);
19 if (priv->wol_irq != dev->irq)
20 free_irq(priv->wol_irq, dev);
21 if (priv->lpi_irq > 0)
22 free_irq(priv->lpi_irq, dev);
23
24 if (priv->eee_enabled) {
25 priv->tx_path_in_lpi_mode = false;
26 del_timer_sync(&priv->eee_ctrl_timer);
27 }
28
29 / Stop TX/RX DMA and clear the descriptors /
30 stmmac_stop_all_dma(priv);
31
32 / Release and free the Rx/Tx resources /
33 free_dma_desc_resources(priv);
34
35 / Disable the MAC Rx/Tx /
36 stmmac_mac_set(priv, priv->ioaddr, false);
37
38 netif_carrier_off(dev);
39
40 if (IS_ENABLED(CONFIG_STMMAC_PTP))
41 stmmac_release_ptp(priv);
42
43 return 0;
44}
第12行,关闭工作队列。
第30行,停止TX/RX的DMA。
第33行,释放TX/RX DMA资源。
第36行,关闭MAC TX/RX。
第38行,关闭网络。
40.5.4 Linux内核PHY子系统与MDIO总线简析
上一小节在讲解MDIO总线的时候讲过,注册MDIO总线的时候也会向内核注册PHY设备,本节我们就来简单了解一下PHY子系统。PHY子系统就是用于PHY设备相关内容的,分为PHY设备和PHY驱动,和platform总线一样,PHY子系统也是一个设备、总线和驱动模型。
1、PHY设备
首先看一下PHY设备,Linux内核使用phy_device结构体来表示PHY设备,结构体定义在include/linux/phy.h,结构体内容如下(为了缩小篇幅,有省略):
示例代码40.5.4.1 phy_device 结构体
1 struct phy_device {
2 struct mdio_device mdio; / mdio 设备 /
3
4 / Information about the PHY type /
5 / And management functions */
6 struct phy_driver drv; / PHY设备驱动 /
7
8 u32 phy_id; / PHY ID /
…
34 int speed; / 速度 /
35 int duplex; / 双共模式 /
36 int pause;
37 int asym_pause;
38
…
65 int irq; / 中断号 /
66
67 / private data pointer /
68 / For use by PHYs to maintain extra state */
69 void priv; / 私有数据 /
70
71 / Interrupt and Polling infrastructure */
72 struct work_struct phy_queue;
73 struct delayed_work state_queue;
74
75 struct mutex lock;
76
77 struct phylink *phylink;
78 struct net_device attached_dev; / PHY 芯片对应的网络设备 */
…
90};
一个PHY设备对应一个phy_device实例,然后需要向Linux内核注册这个实例。使用phy_device_register函数完成PHY设备的注册,函数原型如下:
int phy_device_register(struct phy_device *phy)
函数参数和返回值含义如下:
phy:需要注册的PHY设备。
返回值:0 成功,负值 失败。
PHY设备的注册过程一般是先调用get_phy_device函数获取PHY设备,此函数内容如下:
示例代码40.5.4.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 ®
9 return ERR_PTR®;
10
11 / If the phy_id is mostly Fs, there is no device there /
12 if ((phy_id & 0x1fffffff) == 0x1fffffff)
13 return ERR_PTR(-ENODEV);
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文件中,结构体内容如下(为了缩小篇幅,省略了注释部分):
示例代码40.5.4.3 phy_driver结构体
1 struct phy_driver {
2 struct mdio_driver_common mdiodrv;
3 u32 phy_id; / PHY ID */
4 char name;
5 u32 phy_id_mask; / PHY ID 掩码 */
6 u32 features;
7 u32 flags;
8 const void *driver_data;
9
10 int (*soft_reset)(struct phy_device *phydev);
11 int (*config_init)(struct phy_device *phydev);
12 int (*probe)(struct phy_device *phydev);
13 int (*get_features)(struct phy_device *phydev);
14 int (*suspend)(struct phy_device *phydev);
15 int (*resume)(struct phy_device *phydev);
16 int (*config_aneg)(struct phy_device *phydev);
17 int (*aneg_done)(struct phy_device *phydev);
18 int (*read_status)(struct phy_device *phydev);
19 int (*ack_interrupt)(struct phy_device *phydev);
20 int (*config_intr)(struct phy_device *phydev);
21 int (*did_interrupt)(struct phy_device *phydev);
22 int (*handle_interrupt)(struct phy_device *phydev);
23 void (*remove)(struct phy_device *phydev);
24 int (*match_phy_device)(struct phy_device *phydev);
25 int (*ts_info)(struct phy_device *phydev,
struct ethtool_ts_info *ti);
26 int (*hwtstamp)(struct phy_device *phydev, struct ifreq *ifr);
27 bool (*rxtstamp)(struct phy_device *dev, struct sk_buff *skb,
int type);
28 void (*txtstamp)(struct phy_device *dev, struct sk_buff *skb,
int type);
29 int (*set_wol)(struct phy_device *dev,
struct ethtool_wolinfo *wol);
30 void (*get_wol)(struct phy_device *dev,
struct ethtool_wolinfo *wol);
31 void (*link_change_notify)(struct phy_device *dev);
32 int (*read_mmd)(struct phy_device *dev, int devnum, u16 regnum);
33 int (*write_mmd)(struct phy_device *dev, int devnum,
u16 regnum,u16 val);
34 int (*read_page)(struct phy_device *dev);
35 int (*write_page)(struct phy_device *dev, int page);
36 int (*module_info)(struct phy_device *dev,
37 struct ethtool_modinfo *modinfo);
38 int (*module_eeprom)(struct phy_device *dev,
39 struct ethtool_eeprom *ee, u8 *data);
40 int (*get_sset_count)(struct phy_device *dev);
41 void (*get_strings)(struct phy_device *dev, u8 *data);
42 void (*get_stats)(struct phy_device *dev,
43 struct ethtool_stats *stats, u64 *data);
44 int (*get_tunable)(struct phy_device *dev,
45 struct ethtool_tunable *tuna, void *data);
46 int (*set_tunable)(struct phy_device *dev,
47 struct ethtool_tunable *tuna,
48 const void *data);
49 int (*set_loopback)(struct phy_device *dev, bool enable);
50 };
可以看出,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, struct module *owner);
函数参数和返回值含义如下:
new_driver:需要注册的PHY驱动。
owner:驱动模块所属的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,
struct module *owner);
函数参数和返回值含义如下:
new_driver:需要注册的多个PHY驱动数组。
n:要注册的驱动数量。
owner:驱动模块所属的PHY设备。
返回值:0 成功,负值 失败。
②、卸载PHY驱动
卸载PHY驱动的话使用phy_driver_unregister函数,函数原型如下:
void phy_driver_unregister(struct phy_driver *drv)
函数参数和返回值含义如下:
drv:需要卸载的PHY驱动。
返回值:无。
3、MDIO总线
前面说了,PHY子系统也是遵循设备、总线、驱动模型的,设备和驱动就是phy_device和phy_driver。总线就是MDIO总线,因为PHY芯片是通过MIDO接口来管理的,MDIO总线最主要的工作就是匹配PHY设备和PHY驱动。在文件drivers/net/phy/mdio_bus.c中有如下定义:
示例代码40.5.4.4 mdio 总线
1 struct bus_type mdio_bus_type = {
2 .name = “mdio_bus”,
3 .match = mdio_bus_match,
4 .uevent = mdio_uevent,
5 };
示例代码40.5.4.4定义了一个名为“mdio_bus_type”的总线,这个就是MDIO总线,总线的名字为“mdio_bus”,重点是总线的匹配函数为mdio_bus_match。此函数内容如下:
示例代码40.5.4.5 mdio_bus_match函数
1 static int mdio_bus_match(struct device *dev,
struct device_driver *drv)
2 {
3 struct mdio_device *mdio = to_mdio_device(dev);
4
5 if (of_driver_match_device(dev, drv))
6 return 1;
7
8 if (mdio->bus_match)
9 return mdio->bus_match(dev, drv);
10
11 return 0;
12 }
第5行,采用设备树的话先尝试使用 of_driver_match_device 来对设备和驱动进行匹配,也就是检查 compatible 属性值与匹配表 of_match_table 里面的内容是否一致。但是对于本章教程而言,并不是通过 of_driver_match_device 来完成 PHY 驱动和设备匹配的。
第8~9行,使用PHY驱动的匹配方法,会调用phy_bus_match函数,函数源码如下所示:
示例代码40.5.4.6 phy_bus_match函数
1 static int phy_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 const int num_ids = ARRAY_SIZE(phydev->c45_ids.device_ids);
6 int i;
7
8 if (!(phydrv->mdiodrv.flags & MDIO_DEVICE_IS_PHY))
9 return 0;
10
11 if (phydrv->match_phy_device)
12 return phydrv->match_phy_device(phydev);
13
14 if (phydev->is_c45) {
15 for (i = 1; i < num_ids; i++) {
16 if (phydev->c45_ids.device_ids[i] == 0xffffffff)
17 continue;
18
19 if ((phydrv->phy_id & phydrv->phy_id_mask) ==
20 (phydev->c45_ids.device_ids[i] &
21 phydrv->phy_id_mask))
22 return 1;
23 }
24 return 0;
25 } else {
26 return (phydrv->phy_id & phydrv->phy_id_mask) ==
27 (phydev->phy_id & phydrv->phy_id_mask);
28 }
29 }
第11、12行,检查PHY驱动有没有提供匹配函数match_phy_device,如果有的话就直接调用PHY驱动提供的匹配函数完成与设备的匹配。
第26、27行,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函数,内容如下:
示例代码40.5.4.7 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 features_init();
10
11 rc = phy_driver_register(&genphy_10g_driver, THIS_MODULE);
12 if (rc)
13 goto err_10g;
14
15 rc = phy_driver_register(&genphy_driver, THIS_MODULE);
16 if (rc) {
17 phy_driver_unregister(&genphy_10g_driver);
18 err_10g:
19 mdio_bus_exit();
20 }
21
22 return rc;
23 }
phy_init是整个PHY子系统的入口函数,第11行和15行都会调用phy_drivers_register函数向内核直接注册一个通用PHY驱动:genphy_c45_driver和genphy_driver;genphy_c45_driver为10G网络,genphy_driver为10/100/1000M网络。它们都是是通用PHY驱动,也就是说Linux系统启动以后默认就已经存在了通用PHY驱动。
genphy_c45_driver和genphy_driver这两个结构体,它们定义分别在drivers/net/phy/phy_device.c和drivers/net/phy/phy-c46.c这两个文件中,内容如下所示:
示例代码40.5.4.8 通用phy设备对象
1 struct phy_driver genphy_c45_driver = {
2 .phy_id = 0xffffffff,
3 .phy_id_mask = 0xffffffff,
4 .name = “Generic Clause 45 PHY”,
5 .soft_reset = genphy_no_soft_reset,
6 .read_status = genphy_c45_read_status,
7 };
8
9 static struct phy_driver genphy_driver = {
10 .phy_id = 0xffffffff,
11 .phy_id_mask = 0xffffffff,
12 .name = “Generic PHY”,
13 .soft_reset = genphy_no_soft_reset,
14 .get_features = genphy_read_abilities,
15 .aneg_done = genphy_aneg_done,
16 .suspend = genphy_suspend,
17 .resume = genphy_resume,
18 .set_loopback = genphy_loopback,
19 };
genphy_c45_driver为10G的PHY驱动,名字为“Generic Clause 45 PHY”,genphy_driver为10/100/1000M的PHY驱动,名字为“Generic PHY”。注意,很多专用PHY芯片的驱动程序中也会用到通用PHY驱动的一些函数。
40.5.5 YT8531 PHY驱动
打开drivers/net/phy/motorcomm.c文件,找到YT8531驱动结构体,代码如下所示:
示例代码40.5.4.9 YT8531结构体
1 static struct phy_driver ytphy_drvs[] = {
2 {
…
136 }, {
137 / same as 8511 */
138 .phy_id = PHY_ID_YT8531,
139 .name = “YT8531 Gigabit Ethernet”,
140 .phy_id_mask = MOTORCOMM_PHY_ID_MASK,
141 .features = PHY_GBIT_FEATURES,
142 .flags = PHY_POLL,
143 .config_aneg = genphy_config_aneg,
144
145 .config_init = yt8531_config_init,
146 .read_status = genphy_read_status,
147 .suspend = genphy_suspend,
148 .resume = genphy_resume,
149#if (YTPHY_WOL_FEATURE_ENABLE)
150 .get_wol = &ytphy_wol_feature_get,
151 .set_wol = &ytphy_wol_feature_set,
152#endif
153 }, {
…
215};
第138行,PHY_ID_YT8531是一个宏,定义是0x4f51e91b。
第139行,驱动名字为“YT8531 Gigabit Ethernet”,系统启动过程中,加载网络设备驱动时候就会提示电气PHY驱动文字为“YT8531 Gigabit Ethernet”。
第140行,PHY的ID掩码,MOTORCOMM_PHY_ID_MASK是一个宏,定义为0x00000fff,也就是前12位有效,在进行匹配的时候只需要比较前12位。
40.6 网络驱动实验测试
40.6.1 YT8531 PHY驱动测试
原子的出厂系统默认是启动了网络驱动的,当系统启动的时候会打印当前PHY驱动名字为“YT8531 Gigabit Ethernet”,如下图所示:
图40.6.1.1 YT8531 PHY驱动信息
40.6.2 千兆以太网网速测试
注意:这里要连接千兆路由器或者千兆交换机,没有这两个设备可以开发板和电脑直接连接。
ATK-DLRK3568网络接口为千兆网络,理论最大通信速率可达1000mb/s,YT8531也是千兆 PHY芯片。本小节我们就来测试下这个网口的速率,此时过程我们使用开发板和电脑都是连接千兆路由器的方式,使用千兆网线,比如CAT-5E类网线或CAT-6类网线。
Linux 下网络速率测试可以使用 iper3 工具,iperf3 是一个网络性能测试工具,可以测试 TCP 和 UDP 的带宽质量、测量 TCP 最大带宽、报告带宽、延迟抖动和数据丢包等;这里我们仅仅只是使用 iperf3 工具测试开发板上网口的带宽,其它的一些参数特性就不给大家进行详细测试了。
iperf3 的测试方法需要有一台主机作为服务器端,另一台主机作为客户端,客户端向服务器端发送数据,这里我们可以将 Ubuntu 系统作为服务器端,而开发板作为客户端进行测试。
在ubuntu系统中执行下面这条命令,将ubuntu系统作为iperf3的服务器,如下所示:
iperf3 -s
命令执行如下图所示:
图40.6.2.1 ubuntu作为服务器
-s 表示将其作为服务端。
接下来我们需要在开发板终端执行下面这条命令,将开发板作为客户端,并进行测试:
iperf3 -c 192.168.6.148
-c 选项表示将其作为客户端,后面紧跟着的就是服务器主机的 IP 地址,在这里也就是 Ubuntu 系统的 IP 地址,所以这个 IP 地址大家根据自己 Ubuntu 系统的 IP 进行填写即可,测试结果如下图所示:
图40.6.2.2 开发板客户端测试
图40.6.2.2中执行命令共打印出了 10 次带宽测试报告,每一秒钟报告一次带宽,耗费时间为 10 秒钟。最终测试出来发送和接收带宽分别为940M和935M,基本接近1000M的带宽,说明千M网络工作正常。