Embedded-Lwip Development Guide Chapter 5 LWIP Speed Measurement

Keywords: lwip TCP/IP

Recently, a netizen is asking about the speed of LWIP. This article will do a simple test on the speed of LWIP. For comparison, this article will use both non-system and system environments.

Introduction of 5.1 Network Speed Measuring Tool

However, before measuring speed, you need to introduce the following speed measuring tools, there are two software: iPerf and jperf.

The iPerf is a cross-platform network performance testing tool that supports platforms such as Win/Linux/Mac/Android/iOS. The iPerf can test TCP and UDP (we don't normally speed UDP)Bandwidth quality, iPerf can measure maximum TCP bandwidth, can be tested with a variety of parameters, and iPerf can also report bandwidth, delay jitter and packet loss. We can use these characteristics of iPerf to test the performance of some network devices such as routers, firewalls, switches, etc.

iPerf download address

Although the iPerf is useful, it is a command-line format software that is not friendly to testers and users need to write down their tedious commands. However, there is a graphical interface program called JPerf, which simplifies the construction of complex command-line parameters, andIt also saves the test results and visualizes them in real time to make it more clear. Of course, JPerf certainly has all the functions of the iPerf, which is essentially the functions of the iperf. Therefore, this paper uses JPerf speed measurement.

For JPerf software, please get it by yourself under the instructions below. After downloading JPerf, decompress it.

Then click jperf.bat to open the software.

It is worth noting that a Java environment is also required to run the software, so configure the Java environment yourself.

5.2 No System Speed Measurement (RAW API)

To use JPerf for speed measurement, a TCP server or client must be implemented first. The TCP theory is not covered here. There is a lot of information on the Internet. This section only explains how to use RAW API to implement a TCP server.

5.2.1 TCP-related RAW API

Before we begin to implement TCP servers, let's first take a look at what RAW API functions are associated with TCP in LwIP and briefly understand their functions.

  • API functions for establishing TCP connections
functiondescribe
tcp_new()Create PCB Control Block for TCP
tcp_bind()IP and port number of the binding server
tcp_listen()PCB Control Block Listening for TCP
tcp_accepted()Notifies the LWIP stack that a TCP connection has been accepted
tcp_conect()Connect to remote hosts, used by clients
  • API functions for sending TCP data
functiondescribe
tcp_write()Construct a message and place it in the send buffer queue of the control block
tcp_sent()A callback function registered with the control block sent field, which is called back after the data has been successfully sent
tcp_output()Send data from send buffer queue
  • Receive TCP data
functiondescribe
tcp_recv()A callback function registered with the control block recv field that is called when new data is received
tcp_recved()This function must be called when the program has finished processing the data, notifying the kernel to update the receive window
  • poll function
functiondescribe
tcp_poll()A callback function registered with the control block poll field that is called periodically
  • Close and abort connections
functiondescribe
tcp_close()Close a TCP connection
tcp_err()Callback function registered with control block err field, called when an error is encountered
tcp_abort()Disconnect TCP Connection

Before the implementation of TCP server, cooperate with LWIP. The author's previous articles on how to transplant LWIP can be consulted.

Transplantation of LWIP (no system)

The author uses static IP here and opens the TCP module.

5.2.2 TCP Server Implementation Process

Previously, you learned about the API functions involved in TCP and opened the related configuration through STM32CubeMX, so how to implement a TCP server using these functions? Let's first briefly explain its basic process.

1. New Control Block
Use the tcp_new() function to create a TCP control block.

2. Bind Control Blocks
For the server, after creating a new fast control, you need to bind the local IP and port on the control block to facilitate client connections.

3. Control Block Listening
With the tcp_listen function, for the server, you need to explicitly call the tcp_listen function to put the control block in a listening state and wait for the connection request from the client.

4. Connect
Once the tcp_listen function is in the server listening state, you need to register a receive handler with the tcp_accept function immediately, because once a client connection request is successfully established, the server will call the handler.

5. Accept and process data
Once the connection is successful, the accept callback function calls the tcp_recv function to register a receive-complete handler. For the server, when the client's data or operational requirements are received, the callback function is called to process the call. This is a complex process: after receiving the data, first notify the update acceptance window (using the tcp_recved function), process and send data (using the tcp_write function), clear the sent data (using the tcp_sent function) when the data is sent successfully, and close the connection (using the function tcp_close).

The whole flowchart is shown below:

5.2.3 TCP Server Code Implementation

The implementation process of the TCP server is analyzed before, and the next step is through the API described earlier.

The first is the initialization of the TCP server. The modern code is as follows:

/**
  * @brief  TCP Server Initialization
  * @param  None
  * @retval res
  */
uint8_t tcp_server_init(void)
{
	uint8_t res = 0;		
	err_t err;  
  
	struct tcp_pcb *tcppcbnew;  	//Define a TCP Server Control Block
	struct tcp_pcb *tcppcbconn;  	//Define a TCP Server Control Block
		
  /* Assign a tcp_pcb structure to the TCP server */
  tcppcbnew = tcp_new();

	if(tcppcbnew)			//Created successfully
	{ 
		//Bind local IP with specified port number, IP_ADDR_ANY binds all local IP addresses
		err = tcp_bind(tcppcbnew,IP_ADDR_ANY,TCP_SERVER_PORT);	
		if(err==ERR_OK)	//Binding complete
		{
			tcppcbconn=tcp_listen(tcppcbnew); 			//Set tcppcb to listen
			
			//Callback function to initialize tcp_accept of LWIP
			tcp_accept(tcppcbconn,tcp_server_accept); 	
		}
		else 
		{
			res=1;
		}			
	}
	else 
	{
		res=1;
  }
	
	return res;
}

You can see that the tcp_accept() function registers a callback function with the following implementation code:

/**
  * @brief  lwIP tcp_accept()Callback function
  * @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);//Set the newly created pcb priority
	
	es=(struct tcp_server_struct*)mem_malloc(sizeof(struct tcp_server_struct)); //Allocate memory
 	
	if(es!=NULL) //Successful memory allocation
	{
		es->state = ES_TCPSERVER_ACCEPTED;  	//Receive Connection
		es->pcb = newpcb;
		es->p = NULL;
		
		tcp_arg(newpcb, es);
		tcp_recv(newpcb, tcp_server_recv);	//Initialize the callback function for tcp_recv()
		tcp_err(newpcb, tcp_server_error); 	//Initialize tcp_err() callback function
		tcp_poll(newpcb, tcp_server_poll,1);	//Initialize tcp_poll callback function
		tcp_sent(newpcb, tcp_server_sent);  	//Initialize Send Callback Function
		  
		tcp_server_flag |= 1<<5;				//Marked Client Connection

		ret_err=ERR_OK;
	}
	else 
	{
		ret_err=ERR_MEM;
	}
	
	return ret_err;
}

This function is used for data interaction with the client. Receive Send and other functions are registered in the function. The most important thing in this paper is to need Receive Function, the code is as follows:

/**
  * @brief  lwIP tcp_recv()Callback function of function
  * @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) //Receive empty data from client
	{
		es->state = ES_TCPSERVER_CLOSING;//TCP connection needs to be closed
		es->p = p; 
		ret_err = ERR_OK;
	}
	else if(err != ERR_OK)	//Received a non-empty data from the client, but for some reason err!=ERR_OK
	{
		if(p)
		{
			pbuf_free(p);	//Release Received pbuf
		}
		ret_err = err;
	}
	else if(es->state == ES_TCPSERVER_ACCEPTED) 	//Connected
	{
		if(p != NULL)  //Print out when connected and received data is not empty
		{
			memset(tcp_server_recvbuf, 0, TCP_SERVER_RX_BUFSIZE);  //Data Receive Buffer Zeroing
			for(q = p; q != NULL; q = q->next)  //Traverse the entire pbuf list
			{
				//Determines whether the data to be copied to TCP_SERVER_RX_BUFSIZE is larger than the remaining space of TCP_SERVER_RX_BUFSIZE, if greater than
				//Otherwise all data will be copied
				if(q->len > (TCP_SERVER_RX_BUFSIZE-data_len)) 
				{
					memcpy(tcp_server_recvbuf+data_len,q->payload,(TCP_SERVER_RX_BUFSIZE-data_len));//Copy Data
				}
				else 
				{
					memcpy(tcp_server_recvbuf+data_len,q->payload,q->len);
				}
				data_len += q->len;  	
				if(data_len > TCP_SERVER_RX_BUFSIZE) 
				{
					break; //Out of TCP Client Receive Array, Out
				}					
			}
			tcp_server_flag |= 1<<6;	//Tag Received Data

			tcp_recved(tpcb,p->tot_len);//Used to get received data and notify LWIP that more data is available
			pbuf_free(p);  	//Release memory
			ret_err=ERR_OK;
		}
	}
	else//Server shutdown
	{
		tcp_recved(tpcb,p->tot_len);//Used to get received data and notify LWIP that more data is available
		es->p = NULL;
		pbuf_free(p); //Release memory
		ret_err = ERR_OK;
	}
	return ret_err;
}

As you can see, the above functions are callback functions that are used layer by layer. Please refer to the source code for other related functions. We will not go into details here.

Finally, the main() function initializes the TCP server.

/**
  * @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 Polling Task
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Then compile the project and download it to the board. Open the jperf software with the appropriate parameters, and the results are as follows:

From the figure above, we can see that the transmission speed is about 1-2M or a little slow.

In order to improve the transmission speed of LwIP network, the configuration of LwIP needs to be adjusted appropriately, mainly by increasing the Heap Size of memory, the size of memory pool, the number of TCP segments, the maximum TCP segments, the maximum length of TCP send buffer queue, etc.

Modifications to the above parameters can be configured through STM32CubeMX. This is the following option:


The corresponding files are lwipopts.h and opt.h, with the main configuration parameters in opt.h.

The author directly modified the parameters in the file as follows:

//Heap size of memory heap
#define MEM_SIZE (24*1024)
 
/* memp Number of pbuf s in the structure, which should be set a little higher if the application sends a large amount of data from ROM or static storage */
#define MEMP_NUM_PBUF 24
 
/* Maximum number of message segments concurrently in the TCP buffer queue */
#define MEMP_NUM_TCP_SEG 150
 
/* Memory pool size */
#define PBUF_POOL_SIZE 64
 
/* Maximum TCP message segment, TCP_MSS = (MTU - IP header size - TCP header size) */
#define TCP_MSS (1500 - 40)
 
/* TCP Send Buffer Size (Bytes) */
#define TCP_SND_BUF (11*TCP_MSS)
 
/* TCP Receive window size */
#define TCP_WND (11*TCP_MSS)

Modify and compile, and the test results are as follows:

Comparing with the default parameters used in the previous section, we can see that the speed increases dramatically. It is about 4-5 times faster.

Performance optimization for LWIP will be explained in later chapters, with the focus of this article on speed measurement.

5.3 RT-Thread System Speed Measurement (Socket API)

The previous section uses the RAW API to implement the TCP server. This section uses the Socket API to implement the TCP server. Refer to my blog about the implementation of the TCP server.

TCP Server Implementation

The server code to implement TCP is as follows:

#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 contains the fd of the server
    int clientfds[FD_SETSIZE - 1];

    // Get netdev network card object by name
    netdev = netdev_get_by_name((char*)parameter);
    if (netdev == RT_NULL)
    {
        rt_kprintf("get network interface device(%s) failed.\n", (char*)parameter);
    }

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

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

    // Get IP Address Information in Network Card Object
    server_addr.sin_addr.s_addr = netdev->ip_addr.addr;

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

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

    client_addr_len = sizeof(client_addr);

    //Initialize maxfd equal to sfd
    maxfd = sfd;

    //Empty fdset
    FD_ZERO(&all_set);

    //Add sfd file descriptors to the collection
    FD_SET(sfd, &all_set);

    //Initialize the collection of client FDS
    for(i = 0; i < FD_SETSIZE -1 ; i++)
    {
        //Initialized to -1
        clientfds[i] = -1;
    }
    while(1)
    {
        //Each time a select returns, the fd_set collection changes and cannot be used with a select.
        //So we want to save the settings fd_set and the read fd_set
        read_set = all_set;
        nready = select(maxfd + 1, &read_set, NULL, NULL, NULL);

        //No timeout mechanism, no return 0
        if(nready < 0)
        {
            rt_kprintf("select error \r\n");

        }

        //Determine whether the listened socket has data
        if(FD_ISSET(sfd, &read_set))
        {
            //A client is connected
            cfd = accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len);
            if(cfd < 0)
            {
                rt_kprintf("accept socket error\r\n");
                //Continue Selecting
                continue;
            }
            rt_kprintf("new client connect fd = %d\r\n", cfd);

            //Add a new cfd to the fd_set collection
            FD_SET(cfd, &all_set);

            //Update maxfd to select
            maxfd = (cfd > maxfd)?cfd:maxfd;

            //Save the new cfd to the cfds collection
            for(i = 0; i < FD_SETSIZE -1 ; i++)
            {
                if(clientfds[i] == -1)
                {
                    clientfds[i] = cfd;
                    //Exit without adding
                    break;
                }
            }

            //There are no other sockets to handle: prevent duplicate work here and don't perform other tasks
            if(--nready == 0)
            {
                //Continue Selecting
                continue;
            }
        }

        //Traverse all client file descriptors
        for(i = 0; i < FD_SETSIZE -1 ; i++)
        {
            if(clientfds[i] == -1)
            {
                //Continue traversal
                continue;
            }

            //Determine if it is in the fd_set set set set
            if(FD_ISSET(clientfds[i], &read_set))
            {
                n = recv(clientfds[i], recvbuff, sizeof(recvbuff), 0);
                //rt_kprintf("clientfd %d:  %s \r\n",clientfds[i], recvbuff);

                if(n <= 0)
                {
                    //Clear from collection
                    FD_CLR(clientfds[i], &all_set);
                    //The current client fd assignment is -1
                    clientfds[i] = -1;                }
                else
                {
                    //Write back to client
                    n = send(clientfds[i], sendbuff, strlen(sendbuff), 0);
                    if(n < 0)
                    {
                        //Clear from collection
                        FD_CLR(clientfds[i], &all_set);

                        //The current client fd assignment is -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.\n");
        return -RT_ERROR;
    }

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

    /* Start threads if created successfully */
    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 */

Add the appropriate code and compile it before downloading the firmware. The test results are as follows:

It can be seen that its transmission speed is also around 1M, which is slower than in unsystematic environment.

To increase the speed, configure the LWIP parameters as follows:

Compile, download, and test results as follows:

You can see that the speed has been improved, but the increase is obvious when there is no system. The later chapters on the reasons will be analyzed in detail.


Resource Acquisition Method

1. Long press the QR code below to pay attention to the public number [Embedded Experimental Building]
2. Get information on public number response keywords [LWIP]


Welcome to my website

BruceOu's Beep Mile
BruceOu's Home Page
BruceOu's Blog
BruceOu's CSDN Blog
BruceOu's Brief
BruceOu's knowledge

Posted by desmond_ckl on Sun, 17 Oct 2021 09:38:08 -0700