第5章 LwIP 在嵌入式开发中,如果设备需要联网,通常需要实现TCP/IP的整个功能代码模块。但是TCP/IP非常复杂、庞大,自己独立去实现非常困难。幸运的是,现在有很多开源并免费的TCP/IP帮我们实现了TCP/IP功能。例如嵌入式Linux本身就自带了TCP/IP,而在单片机领域,通常使用LwIP。 12min 5.1初识LwIP 5.1.1LwIP介绍 LwIP全称是Light Weight (轻型)IP,有无操作系统的支持都可以运行。只需十几千字节的RAM和40KB左右的ROM即可运行,非常适合在低端嵌入式系统中使用。 LwIP是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP,目前在嵌入式领域应用广泛。主要特性如下: (1) 支持多网络接口的IP转发功能。 (2) 支持ICMP、DHCP。 (3) 支持扩展UDP。 (4) 支持阻塞控制、RTT 估算和快速转发的TCP。 (5) 提供回调接口(RAW API)。 (6) 支持可选择的Berkeley接口API。 5.1.2源码简析 LwIP的源码可以到官网下载,链接: http://download.savannah.gnu.org/releases/lwip/。读者也可以直接使用本书提供的代码。 1. 代码主目录 本文提供的代码是1.3.2版本,打开Chapter5\01 TCP服务器数据收发实验\lwip_v1.3.2文件夹,可以看到如图5.1的代码主目录。 图5.1代码主目录 doc: LwIP的说明文档。 port: 跟芯片架构相关的文件配置,移植LwIP的部分。通常,每个芯片架构都需要去修改这个文件夹里面的相关代码。 图5.2port目录树 src: LwIP的核心代码部分。 2. port目录 port文件夹存放的主要是跟芯片架构相关的代码,目录结构如图5.2所示。 arch: 这个目录下存放的主要是跟芯片架构相关的文件,通常不需要修改。 Standalone: 这个目录下的ethernetif.c文件是整个LwIP移植的重点。 3. src目录 src目录里面存放的是LwIP的关键源码部分,通常我们不需要修改这个src目录下的文件。整个目录主要包含4个文件夹,如图5.3所示。 api: 提供了两种简单的API: sequential API和socket API。要使用这两个API需要底层操作系统的支持。 core: LwIP的核心代码,包括IP、ICMP、TCP、UDP、DHCP、DNS等核心协议。核心代码可以独立运行,且不需要操作系统支持。 include: 各种头文件,与源码目录对应。 netif: 包括底层网络接口的相关文件,其中部分有效文件已经移到port文件夹中。 图5.3src目录 LwIP源码是非常庞大的,里面有很多细节,本书后面将重点讲驱动和应用部分。其他部分读者可以阅读本书提供的附录A\学习资料\3,LWIP学习资料\LwIP源码详解.pdf文件。 5.1.3系统框架 本文使用的硬件平台STM32F407搭配DP83848芯片与LwIP配合使用,从而实现嵌入式网络通信功能。整个系统的框架和TCP/IP可以对应上,如图5.4所示。本书将从MAC层开始分析源码。 图5.4系统框架 17min 5.2网卡驱动 5.2.1STM32F407以太网控制器 STM32F407自带以太网控制器,提供了可配置、灵活的外设,用以满足客户的各种应用需求。它支持与外部物理层(PHY) 相连的两个工业标准接口: 默认情况下使用的是介质独立接口 (MII,在IEEE 802.3规范中定义)和简化介质独立接口(RMII)。它有多种应用领域,例如交换机、网络接口卡等。遵守以下标准: (1) 支持IEEE 802.3—2002。 (2) 支持IEEE 1588—2008 标准。 (3) AMBA 2.0,用于AHB主/从端口。 (4) RMII联盟的 RMII 规范。 (5) 支持外部PHY接口实现10/100 Mb/s 数据传输速率。 (6) 通过符合IEEE802.3的MII接口与外部快速以太网PHY进行通信。 (7) 支持全双工和半双工操作。 (8) 报头和帧起始数据 (SFD) 在发送路径中插入、在接收路径中删除。 (9) 可逐帧控制CRC和pad自动生成。 (10) 接收帧时可自动去除pad/CRC。 (11) 可编程帧长度,支持高达16KB的巨型帧。 (12) 可编程帧间隔(40~96位时间,以8为步长)。 STM32F407以太网功能如图5.5所示。 图5.5STM32F407以太网 通常STM32F407以太网控制器需要外接PHY芯片,本书配套的开发板使用的是DP83848芯片。 DP83848芯片是美国国家半导体公司生产的一款鲁棒性好、功能全、功耗低的10/100Mb/s单路物理层(PHY)元器件。 5.2.2网卡驱动流程 本书使用STM32F407的以太网控制器、DP83848芯片实现有线网卡网络通信功能。打开本书提供的Chapter5\01_TCP_Server\mdk\LWIP.uvprojx工程文件,网络驱动部分的代码基本在stm32f4x7_eth_bsp.c和stm32f4x7_eth.c两个文件中,整个驱动流程可分为3大部分。 1. 以太网控制器初始化 初始化STM32F407的以太网控制器,初始化DP83848芯片的相关GPIO,设置DMA、网口中断等,入口是ETH_BSP_Config(),代码如下: //Chapter5\01_TCP_Server\USER\DP83848\stm32f4x7_eth_bsp.c21行 void ETH_BSP_Config(void) { RCC_ClocksTypeDef RCC_Clocks; //DP83848芯片相关的GPIO初始化,复用成以太网引脚功能 ETH_GPIO_Config(); //配置以太网接收中断 ETH_NVIC_Config(); // Config NVIC for Ethernet //以太网 DMA 配置 ETH_MACDMA_Config(); // Configure the Ethernet MAC/DMA //设置SysTick时钟 10ms中断 RCC_GetClocksFreq(&RCC_Clocks); SysTick_Config(RCC_Clocks.SYSCLK_Frequency / 100); /*更新SysTick IRQ优先级应高于以太网IRQ */ /*应该在处理以太网数据包时更新本地时间*/ NVIC_SetPriority (SysTick_IRQn,1); } DP83848芯片相关的GPIO初始化和以太网中断设置部分的代码比较简单,读者可以自行查阅代码。本书重点分析DMA配置函数static void ETH_MACDMA_Config(void),代码位于Chapter5\01_TCP_Server\USER\DP83848\stm32f4x7_eth_bsp.c第46行,代码如下: static void ETH_MACDMA_Config(void) { ETH_InitTypeDef ETH_InitStructure; /* 使能 ETHERNET 时钟 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_ETH_MAC | RCC_AHB1Periph_ETH_MAC_Tx | RCC_AHB1Periph_ETH_MAC_Rx,ENABLE); ETH_DeInit();/* 复位AHB Bus的ETHERNET */ ETH_SoftwareReset(); /* ETH 软件复位 */ while (ETH_GetSoftwareResetStatus() == SET); /* 等待软件复位成功 */ /* ETHERNET 配置*/ ETH_StructInit(Ð_InitStructure); /* 中间有一段MAC和DMA设置的代码,很长且比较简单,建议读者自行阅读源码 */ /* 这是最重要的函数,用于初始化以太网和DP83848 */ ETH_Init(Ð_InitStructure,DP83848_PHY_ADDRESS); /* Enable the Ethernet Rx Interrupt */ ETH_DMAITConfig(ETH_DMA_IT_NIS | ETH_DMA_IT_R,ENABLE); } 2. DP83848芯片初始化和配置 ETH_MACDMA_Config最后会调用ETH_Init函数实现以太网和DP83848芯片的相关初始化,代码已经添加注释,位于Chapter5\01_TCP_Server\STM32F4x7_ETH_Driver\src\stm32f4x7_eth.c,读者可以阅读全部源码,这里仅列出关键代码部分,代码如下: //Chapter5\01_TCP_Server\STM32F4x7_ETH_Driver\src\stm32f4x7_eth.c 274行 uint32_t ETH_Init(ETH_InitTypeDef* ETH_InitStruct,uint16_t PHYAddress) { … /* 1.参数校验 */ assert_param(IS_ETH_WATCHDOG(ETH_InitStruct-﹥ETH_Watchdog)); assert_param(IS_ETH_JABBER(ETH_InitStruct-﹥ETH_Jabber)); … //368行 /* 2.DP83848芯片初始化和配置 */ /* 3.先复位DP83848芯片*/ if(!(ETH_WritePHYRegister(PHYAddress,PHY_BCR,PHY_Reset))) { /* 操作超时,返回错误 */ return ETH_ERROR; } /* 等待DP83848芯片复位*/ _eth_delay_(PHY_RESET_DELAY); //如果不是自动协商 if(ETH_InitStruct-﹥ETH_AutoNegotiation != ETH_AutoNegotiation_Disable) { /* 等待linked状态... */ do { timeout++; } while (!(ETH_ReadPHYRegister(PHYAddress,PHY_BSR) & PHY_Linked_Status) && (timeout ﹤ PHY_READ_TO)); /* 如果超时,则返回错误 */ if(timeout == PHY_READ_TO) { return ETH_ERROR; } timeout = 0; /* 使能自动协商 */ if(!(ETH_WritePHYRegister(PHYAddress,PHY_BCR,PHY_AutoNegotiation))) { /* 超时则返回错误 */ return ETH_ERROR; } /* 等待,直到网络自动协商完成 */ do { timeout++; } while (!(ETH_ReadPHYRegister(PHYAddress,PHY_BSR) & PHY_AutoNego_Complete) && (timeout ﹤ (uint32_t)PHY_READ_TO)); /* 超时则返回错误 */ if(timeout == PHY_READ_TO) { return ETH_ERROR; } timeout = 0; /* 读取自动协商的结果 */ RegValue = ETH_ReadPHYRegister(PHYAddress,PHY_SR); /*使用自动协商过程固定的双工模式配置MAC */ if((RegValue & PHY_DUPLEX_STATUS) != (uint32_t)RESET) { /*自动协商后,将以太网双工模式设置为全双工*/ ETH_InitStruct-﹥ETH_Mode = ETH_Mode_FullDuplex; } else { /*自动协商后,将以太网双工模式设置为半双工*/ ETH_InitStruct-﹥ETH_Mode = ETH_Mode_HalfDuplex; } /*以自动协商过程确定的速度配置MAC */ if(RegValue & PHY_SPEED_STATUS) { /*自动协商后将以太网速度设置为10Mb/s*/ ETH_InitStruct-﹥ETH_Speed = ETH_Speed_10M; } else { /*自动协商后将以太网速度设置为100Mb/s*/ ETH_InitStruct-﹥ETH_Speed = ETH_Speed_100M; } /* 当代码运行到这里的时候,DP83848芯片已经可以正常工作,并能和路由器互发数据 */ } … return ETH_SUCCESS; } 3. 中断接收函数 当网卡接收到数据后,会触发ETH中断,我们需要在中断函数中处理数据,代码如下: //Chapter5\01_TCP_Server\Main\stm32f4xx_it.c163 行 void ETH_IRQHandler(void) { /*处理所有收到的frames */ /*检查是否收到任何数据包*/ while(ETH_CheckFrameReceived()) { /*处理收到的以太网数据包*/ LwIP_Pkt_Handle(); } /* Clear the Eth DMA Rx IT pending bits */ ETH_DMAClearITPendingBit(ETH_DMA_IT_R); ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS); } 其中LwIP_Pkt_Handle函数将网络数据包传递到LwIP,代码如下: //Chapter5\01_TCP_Server\USER\LWIP_APP\netconf.c 127行 void LwIP_Pkt_Handle(void) { /*从以太网缓冲区读取收到的数据包,并将其发送到LwIP进行处理*/ ethernetif_input(&netif); } 28min 5.3LwIP初始化 DP83848芯片初始化并配置成功后,STM32F407已经可以通过DP83848芯片和路由器进行数据收发,但是还不能实现网络通信,因为还要初始化LwIP,代码如下: // void LwIP_Init(void) { /*初始化动态内存堆*/ mem_init(); /*初始化内存池*/ memp_init(); //如果已经定义USE_DHCP,则使用动态分配IP的方式,本代码暂时使用静态分配IP的方式 #ifdef USE_DHCP ipaddr.addr = 0; netmask.addr = 0; gw.addr = 0; #else /*静态IP地址在\Chapter5\01_TCP_Server\USER\LWIP_APP\ TCP_SERVER.h 读者可以自行修改*/ Set_IP4_ADDR(&ipaddr,IMT407G_IP); Set_IP4_ADDR(&netmask,IMT407G_NETMASK); Set_IP4_ADDR(&gw,IMT407G_WG); #endif /*netif_add(struct netif *netif,struct ip_addr *ipaddr, struct ip_addr *netmask,struct ip_addr *gw, void *state,err_t (* init)(struct netif *netif), err_t (* input)(struct pbuf *p,struct netif *netif)) 将网络接口添加到netif_list。分配结构netif,并将指向此结构的指针作为第一个参数传递。 使用DHCP时,提供指向已清除的ip_addr结构的指针,或用理智的数字填充它们,否则状态指针可以为NULL 初始化函数指针必须指向用于以太网的netif接口。以下代码说明了它的用法。*/ netif_add(&netif,&ipaddr,&netmask,&gw,NULL,ðernetif_init,ðernet_input); /*注册默认的网络接口*/ netif_set_default(&netif); /*完全配置netif后,必须调用此函数*/ netif_set_up(&netif); } 28min 5.4API LwIP提供3种API: (1) RAW API: 可以不需要操作系统,该接口把协议栈和应用程序放到一个进程里,基于函数回调技术,使用该接口的应用程序可以不用进行连续操作。 (2) NETCONN API: 需要操作系统支持,该接口把接收与处理放在一个线程里。 (3) BSD API: 基于openreadwriteclose模型的UNIX标准API,它的最大特点是使应用程序移植到其他系统时比较容易,但用在嵌入式系统中效率比较低,并且占用资源多。 5.4.1RAW API LwIP提供RAW API,可以把协议栈和应用程序放到一个进程里,该接口基于函数回调机制,适用于没有运行操作系统的场合,但是编程难度较高,需要读者熟悉函数回调机制原理。 1. PCB PCB全称Protocol Control Block,中文名为协议控制块。RAW API的所有函数都基于PCB,通过PCB进行网络通信,在功能上类似于socket套接字。 根据传输协议,PCB又可分为TCP PCB和UDP PCB两种。 2. tcp_new 用户可以使用tcp_new函数创建一个TCP PCB,函数将返回一个struct tcp_pcb接口体指针。 函数代码如下: // Chapter5\01_TCP_Server\lwip_v1.3.2\src\core\ tcp.c1090行 struct tcp_pcb *tcp_new(void) { return tcp_alloc(TCP_PRIO_NORMAL); } 3. tcp_bind tcp_bind将TCP PCB绑定到本地端口号和IP地址。函数代码如下: // Chapter5\01_TCP_Server\lwip_v1.3.2\src\core\ tcp.c276行 err_t tcp_bind(struct tcp_pcb *pcb,struct ip_addr *ipaddr,u16_t port) 参数: struct tcp_pcb*pcb: 需要绑定的TCP PCB,由tcp_new函数创建。tcp_bind不检查此pcb是否已绑定。 struct ip_addr*ipaddr: 绑定到本地IP地址,使用IP_ADDR_ANY绑定到任何本地地址。 u16_t port: 本地端口。 返回值: ERR_USE: 端口已被占用。 ERR_OK: 绑定成功。 4. tcp_listen tcp_listen用于设置TCP PCB为可连接状态,通常作为服务器的一方需要调用此函数,一旦调用,则意味着客户端已经可以开始使用TCP连接服务器了。 tcp_listen函数的定义如下: //Chapter5\01_TCP_Server\lwip_v1.3.2\src\include\lwip\tcp.h 100行 #define tcp_listen(pcb) tcp_listen_with_backlog(pcb,TCP_DEFAULT_LISTEN_BACKLOG) 可以看到,tcp_listen函数最后调用的是tcp_listen_with_backlog函数,它的函数代码如下: // Chapter5\01_TCP_Server\lwip_v1.3.2\src\core\ tcp.c366行 struct tcp_pcb *tcp_listen_with_backlog(struct tcp_pcb *pcb,u8_t backlog) 参数: (1) struct tcp_pcb*pcb: 原始的tcp_pcb。 (2) u8_t backlog: 连接队列最大限制。 返回值: struct tcp_pcb*: 返回一个新的已处于监听状态的TCP PCB。需要注意的是,原始的tcp_pcb将会被释放。因此,必须这样使用该函数: tpcb = tcp_listen(tpcb)。 5. tcp_connect tcp_connect函数用于连接到服务器,并设置连接成功时的回调函数,通常客户端调用此函数,其函数定义如下: // Chapter5\01_TCP_Server\lwip_v1.3.2\src\core\ tcp.c513行 err_ttcp_connect(struct tcp_pcb *pcb,struct ip_addr *ipaddr,u16_t port, err_t (* connected)(void *arg,struct tcp_pcb *tpcb,err_t err)) 参数: (1) struct tcp_pcb*pcb: 需要设置的TCP PCB。 (2) struct ip_addr*ipaddr: 服务器的IP地址。可以定义一个struct ip_addr结构体,然后使用IP4_ADDR(&ipaddr,a,b,c,d)函数设置IP,例如服务器的IP是192.168.1.100,可以使用IP4_ADDR(&ipaddr, 192, 168, 1, 100)进行设置。 (3) u16_t port: 服务器端口号。 (4) err_t (* connected)(void *arg,struct tcp_pcb *tpcb,err_t err): 连接成功时的回调函数。读者需要自己实现该函数,本书提供了一个简单的TCP_Connected函数供参考,其函数代码如下: // Chapter5\03 TCP_Client\USER\LWIP_APP\TCP_CLIENT.C66行 err_t TCP_Connected(void *arg,struct tcp_pcb *pcb,err_t err) { //tcp_client_pcb = pcb; return ERR_OK; } 6. tcp_accept tcp_accept用于设置有连接请求时的回调函数,通常服务器调用此函数。其函数定义如下: // Chapter5\01_TCP_Server\lwip_v1.3.2\src\core\ tcp.c1160行 voidtcp_accept(struct tcp_pcb *pcb, err_t {* accept)(void *arg, struct tcp_pcb *newpcb, err_t err)) { pcb-﹥accept = accept; } 参数: (1) struct tcp_pcb *pcb: 需要设置的TCP PCB。 (2) err_t {* accept)(void *arg, struct tcp_pcb *newpcb, err_t err))回调函数,用户必须自己实现该函数。当有连接请求时,LwIP会调用该回调函数,处理连接请求。 本书提供了一个tcp_server_accept回调函数,读者可以参考,其函数代码如下: //Chapter5\01_TCP_Server\USER\LWIP_APP\TCP_SERVER.C47行 /********************************************************************************** 名称:tcp_server_accept(void *arg,struct tcp_pcb *pcb,struct pbuf *p,err_t err) 功能:回调函数。 说明:这是一个回调函数,当一个连接已经接受时会被调用 **********************************************************************************/ static err_t tcp_server_accept(void *arg,struct tcp_pcb *pcb,err_t err) { //设置回调函数的优先级,当存在几个连接时特别重要,此函数必须被调用 tcp_setprio(pcb,TCP_PRIO_MIN); //设置TCP数据接收回调函数,当有网络数据时,tcp_server_recv会被调用 tcp_recv(pcb,tcp_server_recv); err = ERR_OK; return err; } 7. tcp_recv tcp_recv用于设置TCP数据接收回调函数,其函数代码如下: //Chapter5\01_TCP_Server\lwip_v1.3.2\src\core\tcp.c 1116行 void tcp_recv(struct tcp_pcb *pcb, err_t (* recv)(void *arg,struct tcp_pcb *tpcb,struct pbuf *p,err_t err)) { pcb-﹥recv = recv; } 参数: (1) struct tcp_pcb *pcb: 需要设置的TCP PCB。 (2) err_t (* recv)(void *arg,struct tcp_pcb *tpcb, struct pbuf *p,err_t err): 接收回调函数,用户必须自己实现该函数,当有网络数据时,接收回调函数被调用。 本书提供了一个tcp_server_recv回调函数例程,读者可以参考,其函数代码如下: //Chapter5\01_TCP_Server\USER\LWIP_APP\TCP_SERVER.C 17行 static err_t tcp_server_recv(void *arg,struct tcp_pcb *pcb,struct pbuf *p,err_t err) { //定义一个pbuf指针变量,指向传入的参数p。p接收网络数据并缓存 struct pbuf *p_temp = p; //如果数据不为空 if(p_temp != NULL) { //读取数据 tcp_recved(pcb,p_temp-﹥tot_len); //如果数据不为空 while(p_temp != NULL) { //把收到的数据重新发送给客户端 tcp_write(pcb,p_temp-﹥payload,p_temp-﹥len,TCP_WRITE_FLAG_COPY); //启动发送 tcp_output(pcb); //获取下一个数据包 p_temp = p_temp-﹥next; } } else //数据为空,说明接收失败,可能网络异常或者客户端已断开连接 { //关闭连接 tcp_close(pcb); } //释放内存 pbuf_free(p); //返回OK err = ERR_OK; return err; } 8. RAW API流程图 将LwIP的RAW API和socket接口做比较,可以看到两者在编程方式上非常接近,其流程如图5.6所示。 图5.6RAW API和socket流程 读者可以使用本书提供的01_TCP_Server工程文件,根据自己的需求修改tcp_server_recv回调函数内容。 5.4.2NETCONN API 在NETCONN接口中,无论UDP还是TCP都统一使用一个连接结构netconn,这样应用程序就可以使用统一的连接结构和编程函数。 1. netconn_new netconn又称为连接结构体。一个连接结构体中包含的成员变量很多,如连接的类型和连接的状态,对应的控制块及一些记录的信息。netconn结构体的定义位于api.h文件。代码如下: struct netconn { /** netconn 类型(TCP,UDP or RAW) */ enum netconn_type type; /** netconn当前状态 */ enum netconn_state state; /** LwIP 内部协议控制块 */ union { struct ip_pcb *ip; struct tcp_pcb *tcp; struct udp_pcb *udp; struct raw_pcb *raw; } pcb; /** netconn最后一个错误 */ err_t last_err; #if !LWIP_NETCONN_SEM_PER_THREAD /**用于在内核上下文中同步执行功能*/ sys_sem_t op_completed; #endif /** mbox :接收包的 mbox,直到它们被 netconn 应用程序线程获取(可以变得非常大) */ sys_mbox_t recvmbox; #if LWIP_TCP /** mbox在应用程序线程处理之前,将新连接存储在这个mbox*/ sys_mbox_t acceptmbox; #endif /* LWIP_TCP */ /**仅用于套接字层,通常不使用*/ #if LWIP_SOCKET int socket; #endif /* LWIP_SOCKET */ #if LWIP_SO_SNDTIMEO /**超时等待发送数据,以毫秒为间隔(这意味着将数据以内部缓冲区的形式发送) */ s32_t send_timeout; #endif /* LWIP_SO_RCVTIMEO */ #if LWIP_SO_RCVTIMEO /**超时等待接收新数据,以毫秒为间隔(或连接到侦听netconns的连接) */ int recv_timeout; #endif /* LWIP_SO_RCVTIMEO */ #if LWIP_SO_RCVBUF /** recvmbox中排队的最大字节数 未用于TCP:请改为调整TCP_WND */ int recv_bufsize; /**当前在recvmbox中要接收的字节数, 针对recv_bufsize测试以限制recvmbox上的字节 用于UDP和RAW,用于FIONREAD */ int recv_avail; #endif /* LWIP_SO_RCVBUF */ #if LWIP_SO_LINGER /**值﹤0表示禁用延迟,值﹥ 0表示延迟数秒*/ s16_t linger; #endif /* LWIP_SO_LINGER */ /**更多netconn内部状态的标志,请参见NETCONN_FLAG_ *定义*/ u8_t flags; #if LWIP_TCP /** TCP:当传递给netconn_write的数据不适合发送缓冲区时, 暂时存储已发送的数量*/ size_t write_offset; /** TCP:当传递给netconn_write的数据不适合发送缓冲区时, 此时暂时存储消息。 在连接和关闭期间也使用*/ struct api_msg *current_msg; #endif /* LWIP_TCP */ /**通知此netconn事件的回调函数*/ netconn_callback callback; }; 用户可以使用netconn_new函数创建一个netconn结构体,其函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\include\lwip\ //api.h293行 #define netconn_new(t) netconn_new_with_proto_and_callback(t,0,NULL) 可以看到,netconn_new是一个宏,最终调用的是netconn_new_with_proto_and_callback函数,其函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\api_lib.c //68行 struct netconn*netconn_new_with_proto_and_callback(enum netconn_type t,u8_t proto,netconn_callback callback) 参数: (1) enum netconn_type t: 创建的连接类型,通常的连接类型是TCP或者UDP,其取值可以是netconn_type枚举中的任何一个。netconn_type枚举代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\include\lwip\ //api.h83行 /** Protocol family and type of the netconn */ enum netconn_type { NETCONN_INVALID = 0, /* NETCONN_TCP Group */ NETCONN_TCP = 0x10, /* NETCONN_UDP Group */ NETCONN_UDP = 0x20, NETCONN_UDPLITE = 0x21, NETCONN_UDPNOCHKSUM= 0x22, /* NETCONN_RAW Group */ NETCONN_RAW = 0x40 }; (2) u8_t proto: 原始RAW IP pcb的IP,通常写0即可。 (3) netconn_callback callback: 设置状态发生改变时的回调函数,通常不需要设置。 返回: struct netconn*: 返回创建的netconn结构体。 通常我们只需要使用netconn_new函数即可,传入的参数为创建的连接类型。 2. netconn_delete netconn_delete用于删除netconn结构体,并释放内存。当客户端断开连接后,用户一定要调用该函数删除并释放netconn资源,否则会引起内存泄漏。netconn_delete函数的代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\api_lib.c //103行 err_tnetconn_delete(struct netconn *conn) 参数: struct netconn *conn: 需要删除并释放资源的netconn结构体。 返回: 如果删除成功,返回ERR_OK。 3. netconn_bind netconn_bind用于绑定netconn结构体的IP地址和端口号,其函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\api_lib.c //166行 err_tnetconn_bind(struct netconn *conn,ip_addr_t *addr,u16_t port) 参数: (1) struct netconn *conn: 需要绑定的netconn结构体。 (2) ip_addr_t *addr: 需要绑定的IP地址。可以使用IP_ADDR_ANY绑定本机的所有IP地址。 (3) u16_t port: 需要绑定的端口号。 返回: err_t: 返回ERR_OK则表示绑定成功。 4. netconn_listen netconn_listen函数用于开始监听客户端连接,通常服务器才会使用该函数,其函数实际上是一个宏定义,代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\include\lwip\ //api.h313行 #define netconn_listen(conn) netconn_listen_with_backlog(conn,TCP_DEFAULT_LISTEN_BACKLOG) 最终调用的是netconn_listen_with_backlog函数,该函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\api_lib.c //351行 err_tnetconn_listen_with_backlog(struct netconn *conn,u8_t backlog) 参数: (1) struct netconn *conn: 需要监听的netconn结构体。 (2) u8_t backlog: 连接队列最大限制。 返回: err_t: 返回ERR_OK则表示成功设置为监听状态。 5. netconn_connect netconn_connect函数用于连接到服务器,通常客户端使用该函数,其函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\api_lib.c //294行 err_tnetconn_connect(struct netconn *conn,const ip_addr_t *addr,u16_t port) 参数: (1) struct netconn *conn: netconn结构体指针。 (2) const ip_addr_t *addr: 服务器的IP地址。可以定义一个struct ip_addr结构体,使用IP4_ADDR(&ipaddr,a,b,c,d)函数设置IP,例如服务器的IP是192.168.1.100,可以使用IP4_ADDR(&ipaddr,192,168,1,100)进行设置。 (3) u16_t port: 服务器端口号。 返回: err_t: 返回ERR_OK则表示成功连接到服务器。 6. netconn_accept netconn_accept通常由服务器使用,当有新的客户端发起连接请求时,netconn_accept将会返回,其函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\api_lib.c //388行 err_tnetconn_accept(struct netconn *conn, struct netconn **new_conn) 参数: (1) struct netconn *conn: 服务器最初通过netconn_new创建netconn结构体指针。 (2) struct netconn **new_conn: 新的客户端连接时,将产生一个新的netconn结构体指针,后续该客户端的数据发送和接收都必须使用新的netconn结构体指针。 返回: err_t: 返回ERR_OK则表示有新的客户端连接。 7. netconn_recv netconn_recv用于从网络中接收数据,其函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\api_lib.c //620行 err_tnetconn_recv(struct netconn *conn, struct netbuf **new_buf) 参数: (1) struct netconn *conn: 必须在新的客户端连接时产生一个新的netconn结构体指针。 (2) struct netbuf **new_buf: struct netbuf结构体指针的指针,用来指向接收到的数据。 返回: err_t: 返回ERR_OK则表示接收数据成功。 8. netbuf_datA netbuf_data函数用来从netbuf结构体中获取指定长度的数据,通常netconn_recv函数只是获取netbuf结构体指针,具体的数据内容需要再次使用netbuf_data函数获取,其函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\netbuf.c //192行 err_tnetbuf_data(struct netbuf *buf, void **dataptr, u16_t *len) 参数: (1) struct netbuf *buf: 指定要获取数据的netbuf。 (2) void **dataptr: 获取数据后存放的缓存。 (3) u16_t *len: 要获取的数据长度。 返回: err_t: 返回ERR_OK则表示获取数据成功。 9. netconn_write netconn_write函数用于向网络发送数据,其代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\include\lwip\ //api.h323行 #define netconn_write(conn, dataptr, size, apiflags) \ netconn_write_partly(conn, dataptr, size, apiflags, NULL) 最终会调用netconn_write_partly函数,netconn_write_partly函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\api_lib.c //734行 err_tnetconn_write_partly(struct netconn *conn,const void *dataptr,size_t size,u8_t apiflags,size_t *bytes_written) 参数: (1) struct netconn *conn: 必须在新的客户端连接时所产生一个新的netconn结构体指针。 (2) const void *dataptr: 要发送的数据缓存。 (3) size_t size: 发送的数据长度。 (4) u8_t apiflags: 此参数可使用以下数值。 NETCONN_COPY: 数据将被复制到属于堆栈的内存中。 NETCONN_MORE: 对于TCP连接,将在发送的最后一个数据段上设置PSH标志。 NETCONN_DONTBLOCK: 仅在可以一次写入所有数据时才写入数据。 (5) bytes_writing: 指向接收写入字节数的位置的指针,通常我们置NULL即可。 返回: err_t: 返回ERR_OK则表示发送数据成功。 10. netconn_close netconn_close用于关闭连接,其函数代码如下: //Chapter5\02_rt-thread3.1.1-lwip2.0.2\components\net\lwip-2.0.2\src\api\api_lib.c //837行 err_tnetconn_close(struct netconn *conn) 参数: struct netconn *conn: 需要关闭连接的netconn结构体指针。 返回: err_t: 返回ERR_OK则表示关闭成功。 5.4.3BSD API LwIP还提供一套基于openreadwriteclose模型的UNIX标准API。其函数有socket、bind、recv、send等。但是由于BSD API接口需要占用过多的资源,在嵌入式中基本不使用,故而本书不介绍BSD API的各类函数,如果读者有兴趣可以自行翻看socket相关的UNIX标准API。 5.5LwIP实验 本节将分别介绍基于RAW API和NETCONN API的服务器、客户端的代码实现。 5.5.1RAW API TCP服务器实验 打开Chapter5\01_TCP_Server\mdk\LWIP.uvprojx工程文件,与服务器相关的代码位于TCP_SERVER.C文件中。 1. 初始化TCP服务器 void TCP_server_init(void) { struct tcp_pcb *pcb; //创建新的tcp pcb pcb = tcp_new(); //绑定本机所有IP和TCP_Server_PORT端口,TCP_Server_PORT定义值为2040 tcp_bind(pcb,IP_ADDR_ANY,TCP_Server_PORT); //开始监听 pcb = tcp_listen(pcb); //设置连接回调函数 tcp_accept(pcb,tcp_server_accept); } 2. tcp_server_accept回调函数 //Chapter5\01_TCP_Server\USER\LWIP_APP\TCP_SERVER.C47行 /********************************************************************************** 名称:tcp_server_accept(void *arg,struct tcp_pcb *pcb,struct pbuf *p,err_t err) 功能:回调函数 说明:这是一个回调函数,当一个连接已经接受时会被调用 **********************************************************************************/ static err_t tcp_server_accept(void *arg,struct tcp_pcb *pcb,err_t err) { //设置回调函数的优先级,当存在几个连接特别重要,此函数必须被调用 tcp_setprio(pcb,TCP_PRIO_MIN); //设置TCP数据接收回调函数,当有网络数据时,tcp_server_recv会被调用 tcp_recv(pcb,tcp_server_recv); err = ERR_OK; return err; } 3. tcp_server_recv回调函数 //Chapter5\01_TCP_Server\USER\LWIP_APP\TCP_SERVER.C17行 static err_t tcp_server_recv(void *arg,struct tcp_pcb *pcb,struct pbuf *p,err_t err) { //定义一个pbuf指针变量,指向传入的参数p。p接收网络数据并缓存 struct pbuf *p_temp = p; //如果数据不为空 if(p_temp != NULL) { //读取数据 tcp_recved(pcb,p_temp-﹥tot_len); //如果数据不为空 while(p_temp != NULL) { //把收到的数据重新发送给客户端 tcp_write(pcb,p_temp-﹥payload,p_temp-﹥len,TCP_WRITE_FLAG_COPY); //启动发送 tcp_output(pcb); //获取下一个数据包 p_temp = p_temp-﹥next; } } else//数据为空,说明接收失败,可能网络异常或者客户端已断开连接 { //关闭连接 tcp_close(pcb); } //释放内存 pbuf_free(p); //返回OK err = ERR_OK; return err; } 4. 开发板IP和MAC设置 本书提供的例程采用静态IP地址设置,读者需要根据自己的实际情况修改,其代码如下: //Chapter5\01_TCP_Server\USER\LWIP_APP\TCP_SERVER.h7行 /******************* *******************************/ //开发板的IP地址 #define IMT407G_IP192,168,0,107 //子网掩码 #define IMT407G_NETMASK 255,255,255,0 //网关的IP地址 #define IMT407G_WG 192,168,0,1 //开发板的MAC地址 #define IMT407G_MAC_ADDR 0XD8,0XCB,0X8A,0X82,0X50,0XD1 //服务器端口号 #define TCP_Server_PORT2040 5. 测试 (1) 确保开发板和计算机使用网线都连接到同一个路由器,确保计算机可以ping通开发板IP。 (2) 打开Chapter5\01_TCP_Server\mdk\LWIP.uvprojx工程文件,编译并下载程序。 (3) 打开附录A\软件\串口工具\scom5.13.1.exe程序,端口号选择TCPClient,远程输入开发板的IP地址,本书测试环境的IP地址是192.168.0.107,读者需要根据TCP_SERVER.h中填写的开发板IP地址填写。IP地址后面的方框内填写2040,单击“连接”按钮,计算机此时与开发板建立起TCP连接,如图5.7所示。 图5.7TCP服务器实验 (4) 此时在输入框输入任意字符串,单击“发送”按钮,可以看到接收框收到相同的字符串,通信成功。 5.5.2RAW API TCP客户端实验 打开Chapter5\03 TCP_Client\mdk\LWIP.uvprojx工程文件,客户端相关的代码位于TCP_CLIENT.C文件中。 1. 初始化TCP客户端 //Chapter5\03 TCP_Client\USER\LWIP_APP111行 void TCP_Client_Init(u16_t local_port,u16_t remote_port,unsigned char a,unsigned char b,unsigned char c,unsigned char d) { struct ip_addr ipaddr; err_t err; //a b c d代表了服务器IP地址,这里使用IP4_ADDR构造服务器IP的结构体 IP4_ADDR(&ipaddr,a,b,c,d); //获取一个新的 tcp pcb tcp_client_pcb = tcp_new(); if (!tcp_client_pcb) { return ; } //绑定开发板的IP地址、端口号 err = tcp_bind(tcp_client_pcb,IP_ADDR_ANY,local_port); //如果绑定失败则退出 if(err != ERR_OK) { return ; } //连接到服务器,并设置连接成功的回调函数 tcp_connect(tcp_client_pcb,&ipaddr,remote_port,TCP_Connected); //设置接收网络数据的回调函数 tcp_recv(tcp_client_pcb,TCP_Client_Recv); } 2. 客户端连接成功的回调函数 //Chapter5\03 TCP_Client\USER\LWIP_APP65行 err_t TCP_Connected(void *arg,struct tcp_pcb *pcb,err_t err) { //tcp_client_pcb = pcb; return ERR_OK; } 3. 客户端发送数据 //Chapter5\03 TCP_Client\USER\LWIP_APP20行 err_t TCP_Client_Send_Data(struct tcp_pcb *cpcb,unsigned char *buff,unsigned int length) { err_t err; err = tcp_write(cpcb,buff,length,TCP_WRITE_FLAG_COPY); tcp_output(cpcb); return err; } 4. 开发板IP和MAC设置 本书提供的例程采用静态IP地址设置,读者需要根据自己的实际情况修改,其代码如下: //Chapter5\03 TCP_Client\USER\LWIP_APP\TCP_CLIENT.h7行 /****************************************/ //开发板IP地址 #define IMT407G_IP192,168,0,107 //子网掩码 #define IMT407G_NETMASK 255,255,255,0 //网关 #define IMT407G_WG 192,168,0,1 //开发板MAC地址 #define IMT407G_MAC_ADDR 0XD8,0XCB,0X8A,0X82,0X50,0XD1 //开发板的端口号 #define TCP_LOCAL_PORT 2040 //服务器端口号 #define TCP_Server_PORT 2041 //服务器(计算机)IP地址 #define TCP_Server_IP 192,168,0,106 5. 测试 (1) 确保开发板和计算机使用网线都连接到同一个路由器,确保计算机可以ping通开发板IP。 (2) 打开附录A\软件\串口工具\scom5.13.1.exe程序,端口号选择TCPServer,本地一栏选择计算机对应的IP地址,后面的方框内填写2041,单击“侦听”按钮。 (3) 打开Chapter5\03 TCP_Client\mdk\LWIP.uvprojx工程文件,编译并下载。 (4) 此时可以看到接收框收到客户端发送过来的数据“\0TCP 客户端实验!”,通信成功,如图5.8所示。 图5.8TCP客户端实验 5.5.3RAW API UDP服务器实验 打开Chapter5\04 UDP_server\mdk\LWIP.uvprojx工程文件,服务器相关的代码位于UDP_SERVER.C文件中。 1. UDP服务器初始化 //Chapter5\04 UDP_server\USER\LWIP_APP\UDP_SERVER.C37行 void UDP_server_init(void) { struct udp_pcb *pcb; //获取一个udp pcb pcb = udp_new(); //绑定服务器端口号 udp_bind(pcb,IP_ADDR_ANY,UDP_LOCAL_PORT); //设置接收回调函数 udp_recv(pcb,udp_server_recv,NULL); } 2. 接收回调函数 //Chapter5\04 UDP_server\USER\LWIP_APP\UDP_SERVER.C18行 void udp_server_recv(void *arg,struct udp_pcb *pcb,struct pbuf *p,struct ip_addr *addr,u16_t port) { //获取客户端的IP地址 struct ip_addr destAddr = *addr; · struct pbuf *p_temp = p; //while(p_temp != NULL) //{ //把收到的数据重新发送给客户端 udp_sendto(pcb,p_temp,&destAddr,port); p_temp = p_temp-﹥next; //} //释放内存 pbuf_free(p); } 3. 开发板IP地址设置 //Chapter5\01_TCP_Server\USER\LWIP_APP\UDP_SERVER.h7行 /********************** ****************************/ //开发板的IP地址 #define IMT407G_IP192,168,0,107 //子网掩码 #define IMT407G_NETMASK 255,255,255,0 //网关的IP地址 #define IMT407G_WG 192,168,0,1 //开发板的MAC地址 #define IMT407G_MAC_ADDR 0XD8,0XCB,0X8A,0X82,0X50,0XD1 //服务器端口号 #define TCP_Server_PORT 2040 4. 实验 (1) 确保开发板和计算机使用网线都连接到同一个路由器,确保计算机可以ping通开发板IP。 (2) 打开Chapter5\04 UDP_server\mdk\LWIP.uvprojx工程文件,编译并下载程序。 (3) 打开附录A\软件\串口工具\scom5.13.1.exe程序,端口号选择UDP。远程一栏填写开发板IP地址和端口号,本地一栏选择计算机对应的IP地址和端口号,单击“连接”按钮。 (4) 此时在输入框输入任意字符串,单击“发送”按钮,可以看到接收框收到相同的字符串,通信成功,如图5.9所示。 图5.9UDP服务器实验 5.5.4RAW API UDP客户端实验 打开Chapter5\05_UDP_client\mdk\LWIP.uvprojx工程文件,客户端相关的代码位于UDP_CLIENT.C文件中。 1. UDP客户端初始化 //Chapter5\05_UDP_client\USER\LWIP_APP\UDP_CLIENT.C10行 //客户端要发送的数据内容 const static unsigned char UDPData[]="UDP客户端实验\r\n"; //相关变量定义 struct udp_pcb *udp_pcb; struct ip_addr ipaddr; struct pbuf *udp_p; //Chapter5\05_UDP_client\USER\LWIP_APP\UDP_CLIENT.C23行 //客户端初始化 void UDP_client_init(void) { //分配一个pbuf udp_p = pbuf_alloc(PBUF_RAW,sizeof(UDPData),PBUF_RAM); //设置要发送的数据为UDPData udp_p -﹥ payload = (void *)UDPData; //设置服务器IP地址 Set_IP4_ADDR(&ipaddr,UDP_REMOTE_IP); //创建一个udp pcb udp_pcb = udp_new(); //绑定开发的IP和开发板(客户端)端口号 udp_bind(udp_pcb,IP_ADDR_ANY,UDP_Client_PORT); //连接到服务器 udp_connect(udp_pcb,&ipaddr,UDP_REMOTE_PORT); } 2. 客户端发送函数 //Chapter5\05_UDP_client\USER\LWIP_APP\UDP_CLIENT.C40行 void UDP_Send_Data(struct udp_pcb *pcb,struct pbuf *p) { //将参数传进来的pcb 和pbuf,通过udp_send发送出去 udp_send(pcb,p); //需要延时,不要发得太快 delay_ms(100); } 3. main函数 //Chapter5\05_UDP_client\Main\main.c51行 //循环发送 while (1) { //循环将UDP_client_init初始化好的udp pcb 和 udp_p发送出去 UDP_Send_Data(udp_pcb,udp_p); LwIP_Periodic_Handle(LocalTime); } 4. 开发板IP地址设置 //Chapter5\05_UDP_client\USER\LWIP_APP\UDP_CLIENT.h7行 /**************************************************/ //开发板的IP地址 #define IMT407G_IP192,168,0,107 //子网掩码 #define IMT407G_NETMASK 255,255,255,0 //网关的IP地址 #define IMT407G_WG 192,168,0,1 //开发板的MAC地址 #define IMT407G_MAC_ADDR 0XD8,0XCB,0X8A,0X82,0X50,0XD1 //客户端端口号 #define UDP_Client_PORT 2040 //服务器端口号 #define UDP_REMOTE_PORT 2041 //服务器IP地址 #define UDP_REMOTE_IP 192,168,0,101 5. 实验 (1) 确保开发板和计算机使用网线都连接到同一个路由器,确保计算机可以ping通开发板IP。 (2) 打开附录A\软件\串口工具\scom5.13.1.exe程序,端口号选择UDP,本地一栏选择计算机对应的IP地址,后面的方框内填写2041,单击“连接”按钮。 (3) 打开Chapter5\05_UDP_client\mdk\LWIP.uvprojx工程文件,编译并下载。 (4) 此时可以看到接收框收到客户端发送过来的数据“\0UDP客户端实验例程!”,通信成功,如图5.10所示。 图5.10UDP客户端实验 5.5.5NETCONN API实验 NETCONN API需要操作系统支持,本书将在第6章讲到RTOS实时操作系统时再介绍NETCONN API的用法,本小节不做介绍。