WHCSRL 技术网

《Embedded-Lwip开发指南》第5章LWIP速度测试

最近有个网友在询问关于LWIP的速度,本文就LWIP网速做个简单测试。为了对比,本文将使用无系统和有系统两种环境

5.1网络测速工具介绍

不过在测速之前,需要介绍下测速的工具,这里有两个软件:iPerf与jperf

iPerf 是一个跨平台的网络性能测试工具,它支持Win/Linux/Mac/Android/iOS 等平台,iPerf 可以测试TCP 和UDP(我们一般不对UDP 进行测速)带宽质量,iPerf 可以测量最大TCP 带宽,可以具有多种参数进行测试,同时iPerf 还可以报告带宽,延迟抖动和数据包丢失的情况,我们可以利用iPerf的这些特性来测试一些网络设备如路由器,防火墙,交换机等的性能。

在这里插入图片描述

iPerf下载地址

虽然iPerf 很好用,但是它却是命令行格式的软件,对使用测试的人员并不友好,使用者需要记下他繁琐的命令,不过它还有一个图形界面程序叫做JPerf,使用JPerf 程序能简化了复杂命令行参数的构造,而且 它还保存测试结果,并且将测试结果实时图形化出来,更加一目了然,当然,JPerf 也肯定拥有iPerf 的所有功能,本质执行的iperf的功能。因此本文使用JPerf测速。

关于JPerf软件请自行在后文指引下获取,下载JPerf后,解压。

在这里插入图片描述

然后单击jperf.bat即可打开软件。

在这里插入图片描述

值得注意的是,运行该软件还需要Java环境,请自行配置Java环境。

5.2无系统测速(RAW API)

要想使用JPerf测速,必须先实现TCP服务器或客户端。关于TCP理论这里就不在赘述了,网上的资料很多。这里只讲解如何使用RAW API实现TCP服务器。

5.2.1 TCP相关的RAW API

在开始实现TCP服务器之前,我们首先来看一看LwIP中与TCP相关的RAW API函数有哪些。并简单的了解一下其功能。

  • 建立TCP连接的API函数
函数描述
tcp_new()创建TCP的PCB控制块
tcp_bind()绑定服务器的IP和端口号
tcp_listen()监听TCP的PCB控制块
tcp_accepted()通知 LWIP 协议栈一个 TCP 连接被接受了
tcp_conect()连接远端主机,客户端使用
  • 发送TCP数据的API函数
函数描述
tcp_write()构造一个报文并放到控制块的发送缓冲队列中
tcp_sent()控制块 sent 字段注册的回调函数,数据发送成功后被回调
tcp_output()将发送缓冲队列中的数据发送出去
  • 接收 TCP 数据
函数描述
tcp_recv()控制块 recv 字段注册的回调函数,当接收到新数据时被调用
tcp_recved()当程序处理完数据后一定要调用这个函数,通知内核更新接收窗口
  • 轮询函数
函数描述
tcp_poll()控制块 poll 字段注册的回调函数,该函数周期性调用
  • 关闭和中止连接
函数描述
tcp_close()关闭一个 TCP 连接
tcp_err()控制块 err 字段注册的回调函数,遇到错误时被调用
tcp_abort()中断 TCP 连接

在具体实现TCP服务器之前,先配合着下LWIP,关于如何移植LWIP可以参看笔者以前的文章。

移植LWIP(无系统)

笔者这里使用静态IP,并开启TCP模块。

在这里插入图片描述

5.2.2 TCP服务器实现流程

前面了解了TCP所涉及到的API函数,也通过STM32CubeMX打开了相关配置,那么使用这些函数怎么实现一个TCP服务器呢?我们先简单说明一下其基本的流程。

1.新建控制块
使用tcp_new()函数建立一个TCP控制块。

2.绑定控制块
对于服务器来说,新建一个控制快后,需要在控制块上绑定本地IP和端口,以方便客户端的连接。

3.控制块侦听
使用tcp_listen函数,对于服务器来说,需要显性调用tcp_listen函数以使控制块进入监听状态,等待客户端的连接请求。

4.建立连接
在tcp_listen函数进入服务器监听状态后,需要马上使用tcp_accept函数来注册一个接收处理函数,因为一旦有客户端连接请求被成功建立后,服务器就会调用这个处理函数。

5.接受并处理数据
一旦连接成功,accept回调函数会调用tcp_recv函数注册一个接收完成的处理函数。对于服务器来说,接收到了客户端的数据或操作要求,就会调用这一回调函数进行处理。这其实是一个复杂的过程:接收到数据后,首先通知更新接受窗口(使用tcp_recved函数),处理并发送数据(使用tcp_write函数),数据发送成功则清除已发送的数据(使用tcp_sent函数),最后关闭连接(使用函数tcp_close)。

整个流程图所示如下:

在这里插入图片描述

5.2.3 TCP服务器代码实现

前面分析了TCP服务器的实现流程,接下来就是通过前面介绍的API来实现。

首先是TCP服务器的初始化。其实现代码如下:

/**
  * @brief  TCP服务器初始化
  * @param  None
  * @retval res
  */
uint8_t tcp_server_init(void)
{
	uint8_t res = 0;		
	err_t err;  
  
	struct tcp_pcb *tcppcbnew;  	//定义一个TCP服务器控制块
	struct tcp_pcb *tcppcbconn;  	//定义一个TCP服务器控制块
		
  /* 为tcp服务器分配一个tcp_pcb结构体 */
  tcppcbnew = tcp_new();

	if(tcppcbnew)			//创建成功
	{ 
		//将本地IP与指定的端口号绑定在一起,IP_ADDR_ANY为绑定本地所有的IP地址
		err = tcp_bind(tcppcbnew,IP_ADDR_ANY,TCP_SERVER_PORT);	
		if(err==ERR_OK)	//绑定完成
		{
			tcppcbconn=tcp_listen(tcppcbnew); 			//设置tcppcb进入监听状态
			
			//初始化LWIP的tcp_accept的回调函数
			tcp_accept(tcppcbconn,tcp_server_accept); 	
		}
		else 
		{
			res=1;
		}			
	}
	else 
	{
		res=1;
  }
	
	return res;
}
  • 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

可以看到tcp_accept()函数注册了一个回调函数,实现代码如下:

/**
  * @brief  lwIP tcp_accept()的回调函数
  * @param  arg,newpcb, err
  * @retval ret_err
  */
err_t tcp_server_accept(void *arg, struct tcp_pcb *newpcb,err_t err)
{
	err_t ret_err;
	struct tcp_server_struct *es; 
 	LWIP_UNUSED_ARG(arg);
	LWIP_UNUSED_ARG(err);
	tcp_setprio(newpcb,TCP_PRIO_MIN);//设置新创建的pcb优先级
	
	es=(struct tcp_server_struct*)mem_malloc(sizeof(struct tcp_server_struct)); //分配内存
 	
	if(es!=NULL) //内存分配成功
	{
		es->state = ES_TCPSERVER_ACCEPTED;  	//接收连接
		es->pcb = newpcb;
		es->p = NULL;
		
		tcp_arg(newpcb, es);
		tcp_recv(newpcb, tcp_server_recv);	//初始化tcp_recv()的回调函数
		tcp_err(newpcb, tcp_server_error); 	//初始化tcp_err()回调函数
		tcp_poll(newpcb, tcp_server_poll,1);	//初始化tcp_poll回调函数
		tcp_sent(newpcb, tcp_server_sent);  	//初始化发送回调函数
		  
		tcp_server_flag |= 1<<5;				//标记有客户端连上了

		ret_err=ERR_OK;
	}
	else 
	{
		ret_err=ERR_MEM;
	}
	
	return ret_err;
}
  • 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

这个函数中用于与客户端进行数据交互,函数中有注册了接收发送等函数。本文最重要的就是需要接收函数,代码如下:

/**
  * @brief  lwIP tcp_recv()函数的回调函数
  * @param  arg,tpcb, p, err
  * @retval ret_err
  */
err_t tcp_server_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
	err_t ret_err;
	uint32_t data_len = 0;
	struct pbuf *q;
  struct tcp_server_struct *es;
	LWIP_ASSERT("arg != NULL",arg != NULL);
	es=(struct tcp_server_struct *)arg;
	
	if(p == NULL) //从客户端接收到空数据
	{
		es->state = ES_TCPSERVER_CLOSING;//需要关闭TCP 连接了
		es->p = p; 
		ret_err = ERR_OK;
	}
	else if(err != ERR_OK)	//从客户端接收到一个非空数据,但是由于某种原因err!=ERR_OK
	{
		if(p)
		{
			pbuf_free(p);	//释放接收pbuf
		}
		ret_err = err;
	}
	else if(es->state == ES_TCPSERVER_ACCEPTED) 	//处于连接状态
	{
		if(p != NULL)  //当处于连接状态并且接收到的数据不为空时将其打印出来
		{
			memset(tcp_server_recvbuf, 0, TCP_SERVER_RX_BUFSIZE);  //数据接收缓冲区清零
			for(q = p; q != NULL; q = q->next)  //遍历完整个pbuf链表
			{
				//判断要拷贝到TCP_SERVER_RX_BUFSIZE中的数据是否大于TCP_SERVER_RX_BUFSIZE的剩余空间,如果大于
				//的话就只拷贝TCP_SERVER_RX_BUFSIZE中剩余长度的数据,否则的话就拷贝所有的数据
				if(q->len > (TCP_SERVER_RX_BUFSIZE-data_len)) 
				{
					memcpy(tcp_server_recvbuf+data_len,q->payload,(TCP_SERVER_RX_BUFSIZE-data_len));//拷贝数据
				}
				else 
				{
					memcpy(tcp_server_recvbuf+data_len,q->payload,q->len);
				}
				data_len += q->len;  	
				if(data_len > TCP_SERVER_RX_BUFSIZE) 
				{
					break; //超出TCP客户端接收数组,跳出
				}					
			}
			tcp_server_flag |= 1<<6;	//标记接收到数据了

			tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
			pbuf_free(p);  	//释放内存
			ret_err=ERR_OK;
		}
	}
	else//服务器关闭了
	{
		tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
		es->p = NULL;
		pbuf_free(p); //释放内存
		ret_err = ERR_OK;
	}
	return ret_err;
}
  • 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

可以看到,以上函数都是一层一层的调用,都是使用的回调函数。其他相关函数请自行参看源码。这里就不细讲了。

最后再main()函数初始化TCP服务器即可。

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* Enable I-Cache---------------------------------------------------------*/
  SCB_EnableICache();

  /* Enable D-Cache---------------------------------------------------------*/
  SCB_EnableDCache();

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART3_UART_Init();
  MX_LWIP_Init();
  
	/* USER CODE BEGIN 2 */
	tcp_server_init();

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
		MX_LWIP_Process();					//LWIP轮询任务
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
  • 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

然后编译工程,下载到板子中。打开jperf软件,配合好相应参数,其结果如下:

在这里插入图片描述

从上图可以看出,传输速度大约为1-2M左右,还是有点慢的。

那么要想提高LwIP网络传输速度的方法,就要对LwIP的配置进行合适的调整,主要增加内存的Heap Size、内存池大小、TCP报文段数量、最大TCP报文段、TCP发送缓冲区队列的最大长度等。

关于以上参数的修改可通过STM32CubeMX配置。也就是如下选项:

在这里插入图片描述
其对应的文件是lwipopts.h和opt.h,主要的配置参数在opt.h中。

在这里插入图片描述

笔者直接在文件中修改的,修改的参数如下:

//内存堆 heap 大小
#define MEM_SIZE (24*1024)
 
/* memp 结构的 pbuf 数量,如果应用从 ROM 或者静态存储区发送大量数据时这个值应该设置大一点 */
#define MEMP_NUM_PBUF 24
 
/* 最多同时在 TCP 缓冲队列中的报文段数量 */
#define MEMP_NUM_TCP_SEG 150
 
/* 内存池大小 */
#define PBUF_POOL_SIZE 64
 
/* 最大 TCP 报文段, TCP_MSS = (MTU - IP 报头大小 - TCP 报头大小 */
#define TCP_MSS (1500 - 40)
 
/* TCP 发送缓冲区大小(字节) */
#define TCP_SND_BUF (11*TCP_MSS)
 
/* TCP 接收窗口大小 */
#define TCP_WND (11*TCP_MSS)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

修改后再进行编译,测试结果如下:

在这里插入图片描述

对比前文使用的默认参数,可以发现,速度增加明显。大约快了4-5倍。

关于LWIP的性能优化会在后面的章节讲解,本文的重点是测速。

5.3 RT-Thread系统测速(Socket API)

上一节使用RAW API来实现TCP服务器,本节将使用Socket API来实现TCP服务器,关于TCP服务器的实现可参考笔者博文。

TCP服务器实现

实现TCP的服务器代码如下:

#include <rtthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdev.h>
#include <stdio.h>
#include <string.h>

#define SERVER_PORT   8888
#define BUFF_SIZE 4096

static char recvbuff[BUFF_SIZE];

static void net_server_thread_entry(void *parameter)
{

    int sfd, cfd, maxfd, i, nready, n;

    struct sockaddr_in server_addr, client_addr;

    struct netdev *netdev = RT_NULL;

    char sendbuff[] = "Hello client!";

    socklen_t client_addr_len;
    fd_set all_set, read_set;

    //FD_SETSIZE里面包含了服务器的fd
    int clientfds[FD_SETSIZE - 1];

    // 通过名称获取 netdev 网卡对象
    netdev = netdev_get_by_name((char*)parameter);
    if (netdev == RT_NULL)
    {
        rt_kprintf("get network interface device(%%%%s) failed.
", (char*)parameter);
    }

    //创建socket
    if ((sfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        rt_kprintf("Socket create failed.
");
    }

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    //server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 获取网卡对象中 IP 地址信息
    server_addr.sin_addr.s_addr = netdev->ip_addr.addr;

    //绑定socket
    if (bind(sfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
    {
        rt_kprintf("socket bind failed.
");
        closesocket(sfd);
    }
    rt_kprintf("socket bind network interface device(%%%%s) success!
", netdev->name);

    //监听socket
    if(listen(sfd, 5) == -1)
    {
        rt_kprintf("listen error");
    }
    else
    {
        rt_kprintf("listening...
");
    }

    client_addr_len = sizeof(client_addr);

    //初始化 maxfd 等于 sfd
    maxfd = sfd;

    //清空fdset
    FD_ZERO(&all_set);

    //把sfd文件描述符添加到集合中
    FD_SET(sfd, &all_set);

    //初始化客户端fd的集合
    for(i = 0; i < FD_SETSIZE -1 ; i++)
    {
        //初始化为-1
        clientfds[i] = -1;
    }
    while(1)
    {
        //每次select返回之后,fd_set集合就会变化,再select时,就不能使用,
        //所以我们要保存设置fd_set 和 读取的fd_set
        read_set = all_set;
        nready = select(maxfd + 1, &read_set, NULL, NULL, NULL);

        //没有超时机制,不会返回0
        if(nready < 0)
        {
            rt_kprintf("select error 
");

        }

        //判断监听的套接字是否有数据
        if(FD_ISSET(sfd, &read_set))
        {
            //有客户端进行连接了
            cfd = accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len);
            if(cfd < 0)
            {
                rt_kprintf("accept socket error
");
                //继续select
                continue;
            }
            rt_kprintf("new client connect fd = %%%%d
", cfd);

            //把新的cfd 添加到fd_set集合中
            FD_SET(cfd, &all_set);

            //更新要select的maxfd
            maxfd = (cfd > maxfd)?cfd:maxfd;

            //把新的cfd 保存到cfds集合中
            for(i = 0; i < FD_SETSIZE -1 ; i++)
            {
                if(clientfds[i] == -1)
                {
                    clientfds[i] = cfd;
                    //退出,不需要添加
                    break;
                }
            }

            //没有其他套接字需要处理:这里防止重复工作,就不去执行其他任务
            if(--nready == 0)
            {
                //继续select
                continue;
            }
        }

        //遍历所有的客户端文件描述符
        for(i = 0; i < FD_SETSIZE -1 ; i++)
        {
            if(clientfds[i] == -1)
            {
                //继续遍历
                continue;
            }

            //判断是否在fd_set集合里面
            if(FD_ISSET(clientfds[i], &read_set))
            {
                n = recv(clientfds[i], recvbuff, sizeof(recvbuff), 0);
                //rt_kprintf("clientfd %%%%d:  %%%%s 
",clientfds[i], recvbuff);

                if(n <= 0)
                {
                    //从集合里面清除
                    FD_CLR(clientfds[i], &all_set);
                    //当前的客户端fd 赋值为-1
                    clientfds[i] = -1;                }
                else
                {
                    //写回客户端
                    n = send(clientfds[i], sendbuff, strlen(sendbuff), 0);
                    if(n < 0)
                    {
                        //从集合里面清除
                        FD_CLR(clientfds[i], &all_set);

                        //当前的客户端fd 赋值为-1
                        clientfds[i] = -1;
                    }
                }
            }
        }
    }
}

static int server(int argc, char **argv)
{
    rt_err_t ret = RT_EOK;

    if (argc != 2)
    {
        rt_kprintf("bind_test [netdev_name]  --bind network interface device by name.
");
        return -RT_ERROR;
    }

    /* 创建 serial 线程 */
    rt_thread_t thread = rt_thread_create("server",
                                          net_server_thread_entry,
                                          argv[1],
                                          2048,
                                          5,
                                          10);

    /* 创建成功则启动线程 */
    if (thread != RT_NULL)
    {
        rt_thread_startup(thread);
    }
    else
    {
        ret = RT_ERROR;
    }

    return ret;
}


#ifdef FINSH_USING_MSH
#include <finsh.h>
MSH_CMD_EXPORT(server, network interface device test);
#endif /* FINSH_USING_MSH */
  • 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
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211

添加好相应代码进行编译,编译后才能后下载固件,测试结果如下:

在这里插入图片描述

可以看出,其传输速度也在1M左右,相对无系统的环境,其速度相对慢些。

要想提高速度,就配置下LWIP参数,笔者配置的参数如下:

在这里插入图片描述

编译,下载,测试结果如下:

在这里插入图片描述

可以看到其速度还是有所提升的,只是没有无系统时提升的明显。至于原因后面的章节将会具体分析。




资源获取方法

1.长按下面二维码,关注公众号[嵌入式实验楼]
2.在公众号回复关键词[LWIP]获取资料
在这里插入图片描述




欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎

推荐阅读