基于 zynq7020-lwip 的 echo server 测试

第一遍blog,写完发现还有好多学习的地方,共同进步,互勉。

全文整理逻辑顺序
在这里插入图片描述

1) TCP/IP 协议简介

TCP/IP 协议中文名为传输控制协议/因特网互联协议,又名网络通讯协议,是 Internet 最基本的协议、Internet 国际互联网络的基础,由网络层的 IP 协议和传输层的 TCP 协议组成。 TCP/IP 定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。协议采用了 4 层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求。通俗而言: TCP 负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地。而 IP 是给因特网的每一台联网设备规定一个地址。
在这里插入图片描述

2)LwIP协议栈

(http://www.nongnu.org/lwip/2_1_x/index.html))
在这里插入图片描述LwIP特点
lwip202_v1_2 提供二种用户编程接口方式: raw API 和 socket API。
Socket API: 提供了一个基于 open-read-write-close 模块的 BSD socket-style 接口, 需要操作系统。
Raw API: 是为高性能和低内存开销而定制的。优点是数据的接收和发送不会导致进程的切换,提供了最好的性能,执行速度快,而且消耗的内存资源少;缺点是应用程序无法进行连续运算,因为网络协议的处理和运算是在同一进程中完成的,二者无法并行发生。Raw API 是资源较少的嵌入式系统的首选方法,也是在没有操作系统的情况下运行 lwIP 时唯一可用的 API。

DHCP(Dynamic Host Configuration Protocol) 即动态主机配置协议,通常应用在大型的局域网络环境中,主要作用是集中的管理、分配 IP 地址,使网络环境中的主机动态的获得 IP 地址、 Gateway 地址、 DNS 服务器地址等信息,并能够提升地址的使用率。

3) PS 的千兆以太网控制器及硬件结构

MAC 与 PHY 芯片及 GMII 与 RGMII 接口
以太网卡工作在 OSI 模型的最后两层,物理层和数据链路层,物理层定义了数据传送与接收所需要的电与光信号、线路状态、时钟基准、数据编码和电路等,并向数据链路层设备提供标准接口。物理层的芯片称之为 PHY。此外 PHY 还提供了和对端设备连接的重要功能并通过 LED 灯显示出当前的连接的状态和工作状态。当我们给网卡接入网线的时候, PHY 不断发出的脉冲信号检测到对端有设备,且具有AutoNegotiation,即自协商。

数据链路层则提供寻址机构、数据帧的构建、数据差错检查、传送控制、向网络层提供标准的数据接口等功能。以太网卡中数据链路层的芯片称之为 MAC 控制器。

MAC 控制器与 PHY 通过 MII(Medium Independent Interface)接口进行连接。 MII 接口有很多类型,千兆以太网多使用 GMII (Gigabit Medium Independent Interface)或 RGMII (Reduced Gigabit Media Independent
Interface)接口进行连接。
在这里插入图片描述
GMII 接口
GMII 接口提供了 8 位数据通道, 125MHz 的时钟速率,从而具有 1000Mbps 的数据传输速率。除 MDC和 MDIO 外,有 24 根接口信号线
在这里插入图片描述PS 的千兆以太网控制器(GEM) 。 PS 的千兆以太网控制器实现了与 IEEE 802.3-2008标准兼容的 10/100/1000 Mb/s 以太网 MAC,能够以上述三种速度在半双工或全双工模式下运行。 PS 配备两个千兆以太网控制器。
DMA 控制器通过 AHB 总线接口连接到存储器。 MAC 控制器与 FIFO 接口的连接为嵌入式处理系统中的分组数据存储提供 scatter-gather 类型的功能。如果通过 MIO 连接至 PS 端的以太网 PHY 芯片, 则每个控制器使用 RGMII 接口以节省引脚。 如果通过 EMIO 连接至 PL 端的以太网PHY 芯片,则每个控制器使用 GMII 接口。
通过 APB 总线访问千兆以太网控制器的寄存器。 寄存器用于配置 MAC 的功能、 选择不同的操作模式、 以及启用和监控网络管理统计信息。控制器为管理 PHY 芯片提供 MDIO 接口, 可以从 MDIO 接口控制 PHY 芯片。
千兆以太网控制器
千兆以太网控制器
Mac接口
RJ45 接口原理图
PHY接口
YT8521S 原理图
大致数据流
大致数据流

4) 主体功能设计

  1. 首先设置好板卡的 MAC 地址,IP 地址,子网掩码,网关信息,定义的 netif 结构体类型的指针。
  2. 调用 init_platform 函数配置定时器和建立中断以初始化平台,定时器产生周期性中断,周期为 250ms。
  3. 调用 lwip_init 函数完成对 lwip 协议栈的初始化。
  4. 使用 xemac_add 函数添加以太网 MAC 到协议栈中。
  5. 使用 platform_enable_interrupts 函数使能中断和启动定时器。
  6. 启动 DHCP 服务获取动态 IP 地址,如果超时未获取到动态 IP 地址,则使用默认的静态 IP,并打开网口,打印IP信息。
  7. start_application 函数是用户应用函数,该函数创建了一个 TCP 服务并设定了对应该服务的回调函数。
  8. 服务器使用阻塞方式进入 while(1)循环执行数据包接收操作,数据包接收函数 xemacif_input 处理由中断处理程序接收的数据包,并将它们传递给 lwIP,然后 lwIP 为每个接收到的数据包调用适当的回调处理程序。

几个结构体,netif, udp_pcb, pbuf

每个网络接口都有一个对应的结构体 netif 表示,是协议栈与底层驱动接口模块。
在结构体中定义了链表中下一个结构体,IP地址,子网掩码,网关,输入函数,输出函数,最大传输单元等。
结构体 netif 部分
PLATFORM_ZYNQ,也就是与 ZYNQ 平台相关的,定义在platform_zynq.c 文件

#define PLATFORM_ZYNQ

配置定时器和建立中断以初始化平台,定时器产生周期性中断,周期为 250ms。

void init_platform()
{
	platform_setup_timer();
	platform_setup_interrupts();

	return;
}

定义开发板的 MAC 地址、开发板IP地址,子网掩码,网关信息

	unsigned char mac_ethernet_address[] =
	{ 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };
	
	IP4_ADDR(&ipaddr,  192, 168,   3, 180);
	IP4_ADDR(&netmask, 255, 255, 255,  0);
	IP4_ADDR(&gw,      192, 168,   3,  1);

初始化 lwIP ,并使用 xemac_add 函数添加以太网 MAC 到协议栈中,这里指定以太网中断函数,xemac_add调用函数low_level_init()来进行硬件的实际设置。并利用 netif_set_default 将 echo_netif 设置为默认网卡

	lwip_init();
	if (!xemac_add(echo_netif, &ipaddr, &netmask,
						&gw, mac_ethernet_address,
						PLATFORM_EMAC_BASEADDR)) {
		xil_printf("Error adding N/W interface\n\r");
		return -1;
	}
	netif_set_default(echo_netif);

函数使能中断和启动定时器,启动指定网络,打印IP信息

	platform_enable_interrupts();
	netif_set_up(echo_netif);
	print_ip_settings(&ipaddr, &netmask, &gw)

宏定义进行 DHCP 的设置,启动 DHCP 服务获取动态 IP 地址,如果超时未获取到动态 IP 地址,则使用默认的静态 IP 设置

#if (LWIP_IPV6 == 0)
#if (LWIP_DHCP==1)  /*进行 DHCP 的设置*/
	/* Create a new DHCP client for this interface.  
	 * Note: you must call dhcp_fine_tmr() and dhcp_coarse_tmr() at
	 * the predefined regular intervals after starting the client.
	 */
	dhcp_start(echo_netif);
	dhcp_timoutcntr = 24;

	while(((echo_netif->ip_addr.addr) == 0) && (dhcp_timoutcntr > 0))
		xemacif_input(echo_netif);
		
	if (dhcp_timoutcntr <= 0) {
		if ((echo_netif->ip_addr.addr) == 0) {
			xil_printf("DHCP Timeout\r\n");
			xil_printf("Configuring default IP of 192.168.3.180\r\n");
			IP4_ADDR(&(echo_netif->ip_addr),  192, 168,   3, 180);
			IP4_ADDR(&(echo_netif->netmask), 255, 255, 255,  0);
			IP4_ADDR(&(echo_netif->gw),      192, 168,   3,  1);
		}
	}

	ipaddr.addr = echo_netif->ip_addr.addr;
	gw.addr = echo_netif->gw.addr;
	netmask.addr = echo_netif->netmask.addr;
#endif

用户使用 lwip 实现不同的应用功能时,定义上层函数。

	start_application();

创建PCB(protocol control block)建立连接、绑定IP地址和端口号、监听请求,最后tcp_accept函数用于指定当监听到连接请求时调用的函数accept_callback。

int start_application()
{
	struct tcp_pcb *pcb;
	err_t err;
	unsigned port = 7;

	/* create new TCP PCB structure--LWIP_IPV4 */
	pcb = tcp_new_ip_type(IPADDR_TYPE_ANY);
	if (!pcb) {
		xil_printf("Error creating PCB. Out of Memory\n\r");
		return -1;
	}
	
	/* bind to specified @port port number and IP address
	IP4_ADDR_ANY to bind to any local address */
	err = tcp_bind(pcb, IP_ANY_TYPE, port);
	if (err != ERR_OK) {
		xil_printf("Unable to bind to port %d: err = %d\n\r", port, err);
		return -2;
	}

	/* we do not need any arguments to callback functions */
	tcp_arg(pcb, NULL);

	/* listen for connections */
	pcb = tcp_listen(pcb);
	if (!pcb) {
		xil_printf("Out of memory while tcp_listen\n\r");
		return -3;
	}

	/* specify callback to use for incoming connections */
	tcp_accept(pcb, accept_callback);

	xil_printf("TCP echo server started @ port %d\n\r", port);

	return 0;
}

主程序中通过轮询的方式,执行数据包接收操作,从接收驱动函数中将最新来到的数据包移入内核。
TcpFastTmrFlag和TcpSlowTmrFlag是 TCP TX 处理所必需的标志位,定时器中断分别以 250ms 和 500ms的周期来改变这两个标志位。
主程序中通过轮询的方式不断调用 xemacif_input(netif) 函数,如果有数据包的话,最终从前文中提到的 pbuf 队列中取出数据包。
第二种方式:轮询的方式消耗了很多时间资源,也可以使用中断的方式将数据包从驱动移至内核,在 DMA 接收中断到来,并有数据包时触发中断,调用emacif_input(netif) 函数。
网卡驱动调用过程
xemacif_input(netif) 函数

	while (1) {
		if (TcpFastTmrFlag) {
			tcp_fasttmr();  //250ms
			TcpFastTmrFlag = 0;
		}
		if (TcpSlowTmrFlag) {
			tcp_slowtmr();  //500ms
			TcpSlowTmrFlag = 0;
		}
		/*数据包接收函数 xemacif_input 处理由中断处理程序接收的数据包,并将它们传递给 lwIP*/
		xemacif_input(echo_netif);
		transfer_data();//不主动发数据,未写功能
	}

accept_callback是连接建立接收中断程序。内部主要通过tcp_recv函数来指定当收到TCP包后调用的函数recv_callback。

err_t accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err)
{
	static int connection = 1;

	/* set the receive callback for this connection */
	tcp_recv(newpcb, recv_callback);

	/* just use an integer number indicating the connection id as the
	   callback argument */
	tcp_arg(newpcb, (void*)(UINTPTR)connection);

	/* increment for subsequent accepted connections */
	connection++;

	return ERR_OK;
}

recv_callback是数据接收中断程序,pbuf 结构体用于存储接收或发送的数据,也是链表形式。tcp_recved函数指示用来告知LWIP接收数据量,然后检测发送缓冲区是否足够容纳接收内容,若大于则调用tcp_write函数将接收数据写入发送缓冲区等待发送。

err_t recv_callback(void *arg, struct tcp_pcb *tpcb,
                               struct pbuf *p, err_t err)
{
	/* do not read the packet if we are not in ESTABLISHED state */
	if (!p) {
		tcp_close(tpcb);
		tcp_recv(tpcb, NULL);
		return ERR_OK;
	}

	/* indicate that the packet has been received */
	tcp_recved(tpcb, p->len);

	/* echo back the payload */
	/* in this case, we assume that the payload is < TCP_SND_BUF */
	if (tcp_sndbuf(tpcb) > p->len) {
		err = tcp_write(tpcb, p->payload, p->len, 1);
	} else
		xil_printf("no space in tcp_sndbuf\n\r");

	/* free the received pbuf */
	pbuf_free(p);

	return ERR_OK;
}

综上,整体的调用流程为:tcp_accept -> accept_callback -> tcp_recv -> recv_callback -> tcp_recved和tcp_write。前四个用于接收,后两个用于发送。

void tcp_slowtmr(void)每500ms调用,该函数完成了超时重传,tcp保活功能,并会遍历active和timewait链表的PCB,删除那些超时或者出错的PCB,同时将PCB中unsent队列中的数据发送出去。一般使用tcp_write();写入数据后,数据不会马上发送,而是在定时任务中发送。

    //请求连接次数超出限制
    if (pcb->state == SYN_SENT && pcb->nrtx >= TCP_SYNMAXRTX) {
      ++pcb_remove; //移除增加
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: max SYN retries reached\n"));
    }
    //数据重发次数超出限制
    else if (pcb->nrtx >= TCP_MAXRTX) {
      ++pcb_remove;
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: max DATA retries reached\n"));
    } else {

      //如果坚持定时器已经开启
      if (pcb->persist_backoff > 0) {

        //获取坚持定时器触发值
        u8_t backoff_cnt = tcp_persist_backoff[pcb->persist_backoff-1];
        //坚持定时器不超过触发值,则加1
        if (pcb->persist_cnt < backoff_cnt) {
          pcb->persist_cnt++;
        }
        //坚持定时器触发,发送窗口探查
        if (pcb->persist_cnt >= backoff_cnt) {
          if (tcp_zero_window_probe(pcb) == ERR_OK) {
            pcb->persist_cnt = 0; //发送成功,清除计数值
            if (pcb->persist_backoff < sizeof(tcp_persist_backoff)) {
              pcb->persist_backoff++; //发送探查次数+1
            }
          }
        }
      } else {  //无开启坚持定时器
        //如果开启了超时重传定时器,则加1
        if (pcb->rtime >= 0) {
          ++pcb->rtime;
        }
        //有未确认报文且重传时间到,要重传了
        if (pcb->unacked != NULL && pcb->rtime >= pcb->rto) {
          LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_slowtmr: rtime %"S16_F
                                      " pcb->rto %"S16_F"\n",
                                      pcb->rtime, pcb->rto));
          if (pcb->state != SYN_SENT) {
            u8_t backoff_idx = LWIP_MIN(pcb->nrtx, sizeof(tcp_backoff)-1);  //获得重传次数,但重传次数不会超过7
            //动态设置rto,每次超时后,rto时间会增加
            pcb->rto = ((pcb->sa >> 3) + pcb->sv) << tcp_backoff[backoff_idx];  
          }

          pcb->rtime = 0; //重置超时重传定时器

          //TODO 出现重传,说明报文丢失了,可能是网络出现阻塞,减小拥塞窗口
          eff_wnd = LWIP_MIN(pcb->cwnd, pcb->snd_wnd);
          pcb->ssthresh = eff_wnd >> 1; //ssthresh减少到拥塞窗口的一半

          //若ssthresh比最大报文长度的两倍还小,后者的数值(限制了ssthresh的最小值)
          if (pcb->ssthresh < (tcpwnd_size_t)(pcb->mss << 1)) {
            pcb->ssthresh = (pcb->mss << 1);
          }
          pcb->cwnd = pcb->mss; //拥塞窗口设置成最大报文长度,一个报文长度
          LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_slowtmr: cwnd %"TCPWNDSIZE_F
                                       " ssthresh %"TCPWNDSIZE_F"\n",
                                       pcb->cwnd, pcb->ssthresh));

          //重传报文
          tcp_rexmit_rto(pcb);
        }
      }
    }

    if (pcb->state == FIN_WAIT_2) {
  
      //TODO 处于FIN_WAIT_2的时间太长(由于对方长时间无反应)则删除
      if (pcb->flags & TF_RXCLOSED) {
        /* PCB was fully closed (either through close() or SHUT_RDWR):
           normal FIN-WAIT timeout handling. */
        if ((u32_t)(tcp_ticks - pcb->tmr) >
            TCP_FIN_WAIT_TIMEOUT / TCP_SLOW_INTERVAL) {
          ++pcb_remove;
          LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: removing pcb stuck in FIN-WAIT-2\n"));
        }
      }
    }

void tcp_fasttmr(void)比较简单,它的功能主要是每250ms处理延时发送的ack报文和fin报文,同时通知上层应用处理数据。

void
tcp_fasttmr(void)
{
  struct tcp_pcb *pcb;

  ++tcp_timer_ctr;

tcp_fasttmr_start:
  pcb = tcp_active_pcbs;  //在active中遍历

  while (pcb != NULL) {
    if (pcb->last_timer != tcp_timer_ctr) {
      struct tcp_pcb *next;
      pcb->last_timer = tcp_timer_ctr;
      //发送延时的ack
      if (pcb->flags & TF_ACK_DELAY) {
        LWIP_DEBUGF(TCP_DEBUG, ("tcp_fasttmr: delayed ACK\n"));
        tcp_ack_now(pcb);
        tcp_output(pcb);
        pcb->flags &= ~(TF_ACK_DELAY | TF_ACK_NOW);
      }
      //发送延时的fin
      if (pcb->flags & TF_CLOSEPEND) {
        LWIP_DEBUGF(TCP_DEBUG, ("tcp_fasttmr: pending FIN\n"));
        pcb->flags &= ~(TF_CLOSEPEND);
        tcp_close_shutdown_fin(pcb);
      }

      next = pcb->next;

      //若当前tcp有未被上层应用接收的数据
      if (pcb->refused_data != NULL) {
        tcp_active_pcbs_changed = 0;
        tcp_process_refused_data(pcb);  //通过回调函数使上层处理数据
        if (tcp_active_pcbs_changed) {
          goto tcp_fasttmr_start;
        }
      }
      pcb = next; //下一个
    } else {
      pcb = pcb->next;
    }
  }
}

5)底层部分驱动过程

TCP协议中许多地方是需要使用到定时功能的,如定时重传功能,保活keepalive功能,坚持定时器功能,这些定时功能会在lwip中的两个定时器函数中实现。
void tcp_fasttmr(void)比较简单,它的功能主要是每250ms处理延时发送的ack报文和fin报文,同时通知上层应用处理数据。

对于 LwIP 协议栈的移植来说,用户的主要工作是为其提供网卡驱动函数。LwIP 可以运行在多种不同的硬件平台上,配合不同型号的网络 Phy 芯片使用。LwIP 为不同的网卡芯片提供了统一的抽象驱动接口,用户根据使用的网卡芯片以及硬件平台,实现功能函数。在 Lwip 中,很多结构体都是以链表形式存在的。
用户需要实现三项功能驱动:网卡的初始化函数,接收函数以及发送函数。本文将基于 Xilinx LwIP 例程讨论网卡的接收驱动编写。
网卡接收驱动的工作方式可以分为两类:以轮询或者中断方式从网络 Phy 芯片中接收数据包。在 Xilinx SDK example 中,Xilinx 提供的驱动函数使用 DMA 搬运数据,以中断方式工作。

接收驱动函数需要完成两项工作:

  1. 将从 Phy 芯片接收到的数据转移给 LwIP 所在的应用程序。
  2. 将数据封装成 LwIP 的数据结构,一般为 pbuf 数据包结构体,并调整 pbuf 链的前后指向关系。

DMA 接收中断触发过程
DMA 接收中断触发

接收驱动的工作流程
接收驱动的工作流程
Xilinx 网卡驱动的目录结构
Xilinx 提供了三种网卡驱动,分别对应为使用 emacps,axiemac 以及 emaclite 三种 mac IP。其中 emacps 为 Zynq PS 中独有的硬件 mac 模块,axiemac 为一个全功能的逻辑 IP,emaclite 是一个消耗资源较少,但只支持百兆网的轻量级 IP 。

三种网卡驱动在 xadapter.c 中进行抽象,根据使用的网卡类型,调用对应的驱动函数。
Xilinx 网卡驱动的目录结构
xemacpsif_dma 中为 DMA 外设的相关函数,包括初始化,发送以及接收中断函数。
xemacpsif_hw 中为 emac 外设的相关函数,包括初始化,启动函数等。
xemacpsif 中为与 emac 相关的接口函数。
xemacpsif_physpeed 为与网络 phy 芯片通信握手的相关函数。
xpqueue 为数据包指针缓冲队列相关的函数。
网卡驱动调用
网卡驱动调用

启动网络连接确保防火墙已关闭
使用静态 IP 地址进行网络连接时,需要确保同网段内没有主机使用该IP地址,否则会造成 IP 冲突。
除了可以使用网络调试助手软件外,
也可以开启 Windows 的 telnet 客户端功能调试。

Note:
使用网络调试助手或者Windows-Telnet测试,
使用wireshark抓包测试完整性和具体传输数据
MAC 地址设置为 00:0a:35:00:01: 02,
默认使用 DHCP 获取动态 IP 地址, 如果 DHCP失败,
则使用默认设置的静态 IPv4 地址 192.168.3.180
或 IPv6 地址 FE80:0:0:0:0A:35FF:FE00:102。
主机IP:192.168.3.179

测试结果:
ping192.168.3.180可以ping通
采用TCP传输用wireshark抓包发送和接收为82,无丢包

在这里插入图片描述

在这里插入图片描述

6)参考

领航者 ZYNQ 之嵌入式 SDK开发指南 V2.0
88E1510/88E1518/ 88E1512/88E1514Integrated 10/100/1000 Mbps Energy Efficient Ethernet Transceiver
https://blog.csdn.net/weixin_44821644/article/details/111396150
Zynq-7000 SoC Technical Reference Manual UG585 (v1.12.2) July 1, 2018
https://www.elecfans.com/d/1310000.html
https://blog.csdn.net/weixin_42066185/article/details/106421611
http://savannah.nongnu.org/projects/lwip/
https://zhuanlan.zhihu.com/p/61912306

第一次更新博客,还有好多不足的地方。
lwip开始觉得好像懂了,后来梳理发现不懂的越来越多,O(∩_∩)O哈哈~ 学无止境嘛, 不放弃,慢慢啃。
我要去学习了,等学到了新东西再来记录成长吧!

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值