fuzidage
专注嵌入式、linux驱动 、arm裸机研究

导航

 

1 网络硬件接口介绍

image

1.1 MAC和PHY介绍

嵌入式网络硬件分为两部分:MAC 和 PHY,大家都是通过看数据手册来判断一款 SOC 是否支持网络,如果一款芯片数据手册说自己支持网络,一般都是说的这款 SOC 内置 MAC,MAC 类似 I2C 控制器、SPI 控制器一样的外设。

光有 MAC 还不能直接驱动网络,还需要另外一个芯片:PHY,因此对于内置 MAC 的 SOC,其外部必须搭配一个 PHY 芯片。
但是有些 SOC 内部没有 MAC,那也就没法搭配 PHY 芯片了,需要外挂MAC+PHY。

其实不只是网口,对于很多IP都涉及到PHY+MAC的结构,如前面说的

mipi-csi

mipi-csi-fuzidage-hexo

1.1.1 外挂MAC+PHY

image

1.1.1.1 DM9000

有些SOC不支持网口,但是并不是说该芯片就不能上网,可以外挂MAC+PHY。 很多网络芯片IP都是MAC+PHY 一体的。比如三星 linux 开发板里面用的最多的 DM9000,因为 三星的芯片基本没有内部 MAC(比如 S3C2440、S5PV210,4412 等),所以三星的开发板都是通 过外置的 DM9000 来完成有线网络功能的,DM9000 对 SOC 提供了一个 SRAM 接口,SOC 会 以 SRAM 的方式操作 DM9000。

1.1.1.2 W5500

有些外置的网络芯片更强大,内部甚至集成了硬件 TCP/IP 协议栈,对外提供一个 SPI 接 口,比如 W5500。这个一般用于单片机领域,单片机通过 SPI 接口与 W5500 进行通信,由于 W5500 内置了硬件 TCP/IP 协议栈,因此单片机就不需要移植软件协议栈,直接通过 SPI 来操 作 W5500,简化了单片机联网方案。

1.1.1.3 外挂MAC+PHY的优缺点

优点:让不支持网络的SOC也能使用网络,硬件裁剪性高。
缺点:

  1. 速率不够(一般芯片内置的 MAC 会有网络加速引擎,比如网络专用 DMA,网络处理效 率会很高),基本就是 10/100M
  2. 由于不是内置自研IP,是授权购买的,因此价格贵

1.1.2 SOC 内置网络 MAC

我们一般说某个 SOC 支持网络,说的就是他内部集成网络 MAC 外设,此时我们还需要外 接一个网络 PHY 芯片。

那为什么不把PHY 芯片也集成进 SOC 呢?笔者目前还没见过将 PHY 也集成到芯片里面的 SOC。

image

1.1.2.1 内置网络 MAC 优点

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

1.2 MAC与PHY接线

1.2.1 MII 接口

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

image

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

1.2.2 RMII 接口

RMII 全称是 Reduced Media Independent Interface,翻译过来就是Reduced 精简的介质独立接口,也 就是 MII 接口的精简版本:

image

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

RMII一共只需7根线。

1.2.3 MDIO 接口

MDIO 全称是 Management Data Input/Output,是一 个简单的两线串行接口(类似I2C),一根 MDIO 数据线,一根 MDC 时钟线,用来操作外置PHY的寄存器。

MDIO 接口支持多达 32 个 PHY。同一时刻 内只能对一个 PHY 进行操作,那么如何区分这 32 个 PHY 芯片呢?和 IIC 一样,使用器件地址 即可。同一 MDIO 接口下的所有 PHY 芯片,其器件地址不能冲突,必须保证唯一。

1.2.4 RJ45 网线座子

image

PHY和RJ45座子还要接一个网络变压器,网络变压器用于隔离及滤波:

image

image

RJ45 座子上一般有两个灯,一个黄色(橙色),一个绿色,绿色亮的话表示网络连接正常。黄色闪烁的话说明当前正在进行网络通信。

2 MAC和PHY软件协议

2.1 I.MX6ULL MAC介绍

IMX6ULL内部自带的ENET外设其实就是一个网络 MAC,支持 10/100M。实现了三层网络加速,用于加速那些通用的网络协议,比如 IP、TCP、UDP 和 ICMP 等,为客户端应用程序提供加速服务。

IMX6ULL 内部 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 设备。

2.2 PHY 芯片

IEEE定义了015这16个寄存器的功能,1631这16个寄存器由厂商自行实现。也就是说不管你用的哪个厂家的 PHY 芯片,其中0~15这 16个寄存器是一模一样的。仅靠这16个寄存器是完全可以驱动起PHY芯片的,至少能保证基本的网络数据通信。

2.2.1 IEEE 规定的前16 个寄存器

image

我们无论使用哪个厂家的 PHY 芯片,前16个寄存器地址都是统一的内容。

2.2.2 LAN8720A PHY介绍

LAN8720A 是低功耗的 10/100M 单以太网 PHY 层芯片,可应用于机顶盒、网络打印机、 嵌入式通信设备、IP 电话等领域。支持RMII 接口与以太网 MAC 层通信,内置 10-BASE-T/100BASE-TX 全双工传输模块,支持 10Mbps 和 100Mbps。

特点:

· 高性能的 10/100M 以太网传输模块
· 支持 RMII 接口以减少引脚数
· 支持全双工和半双工模式
· 两个状态 LED 输出
· 可以使用 25M 晶振以降低成本
· 支持自协商模式
· 支持 HP Auto-MDIX 自动翻转功能
· 支持 SMI 串行管理接口
· 支持 MAC 接口

image

image

2.2.2.1 引脚介绍

2.2.2.1.1 interrupt generator

image

当一个中断事件发生并且中断位使能,LAN8720A 就会在 nINT(14 脚)产生一个低电平有效的中断信号。
LAN8720A 的中断系统提供两种中断模式:主中断模式和复用中断模式。主中断模式是默认中断模式,LAN8720A 上电或复位后就工作在主中断模式,当模式控制/状态寄存器(十进制地址为17)的 ALTINT 位为 0 时 LAN8720A 工作在主模式,当 ALTINT 位为 1 时工作在复用中断模式。

2.2.2.1.2 PHY 地址设置

MAC 对 PHY 进行寄存器读写操作要通过MDIO接口,MDIO 最多可以控制 32 个 PHY 芯 片,通过不同的 PHY 芯片地址来对不同的 PHY 操作。

LAN8720A 通过设置 RXER/PHYAD0 引脚来设置其 PHY 地址,默认情况下为 0:

image

image

2.2.2.1.3 nINT/REFCLKO 配置

nINTSEL 引脚(2 号引脚)用于设置 nINT/REFCLKO 引脚(14 号引脚)的功能:

image

nINTSEL 是来选择REFCLKO 到底是用来当做时钟还是中断引脚。

2.2.2.1.3.1 REF_CLK In模式

我使用的正点原子的 ALPHA 开发板的两个 LAN8720A ,全都工作在默认的 REF_CLK In模式,当 LAN8720A 工作在 REF_CLK In 模式时,50MHz 的外部时钟信号应接到 LAN8720 的 XTAL1/CKIN 引脚(5 号引脚)上:

image

2.2.2.1.3.2 REF_CLK Out 模式

image

2.2.2.2 LAN8720A 内部寄存器

2.2.2.2.1 BCR(Basic Control Rgsister)寄存器

LAN8720A 的前 16 个寄存器满足 IEEE 的要求,BCR(Basic Control Rgsister)寄存器,地址为 0。BCR各位如下:

image

一般配置 PHY 芯片,重点就是配置 BCR 寄存器就够了。

2.2.2.2.2 BSR(Basic Status Register)寄存器

BCR(Basic Status Rgsister)寄存器,地址为 1。BSR各位如下:

image

通过状态寄存器能够看到当前的连接速度、双工状态和连接状态等。

2.2.2.2.3 PHY ID寄存器

根据IEEE 规定地址2和地址3是phy identifier。IEEE 规定了一叫做 OUI 的 ID 组成方式,全称是 Organizationally Unique Identifier,OUI 一共 32 位,分为三部分:22 位的 ID+6 位厂商型号 ID+4 位厂商版本 ID。

image

LAN8720A 的 ID 寄存器地址2如下:

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

LAN8720A 的 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
2.2.2.2.4 特殊控制/状态寄存器
描述 类型
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

2.2.3 SR8201F PHY介绍

image

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

2.2.3.1 PHY 地址设置

MAC通过 MDIO总线对 PHY 进行读写操作。LED0/PHYAD[0]和 LED1/PHYAD[1]这两个引脚来设置其 PHY 地址:

image

2.2.3.2 SR8021F内部寄存器

2.2.3.2.1 BCR(Basic Control Rgsister)寄存器

根据IEEE规定地址0

image

2.2.3.2.2 BSR(Basic Status Register)寄存器

根据IEEE规定地址1

image

2.2.3.2.3 PHY ID 寄存器

SR8201F 的 ID 寄存器地址2

描述 类型 默认值
15:0 分配到 OUI 的第 6 到第 21 位组织唯一识别符 RO 001Ch

SR8201F 的 ID 寄存器地址3

描述 类型 默认值
15:10 分配到 OUI 的第 0 到 5 位 RO 110010
9:4 型号 RO 000001
3:0 版本号 RO 0110

3 Linux网络驱动介绍

3.1 linux下网络驱动框架图

image

3.2 数据结构和API

3.2.1 net_device

网络驱动的核心就是初始化 net_device,注册到linux内核。

struct net_device {
	char name[IFNAMSIZ]; //网络设备的名字
	struct hlist_node name_hlist;
	char *ifalias;
	/*
	* I/O specific fields
	* FIXME: Merge these and struct ifmap into one
	*/
	unsigned long mem_end;//共享内存结束地址
	unsigned long mem_start;//共享内存起始地址
	unsigned long base_addr;//是网络设备 I/O 地址
	int irq;//网络设备的中断号
	atomic_t carrier_changes;
	/*
	* Some hardware also needs these fields (state,dev_list,
	* napi_list,unreg_list,close_list) but they are not
	* part of the usual set specified in Space.c.
	*/
	unsigned long state;
	struct list_head dev_list;//全局网络设备列表
	struct list_head napi_list;//napi网络设备的列表入口
	struct list_head unreg_list;//注销(unregister)的网络设备列表入口
	struct list_head close_list;//关闭的网络设备列表入口
	...
	const struct net_device_ops *netdev_ops;//网络设备的操作集函数
	const struct ethtool_ops *ethtool_ops;//网络管理工具相关函数集,用户空间网络管理工具会调用此结构体中的相关函数获取网卡状态或者配置网卡
	#ifdef CONFIG_NET_SWITCHDEV
	const struct swdev_ops *swdev_ops;
	#endif
	const struct header_ops *header_ops;//头部的相关操作函数集,比如创建、解析、缓冲等
	unsigned int flags;//详见 enum net_device_flags
	...
	unsigned char if_port;//接口的端口类型,如:IF_PORT_UNKNOWN
	unsigned char dma;//网络设备所使用的 DMA 通道
	unsigned int mtu;//是网络最大传输单元,为 1500
	unsigned short type;//指定ARP模块的类型,以太网的ARP接口为ARPHRD_ETHER,Linux内核所支持的ARP协议定义在 include/uapi/linux/if_arp.h 中
	unsigned short hard_header_len;
	unsigned short needed_headroom;
	unsigned short needed_tailroom;
	/* Interface address info. */
	unsigned char perm_addr[MAX_ADDR_LEN];//永久的硬件地址
	unsigned char addr_assign_type;
	unsigned char addr_len;//硬件地址长度
	...
	/*
	* Cache lines mostly used on receive path (including
	type_trans())
	*/
	unsigned long last_rx;//最后接收的数据包时间戳,单位为 jiffies
	/* Interface address info used in eth_type_trans() */
	unsigned char *dev_addr;//也是硬件地址,是当前分配的 MAC 地址,可以通过软件修改
	#ifdef CONFIG_SYSFS
	struct netdev_rx_queue *_rx;//接收队列
	unsigned int num_rx_queues;//接收队列数量,在调用 register_netdev 注册网络设备的时候会分配指定数量的接收队列。
	unsigned int real_num_rx_queues;//当前活动的队列数量
	#endif
	...
	/*
	* Cache lines mostly used on transmit path
	*/
	struct netdev_queue *_tx ____cacheline_aligned_in_smp;//发送队列
	unsigned int num_tx_queues;//发送队列数量,通过 alloc_netdev_mq 函数分配指定数量的发送队列
	unsigned int real_num_tx_queues;//当前有效的发送队列数量
	struct Qdisc *qdisc;
	unsigned long tx_queue_len;
	spinlock_t tx_global_lock;
	int watchdog_timeo;
	...
	/* These may be needed for future network-power-down code. */
	/*
	* trans_start here is expensive for high speed devices on SMP,
	* please use netdev_queue->trans_start instead.
	*/
	unsigned long trans_start;//最后的数据包发送的时间戳,单位为 jiffies
	...
	struct phy_device *phydev;//对应的PHY设备
	struct lock_class_key *qdisc_tx_busylock;
};  //include/linux/netdevice.h

3.2.2 申请/删除net_device

alloc_netdev:

使用 alloc_netdev 函数来申请 net_device。

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

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

image

sizeof_priv:私有数据块大小。
name:设备名字。
setup:回调函数,初始化设备的设备后调用此函数。
txqs:分配的发送队列数量。
rxqs:分配的接收队列数量。

net_device包含很多种网络设备,如以太网设备(Ethernet)、红外数据接口(InDA)、高 性能并行接口(HPPI)、CAN 网络等。内核针对不同的网络设备在 alloc_netdev 的基础上提供了 一层封装.

alloc_etherdev:

对于以太网Ethernet:alloc_etherdev

image

struct net_device *alloc_etherdev_mqs(int sizeof_priv, unsigned int txqs, unsigned int rxqs) {
    return alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_UNKNOWN, ether_setup, txqs, rxqs);
}

可以看到大家进系统后输入ifconfig,eth0,eth1就是从这里来的。

这里设置了以太网的 setup 函数为 ether_setup,不同的网络设备其 setup 函数不同,比如 CAN 网络里面 setup 函数就是 can_setup

image

ether_setup 函数会对 net_device 做初步的初始化。

删除 net_device:

void free_netdev(struct net_device *dev);

3.2.3 注册/注销net_device

int register_netdev(struct net_device *dev);//注册net_device
void unregister_netdev(struct net_device *dev);//注销net_device

3.2.4 net_device_ops

net_device 有个非常重要的成员变量:netdev_ops,为 net_device_ops 结构体指针, 就是网络设备操作集。

struct net_device_ops {
	int (*ndo_init)(struct net_device *dev);
	void (*ndo_uninit)(struct net_device *dev);
	int (*ndo_open)(struct net_device *dev);
	int (*ndo_stop)(struct net_device *dev);
	netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb, struct net_device *dev);
	u16 (*ndo_select_queue)(struct net_device *dev,
                        struct sk_buff *skb,
                        void *accel_priv,
                        select_queue_fallback_t fallback);
	void (*ndo_change_rx_flags)(struct net_device *dev, int flags);
	void (*ndo_set_rx_mode)(struct net_device *dev);
	int (*ndo_set_mac_address)(struct net_device *dev, void *addr);
	int (*ndo_validate_addr)(struct net_device *dev);
	int (*ndo_do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
	int (*ndo_set_config)(struct net_device *dev, struct ifmap *map);
	int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
	int (*ndo_neigh_setup)(struct net_device *dev, struct neigh_parms *);
	void (*ndo_tx_timeout) (struct net_device *dev);
	...
	#ifdef CONFIG_NET_POLL_CONTROLLER
	void (*ndo_poll_controller)(struct net_device *dev);
	int (*ndo_netpoll_setup)(struct net_device *dev, struct netpoll_info *info);
	void (*ndo_netpoll_cleanup)(struct net_device *dev);
	#endif
	...
	int (*ndo_set_features)(struct net_device *dev, netdev_features_t features);
	...
};

ndo_init: 当第一次注册网络设备的时候此函数会执行,设备可以在此函数中做一些需要推后初始化的内容,不过一般驱动中不使用此函数,虚拟网络设备可能会使用。
ndo_uninit : 卸载网络设备的时候此函数会执行。
ndo_open: 打开网络设备的时候此函数会执行,网络驱动程序需要实现此函数,非常重要!
以 NXP 的imx SOC 网络驱动为例,会在此函数中做如下工作:

  1. 使能网络外设时钟。
  2. 申请网络所使用的环形缓冲区。
  3. 初始化 MAC 外设。
  4. 绑定接口对应的 PHY。
  5. 如果使用 NAPI 的话要使能 NAPI 模块,通过 napi_enable 函数来使能。
  6. 开启 PHY。
  7. 调用 netif_tx_start_all_queues 来使能传输队列,也可能调用 netif_start_queue 函数。

ndo_stop:关闭网络设备的时候此函数会执行,网络驱动程序也需要实现此函数.

以 NXP 的imx SOC 网络驱动为例,会在此函数中做如下工作:

  1. 停止 PHY。
  2. 停止 NAPI 功能。
  3. 停止发送功能。
  4. 关闭 MAC。
  5. 断开 PHY 连接。
  6. 关闭网络时钟。
  7. 释放数据缓冲区。

ndo_start_xmit: 当需要发送数据的时候此函数就会执行,sk_buff 保存了上层传 递给网络驱动层的数据。也就是说,要发送出去的数据都存在了sk_buff中。发送成功的话此函数返回 NETDEV_TX_OK,如果发送失败了就返回 NETDEV_TX_BUSY,如果发送失败了我们就需要停止队列。

ndo_select_queue:多传输队列的时候选择使用哪个队列。

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

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

ndo_validate_addr:验证 MAC 地址是否合法,也就是验证 net_device 的 dev_addr 中的 MAC 地址是否合法,直接调用 is_valid_ether_addr 函数

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

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

ndo_poll_controller:使用查询方式来处理网卡数据的收发

ndo_set_features:修改 net_device 的 features 属性,设置相应的硬件属性。

3.2.5 sk_buff

sk_buff 是 Linux 网络重要的数据结构,用于管理 接收或发送数据包.

struct sk_buff {
	union {
		struct {
		/* These two members must be first. */
		struct sk_buff *next;
		struct sk_buff *prev;//分别指向下一个和前一个 sk_buff,构成一个双向链表。
			union {
				ktime_t tstamp; //数据包接收时或准备发送时的时间戳
				struct skb_mstamp skb_mstamp;
			};
		};
		struct rb_node rbnode; /* used in netem & tcp stack */
	};
	struct sock *sk; //当前 sk_buff 所属的 Socket
	struct net_device *dev; //当前 sk_buff 从哪个设备接收到或者发出的
	/*
	* This is the control buffer. It is free to use for every
	* layer. Please put your private variables there. If you
	* want to keep them across layers you have to do a skb_clone()
	* first. This is owned by whoever has the skb queued ATM.
	*/
	char cb[48] __aligned(8);
	unsigned long _skb_refdst;
	void (*destructor)(struct sk_buff *skb);//当释放缓冲区的时候可以在此函数里面完成某些动作
	unsigned int len, data_len;
	__u16 mac_len, hdr_len;
	__be16 protocol;//protocol 协议
	__u16 transport_header;//传输层头部
	__u16 network_header;//网络层头部
	__u16 mac_header;//链接层头部
	/* private: */
	__u32 headers_end[0];
	/* public: */
	/* These elements must be at the end, see alloc_skb() for details. */
	sk_buff_data_t tail;//指向实际数据的尾部
	sk_buff_data_t end;//指向缓冲区的尾部
	unsigned char *head, *data;//指向缓冲区的头部,data 指向实际数据的头部
	unsigned int truesize;
	atomic_t users;
}; //include/linux/skbuff.h

image

3.2.6.1 分配 sk_buf

static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority); //include/linux/skbuff.h
size:要分配的大小,也就是 skb 数据段大小。
priority:为 GFP MASK 宏,比如 GFP_KERNEL、GFP_ATOMIC等。

netdev_alloc_skb 来为某个设备申请一个用于接收的 skb_buf:

static inline struct sk_buff *netdev_alloc_skb(struct net_device *dev, unsigned int length);
dev:要给哪个设备分配 sk_buff。
length:要分配的大小。

3.2.6.2 释放 sk_buff

void kfree_skb(struct sk_buff *skb); //include/linux/skbuff.c

void dev_kfree_skb (struct sk_buff *skb);

3.2.6.3 变更 sk_buff

skb_put、skb_push、sbk_pull 和 skb_reserve

1.skb_put 函数

unsigned char *skb_put(struct sk_buff *skb, unsigned int len);

image

skb:要操作的 sk_buff。
len:要增加多少个字节。
返回值:扩展出来的那一段数据区首地址

2.skb_push函数

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

image

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

3.skb_pull

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

image

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

4.sbk_reserve

sbk_reserve 函数用于调整缓冲区的头部大小,方法很简单将 skb_buff 的 data 和 tail 同时后 移 n 个字节即可

static inline void skb_reserve(struct sk_buff *skb, int len);

skb:要操作的 sk_buff。
len:要增加的缓冲区头部大小。

3.2.6 dev_queue_xmit

将数据sk_buff发送出去,接收数据的话使用netif_rx 函数即可.

static inline int dev_queue_xmit(struct sk_buff *skb); //include/linux/netdevice.h

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

image

ndo_start_xmit 就是网络驱 动编写人员去实现的。

3.2.7 netif_rx

上层接收数据的话使用 netif_rx 函数,但是最原始的网络数据一般是通过轮询、中断或 NAPI 的方式来接收.

int netif_rx(struct sk_buff *skb); //net/core/dev.c

返回值:NET_RX_SUCCESS 成功,NET_RX_DROP 数据包丢弃

3.3 NAPI处理机制

NAPI 是一种高效的网络处理技术。 NAPI 的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL 的方法来轮询处理数据。这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。目前 NAPI 已经在 Linux 的网络驱动中得到了大量的应用,NXP 官方编写的网络驱动都是采用的 NAPI 机制。

3.3.1 napi_struct

image

3.3.2 初始化 NAPI

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。

此函数定义在 net/core/dev.c

3.3.3 删除 NAPI

void netif_napi_del(struct napi_struct *napi);

3.3.3 使能 NAPI

初始化完 NAPI 以后,必须使能才能使用:

inline void napi_enable(struct napi_struct *n);

3.3.4 关闭 NAPI

void napi_disable(struct napi_struct *n);

3.3.5 检查 NAPI 是否可以进行调度

inline bool napi_schedule_prep(struct napi_struct *n);

3.3.6 NAPI 调度

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

void __napi_schedule(struct napi_struct *n);

我们也可以使用 napi_schedule 函数来一次完成 napi_schedule_prep __napi_schedule 这两 个函数的工作:

static inline void napi_schedule(struct napi_struct *n) {
	if (napi_schedule_prep(n))
		__napi_schedule(n);
}

3.3.7 NAPI 处理完成

inline void napi_complete(struct napi_struct *n);

3.4 Linux 内核PHY 子系统与 MDIO 总线

PHY 子系统就是用于 PHY 设备相关内容的,分 为 PHY 设备和 PHY 驱动,和 platform 总线一样,PHY 子系统也是一个设备、总线和驱动模型。

3.4.1 phy设备

phy_device 结构体来表示 PHY 设备,结构体定义 在 include/linux/phy.h:

struct phy_device {
	/* Information about the PHY type */
	/* And management functions */
	struct phy_driver *drv; /* PHY 设备驱动 */
	struct mii_bus *bus; /* 对应的 MII 总线 */
	struct device dev; /* 设备文件 */
	u32 phy_id; /* PHY ID */
	struct phy_c45_device_ids c45_ids;
	bool is_c45;
	bool is_internal;
	bool has_fixups;
	bool suspended;
	enum phy_state state; /* PHY 状态 */
	u32 dev_flags;
	phy_interface_t interface; /* PHY 接口 */
	/* Bus address of the PHY (0-31) */
	int addr; /* PHY 地址(0~31) */
	/*
	* forced speed & duplex (no autoneg)
	* partner speed & duplex & pause (autoneg)
	*/
	int speed; /* 速度 */
	int duplex; /* 双共模式 */
	int pause;
	int asym_pause;
	/* The most recently read link state */
	int link;
	/* Enabled Interrupts */
	u32 interrupts; /* 中断使能标志 */
	/* Union of PHY and Attached devices' supported modes */
	/* See mii.h for more info */
	int irq; /* 中断号 */
	/* private data pointer */
	/* For use by PHYs to maintain extra state */
	void *priv; /* 私有数据 */
	/* Interrupt and Polling infrastructure */
	struct work_struct phy_queue;
	struct delayed_work state_queue;
	atomic_t irq_disable;
	struct mutex lock;
	struct net_device *attached_dev; /* PHY 芯片对应的网络设备 */
	void (*adjust_link)(struct net_device *dev);
};

3.4.1.1 phy_device_register

使用 phy_device_register 函数完成 PHY 设备的注册:
int phy_device_register(struct phy_device *phy);
PHY 设备的注册过程一般是先调用 get_phy_device 函数获取 PHY 设备:

struct phy_device *get_phy_device(struct mii_bus *bus, int addr, bool is_c45) {
	struct phy_c45_device_ids c45_ids = {0};
    u32 phy_id = 0;
	int r;
	
	r = get_phy_id(bus, addr, &phy_id, is_c45, &c45_ids);
	if (r)
		return ERR_PTR(r);
	/* If the phy_id is mostly Fs, there is no device there */
	if ((phy_id & 0x1fffffff) == 0x1fffffff)
		return NULL;
	return phy_device_create(bus, addr, phy_id, is_c45, &c45_ids);
}

get_phy_id 函数获取 PHY ID,也就是读取 PHY 芯片的那两个 ID 寄存器, 得到 PHY 芯片 ID 信息。
phy_device_create 函数创建 phy_device,此函数先申请 phy_device 内存,然 后初始化 phy_device 的各个结构体成员,最终返回创建好的 phy_device。
最后调用phy_device_register 函 数注册的就是这个创建好的 phy_device。

3.4.2 PHY 驱动

struct phy_driver {
	u32 phy_id; /* PHY ID */
	char *name;
	unsigned int phy_id_mask; /* PHY ID 掩码 */
	u32 features;
	u32 flags;
	const void *driver_data;
	int (*soft_reset)(struct phy_device *phydev);
	int (*config_init)(struct phy_device *phydev);
	int (*probe)(struct phy_device *phydev);
	int (*suspend)(struct phy_device *phydev);
	int (*resume)(struct phy_device *phydev);
	int (*config_aneg)(struct phy_device *phydev);
	int (*aneg_done)(struct phy_device *phydev);
	int (*read_status)(struct phy_device *phydev);
	int (*ack_interrupt)(struct phy_device *phydev);
	int (*config_intr)(struct phy_device *phydev);
	int (*did_interrupt)(struct phy_device *phydev);
	void (*remove)(struct phy_device *phydev);
	int (*match_phy_device)(struct phy_device *phydev);
	int (*ts_info)(struct phy_device *phydev, struct ethtool_ts_info *ti);
	int (*hwtstamp)(struct phy_device *phydev, struct ifreq *ifr);
	bool (*rxtstamp)(struct phy_device *dev, struct sk_buff *skb,int type);
	void (*txtstamp)(struct phy_device *dev, struct sk_buff *skb,int type);
	int (*set_wol)(struct phy_device *dev,struct ethtool_wolinfo *wol);
	void (*get_wol)(struct phy_device *dev,struct ethtool_wolinfo *wol);
	void (*link_change_notify)(struct phy_device *dev);
	int (*read_mmd_indirect)(struct phy_device *dev, int ptrad,
	int devnum, int regnum);
	void (*write_mmd_indirect)(struct phy_device *dev, int ptrad,
	int devnum, int regnum, u32 val);
	int (*module_info)(struct phy_device *dev,
	struct ethtool_modinfo *modinfo);
	int (*module_eeprom)(struct phy_device *dev,
	struct ethtool_eeprom *ee, u8 *data);
	struct device_driver driver;
};// include/linux/phy.h

3.4.2.1 PHY 驱动注册卸载

int phy_driver_register(struct phy_driver *new_driver);

new_driver:需要注册的 PHY 驱动。
返回值:0 成功,负值 失败。

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

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

new_driver:需要注册的多个 PHY 驱动数组。
n:要注册的驱动数量。
返回值:0 成功,负值 失败

void phy_driver_unregister(struct phy_driver *drv);

3.4.3 MDIO 总线

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

struct bus_type mdio_bus_type = {
	.name = "mdio_bus",
	.match = mdio_bus_match,
	.pm = MDIO_BUS_PM_OPS,
	.dev_groups = mdio_dev_groups,
};

总线的匹配函数为 mdio_bus_match:

static int mdio_bus_match(struct device *dev, struct device_driver *drv) {
	struct phy_device *phydev = to_phy_device(dev);
	struct phy_driver *phydrv = to_phy_driver(drv);

	if (of_driver_match_device(dev, drv))
		return 1;
	
	if (phydrv->match_phy_device)
		return phydrv->match_phy_device(phydev);
	
	return (phydrv->phy_id & phydrv->phy_id_mask) == (phydev->phy_id & phydrv->phy_id_mask);
}
  1. 首先定义了设备树的话先尝试使用of_driver_match_device来对设备和驱动进行匹配。

  2. 然后使用phy_driver实例中的match_phy_device进行匹配。

  3. 最后用phy_id行匹配。

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

3.4.4 Linux 内核通用 PHY 驱动

Linux内核已经集成了通用PHY驱动,通用PHY驱动名字为“Generic PHY”, 打开 drivers/net/phy/phy_device.c:

static int __init phy_init(void) {
    int rc;

    rc = mdio_bus_init();
    if (rc)
        return rc;
    rc = phy_drivers_register(genphy_driver, ARRAY_SIZE(genphy_driver));
    if (rc)
        mdio_bus_exit();
    return rc;
}

phy_drivers_register会注册phy驱动,genphy_driver 是一个数组,有两个数组元素,表示有两个通用的 PHY 驱动,一个是针对 10/100/1000M 网络的,一个是针对10G 网络的。genphy_driver 定义在 drivers/net/phy/phy_device.c:

static struct phy_driver genphy_driver[] = {
	{
		.phy_id = 0xffffffff,
		.phy_id_mask = 0xffffffff,
		.name = "Generic PHY",
		.soft_reset = genphy_soft_reset,
		.config_init = genphy_config_init,
		.features = PHY_GBIT_FEATURES | SUPPORTED_MII | SUPPORTED_AUI | SUPPORTED_FIBRE | SUPPORTED_BNC,
		.config_aneg = genphy_config_aneg,
		.aneg_done = genphy_aneg_done,
		.read_status = genphy_read_status,
		.suspend = genphy_suspend,
		.resume = genphy_resume,
		.driver = { .owner = THIS_MODULE, },
		}, 
	{
		.phy_id = 0xffffffff,
		.phy_id_mask = 0xffffffff,
		.name = "Generic 10G PHY",
		.soft_reset = gen10g_soft_reset,
		.config_init = gen10g_config_init,
		.features = 0,
		.config_aneg = gen10g_config_aneg,
		.read_status = gen10g_read_status,
		.suspend = gen10g_suspend,
		.resume = gen10g_resume,
		.driver = {.owner = THIS_MODULE, },
	} 
};

3.4.5 LAN8720A PHY驱动测试

LAN8720A 的 驱 动 文 件 为 drivers/net/phy/smsc.c,这个文件是 SMSC 针对自家的一些 PHY 芯片编写的驱动文件,其中就 包含了LAN8720A这个 PHY 芯片。menuconfig开启如下:

-> Device Drivers
	-> Network device support
		-> PHY Device support and infrastructure
			-> Drivers for SMSC PHYs

image

static struct phy_driver smsc_phy_driver[] = {
	{
		.phy_id = 0x0007c0a0, /* OUI=0x00800f, Model#=0x0a */
		.phy_id_mask = 0xfffffff0,
		.name = "SMSC LAN83C185",
		......
		.driver = { .owner = THIS_MODULE, }
	}, 
	{
		.phy_id = 0x0007c0b0, /* OUI=0x00800f, Model#=0x0b */
		.phy_id_mask = 0xfffffff0,
		.name = "SMSC LAN8187",
		......
		.driver = { .owner = THIS_MODULE, }
	}, 
	{
		.phy_id = 0x0007c0c0, /* OUI=0x00800f, Model#=0x0c */
		.phy_id_mask = 0xfffffff0,
		.name = "SMSC LAN8700",
		......
		.driver = { .owner = THIS_MODULE, }
	}, 
	{
		.phy_id = 0x0007c0d0, /* OUI=0x00800f, Model#=0x0d */
		.phy_id_mask = 0xfffffff0,
		.name = "SMSC LAN911x Internal PHY",
		......
		.driver = { .owner = THIS_MODULE, }
	}, 
	{
		.phy_id = 0x0007c0f0, /* OUI=0x00800f, Model#=0x0f */
		.phy_id_mask = 0xfffffff0,
		.name = "SMSC LAN8710/LAN8720",
		.features = (PHY_BASIC_FEATURES | SUPPORTED_Pause | SUPPORTED_Asym_Pause),
		.flags = PHY_HAS_INTERRUPT | PHY_HAS_MAGICANEG,
		/* basic functions */
		.config_aneg = genphy_config_aneg,
		.read_status = lan87xx_read_status,
		.config_init = smsc_phy_config_init,
		.soft_reset = smsc_phy_reset,
		/* IRQ related */
		.ack_interrupt = smsc_phy_ack_interrupt,
		.config_intr = smsc_phy_config_intr,
		.suspend = genphy_suspend,
		.resume = genphy_resume,
		.driver = { .owner = THIS_MODULE, }
	} 
};
module_phy_driver(smsc_phy_driver);

从示例代码看出,smsc_phy_driver 还支持了不少 SMSC 家的 PHY 芯片, 比如LAN83C185、LAN8187、LAN8700等等,当然了,肯定也包括了LAN8720系列。

  1. PHY ID 为 0X0007C0F0。
  2. PHY 的 ID 掩码为 0XFFFFFFF0,也就是前 28 位有效,在进行匹配的时候只需 要比较前 28 位,第 4 位不用比较。
  3. 驱动名字为“SMSC LAN8710/LAN8720”,系统启动以后,打开网卡就会提示当 前 PHY 驱动名字为“SMSC LAN8710/LAN8720”
  4. module_phy_driver(本质是一个宏)来完成 smsc_phy_driver 的注册。

image

3.4.6 DHCP 功能配置

udhcpc 命令已经集成到了 busybox 中,所以不需要我们另外移植。

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 地址的网卡名字。

ifconfig eth1 up #打开 eth1 网卡
udhcpc -i eth1 #为 eth1 网卡申请 IP 地址

测试结果如下:

image

4 Linux网络驱动示例

以恩智浦nxp的imx6ull的以太网ethernet为例:

4.1 ethernet设备树

NXP的IMX系列SOC网络绑定文档为Documentation/devicetree/bindings/net/fsl-fec.txt。

①必要属性

compatible:一般是“fsl,<soc>-fec”,比如 I.MX6ULL 的 compatible 属性就是"fsl,imx6ul-fec",和"fsl,imx6q-fec"。
reg:寄存器地址范围。
interrupts:网络中断。
phy-mode:网络所使用的 PHY 接口模式,是 MII 还是 RMII。

②可选属性

phy-reset-gpios:PHY 芯片的复位引脚。
phy-reset-duration:PHY 复位引脚复位持续时间,单位为毫秒。只有当设置了 phy-resetgpios 属性此属性才会有效,如果不设置此属性的	话 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 总线,主要作为 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。

4.1.1 fec1网络子节点

打开nxp官方的imx6ull.dtsi:可以看到网络子节点fec1是关闭的。

fec1: ethernet@02188000 {
	compatible = "fsl,imx6ul-fec", "fsl,imx6q-fec";
	reg = <0x02188000 0x4000>;
	interrupts = <GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH>,
		<GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_ENET>,
		<&clks IMX6UL_CLK_ENET_AHB>,
		<&clks IMX6UL_CLK_ENET_PTP>,
		<&clks IMX6UL_CLK_ENET_REF>,
		<&clks IMX6UL_CLK_ENET_REF>;
	clock-names = "ipg", "ahb", "ptp",
		"enet_clk_ref", "enet_out";
	stop-mode = <&gpr 0x10 3>;
	fsl,num-tx-queues=<1>;
	fsl,num-rx-queues=<1>;
	fsl,magic-packet;
	fsl,wakeup_irq = <0>;
	status = "disabled";
};
fec2: ethernet@020b4000 {
    ......
}

我们外部修改使能它,打开 imx6ull-alientek-emmc.dts:继续编辑fec1节点。

pinctrl_enet1: enet1grp {
	fsl,pins = <
		MX6UL_PAD_ENET1_RX_EN__ENET1_RX_EN 0x1b0b0
		MX6UL_PAD_ENET1_RX_ER__ENET1_RX_ER 0x1b0b0
		MX6UL_PAD_ENET1_RX_DATA0__ENET1_RDATA00 0x1b0b0
		MX6UL_PAD_ENET1_RX_DATA1__ENET1_RDATA01 0x1b0b0
		MX6UL_PAD_ENET1_TX_EN__ENET1_TX_EN 0x1b0b0
		MX6UL_PAD_ENET1_TX_DATA0__ENET1_TDATA00 0x1b0b0
		MX6UL_PAD_ENET1_TX_DATA1__ENET1_TDATA01 0x1b0b0
		MX6UL_PAD_ENET1_TX_CLK__ENET1_REF_CLK1 0x4001b009
	>;
};
pinctrl_enet2: enet2grp {
	fsl,pins = <
		MX6UL_PAD_GPIO1_IO07__ENET2_MDC 0x1b0b0
		MX6UL_PAD_GPIO1_IO06__ENET2_MDIO 0x1b0b0
		MX6UL_PAD_ENET2_RX_EN__ENET2_RX_EN 0x1b0b0
		MX6UL_PAD_ENET2_RX_ER__ENET2_RX_ER 0x1b0b0
		MX6UL_PAD_ENET2_RX_DATA0__ENET2_RDATA00 0x1b0b0
		MX6UL_PAD_ENET2_RX_DATA1__ENET2_RDATA01 0x1b0b0
		MX6UL_PAD_ENET2_TX_EN__ENET2_TX_EN 0x1b0b0
		MX6UL_PAD_ENET2_TX_DATA0__ENET2_TDATA00 0x1b0b0
		MX6UL_PAD_ENET2_TX_DATA1__ENET2_TDATA01 0x1b0b0
		MX6UL_PAD_ENET2_TX_CLK__ENET2_REF_CLK2 0x4001b009
	>;
};
pinctrl_enet1_reset: enet1resetgrp {
	fsl,pins = <
		/* used for enet1 reset */
		MX6ULL_PAD_SNVS_TAMPER7__GPIO5_IO07 0x10B0
	>;
};
pinctrl_enet2_reset: enet2resetgrp {
	fsl,pins = <
		/* used for enet2 reset */
		MX6ULL_PAD_SNVS_TAMPER8__GPIO5_IO08 0x10B0
	>;
};
&fec1 {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_enet1
			&pinctrl_enet1_reset>;
	phy-mode = "rmii";
	phy-handle = <&ethphy0>;
	phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
	phy-reset-duration = <200>;
	status = "okay";
};
&fec2 {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_enet2
			&pinctrl_enet2_reset>;
	phy-mode = "rmii";
	phy-handle = <&ethphy1>;
	phy-reset-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
	phy-reset-duration = <200>;
	status = "okay";

	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>;
		};
	};
};
  1. 配置fec1和fec2的引脚配置,由于使用的RMII接口如下,引脚配置如enet1grp, enet2grp完成RXD TXD引脚的配置。

image

  1. 配置复位引脚。
  2. phy描述:使用的RMII接口,phy的复位引脚,handle表示引用哪个phy,具体定义在mdio子节点下面。
  3. status = "okay"使能fec1和fec2。
  4. mido 子节点用于描述 MIDO 总线,在此子 节点内会包含 PHY 节点信息。这里一共有两个 PHY 子节点:ethphy0 和 ethphy1,分别对应 ENET1 和 ENET2 的 PHY 芯片。
    1. ethphy0: ethernet-phy@0就是 ENET1 的 PHY 节点名字,“@”后面的 0 就是此 PHY 芯片的芯片地址,reg 属性也是描述 PHY 芯片地址的, 这点和 IIC 设备节点很像。

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

4.2 驱动源码分析

4.2.1 fec_probe

一般来说,SOC 内置网络 MAC+外置 PHY 2部分驱动。打开飞思卡尔的驱动文件drivers/net/ethernet/freescale/fec_main.c

image

compatible匹配后执行probe函数。

4.2.1.1 dts解析资源获取

image

image

fec_enet_get_queue_num用来获取dts中指定发送队列和接收队列数量,都为1。
alloc_etherdev_mqs申请 net_device。

image

获取 net_device 中私有数据内存首地址,设置成fep, fep是该enet的的私有数据。然后对fep结构体赋值。
platform_get_resource获取fec1的寄存器基地址,进行ioremap。同理多个网卡比如fec2,probe就会执行多次,得到多份驱动。
fec_enet_of_parse_stop_mode解析设备树中关于 ENET 的停止模式属性值,属性名字为“stop-mode”,我们没有用到。
查找fsl,magic-packet属性是否存在,如果存在的话就说明有魔术包,有魔术包的话就将 fep 的 wol_flag 成员与 FEC_WOL_HAS_MAGIC_PACKET 进行或运算,也就是在 wol_flag 中做登记,登记支持魔术包。

image

获取phy-handle属性对应节点,在设备树的 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>;
		};
};

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 地址。
of_get_phy_mode获取phy模式,drivers\of\of_net.c中实现。这里模式为RMII接口。赋值给phy_interface。

/* Interface Mode definitions */
typedef enum {
	PHY_INTERFACE_MODE_NA,
	PHY_INTERFACE_MODE_MII,
	PHY_INTERFACE_MODE_GMII,
	PHY_INTERFACE_MODE_SGMII,
	PHY_INTERFACE_MODE_TBI,
	PHY_INTERFACE_MODE_REVMII,
	PHY_INTERFACE_MODE_RMII,
	PHY_INTERFACE_MODE_RGMII,
	PHY_INTERFACE_MODE_RGMII_ID,
	PHY_INTERFACE_MODE_RGMII_RXID,
	PHY_INTERFACE_MODE_RGMII_TXID,
	PHY_INTERFACE_MODE_RTBI,
	PHY_INTERFACE_MODE_SMII,
	PHY_INTERFACE_MODE_XGMII,
	PHY_INTERFACE_MODE_MOCA,
	PHY_INTERFACE_MODE_QSGMII,
	PHY_INTERFACE_MODE_MAX,
} phy_interface_t;//kernel\linux\include\linux\phy.h

image

然后再获取和使能时钟。

image

4.2.1.2 复位phy

fec_reset_phy()用来复位网卡PHY,获取复位gpio和复位时间。拉低复位引脚一段时间后再拉高引脚。

image

4.2.1.3 初始化mac

fec_enet_init()初始化 enet,此函数会分配队列、申请 dma、设置 MAC 地址,初始化 net_device 的 netdev_ops 和 ethtool_ops 如下图,还会调用netif_napi_add来设置 poll 函 数,说明 NXP 官方编写的此网络驱动是 NAPI 兼容驱动。通过netif_napi_add函数向网卡添加了一个 napi 示例,使用 NAPI 驱动要提供一个 poll 函数来轮询处理接收数据,此处的 poll 函数为 fec_enet_rx_napi,后面做网络数据接收处理流程会具体分析。

image

fec_enet_init()最后还会调用fec_restart对mac寄存器参数进行设定,进行reset,使能MII模式,设置速率等等。

image

4.2.1.4 注册fec中断

fec_enet_init()完后获取中断号,注册中断服务程序isr。

从设备树中获取属性fsl,wakeup_irq的值,也就是唤醒中断。

初始化完成量 completion,用于一个执行单元等待另一个执行单元执行完某事。

4.2.1.5 MII/RMII 接口的初始化

fec_enet_mii_init 完成 MII/RMII 接口的初始化。

image

4.2.1.5.1 注册MDIO总线

mii_bus 下的 read 和 write 这两个成员变量分别是读/写 PHY 寄存器的操作函数,可以看到就是利用MDIO总线完成的。

of_get_child_by_name获取叫做mdio的子节点,前面dts中定义了mdio子节点。因此of_mdiobus_register注册 MDIO 总线。
of_mdiobus_register调用 mdiobus_register 函数来向 Linux 内核注册 mdio 总线,同时轮询 mdio 节点下的所有子节点。比如我这里就是ethphy0和ethphy1。然后调用 of_mdiobus_register_phy 向 Linux 内核注册 phy。

4.2.1.5.1.1 注册PHY

of_mdiobus_register_phy向 Linux 内核注册 phy:

image

调用 get_phy_device 获取 PHY 设备,此函数里面会调用 phy_device_create 来创建一个 phy_device 设备并返回。
调用 phy_device_register 函数向 Linux 内核注册 PHY 设备。

image

4.2.1.6 注册fec net_device

image

4.2.2 fec_drv_remove

image

调用 unregister_netdev 注销注册的 net_device。
调用 fec_enet_mii_remove 移除掉 MDIO 总线相关的内容,会调用 mdiobus_unregister 来注销掉 mii_bus,并且通过 mdiobus_free 释放掉 mii_bus 。
调用 free_netdev 释放申请的 net_device。

4.2.3 netdev_ops

image

4.2.3.1 fec_enet_open

image

  1. 使能 enet 时钟
  2. 申请环形缓冲区 buffer,里面会调用fec_enet_alloc_rxq_buffers 和 fec_enet_alloc_txq_buffers 分别实现发送队列和接收队 列缓冲区的申请。
  3. 重启网络,一般连接状态改变、传输超时或者配置网络的时候都会调用 fec_restart 函数。
  4. 打开网卡的时候调用 fec_enet_mii_probe 函数来探测并连接对应的 PHY 设备。
  5. 调用 napi_enable 函数使能 NAPI 调度。
  6. 调用 phy_start 函数开启 PHY 设备。
  7. 调用 netif_tx_start_all_queues 函数来激活发送队列

4.2.3.2 fec_enet_close

image

open的反操作。phy_stop 函数停止 PHY 设备。
调用 napi_disable 函数关闭 NAPI 调度。
调用 netif_tx_disable 函数关闭 NAPI 的发送队列。
调用 fec_stop 函数关闭 ENET 外设。
调用 phy_disconnect 函数断开与 PHY 设备的连接。
调用 fec_enet_clk_enable 函数关闭 ENET 外设时钟。
调用 fec_enet_free_buffers 函数释放发送和接收的环形缓冲区内存。

4.2.3.3 fec_enet_start_xmit(发送)

网络数据发送, 将上层传递过来的 sk_buff 中的数据通过硬件发送出去。

参数:第一个参数 skb 就是上层应用传递下来的要发送的网络数据,第二个参数 ndev 就是要发送数据的设备。

image

通过发送队列发送数据:

判断 skb 是否为 GSO(Generic Segmentation Offload), 如果是 GSO 的话就通过 fec_enet_txq_submit_tso 函数发送,如果不是的话就通过 fec_enet_txq_submit_skb 发送。
TSO:全称是 TCP Segmentation Offload,利用网卡对大数据包进行自动分段处理,降低 CPU 负载。
GSO:全称是 Generic Segmentation Offload,在发送数据之前先检查一下网卡是否支持 TSO, 如果支持的话就让网卡分段,不过不支持的话就由协议栈进行分段处理,分段处理完成以后再 交给网卡去发送。

fec_enet_get_free_txdesc_num 函数获取剩余的发送描述符数量。
如果剩余的发送描述符的数量小于设置的阈值(tx_stop_threshold)的话就调用函数 netif_tx_stop_queue 来暂停发送,通过暂停发送来通知应用层停止向网络发送 skb,发送中断中 会重新开启的。

4.2.3.4 fec_enet_interrupt-中断服务函数(接收)

image

  1. readl, writel, 中断服务中获取中断状态,清中断。

  2. 调用 fec_enet_collect_events 函数统计中断信息,也就是统计都发生了哪些中断。 fep 中成员变量 work_tx 和 work_rx 的 bit0、bit1 和 bit2 用来做不同的标记,work_rx 的 bit2 表 示接收到数据帧,work_tx 的 bit2 表示发送完数据帧。

  3. 调用 napi_complete 函数来宣布一次轮询结束。

  4. 设置 ENET 的 EIMR 寄存器,重新使能中断。

posted on 2024-07-10 15:24  fuzidage  阅读(55)  评论(0编辑  收藏  举报