Linux 网络驱动实验(有线)

目录

  • 嵌入式网络简介
    • 嵌入式下的网络硬件接口
    • MII/RMII 接口
    • MDIO 接口
    • RJ45 接口
    • I.MX6ULL ENET 接口简介
  • PHY 芯片详解
    • PHY 基础知识简介
    • LAN8720A 详解
    • SR8201F 详解
  • Linux 内核网络驱动框架
    • net_device 结构体
    • net_device_ops 结构体
    • sk_buff 结构体
    • 网络NAPI 处理机制
  • I.MX6ULL 网络驱动简介
    • I.MX6ULL 网络外设设备树
    • I.MX6ULL 网络驱动源码简析
    • fec_netdev_ops 操作集
    • Linux 内核PHY 子系统与MDIO 总线简析
  • 网络驱动实验测试
    • LAN8720 PHY 驱动测试
    • 通用PHY 驱动测试
    • DHCP 功能配置
  • 单网卡使用
    • 只使用ENET2 网卡
    • 只使用ENET1 网卡

网络驱动是linux 里面驱动三巨头之一,linux 下的网络功能非常强大,嵌入式linux 中也常
常用到网络功能。前面我们已经讲过了字符设备驱动和块设备驱动,本章我们就来学习一下
linux 里面的网络设备驱动。

嵌入式网络简介

嵌入式下的网络硬件接口

本章节讨论的都是有线网络!
注意!正点原子I.MX6U-ALPHA开发板V2.4版本以前的底板使用的网络PHY 为LAN8720,
V2.4 及其以后的版本使用的网络PHY 为SR8201F,而且网络PHY 地址有改变,大家一定要
看准自己所使用的底板版本号!原理基本一致,本章就以LAN8720 为例进行讲解。
提起网络,我们一般想到的硬件就是“网卡”,“网卡”这个概念最早从电脑领域传出来,
顾名思义就是能上网的卡。在电脑领域的“原始社会”,网卡是独立的硬件,如果电脑要上网就
得买个网卡插上去,类似现在的显卡一样。但是大家现在观察自己的笔记本或者台式机主板会
发现并没有类似显卡一样的网卡设备,原因是随着技术的不断发展,现在只需要一个芯片就可
以实现有线网卡功能,因此网卡芯片都直接放到了主板上。所以大家在接触嵌入式的时候听到
“网卡”这两个字,不要急着在开发板上找“卡”一样的东西。

既然现在网卡已经是通过一个芯片来完成了,那么是什么样的芯片呢?这个就要先了解一
下嵌入式中的网络硬件方案。首先,嵌入式网络硬件分为两部分: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 所示:

Linux 网络驱动实验(有线)_第1张图片

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 所示:

Linux 网络驱动实验(有线)_第2张图片

大家在做项目的时候,如果要用到网络功能,强烈建议大家选择内部带有网络MAC 外设
的主控SOC!I.MX6ULL 就有两个10M/100M 的网络MAC 外设,正点原子ALPHA 开发板板
载了两颗PHY 芯片,型号为LAN8720 或SR8201F。因此,本章节只讲解SOC 内部MAC+外
置PHY 芯片这种方案。

MII/RMII 接口

前面我们说了,内部MAC 通过MII/RMII 接口来与外部的PHY 芯片连接,完成网络数据
传输,本节我们就来学习一下什么是MII 和RMII 接口。
1、MII 接口
MII 全称是Media Independent Interface,直译过来就是介质独立接口,它是IEEE-802.3 定
义的以太网标准接口,MII 接口用于以太网MAC 连接PHY 芯片,连接示意图如图69.1.2.1 所示:

Linux 网络驱动实验(有线)_第3张图片

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 所示:

Linux 网络驱动实验(有线)_第4张图片

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 芯片。

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 接口,另外可能
还需要复位、中断等其他引脚。

RJ45 接口

网络设备是通过网线连接起来的,插入网线的叫做RJ45 座,如图69.1.4.1 所示:

Linux 网络驱动实验(有线)_第5张图片

RJ45 座要与PHY 芯片连接在一起,但是中间需要一个网络变压器,网络变压器用于隔离
以及滤波等,网络变压器也是一个芯片,外形一般如图69.1.4.2 所示:

Linux 网络驱动实验(有线)_第6张图片

但是现在很多RJ45 座子内部已经集成了网络变压器,比如正点原子ALPHA 开发板所使用
的HR911105A 就是内置网络变压器的RJ45 座。内置网络变压器的RJ45 座和不内置的引脚一
样,但是一般不内置的RJ45 座会短一点。因此,大家在画板的时候一定要考虑你所使用的RJ45
座是否内置网络变压器,如果不内置的话就要自行添加网络变压器部分电路!!!同理,如果你
所设计的硬件是需要内置网络变压器的RJ45 座,肯定不能随便焊接一个不内置变压器的RJ45
座,否则网络工作不正常!
RJ45 座子上一般有两个灯,一个黄色(橙色),一个绿色,绿色亮的话表示网络连接正常,黄色闪烁的话说明当前正在进行网络通信。这两个灯由PHY 芯片控制,PHY 芯片会有两个引
脚来连接RJ45 座上的这两个灯。内部MAC+外部PHY+RJ45 座(内置网络变压器)就组成了一
个完整的嵌入式网络接口硬件,如图69.1.4.2 所示:

Linux 网络驱动实验(有线)_第7张图片

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 芯片以后
哪里需要调整。

PHY 芯片详解

PHY 基础知识简介

PHY 是IEEE 802.3 规定的一个标准模块,前面说了,SOC 可以对PHY 进行配置或者读取
PHY 相关状态,这个就需要PHY 内部寄存器去实现了。PHY 芯片寄存器地址空间为5 位,地
址0~31 共32 个寄存器,IEEE 定义了0~15 这16 个寄存器的功能,16~31 这16 个寄存器由厂
商自行实现。也就是说不管你用的哪个厂家的PHY 芯片,其中0~15 这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 所示:

Linux 网络驱动实验(有线)_第8张图片

关于这16 个寄存器的内容协议里面也进行了详细的讲解,这里就不分析了。我们后面会以
ALPHA 开发板所使用的LAN8720A/SR8201F 这两个PHY 为例,详细的分析一下PHY 芯片的
寄存器。

LAN8720A 详解

V2.4 版本以前的底板PHY 芯片采用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 所示。

Linux 网络驱动实验(有线)_第9张图片

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 所示。

在这里插入图片描述

正点原子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 所示。。

在这里插入图片描述

对于正点原子的ALPHA 开发板的两个LAN8720A 而言,全都工作在默认的REF_CLK In 模
式下。当LAN8720A 工作在REF_CLK In 模式时,50MHz 的外部时钟信号应接到LAN8720 的
XTAL1/CKIN 引脚(5 号引脚)上,如图69.2.2.3 所示:

Linux 网络驱动实验(有线)_第10张图片

为了降低成本,LAN8720A 可以从外部的25MHz 的晶振中产生REF_CLK 时钟。使用此功
能时应工作在REF_CLK Out 模式时。当工作在REF_CLO Out 模式时REF_CLK 的时钟源如图
69.2.2.4 所示。

Linux 网络驱动实验(有线)_第11张图片

前面说了,正点原子的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 所示。

Linux 网络驱动实验(有线)_第12张图片
Linux 网络驱动实验(有线)_第13张图片

我们说的配置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 所示:

Linux 网络驱动实验(有线)_第14张图片
Linux 网络驱动实验(有线)_第15张图片
Linux 网络驱动实验(有线)_第16张图片

从表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 所示:

Linux 网络驱动实验(有线)_第17张图片

LAN8720A 的ID 寄存器2 如表69.2.2.5 所示:

在这里插入图片描述

ID 寄存器3 如表69.2.2.6 所示:

Linux 网络驱动实验(有线)_第18张图片

最后来看一下LAN8720A 的特殊控制/状态寄存器,此寄存器地址为31,寄存器内容是
LAN8720A 厂商自定义的,此寄存器的各个位如表69.2.2.7 所示:

在这里插入图片描述
Linux 网络驱动实验(有线)_第19张图片

特殊控制/状态寄存器中我们关心的是bit2~bit4 这三位,因为通过这3 位来确定连接的状态
和速度,关于LAN8720A 这个PHY 就讲解到这里。

SR8201F 详解

V2.4 及以后版本的底板PHY 芯片采用SR8201F,因此请看本小节!
1、SR8201F 简介
SR8201F 是单芯片/单端口的10/100Mbps 以太网PHY 收发器。芯片支持RMII (简化介质独
立)接口,SR8201F 实现了所有10/100M 以太网物理层,包括:物理编码子层(PCS),物理介质
接入层(PMA),双绞线物理介质相关子层(TP-PMD),同时芯片支持端口自动翻转(Auto-MDIX)。
SR8201F 的主要特点如下:

· 支持IEEE 802.3az-2010(EEE)
· 兼容100Base-TX IEEE 802.3u
· 兼容10Base-T IEEE 802.3
· 支持RMII 模式
· 全/半双工工作
· 支持自协商模式
· 支持Auto-MDIX
· 支持中断功能
· SR8201F 提供两个网络状态的LED
·支持25MHz 晶振或外部振荡输入
· RMII 模式支持50MHz 外部时钟输入

SR8201F 功能框图如图69.2.3.1 所示。

Linux 网络驱动实验(有线)_第20张图片

2、SR8201F 中断管理
SR8201F 检测到媒介端的状态改变,就会写入对应中断状态的寄存器(第0 页寄存器14),
并且在中断管脚使能时,中断管脚会被置低表示发生一个中断事件。当MAC 检测到中断事件
时,可以通过MDC/MDIO 端口访问到第0 页寄存器14 获取相应的中断状态。正点原子的
ALPHA 开发板虽然将SR8201F 的中断引脚连接到了I.MX6ULL 上,但是并没有使用中断功能
关于中断的具体用法可以参考SR8201F 数据手册。
3、PHY 地址设置
MAC 层通过MDIO/MDC 总线对PHY 进行读写操作,MDIO 最多可以控制32 个PHY 芯
片,通过不同的PHY 芯片地址来对不同的PHY 操作。SR8201F 通过设置LED0/PHYAD[0]和
LED1/PHYAD[1]这两个引脚来设置其PHY 地址,其地址设置如表69.2.3.1 所示。

Linux 网络驱动实验(有线)_第21张图片

正点原子ALPHA 开发板ENET1 网络的SR8201F 上的LED1/PHYAD1 引脚上拉,
LED0/PHYDAD0 引脚下来下拉,因此ENET1 上的SR8201F 地址为0X02。ENET2 网络上的
SR8201F 的LED1/PHYAD1 引脚下拉,LED0/PHYAD0 引脚上拉,因此ENET2 上的SR8201F
地址为1。

4、SR8021F 内部寄存器
SR8201F 的前16 个寄存器满足IEEE 的要求,在这里我们只介绍几个常用的寄存器,首先是
BCR(Basic Control Rgsister)寄存器,地址为0,BCR 寄存器各位如表69.2.3.2 所示。

Linux 网络驱动实验(有线)_第22张图片

我们说的配置PHY 芯片,重点就是配置BCR 寄存器,由于SR8201F 是个10/100M 的PHY,
因此表69.2.3.3 中没有体现出1000M 相关的配置。但是10/100M 相关的配置是和IEEE 的规定
完全相符的,大家可以选择一个其他的10/100M 的PHY 芯片对比看一下,比如NXP 官方EVK
开发板使用的KSZ8081。
接下来看一下BSR(Basic Status Register)寄存器,地址为1。此寄存器为PHY 的状态寄存
器,通过此寄存器可以获取到PHY 芯片的工作状态,BSR 寄存器各位如表69.2.3.4 所示:

Linux 网络驱动实验(有线)_第23张图片

从表69.2.3.4 可以看出,和IEEE 标准规定相比,SR8201F 的BSR 寄存器少了几个位,这
个没关系的,不管什么PHY 芯片,只要它实现了的位和IEEE 规定相符就行。通过读取BSR 寄
存器的值我们可以得到当前的连接速度、双工状态和连接状态等。
接下来看一下SR8201F 的PHY ID 寄存器1 和ID 寄存器2,地址为2 和3,后面就称为寄
存器2 和寄存器3。这两个寄存器都是PHY 的ID 寄存器。IEEE 规定寄存器2 和寄存器3 为
PHY 的ID 寄存器,这两个寄存器组成一个32 位的唯一ID 值。
SR8201F 的ID 寄存器2 如表69.2.3.5 所示:

在这里插入图片描述

ID 寄存器3 如表69.2.3.6 所示:

Linux 网络驱动实验(有线)_第24张图片

关于SR8201F 的PHY 寄存器就简单讲这几个,其他的寄存器请自行查阅SR8201F 数据手
册。

Linux 内核网络驱动框架

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 = 1<<0, /* sysfs */
3 IFF_BROADCAST = 1<<1, /* volatile */
4 IFF_DEBUG = 1<<2, /* sysfs */
5 IFF_LOOPBACK = 1<<3, /* volatile */
6 IFF_POINTOPOINT = 1<<4, /* volatile */
7 IFF_NOTRAILERS = 1<<5, /* sysfs */
8 IFF_RUNNING = 1<<6, /* volatile */
9 IFF_NOARP = 1<<7, /* sysfs */
10 IFF_PROMISC = 1<<8, /* sysfs */
11 IFF_ALLMULTI = 1<<9, /* sysfs */
12 IFF_MASTER = 1<<10, /* volatile */
13 IFF_SLAVE = 1<<11, /* volatile */
14 IFF_MULTICAST = 1<<12, /* sysfs */
15 IFF_PORTSEL = 1<<13, /* sysfs */
16 IFF_AUTOMEDIA = 1<<14, /* sysfs */
17 IFF_DYNAMIC = 1<<15, /* sysfs */
18 IFF_LOWER_UP = 1<<16, /* volatile */
19 IFF_DORMANT = 1<<17, /* volatile */
20 IFF_ECHO = 1<<18, /* volatile */
21 };

继续回到示例代码69.3.1.1 接着看net_device 结构体。
第77 行:if_port 指定接口的端口类型,如果设备支持多端口的话就通过if_port 来指定所
使用的端口类型。可选的端口类型定义在include/uapi/linux/netdevice.h 中,为一个枚举类型,如
下所示:

示例代码69.3.1.3 端口类型
1 enum {
2 IF_PORT_UNKNOWN = 0,
3 IF_PORT_10BASE2,
4 IF_PORT_10BASET,
5 IF_PORT_AUI,
6 IF_PORT_100BASET,
7 IF_PORT_100BASETX,
8 IF_PORT_100BASEFX
9 };

第78 行:dma 是网络设备所使用的DMA 通道,不是所有的设备都会用到DMA。
第80 行:mtu 是网络最大传输单元,为1500。
第81 行:type 用于指定ARP 模块的类型,以太网的ARP 接口为ARPHRD_ETHER,Linux
内核所支持的ARP 协议定义在include/uapi/linux/if_arp.h 中,大家自行查阅。
第88 行:perm_addr 是永久的硬件地址,如果某个网卡设备有永久的硬件地址,那么就会填充perm_addr。
第90 行:addr_len 是硬件地址长度。
第133 行:last_rx 是最后接收的数据包时间戳,记录的是jiffies。
第136 行:dev_addr 也是硬件地址,是当前分配的MAC 地址,可以通过软件修改。
第140 行:_rx 是接收队列。
第142 行:num_rx_queues 是接收队列数量,在调用register_netdev 注册网络设备的时候会
分配指定数量的接收队列。
第143 行:real_num_rx_queues 是当前活动的队列数量。
第161 行:_tx 是发送队列。
第162 行:num_tx_queues 是发送队列数量,通过alloc_netdev_mq 函数分配指定数量的发
送队列。
第163 行:real_num_tx_queues 是当前有效的发送队列数量。
第179 行:trans_start 是最后的数据包发送的时间戳,记录的是jiffies。
第248 行:phydev 是对应的PHY 设备。

1、申请net_device
编写网络驱动的时候首先要申请net_device,使用alloc_netdev 函数来申请net_device,这
是一个宏,宏定义如下:

示例代码69.3.1.4 alloc_netdev
1 #define alloc_netdev(sizeof_priv, name, name_assign_type, setup) \
2 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,
										void (*setup) (struct net_device *))
										unsigned int txqs,
										unsigned int rxqs);

函数参数和返回值含义如下:
sizeof_priv:私有数据块大小。
name:设备名字。
setup:回调函数,初始化设备的设备后调用此函数。
txqs:分配的发送队列数量。
rxqs:分配的接收队列数量。
返回值:如果申请成功的话就返回申请到的net_device 指针,失败的话就返回NULL。
事实上网络设备有多种,大家不要以为就只有以太网一种。Linux 内核内核支持的网络接
口有很多,比如光纤分布式数据接口(FDDI)、以太网设备(Ethernet)、红外数据接口(InDA)、高
性能并行接口(HPPI)、CAN 网络等。内核针对不同的网络设备在alloc_netdev 的基础上提供了
一层封装,比如我们本章讲解的以太网,针对以太网封装的net_device 申请函数是alloc_etherdev
和,这也是一个宏,内容如下:

示例代码69.3.1.5 alloc_etherdev 函数
1 #define alloc_etherdev(sizeof_priv) alloc_etherdev_mq(sizeof_priv, 1)
2 #define alloc_etherdev_mq(sizeof_priv, count) alloc_etherdev_mqs(sizeof_priv, count, count)

可以看出,alloc_etherdev 最终依靠的是alloc_etherdev_mqs 函数,此函数就是对alloc_netdev_mqs 的简单封装,函数内容如下:

示例代码69.3.1.6 alloc_etherdev_mqs 函数
1 struct net_device *alloc_etherdev_mqs(int sizeof_priv,
2 unsigned int txqs,
3 unsigned int rxqs)
4 {
5 return alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_UNKNOWN,
6 ether_setup, txqs, rxqs);
7 }

第5 行调用alloc_netdev_mqs 来申请net_device,注意这里设置网卡的名字为“eth%d”,这
是格式化字符串,大家进入开发板的linux 系统以后看到的“eth0”、“eth1”这样的网卡名字就
是从这里来的。同样的,这里设置了以太网的setup 函数为ether_setup,不同的网络设备其setup
函数不同,比如CAN 网络里面setup 函数就是can_setup。
ether_setup 函数会对net_device 做初步的初始化,函数内容如下所示:

示例代码69.3.1.7 ether_setup 函数
1 void ether_setup(struct net_device *dev)
2 {
3 dev->header_ops = &eth_header_ops;
4 dev->type = ARPHRD_ETHER;
5 dev->hard_header_len = ETH_HLEN;
6 dev->mtu = ETH_DATA_LEN;
7 dev->addr_len = ETH_ALEN;
8 dev->tx_queue_len = 1000; /* Ethernet wants good queues */
9 dev->flags = IFF_BROADCAST|IFF_MULTICAST;
10 dev->priv_flags |= IFF_TX_SKB_SHARING;
11
12 eth_broadcast_addr(dev->broadcast);
13 }

关于net_device 的申请就讲解到这里,对于网络设备而言,使用alloc_etherdev 或
alloc_etherdev_mqs 来申请net_device。NXP 官方编写的网络驱动就是采用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 注册成功,负值注册失败。
4、注销net_device
既然有注册,那么必然有注销,注销net_device 使用函数unregister_netdev,函数原型如下:

void unregister_netdev(struct net_device *dev)

函数参数和返回值含义如下:
dev:要注销的net_device 指针。
返回值:无。

net_device_ops 结构体

net_device 有个非常重要的成员变量:netdev_ops,为net_device_ops 结构体指针类型,这
就是网络设备的操作集。net_device_ops 结构体定义在include/linux/netdevice.h 文件中,
net_device_ops 结构体里面都是一些以“ndo_”开头的函数,这些函数就需要网络驱动编写人员
去实现,不需要全部都实现,根据实际驱动情况实现其中一部分即可。结构体内容如下所示(结
构体比较大,这里有缩减):

示例代码69.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 u16 (*ndo_select_queue)(struct net_device *dev,
9 struct sk_buff *skb,
10 void *accel_priv,
11 select_queue_fallback_t fallback);
12 void (*ndo_change_rx_flags)(struct net_device *dev,
13 int flags);
14 void (*ndo_set_rx_mode)(struct net_device *dev);
15 int (*ndo_set_mac_address)(struct net_device *dev,
16 void *addr);
17 int (*ndo_validate_addr)(struct net_device *dev);
18 int (*ndo_do_ioctl)(struct net_device *dev,
19 struct ifreq *ifr, int cmd);
20 int (*ndo_set_config)(struct net_device *dev,
21 struct ifmap *map);
22 int (*ndo_change_mtu)(struct net_device *dev,
23 int new_mtu);
24 int (*ndo_neigh_setup)(struct net_device *dev,
25 struct neigh_parms *);
26 void (*ndo_tx_timeout) (struct net_device *dev);
......
36 #ifdef CONFIG_NET_POLL_CONTROLLER
37 void (*ndo_poll_controller)(struct net_device *dev);
38 int (*ndo_netpoll_setup)(struct net_device *dev,
39 struct netpoll_info *info);
40 void (*ndo_netpoll_cleanup)(struct net_device *dev);
41 #endif
......
104 int (*ndo_set_features)(struct net_device *dev,
105 netdev_features_t features);
......
166 };

第2 行:ndo_init 函数,当第一次注册网络设备的时候此函数会执行,设备可以在此函数中
做一些需要推后初始化的内容,不过一般驱动中不使用此函数,虚拟网络设备可能会使用。
第3 行:ndo_uninit 函数,卸载网络设备的时候此函数会执行。
第4 行:ndo_open 函数,打开网络设备的时候此函数会执行,网络驱动程序需要实现此函
数,非常重要!以NXP 的I.MX 系列SOC 网络驱动为例,会在此函数中做如下工作:
·使能网络外设时钟。
·申请网络所使用的环形缓冲区。
·初始化MAC 外设。
·绑定接口对应的PHY。
·如果使用NAPI 的话要使能NAPI 模块,通过napi_enable 函数来使能。
·开启PHY。
·调用netif_tx_start_all_queues 来使能传输队列,也可能调用netif_start_queue 函数。
·……
第5 行:ndo_stop 函数,关闭网络设备的时候此函数会执行,网络驱动程序也需要实现此
函数。以NXP 的I.MX 系列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,如果发送失败了我们就需要停止队列。
第8 行:ndo_select_queue 函数,当设备支持多传输队列的时候选择使用哪个队列。
第14 行:ndo_set_rx_mode 函数,此函数用于改变地址过滤列表,根据net_device 的flags

成员变量来设置SOC 的网络外设寄存器。比如flags 可能为IFF_PROMISC、IFF_ALLMULTI 或
IFF_MULTICAST,分别表示混杂模式、单播模式或多播模式。
第15 行:ndo_set_mac_address 函数,此函数用于修改网卡的MAC 地址,设置net_device
的dev_addr 成员变量,并且将MAC 地址写入到网络外设的硬件寄存器中。
第17 行:ndo_validate_addr 函数,验证MAC 地址是否合法,也就是验证net_device 的
dev_addr 中的MAC 地址是否合法,直接调用is_valid_ether_addr 函数。
第18 行:ndo_do_ioctl 函数,用户程序调用ioctl 的时候此函数就会执行,比如PHY 芯片
相关的命令操作,一般会直接调用phy_mii_ioctl 函数。
第22 行:ndo_change_mtu 函数,更改MTU 大小。
第26 行:ndo_tx_timeout 函数,当发送超时的时候函数会执行,一般都是网络出问题了导
致发送超时。一般可能会重启MAC 和PHY,重新开始数据发送等。
第37 行:ndo_poll_controller 函数,使用查询方式来处理网卡数据的收发。
第104 行:ndo_set_features 函数,修改net_device 的features 属性,设置相应的硬件属性。

sk_buff 结构体

网络是分层的,对于应用层而言不用关心具体的底层是如何工作的,只需要按照协议将要
发送或接收的数据打包好即可。打包好以后都通过dev_queue_xmit 函数将数据发送出去,接收
数据的话使用netif_rx 函数即可,我们依次来看一下这两个函数。
1、dev_queue_xmit 函数
此函数用于将网络数据发送出去,函数定义在include/linux/netdevice.h 中,函数原型如下:

static inline 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 就是网络驱
动编写人员去实现的,整个流程如图69.3.3.1 所示:

Linux 网络驱动实验(有线)_第25张图片

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 中,结构体内容如下(由于结构
体比较大,为了缩小篇幅只列出部分重要的内容):

示例代码69.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 ktime_t tstamp;
10 struct skb_mstamp skb_mstamp;
11 };
12 };
13 struct rb_node rbnode; /* used in netem & tcp stack */
14 };
15 struct sock *sk;
16 struct net_device *dev;
17
18 /*
19 * This is the control buffer. It is free to use for every
20 * layer. Please put your private variables there. If you
21 * want to keep them across layers you have to do a skb_clone()
22 * first. This is owned by whoever has the skb queued ATM.
23 */
24 char cb[48] __aligned(8);
25
26 unsigned long _skb_refdst;
27 void (*destructor)(struct sk_buff *skb);
......
37 unsigned int len, data_len;
38 __u16 mac_len, hdr_len;
......
145 __be16 protocol;
146 __u16 transport_header;
147 __u16 network_header;
148 __u16 mac_header;
149
150 /* private: */
151 __u32 headers_end[0];
152 /* public: */
153
154 /* These elements must be at the end, see alloc_skb() for details. */
155 sk_buff_data_t tail;
156 sk_buff_data_t end;
157 unsigned char *head, *data;
158 unsigned int truesize;
159 atomic_t users;
160 };

第5~6 行:next 和prev 分别指向下一个和前一个sk_buff,构成一个双向链表。
第9 行:tstamp 表示数据包接收时或准备发送时的时间戳。
第15 行:sk 表示当前sk_buff 所属的Socket。
第16 行:dev 表示当前sk_buff 从哪个设备接收到或者发出的。
第24 行:cb 为控制缓冲区,不管哪个层都可以自由使用此缓冲区,用于放置私有数据。
第27 行:destructor 函数,当释放缓冲区的时候可以在此函数里面完成某些动作。
第37 行:len 为实际的数据长度,包括主缓冲区中数据长度和分片中的数据长度。data_len
为数据长度,只计算分片中数据的长度。
第38 行:mac_len 为连接层头部长度,也就是MAC 头的长度。
第145 行:protocol 协议。
第146 行:transport_header 为传输层头部。
第147 行:network_header 为网络层头部
第148 行:mac_header 为链接层头部。
第155 行:tail 指向实际数据的尾部。
第156 行:end 指向缓冲区的尾部。
第157 行:head 指向缓冲区的头部,data 指向实际数据的头部。data 和tail 指向实际数据
的头部和尾部,head 和end 指向缓冲区的头部和尾部。结构如图69.3.3.2 所示:

Linux 网络驱动实验(有线)_第26张图片

针对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 操作之前和操作之后的数据区如图69.3.3.3 所示:

Linux 网络驱动实验(有线)_第27张图片

skb_push 函数用于在头部扩展skb_buff 的数据区,函数原型如下所示:

unsigned char *skb_push(struct sk_buff *skb, unsigned int len)

函数参数和返回值含义如下:
skb:要操作的sk_buff。
len:要增加多少个字节。
返回值:扩展完成以后新的数据区首地址。
skb_push 操作之前和操作之后的数据区如图69.3.3.4 所示:

Linux 网络驱动实验(有线)_第28张图片
sbk_pull 函数用于从sk_buff 的数据区起始位置删除数据,函数原型如下所示:

unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)

函数参数和返回值含义如下:
skb:要操作的sk_buff。
len:要删除的字节数。
返回值:删除以后新的数据区首地址。
skb_pull 操作之前和操作之后的数据区如图69.3.3.5 所示:

Linux 网络驱动实验(有线)_第29张图片

sbk_reserve 函数用于调整缓冲区的头部大小,方法很简单将skb_buff 的data 和tail 同时后
移n 个字节即可,函数原型如下所示:
static inline void skb_reserve(struct sk_buff *skb, int len)
函数参数和返回值含义如下:
skb:要操作的sk_buff。
len:要增加的缓冲区头部大小。
返回值:无。

网络NAPI 处理机制

如果玩过单片机的话应该都知道,像IIC、SPI、网络等这些通信接口,接收数据有两种方
法:轮询或中断。Linux 里面的网络数据接收也轮询和中断两种,中断的好处就是响应快,数据
量小的时候处理及时,速度快,但是一旦当数据量大,而且都是短帧的时候会导致中断频繁发
生,消耗大量的CPU 处理时间在中断自身处理上。轮询恰好相反,响应没有中断及时,但是在
处理大量数据的时候不需要消耗过多的CPU 处理时间。Linux 在这两个处理方式的基础上提出
了另外一种网络数据接收的处理方法:NAPI(New API),NAPI 是一种高效的网络处理技术。
NAPI 的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程
序,在接收服务程序中采用POLL 的方法来轮询处理数据。这种方法的好处就是可以提高短数
据包的接收效率,减少中断处理的时间。目前NAPI 已经在Linux 的网络驱动中得到了大量的
应用,NXP 官方编写的网络驱动都是采用的NAPI 机制。
关于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,函数原型如下:

inline 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 函数内容如下所示:

示例代码69.3.4.1 napi_schedule 函数
1 static inline void napi_schedule(struct napi_struct *n)
2 {
3 if (napi_schedule_prep(n))
4 __napi_schedule(n);
5 }

从示例代码69.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。
返回值:无。

I.MX6ULL 网络驱动简介

I.MX6ULL 网络外设设备树

上一小节我们对Linux 的网络驱动框架进行了一个简单的介绍,本节我们就来简单分析一
下I.MX6ULL 的网络驱动源码。I.MX6ULL 有两个10/100M 的网络MAC 外设,因此I.MX6ULL
网络驱动主要就是这两个网络MAC 外设的驱动。这两个外设的驱动都是一样的,我们分析其
中一个就行了,首先肯定是设备树,NXP 的I.MX 系列SOC 网络绑定文档为
Documentation/devicetree/bindings/net/fsl-fec.txt,此绑定文档描述了I.MX 系列SOC 网络设备树
节点的要求。
①、必要属性
compatible:这个肯定是必须的,一般是“fsl,-fec”,比如I.MX6ULL 的compatible 属
性就是"fsl,imx6ul-fec",和"fsl,imx6q-fec"。
reg:SOC 网络外设寄存器地址范围。
interrupts:网络中断。
phy-mode:网络所使用的PHY 接口模式,是MII 还是RMII。
②、可选属性
phy-reset-gpios:PHY 芯片的复位引脚。
phy-reset-duration:PHY 复位引脚复位持续时间,单位为毫秒。只有当设置了phy-reset-gpios 属性此属性才会有效,如果不设置此属性的话PHY 芯片复位引脚的复位持续时间默认为
1 毫秒,数值不能大于1000 毫秒,大于1000 毫秒的话就会强制设置为1 毫秒。
phy-supply:PHY 芯片的电源调节。
phy-handle:连接到此网络设备的PHY 芯片句柄。
fsl,num-tx-queues:此属性指定发送队列的数量,如果不指定的话默认为1。
fsl,num-rx-queues:此属性指定接收队列的数量,如果不指定的话默认为2。
fsl,magic-packet:此属性不用设置具体的值,直接将此属性名字写到设备树里面即可,表
示支持硬件魔术帧唤醒。
fsl,wakeup_irq:此属性设置唤醒中断索引。
stop-mode:如果此属性存在的话表明SOC 需要设置GPR 位来请求停止模式。
③、可选子节点
mdio:可以设置名为“mdio”的子节点,此子节点用于指定网络外设所使用的MDIO 总线,
主要作为PHY 节点的容器,也就是在mdio 子节点下指定PHY 相关的属性信息,具体信息可
以参考PHY 的绑定文档Documentation/devicetree/bindings/net/phy.txt。
PHY 节点相关属性内容如下:
interrupts:中断属性,可以不需要。
interrupt-parent:中断控制器句柄,可以不需要。
reg:PHY 芯片地址,必须的!
compatible:兼容性列表,一般为“ethernet-phy-ieee802.3-c22”或“ethernet-phy-ieee802.3-
c45”,分别对应IEEE802.3 的22 簇和45 簇,默认是22 簇。也可以设置为其他值,如果PHY
的ID 不知道的话可以compatible 属性可以设置为“ethernet-phy-idAAAA.BBBB”,AAAA 和
BBBB 的含义如下:
AAAA:PHY 的16 位ID 寄存器1 值,也就是OUI 的bit3~18,16 进制格式。
BBBB:PHY 的16 位ID 寄存器2 值,也就是OUI 的bit19~24,16 进制格式。
max-speed:PHY 支持的最高速度,比如10、100 或1000。
打开imx6ull.dtsi,找到如下I.MX6ULL 的两个网络外设节点,如下所示:

示例代码69.4.1.1 网络节点信息
1 fec1: ethernet@02188000 {
2 compatible = "fsl,imx6ul-fec", "fsl,imx6q-fec";
3 reg = <0x02188000 0x4000>;
4 interrupts = <GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH>,
5 <GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH>;
6 clocks = <&clks IMX6UL_CLK_ENET>,
7 <&clks IMX6UL_CLK_ENET_AHB>,
8 <&clks IMX6UL_CLK_ENET_PTP>,
9 <&clks IMX6UL_CLK_ENET_REF>,
10 <&clks IMX6UL_CLK_ENET_REF>;
11 clock-names = "ipg", "ahb", "ptp",
12 "enet_clk_ref", "enet_out";
13 stop-mode = <&gpr 0x10 3>;
14 fsl,num-tx-queues=<1>;
15 fsl,num-rx-queues=<1>;
16 fsl,magic-packet;
17 fsl,wakeup_irq = <0>;
18 status = "disabled";
19 };
20
21 fec2: ethernet@020b4000 {
22 compatible = "fsl,imx6ul-fec", "fsl,imx6q-fec";
23 reg = <0x020b4000 0x4000>;
24 interrupts = <GIC_SPI 120 IRQ_TYPE_LEVEL_HIGH>,
25 <GIC_SPI 121 IRQ_TYPE_LEVEL_HIGH>;
26 clocks = <&clks IMX6UL_CLK_ENET>,
27 <&clks IMX6UL_CLK_ENET_AHB>,
28 <&clks IMX6UL_CLK_ENET_PTP>,
29 <&clks IMX6UL_CLK_ENET2_REF_125M>,
30 <&clks IMX6UL_CLK_ENET2_REF_125M>;
31 clock-names = "ipg", "ahb", "ptp",
32 "enet_clk_ref", "enet_out";
33 stop-mode = <&gpr 0x10 4>;
34 fsl,num-tx-queues=<1>;
35 fsl,num-rx-queues=<1>;
36 fsl,magic-packet;
37 fsl,wakeup_irq = <0>;
38 status = "disabled";
39 };

fec1 和fec2 分别对应I.MX6ULL 的ENET1 和ENET2,至于节点的具体属性就不分析了,
上面在讲解绑定文档的时候就已经详细的讲过了。示例代码69.4.1.1 是NXP 官方编写的,我们
不需要去修改,但是示例代码69.4.1.1 是不能工作的,还需要根据实际情况添加或修改一些属
性。打开imx6ull-alientek-emmc.dts,找到如下内容:

示例代码69.4.1.2 imx6ull-alientek-emmc.dts 中的网络节点
1 &fec1 {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_enet1
4 &pinctrl_enet1_reset>;
5 phy-mode = "rmii";
6 phy-handle = <&ethphy0>;
7 phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
8 phy-reset-duration = <200>;
9 status = "okay";
10 };
11
12 &fec2 {
13 pinctrl-names = "default";
14 pinctrl-0 = <&pinctrl_enet2
15 &pinctrl_enet2_reset>;
16 phy-mode = "rmii";
17 phy-handle = <&ethphy1>;
18 phy-reset-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
19 phy-reset-duration = <200>;
20 status = "okay";
21
22 mdio {
23 #address-cells = <1>;
24 #size-cells = <0>;
25
26 ethphy0: ethernet-phy@0 {
27 compatible = "ethernet-phy-ieee802.3-c22";
28 reg = <0>;
29 };
30
31 ethphy1: ethernet-phy@1 {
32 compatible = "ethernet-phy-ieee802.3-c22";
33 reg = <1>;
34 };
35 };
36 };

示例代码69.4.1.2 是作者在移植Linux 内核的时候已经根据ALPHA 开发板修改后的,并
不是NXP 官方原版节点信息,所以会有一点出入,这个不要紧。
第1~10 行:ENET1 网口的节点属性,第3、4 行设置ENET1 所使用的引脚pinctrl 节点信
息,第5 行设置网络对应的PHY 芯片接口为RMII,这个要根据实际的硬件来设置。第6 行设
置PHY 芯片的句柄为ethphy0,MDIO 节点会设置PHY 信息。其他的属性信息就很好理解了,
基本已经在上面讲解绑定文档的时候说过了。
第12~36 行:ENET2 网口的节点属性,基本和ENET1 网口一致,区别就是多了第22~35
行的mdio 子节点,前面讲解绑定文档的时候说了,mido 子节点用于描述MIDO 总线,在此子
节点内会包含PHY 节点信息。这里一共有两个PHY 子节点:ethphy0 和ethphy1,分别对应
ENET1 和ENET2 的PHY 芯片。比如第26 行的“ethphy0: ethernet-phy@0”就是ENET1 的PHY
节点名字,“@”后面的0 就是此PHY 芯片的芯片地址,reg 属性也是描述PHY 芯片地址的,
这点和IIC 设备节点很像。其他地方就没什么好多的了,绑定文档已经讲解的很清楚了。
最后就是设备树中网络相关引脚的描述,打开imx6ull-alientek-emmc.dts,找到如下所示内
容:

示例代码69.4.1.2 网络引脚pinctrl 信息
1 pinctrl_enet1: enet1grp {
2 fsl,pins = <
3 MX6UL_PAD_ENET1_RX_EN__ENET1_RX_EN 0x1b0b0
4 MX6UL_PAD_ENET1_RX_ER__ENET1_RX_ER 0x1b0b0
5 MX6UL_PAD_ENET1_RX_DATA0__ENET1_RDATA00 0x1b0b0
6 MX6UL_PAD_ENET1_RX_DATA1__ENET1_RDATA01 0x1b0b0
7 MX6UL_PAD_ENET1_TX_EN__ENET1_TX_EN 0x1b0b0
8 MX6UL_PAD_ENET1_TX_DATA0__ENET1_TDATA00 0x1b0b0
9 MX6UL_PAD_ENET1_TX_DATA1__ENET1_TDATA01 0x1b0b0
10 MX6UL_PAD_ENET1_TX_CLK__ENET1_REF_CLK1 0x4001b009
11 >;
12 };
13
14 pinctrl_enet2: enet2grp {
15 fsl,pins = <
16 MX6UL_PAD_GPIO1_IO07__ENET2_MDC 0x1b0b0
17 MX6UL_PAD_GPIO1_IO06__ENET2_MDIO 0x1b0b0
18 MX6UL_PAD_ENET2_RX_EN__ENET2_RX_EN 0x1b0b0
19 MX6UL_PAD_ENET2_RX_ER__ENET2_RX_ER 0x1b0b0
20 MX6UL_PAD_ENET2_RX_DATA0__ENET2_RDATA00 0x1b0b0
21 MX6UL_PAD_ENET2_RX_DATA1__ENET2_RDATA01 0x1b0b0
22 MX6UL_PAD_ENET2_TX_EN__ENET2_TX_EN 0x1b0b0
23 MX6UL_PAD_ENET2_TX_DATA0__ENET2_TDATA00 0x1b0b0
24 MX6UL_PAD_ENET2_TX_DATA1__ENET2_TDATA01 0x1b0b0
25 MX6UL_PAD_ENET2_TX_CLK__ENET2_REF_CLK2 0x4001b009
26 >;
27 };
28
29 /*enet1 reset zuozhongkai*/
30 pinctrl_enet1_reset: enet1resetgrp {
31 fsl,pins = <
32 /* used for enet1 reset */
33 MX6ULL_PAD_SNVS_TAMPER7__GPIO5_IO07 0x10B0
34 >;
35 };
36
37 /*enet2 reset zuozhongkai*/
38 pinctrl_enet2_reset: enet2resetgrp {
39 fsl,pins = <
40 /* used for enet2 reset */
41 MX6ULL_PAD_SNVS_TAMPER8__GPIO5_IO08 0x10B0
42 >;
43 };

pinctrl_enet1 和pinctrl_enet1_reset 是ENET1 所有的IO 引脚pinctrl 信息,之所以分两部分
主要是因为ENET1 的复位引脚为GPIO5_IO07 ,而GPIO5_IO07 对应的引脚就是
SNVS_TAMPER7,要放到iomuxc_snvs 节点下,所以就分成了两部分。
注意第16、17 行,这两行设置GPIO1_IO07 和GPIO1_IO06 为ENET2 的MDC 和MDIO,
大家可能会疑问,为什么不将其设置为ENET1 的MDC 和MDIO 呢?经过笔者实测,在开启两
个网口的情况下,将GPIO1_IO07 和GPIO1_IO06 设置为ENET1 的MDC 和MDIO 会对导致网
络工作不正常。前面说了,一个MDIO 接口可以管理32 个PHY,所以设置ENET2 的MDC 和
MDIO 以后也是可以管理ENET1 上的PHY 芯片。

I.MX6ULL 网络驱动源码简析

1、fec_probe 函数简析

对于I.MX6ULL 而言网络驱动主要分两部分:I.MX6ULL 网络外设驱动以及PHY 芯片驱
动,网络外设驱动是NXP 编写的,PHY 芯片有通用驱动文件,有些PHY 芯片厂商还会针对自
己的芯片编写对应的PHY 驱动。总体来说,SOC 内置网络MAC+外置PHY 芯片这种方案我们
是不需要编写什么驱动的,基本可以直接使用。但是为了学习,我们还是要简单分析一下具体
的网络驱动编写过程。
首先来看一下I.MX6ULL 的网络控制器部分驱动,从示例代码69.4.1.1 中可以看出,
compatible 属性有两个值“fsl,imx6ul-fec”和“fsl,imx6q-fec”,通过在linux 内核源码中搜索这两个
字符串即可找到对应的驱动文件,驱动文件为drivers/net/ethernet/freescale/fec_main.c,打开
fec_main.c,找到如下所示内容:

示例代码69.4.2.1 I.MX 系列SOC 网络平台驱动匹配表
1 static const struct of_device_id fec_dt_ids[] = {
2 { .compatible = "fsl,imx25-fec", .data =
&fec_devtype[IMX25_FEC], },
3 { .compatible = "fsl,imx27-fec", .data =
&fec_devtype[IMX27_FEC], },
4 { .compatible = "fsl,imx28-fec", .data =
&fec_devtype[IMX28_FEC], },
5 { .compatible = "fsl,imx6q-fec", .data =
&fec_devtype[IMX6Q_FEC], },
6 { .compatible = "fsl,mvf600-fec", .data =
&fec_devtype[MVF600_FEC], },
7 { .compatible = "fsl,imx6sx-fec", .data =
&fec_devtype[IMX6SX_FEC], },
8 { .compatible = "fsl,imx6ul-fec", .data =
&fec_devtype[IMX6UL_FEC], },
9 { /* sentinel */ }
10 };
11
12 static struct platform_driver fec_driver = {
13 .driver = {
14 .name = DRIVER_NAME,
15 .pm = &fec_pm_ops,
16 .of_match_table = fec_dt_ids,
17 },
18 .id_table = fec_devtype,
19 .probe = fec_probe,
20 .remove = fec_drv_remove,
21 };

第8 行,匹配表包含“fsl,imx6ul-fec”,因此设备树和驱动匹配上,当匹配成功以后第19 行
的fec_probe 函数就会执行,我们简单分析一下fec_probe 函数,函数内容如下:

示例代码69.4.2.1 fec_probe 函数
1 static int fec_probe(struct platform_device *pdev)
2 {
3 struct fec_enet_private *fep;
4 struct fec_platform_data *pdata;
5 struct net_device *ndev;
6 int i, irq, ret = 0;
7 struct resource *r;
8 const struct of_device_id *of_id;
9 static int dev_id;
10 struct device_node *np = pdev->dev.of_node, *phy_node;
11 int num_tx_qs;
12 int num_rx_qs;
13
14 fec_enet_get_queue_num(pdev, &num_tx_qs, &num_rx_qs);
15
16 /* Init network device */
17 ndev = alloc_etherdev_mqs(sizeof(struct fec_enet_private),
18 num_tx_qs, num_rx_qs);
19 if (!ndev)
20 return -ENOMEM;
21
22 SET_NETDEV_DEV(ndev, &pdev->dev);
23
24 /* setup board info structure */
25 fep = netdev_priv(ndev);
26
27 of_id = of_match_device(fec_dt_ids, &pdev->dev);
28 if (of_id)
29 pdev->id_entry = of_id->data;
30 fep->quirks = pdev->id_entry->driver_data;
31
32 fep->netdev = ndev;
33 fep->num_rx_queues = num_rx_qs;
34 fep->num_tx_queues = num_tx_qs;
35
36 #if !defined(CONFIG_M5272)
37 /* default enable pause frame auto negotiation */
38 if (fep->quirks & FEC_QUIRK_HAS_GBIT)
39 fep->pause_flag |= FEC_PAUSE_FLAG_AUTONEG;
40 #endif
41
42 /* Select default pin state */
43 pinctrl_pm_select_default_state(&pdev->dev);
44
45 r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
46 fep->hwp = devm_ioremap_resource(&pdev->dev, r);
47 if (IS_ERR(fep->hwp)) {
48 ret = PTR_ERR(fep->hwp);
49 goto failed_ioremap;
50 }
51
52 fep->pdev = pdev;
53 fep->dev_id = dev_id++;
54
55 platform_set_drvdata(pdev, ndev);
56
57 fec_enet_of_parse_stop_mode(pdev);
58
59 if (of_get_property(np, "fsl,magic-packet", NULL))
60 fep->wol_flag |= FEC_WOL_HAS_MAGIC_PACKET;
61
62 phy_node = of_parse_phandle(np, "phy-handle", 0);
63 if (!phy_node && of_phy_is_fixed_link(np)) {
64 ret = of_phy_register_fixed_link(np);
65 if (ret < 0) {
66 dev_err(&pdev->dev,
67 "broken fixed-link specification\n");
68 goto failed_phy;
69 }
70 phy_node = of_node_get(np);
71 }
72 fep->phy_node = phy_node;
73
74 ret = of_get_phy_mode(pdev->dev.of_node);
75 if (ret < 0) {
76 pdata = dev_get_platdata(&pdev->dev);
77 if (pdata)
78 fep->phy_interface = pdata->phy;
79 else
80 fep->phy_interface = PHY_INTERFACE_MODE_MII;
81 } else {
82 fep->phy_interface = ret;
83 }
84
85 fep->clk_ipg = devm_clk_get(&pdev->dev, "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 < FEC_IRQ_NUM; i++) {
146 irq = platform_get_irq(pdev, i);
147 if (irq < 0) {
148 if (i)
149 break;
150 ret = irq;
151 goto failed_irq;
152 }
153 ret = devm_request_irq(&pdev->dev, irq, fec_enet_interrupt,
154 0, pdev->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 < FEC_IRQ_NUM)
163 fep->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 = <&ethphy0>;
phy-handle = <&ethphy1>;

而ethphy0 和ethphy1 都定义在mdio 子节点下,内容如下所示:

mdio {
#address-cells = <1>;
#size-cells = <0>;
ethphy0: ethernet-phy@0 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = <0>;
};
ethphy1: ethernet-phy@1 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = <1>;
};
};

可以看出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 可以看出,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 函数向网卡添加了一个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
中的两行代码:

在这里插入图片描述

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; i<PHY_MAX_ADDR; i++)
15 mdio->irq[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) {
28 scanphys = true;
29 continue;
30 }
31
32 rc = of_mdiobus_register_phy(mdio, child, addr);
33 if (rc)
34 continue;
35 }
36
37 if (!scanphys)
38 return 0;
39
......
62 return 0;
63 }

第20 行,调用mdiobus_register 函数来向Linux 内核注册mdio 总线!
第25 行,轮询mdio 节点下的所有子节点,比如示例代码69.4.1.2 中的“ethphy0: ethernet-phy@0”和“ethphy1: ethernet-phy@1”这两个子节点,这两个子节点描述的是PHY 芯片信息。
第26 行,提取设备树子节点中PHY 地址,也就是ethphy0: ethernet-phy@0”和“ethphy1: ethernet-phy@1”这两个子节点对应的PHY 芯片地址,分别为0 和1。
第32 行,调用of_mdiobus_register_phy 函数向Linux 内核注册phy。
简单总结一下,of_mdiobus_register 函数有两个主要的功能,一个是通过mdiobus_register
函数向Linux 内核注册mdio 总线,另一个就是通过of_mdiobus_register_phy 函数向内核注册
PHY。
接下来简单分析一下of_mdiobus_register_phy 函数,看看是如何向Linux 内核注册PHY 设
备的,of_mdiobus_register_phy 函数内容如下所示:

示例代码69.4.2.5 of_mdiobus_register_phy 函数
1 static int of_mdiobus_register_phy(struct mii_bus *mdio,
struct device_node *child,
2 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 (!phy || IS_ERR(phy))
17 return 1;
18
19 rc = irq_of_parse_and_map(child, 0);
20 if (rc > 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 所示:

Linux 网络驱动实验(有线)_第30张图片

注册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。

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 <= txq->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_queue 来暂停发送,通过暂停发送来通知应用层停止向网络发送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 < budget) {
12 napi_complete(napi);
13 writel(FEC_DEFAULT_IMASK, fep->hwp + FEC_IMASK);
14 }
15 return pkts;
16 }

第7 行,调用fec_enet_rx 函数进行真正的数据接收。
第9 行,调用fec_enet_tx 函数进行数据发送。
第12 行,调用napi_complete 函数来宣布一次轮询结束,
第13 行,设置ENET 的EIMR 寄存器,重新使能中断。

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 驱动。
4、通用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 驱动相关函数。
5、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 所示:

Linux 网络驱动实验(有线)_第31张图片
选中图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 等。

网络驱动实验测试

LAN8720 PHY 驱动测试

首先肯定是驱动修改,这个已经在37.4.3 小节做了详细的讲解,参考修改即可。系统启动
以后就会打印出当前PHY 驱动名字为“SMSC LAN8710/LAN8720”,如图69.5.1.1 所示:

在这里插入图片描述

从图69.5.1.1 可以看出,此时PHY 驱动使用的是“SMSC LAN8710/8720”,当我们使用
ifconfig 命令打开网卡的时候也会提示当前PHY 驱动名字。至于网络的测试就很简单了,大家
可以ping 一下主机或者ubuntu 的地址,如果能ping 通就说明网络工作正常。

通用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 可以看出,此时网卡驱动名字为“Generic PHY”,说明使用的是通用PHY 驱
动。

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 所示:

Linux 网络驱动实验(有线)_第32张图片

从图69.5.3.1 可以看出,eth1 申请到的IP 地址是192.168.1.156,并且也修改了/etc/resolv.conf
文件中DNS 服务器地址。可以输入“ifconfig”命令来查看eth1 网卡的详细信息,这里就不演
示了。

单网卡使用

有时候我们在实际的产品开发中由于I.MX6ULL 引脚数量的限制,用到的一些外设引脚和网卡冲突了,但是我们的产品又不需要两个网卡,有一个就够了。这个时候就需要修改linux 内
核,实现一个网卡的使用,另外一个网卡的IO 用作其他的外设。

只使用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 = <&pinctrl_enet1
4 &pinctrl_enet1_reset>;
5 phy-mode = "rmii";
6 phy-handle = <&ethphy0>;
7 phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
8 phy-reset-duration = <200>;
9 status = "disabled";
10 };

第9 行,将status 改为“disabled”。
修改完成以后重新编译设备树,使用新的设备树启动内核。内核启动以后输入如下命令查
看当前系统中所有网卡:

ifconfig -a //查看所有网卡

结果如图69.6.1.1 所示:

Linux 网络驱动实验(有线)_第33张图片

从图69.6.1.1 可以看出,此时只要一个eth0 网卡,并没有eth1,说明fec1 被我们关闭了,
接下来大家就可以将fec1 网卡对应的IO 复用为其他外设功能了。

只使用ENET1 网卡

如果只使用ENET1 网卡的话就稍微复杂一点了,不是简单的将fec2 节点下的status 改为
“disabled”,按照如下步骤来修改设备树imx6ull-alientek-emmc.dts:
1、屏蔽或删除掉fec2 节点内容
首先,屏蔽或删除掉imx6ull-alientek-emmc.dts 文件中ENET2 网卡对应的fec2 节点内容,
如图69.6.2.1 所示:

Linux 网络驱动实验(有线)_第34张图片

2、修改ENET1 对应的fec1 节点信息。
接下来需要fec1 节点进行修改,重点是在fec1 节点下添加mdio 子节点,修改后的fec1 节
点内容如下所示:
示例代码69.6.1.2 修改后的fec1 节点

1 &fec1 {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_enet1
4 &pinctrl_enet1_reset>;
5 phy-mode = "rmii";
6 phy-handle = <&ethphy0>;
7 phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
8 phy-reset-duration = <200>;
9 status = "okay";
10
11 mdio {
12 #address-cells = <1>;
13 #size-cells = <0>;
14
15 ethphy0: ethernet-phy@0 {
16 compatible = "ethernet-phy-ieee802.3-c22";
17 reg = <0>;
18 };
19 };
20 };

重点是在fec1 中添加第11~19 行的mdio 子节点,而且仅仅留下ENET1 对应的PHY 子节
点。
3、屏蔽或删除掉ENET2 对应的pinctrl 节点
屏蔽后删除掉ENET2 对应的pinctrl_enet2,如图69.6.2.2 所示:

Linux 网络驱动实验(有线)_第35张图片

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 = <
3 MX6UL_PAD_GPIO1_IO07__ENET1_MDC 0x1b0b0
4 MX6UL_PAD_GPIO1_IO06__ENET1_MDIO 0x1b0b0
5 MX6UL_PAD_ENET1_RX_EN__ENET1_RX_EN 0x1b0b0
6 MX6UL_PAD_ENET1_RX_ER__ENET1_RX_ER 0x1b0b0
7 MX6UL_PAD_ENET1_RX_DATA0__ENET1_RDATA00 0x1b0b0
8 MX6UL_PAD_ENET1_RX_DATA1__ENET1_RDATA01 0x1b0b0
9 MX6UL_PAD_ENET1_TX_EN__ENET1_TX_EN 0x1b0b0
10 MX6UL_PAD_ENET1_TX_DATA0__ENET1_TDATA00 0x1b0b0
11 MX6UL_PAD_ENET1_TX_DATA1__ENET1_TDATA01 0x1b0b0
12 MX6UL_PAD_ENET1_TX_CLK__ENET1_REF_CLK1 0x4001b009
13 >;
14 };

第3 行,将GPIO1_IO07 复用为ENET1_MDC 引脚。
第4 行,将GPIO1_IO06 复用为ENET1_MDIO 引脚。
至此,设备树修改完成,重新编译设备树,然后用新的设备树启动系统。启动以后输入如
下命令查看开发板所有网卡信息:

ifconfig -a

结果如图69.6.2.3 所示:

Linux 网络驱动实验(有线)_第36张图片

从图69.6.2.3 可以看出,此时整个系统只有一个eth0 网卡,注意!这里的eth0 网卡就是
ENET1,不要和前面的混淆了,以为eth0 是ENET2 网卡名字。
至此,关于Linux 的网络驱动就讲到这里,整体比较复杂,但是实际使用起来确实非常简
单的,尤其是对这种内置MAC+外置PHY 的网络方案而言,几乎不需要我们修改驱动,因为内
核已经继承了通用PHY 驱动了。

你可能感兴趣的:(网络,linux,运维)