S03_CH08_DMA_LWIP以太网传输
8.1概述
本例程详细创建过程和本季课程第一课《S03_CH01_AXI_DMA_LOOP 环路测试》非常类似,因此如果读者不清楚如何创建工程,请仔细阅读本季第一课时。
本例程的基本原理如下。
PS通过AXI GPIO IP核启动PL不间断循环构造16bit位宽的0~1023的数据,利用AXI DMA IP核,通过PS的Slave AXI GP接口传输至PS DDR3的乒乓缓存中。PL每发完一次0~1023,AXI DMA IP核便会产生一个中断信号,PS得到中断信号后将DDR3缓存的数据通过乒乓操作的方式由TCP协议发送至PC机。
8.2搭建硬件系统
8.2.1系统构架
这个系统实际上就是在前面章节DMA的基础上去掉FPGA读DMA通道,只有FPGA往DMA写输入数据,当DMA接收中断产生后,在通过LWIP协议,把数据通过网口发送出去。网口是接在PS的ARM端口的因此,ARM部分也是要把网口配置好。如下图所示。
8.2.1 启用HP接口
双击ZYNQ的IP之后启动HP接口,这里只要用到1个HP接口通道
8.2.2启用PL到PS的中断资源
8.2.3启动PS部分的以太网接口
8.2.4 时钟的设置
将FCLK_CLK0和FCLK_CLK1均设为100Mhz,其中CLK1为PL构造数据部分逻辑的时钟源,在实际应用中可自由调节时钟频率,故与CLK0分开使用。设置如下图所示。
8.2.5 DMA IP 配置
由于只用到了写通道,因次,只要把勾选写DMA通道既可以,如下图所示:
8.2.6 GPIO的配置
双击axi_gpio_0。设置如下图所示
8.2.7配置axi_ data_fifo _0
双击axis_data_fifo_0,设置如下图所示。fifo在本例程中作为axi_dma_0的S_AXIS_S2MM接口所在的FCLK0时钟域与外部数据生成逻辑stream接口所在的FCLK1时钟域之间的转换媒介。
8.2.8设置S_AXIS接口
双击S_AXIS端口,进行如下图设置
8.2.9地址空间映射
打开Address Editor,设置如下图所示
8.3 FPGA的发送代码
表8-3-1
always@(posedge FCLK_CLK1) begin if(!peripheral_aresetn) begin //系统复位 S_AXIS_tvalid <= 1'b0; S_AXIS_tdata <= 32'd0; S_AXIS_tlast <= 1'b0; state <=0; end else begin case(state) //状态机 0: begin if(gpio_tri_o_0&& S_AXIS_tready) begin //当FIFO非满的时候 S_AXIS_tvalid <= 1'b1;//设置写FIFO数据有效标志为1 state <= 1;//转入状态1 end else begin S_AXIS_tvalid <= 1'b0; state <= 0; end end 1:begin if(S_AXIS_tready) begin //当FIFO非满 S_AXIS_tdata <= S_AXIS_tdata + 1'b1; if(S_AXIS_tdata == 16'd1022) begin //从0-1022一共写入1023个字节 S_AXIS_tlast <= 1'b1;//发送最后一个数据 state <= 2; end else begin S_AXIS_tlast <= 1'b0; state <= 1; end end else begin S_AXIS_tdata <= S_AXIS_tdata; state <= 1; end end 2:begin if(!S_AXIS_tready) begin //如果FIFO满,等待 S_AXIS_tvalid <= 1'b1; S_AXIS_tlast <= 1'b1; S_AXIS_tdata <= S_AXIS_tdata; state <= 2; end else begin //写入结束 S_AXIS_tvalid <= 1'b0; S_AXIS_tlast <= 1'b0; S_AXIS_tdata <= 16'd0; state <= 0; end end default: state <=0; endcase end end |
发送代码和本季第一课程的代码一样,每次发送1024X16bit的数据到通过DMA到DDR内存。代码比较简单,如果verilog语法基础不好的,无法对发送时序精确把我的FPGA初学者可以多思考思考,如果还是无法理解可以找我们FPGA技术支持。
8.4 PS部分BSP设置
8.4.1 SDK工程BSP设置
进入sdk后,新建空工程,命名为AXI_DMA_PL_PS_Test,同时创建相应的bsp。
修改AXI_DMA_PL_PS_Test_bsp的设置,使能lwip 1.4.1函数库。如下图所示。
8.4.2 lwip函数库设置
本例程使用RAW API,即函数调用不依赖操作系统。传输效率也比SOCKET API高,(具体可参考xapp1026)。将use_axieth_on_zynq和use_emaclite_on_zynq设为0。如下图所示。
修改lwip_memory_options设置,将mem_size,memp_n_pbuf,mem_n_tcp_pcb,memp_n_tcp_seg这4个参数值设大,这样会提高TCP传输效率。如下图所示。
修改pbuf_options设置,将pbuf_pool_size设大,增加可用的pbuf数量,这样同样会提高TCP传输效率。如下图所示。
修改tcp_options设置,将tcp_snd_buf,tcp_wnd参数设大,这样同样会提高TCP传输效率。如下图所示。
修改temac_adapter_options设置,将n_rx_descriptors和n_tx_descriptors参数设大。这样可以提高zynq内部emac dma的数据迁移效率,同样能提高TCP传输效率。如下图所示。
其余选项的参数默认即可,不用修改。点击OK,重建bsp。
8.5 PS部分程序分析
8.5.1 main.c分析
main函数的主要流程为:
1):初始化并配置PL侧的AXI GPIO
2):初始化并配置PL侧的AXI DMA
3):初始化并配置PS的中断控制器
4):初始化lwip协议栈和PS的以太网控制器
5):配置TCP传输所需的相关参数,并与服务器建立TCP连接
6):通过AXI GPIO启动PL进行数据生成和传输
7):通过AXI DMA接收PL传输的数据,通过TCP发送至PC机,并不断循环该过程。
表8-5-1
/* * * www.osrc.cn * www.milinker.com * copyright by nan jin mi lian dian zi www.osrc.cn * axi dma test * */ #include "dma_intr.h" #include "timer_intr.h" #include "sys_intr.h" #include "xgpio.h" #include "OLED.h" #include "lwip/err.h" #include "lwip/tcp.h" #include "lwipopts.h" #include "netif/xadapter.h" #include "lwipopts.h" static XScuGic Intc; //GIC static XScuTimer Timer;//timer XAxiDma AxiDma; u16 *RxBufferPtr[2]; /* ping pong buffers*/ volatile u32 RX_success; volatile u32 TX_success; volatile u32 RX_ready=1; volatile u32 TX_ready=1; #define TIMER_LOAD_VALUE XPAR_CPU_CORTEXA9_0_CPU_CLK_FREQ_HZ / 8 //0.25S extern void send_received_data(void); extern unsigned tcp_client_connected; char oled_str[17]=""; static XGpio Gpio; #define AXI_GPIO_DEV_ID XPAR_AXI_GPIO_0_DEVICE_ID int init_intr_sys(void) { DMA_Intr_Init(&AxiDma,0);//initial interrupt system Timer_init(&Timer,TIMER_LOAD_VALUE,0); Init_Intr_System(&Intc); // initial DMA interrupt system Setup_Intr_Exception(&Intc); DMA_Setup_Intr_System(&Intc,&AxiDma,0,RX_INTR_ID);//setup dma interrpt system Timer_Setup_Intr_System(&Intc,&Timer,TIMER_IRPT_INTR); DMA_Intr_Enable(&Intc,&AxiDma); } int main(void) { int Status; struct netif *netif, server_netif; struct ip_addr ipaddr, netmask, gw; /* the mac address of the board. this should be unique per board */ unsigned char mac_ethernet_address[] = { 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 }; /* Initialize the ping pong buffers for the data received from axidma */ RxBufferPtr[0] = (u16 *)RX_BUFFER0_BASE; RxBufferPtr[1] = (u16 *)RX_BUFFER1_BASE; XGpio_Initialize(&Gpio, AXI_GPIO_DEV_ID); XGpio_SetDataDirection(&Gpio, 1, 0); init_intr_sys(); TcpTmrFlag = 0; netif = &server_netif; IP4_ADDR(&ipaddr, 192, 168, 1, 10); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 1, 1); /*lwip library init*/ lwip_init(); /* Add network interface to the netif_list, and set it as default */ if (!xemac_add(netif, &ipaddr, &netmask, &gw, mac_ethernet_address, XPAR_XEMACPS_0_BASEADDR)) { xil_printf("Error adding N/W interface\r\n"); return -1; } netif_set_default(netif); /* specify that the network if is up */ netif_set_up(netif); /* initialize tcp pcb */ tcp_send_init(); XGpio_DiscreteWrite(&Gpio, 1, 1); oled_fresh_en();// enable oled Timer_start(&Timer); while (1) { /* call tcp timer every 250ms */ if(TcpTmrFlag) { tcp_tmr(); TcpTmrFlag = 0; } /*receive input packet from emac*/ xemacif_input(netif);//将MAC队列里的packets传输到你的LwIP/IP stack里 /* if connected to the server, start receive data from PL through axidma, then transmit the data to the PC software by TCP*/ if(tcp_client_connected) send_received_data(); } return 0; } |
8.5.2 AXI DMA数据传输过程
例程中axi dma采用了simple transfer方式,通过XAxiDma_SimpleTransfer函数完成。每次dma传输都需要PS主动发起,PS通过AXI总线配置PL侧axi dma内部寄存器,发起一次dma传输。dma传输发起后,axi dma开始通过S_AXIS_S2MM接口接收数据,当其中的tlast信号被拉高,则代表当次传输所需要的数据发送完毕,当该次dma传输结束,axi dma通过s2mm_introut产生中断信号,触发PS中断控制器产生中断,PS通过中断服务函数Dma_RxIsr清除axi dma的中断状态,在DM中断函数中,拉高dma完成指示信号packet_trans_done,一次完整的simple transfer的dma传输结束。下表为dma中断接收函数,接收来自PL的中断信号,并且设置packet_trans_done。
表:8-5-2-1 DMA_RxIntrHandler DMA中断接收函数
/*****************************************************************************/ /* * * This is the DMA RX interrupt handler function * * It gets the interrupt status from the hardware, acknowledges it, and if any * error happens, it resets the hardware. Otherwise, if a completion interrupt * is present, then it sets the RxDone flag. * * @param Callback is a pointer to RX channel of the DMA engine. * * @return None. * * @note None. * ******************************************************************************/ static void DMA_RxIntrHandler(void *Callback) { u32 IrqStatus; int TimeOut; XAxiDma *AxiDmaInst = (XAxiDma *)Callback; /* Read pending interrupts */ IrqStatus = XAxiDma_IntrGetIrq(AxiDmaInst, XAXIDMA_DEVICE_TO_DMA); /* Acknowledge pending interrupts */ XAxiDma_IntrAckIrq(AxiDmaInst, IrqStatus, XAXIDMA_DEVICE_TO_DMA); /* * If no interrupt is asserted, we do not do anything */ if (!(IrqStatus & XAXIDMA_IRQ_ALL_MASK)) { return; } /* * If error interrupt is asserted, raise error flag, reset the * hardware to recover from the error, and return with no further * processing. */ if ((IrqStatus & XAXIDMA_IRQ_ERROR_MASK)) { xil_printf("rx error! \r\n"); return; } /* * If completion interrupt is asserted, then set RxDone flag */ if ((IrqStatus & XAXIDMA_IRQ_IOC_MASK)) { if(packet_trans_done) xil_printf("last transmission has not finished!\r\n"); else /*set the axidma done flag*/ packet_trans_done = 1; } } |
PS的dma数据接收采用了乒乓操作的模式,两个缓冲区交替进行数据接收。
需要注意的是, XAxiDma_SimpleTransfer函数中Length,以字节为单位,每次发起dma时,所设置的Length的值必须大于或等于PL实际传输的数据长度,否则会出现错误。本例程中设置的长度为2048字节。
first_trans_start是为了进行第一次先进行一次DMA中断传输,这样完成后设置first_trans_start为0。以后每次完成网络传输后,再启动DMA接受。
TCP数据包的发送主要依赖于tcp_write和tcp_output两个函数,tcp_write将所需要发送的数据写入tcp发送缓冲区等待发送,tcp_output函数则将缓存区内数据包发送出去。在发送TCP数据包时,这两个函数往往要同时配合使用。
收发送函数的具体源码如下表。
8-5-2:send_received_data() 发送函数源码
void send_received_data() { #if __arm__ int copy = 3; #else int copy = 0; #endif err_t err; int Status; struct tcp_pcb *tpcb = connected_pcb; /*initial the first axdma transmission, only excuse once*/ if(!first_trans_start) { Status = XAxiDma_SimpleTransfer(&AxiDma, (u32)RxBufferPtr[0], (u32)(PAKET_LENGTH), XAXIDMA_DEVICE_TO_DMA); if (Status != XST_SUCCESS) { xil_printf("axi dma failed! 0 %d\r\n", Status); return; } /*set the flag, so this part of code will not excuse again*/ first_trans_start = 1; } /*if the last axidma transmission is done, the interrupt triggered, then start TCP transmission*/ if(packet_trans_done) { if (!connected_pcb) return; /* if tcp send buffer has enough space to hold the data we want to transmit from PL, then start tcp transmission*/ if (tcp_sndbuf(tpcb) > SEND_SIZE) { /*transmit received data through TCP*/ err = tcp_write(tpcb, RxBufferPtr[packet_index & 1], SEND_SIZE, copy); if (err != ERR_OK) { xil_printf("txperf: Error on tcp_write: %d\r\n", err); connected_pcb = NULL; return; } err = tcp_output(tpcb); if (err != ERR_OK) { xil_printf("txperf: Error on tcp_output: %d\r\n",err); return; } packet_index++; /*clear the axidma done flag*/ packet_trans_done = 0; /*initial the other axidma transmission when the current transmission is done*/ Status = XAxiDma_SimpleTransfer(&AxiDma, (u32)RxBufferPtr[(packet_index + 1)&1], (u32)(PAKET_LENGTH), XAXIDMA_DEVICE_TO_DMA); if (Status != XST_SUCCESS) { xil_printf("axi dma %d failed! %d \r\n", (packet_index + 1), Status); return; } } } } |
3.3 TCP发送流程
3.3.1 TCP连接建立
在本例程中,zynq作为客户端,PC作为服务器。由zynq向PC主动发起TCP连接请求,通过tcp_connect函数便可以完成这个过程。该函数的参数包含了一个回调函数指针tcp_connected_fn,该回调函数将在TCP连接请求三次握手完成后被自动调用。该回调函数被调用时代表客户端和服务器之间的TCP连接建立完成。在本例程中,该回调函数被定义为tcp_connected_callback,在该函数中,拉高连接建立完成信号tcp_client_connected,并通过tcp_sent函数配置另一个TCP发送完成的回调函数。该回调函数在每个TCP包发送完成后会被自动调用,代表TCP包发送完成。该回调函数在本例程中被定义为tcp_sent_callback,仅作发送完成数据包的计数。
表tcp_connected_callback 回调函数
static err_t tcp_connected_callback(void *arg, struct tcp_pcb *tpcb, err_t err) { xil_printf("txperf: Connected to iperf server\r\n"); /* store state */ connected_pcb = tpcb; /* set callback values & functions */ tcp_arg(tpcb, NULL); tcp_sent(tpcb, tcp_sent_callback); tcp_client_connected = 1; /* initiate data transfer */ return ERR_OK; } |
表 tcp_sent_callback 回调函数
/*this fuction just used to count the tcp transmission times*/ static err_t tcp_sent_callback(void *arg, struct tcp_pcb *tpcb, u16_t len) { tcp_trans_done ++; return ERR_OK; } |
8.6 连接测试
把开发板网卡通过网线接到PC网口上,修改 IP地址如下图
打开网络调试助手,第一次用的时候windows会提示你是否允许访问网络一定要选择是,否则你就无法通信了。设置电脑为TCP Server 本机IP为刚才设置的192.168.1.209 端口号为7.
在SDK里面打开串口,并且启动SDK调试(调试方法前面已经讲过),板子是client可以看到成功连接到了PCB上。
这个时候可以看到网络调试助手接收到数据了,由于数据量大,刷新数据显示,会导致电脑变慢,这里把勾选暂停显示。右下角是接收数据的计数器,可以看到计数器在飞奔中。
现在检测下网速,可以看到时间速度在500Mbps/s左右查看网速,大概是在62MB/S-70MB/S的速度,当然我们也可以通过优化实现更高速度的传输。
在SDK里面设定内存空间的查看地址,查看内存中的数据,可以看到正是PL 发出的数据。