从前面的章节我们知道,网络接口(如以太网接口)是硬件接口,(提示:网络接口又可以称之为网卡,为了统一,下文均采用网卡表示网络接口),LwIP是软件,那么怎么让硬件与软件无缝连接起来呢?而且,网卡又有多种多样,怎么能让LwIP使用同样的软件能兼容不同的硬件呢?原来LwIP使用一个数据结构——netif来描述一个网卡,但是由于网卡是直接与硬件打交道的,硬件不同则处理基本是不同的,所以必须由用户提供最底层接口函数,LwIP提供统一的接口,但是底层的实现需要用户自己去完成,比如网卡的初始化,网卡的收发数据,当LwIP底层得到了网络的数据之后,才会传入内核中去处理;同理,LwIP内核需要发送一个数据包的时候,也需要调用网卡的发送函数,这样子才能把数据从硬件接口到软件内核无缝连接起来。LwIP中的 ethernetif.c文件即为底层接口的驱动的模版,用户为自己的网络设备实现驱动时应参照此模块做修改。ethernetif.c文件中的函数通常为与硬件打交道的底层函数,当有数据需要通过网卡接收或者发送数据的时候就会被调用,经过LwIP协议栈内部进行处理后,从应用层就能得到数据或者可以发送数据。
简单来说,netif是LwIP抽象出来的网卡,LwIP协议栈可以使用多个不同的接口,而ethernetif.c文件则提供了netif访问各种不同的网卡,每个网卡有不同的实现方式,用户只需要修改ethernetif.c文件即可。
在单网卡中,这个netif结构体只有一个,可能还有人会问,那么一个设备中有多个网卡怎么办,很简单,LwIP会将每个用netif描述的网卡连接成一个链表(单向链表),该链表就记录每个网卡的netif。屏蔽硬件接口的差异,完成了对不同网卡的抽象,因此了解netif结构体是移植LwIP的关键。
我们可以理解将整个网络的数据传输理解为物流,那么网卡就是不同的运输工具,我们可以选择汽车、飞机、轮船等运输工具,不同的运输工具速度是不一样的,但是对于一个物流公司而言,可能同时存在很多种运输的工具,这就需要物流公司去记录这些运输工具,当有一个包裹需要通过飞机运输出去,那么物流公司就会将这个包裹通过飞机发送出去,这就好比我们的网卡,需要哪个网卡发送或者接收网络数据的时候,就会让对应的网卡去工作。
下面一起来看看netif数据结构是怎么样的,具体见代码清单 4‑1。
```
1 struct netif
2 {
3 #if !LWIP_SINGLE_NETIF
4 /* 指向netif链表中的下一个 */
5 struct netif *next; (1)
6 #endif
7
8 #if LWIP_IPV4
9 /* 网络字节中的IP地址、子网掩码、默认网关配置 */
10 ip_addr_t ip_addr;
11 ip_addr_t netmask;
12 ip_addr_t gw; (2)
13 #endif /* LWIP_IPV4 */
14
15 /* 此函数由网络设备驱动程序调用,将数据包传递到TCP/IP协议栈。
16 * 对于以太网物理层,这通常是ethernet_input()*/
17 netif_input_fn input; (3)
18
19 #if LWIP_IPV4
20
21 /* 此函数由IP层调用,在接口上发送数据包。通常这个功能,
22 * 首先解析硬件地址,然后发送数据包。
23 * 对于以太网物理层,这通常是etharp_output() */
24 netif_output_fn output; (4)
25
26 #endif /* LWIP_IPV4 */
27 /* 此函数由ethernet_output()调用,当需要在网卡上发送一个数据包时。
28 * 底层硬件输出数据函数,一般是调用自定义函数low_level_output*/
29 netif_linkoutput_fn linkoutput; (5)
30
31 #if LWIP_NETIF_STATUS_CALLBACK
32 /* 当netif状态设置为up或down时调用此函数 */
33 netif_status_callback_fn status_callback; (6)
34 #endif /* LWIP_NETIF_STATUS_CALLBACK */
35
36 #if LWIP_NETIF_LINK_CALLBACK
37 /* 当netif链接设置为up或down时,将调用此函数 */
38 netif_status_callback_fn link_callback; (7)
39 #endif /* LWIP_NETIF_LINK_CALLBACK */
40
41 #if LWIP_NETIF_REMOVE_CALLBACK
42 /* 当netif被删除时调用此函数 */
43 netif_status_callback_fn remove_callback; (8)
44 #endif /* LWIP_NETIF_REMOVE_CALLBACK */
45
46 /* 此字段可由设备驱动程序设置并指向设备的状态信息。
47 * 主要是将网卡的某些私有数据传递给上层,用户可以自由发挥,也可以不用。*/
48 void *state; (9)
49
50 #ifdef netif_get_client_data
51 void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA];
52 #endif
53 #if LWIP_NETIF_HOSTNAME
54 /* 这个netif的主机名,NULL也是一个有效值 */
55 const char* hostname;
56 #endif /* LWIP_NETIF_HOSTNAME */
57
58 #if LWIP_CHECKSUM_CTRL_PER_NETIF
59 u16_t chksum_flags;
60 #endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/
61
62 /** 最大传输单位(以字节为单位),对于以太网一般设为 1500 */
63 u16_t mtu; (10)
64
65 /** 此网卡的链路层硬件地址 */
66 u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; (11)
67
68 /** 硬件地址长度,对于以太网就是 MAC 地址长度,为6字节 */
69 u8_t hwaddr_len; (12)
70
71 /* 网卡状态信息标志位,是很重要的控制字段,
72 * 它包括网卡功能使能、广播使能、 ARP 使能等等重要控制位。 */
73 u8_t flags; (13)
74
75 /* 字段用于保存每一个网卡的名字。用两个字符的名字来标识网络接
76 * 口使用的设备驱动的种类,名字由设备驱动来设置并且应该反映通过网卡
77 * 表示的硬件的种类。比如蓝牙设备( bluetooth)的网卡名字可以是 bt,
78 * 而 IEEE 802.11b WLAN 设备的名字就可以是wl,当然设置什么名字用户是可
79 * 以自由发挥的,这并不影响用户对网卡的使用。当然,如果两个网卡
80 * 具有相同的网络名字,我们就用 num 字段来区分相同类别的不同网卡*/
81 char name[2]; (14)
82
83 /* 用来标示使用同种驱动类型的不同网卡 */
84 u8_t num; (15)
85
86 #if MIB2_STATS
87 /* 连接类型 */
88 u8_t link_type;
89 /* 连接速度 */
90 u32_t link_speed;
91 /* 最后一次更改的时间戳 */
92 u32_t ts;
93 /** counters */
94 struct stats_mib2_netif_ctrs mib2_counters;
95 #endif /* MIB2_STATS */
96
97 #if LWIP_IPV4 && LWIP_IGMP
98 /** 可以调用此函数来添加或删除多播中的条目
99 以太网MAC的过滤表。*/
100 netif_igmp_mac_filter_fn igmp_mac_filter;
101 #endif /* LWIP_IPV4 && LWIP_IGMP */
102
103 #if LWIP_NETIF_USE_HINTS
104 struct netif_hint *hints;
105 #endif /* LWIP_NETIF_USE_HINTS */
106
107 #if ENABLE_LOOPBACK
108 /* List of packets to be queued for ourselves. */
109 struct pbuf *loop_first;
110 struct pbuf *loop_last;
111
112 #if LWIP_LOOPBACK_MAX_PBUFS
113 u16_t loop_cnt_current;
114 #endif /* LWIP_LOOPBACK_MAX_PBUFS */
115
116 #endif /* ENABLE_LOOPBACK */
117 };
```
我们挑一些比较重要的netif字段进行讲解:
* (1):LwIP使用链表来管理同一设备的多个网卡。在netif.c文件中定义两个全局指针:struct netif *netif_list和struct netif *netif_default,其中netif_list就是网卡链表指针,指向网卡链表的首节点(第一个网卡),后者表示默认情况下(有多网口时)使用哪个网卡。next字段指向下一个netif结构体指针,在一个设备中有多个网卡时,才使用该字段。
* (2):ip_addr字段记录的是网络中的IP地址,netmask字段记录的是子网掩码,gw记录的是网关地址,这些字段是用于描述网卡的网络地址属性。
IP地址必须与网卡对应,即设备拥有多少个网卡那就必须有多少个IP地址;子网掩码可以用来判断某个IP地址与当前网卡是否处于同一个子网中,IP在发送数据包的时候会选择与目标IP地址处于同一子网的网卡来发送;网关地址在数据包的发送、转发过程非常重要,如果要向不属于同一子网的主机(主机目标IP地址与网卡不属于同一子网)发送一个数据包,那么LwIP就会将数据包发送到网关中,网关设备会对该数据包进行正确的转发,除此之外,网关还提供很多高级功能,如DNS,DHCP等。
* (3):input是一个函数指针,指向一个函数,该函数由网络设备驱动程序调用,将数据包传递到TCP/IP协议栈(IP层)。对于以太网物理层,这通常是ethernet_input(),参数为pbuf和netif类型,其中pbuf为接收到的数据包。
* (4):output也是一个函数指针,指向一个函数,此函数由IP层调用,在接口上发送数据包。用户需要编写该函数并使output指向它,通这个函数的处理步骤是首先解析硬件地址,然后发送数据包。对于以太网物理层,该函数通常是etharp_output(),参数为pbuf、netif和ip_addr类型,其中,ipaddr代表要将该数据包发送到的地址,但不一定是数据包最终到到达的IP地址,比如,要发送IP数据报到一个并不在本网络的主机上,该数据包要被发送到一个路由器上,这里的ipaddr就是路由器IP地址。
* (5):linkoutput字段和output类似,也需要用户自己实现一个函数,但只有两个参数,它是由ARP模块调用的,一般是自定义函数low_level_output()。当需要在网卡上发送一个数据包时,该函数会被ethernet_output()函数调用。
* (6):当netif状态设置为up或down时,将调用此函数。
* (7):当netif连接设置为up或down时,将调用此函数。
* (8):当netif被删除时调用此函数。
* (9):此字段可由设备驱动程序设置并指向设备的状态信息。主要是将网卡的某些私有数据传递给上层,用户可以自由发挥,也可以不用。
* (10):最大传输单位(以字节为单位),对于以太网一般设为 1500,在IP层发送数据的时候,LwIP会使用该字段决定是否需要对数据包进行分片处理,为什么是在IP层进行分片处理?因为链路层不提供任何的差错处理机制,如果在网卡中接收的数据包不满足网卡自身的属性,那么网卡可能就会直接丢弃该数据包,也可能在底层进行分包发送,但是这种分包在IP层看来是不可接受的,因为它打乱了数据的结构,所以只能由IP层进行分片处理。
* (11):此网卡的链路层硬件地址。
* (12):硬件地址长度,对于以太网就是 MAC 地址长度,为6字节
* (13):网卡状态信息标志位,是很重要的控制字段,它包括网卡功能使能、广播使能、 ARP 使能等等重要控制位。
* (14):name字段用于保存每一个网卡的名字。用两个字符的名字来标识网卡使用的设备驱动的种类,名字由设备驱动来设置并且应该反映通过网卡表示的硬件的种类。比如蓝牙设备(bluetooth)的网卡名字可以是 bt,而 IEEE 802.11b WLAN设备的名字就可以是wl,当然设置什么名字用户是可以自由发挥的,这并不影响用户对网卡的使用。当然,如果两个网卡具有相同的网络名字,我们就用 num字段来区分相同类别的不同网卡。
* (15):用来标识使用同种驱动类型的不同网卡。
- 说明
- 第1章:网络协议简介
- 1.1:常用网络协议
- 1.2:网络协议的分层模型
- 1.3:协议层报文间的封装与拆封
- 第2章:LwIP简介
- 2.1:LwIP的优缺点
- 2.2:LwIP的文件说明
- 2.2.1:如何获取LwIP源码文件
- 2.2.2:LwIP文件说明
- 2.3:查看LwIP的说明文档
- 2.4:使用vscode查看源码
- 2.4.1:查看文件中的符号列表(函数列表)
- 2.4.2:函数定义跳转
- 2.5:LwIP源码里的example
- 2.6:LwIP的三种编程接口
- 2.6.1:RAW/Callback API
- 2.6.2:NETCONN API
- 2.6.3:SOCKET API
- 第3章:开发平台介绍
- 3.1:以太网简介
- 3.1.1:PHY层
- 3.1.2:MAC子层
- 3.2:STM32的ETH外设
- 3.3:MII 和 RMII 接口
- 3.4:PHY:LAN8720A
- 3.5:硬件设计
- 3.6:软件设计
- 3.6.1:获取STM32的裸机工程模板
- 3.6.2:添加bsp_eth.c与bsp_eth.h
- 3.6.3:修改stm32f4xx_hal_conf.h文件
- 第4章:LwIP的网络接口管理
- 4.1:netif结构体
- 4.2:netif使用
- 4.3:与netif相关的底层函数
- 4.4:ethernetif.c文件内容
- 4.4.1:ethernetif数据结构
- 4.4.2:ethernetif_init()
- 4.4.3:low_level_init()
- 第5章:LwIP的内存管理
- 5.1:几种内存分配策略
- 5.1.1:固定大小的内存块
- 5.1.2:可变长度分配
- 5.2:动态内存池(POOL)
- 5.2.1:内存池的预处理
- 5.2.2:内存池的初始化
- 5.2.3:内存分配
- 5.2.4:内存释放
- 5.3:动态内存堆
- 5.3.1:内存堆的组织结构
- 5.3.2:内存堆初始化
- 5.3.3:内存分配
- 5.3.4:内存释放
- 5.4:使用C库的malloc和free来管理内存
- 5.5:LwIP中的配置
- 第6章:网络数据包
- 6.1:TCP/IP协议的分层思想
- 6.2:LwIP的线程模型
- 6.3:pbuf结构体说明
- 6.4:pbuf的类型
- 6.4.1:PBUF_RAM类型的pbuf
- 6.4.2:PBUF_POOL类型的pbuf
- 6.4.3:PBUF_ROM和PBUF_REF类型pbuf
- 6.5:pbuf_alloc()
- 6.6:pbuf_free()
- 6.7:其它pbuf操作函数
- 6.7.1:pbuf_realloc()
- 6.7.2:pbuf_header()
- 6.7.3:pbuf_take()
- 6.8:网卡中使用的pbuf
- 6.8.1:low_level_output()
- 6.8.2:low_level_input()
- 6.8.3:ethernetif_input()
- 第7章:无操作系统移植LwIP
- 7.1:将LwIP添加到裸机工程
- 7.2:移植头文件
- 7.3:移植网卡驱动
- 7.4:LwIP时基
- 7.5:协议栈初始化
- 7.6:获取数据包
- 7.6.1:查询方式
- 7.6.2:ping命令详解
- 7.6.3:中断方式
- 第8章:有操作系统移植LwIP
- 8.1:LwIP中添加操作系统
- 8.1.1:拷贝FreeRTOS源码到工程文件夹
- 8.1.2:添加FreeRTOS源码到工程组文件夹
- 8.1.3:指定FreeRTOS头文件的路径
- 8.1.4:修改stm32f10x_it.c
- 8.2:lwipopts.h文件需要加入的配置
- 8.3:sys_arch.c/h文件的编写
- 8.4:网卡底层的编写
- 8.5:协议栈初始化
- 8.6:移植后使用ping测试基本响应
- 第9章:LwIP一探究竟
- 9.1:网卡接收数据的流程
- 9.2:内核超时处理
- 9.2.1:sys_timeo结构体与超时链表
- 9.2.2:注册超时事件
- 9.2.3:超时检查
- 9.3:tcpip_thread线程
- 9.4:LwIP中的消息
- 9.4.1:消息结构
- 9.4.2:数据包消息
- 9.4.3:API消息
- 9.5:揭开LwIP神秘的面纱
- 第10章:ARP协议
- 10.1:链路层概述
- 10.2:MAC地址的基本概念
- 10.3:初识ARP
- 10.4:以太网帧结构
- 10.5:IP地址映射为物理地址
- 10.6:ARP缓存表
- 10.7:ARP缓存表的超时处理
- 10.8:ARP报文
- 10.9:发送ARP请求包
- 10.10:数据包接收流程
- 10.10.1:以太网之数据包接收
- 10.10.2:ARP数据包处理
- 10.10.3:更新ARP缓存表
- 10.11:数据包发送流程
- 10.11.1:etharp_output()函数
- 10.11.2:etharp_output_to_arp_index()函数
- 10.11.3:etharp_query()函数
- 第11章:IP协议
- 11.1:IP地址.md
- 11.1.1:概述
- 11.1.2:IP地址编址
- 11.1.3:特殊IP地址