欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

I.MX6ULL_Linux_Driver(55)linux 网络驱动程序

最编程 2024-03-26 07:05:51
...

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

嵌入式网络简介

网络硬件接口

首先,嵌入式网络硬件分为两部分: MAC 和 PHY,大家都是通过看数据手册来判断一款 SOC 是否支持网络,如果一款芯片数据手册说自己支持网络,一般都是说的这款 SOC 内置 MAC, MAC 类似 I2C 控制器、 SPI 控制器一样的外设。但是光有 MAC还不能直接驱动网络,还需要另外一个芯片: PHY,因此对于内置 MAC 的 SOC,其外部必须搭配一个 PHY 芯片。但是有些 SOC 内部没有 MAC,那也就没法搭配 PHY 芯片了,这些内部没有网络 MAC 的芯片如何上网呢?这里就要牵扯出常见的两个嵌入式网络硬件方案了。

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 芯片的连接如图所示:

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 的内部寄存器,所以还需要一个控制接口,叫做 MDIO, MDIO 很类似 IIC,也是两根线,一根数据线叫做 MDIO,一根时钟线叫做 MDC。
SOC 内部 MAC 外设与外部 PHY 芯片的连接如图所示:

MII/RMII接口

前面我们说了,内部 MAC 通过 MII/RMII 接口来与外部的 PHY 芯片连接,完成网络数据传输,本节我们就来学习一下什么是 MII 和 RMII 接口。

MII 接口

MII 全称是 Media Independent Interface,直译过来就是介质独立接口,它是 IEEE-802.3 定义的以太网标准接口, MII 接口用于以太网 MAC 连接 PHY 芯片,连接示意图如图所示:

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 接口使用已经越来越少了。

RMII 接口

RMII 全称是 Reduced Media Independent Interface,翻译过来就是精简的介质独立接口,也就是 MII 接口的精简版本。 RMII 接口只需要 7 根数据线,相比 MII 直接减少了 9 根,极大的方便了板子布线, RMII 接口连接 PHY 芯片的示意图如图所示:

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、 SGMII 等,关于其他接口基本都是大同小异的,这里就不做讲解了。正点原子 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 座,如图所示:

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

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

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 参考手册》的“Chapter22 10/100-Mbps Ethernet MAC(ENET)”章节。我们在编写驱动的时候其实并不需要关注 ENET外设的具体内容,因为这部分驱动是 SOC 厂商编写的,我们重点关注的是更换 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 协议对 PHY 的前16 个寄存器功能进行了规定,如图所示:

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 功能框图如图所示。

LAN8720A 中断管理

LAN8720A 的器件管理接口支持非 IEEE 802.3 规范的中断功能。当一个中断事件发生并且相应事件的中断位使能, LAN8720A 就会在 nINT(14 脚)产生一个低电平有效的中断信号。
LAN8720A 的中断系统提供两种中断模式:主中断模式和复用中断模式。主中断模式是默认中断模式, LAN8720A 上电或复位后就工作在主中断模式,当模式控制/状态寄存器(十进制地址
为 17)的 ALTINT 位为 0 时 LAN8720A 工作在主模式,当 ALTINT 位为 1 时工作在复用中断模式。正点原子的 ALPHA 开发板虽然将 LAN8720A 的中断引脚连接到了 I.MX6ULL 上,但是并
没有使用中断功能,关于中断的具体用法可以参考 LAN8720A 数据手册。

PHY 地址设置

MAC 层通过 MDIO/MDC 总线对 PHY 进行读写操作, MDIO 最多可以控制 32 个 PHY 芯片,通过不同的 PHY 芯片地址来对不同的 PHY 操作。 LAN8720A 通过设置 RXER/PHYAD0引脚来设置其 PHY 地址,默认情况下为 0,其地址设置如表所示。

RXER/PHYAD0 引脚状态 PHY 地址
上拉 0X01
下拉(默认) 0X00

正点原子 ALPHA 开发板的 ENET1 网络的 LAN8720A 上的 RXER/PHYAD0 引脚为默认状态(原理图上有个 10K 下拉,但是没有焊接),因此 ENET1 上的 LAN8720A 地址为 0。 ENET2网络上的 LAN8720A 上的 RXER/PHYAD0 引脚接了个 10K 上拉电阻,因此 ENET2 上的LAN8720A 地址为 1。

nINT/REFCLKO 配置

nINTSEL 引脚(2 号引脚)用于设置 nINT/REFCLKO 引脚(14 号引脚)的功能。 nINTSEL 配置如表所示。。

nINTSEL 引脚值 模式 nINT/REFCLKO 引脚功能
nINTSEL= 0 REF_CLK Out 模式 nINT/REFCLKO 作为 REF_CLK 时钟源
nINTSEL = 1(默认) REF_CLK In 模式 nINT/REFCLKO 作为中断引脚

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

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

前面说了,正点原子的 ALPHA 开发板工作在 REF_CLK In 模式下,因此需要外部 50MHz时钟信号, I.MX6ULL 有专用的网络时钟引脚,因此 ALPHA 开发板是通过 I.MX6ULL 的ENET1_REF_CLK 和 ENET2_REF_CLK 这两个网络时钟引脚来为 LAN8720A 提供 50MHz 的时钟。

LAN8720A 内部寄存器

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

描述 类型
15 软件复位
1:软件复位,此位自动清零
R/W
14 回测
0:正常运行
1:使能回测模式
R/W
13 速度选择
0: 10Mbps
1: 100Mbps
注意:当使用自动协商功能时此位失能
R/W
12 自动协商功能
0:关闭自动协商功能
1:打开自动协商功能
R/W
11 掉电(power down)
0:正常运行
1:进入掉电模式
注意:进入掉电模式前自动协商必须失能
R/W
10

隔离
0:正常运行

1: PHY 的 RMII 接口电气隔离

R/W
9 重启自动协商功能
0:正常运行
1:重启自动协商功能
注意:此位会被自动清零
R/W
SC
8 双工模式
0:半双工
1:全双工
注意:开启自动协商功能后此位失效
R/W
7:0 保留 RO

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

描述 类型
15 100BAST-T4
0:不支持 T4
1:支持 T4
RO
14 100BAST-TX 全双工
0:不支持 TX 全双工
1:支持 TX 全双工
RO
13 100BAST-TX 半双工
0:不支持 TX 半双工
1:支持 TX 半双工个
RO
12 10BAST-T 全双工
0:不支持 10Mbps 全双工
1:支持 10Mbps 全双工
RO
11 10BAST-T 半双工
0:不支持 10Mbps 半双工
1:支持 10Mbps 半双工
RO
10:6 保留 RO
5 自协商功能完成
0:自动协商未完成
1:自动协商完成
RO
4 远端错误
0:无远端错误
1:检测到远端错误
RO/HL
3

自协商功能
0:不能执行自协商功能

1:可以执行自协商功能

RO
2 连接状态
0:连接断开
1:连接建立
RO/LL
1 Jabber 检测
0:未检测到 jabber
1:检测到 jabber
RO/LH
0 扩展功能
0:不支持扩展寄存器
1:支持扩展寄存器
RO

从上表可以看出,和 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,组成如图所示:

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

描述 类型 默认值
15:0 PHY ID 号,对应于 OUI 的 3~18 位, bit15 对应 OUI 的
bit3, bit0 对应 OUI 的 bit18。和寄存器 3 的 bit15: 10
共同组成 22 位 ID。
R/W 0007h

ID 寄存器 3 如表所示:

描述 类型 默认值
15:10 PHY ID 号,对应于 OUI 的 19~24 位, bit15 对
应 OUI 的 19 位, bit10 对应 OUI 的 24 位。
R/W 110000h
9:4 厂商型号 ID R/W 001111b
3:0 厂商版本 ID R/W 视具体版本而定

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

描述 类型
15:13 保留 RO
12 自协商完成
0:自协商未完成或者自协商关闭
1:自协商完成
RO
11:5 保留 R/W
4:2 速度指示
001: 10BASE-T 半双工
101: 10BAST-T 全双工
010: 100BAST-TX 半双工
110: 100BAST-TX 全双工
RO
1:0 保留 RO

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

Linux 内核网络驱动框架

net_device 结构体

Linux 内核使用 net_device 结构体表示一个具体的网络设备, net_device 是整个网络驱动的灵魂。网络驱动的核心就是初始化 net_device 结构体中的各个成员变量,然后将初始化完成以后的 net_device 注册到 Linux 内核中。 net_device 结构体定义在 include/linux/netdevice.h 中,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 文件中,为一个枚举类型,内容如下:

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 };

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

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 设备。

申请 net_device

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

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,这也是一个宏,内容如下:

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 的简单封装,函数内容如下:

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 做初步的初始化,函数内容如下所示:

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。

删除 net_device

当我们注销网络驱动的时候需要释放掉前面已经申请到的 net_device,释放函数为free_netdev,函数原型如下:

void free_netdev(struct net_device *dev)

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

注册 net_device

net_device 申请并初始化完成以后就需要向内核注册 net_device,要用到函数 register_netdev,函数原型如下:

int register_netdev(struct net_device *dev)

函数参数和返回值含义如下:
dev: 要注册的 net_device 指针。
返回值: 0 注册成功,负值 注册失败。

注销 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_”开头的函数,这些函数就需要网络驱动编写人员去实现,不需要全部都实现,根据实际驱动情况实现其中一部分即可。结构体内容如下所示(结
构体比较大,这里有缩减):

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 函数即可,我们依次来看一下这两个函数。

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

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

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 指向缓冲区的头部和尾部。结构如图所示:

针对 sk_buff 内核提供了一系列的操作与管理函数,我们简单看一些常见的 API 函数:

分配 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。

释放 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。

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

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

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

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

sbk_pull 函数用于从 sk_buff 的数据区起始位置删除数据,函数原型如下所示:

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

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

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 实例。

初始化 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。
返回值: 无。

删除 NAPI

如果要删除 NAPI,使用 netif_napi_del 函数即可,函数原型如下:

void netif_napi_del(struct napi_struct *napi)

函数参数和返回值含义如下:
napi: 要删除的 NAPI。
返回值: 无。

使能 NAPI

初始化完 NAPI 以后,必须使能才能使用,使用函数 napi_enable,函数原型如下:

inline void napi_enable(struct napi_struct *n)

函数参数和返回值含义如下:
n: 要使能的 NAPI。
返回值: 无。

关闭 NAPI

关闭 NAPI 使用 napi_disable 函数即可,函数原型如下:

void napi_disable(struct napi_struct *n)

函数参数和返回值含义如下:
n: 要关闭的 NAPI。
返回值: 无。

检查 NAPI 是否可以进行调度

使用 napi_schedule_prep 函数检查 NAPI 是否可以进行调度,函数原型如下:

inline bool napi_schedule_prep(struct napi_struct *n)

函数参数和返回值含义如下:
n: 要检查的 NAPI。
返回值: 如果可以调度就返回真,如果不可调度就返回假。

NAPI 调度

如果可以调度的话就进行调度,使用__napi_schedule 函数完成 NAPI 调度,函数原型如下:

void __napi_schedule(struct napi_struct *n)

函数参数和返回值含义如下:
n: 要调度的 NAPI。
返回值: 无。

我们也可以使用 napi_schedule 函数来一次完成 napi_schedule_prep 和__napi_schedule 这两个函数的工作, napi_schedule 函数内容如下所示:

1 static inline void napi_schedule(struct napi_struct *n)
2 {
3     if (napi_schedule_prep(n))
4         __napi_schedule(n);
5 }

从示例代码可以看出,napi_schedule函数就是对 napi_schedule_prep 和__napi_schedule 的简单封装,一次完成判断和调度。

NAPI 处理完成

NAPI 处理完成以后需要调用 napi_complete 函数来标记 NAPI 处理完成,函数原型如下:

inline void napi_complete(struct napi_struct *n)

函数参数和返回值含义如下:
n: 处理完成的 NAPI。
返回值: 无。

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,<soc>-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 的两个网络外设节点,如下所示:

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,至于节点的具体属性就不分析了,上面在讲解绑定文档的时候就已经详细的讲过了。上述dts是 NXP 官方编写的,我们不需要去修改,但是只有以上配置是不能工作的,还需要根据实际情况添加或修改一些属性。打开 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 };

示例代码是正点原子在移植 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,找到如下所示内容:

1 pinctrl_