学会Zynq(13)lwIP官方应用程序示例

XAPP1026中记录一些lwIP的应用程序示例和性能测试情况,不过提供的示例工程都是在几个Xilinx的官方板子中跑的。可能很多学生没有机会碰到这些板子。。。另外这份应用笔记使用的SDK 2014.3版本也比较老,那个版本lwip还没有直接集成到SDK中。本文将这份笔记其中比较有用的代码编写思路和性能测试结果部分摘取出来。

1. 硬件系统

这个表是几个开发板上搭建的硬件系统。纯FPGA使用的处理器是软核MicroBlaze,以太网控制器也是软核axi_ethernet或axi_ethernetlite。现在软核的选择也越来越多了,除了Xilinx提供官方支持的PowerPC和MicroBlaze,ARM也提供了一些处理器软核。比如最近的“第三届集电赛”上ARM杯就是要求用ArmCortex-M3 DesignStart处理器搭建SoC。

Zynq用的是硬核Cortex-A9,以太网控制器是GigE。硬核的处理器速度比软核要高很多,在后面的测试结果中也可以看到lwIP的性能表现好很多。

2. Echo服务器
Echo将由网络传给程序的输入信息重复返回,作为一个简单的程序,可以让大家学习如何编写一个lwIP应用程序。Socket模式下echo服务器的结构如下:

1.主线程在制定的echo服务器端口上持续监听。
2.对于每个连接请求,生成一个单独的echo服务线程。
3.继续监听echo端口。

while (1) {
new_sd = lwip_accept(sock, (struct sockaddr *)&remote, &size);
sys_thread_new(process_echo_request, (void*)new_sd, DEFAULT_THREAD_PRIO);
}

echo服务线程接收一个新的socket描述符作为其输入,在该描述符上读取接收到的数据,并进行数据回传。

while (1) {
/* read a max of RECV_BUF_SIZE bytes from socket */
n = lwip_read(sd, recv_buf, RECV_BUF_SIZE));
/* handle request */
nwrote = lwip_write(sd, recv_buf, n));
}

Socket模式提供了一个简单的API,会阻塞socket的读和写,直到处理完为止。但是socket API需要很多部分来实现这个功能,主要是一个简单的多线程内核(MicroBlaze使用xilkernel、Zynq使用FreeRTOS)。由于socket API的操作开销较大,导致性能比较差。

RAW API提供了一个基于回调方式的接口。应用程序中使用RAW API注册回调函数,这些函数会在接收accept、读取read、写入write等重要事件发生时被调用。基于RAW API的echo服务是单线程的,所有的工作在回调函数中完成。主程序的循环结构如下:

while (1) {
if (TcpFastTmrFlag) {
tcp_fasttmr();
TcpFastTmrFlag = 0;
}
if (TcpSlowTmrFlag) {
tcp_slowtmr();
TcpSlowTmrFlag = 0;
}
xemacif_input(netif);
transfer_data();
}

TCP发送处理需要用到TcpFastTmrFlag和TcpSlowTmrFlag,程序中两个定时器分别设置为250ms和500ms。程序循环的功能是不断地接收数据包(xemacif_input),将它们传递给lwIP。在进入主循环之前,应用程序要注册一些回调函数:
/* 创建新的TCP控制块结构 */
pcb = tcp_new();
/* 绑定到指定端口 */
err = tcp_bind(pcb, IP_ADDR_ANY, port);
/* 回调函数不需要参数 */
tcp_arg(pcb, NULL);
/* 监听连接 */
pcb = tcp_listen(pcb);
/* 指定用于传入连接的回调函数accept_callback*/
tcp_accept(pcb, accept_callback);

上面这串操作创建了一个TCP连接,并为在“接受accept”的连接设置了一个回调函数。当连接请求被接受时,函数accept_callback将被异步调用。echo只需要在接收到数据时相应,因此accept回调函数中还要设置一个“接收receive”的回调函数:
/* 为连接设置接收的回调函数 */
tcp_recv(newpcb, recv_callback);

当接收到数据包时,recv_callback函数被调用,该函数中将接收到的数据回传给发送者:
/* 数据包已被接收到 */
tcp_recved(tpcb, p->len);
/* 回传数据 */
err = tcp_write(tpcb, p->payload, p->len, 1);

尽管RAW API比Socket API要复杂,但因为没有很高的开销从而提供了更大的吞吐量。

3. Web服务器
这里是一个简单的Web服务器的实现,作为基于TCP的应用程序的参考。该Web服务器只实现了一个HTTP 1.1协议的子集。这样的Web服务器可用于通过浏览器控制或监听嵌入式平台。示例中的Web服务器具有如下特性:

  • 通过HTTP GET命令访问驻留在内存文件系统中的文件;
  • 使用HTTP POST命令控制开发板上的LED灯;
  • 使用HTTP POST命令获取开发板上的DIP开关的状态。
  • Xilinx内存文件系统(xilmfs)用于在开发板的内存中存储一组文件。将web浏览器的IP地址指向开发板,通过HTTP GET命令可以访问这些文件。

    通过向一组映射到设备的URL发出POST命令,可以控制或监视开发板上组件的状态。当web服务器接收到它识别到的URL的POST命令时,会调用一个指定的函数来完成请求的工作。这个函数的输出以JavaScript对象表示法(JSON)格式发送回web浏览器。web浏览器根据接收到的数据更新显示。

    web服务器的总体架构和echo服务器类似,有一个主线程在HTTP端口(80)上监听传入的连接。对于每个传入的连接,都会生成一个新线程来处理该连接上的请求。

    HTTP线程首先读取请求,识别它是GET操作还是POST操作,据此执行对应的操作。对于GET请求,线程在内存文件系统中查找特定的文件。如果存在此文件,则将其返回到浏览器;如果该文件不可用,会返回HTTP 404的错误代码。

    Socket模式下HTTP线程的结构如下:
    /* 读取请求 */
    if ((read_len = read(sd, recv_buf, RECV_BUF_SIZE)) return;

    /* 回应请求 */
    generate_response(sd, recv_buf, read_len);

    回应请求函数的伪代码结构如下:
    /* 根据HTTP请求执行对应的响应 */
    int generate_response(int sd, char *http_req, int http_req_len)
    {
    enum http_req_type request_type = decode_http_request(http_req, http_req_len);
    switch(request_type) {
    case HTTP_GET: return do_http_get(sd, http_req, http_req_len);
    case HTTP_POST: return do_http_post(sd, http_req, http_req_len);
    default: return do_404(sd, http_req, http_req_len);
    }
    }

    RAW模式下web服务器主要使用回调函数来执行其任务。当接受(accept)新连接时,接受回调函数设置发送send和接收receive的回调函数。当发送的数据被确认或接收到数据时会执行注册的回调函数。
    err_t accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err)
    {
    /* 记住连接号,作为回调函数参数 */
    tcp_arg(newpcb, (void*)palloc_arg());

    tcp_recv(newpcb, recv_callback);
    tcp_sent(newpcb, sent_callback);

    return ERR_OK;
    }

    当web浏览器发出请求时,recv_callback函数被调用。这个函数中解码请求并执行对应的响应。

    /* 确认已读取了有效载荷(payload) */
    tcp_recved(tpcb, p->len);
    /* 读取并解译请求 */
    generate_response(tpcb, p->payload, p->len);

    /*释放接收到的包cket */
    pbuf_free(p);

    数据传输过程是很复杂的。在socket模式下,应用程序使用lwip_write API发送数据。如果TCP发送缓冲区已满,会阻塞这个函数。然而在RAW模式下,应用程序决定可以发送多少数据、只发送多少数据。只有当发送缓冲区中有空间可用时,才能进一步发送数据。当接收方(客户端)对发送的数据产生应答时,空间就可以用了。此时lwIP会调用sent_callback函数,表示数据已经发送,并且send缓冲区中现在有空间存储更多数据。sent_callback的结构如下:

    err_t sent_callback(void *arg, struct tcp_pcb *tpcb, u16_t len)
    {
    int BUFSIZE = 1024, sndbuf, n;
    char buf[BUFSIZE];
    http_arg *a = (http_arg*)arg;
    /* 当连接关闭或没有数据发送时 */
    if (tpcb->state > ESTABLISHED) {
    return ERR_OK;
    }
    /* 从文件中读取更多数据并发送 */
    sndbuf = tcp_sndbuf(tpcb);
    if (sndbuf return ERR_OK;
    n = mfs_file_read(a->fd, buf, BUFSIZE);
    tcp_write(tpcb, buf, n, 1);
    /* 计算还剩多少字节没有发送 */
    a->fsize -= n;
    if (a->fsize == 0) {
    mfs_file_close(a->fd);
    a->fd = 0;
    }

    return ERR_OK;
    }

    发送send和接收receive的回调函数都可以用tcp_arg设置参数来调用。对于web服务器,这个参数指向一个数据结构,记录了剩余要发送的字节数以及用来读取该文件的文件描述符。

    4. TFTP服务器
    TFTP是一个基于UDP的为文件发送和接收协议。因为UDP不能保证包的可靠传输,TFTP实现了另一个确保包在传输过程中不会丢失的协议。

    Socket模式下TFTP服务器的应用程序主架构和web服务器很类似。主线程监TFTP端口,并为每个传入的连接请求生成一个新的TFTP线程。新线程中为实现了TFTP协议的一个子集,支持读或写请求。最多只运行一个TFTP数据包或应答包,极大地简化了TFTP协议的实现。RAW模式下的TFTP服务器也非常简单,没有对超时做处理,因此只能用于点对点的以太网连接(0数据包丢失)。

    5. TCP RX与TX吞吐量测试
    TCP传输和接收吞吐量测试是一个很简单的应用程序,检测使用lwIP和Xilinx Ethernet MAC适配器可以实现的最大TCP传输和接收的吞吐量。

    发送测试测量从运行lwIP的开发板到主机的传输吞吐量。lwIP应用程序连接到运行在主机上的Iperf服务器(一个开源软件https://sourceforge.net/projects/iperf/),然后不断向主机发送数据。Iperf会检测数据传输的速率并在终端显示(这份应用笔记比较老,现在win10的任务管理器->性能->以太网也可以测速)。

    接收测试测量板上的最大接收传输吞吐量。lwIP应用程序作为服务器,接受来自任一主机的某个端口的连接。程序一边接收一边删除,主机上的Iperf(客户端模式)连接到此服务器,并根据需要向其传输数据。Iperf会将计算的吞吐量显示在控制台中。

    6. 创建lwIP应用程序
    lwIP的Socket API与Barkeley/BSD Socket很类似,唯一的区别的是初始化过程需要耦合到lwip 1.4.1库和xilkernel(或FreeTROS)中。所有socket模式的应用程序都要执行下面这些步骤

    1. MicroBlaze使用Xilkernel,为其配置一个静态线程(示例工程中都叫main_thread);Zynq使用FreeRTOS,启动调度器前要创建一个任务。
    2. 主线程调用lwip_init函数初始化lwIP,然后使用sys_thread_new函数启动网络线程。所有使用lwIP socket API的线程都必须由sys_thread_new函数启动。
    3. 主线程使用xemac_add函数添加一个网络接口,这个函数要为接口设置IP地址和以太网MAC地址,并对其初始化。
    4. 由网络线程启动xemacif_input_thread线程。这个线程将从中断处理程序接收到的数据移动到tcpip_thread(lwIP通过它来处理TCP/IP)。
    5. 这样便已经初始化了lwIP库,然后根据应用程序的需求启动其它线程。
    6. lwIP RAW模式的API更为复杂,因为需要编程者了解lwIP的内部结构。

    RAW模式下程序的典型结构如下:

    1. 首先使用lwip_init初始化所有的lwIP结构。
    2. lwIP初始化后,使用xemac_add函数添加以太网MAC。
    3. 由于Xilinx lwIP适配器是基于中断的,所以要启用处理器中的中断和中断控制器。
    4. 设置一个定时器,以固定间隔产生中断,通常取250ms。在定时器中断中更新标志信号(flag),主程序循环中根据flag来调用lwip TCP的API函数tcp_fasttmmr和tcp_slowtmr。
    5. 应用程序初始化后,主程序进入一个无限循环,执行包接收操作,以起其它应用程序需要完成的操作。
    6. 包接收操作(xemacif_input)会把中断处理程序接收到的包传递给lwIP,然后lwIP为每个接收到的包调用相应的回调函数。

    7. lwIP性能测试
    下表是不同配置下得到TCP最大吞吐量测试结果。可以看到cache大小、lwIP模式、处理器选择都会影响到lwIP的性能表现。

    8. DHCP的支持
    XAPP1026中的所有应用程序都使用了动态主机配置协议(DHCP)。应用程序会假设网络中有一个DHCP服务器,会为连接的板子分配一个IP地址。如果板子连接的网络中没有DHCP服务器,在一定时间后会发生DHCP超时,此时会为开发板分配一个静态地址(SDK自带的lwip echo例程用的IP地址是192.168.1.10)。在BSP设置中可以选择是否启用DHCP。

    ---------------------


    文章来源:FPGADesigner的博客
    *本文由作者授权转发,如需转载请联系作者本人

    推荐阅读