本帖最后由 尹小舟 于 2024-6-5 14:05 编辑
LWIP(Lightweight IP)是一个开源的TCP/IP协议栈实现,特别设计用于嵌入式系统,它注重减少内存使用和代码大小,以适应资源受限的环境。以下是您提到的LWIP的一些主要特性的详细说明:
- 支持多网络接口下的IP转发:
- LWIP允许设备具有多个网络接口,并能够在这些接口之间转发IP数据包。这在路由器或网关设备中特别有用。
- 支持IP数据分片传输功能:
- 对于较大的数据包,LWIP可以将其拆分成较小的片段,以便在网络上进行传输。这允许设备处理大于其MTU(最大传输单元)的数据包。
- 支持ICMP协议:
- ICMP(Internet Control Message Protocol)用于在IP主机和路由器之间发送控制消息,如错误报告和诊断消息。
- 支持UDP协议:
- UDP是一种无连接的协议,它提供尽最大努力的数据传输服务,但不保证数据的顺序或可靠性。UDP适用于不需要可靠传输的应用,如实时视频流或音频流。
- 支持TCP协议:
- TCP(Transmission Control Protocol)是一种面向连接的协议,它提供可靠的数据传输服务。TCP包括阻塞控制、RTT(Round-Trip Time)估算、快速恢复和快速转发等特性,以确保数据包的可靠交付。
- 支持PPP点对点通信协议:
- PPP允许通过串行链路(如电话线或ISDN线)建立TCP/IP连接。它常用于拨号上网和VPN连接。
- 支持DHCP协议:
- DHCP(Dynamic Host Configuration Protocol)允许设备自动获取IP地址和其他网络配置信息,如子网掩码和默认网关。这简化了网络配置过程。
- 支持IPv6协议:
- IPv6是下一代互联网协议,它提供了比IPv4更多的地址空间和其他改进功能。LWIP支持IPv6允许设备在IPv6网络上运行。
- 提供专门的内部回调函数接口:
- LWIP允许用户定义回调函数,以便在特定事件发生时(如数据包到达或定时器超时)执行自定义操作。这提高了应用程序的性能和灵活性。
- 提供可选择的Berkeley接口函数:
- Berkeley套接字API(也称为BSD套接字)是一种广泛使用的网络编程接口。LWIP提供了可选择的Berkeley套接字API实现,允许开发人员使用熟悉的套接字编程模型来开发网络应用程序。
由于LWIP的轻量级设计,它非常适合在嵌入式系统中使用,特别是在那些内存和处理器资源有限的设备上。此外,LWIP的高度可配置性使得开发人员可以根据特定应用的需求来定制协议栈的功能和大小。
1. 内存管理
TCP/IP协议栈中的各层都需要处理具有特定格式的数据包,并在数据包通过各层时添加或移除头部和尾部信息。这种对数据缓冲区的频繁操作可能会对嵌入式系统的性能和稳定性产生影响,特别是在资源受限的情况下。所以对于TCP/IP协议栈而言,内存管理始终是最重要的一环,内存管理的选择将从根本上决定内 存分配和回收效率,最终决定系统的性能。
在LwIP中,动态内存管理机制包括了几种不同的方式,如使用标准C库函数(如malloc()和free())、自定义的动态内存堆(heap)分配以及动态内存池(pool)分配。具体使用哪种机制,通常是通过配置LwIP的opt.h(或类似的配置头文件)中的宏定义来决定的。
以下是与LwIP动态内存管理相关的宏定义示例和解释:
-
标准C库内存分配:
如果LwIP配置为使用标准C库函数进行内存分配,那么它会直接调用malloc()和free()。这种配置在操作系统环境中比较常见,但在嵌入式系统中可能不太适用,因为标准C库的内存管理函数可能不适合嵌入式系统的特定需求。
通常,这种配置不需要特定的宏定义,因为默认情况下LwIP可能就会使用它们(除非明确禁用了它们)。
-
LwIP动态内存堆分配:
LwIP提供了一个自己的内存堆管理实现,它通常用于在没有操作系统或需要更精细控制内存分配的情况下。这种堆管理实现通常会在mem.c和memp.c文件中定义。
为了启用LwIP的内存堆分配,你需要在opt.h中定义相关的宏,例如:
#define MEM_LIBC_MALLOC 0 // 禁用标准C库malloc
#define MEM_USE_POOLS 0 // 禁用内存池分配
// 可能还需要配置MEM_SIZE等宏来定义堆的大小
-
LwIP动态内存池分配:
内存池分配是LwIP中用于优化性能和减少内存碎片化的一种策略。它预先分配了固定大小的内存块,并在需要时从池中分配和释放这些块。
为了启用内存池分配,你需要在opt.h中定义相关的宏,例如:
#define MEM_LIBC_MALLOC 0 // 禁用标准C库malloc
#define MEM_USE_POOLS 1 // 启用内存池分配
// 还需要配置一些其他的宏来定义内存池的大小和数量,例如MEMP_NUM_PBUF等
在opt.h中,你会看到很多与内存管理相关的宏定义,它们允许你精细地控制LwIP的内存使用。这些宏通常包括:
- MEM_LIBC_MALLOC:定义是否使用标准C库进行内存分配。
- MEM_USE_POOLS:定义是否使用LwIP的内存池分配。
- MEM_SIZE:定义用于堆分配的内存大小(如果使用了堆分配)。
- 各种MEMP_NUM_宏:定义各种内存池中的内存块数量。
- PBUF_POOL_SIZE:定义用于pbuf内存池的缓冲区大小。
请注意,具体的宏定义和配置可能因LwIP的不同版本而有所变化。因此,在配置LwIP的内存管理时,最好参考你所使用的LwIP版本的官方文档或源代码中的注释。
2 . 网络接口
在lwIP中,struct netif是用于描述一个硬件网络接口的数据结构。对于单网卡系统,通常只会有一个netif结构体实例。然而,对于多网卡系统,可以创建多个netif结构体实例,每个实例对应一个物理网络接口。
这些netif结构体实例可以通过一个链表来组织,以便lwIP可以遍历所有的网络接口。在struct netif中,如果定义了LWIP_SINGLE_NETIF宏为0(或者未定义),则会包含一个指向下一个netif结构体的指针next,用于构建链表。
/** ETHARP_TABLE_MATCH_NETIF==1: Match netif for ARP table entries.
* If disabled, duplicate IP address on multiple netifs are not supported
* (but this should only occur for AutoIP).
*/
#if !defined ETHARP_TABLE_MATCH_NETIF || defined __DOXYGEN__
#define ETHARP_TABLE_MATCH_NETIF !LWIP_SINGLE_NETIF
#endif
当lwIP需要发送或接收数据时,它会遍历这个链表,找到正确的网络接口来处理数据。例如,当收到一个数据包时,lwIP会检查数据包的源地址或目标地址,以确定应该使用哪个网络接口来处理这个数据包。同样地,当lwIP需要发送一个数据包时,它会选择一个合适的网络接口来发送数据。
/** Generic data structure used for all lwIP network interfaces.
* The following fields should be filled in by the initialization
* function for the device driver: hwaddr_len, hwaddr[], mtu, flags */
struct netif {
#if !LWIP_SINGLE_NETIF
/** pointer to next in linked list */
struct netif *next;
#endif
#if LWIP_IPV4
/** IP address configuration in network byte order */
ip_addr_t ip_addr;
ip_addr_t netmask;
ip_addr_t gw;
#endif /* LWIP_IPV4 */
#if LWIP_IPV6
/** Array of IPv6 addresses for this netif. */
ip_addr_t ip6_addr[LWIP_IPV6_NUM_ADDRESSES];
/** The state of each IPv6 address (Tentative, Preferred, etc).
* @see ip6_addr.h */
u8_t ip6_addr_state[LWIP_IPV6_NUM_ADDRESSES];
#if LWIP_IPV6_ADDRESS_LIFETIMES
/** Remaining valid and preferred lifetime of each IPv6 address, in seconds.
* For valid lifetimes, the special value of IP6_ADDR_LIFE_STATIC (0)
* indicates the address is static and has no lifetimes. */
u32_t ip6_addr_valid_life[LWIP_IPV6_NUM_ADDRESSES];
u32_t ip6_addr_pref_life[LWIP_IPV6_NUM_ADDRESSES];
#endif /* LWIP_IPV6_ADDRESS_LIFETIMES */
#endif /* LWIP_IPV6 */
/** This function is called by the network device driver
* to pass a packet up the TCP/IP stack. */
netif_input_fn input;
#if LWIP_IPV4
/** This function is called by the IP module when it wants
* to send a packet on the interface. This function typically
* first resolves the hardware address, then sends the packet.
* For ethernet physical layer, this is usually etharp_output() */
netif_output_fn output;
#endif /* LWIP_IPV4 */
/** This function is called by ethernet_output() when it wants
* to send a packet on the interface. This function outputs
* the pbuf as-is on the link medium. */
netif_linkoutput_fn linkoutput;
#if LWIP_IPV6
/** This function is called by the IPv6 module when it wants
* to send a packet on the interface. This function typically
* first resolves the hardware address, then sends the packet.
* For ethernet physical layer, this is usually ethip6_output() */
netif_output_ip6_fn output_ip6;
#endif /* LWIP_IPV6 */
#if LWIP_NETIF_STATUS_CALLBACK
/** This function is called when the netif state is set to up or down
*/
netif_status_callback_fn status_callback;
#endif /* LWIP_NETIF_STATUS_CALLBACK */
#if LWIP_NETIF_LINK_CALLBACK
/** This function is called when the netif link is set to up or down
*/
netif_status_callback_fn link_callback;
#endif /* LWIP_NETIF_LINK_CALLBACK */
#if LWIP_NETIF_REMOVE_CALLBACK
/** This function is called when the netif has been removed */
netif_status_callback_fn remove_callback;
#endif /* LWIP_NETIF_REMOVE_CALLBACK */
/** This field can be set by the device driver and could point
* to state information for the device. */
void *state;
#ifdef netif_get_client_data
void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA];
#endif
#if LWIP_NETIF_HOSTNAME
/* the hostname for this netif, NULL is a valid value */
const char* hostname;
#endif /* LWIP_NETIF_HOSTNAME */
#if LWIP_CHECKSUM_CTRL_PER_NETIF
u16_t chksum_flags;
#endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/
/** maximum transfer unit (in bytes) */
u16_t mtu;
#if LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES
/** maximum transfer unit (in bytes), updated by RA */
u16_t mtu6;
#endif /* LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES */
/** link level hardware address of this interface */
u8_t hwaddr[NETIF_MAX_HWADDR_LEN];
/** number of bytes used in hwaddr */
u8_t hwaddr_len;
/** flags (@see @ref netif_flags) */
u8_t flags;
/** descriptive abbreviation */
char name[2];
/** number of this interface. Used for @ref if_api and @ref netifapi_netif,
* as well as for IPv6 zones */
u8_t num;
#if LWIP_IPV6_AUTOCONFIG
/** is this netif enabled for IPv6 autoconfiguration */
u8_t ip6_autoconfig_enabled;
#endif /* LWIP_IPV6_AUTOCONFIG */
#if LWIP_IPV6_SEND_ROUTER_SOLICIT
/** Number of Router Solicitation messages that remain to be sent. */
u8_t rs_count;
#endif /* LWIP_IPV6_SEND_ROUTER_SOLICIT */
#if MIB2_STATS
/** link type (from "snmp_ifType" enum from snmp_mib2.h) */
u8_t link_type;
/** (estimate) link speed */
u32_t link_speed;
/** timestamp at last change made (up/down) */
u32_t ts;
/** counters */
struct stats_mib2_netif_ctrs mib2_counters;
#endif /* MIB2_STATS */
#if LWIP_IPV4 && LWIP_IGMP
/** This function could be called to add or delete an entry in the multicast
filter table of the ethernet MAC.*/
netif_igmp_mac_filter_fn igmp_mac_filter;
#endif /* LWIP_IPV4 && LWIP_IGMP */
#if LWIP_IPV6 && LWIP_IPV6_MLD
/** This function could be called to add or delete an entry in the IPv6 multicast
filter table of the ethernet MAC. */
netif_mld_mac_filter_fn mld_mac_filter;
#endif /* LWIP_IPV6 && LWIP_IPV6_MLD */
#if LWIP_NETIF_USE_HINTS
struct netif_hint *hints;
#endif /* LWIP_NETIF_USE_HINTS */
#if ENABLE_LOOPBACK
/* List of packets to be queued for ourselves. */
struct pbuf *loop_first;
struct pbuf *loop_last;
#if LWIP_LOOPBACK_MAX_PBUFS
u16_t loop_cnt_current;
#endif /* LWIP_LOOPBACK_MAX_PBUFS */
#endif /* ENABLE_LOOPBACK */
};
通过该结构体的定义可以看到,netif是一个链表结构,链表中的元素通过next指针连接。个网络硬件接口对应一个netif结构的变量,当存在多个网络硬件接口的时候,这些netif结构的变量通过 next指针相互连接,构成了一个链表。因此,用户程序应当建立一个全局变量,以维护该链表。
3 . LwIP 的应用程序接口
LwIP提供了一套应用程序接口(API),这些接口使得应用程序能够方便地使用TCP/IP协议栈进行网络通信。
以下是LwIP的主要应用程序接口及其功能的简要介绍:
- Socket API
- 功能:封装了底层的网络操作,为应用层提供了一个类似于伯克利套接字(Berkeley sockets)的接口。
- 文件:Socket.c
- 特点:提供了标准的socket编程接口,如socket(), bind(), listen(), accept(), connect(), send(), recv()等,方便应用程序移植。
- Netconn API
- 功能:在Socket API之上提供了一个更高级的抽象层,使得应用程序能够更加方便地使用TCP/IP协议栈。
- 文件:Api_lib.c
- 特点:封装了底层的socket操作,提供了更简洁的接口,如netconn_new(), netconn_bind(), netconn_listen(), netconn_accept(), netconn_connect(), netconn_send(), netconn_recv()等。
- Raw/Callback API
- 功能:提供了更底层的接口,允许应用程序直接注册回调函数来处理网络事件,如数据接收、发送完成等。
- 特点:适用于需要更精细控制网络操作的应用程序,如实时性要求较高的系统。
- netif API
- 功能:用于管理网络接口,如添加、删除、设置默认网络接口等。
- 函数:netif_add(), netif_remove(), netif_set_default()等。
- 特点:抽象了不同硬件平台的网络接口差异,使得LwIP能够支持多种网络接口类型。
- Pbuf API
- 功能:用于处理网络数据包的内存缓冲区。
- 特点:提供了对数据包内存的分配、释放、引用计数等管理功能,支持链式内存结构,提高了内存使用效率。
- IP/ICMP/IGMP/UDP/TCP等协议API
- 功能:分别用于处理IP、ICMP、IGMP、UDP、TCP等网络协议的操作。
- 特点:这些API提供了对底层网络协议的直接操作接口,允许应用程序根据需要直接处理网络协议相关的操作。
在嵌入式系统中,由于资源有限(如内存、处理器速度等),使用传统的BSD API(如UNIX或Linux中的套接字API)可能会导致资源消耗过多,从而不适合这些环境。为此,LwIP(Lightweight IP)被设计为轻量级的TCP/IP协议栈,特别针对嵌入式系统进行了优化。
LwIP API的优势
LwIP API在设计上充分考虑了嵌入式系统的特点,它与BSD API类似,但操作更为低级,能够充分利用LwIP的内部结构来避免BSD API对系统资源的过度依赖。这一特点使得LwIP API在嵌入式系统中具有显著的优势:
- 减少数据复制:LwIP API的设计避免了在应用程序和协议栈之间频繁地复制数据。通过直接处理内部缓冲区,LwIP API显著减少了数据复制带来的额外开销,提高了系统性能和响应速度。
- 高效的内存管理:LwIP采用了独特的缓冲池(pbuf)系统来管理网络数据包,这种机制使得内存使用更加高效。通过精心设计的内存分配和释放策略,LwIP能够最大程度地减少内存碎片,提高内存利用率。
- 可配置性:LwIP API允许用户根据具体的应用需求进行配置。用户可以选择性地支持TCP、UDP、ICMP等协议,并调整各种参数以优化资源使用。这种灵活性使得LwIP能够适用于各种嵌入式系统场景。
- 实时性能:由于LwIP的轻量级设计和高效的内部机制,它通常能够在嵌入式系统中提供出色的实时性能。这对于需要快速响应网络事件的应用程序来说至关重要。
BSD Socket兼容层
尽管LwIP API具有诸多优势,但BSD套接字API的易用性和普及性仍然不容忽视。为了充分利用现有的代码库和工具,LwIP保留了一个BSD Socket兼容层。这使得开发人员能够轻松地将为BSD套接字编写的应用程序迁移到LwIP环境中,同时享受LwIP带来的性能优势。
3 . LwIP 的应用程序接口
在将LWIP协议栈移植到GD32H7处理器上时,底层驱动的移植工作确实主要集中在两个方面:修改ethernetif.c文件以适配GD32H7的网络硬件,以及编写GD32H7的网络驱动程序。以下是这两部分工作的详细描述:
3.1修改ethernetif.c
文件
ethernetif.c文件是LWIP协议栈提供的一个示例文件,用于连接网络硬件驱动和LWIP的网络协议栈。这个文件需要被修改以适配GD32H7的特定网络硬件接口。主要的工作包括:
-
接口定义:定义用于与GD32H7网络硬件通信的接口函数。这些函数通常包括初始化函数(如low_level_init)、发送函数(如low_level_output)和接收函数(如low_level_input)。
-
初始化函数:在low_level_init函数中,配置GD32H7的网络硬件接口,如设置MAC地址、初始化DMA描述符、配置中断等。
-
发送函数:在low_level_output函数中,实现将数据帧从LWIP缓冲区发送到网络硬件的功能。这通常涉及将数据帧复制到网络硬件的发送缓冲区,并启动发送过程。
-
接收函数:在low_level_input函数中,从网络硬件的接收缓冲区读取数据帧,并将其传递给LWIP的协议栈。这个函数可能会被设置为网络硬件接收中断的处理程序。
3.2编写GD32H7的网络驱动程序
编写GD32H7的网络驱动程序是实现网络底层功能的关键部分。这个驱动程序需要实现网络接口的初始化、收发报文等功能。具体的工作包括:
-
硬件初始化:在驱动程序中,需要配置GD32H7的网络硬件接口,包括设置MAC地址、配置时钟、初始化DMA等。
-
中断处理:网络硬件通常使用中断来通知CPU数据已经到达或发送完成。在驱动程序中,需要编写中断处理程序来响应这些中断,并执行相应的操作(如读取接收缓冲区、清除发送完成标志等)。
-
数据收发:驱动程序需要实现数据的发送和接收功能。发送功能通常涉及将数据帧复制到网络硬件的发送缓冲区,并启动发送过程;接收功能则涉及从网络硬件的接收缓冲区读取数据帧,并将其传递给上层协议栈。
-
错误处理:在网络通信过程中,可能会出现各种错误(如发送失败、接收缓冲区溢出等)。驱动程序需要能够检测这些错误,并采取相应的措施(如重试发送、丢弃接收到的数据帧等)。
-
性能优化:为了提高网络通信的性能,驱动程序可能需要实现一些优化措施,如使用DMA进行数据传输、使用中断合并来减少中断次数等。