正点原子嵌入式linux驱动开发——Linux 网络设备驱动

网络驱动是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+PHY一体的。比如三星linux开发板里面用的最多的DM9000,因为三星的芯片基本没有内部MAC(比如S3C2440S5PV210,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+PHY芯片连接

SoC内部集成网络MAC外设

一般说某个SoC支持网络,说的就是他内部集成网络MAC外设,此时还需要外接一个网络PHY芯片。目前将PHY也集成到芯片里面的SoC很少见。一般常见的通用SoC都会集成网络MAC外设,比如STM32F4/F7/H7系列、NXP的I.MX系列以及STM32MP1系列,内部集成网络MAC的优点如下:

  1. 内部MAC外设会有专用的加速模块,比如专用的DMA,加速网速数据的处理。
  2. 网速快,可以支持10/100/1000M网速。
  3. 外接PHY可选择性多,成本低。

内部的MAC外设会通过相应的接口来连接外部PHY芯片,根据数据传输模式不同,大致
可以分为以下两类

  1. MII/RMII 接口:支持10Mbit/s和100Mbit/s数据传输模式;
  2. GMII/RGMII接口:支持10Mbit/s、100Mbit/s 以及1000Mbit/s数据传输模式。

从这里可以知道,MII/RMII接口最大传输速率为100Mbit/s,而GMII/RGMII接口最大传输速率可达1000Mbit/s;所以一般把MII/RMII称为百兆以太网接口,而把GMII/RGMII称为千兆以太网接口。

关于这两组接口更加详细的内容会在后面给大家进行介绍,MII/RMII或GMII/RGMII接口是用来传输网络数据的,另外主控SoC需要配置或读取PHY芯片,也就是读写PHY的内部寄存器,所以还需要一个控制接口,叫做MIDO,MDIO很类似IIC,也是两根线,一根数据线叫做MDIO,一根时钟线叫做MDC

SoC内部MAC外设与外部PHY芯片的连接如下图所示:

内部MAC与外部PHY之间连接(千兆以太网)

内部MAC与外部PHY之间连接(百兆以太网)

STM32MP1就有一颗10M/100M/1000M的网络MAC外设,正点原子STM32MP1开发板板载了一颗PHY芯片,V1.2版本及其以前的核心板使用RTL8211F-CG这颗PHY芯片, V1.3版本及其以后核心板使用YT8511C/H这颗PHY芯片

因此,这里只讲解SoC内部MAC+外置PHY芯片这种方案

MII/RMII、GMII\RGMII接口

MII接口

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

MII接口

MII接口一共有16根信号线,含义如下:

  • TX_CLK:发送时钟,如果网速为100M的话时钟频率为25MHz,10M网速的话时钟频率为2.5MHz,此时钟由PHY产生并发送给MAC。
  • TX_EN:发送使能信号。
  • TX_ER:发送错误信号,高电平有效,表示TX_ER有效期内传输的数据无效。10Mpbs网速下TX_ER不起作用。
  • TXD[3:0]:发送数据信号线,一共4根。
  • RXD[3:0]:接收数据信号线,一共4根。
  • RX_CLK:接收时钟信号,如果网速为100M的话时钟频率为25MHz,10M网速的话时钟频率为2.5MHz,RX_CLK也是由PHY产生的。
  • RX_ER:接收错误信号,高电平有效,表示RX_ER有效期内传输的数据无效。10Mpbs网速下RX_ER不起作用。
  • RX_DV:接收数据有效,作用类似TX_EN。
  • CRS:载波侦听信号。
  • COL:冲突检测信号。

MII接口的缺点就是所需信号线太多,这还没有算MDIO和MDC这两根管理接口的数据线,因此MII接口使用已经越来越少了。

RMII接口

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

RMII接口

  • TX_EN:发送使能信号。
  • TXD[1:0]:发送数据信号线,一共2根。
  • RXD[1:0]:接收数据信号线,一共2根。
  • CRS_DV:相当于MII接口中的RX_DV和CRS这两个信号的混合。
  • REF_CLK:参考时钟,由外部时钟源提供,频率为50MHz。这里与MII不同,MII的接收和发送时钟是独立分开的,而且都是由PHY芯片提供的。

GMII接口

GMII(Gigabit Media Independant Interface),千兆MII接口GMII采用8位接口数据,工作时钟125MHz,因此传输速率可达1000Mbps;同时兼容MII所规定的10/100Mbps工作方式。GMII接口数据结构符合IEEE以太网标准,该接口定义见IEEE 802.3-2000。信号定义如下:

GMII接口

  • GTX_CLK:1000M工作模式下的发送时钟(25MHz)。
  • TX_EN:发送使能信号。
  • TX_ER:发送错误信号,高电平有效,表示TX_ER有效期内传输的数据无效。
  • TXD[7:0]:发送数据信号线,一共8根。
  • RXD[7:0]:接收数据信号线,一共8根。
  • RX_CLK:接收时钟信号。
  • RX_ER:接收错误信号,高电平有效,表示RX_ER有效期内传输的数据无效。
  • RX_DV:接收数据有效,作用类似TX_EN。
  • CRS:载波侦听信号。
  • COL:冲突检测信号。

与MII接口相比,GMII的数据宽度由4位变为8位,GMII接口中的控制信号如TX_ER、TX_EN、RX_ER、RX_DV、CRS和COL的作用同MII接口中的一样,发送参考时钟GTX_CLK和接收参考时钟RX_CLK的频率均为125MHz(在1000Mbps工作模式下)。

在实际应用中,绝大多数GMII接口都是兼容MII接口的,所以,一般的GMII接口都有两个发送参考时钟:TX_CLK和GTX_CLK(两者的方向是不一样的,前面已经说过了),在用作MII模式时,使用TX_CLK和8根数据线中的4根。

RGMII接口

RGMII(Reduced Gigabit Media Independant Interface),精简版GMII接口。将接口信号线数量从24根减少到14根(COL/CRS 端口状态指示信号,这里没有画出),时钟频率仍旧为125MHz,TX/RX数据宽度从8为变为4位,为了保持1000Mbps的传输速率不变,RGMII接口在时钟的上升沿和下降沿都采样数据,在参考时钟的上升沿发送GMII接口中的TXD[3:0]/RXD[3:0],在参考时钟的下降沿发送GMII接口中的TXD[7:4]/RXD[7:4]。RGMII同时也兼容100Mbps 和10Mbps两种速率,此时参考时钟速率分别为25MHz和2.5MHz。

TX_EN信号线上传送TX_EN 和TX_ER两种信息,在TX_CLK的上升沿发送TX_EN,下降沿发送TX_ER;同样的,RX_DV信号线上也传送RX_DV和RX_ER两种信息,在RX_CLK的上升沿发送RX_DV,下降沿发送RX_ER。

RGMII接口定义如下所示:

RGMII接口

关于这些接口定义相关的内容就讲到这里,除了上面说到4 种接口以外,还有其他接口,比如SMII、SSMII和SGMII等,关于其他接口基本都是大同小异的,这里就不做讲解了。正点原子STM32MP1开发板上的网口是采用RGMII接口来连接MAC与外部PHY芯片

MDIO接口

MDIO全称是Management Data Input/Output,直译过来就是管理数据输入输出接口,是一个简单的两线串行接口,一根MDIO数据线,一根MDC时钟线。驱动程序可以通过MDIO和MDC这两根线访问PHY芯片的任意一个寄存器。MDIO接口支持多达32个PHY。同一时刻内只能对一个PHY进行操作。和IIC一样,使用器件地址即可区分PHY芯片。同一MDIO接口下的所有PHY芯片,其器件地址不能冲突,必须保证唯一,具体器件地址值要查阅相应的PHY数据手册。

因此,MAC和外部PHY芯片进行连接的时候主要是MII/RMII(百兆网)或GMII/RGMII(千兆网)和MDIO接口,另外可能还需要复位、中断等其他引脚。

RJ45接口

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

RJ45座子

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

网络变压器

但是现在很多RJ45座子内部已经集成了网络变压器,比如正点原子的STM32MP1开发板所使用的ATK911130A就是内置网络变压器的RJ45座。内置网络变压器的RJ45座和不内置的引脚一样,但是一般不内置的RJ45座会短一点。因此,在画板的时候一定要考虑所使用的RJ45座是否内置网络变压器,如果不内置的话就要自行添加网络变压器部分电路!同理,如果设计的硬件是需要内置网络变压器的RJ45座,肯定不能随便焊接一个不内置变压器的RJ45座,否则网络工作不正常!

RJ45座子上一般有两个灯,一个黄色(橙色),一个绿色,一般绿色亮的话表示网络连接正常,黄色闪烁的话说明当前正在进行网络通信,当然了有时候两个灯的状态会反过来,以实际为准。这两个灯由PHY芯片控制,对于千M网络PHY芯片,一般PHY芯片会有3个LED灯引脚,多了一个千M网络状态指示灯PHY芯片会通过这几个LED灯引脚来连接RJ45座上的这两个灯(千M PHY会有3个LED灯引脚,一般硬件设计人员会自行选择将其中的哪两个连接到RJ45座上)。由于正点原子STM32MP157开发板采用的千M网络PHY,所以后面只讲千M网络。内部MAC+外部PHY+RJ45座(内置网络变压器)就组成了一个完整的嵌入式网络接口硬件,如下图所示:

嵌入式网络硬件接口示意图

STM32MP1 GMAC接口简介

STM32MP1内核集成了一个10M/100M/1000M的网络MAC,符合IEEE802.3-2002标准,MAC层支持双工或者半双工模式下运行。MAC可编程,有直接存储器接口的专用DMA,介质访问控制器(MAC)和支持多种格式的PHY接口模块

STM32MP1内部ENET外设主要特性如下:

  1. 、支持全工和半双工操作。
  2. 全双工流控制操作(IEEE 802.3X暂停包和优先级流控制)。
  3. 报头和帧起始数据(SFD)在发送模式下自动插入、在接收中自动删除。
  4. 可逐帧控制CRC和pad自动生成。
  5. 可编程数据包长度,支持标准以太网数据包或高达16KB的巨型以太网数据包。
  6. 可编程数据包间隙。
  7. 两组 FIFO:一个具有可编程阈值功能的4096字节发送FIFO和一个具有可配置阈值功能的4096字节接收FIFO。
  8. ……

STM32MP1的GMAC外设内容比较多,详细的介绍请查阅《STM32MP1 参考手册》的“Ethernet (ETH): Gigabit media access control(GMAC) with DMA controller”章节。在编写驱动的时候其实并不需要关注GMAC控制器外设的具体内容,因为这部分驱动是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驱动。在实际开发中可能会遇到一些其他的问题导致Linux内核的通用PHY驱动工作不正常,这个时候就需要驱动开发人员去调试了。但是,随着现在的PHY芯片性能越来越强大,32个寄存器可能满足不了厂商的需求,因此很多厂商采用分页技术来扩展寄存器地址空间,以求定义更多的寄存器。这些多出来的寄存器可以用于实现厂商特有的一些技术,因此Linux内核的通用PHY驱动就无法驱动这些特色
功能了,这个时候就需要PHY厂商提供相应的驱动源码了,所以也会在Linux内核里面看到很多具体的PHY芯片驱动源码。不管PHY芯片有多少特色功能,按道理来讲,Linux内核的通用PHY驱动是绝对可以让这PHY芯片实现基本的网络通信,因此也不用担心更换PHY芯片以后网络驱动编写是不是会很复杂。

IEEE802.3协议英文原版已经放到了开发板光盘中,其中有对PHY的前16个寄存器功能进行了规定,如下图所示:

IEEE规定的前16个寄存器

关于这16个寄存器的内容协议里面也进行了详细的讲解,这里就不分析了。后面会以正点原子STM32MP1开发板所使用的RTL8211F-CG(YT8511C/H)这个PHY为例,详细的分析一下PHY芯片的寄存器

RTL8211F-CG详解

此小节是针对核心板为V1.2版本的PHY讲解RTL8211F-CG这颗PHY芯片是由realtek公司出品的。

本小节以RTL8211F-CG为例做一个简单第介绍,虽然讲解的是RTL8211F-CG这颗PHY,但是前面说了,IEEE规定了PHY的前16个寄存器的功能,因此如果所使用的板子用的其它厂家的PHY芯片,也是可以看本小节的

RTL8211F-CG简介

Realtek RTL8211F-CG是高度集成的以太网收发器,符合10Base-T、100Base-TX和1000Base-T IEEE 802.3标准。它提供了所有通过CAT.5 UTP电缆收发以太网数据包所需的必要物理层功能。

RTL8211F使用最新的DSP技术和模拟前端(AFE),可以通过UTP电缆进行高速数据传输和接收。RTL8211F中实现了交叉检测和自动校正、极性校正、自适应均衡、串扰消除、回声消除、定时恢复和错误校正等功能,以提供10Mbps100Mbps或1000Mbps的强大收发功能

MAC和PHY之间的数据传输是通过RGMII接口进行的,RTL8211E支持RGMII的1.5V信号

RTL8211E的主要特点如下:

  • 兼容1000Base-T IEEE 802.3ab标准。
  • 兼容100Base-TX IEEE 802.3u标准。
  • 兼容10Base-T IEEE 802.3标准。
  • 支持GMII、RGMII接口。
  • 支持IEEE 802.3az-2010(节能以太网)。
  • 内置LAN唤醒(WOL)。
  • 支持中断、并行检测、交叉检测、自动校正、自动极性校正。
  • 支持120m的1000Base-T的CAT.5类电缆。
  • 支持RGMII的1.5V信号。
  • ……

RTL8211F千兆PHY的系统应用场景如下:

  • 数字电视(DTV)。
  • 媒体访问单元(MAU)。
  • 通讯和网络提升板(CNR)。
  • 游戏机。
  • 打印机和办公机器。
  • DVD播放机和刻录机。
  • 以太网集线器、交换机。

RTL8211F功能框图如下所示:

RTL8211F-CG详解

节能以太网(EEE)

RTL8211F支持IEEE 802.3az-2010(也称为节能以太网(EEE)),速率为10Mbps、100Mbps或1000Mbps时它提供了一个协议,可根据链路利用率协调进出较低功耗级别(低功耗空闲模式)的转换,当没有数据包传输时,系统进入低功耗空闲模式以降低功耗,一旦需要发送数据包,系统将返回正常模式,并且无需更改链接状态和丢弃/破坏帧即可进行此操作。

为了节省功率,当系统处于低功耗空闲模式时,大多数电路功能都被禁用;但是,低功耗空闲模式的过渡时间保持足够小,以使得对上层协议和应用程序透明。

中断管理

每当RTL8211F检测到介质状态发生变化时,它就会将中断引脚(INTB)拉低发出中断事件。SoC MAC端会感应到状态更改,并通过MDC/MDIO接口访问相关的寄存器

一旦MAC通过MDC/MDIO读取了这些状态寄存器,就会将INTB置为无效。所以不需要通过轮训的方式去反复查询状态寄存器的变化

PHY地址设置

MAC层通过MDIO/MDC总线对PHY进行读写操作,MDIO最多可以控制32个PHY芯片,通过不同的PHY芯片地址来对不同的PHY操作。RTL8211F通过配置PHYAD[2:0]这三个引脚来设置PHY地址,其中PHYAD2与RXCTL共用一个引脚PHYAD1与RXC共用一个引脚、PHYAD0与RXD3共用一个引脚。可设置的PHY地址范围为:0X001-0X111,如下图所示:

PHY地址设置

自动协商

RTL8211F可以开始启动协商功能,将PHYSR寄存器的bit12置1即可使能自动协商,如下图所示:

使能自动协商

RGMII接口电平支持

RTL8211F支持RGMII接口,可以通过CFG_EXT、CFG_LDO0和CFG_LDO1这三个引脚去配置RGMII的电平信号,配置方法如下图所示:

RGMII电平信号设置

正点原子V1.2版本之前的核心板CFG_EXT引脚外接3.3V上拉电阻,CFG_LDO0和CFG_LDO1都是接到低了,因此RGMII电压信号为 3.3V。

RTL8211F内部寄存器

RTL8211F的前16个寄存器满足IEEE的要求,在这里只介绍几个常用的寄存器,首先是BMCR(Basic Mode Control Register)寄存器,地址为0,BMCR 寄存器各位如下图所示:

BMCR寄存器

之前说的配置PHY芯片,重点就是配置BMCR寄存器。

接下来看一下BMSR(Basic Mode Status Register)寄存器,地址为1。此寄存器为PHY的状态寄存器,通过此寄存器可以获取到PHY芯片的工作状态,BMSR寄存器各位如下图所示:

BMSR寄存器

从上图中可以看出,不管什么PHY芯片,只要它实现了的位和IEEE规定相符就行。通过读取BMSR寄存器的值可以得到当前的连接速度、双工状态和连接状态等

接下来看一下RTL8211F的PHY ID 1和PHY ID 2这两个寄存器,这两个寄存器的地址分别为2和3,后面就把这两个寄存器叫做寄存器2和寄存器3。IEEE规定寄存器2和寄存器3为PHY的ID寄存器,这两个寄存器组成一个32位的唯一ID值。IEEE规定了一个叫做OUI的ID组成方式,全称是Organizationally Unique Identifier,OUI一共32位,分为三部分:22位的ID+6位厂商型号ID+4位厂商版本ID,组成如下图所示:

OUI组成方式

RTL8211F的ID寄存器2如下图所示:

PHY ID寄存器2

ID 寄存器3如下图所示:

PHY ID寄存器3

接着来看一下RTL8211F的GBCR(1000Base-T Control Register)寄存器,寄存器地址为0x9;该寄存器定义了1000Base-T功能的相关控制位,寄存器描述信息如下所示:

GBCR寄存器

关于RTL8211F这个PHY就讲解到这里,更加详细的内容大家可以查看RTL8211F对应的参考手册RTL8211F-CG.pdf。

YT8511C详解

此小节针对V1.3版本及以后核心板,YT8511C这颗PHY芯片是由苏州裕太车通电子科技有限公司出品的。

前面说了,PHY芯片的前16个寄存器都是一样的,而且只需要使用这前16个寄存器基本就可以驱动起来PHY芯片。这里就不详细讲解了,YT8511C和RTL8211F区别不大,此章节主
要是讲一下YT8511C的对于RTL8211F差异。

PHY地址设置

YT8511通过PHYADDR[2:0]这三个引脚来确定 PHY芯片地址 , 其中PHYADDR2-PHYADDR0分别对应LED_AC、RXD1和RXD0这三个引脚,配置如下图所示:

YT8511C PHY地址设置

正点原子的STM32MP157开发板上的YT8511的地址为0X00。

低功率模式

YT8511C的低功率模式是由RXD3引脚控制,配置如下图所示:

YT8511C低功率模式配置

从上图中可以看出来,RXD3为低电平的时候YT8511C进入低功率模式,RXD3为高电平的话YT8511C芯片就为正常功率模式。正点原子开发板RXD3连接高电平,所以YT8511C芯片为正常功率模式

工作模式

YT8511C的LED_1000引脚用来控制工作模式,配置如下图所示:

正常模式配置

从上图中可以看出来,LED_1000为低电平的话就进入测试模式,为高电平的话就是正常模式。正点原子开发板LED_1000连接高电平,所以YT8511C芯片为正常模式

RGMII电压配置

YT8511C的RGMII电压是由RX_DV引脚控制,如下图所示:

RGMII电压配置

从上图可以看出如果RX_DV为低电平的时候RGMII电平为3.3V,RX_DV为高电平的时候RGMII电平为2.5V。正点原子开发板RX_DV连接低电平,所以YT8511C的引脚电平为3.3V

125M时钟使能

STM32MP157千M网络工作的时候需要和PHY之间有一个125MHz的时钟,在这里YT8511C提供这个125MHz的时钟。但是默认情况下,YT8511C这个125MHz时钟没有使能,所以需要配置寄存器,使能这个125MHz。这里需要用到YT8511C的时钟控制寄存器,这是个扩展寄存器,地址为0X0C,这个寄存器是YT8511C官方自定义的,不属于IEEE规定的寄存器。时钟控制寄存器的bit2和bit1用于设置时钟输入,如下图所示:

时钟控制寄存器

从上图可以看出,当bit2:1为11的时候,输出125M时钟。

芯片YT8511C还有很多设置,这边就不一一列出了,可以直接查看芯片原理图就知道了。

Linux内核网络驱动框架

net_device结构体

Linux内核使用net_device结构体表示一个具体的网络设备,net_device是整个网络驱动的灵魂。网络驱动的核心就是初始化net_device结构体中的各个成员变量,然后将初始化完成以后的net_device注册到Linux内核中。net_device结构体定义在include/linux/netdevice.h中,net_device是一个庞大的结构体,内容如下(有缩减):

示例代码 52.3.1.1 net_device 结构体
1783 struct net_device {
1784     char name[IFNAMSIZ];
1785     struct hlist_node name_hlist;
1786     struct dev_ifalias __rcu *ifalias;
1787     /*
1788      * I/O specific fields
1789      * FIXME: Merge these and struct ifmap into one
1790      */
1791     unsigned long mem_end;
1792     unsigned long mem_start;
1793     unsigned long base_addr;
1794     int irq;
1795
1796     /*
1797      * Some hardware also needs these fields (state,dev_list,
1798      * napi_list,unreg_list,close_list) but they are not
1799      * part of the usual set specified in Space.c.
1800      */
1801
1802     unsigned long state;
1803
1804     struct list_head dev_list;
1805     struct list_head napi_list;
1806     struct list_head unreg_list;
1807     struct list_head close_list;
......
1841     const struct net_device_ops *netdev_ops;
1842     const struct ethtool_ops *ethtool_ops;
1843 #ifdef CONFIG_NET_L3_MASTER_DEV
1844     const struct l3mdev_ops *l3mdev_ops;
1845 #endif
1846 #if IS_ENABLED(CONFIG_IPV6)
1847     const struct ndisc_ops *ndisc_ops;
1848 #endif
1849
1850 #ifdef CONFIG_XFRM_OFFLOAD
1851     const struct xfrmdev_ops *xfrmdev_ops;
1852 #endif
1853
1854 #if IS_ENABLED(CONFIG_TLS_DEVICE)
1855     const struct tlsdev_ops *tlsdev_ops;
1856 #endif
1857
1858     const struct header_ops *header_ops;
1859
1860     unsigned int flags;
......
1869     unsigned char if_port;
1870     unsigned char dma;
1871
1872     /* Note : dev->mtu is often read without holding a lock.
1873      * Writers usually hold RTNL.
1874      * It is recommended to use READ_ONCE() to annotate the 
1875      * reads, and to use WRITE_ONCE() to annotate the writes.
1876      */
1877     unsigned int mtu;
1878     unsigned int min_mtu;
1879     unsigned int max_mtu;
1880     unsigned short type;
1881     unsigned short hard_header_len;
1882     unsigned char min_header_len;
1883
1884     unsigned short needed_headroom;
1885     unsigned short needed_tailroom;
1886
1887     /* Interface address info. */
1888     unsigned char perm_addr[MAX_ADDR_LEN];
1889     unsigned char addr_assign_type;
1890     unsigned char addr_len;
.....
1938     /*
1939      * Cache lines mostly used on receive path (including 
1940     eth_type_trans()) */
1941     /* Interface address info used in eth_type_trans() */
1942     unsigned char *dev_addr;
1943
1944     struct netdev_rx_queue *_rx;
1945     unsigned int num_rx_queues;
1946     unsigned int real_num_rx_queues;
......
1967     /*
1968      * Cache lines mostly used on transmit path
1969      */
1970     struct netdev_queue *_tx ____cacheline_aligned_in_smp;
1971     unsigned int num_tx_queues;
1972     unsigned int real_num_tx_queues;
1973     struct Qdisc *qdisc;
1974 #ifdef CONFIG_NET_SCHED
1975     DECLARE_HASHTABLE (qdisc_hash, 4);
1976 #endif
1977     unsigned int tx_queue_len;
1978     spinlock_t tx_global_lock;
1979     int watchdog_timeo;
.....
2061     struct phy_device *phydev;
.....
2069 };

下面介绍一些关键的成员变量,如下:

第1784行:name是网络设备的名字。

第1791行:mem_end是共享内存结束地址。

第1792行:mem_start是共享内存起始地址。

第1793行:base_addr是网络设备I/O地址。

第1794行:irq是网络设备的中断号。

第1804行:dev_list是全局网络设备列表。

第1805行:napi_list是napi网络设备的列表入口。

第1806行:unreg_list是注销(unregister)的网络设备列表入口。

第1807行:close_list是关闭的网络设备列表入口。

第1841行:netdev_ops是网络设备的操作集函数,包含了一系列的网络设备操作回调函数,
类似字符设备中的file_operations,稍后会讲解netdev_ops结构体。

第1842行:ethtool_ops是网络管理工具相关函数集,用户空间网络管理工具会调用此结构体中的相关函数获取网卡状态或者配置网卡。

第1858行:header_ops是头部的相关操作函数集,比如创建、解析、缓冲等。

第1860行:flags是网络接口标志,标志类型定义在include/uapi/linux/if.h文件中,为一个枚举类型,内容如下:

网络标志类型flags

继续回到示例代码52.3.1.1中,接着看net_device结构体。

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

端口类型if_port

第1870行:dma是网络设备所使用的DMA通道,不是所有的设备都会用到DMA。

第1877行:mtu是网络最大传输单元,为1500。

第1880行:type用于指定ARP模块的类型,以太网的ARP接口为ARPHRD_ETHER,Linux内核所支持的ARP协议定义在include/uapi/linux/if_arp.h中,可以自行查阅。

第1888行:perm_addr是永久的硬件地址,如果某个网卡设备有永久的硬件地址,那么就会填充perm_addr。

第1890行:addr_len是硬件地址长度。

第1942行:dev_addr也是硬件地址,是当前分配的MAC地址,可以通过软件修改。

第1944行:_rx是接收队列。

第1945行:num_rx_queues是接收队列数量,在调用register_netdev注册网络设备的时候会分配指定数量的接收队列。

第1946行:real_num_rx_queues是当前活动的队列数量。

第1970行:_tx是发送队列。

第1971行:num_tx_queues是发送队列数量,通过alloc_netdev_mq函数分配指定数量的发送队列。

第1972行:real_num_tx_queues是当前有效的发送队列数量。

第2061行:phydev是对应的PHY设备。

申请net_device

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

#define alloc_netdev(sizeof_priv, name, name_assign_type, setup) \
 	alloc_netdev_mqs(sizeof_priv, name, name_assign_type, setup, 1, 1)

可以看出alloc_netdev的本质是alloc_netdev_mqs 函数,此函数原型如下:

struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
						unsigned char name_assign_type,
						void (*setup)(struct net_device *),
						unsigned int txqs, unsigned int rxqs)

函数参数和返回值含义如下:

  • sizeof_priv:私有数据块大小。
  • name:设备名字。
  • name_assign_type:设备名字的来源。
  • setup:回调函数,初始化设备的设备后调用此函数。
  • txqs:分配的发送队列数量。
  • rxqs:分配的接收队列数量。
  • 返回值:如果申请成功的话就返回申请到的net_device指针,失败的话就返回NULL。

事实上网络设备有多种,不要以为就只有以太网一种。Linux内核支持的网络接口有很多,比如光纤分布式数据接口(FDDI)、以太网设备(Ethernet)、红外数据接口(InDA)、高性能并行接口(HPPI)、CAN网络等。内核针对不同的网络设备在alloc_netdev的基础上提供了一层封装,比如本章讲解的以太网,针对以太网封装的net_device申请函数是alloc_etherdev和alloc_etherdev_mq,这也是一个宏,内容如下:

#define alloc_etherdev(sizeof_priv) alloc_etherdev_mq(sizeof_priv, 1)
#define alloc_etherdev_mq(sizeof_priv, count) alloc_etherdev_mqs(sizeof_priv, count, count)

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

alloc_etherdev_mqs函数

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

ether_setup函数会对net_device做初步的初始化,函数内容如下所示:

ether_setup函数

关于net_device的申请就讲解到这里,对于网络设备而言,使用alloc_etherdev或alloc_etherdev_mqs来申请net_device。ST官方编写的网络驱动就是采用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_”开头的函数,这些函数就需要网络驱动编写人员去实现,不需要全部都实现,根据实际驱动情况实现其中一部分即可。结构体内容如下所示(结
构体成员变量比较多,这里有缩减):

net_device_ops结构体内容

第1250行:ndo_init函数,当第一次注册网络设备的时候此函数会执行,设备可以在此函数中做一些需要推后初始化的内容,不过一般驱动中不使用此函数,虚拟网络设备可能会使用

第1251行:ndo_uninit函数,卸载网络设备的时候此函数会执行

第1252行:ndo_open函数,打开网络设备的时候此函数会执行,网络驱动程序需要实现此函数,非常重要!以STM32MP1系列SoC网络驱动为例会在此函数中做如下工作:

  • 初始化接收缓冲区。
  • 如果使用NAPI的话要使能NAPI模块,通过napi_enable函数来使能。
  • STM32MP1 MAC控制器硬件相关初始化。
  • 开启PHY。
  • 调用netif_tx_start_all_queues来使能传输队列,也可能调用netif_start_queue函数。
  • ……

第1253行:ndo_stop函数,关闭网络设备的时候此函数会执行,网络驱动程序也需要实现此函数。以STM32MP1系列SoC网络驱动为例,会在此函数中做如下工作:

  • 停止PHY。
  • 停止NAPI功能。
  • 停止发送功能。
  • 关闭MAC。
  • 断开PHY连接。
  • 释放数据缓冲区。
  • ……

第1254行:ndo_start_xmit函数,当需要发送数据的时候此函数就会执行,此函数有一个参数为sk_buff结构体指针,sk_buff结构体在Linux的网络驱动中非常重要,sk_buff保存了上层传递给网络驱动层的数据。也就是说,要发送出去的数据都存在了sk_buff中,关于sk_buff稍后会做详细的讲解。如果发送成功的话此函数返回NETDEV_TX_OK,如果发送失败了就返回NETDEV_TX_BUSY,如果发送失败了就需要停止队列。

第1259行:do_select_queue函数,当设备支持多传输队列的时候选择使用哪个队列。

第1264行:ndo_set_rx_mode函数,此函数用于改变地址过滤列表,根据net_device的flags成员变量来设置SoC的网络外设寄存器。比如flags可能为IFF_PROMISC、IFF_ALLMULTI或IFF_MULTICAST,分别表示混杂模式、单播模式或多播模式。

第1265行:ndo_set_mac_address函数,此函数用于修改网卡的MAC地址,设置net_device的dev_addr成员变量,并且将MAC地址写入到网络外设的硬件寄存器中。

第1267行:ndo_validate_addr函数,验证MAC地址是否合法,也即是验证net_devic的dev_addr中的MAC地址是否合法,直接调用is_valid_ether_addr函数。

第1268行:ndo_do_ioctl函数,用户程序调用ioctl的时候此函数就会执行,比如PHY芯片相关的命令操作,一般会直接调用phy_mii_ioctl函数。

第1272行:ndo_change_mtu函数,更改MTU大小。

第1276行:ndo_tx_timeout函数,当发送超时的时候产生会执行,一般都是网络出问题了导致发送超时。一般可能会重启MAC和PHY,重新开始数据发送等。

第1291行:ndo_poll_controller函数,使用查询方式来处理网卡数据的收发。

第1368行:ndo_set_features函数,修改net_device的features属性,设置相应的硬件属性。

sk_buff结构体

网络是分层的,对于应用层而言不用关心具体的底层是如何工作的,只需要按照协议将要发送或接收的数据打包好即可。打包好以后都通过dev_queue_xmit函数将数据发送出去,接收数据的话使用netif_rx函数即可,依次来看一下这两个函数

dev_queue_xmit函数

此函数用于将网络数据发送出去,函数定义在include/linux/netdevice.h中,函数原型如下:

int dev_queue_xmit(struct sk_buff *skb)

函数参数和返回值含义如下:

  • skb:要发送的数据,这是一个sk_buff结构体指针,sk_buff是Linux网络驱动中一个非常重要的结构体,网络数据就是以sk_buff保存的,各个协议层在sk_buff中添加自己的协议头,最终由底层驱动将sk_buff中的数据发送出去。网络数据的接收过程恰好相反,网络底层驱动将接收到的原始数据打包成sk_buff,然后发送给上层协议,上层会取掉相应的头部,然后将最终的数据发送给用户
  • 返回值:0,发送成功;负值,发送失败。

dev_queue_xmit函数太长,这里就不详细的分析了,dev_queue_xmit函数最终是通过net_device_ops操作集里面的ndo_start_xmit函数来完成最终发送了ndo_start_xmit就是网络驱动编写人员去实现的,整个路程如下图所示:

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

示例代码 52.3.3.1 sk_buff 结构体
685 struct sk_buff {
686     union {
687         struct {
688             /* These two members must be first. */
689             struct sk_buff *next;
690             struct sk_buff *prev;
691
692             union {
693                 struct net_device *dev;
694                 /* Some protocols might use this space to store 
695                  * information, while device pointer would be NULL.
696                  * UDP receive path is one user.
697                  */
698                 unsigned long dev_scratch;
699             };
700         };
701         struct rb_node rbnode;
702         struct list_head list;
703     };
704
705     union {
706         struct sock *sk;
707         int ip_defrag_offset;
708     };
709
710     union {
711         ktime_t tstamp;
712         u64 skb_mstamp_ns; /* earliest departure time */
713     };
714     /*
715      * This is the control buffer. It is free to use for every
716      * layer. Please put your private variables there. If you
717      * want to keep them across layers you have to do a skb_clone()
718      * first. This is owned by whoever has the skb queued ATM.
719      */
720     char cb[48] __aligned(8);
721
722     union {
723         struct {
724             unsigned long _skb_refdst;
725             void (*destructor)(struct sk_buff *skb);
726         };
727         struct list_head tcp_tsorted_anchor;
728     };
.....
733     unsigned int len,
734                  data_len;
735     __u16        mac_len,
736                  hdr_len;
......
868     __be16       protocol;
869     __u16        transport_header;
870     __u16        network_header;
871     __u16        mac_header;
872
873     /* private: */
874     __u32        headers_end[0];
875     /* public: */
876
877     /* These elements must be at the end, see alloc_skb() for 
details. */
878     sk_buff_data_t tail;
879     sk_buff_data_t end;
880     unsigned char *head,
881                   *data;
882     unsigned int truesize;
883     refcount_t users;
884
885 #ifdef CONFIG_SKB_EXTENSIONS
886     /* only useable after checking ->active_extensions != 0 */
887     struct skb_ext *extensions;
888 #endif
889 };

第687-700 行:next和prev分别指向下一个和前一个sk_buff,构成一个双向链表

第693行:dev表示当前sk_buff从哪个设备接收到或者发出的。

第706行:sk表示当前sk_buff所属的Socket。

第711行:tstamp表示数据包接收时或准备发送时的时间戳。

第720行:cb为控制缓冲区,不管哪个层都可以自由使用此缓冲区,用于放置私有数据。

第725行:destructor函数,当释放缓冲区的时候可以在此函数里面完成某些动作。

第733-734行:len为实际的数据长度,包括主缓冲区中数据长度和分片中的数据长度。data_len为数据长度,只计算分片中数据的长度。

第735-736行:mac_len为连接层头部长度,也就是MAC头的长度。

第868行:protocol协议。

第869行:transport_header为传输层头部。

第870行:network_header为网络层头部。

第871行:mac_header为链接层头部。

第878行:tail指向实际数据的尾部。

第879行:end指向缓冲区的尾部。

第880行:head指向缓冲区的头部,data指向实际数据的头部。data和tail指向实际数据的头部和尾部,head和end指向缓冲区的头部和尾部。结构如下图所示:

sk_buff数据区结构示意图

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

1、分配sk_buff

要使用sk_buff必须先分配,首先来看一下alloc_skb这个函数,此函数定义include/linux/skbuff.h中,函数原型如下:

static inline struct sk_buff *alloc_skb(unsigned int size,
										gfp_t priority)

函数参数和返回值含义如下:

  • size:要分配的大小,也就是skb数据段大小。
  • priority:为GFP MASK宏,比如GFP_KERNEL、GFP_ATOMIC等。
  • 返回值:分配成功的话就返回申请到的sk_buff首地址,失败的话就返回NULL。

在网络设备驱动中常常使用netdev_alloc_skb来为某个设备申请一个用于接收的skb_buff,此函数也定义在include/linux/skbuff.h中,函数原型如下:

static inline struct sk_buff *netdev_alloc_skb(struct net_device *dev,
											   unsigned int length)

函数参数和返回值含义如下:

  • dev:要给哪个设备分配sk_buff。
  • length:要分配的大小。
  • 返回值:分配成功的话就返回申请到的sk_buff首地址,失败的话就返回NULL。

2、释放sk_buff

使用完成以后就要释放掉sk_buff,释放函数可以使用kfree_skb,函数定义在include/linux/skbuff.c中,函数原型如下:

void kfree_skb(struct sk_buff *skb)

函数参数和返回值含义如下:

  • skb:要释放的sk_buff。
  • 返回值:无。

对于网络设备而言最好使用如下所示释放函数

void dev_kfree_skb (struct sk_buff *skb)

函数只要一个参数skb,就是要释放的sk_buff

3、skb_put、skb_push、skb_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_put函数操作前后对比

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

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

函数参数和返回值含义如下:

  • skb:要操作的sk_buff。
  • len:要增加多少个字节。
  • 返回值:扩展完成以后新的数据区首地址。

skb_push操作之前和操作之后的数据区如下图所示:

skb_push函数操作前后对比

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

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

函数参数和返回值含义如下:

  • skb:要操作的sk_buff。
  • len:要删除的字节数。
  • 返回值:删除以后新的数据区首地址。

skb_pull操作之前和操作之后的数据区如下图所示:

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的网络驱动中得到了大量的应用,ST官方编写的网络驱动都是采用的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,函数原型如下:

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

示例代码 52.3.4.1 napi_schedule 函数代码
442 static inline void napi_schedule(struct napi_struct *n)
443 {
444     if (napi_schedule_prep(n))
445     __napi_schedule(n);
446 }

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

NAPI处理完成

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

inline void napi_complete(struct napi_struct *n)

函数参数和返回值含义如下:

  • n:处理完成的NAPI。
  • 返回值:无。

原理图介绍

正点原子的STM32MP1开发板网络相关的硬件有两个版本分别为:V1.2版本以前的核心板和V1.3版本以后的核心板。

V1.2版本以前的核心板

V1.2版本以前的核心板的原理图,如下所示:

V1.2版本以前的核心板原理图

在之前的学习中已经知道了PHY地址是由RXCTL、RXC和RXD3控制的,从上图中知道了这三个引脚为0、0和1,PHY的地址为0x1,RGMII的电平为3.3V。

V1.3版本以后的核心板

V1.3版本以后的核心板原理图,如下所示:

V1.3版本以后得核心板原理图

STM32MP1网络驱动框架

STM32MP1网络外设设备树

上一小节对Linux的网络驱动框架进行了一个简单的介绍,本节就来简单分析一下STM32MP1的网络驱动源码。肯定是先分析设备树,STM32MP1系列SoC网络绑定文档为Documentation/devicetree/bindings/net/stm32-dwmac.txt,此绑定文档描述了STM32MP1系列SoC网络设备树节点的要求 。 此外还有一份文档Documentation/devicetree/bindings/net/ethernet.txt,该文档描述了网络设备节点的一些通用属性。
在内核源码目录arch/arm/boot/dts/stm32mp152.dtsi设备树文件中,定义了网络设备节点,如下所示:

ethernet0节点

第1605行,compatible属性有两个分别为st,stm32mp1-dwmac和snps,dwmac-4.20a。在内
核源码目录里搜索这两个属性值,就能找到drivers/net/ethernet/stmicro/stmmac/dwmac-stm32.c文件。dwmac-stm32.c就是stm32mp1的驱动源码,后面在分析。

第1606行,reg属性值表示网络的物理地址大小。

第1621行,snps,mixed-burst属性,表示DMA使用混合突发模式。

第1622行,snps,pbl属性值为2,表示突发长度为2。

第1623行,snps,en-tx-lpi-clockgating属性,在TX低功耗模式下,使能MAC TX门控时钟。

第1625行,snps,tso属性,表示启动tso功能,tso全称为TCP Segment Offload,也就是利用网卡的少量处理能力,降低CPU发送数据包负载的技术,需要网卡硬件及驱动支持。

示例代码52.5.1.1是ST官方编写的,不需要去修改,此示例代码52.5.1.1是不能正常工作的,还需要根据实际情况添加或者修改一些属性。打开stm32mp157d-atk.dtsi,找到如下内容:

stm32mp157d-atk.dtsi中的网络设备

从示例代码52.5.1.2中可以看到,ethernet0就是要追加的内容,接下来来看看这些追加属性表示什么意思。

  • status:这个属性就不用说了,把status属性值修改为“okay”,使能网络。
  • pinctrl-XXX:这些属性不用说了,就是定义了网络的引脚复用功能。在stm32mp15-pinctrl.dtsi里定义了ethernet0_rgmii_pins_a和ethernet0_rgmii_pins_sleep_a。
  • phy-mode:该属性值是一个字符串,表示网络所使用的PHY接口模式,是MII、RMII、GMII还是RGMII等,它是一个标准的属性,支持的字符串值有‘mii’、‘gmii’、‘sgmii’、‘rmii’、‘rgmii’、‘rgmii-id’、‘rgmii-rxid’、‘rgmii-txid’、‘xgmii’等等。第6行中,phy-mode属性的值等于‘rgmii-id’表示使用的是RGMII接口模式,由PHY提供RX和TX延迟,MAC不需要添加RX和TX延时内容。与RGMII接口相关的值有‘rgmii’、‘rgmii-id’、‘rgmii-rxid’、‘rgmii-txid’这些,ethernet.txt文件里面详细讲解了这些属性值的含义,这里就不详细介绍了。
  • max-speed:PHY支持的最高速度,比如10、100或1000。
  • phy- handle:连接到此网络设备的PHY芯片句柄。
  • mdio:第10-17行中定义了一个mdio子节点,该节点用于描述MDIO总线。#address-cell属性和#size-cells属性用于描述mdio节点的子节点中所定义的reg属性,这个reg属性其实就表示PHY芯片的地址。

STM32MP1网络驱动源码简析

重要的结构体

每个驱动都会自定义自己的结构体,要了解驱动的注册过程,就先从自定义结构体看起。前面说了驱动文件为:drivers/net/ethernet/stmicro/stmmac/dwmac-stm32.c,打开dwmac-stm32.c,找到如下所示内容:

stm32_dwmac结构体

示例代码52.5.2.1中,主要是第101行,ops成员,表示网络相关的操作集,驱动人员要实现的,ST官方已经写好了;其它属性成员都是时钟相关的,肯定是通过设备树配置时钟。去了解一下stm32_ops结构体代码如下所示:

stm32_ops结构体

单片机要使用网络的时候,需要初始化网络相关的寄存器,stm32_ops就是根据设备树参数去设置网络相关的寄存器,后面会分析stm32_ops。

plat_stmmacenet_data结构体很重要,将网络设备注册到内核的时候需要使用到此结构体。此结构体定义在include/linux/stmmac.h,plat_stmmacenet_data结构体原型如下示例代码所示:

plat_stmmacenet_data结构体

示例代码52.5.2.3中的内容比较多,有省略,只列出了驱动要用到的一些参数。stm32_dwmac和stm32_ops这两个结构体主要是用作初始化网络,plat_stmmacenet_data结构体主要用于注册网络设备

stm32_dwmac_probe函数

STM32MP1网络驱动主要分两部分:STM32MP1网络外设MAC驱动以及PHY芯片驱动,MAC驱动是ST编写的,PHY芯片有通用驱动文件,有些PHY芯片厂商还会针对自己的芯片编写对应的PHY驱动。总体来说,SoC内置网络MAC+外置PHY芯片这种方案是不需要编写什么驱动的,基本可以直接使用。但是为了学习,还是要简单分析一下具体的网络驱动编写过程。打开dwmac-stm32.c,找到如下所示内容:

STM32系列SoC网络平台驱动匹配表

第528行,匹配表包含“st,stm32mp1-dwmac”,因此设备树和驱动匹配上,当匹配成功以后第534行的stm32_dwmac_probe函数就会执行,简单分析一下stm32_dwmac_probe函数,函数内容如下:

示例代码 52.5.2.5 stm32_dwmac_probe 函数
360 static int stm32_dwmac_probe(struct platform_device *pdev)
361 {
362     struct plat_stmmacenet_data *plat_dat;
363     struct stmmac_resources stmmac_res;
364     struct stm32_dwmac *dwmac;
365     const struct stm32_ops *data;
366     int ret;
367
368     ret = stmmac_get_platform_resources(pdev, &stmmac_res);
369     if (ret)
370         return ret;
371
372     plat_dat = stmmac_probe_config_dt(pdev, &stmmac_res.mac);
373     if (IS_ERR(plat_dat))
374         return PTR_ERR(plat_dat);
375
376     dwmac = devm_kzalloc(&pdev->dev, sizeof(*dwmac), GFP_KERNEL);
377     if (!dwmac) {
378         ret = -ENOMEM;
379         goto err_remove_config_dt;
380     }
381
382     data = of_device_get_match_data(&pdev->dev);
383     if (!data) {
384         dev_err(&pdev->dev, "no of match data provided\n");
385         ret = -EINVAL;
386         goto err_remove_config_dt;
387     }
388
389     dwmac->ops = data;
390     dwmac->dev = &pdev->dev;
391
392     ret = stm32_dwmac_parse_data(dwmac, &pdev->dev);
393     if (ret) {
394         dev_err(&pdev->dev, "Unable to parse OF data\n");
395         goto err_remove_config_dt;
396     }
397
398     if (stmmac_res.wol_irq && !dwmac->clk_eth_ck) {
399     ret = stm32_dwmac_wake_init(&pdev->dev, &stmmac_res);
400     if (ret)
401         return ret;
402     }
403
404     plat_dat->bsp_priv = dwmac;
405
406     ret = stm32_dwmac_init(plat_dat);
407     if (ret)
408         goto err_remove_config_dt;
409
410     ret = stmmac_dvr_probe(&pdev->dev, plat_dat, &stmmac_res);
411     if (ret)
412         goto err_clk_disable;
413
414     return 0;
415
416 err_clk_disable:
417     stm32_dwmac_clk_disable(dwmac);
418 err_remove_config_dt:
419     stmmac_remove_config_dt(pdev, plat_dat);
420
421     return ret;
422 }

第368行,使用stmmac_get_platform_resources函数获取设备树上的资源,保存到struct
stmmac_resources类型的结构体里。

第372行,plat_dat变量指针类型为plat_stmmacenet_data结构体,plat_dat结构体主要是保存网络设备的参数,根据plat_dat的参数将网络设备注册进内核stmmac_probe_config_dt函数作用是根据设备树上的属性值去填充plat_dat各个成员。stmmac_probe_config_dt函数内容如下示例代码所示(有省略):

示例代码 52.5.2.6 stmmac_probe_config_dt 函数
395 struct plat_stmmacenet_data *
396 stmmac_probe_config_dt(struct platform_device *pdev,
const char **mac)
397 {
398     struct device_node *np = pdev->dev.of_node;
399     struct plat_stmmacenet_data *plat;
400     struct stmmac_dma_cfg *dma_cfg;
401     int rc;
402
403     plat = devm_kzalloc(&pdev->dev, sizeof(*plat), GFP_KERNEL);
404     if (!plat)
405         return ERR_PTR(-ENOMEM);
406
407     *mac = of_get_mac_address(np);
408     if (IS_ERR(*mac)) {
409         if (PTR_ERR(*mac) == -EPROBE_DEFER)
410             return ERR_CAST(*mac);
411
412         *mac = NULL;
413     }
414
415     plat->phy_interface = of_get_phy_mode(np);
416     if (plat->phy_interface < 0)
417         return ERR_PTR(plat->phy_interface);
.....
425     plat->phy_node = of_parse_phandle(np, "phy-handle", 0);
426
427     /* PHYLINK automatically parses the phy-handle property */
428     plat->phylink_node = np;
429
430     /* Get max speed of operation from device tree */
431     if (of_property_read_u32(np, "max-speed", &plat->max_speed))
432         plat->max_speed = -1;
433
434     plat->bus_id = of_alias_get_id(np, "ethernet");
435     if (plat->bus_id < 0)
436         plat->bus_id = 0;
437
438     /* Default to phy auto-detection */
439     plat->phy_addr = -1;
......
454     rc = stmmac_dt_phy(plat, np, &pdev->dev); 
......
607     return plat;
608
609 error_hw_init:
610     clk_disable_unprepare(plat->pclk);
611 error_pclk_get:
612     clk_disable_unprepare(plat->stmmac_clk);
613
614     return ERR_PTR(-EPROBE_DEFER);
615 }

stmmac_probe_config_dt函数会新建一个plat_dat 指针变量,从设备树获取数据初始化palt_dat里的成员,返回plat_dat的指针地址

第407行,使用of_get_mac_address函数从设备树获取MAC地址,这里没有定义。

第415行,使用of_get_phy_mode函数从设备树获取接口模式,获取phy-mode属性值。这里属性值为“rgmii-id”。

第425行,使用of_parse_phandle函数从设备树获取PHY句柄。

第431行,使用of_property_read_u32函数从设备树获取max-speed属性值。这里属性值为“1000”,表示网络为1000M网络。

第434行,获取网络的别名。

第439行,设置plat结构体里的phy_addr成员phy_addr设置为“-1”,phy_addr表示PHY的地址。设置为-1表示自动检测,一个MDIO接口最多可以连接32个PHY芯片,设置为-1就是自动遍历所有的PHY地址,看看哪个地址上有PHY芯片,然后将对应的PHY地址重新赋值给phy_addr。

第454行,使用stmmac_dt_phy函数去获取“mdio”节点中的信息。

继续返回示例代码52.5.2.4中的进行解释。

第376行,给dwmac结构体开空间。

第382行,获取of_device_id里的data成员值,获取ST官方自定义的stm32_ops操作集。

第389行,把获取的stm32_ops地址,赋值到stm32_dwmac结构体里的ops成员,初始化网络的时候,可以调用ops就能操作到网络相关的寄存器。

第392行,使用stm32_dwmac_parse_data函数主要是负责从设备树里获取时钟和基地址。

第405行,调用stm32_dwmac_init函数进行,初始化网络设备,函数的原型如下示例代码所示:

stm32_dwmac_init函数

第121行,调用stm32_ops结构体里的set_mode函数进行寄存器的初始化。

第126行,使能时钟。

继续返回示例代码52.5.2.5中stm32_dwmac_probe函数。

stm32_dwmac_probe函数第410行,调用stmmac_dvr_probe函数进行网络注册,同时完成GMII/RGMII接口初始化。

stmmac_dvr_probe函数调用netif_napi_add函数来设置poll函数,如下图所示:

设置poll函数

从上图可以知道,通过netif_napi_add函数向网卡加了一个napi示例,使用NAPI驱动要提供一个poll函数来轮询处理接收数据,发送和接收的poll函数分别为:“stmmac_napi_poll_tx”和“stmmac_napi_poll_rx”。后面分析网络数据接收处理流程的时候详细讲解这个两个函数。

继续分析stmmac_dvr_probe函数,此函数会调用stmmac_mdio_register函数来向内核注册MDIO总线,stmmac_mdio_register函数重点是下图中的两行代码:

mdio读写函数

new_bus下的read和write这两个成员变量分别是读/写PHY寄存器的操作函数,这设置为stmmac_mdio_read和stmmac_mdio_write,这两个函数就是STM32MP1系列SoC读写PHY内部寄存器的函数。读写PHY寄存器都会通过这两个MDIO总线函数完成。stmmac_mdio_register函数最终会向Linux内核注册MDIO总线,相关代码如下所示:

stmmac_mdio_register函数注册mdio总线

第1行,通过of_mdiobus_register向内核注册MDIO总线,如果设备树定义了mdio节点和PHY句柄还会注册PHY设备。

第7行,判断PHY设备是否注册成功,没有就遍历所有的PHY地址找到对应的PHY设备。

第10-28行,如果设备树里没有mdio节点和PHY句柄,通过遍历所有的PHY地址,找到硬件所对应的PHY设备。

最后stmmac_dvr_probe函数调用register_netdev函数进行网络设备注册。如下图所示:

register_netdev函数

MDIO总线注册

MDIO讲了很多次了,就是用来管理PHY芯片的,分为MDIO和MDC两根线,Linux内核专门为MDIO准备一个总线,叫做MDIO总线,采用mii_bus结构体表示,定义在include/linux/phy.h文件中,mii_bus结构体如下所示(限于篇幅,有省略):

mii_bus结构体

重点是第217、218两行的read和write函数,这两个函数就是读/写PHY芯片的操作函数,不同的SoC其MDIO主控部分是不一样的,因此需要驱动编写人员去编写。前面在分析stm32_dwmac_probe函数的时候已经讲过了,stm32_dwmac_probe函数会调用stmmac_mdio_register函数完成MII接口的初始化,其中就包括初始化mii_bus下的read和write这两个函数。最终通过of_mdiobus_register或者mdiobus_register函数将初始化以后的mii_bus注册到Linux内核,of_mdiobus_register函数其实也是调用的mdiobus_register函数来完成mii_bus注册的。先看下of_mdiobus_register函数,如下示例代码所示:

示例代码 52.5.2.10 of_mdiobus_register 函数
199 int of_mdiobus_register(struct mii_bus *mdio,
struct device_node *np)
200 {
201     struct device_node *child;
202     bool scanphys = false;
203     int addr, rc;
204
205     if (!np)
206         return mdiobus_register(mdio);
207
208     /* Do not continue if the node is disabled */
209     if (!of_device_is_available(np))
210         return -ENODEV;
211
212     /* Mask out all PHYs from auto probing. Instead the PHYs listed 
213      * in the device tree are populated after the bus has been 
registered */
214     mdio->phy_mask = ~0;
215
216     mdio->dev.of_node = np;
217     mdio->dev.fwnode = of_fwnode_handle(np);
218
219     /* Get bus level PHY reset GPIO details */
220     mdio->reset_delay_us = DEFAULT_GPIO_RESET_DELAY;
221     of_property_read_u32(np, "reset-delay-us",
&mdio->reset_delay_us);
222
223     /* Register the MDIO bus */
224     rc = mdiobus_register(mdio);
225     if (rc)
226         return rc;
227
228     /* Loop over the child nodes and register a phy_device for each 
phy      */
229     for_each_available_child_of_node(np, child) {
230         addr = of_mdio_parse_addr(&mdio->dev, child);
231         if (addr < 0) {
232             scanphys = true;
233             continue;
234         }
235
236         if (of_mdiobus_child_is_phy(child))
237             rc = of_mdiobus_register_phy(mdio, child, addr);
238         else
239             rc = of_mdiobus_register_device(mdio, child, addr);
240
241         if (rc == -ENODEV)
242             dev_err(&mdio->dev,
243                 "MDIO device at address %d is missing.\n",
244                 addr);
245         else if (rc)
246             goto unregister;
247     }
.....
281 }

of_mdiobus_register函数主要是注册mii总线(mii_bus),然后遍历所有的PHY地址,当找到对应的PHY芯片以后就会创建PHY设备,最后将这个PHY设备注册到内核中。使用mdiobus_scan函数进行注册PHY设备。

第206行,设备节点不存在的时候此行代码执行,本实验设备节点肯定存在,所以之类不会运行。

第224行,当设备树中mdio节点存在,就会将相关属性信息赋值给mdio参数,然后调用mdiobus_register向内核注册此mii总线。

第229行,轮询mdio节点下的所有phy子节点,比如示例代码52.5.1.2中的“phy0:ethernet-phy@0”这个子节点,它描述的是PHY芯片信息。

第237行,如果找到一个PHY子节点,就说明找到了一个PHY芯片,那么就调用of_mdiobus_register_phy函数向内核注册此PHY设备。

接下来简单分析一下of_mdiobus_register_phy函数,看看如何向Linux内核注册PHY设备的,of_mdiobus_register_phy函数内容如下所示:

示例代码 52.5.2.11 of_mdiobus_register_phy 函数
1  static int of_mdiobus_register_phy(struct mii_bus *mdio,
2                      struct device_node *child, u32 addr)
3  {
4      struct phy_device *phy;
5      bool is_c45;
6      int rc;
7      u32 phy_id;
8
9      is_c45 = of_device_is_compatible(child,
10                     "ethernet-phy-ieee802.3-c45");
11
12     if (!is_c45 && !of_get_phy_id(child, &phy_id))
13         phy = phy_device_create(mdio, addr, phy_id, 0, NULL);
14     else
15         phy = get_phy_device(mdio, addr, is_c45);
16     if (IS_ERR(phy))
17         return PTR_ERR(phy);
18
19     rc = of_irq_get(child, 0);
20     if (rc == -EPROBE_DEFER) {
21         phy_device_free(phy);
22         return rc;
23     }
24     if (rc > 0) {
25         phy->irq = rc;
26         mdio->irq[addr] = rc;
27     } else {
28         phy->irq = mdio->irq[addr];
29     }
30
31     if (of_property_read_bool(child, "broken-turn-around"))
32         mdio->phy_ignore_ta_mask |= 1 << addr;
33
34     of_property_read_u32(child, "reset-assert-us",
35             &phy->mdio.reset_assert_delay);
36     of_property_read_u32(child, "reset-deassert-us",
37             &phy->mdio.reset_deassert_delay);
38
39     /* Associate the OF node with the device structure so it
40      * can be looked up later */
41     of_node_get(child);
42     phy->mdio.dev.of_node = child;
43     phy->mdio.dev.fwnode = of_fwnode_handle(child);
44
45     /* All data is now stored in the phy struct;
46      * register it */
47     rc = phy_device_register(phy);
48     if (rc) {
49         phy_device_free(phy);
50         of_node_put(child);
51         return rc;
52     }
53
54     dev_dbg(&mdio->dev, "registered phy %pOFn at address %i\n",
55         child, addr);
56     return 0;
57 }

第9行,使用函数of_device_is_compatible检查PHY节点的compatible属性是否为“ethernet-phy-ieee802.3-c45”,如果是的话要做其他的处理,这里没有设置此属性。

第15行,调用get_phy_device函数获取PHY设备,此函数里面会调用phy_device_create来创建一个phy_device设备并返回。

第19行,获取PHY芯片的中断信息,本章节并未用到。

第47行,调用phy_device_register函数向Linux内核注册PHY设备。

从上面的分析可以看出,向Linux内核注册MDIO总线的时候也会同时向Linux内核注册PHY设备,流程如下图所示:

MDIO总线注册流程

注册MDIO总线的时候会先从设备树中查找PHY设备,没有就会遍历所有的PHY设备,然后通过phy_device_register函数向内核注册PHY设备。

stm32_dwmac_remove函数简析

卸载STM32MP1网络驱动的时候stm32_dwmac_remove函数就会执行,函数内容如下所示:

stm32_dwmac_remove函数

第428行调用了stmmac_dvr_remove函数进行卸载,此函数内容如下所示:

stmmac_dvr_remove函数

第8行,调用stmmac_stop_all_dma函数结束所有的DMA。

第12行,调用unregister_netdev函数注销前面注册的net_device。

第24行,调用stmmac_mdio_unregister函数来移除掉MDIO总线相关的内容,此函数会调用mdiobus_unregister来注销掉mii_bus,并且通过函数mdiobus_free释放掉mii_bus。

第25行,释放工作队列。

stmmac_netdev_ops操作集

stmmac_dvr_probe函数设置了网卡驱动的net_dev_ops操作集为stmmac_netdev_ops,stmmac_netdev_ops内容如下:

stmmac_netdev_ops操作集

1、stmmac_open函数简析

打开一个网卡的时候stmmac_open函数就会执行,函数源码如下所示(限于篇幅原因,有省
略):

示例代码 52.5.3.1 stmmac_open 函数
1  static int stmmac_open(struct net_device *dev)
2  {
3      struct stmmac_priv *priv = netdev_priv(dev);
4      int bfsize = 0;
5      u32 chan;
6      int ret;
7
8      if (priv->hw->pcs != STMMAC_PCS_RGMII &&
9          priv->hw->pcs != STMMAC_PCS_TBI &&
10         priv->hw->pcs != STMMAC_PCS_RTBI) {
11         ret = stmmac_init_phy(dev);
12         if (ret) {
13             netdev_err(priv->dev,
14                 "%s: Cannot attach to PHY (error: %d)\n",
15                 __func__, ret);
16             return ret;
17         }
18     }
......
36     ret = alloc_dma_desc_resources(priv);
37     if (ret < 0) {
38         netdev_err(priv->dev, "%s: DMA descriptors allocation 
failed\n",
39             __func__);
40         goto dma_desc_error;
41     }
42
43     ret = init_dma_desc_rings(dev, GFP_KERNEL);
44     if (ret < 0) {
45         netdev_err(priv->dev, "%s: DMA descriptors initialization 
failed\n",
46             __func__);
47         goto init_error;
48     }
49
50     ret = stmmac_hw_setup(dev, true);
51     if (ret < 0) {
52         netdev_err(priv->dev, "%s: Hw setup failed\n", __func__);
53         goto init_error;
54     }
55
56     stmmac_init_coalesce(priv);
57
58     phylink_start(priv->phylink);
59
60     /* Request the IRQ lines */
61     ret = request_irq(dev->irq, stmmac_interrupt,
62             IRQF_SHARED, dev->name, dev);
63     if (unlikely(ret < 0)) {
64         netdev_err(priv->dev,
65             "%s: ERROR: allocating the IRQ %d (error: %d)\n",
66             __func__, dev->irq, ret);
67         goto irq_error;
68     }
69
70     /* Request the Wake IRQ in case of another line is used for WoL */
71     if (priv->wol_irq != dev->irq) {
72         ret = request_irq(priv->wol_irq, stmmac_interrupt,
73             IRQF_SHARED, dev->name, dev);
74         if (unlikely(ret < 0)) {
75             netdev_err(priv->dev,
76                 "%s: ERROR: allocating the WoL IRQ %d (%d)\n",
77                 __func__, priv->wol_irq, ret);
78             goto wolirq_error;
79         }
80     }
81
82     /* Request the IRQ lines */
83     if (priv->lpi_irq > 0) {
84         ret = request_irq(priv->lpi_irq, stmmac_interrupt,
IRQF_SHARED,
85                 dev->name, dev);
86         if (unlikely(ret < 0)) {
87             netdev_err(priv->dev,
88                 "%s: ERROR: allocating the LPI IRQ %d (%d)\n",
89                 __func__, priv->lpi_irq, ret);
90             goto lpiirq_error;
91         }
92     }
93
94     stmmac_enable_all_queues(priv);
95     stmmac_start_all_queues(priv);
96
97     return 0;
98 }

第11行,看名字就知道了,初始化PHY设备。

第36行,分配DMA资源,分为TX和RX两个DMA资源。

第43行,初始化TX和RX DMA描述符,并且分配socket缓冲区。

第50行,设置mac为可用状态,配置mac核心寄存器,然后DMA数据准备接收和发送。

第58行,启动phylink。

第60-92行,申请中断,中断函数为stmmac_interrupt,重点,后面会解析。

第94-95行,使能队列和开启队列。

2、stmmac_release函数简析

关闭网卡的时候stmmac_release函数就会执行,函数内容如下:

stmmac_release函数

第10行,停止phylink。

第11行,断开phylink。

第13行,停止工作队列。

第15行,关闭工作队列。

第28行,停止TX/RX的DMA。

第31行,释放TX/RX DMA资源。

第34行,关闭MAC TX/RX。

第36行,关闭网络。

3、stmmac_xmit函数简析

STM32MP1的网络数据发送是通过stmmac_xmit函数来完成,这个函数将上层传递过来的sk_buff中的数据通过DMA发送出去,函数源码如下所示(限于篇幅原因,有省略):

示例代码 52.5.3.3 stmmac_xmit 函数
1  static netdev_tx_t stmmac_xmit(struct sk_buff *skb,
struct net_device *dev)
2  {
3      struct stmmac_priv *priv = netdev_priv(dev);
4      unsigned int nopaged_len = skb_headlen(skb);
5      int i, csum_insertion = 0, is_jumbo = 0;
6      u32 queue = skb_get_queue_mapping(skb);
7      int nfrags = skb_shinfo(skb)->nr_frags;
......
164     /* Ready to fill the first descriptor and set the OWN bit w/o 
165      * any problems because all the descriptors are actually ready 
166      * to be passed to the DMA engine.
167      */
168     if (likely(!is_jumbo)) {
169         bool last_segment = (nfrags == 0);
170
171     des = dma_map_single(priv->device, skb->data,
172                 nopaged_len, DMA_TO_DEVICE);
173     if (dma_mapping_error(priv->device, des))
174         goto dma_map_err;
175
176     tx_q->tx_skbuff_dma[first_entry].buf = des;
177
......
190     /* Prepare the first descriptor setting the OWN bit too */
191     stmmac_prepare_tx_desc(priv, first, 1, nopaged_len,
192         csum_insertion, priv->mode, 1, last_segment,
193         skb->len);
194     } else {
195         stmmac_set_tx_owner(priv, first);
196     }
197
198     /* The own bit must be the latest setting done when prepare the
199      * descriptor and then barrier is needed to make sure that
200      * all is coherent before granting the DMA engine.
201      */
202     wmb();
203
204     netdev_tx_sent_queue(netdev_get_tx_queue(dev, queue), skb->len);
205
206     stmmac_enable_dma_transmission(priv, priv->ioaddr);
207
208     tx_q->tx_tail_addr = tx_q->dma_tx_phy + (tx_q->cur_tx *
sizeof(*desc));
209     stmmac_set_tx_tail_ptr(priv, priv->ioaddr, tx_q->tx_tail_addr,
queue);
210     stmmac_tx_timer_arm(priv, queue);
211
212     return NETDEV_TX_OK;
......
219 }

第171行,调用dma_map_single函数,进行DMA映射,返回值就是DMA映射后的虚拟地址。

第176行,保存DMA的虚拟地址,在stmmac_interrupt中断函数里使用。

第191行,调用stmmac_prepare_tx_desc函数,填充描述符其它信息,让DMA拥有该描述符。

第209行,设置尾指针,启动DMA发送。

4、stmmac_interrupt中断函数简析

当DMA发送完数据或者接收数据都会产生中断,调用stmmac_interrupt函数就行处理,stmmac_interrupt函数通过调用stmmac_dma_interrupt函数处理DMA相关中断,函数的源码如下所示(有省略):

stmmac_dma_interrupt函数

第15行,调用stmmac_napi_check函数设置NAPI软中断标志位,当触发NAPI机制软中断,这个时候napi的poll函数就会执行,如果是接收数据的就会触发stmmac_napi_poll_rx函数,是发送数据的就会触发stmmac_napi_poll_tx函数。

5、stmmac_napi_poll_rx函数简析

stmmac_napi_poll_rx函数内容如下所示:

stmmac_napi_poll_rx函数

第11行,stmmac_rx函数是用来接收数据的。

第12行,napi_complete_done函数结束NAPI轮询。

第13行,重新开启dma的中断。

6、stmmac_napi_poll_tx函数简析

stmmac_napi_poll_tx函数内容如下所示:

stmmac_napi_poll_tx函数

第12行,调用stmmac_tx_clean进行数据传输,传输完成以后会进行资源回收。

第19-24行,重启发送数据。

Linux内核PHY子系统与MDIO总线简析

上一小节在讲解MDIO总线的时候讲过,注册MDIO总线的时候也会向内核注册PHY设备,本节就来简单了解一下PHY子系统。PHY子系统就是用于PHY设备相关内容的,分为PHY设备和PHY驱动,和platform总线一样,PHY子系统也是一个设备、总线和驱动模型

PHY设备

首先看一下PHY设备,Linux内核使用phy_device结构体来表示PHY设备,结构体定义在include/linux/phy.h,结构体内容如下(为了缩小篇幅,有省略):

phy_device结构体

一个PHY设备对应一个phy_device实例,然后需要向Linux内核注册这个实例。使用phy_device_register函数完成PHY设备的注册,函数原型如下:

int phy_device_register(struct phy_device *phy)

函数参数和返回值含义如下:

  • phy:需要注册的PHY设备。
  • 返回值:0,成功;负值,失败。

PHY设备的注册过程一般是先调用get_phy_device函数获取PHY设备,此函数内容如下:

get_phy_device函数

第827行,调用get_phy_id函数获取PHY ID,也就是读取PHY芯片的那两个ID寄存器,得到PHY芯片ID信息。

第835行,调用phy_device_create函数创建phy_device,此函数先申请phy_device内存,然后初始化phy_device的各个结构体成员,最终返回创建好的phy_device。phy_device_register函数注册的就是这个创建好的phy_device。

PHY驱动

PHY驱动使用结构体phy_driver表示,结构体也定义在include/linux/phy.h 文件中,结构体内容如下(为了缩小篇幅,省略了注释部分):

示例代码 52.5.4.3 phy_driver 结构体
1  struct phy_driver {
2      struct mdio_driver_common mdiodrv;
3      u32 phy_id; /* PHY ID */
4      char *name;
5      u32 phy_id_mask; /* PHY ID 掩码 */
6      const unsigned long * const features;
7      u32 flags;
8      const void *driver_data;
9
10     int (*soft_reset)(struct phy_device *phydev);
11     int (*config_init)(struct phy_device *phydev);
12     int (*probe)(struct phy_device *phydev);
13     int (*get_features)(struct phy_device *phydev);
14     int (*suspend)(struct phy_device *phydev);
15     int (*resume)(struct phy_device *phydev);
16     int (*config_aneg)(struct phy_device *phydev);
17     int (*aneg_done)(struct phy_device *phydev);
18     int (*read_status)(struct phy_device *phydev);
19     int (*ack_interrupt)(struct phy_device *phydev);
20     int (*config_intr)(struct phy_device *phydev);
21     int (*did_interrupt)(struct phy_device *phydev);
22     int (*handle_interrupt)(struct phy_device *phydev);
23     void (*remove)(struct phy_device *phydev);
24     int (*match_phy_device)(struct phy_device *phydev);
25     int (*ts_info)(struct phy_device *phydev,
struct ethtool_ts_info *ti);
26     int (*hwtstamp)(struct phy_device *phydev, struct ifreq *ifr);
27     bool (*rxtstamp)(struct phy_device *dev, struct sk_buff *skb,
int type);
28     void (*txtstamp)(struct phy_device *dev, struct sk_buff *skb,
int type);
29     int (*set_wol)(struct phy_device *dev,
struct ethtool_wolinfo *wol);
30     void (*get_wol)(struct phy_device *dev,
struct ethtool_wolinfo *wol);
31     void (*link_change_notify)(struct phy_device *dev);
32     int (*read_mmd)(struct phy_device *dev, int devnum, u16 regnum);
33     int (*write_mmd)(struct phy_device *dev, int devnum,
u16 regnum,u16 val);
34     int (*read_page)(struct phy_device *dev);
35     int (*write_page)(struct phy_device *dev, int page);
36     int (*module_info)(struct phy_device *dev,
37     struct ethtool_modinfo *modinfo);
38     int (*module_eeprom)(struct phy_device *dev,
39     struct ethtool_eeprom *ee, u8 *data);
40     int (*get_sset_count)(struct phy_device *dev);
41     void (*get_strings)(struct phy_device *dev, u8 *data);
42     void (*get_stats)(struct phy_device *dev,
43     struct ethtool_stats *stats, u64 *data);
44     int (*get_tunable)(struct phy_device *dev,
45     struct ethtool_tunable *tuna, void *data);
46 int (*set_tunable)(struct phy_device *dev,
47     struct ethtool_tunable *tuna,
48         const void *data);
49     int (*set_loopback)(struct phy_device *dev, bool enable);
50 };

可以看出,phy_driver重点是大量的函数,编写PHY驱动的主要工作就是实现这些函数,但是不一定全部实现,稍后会简单分析一下Linux内核通用PHY驱动。

1、注册PHY驱动

phy_driver结构体初始化完成以后,就需要向Linux内核注册,PHY驱动的注册使用phy_driver_register函数,注册phy驱动时候会设置驱动的总线为mdio_bus_type,也就是MDIO总线,关于MDIO总线稍后会讲解,函数原型如下:

int phy_driver_register(struct phy_driver *new_driver, struct module *owner);

函数参数和返回值含义如下:

  • new_driver:需要注册的PHY驱动。
  • owner:驱动模块所属的PHY设备。
  • 返回值:0,成功;负值,失败。

2、连续注册多个PHY驱动

一个厂家会生产多种PHY芯片,这些PHY芯片内部差别一般不大,如果一个个的去注册驱动将会导致一堆重复的驱动文件,因此Linux内核提供了一个连续注册多个PHY驱动的函数phy_drivers_register。首先准备一个phy_driver数组,一个数组元素就表示一个PHY芯片的驱动,然后调用phy_drivers_register一次性注册整个数组中的所有驱动,函数原型如下:

int phy_drivers_register(struct phy_driver *new_driver, 
						 int n,
						 struct module *owner);

函数参数和返回值含义如下:

  • new_driver:需要注册的多个PHY驱动数组。
  • n:要注册的驱动数量。
  • owner:驱动模块所属的PHY设备。
  • 返回值:0,成功;负值,失败。

3、卸载PHY驱动

卸载PHY驱动的话使用phy_driver_unregister函数,函数原型如下:

void phy_driver_unregister(struct phy_driver *drv)

函数参数和返回值含义如下:

  • drv:需要卸载的PHY驱动。
  • 返回值:无。

MDIO总线

前面说了,PHY子系统也是遵循设备、总线、驱动模型的,设备和驱动就是phy_device和phy_driver。总线就是MDIO总线,因为PHY芯片是通过MDIO接口来管理的,MDIO总线最主要的工作就是匹配PHY设备和PHY驱动。在文件drivers/net/phy/mdio_bus.c中有如下定义:

mdio总线

示例代码52.5.4.4定义了一个名为“mdio_bus_type”的总线,这个就是MDIO总线,总线的名字为“mdio_bus”,重点是总线的匹配函数为mdio_bus_match。此函数内容如下:

mdio_bus_match函数

第5行,采用设备树的话先尝试使用of_driver_match_device来对设备和驱动进行匹配,也就是检查compatible属性值与匹配表of_match_table里面的内容是否一致。但是对于本章教程而言,并不是通过of_driver_match_device来完成PHY驱动和设备匹配的。

第8-9行,使用PHY驱动的匹配方法,会调用phy_bus_match函数,函数源码如下所示:

phy_bus_match函数

第11、12行,检查PHY驱动有没有提供匹配函数match_phy_device,如果有的话就直接调用PHY驱动提供的匹配函数完成与设备的匹配。

第26、27行,phy_driver里面有两个成员变量phy_id和phy_id_mask,表示此驱动所匹配的PHY芯片ID以及ID掩码,PHY驱动编写人员需要给这两个成员变量赋值phy_device也有一个phy_id成员变量,表示此PHY芯片的ID,phy_device里面的phy_id是在注册PHY设备的时候调用get_phy_id函数直接读取PHY芯片内部ID寄存器得到的很明显PHY驱动和PHY设备中的ID要一样,这样才能匹配起来。所以最后一种方法就是对比PHY驱动和PHY设备中的phy_id是否一致,这里需要与PHY驱动里面的phy_id_mask进行与运算,如果结果一致的话就说明驱动和设备匹配。

如果PHY设备和PHY驱动匹配,那么就使用指定的PHY驱动,如果不匹配的话就使用Linux内核自带的通用PHY驱动

通用PHY驱动

前面多次提到Linux内核已经集成了通用PHY驱动,通用PHY驱动名字为“Generic PHY”,打开drivers/net/phy/phy_device.c,找到phy_init函数,内容如下:

phy_init函数

phy_init是整个PHY子系统的入口函数,第11行和15行都会调用phy_drivers_register函数向内核直接注册一个通用PHY驱动:genphy_c45_driver和genphy_driver;genphy_c45_driver为10G网络,genphy_driver为10/100/1000M网络。它们都是通用PHY驱动,也就是说Linux系统启动以后默认就已经存在了通用PHY驱动。

genphy_c45_driver和genphy_driver这两个结构体 , 它们定义分别在drivers/net/phy/phy_device.c和drivers/net/phy/phy-c46.c这两个文件中,内容如下所示:

通用PHY设备对象

genphy_c45_driver为10G的PHY驱动,名字为“Generic Clause 45 PHY”,genphy_driver为10/100/1000M的PHY驱动,名字为“Generic PHY”。注意,很多专用PHY芯片的驱动程序中也会用到通用PHY驱动的一些函数。

RTL8211F PHY驱动

这里因为我是最新的开发板,是V1.3版本之后的,所以这一小节就直接跳过了。

YT8511 PHY驱动

如果是使用V1.3版本以后的核心板,网络的PHY芯片为YT8511芯片,此芯片为国产的网络芯片,在Linux内核没有对应的PHY驱动,但是厂家会提供linux下的PHY驱动不过这个原厂驱动有点小问题,那就是没有使能YT8511的125M时钟输出,导致驱动工作不正常。正点原子已经修改好了这个小问题

在之前的移植Linux驱动的时候已经详细讲解过了如何在Linux内核里面添加YT8511驱动,但是为了教程的连贯性,这里再讲解一遍!

将驱动文件添加到Linux内核

首先需要将28_YT8511C_PHY目录里面的linux驱动文件拷贝到Linux内核源码相应的目录中。这里一共有motorcomm.c和motorcomm_phy.h这两个文件,一个是驱动C文件,一个是头文件。把motorcomm.c拷贝到drivers/net/phy下,把motorcomm_phy.h拷贝到include/linux目录下

这样就把YT8511的驱动文件和头文件添加到了Linux内核源码中。

修改Makefile

前面已经将YT8511的驱动文件添加到Linux内核源码里面了,但是还不能编译,因为还没有添加到Makefile里面。在phy驱动的源码目录drivers/net/phy,打开Makefile文件,添
加如下代码:

obj-$(CONFIG_MOTORCOMM_PHY) += motorcomm.o

添加结果如下所示:

在Makefile添加YT8511驱动文件

修改Kconfig

最后修改一下drivers/net/phy/Kconfig这个文件,这样以后就可以通过图形化界面来使能或者禁止YT8511驱动了。在Kconfig里面添加如下内容:

config MOTORCOMM_PHY
tristate "Motorcomm PHYs"
---help---
Supports the YT8010, YT8510, YT8511, YT8512 PHYs.

添加结果如下所示:

在Kconfig添加YT8511C

使能YT8511C PHY驱动

最后就要进入内核的图形界面去使能YT8511驱动,配置路径如下:

-> Device Drivers
-> Network device support (NETDEVICES [=y])
-> PHY Device support and infrastructure (PHYLIB [=y])
-> <*> Motorcomm PHYs //选中

配置界面如图所示:

使能YT8511驱动

使能以后就可以重新编译Linux内核,然后使用新的Linux内核启动开发板。

YT8511C驱动源码简析

打开motorcomm.c文件,找到YT8511驱动结构体,代码如下所示:

YT8511 PHY驱动结构体

第21行,PHY_ID_YT8511是一个宏,定义为0x0000010a,是PHY ID。

第22行,驱动名字为"YT8511 Gigabit Ethernet",系统启动过程中,加载网络设备驱动的时候就会提示电气PHY驱动文字为"YT8511 Gigabit Ethernet"。

第23行,PHY 的ID掩码,MOTORCOMM_PHY_ID_MASK是一个宏,定义为0x00000fff,也就是前12位有效,在进行匹配的时候只需要比较前12位。

最后,第72行使用module_phy_driver(本质是一个宏)来完成ytphy_drvs的注册,以genphy开头的函数都是通用PHY驱动。

网络驱动实验测试

RTL8211F PHY驱动测试

这里因为没有这个PHY芯片,所以直接跳过。

YT8511C PHY驱动测试

之前已经把YT8511C PHY驱动加进入内核里,运行以下命令重新编译内核:

make uImage LOADADDR=0XC2000040 -j8

使用新的uImage内核,重新启动开发板,当系统驱动以后就打印出当前PHY驱动名字为“YT8511 Gigabit Ethernet”,如下图所示:

YT8511C PHY驱动信息

从上图可以知道,此时PHY网络使用的是“YT8511 Gigabit Ethernet”。由于教程中开发板连接到了千M路由器上,因此此时的网速为1Gpbs,也就是1000M网络。测试网络就很简单了,一直都是用网络在挂载文件系统的,一直在测试中

千兆以太网网速测试

注意:这里要连接千兆路由器或者千兆交换机,没有这两个设备可以开发板和电脑直接连接。

STM32MP1网络接口为千兆网络,理论最大通信速率可达1000mb/s,RTL8211和YT8511也是千M PHY芯片。本小节就来测试下这个网口的速率,此时使用开发板和PC机直连方式,使用千兆网线,比如CAT-5E类网线或CAT-6类网线

Linux下网络速率测试可以使用iperf3工具,iperf3是一个网络性能测试工具,可以测试TCP和UDP的带宽质量、测量TCP最大带宽、报告带宽、延迟抖动和数据丢包等;这里仅仅只是使用iperf3工具测试开发板上网口的带宽,其它的一些参数特性就不进行详细测试了。

iperf3的测试方法需要有一台主机作为服务器端,另一台主机作为客户端,客户端向服务器端发送数据,这里可以将Ubuntu系统作为服务器端,而开发板作为客户端进行测试

使能iperf3命令

跳转到buildroot源码目录下,启动到图形配置选项,按照以下配置去是能iperf3命令:

-> Target packages
-> Networking applications
-> [*] iperf3 //选中

配置如下图所示:

使能iperf3命令

保存配置项,重新编译新的文件系统,使用新的文件系统启动开发板。

Ubuntu下安装iperf3工具

前面已经将iperf3工具移植到开发板了,接下来需要在Ubuntu下也安装iperf3 工具,因为Ubuntu系统默认没有安装这个工具,在Ubuntu系统中执行下面这条命令安装iperf3:

sudo apt-get install iperf3

安装完成之后就可以进行测试了!

测试

在Ubuntu系统中执行下面这条命令,将Ubuntu系统作为iperf3的服务器,如下所示:

iperf3 -s

命令执行如下图所示:

将Ubuntu作为服务器

-s 表示将其作为服务器端

接下来需要在开发板终端执行下面这条命令,将开发板作为客户端,并进行测试:

iperf3 -c 192.168.1.250

-c 选项表示将其作为客户端,后面紧跟着的就是服务器主机的IP地址,在这里也就是Ubuntu系统的IP地址,所以这个IP地址根据自己Ubuntu系统的IP进行填写即可,测试结果如下图所示:

开发板作为客户端测试结果

上图中执行命令共打印出了10次带宽测试报告,每一秒钟报告一次带宽,耗费时间为10秒钟。最终测试出来发送和接收带宽分别为935M和934M,基本接近1000M的带宽,说明千M网络工作正常。

总结

这一章节的内容,基本就是底层的PHY芯片+STM32MP1内置的MAC芯片+RJ45座构成的完整嵌入式网络接口硬件。然后学习了PHY芯片的一些内容,在Linux内核中的网络通讯相关知识

其实这一部分知识可以去看我之前的lwIP协议的学习,那边就是完整的一个在STM32单片机上的网络硬件+协议架构,会更加的详细和完整,当然了通讯协议的实现一个是lwIP,一个是在Linux内核

最后完成了一下网络接口的测试,其实这一部分没啥好多说的,在当时移植Linux内核的时候就已经完成了网络的通讯移植,之后所有的linux驱动的测试都是挂载在nfs和tfts之上给开发板加载的,所以肯定是可以通讯的,主要是通过iperf3进行一下网速的测试。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值