网络驱动硬件

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

1、 SOC 内部没有网络 MAC 外设
我们一般说某个 SOC 不支持网络,说的就是它没有网络 MAC。那么这个芯片就不能上网了吗?显然不是的,既然没有内部 MAC,那么可以找个外置的 MAC 芯片啊,不过一般这种外置的网络芯片都是 MAC+PHY 一体的。

这种方案的优点就是让不支持网络的 SOC 能够另辟蹊径,实现网络功能,但是缺点就是网络效率不高,因为一般芯片内置的 MAC 会有网络加速引擎,比如网络专用 DMA,网络处理效率会很高。而且此类芯片网速都不快,基本就是 10/100M。

2、 SOC 内部集成网络 MAC 外设

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

内部集成网络 MAC 的优点如下:

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

MII接口

MII 全称是 Media Independent Interface,直译过来就是介质独立接口,它是 IEEE-802.3 定义的以太网标准接口 。

MII接口一共16根线,

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

RMII 全称是 Reduced Media Independent Interface,翻译过来就是精简的介质独立接口,也就是 MII 接口的精简版本。 RMII 接口只需要 7 根数据线,

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

嵌入式网络硬件框图

一个 MAC 连接一个 PHY 芯片形成一个完整网络接口

PHY 是 IEEE 802.3 规定的一个标准模块,前面说了, SOC 可以对 PHY 进行配置或者读取PHY 相关状态,这个就需要 PHY 内部寄存器去实现了。 PHY 芯片寄存器地址空间为 5 位,地址 031 共 32 个寄存器, IEEE 定义了 015 这 16 个寄存器的功能, 16~31 这 16 个寄存器由厂商自行实现。 后面16位的引脚为不同厂商的特色功能引脚,前16位就能保证基本的网络数据通信。

LAN8720A (PHY芯片)详解

1、 LAN8720A 简介
LAN8720A 是低功耗的 10/100M 单以太网 PHY 层芯片, 可应用于机顶盒、网络打印机、嵌入式通信设备、 IP 电话等领域。 I/O 引脚电压符合 IEEE802.3-2005 标准。 LAN8720A 支持通过 RMII 接口与以太网 MAC 层通信,内置 10-BASE-T/100BASE-TX 全双工传输模块,支持10Mbps 和 100Mbps。 LAN8720A 可以通过自协商的方式选择与目的主机最佳的连接方式(速度和双工模式)。支持 HP Auto-MDIX 自动翻转功能,无需更换网线即可将连接更改为直连或交叉连接。
LAN8720A 的主要特点如下:
· 高性能的 10/100M 以太网传输模块
· 支持 RMII 接口以减少引脚数
· 支持全双工和半双工模式
· 两个状态 LED 输出
· 可以使用 25M 晶振以降低成本
· 支持自协商模式
· 支持 HP Auto-MDIX 自动翻转功能
· 支持 SMI 串行管理接口
· 支持 MAC 接口

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

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

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

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 作为中断引脚

LAN8720寄存器引脚

描述 类型
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:正常运行 R/W
9 重启自动协商功能 0:正常运行 1:重启自动协商功能 注意:此位会被自动清零 R/W SC
8 双工模式 0:半双工 1:全双工 注意:开启自动协商功能后此位失效 R/W
7:0 保留 RO

Linux内核网络驱动框架

Linux 内核使用 net_device 结构体表示一个具体的网络设备, net_device 是整个网络驱动的灵魂。网络驱动的核心就是初始化 net_device 结构体中的各个成员变量,然后将初始化完成以后的 net_device 注册到 Linux 内核中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
1 struct net_device {
2 char name[IFNAMSIZ]; //name 是网络设备的名字。
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; //mem_end 是共享内存结束地址。
10 unsigned long mem_start; //mem_start 是共享内存起始地址。
11 unsigned long base_addr; //base_addr 是网络设备 I/O 地址。
12 int irq; //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; //dev_list 是全局网络设备列表。
25 struct list_head napi_list; //napi_list 是 napi 网络设备的列表入口。
26 struct list_head unreg_list; //unreg_list 是注销(unregister)的网络设备列表入口。
27 struct list_head close_list; //close_list 是关闭的网络设备列表入口。
......
60 const struct net_device_ops *netdev_ops; /*netdev_ops 是网络设备的操作集函数,包含了一系列的网络设备操作回调函数,类似字符设备中的 file_operations,稍后会讲解 netdev_ops 结构体。*/
61 const struct ethtool_ops *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; // header_ops 是头部的相关操作函数集,比如创建、解析、缓冲等。
67
68 unsigned int flags; //flags 是网络接口标志
......
77 unsigned char if_port; //if_port 指定接口的端口类型,如果设备支持多端口的话就通过 if_port 来指定所使用的端口类型。
78 unsigned char dma; //dma 是网络设备所使用的 DMA 通道,不是所有的设备都会用到 DMA。
79
80 unsigned int mtu; //mtu 是网络最大传输单元,为 1500
81 unsigned short type; //type 用于指定 ARP 模块的类型,以太网的 ARP 接口为 ARPHRD_ETHER
82 unsigned short hard_header_len; //perm_addr 是永久的硬件地址
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; //addr_len 是硬件地址长度
......
130 /*
131 * Cache lines mostly used on receive path (including
eth_type_trans())
132 */
133 unsigned long last_rx; //last_rx 是最后接收的数据包时间戳,记录的是 jiffies
134
135 /* Interface address info used in eth_type_trans() */
136 unsigned char *dev_addr; //dev_addr 也是硬件地址,是当前分配的 MAC 地址,可以通过软件修改。
137
138
139 #ifdef CONFIG_SYSFS
140 struct netdev_rx_queue *_rx; //_rx 是接收队列。
141
142 unsigned int num_rx_queues; /*num_rx_queues 是接收队列数量,在调用 register_netdev 注册网络设备的时候会
分配指定数量的接收队列。*/
143 unsigned int real_num_rx_queues; //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; //_tx 是发送队列。
162 unsigned int num_tx_queues; /*num_tx_queues 是发送队列数量,通过 alloc_netdev_mq 函数分配指定数量的发
送队列。*/
163 unsigned int real_num_tx_queues; //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; //trans_start 是最后的数据包发送的时间戳,记录的是 jiffies。
......
248 struct phy_device *phydev; //phydev 是对应的 PHY 设备。
249 struct lock_class_key *qdisc_tx_busylock;
250 };

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

1
#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 函数,此函数原型如下

1
2
3
4
5
struct net_device * alloc_netdev_mqs ( int sizeof_priv,	//sizeof_priv: 私有数据块大小。
const char *name, //name: 设备名字。
void (*setup) (struct net_device *))//setup: 回调函数
unsigned int txqs, //txqs: 分配的发送队列数量。
unsigned int rxqs); //rxqs: 分配的接收队列数量。

返回值: 如果申请成功的话就返回申请到的 net_device 指针,失败的话就返回 NULL。

以太网的网络初始化封装为

1
2
#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)

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

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

对于网络设备而言,使用 alloc_etherdev 或alloc_etherdev_mqs 来申请 net_device。 NXP 官方编写的网络驱动就是采用alloc_etherdev_mqs来申请 net_device。

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

1
void free_netdev(struct net_device *dev)//dev: 要释放掉的 net_device 指针。

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

1
int register_netdev(struct net_device *dev)//dev: 要注册的 net_device 指针。返回值: 0 注册成功,负值 注册失败。

注销 net_device
既然有注册,那么必然有注销,注销 net_device 使用函数 unregister_netdev,函数原型如下:

1
void unregister_netdev(struct net_device *dev)	//dev: 要注销的 net_device 指针。

操作集

net_device 有个非常重要的成员变量: netdev_ops,为 net_device_ops 结构体指针类型,这就是网络设备的操作集。 net_device_ops 结构体定义在 include/linux/netdevice.h 文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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);
/*·使能网络外设时钟。
·申请网络所使用的环形缓冲区。
·初始化 MAC 外设。
·绑定接口对应的 PHY。
·如果使用 NAPI 的话要使能 NAPI 模块,通过 napi_enable 函数来使能。
·开启 PHY。
·调用 netif_tx_start_all_queues 来使能传输队列,也可能调用 netif_start_queue 函数。等*/
5 int (*ndo_stop)(struct net_device *dev);
6 netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb, /*需要发送数据的时候此函数就会执行, sk_buff 保存了上层传
递给网络驱动层的数据。*/
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);//此函数用于改变地址过滤列表,根据 net_device 的 flags成员变量来设置 SOC 的网络外设寄存器
15 int (*ndo_set_mac_address)(struct net_device *dev,//此函数用于修改网卡的 MAC 地址,并且将 MAC 地址写入到网络外设的硬件寄存器中。
16 void *addr);
17 int (*ndo_validate_addr)(struct net_device *dev);//验证 MAC 地址是否合法
18 int (*ndo_do_ioctl)(struct net_device *dev, //用户程序调用 ioctl 的时候此函数就会执行用户程序调用 ioctl 的时候此函数就会执行
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, //更改 MTU(最大传输单元)大小
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);//当发送超时的时候函数会执行,一般可能会重启 MAC 和 PHY,重新开始数据发送等
......
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, //修改 net_device 的 features 属性,设置相应的硬件属性
105 netdev_features_t features);
......
166 };

NAPI机制

如果玩过单片机的话应该都知道,像 IIC、 SPI、网络等这些通信接口,接收数据有两种方法:轮询或中断。 Linux 里面的网络数据接收也轮询和中断两种,中断的好处就是响应快,数据量小的时候处理及时,速度快,但是一旦当数据量大,而且都是短帧的时候会导致中断频繁发生,消耗大量的 CPU 处理时间在中断自身处理上。轮询恰好相反,响应没有中断及时,但是在处理大量数据的时候不需要消耗过多的 CPU 处理时间。

Linux 在这两个处理方式的基础上提出了另外一种网络数据接收的处理方法: NAPI(New API), NAPI 是一种高效的网络处理技术。NAPI 的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL 的方法来轮询处理数据。这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。目前 NAPI 已经在 Linux 的网络驱动中得到了大量的应用,

1、初始化 NAPI
首先要初始化一个 napi_struct 实例,使用 netif_napi_add 函数,此函数定义在 net/core/dev.c中,

1
2
3
4
void netif_napi_add(struct net_device *dev,	// 每个 NAPI 必须关联一个网络设备,此参数指定 NAPI 要关联的网络设备
struct napi_struct *napi, //要初始化的 NAPI 实例
int (*poll)(struct napi_struct *, int),//NAPI 所使用的轮询函数,一般在此完成网络数据接收
int weight) //NAPI 默认权重(weight),一般为 NAPI_POLL_WEIGHT。

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

1
void netif_napi_del(struct napi_struct *napi)//napi: 要删除的 NAPI。

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

1
inline void napi_enable(struct napi_struct *n)//n: 要使能的 NAPI。

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

1
void napi_disable(struct napi_struct *n)	//n: 要关闭的 NAPI。

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

1
inline bool napi_schedule_prep(struct napi_struct *n)	//n: 要检查的 NAPI。返回值可调度为真

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

1
void __napi_schedule(struct napi_struct *n)	//n: 要调度的 NAPI。

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

1
inline void napi_complete(struct napi_struct *n)	//n: 处理完成的 NAPI。

网络外设驱动树

NXP 的 I.MX 系 列 SOC 网 络 绑 定 文 档 为
Documentation/devicetree/bindings/net/fsl-fec.txt,此绑定文档描述了 I.MX 系列 SOC 网络设备树节点的要求。
①、 必要属性
compatible: 这个肯定是必须的,一般是“fsl,-fec”,比如 I.MX6ULL 的 compatible 属
性就是”fsl,imx6ul-fec”,和”fsl,imx6q-fec”。
reg: SOC 网络外设寄存器地址范围。
interrupts:网络中断。
phy-mode: 网络所使用的 PHY 接口模式,是 MII 还是 RMII。
②、 可选属性
phy-reset-gpios: PHY 芯片的复位引脚。
phy-reset-duration: PHY 复位引脚复位持续时间,单位为毫秒。只有当设置了 phy-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”的子节点,此子节点用于指定网络外设所使用的 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 的 bit318, 16 进制格式。
BBBB: PHY 的 16 位 ID 寄存器 2 值,也就是 OUI 的 bit19
24, 16 进制格式。
max-speed: PHY 支持的最高速度,比如 10、 100 或 1000。