[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输

一、实验要求

本文基于AN108模块,将ADC采集的数据通过以太网传输到上位机。

二、任务分析

本实验的硬件设计部分及vitis均参照了ALINX FPGA ZYNQ Ultrascale+ MPSOC教程中实验 基于 AN9280模块的 ADC 采集以太网传输,其B站视频链接如下

【62】ALINX Zynq MPSoC XILINX FPGA视频教程 SDK 裸机开发—ADC以太网传输协议_哔哩哔哩_bilibili

首先AD9280模块进行AD转换的模拟信号来源,根据实验二ADDA测试的经验,本实验通过FPGA+DAC产生正弦波,通过同轴电缆自环将正弦波输入到AD9280模块进行AD转换。

 FPGA实验二:ADDA测试_Laid-back guy的博客-CSDN博客https://blog.csdn.net/weixin_45303812/article/details/123762811?spm=1001.2014.3001.5502

然后,我们需要用zynq读取ADC数据,该过程涉及了PL与PS之间的数据交换,对于传输速度要求较高、数据量大、地址连续的场合,如高速AD/DA,可以通过 AXI DMA 来完成。通过在 PL 中添加 AXI DMA IP 核,并利用 AXI_HP 接口完 成高速的数据传输(ZYNQ 提供了两种 DMA,一种是集成在 PS 中的硬核 DMA,另一种是PL中使用的软核AXI DMA IP,关于DMA(Direct Memory Access,直接存储器访问)的具体内容计划另写一篇文章呈现,先挖个坑)

另外,由于ADC采集模块需要将ADC数据发送到DMA,AXI DMA IP 核的接口形式为AXIS接口,因此我们需要创建一个带有 AXI 接口的 IP 核来实现ADC数据的发送。

关于以太网传输方面,采用UDP将ADC采集的数据传输到上位机,因此采用了以下基于UDP传输的通信协议,该协议包含在UDP数据包中:

1、获取板卡信息

①询问命令(共5字节,由上位机通过以太网发送)

字节数

1

4

命令信息

Header

0x00000000或0x00010001

②应答命令(共27字节,由开发板通过以太网发送)

字节数

命令信息

1

Header|0x01

4

0x00010001

6

板卡MAC地址

4

板卡IP地址

1

符号位 0x00:无符号数 0x01 有符号数此功能无效,上位机要求无符号数

1

ADC有效数据长度,如AD9280为8位,即ADC_BITS=8

1

采集一次ADC的字节数,此功能无效,上位机要求数据位宽为两个字节

1

采样通道,此功能上位机未实现,本实验为单通道采样

4

采样率,即采样的频率,程序中设为32MHz

4

缓存的ADC数据长度,单位为字节

2、获取数据

①、控制命令(共19字节,上位机发送数据请求)

字节数

命令信息

1

Header

4

0x00010002

6

板卡MAC地址,确认是本地的MAC地址

4

采样通道(本功能未实现)

4

采样次数(采集数据为8位,采样次数位缓存数据长度的一半)

②、应答命令(共1029字节,由开发板通过以太网发送)

字节数

命令信息

1

Header|0x01

4

0x00010002

1024

ADC数据

每个UDP包都含有 Header,第一个字节其格式及意义如下:

比特位

值(0)

值(1)

bit0

查询或控制

应答

bit1-7

随机数据

开发板应答时,Header|0x01即高7位随机数据不变,bit0设置为1

整个通讯流程分为以下几点:

  • 获取板卡信息:1、上位机发送询问命令;2、开发板应答询问
  • 获取数据:3、上位机发送控制命令请求数据;4、开发板应答发送数据
  • 重复步骤3、4

三、基于 AN108 模块的ADC 采集以太网传输

3.1硬件环境搭建

相关IP核的配置如下所示:

FPGA+DAC产生正弦波的部分分别为一个DDS IP核与ad9708_send模块

1.DDS Compiler

System Clock为100MHz,Parameter Selection为System  Parameter,输出波形为正弦波,频率1MHz,其余保持默认即可。

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第1张图片

2.ad9708_send

 DA数据发送模块通过编写Verilog代码生成ad9708_send RTL模块,其Verilog代码如下所示:

`timescale 1ns / 1ps
module ad9708_send(
     input                 dac_clk    ,  //时钟
     input                 dac_rst_n  ,  //复位信号,低电平有效
     output reg [7:0]            dac_data,
     data to DAC
     input        [7:0]          s_axis_tdata,  
     output                      s_axis_tready,     // 由于DAC的工作频率小于DDS工作频率,所                            以DAC接口控制器给FIFO的RDY信号应该一直为高。
     input                       s_axis_tvalid
     );
 //*****************************************************
 //**                    main code
 //*****************************************************
 
assign  s_axis_tready = 1'b1;
reg [7:0]   dac_buf_data ;  //fifo read data
/* When s_axis_tready and s_axis_tvalid both valid, write data from fifo to da9708_send*/
always@(posedge dac_clk or negedge dac_rst_n)
begin
	if(dac_rst_n == 1'b0)
	begin
	  dac_buf_data <= 8'd0 ;
	end
	else if (s_axis_tready & s_axis_tvalid)
	begin
      dac_buf_data <= s_axis_tdata ;
	end
end

always@(posedge dac_clk or negedge dac_rst_n)
begin
	if(dac_rst_n == 1'b0)
	begin
		dac_data     <= 8'd0 ;
	end
	else
            begin
                  dac_data <= dac_buf_data^'h0x80;   //将读到的DDS数据与0x80取异或赋值给DA数据端口  
            end
end
endmodule

AD数据接受模块引用了ALINX教程中实验第十七章 DMA 使用之 ADC 示波器(AN108)中的自定义IP核ad9280_sample

3.ad9280_sample

本实验引用了ALINX教程第十七章 DMA 使用之 ADC 示波器(AN108)中的ADC自定义IP核,需要注意的是

在模块ad9280_sample_v1_0_S00_AXI例化用户自定义代码ad9280_sample时,将采样长度与开始采样标志分别连接至寄存器地址1对应的数据与寄存器地址0对应数据低1位。

为此需要在Vitis中做如下对应设置,该部分定义于adc_dma.h文件

此外,在用户自定义代码ad9280_sample中使用XPM定义了一个读写深度1024,数据位宽为8的异步FIFO进行跨时钟域数据处理 

xpm_fifo_async #(
   .CDC_SYNC_STAGES      (2),        
   .DOUT_RESET_VALUE     ("1"),      
   .ECC_MODE             ("no_ecc"), 
   .FIFO_MEMORY_TYPE     ("auto"),   
   .FIFO_READ_LATENCY    (1),        
   .FIFO_WRITE_DEPTH     (1024),     
   .FULL_RESET_VALUE     (0),        
   .PROG_EMPTY_THRESH    (10),       
   .PROG_FULL_THRESH     (10),       
   .RD_DATA_COUNT_WIDTH  (11),       
   .READ_DATA_WIDTH      (8),        
   .READ_MODE            ("std"),    
   .RELATED_CLOCKS       (0),        
   .USE_ADV_FEATURES     ("0707"),   
   .WAKEUP_TIME          (0),        
   .WRITE_DATA_WIDTH     (8),        
   .WR_DATA_COUNT_WIDTH  (11)        
)
xpm_fifo_async_inst (
   .rst            (~adc_rst_n),
   .wr_clk         (adc_clk),
   .wr_en          (adc_buf_wr),
   .din            (adc_buf_data),
   .rd_clk         (M_AXIS_CLK),
   .rd_en          (adc_buf_rd),
   .dout           (M_AXIS_tdata),
   .empty          (empty),
   .full           (),
   .almost_empty   (),
   .almost_full    (),
   .wr_data_count  (),
   .rd_data_count  (),    
   .prog_empty     (),
   .prog_full      (),    
   .data_valid     (),
   .dbiterr        (),
   .sbiterr        (),
   .overflow       (),
   .underflow      (),
   .wr_ack         (),   
   .wr_rst_busy    (),   
   .rd_rst_busy    (),
   .injectdbiterr  (1'b0),
   .injectsbiterr  (1'b0),   
   .sleep          (1'b0)   
   );

4.AXI4-Stream Register Slice

查阅官方文档pg085-axi4stream-infrastructure.pdf ,该IP核在Master和Slave接口之间插入了pipeline register,利用两个两个深度寄存器创建时序隔离和流水线主从,大概意思是利用寄存器使时序关系更好,其配置如下图所示,使能TKEEP与TLAST信号,其余保持默认。

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第2张图片

5.AXI Direct Memory Access

本实验AXI DMA IP核采用了效率更高的S/G模式,即Scatter/Gather(分散/聚集)。SG DMA模式允许在单个 DMA 事务中将数据传输到多个存储区域或从多个存储区域传输数据,相当于将多个 Simple DMA 请求链接在一起,因此其效率更高。

配置AXI DMA IP核,使能Enable Scatter Gather Engine时,会出现 M_AXI_SG 接口,用于读写链表。

使能Enable Write Channel

Memory Map Data Width:AXI S2MM存储映射读取总线的数据位宽,设置为64

Stream Data Width:AXI S2MM AXI-Stream数据总线的位宽,设置为8(该值必须小于 Memory Map Data Width)

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第3张图片

6.ZYNQ7 Processing System

关于ZYNQ7 处理系统的配置主要有以下两点需要注意,其他方面及配置方法可以参考ALINX教程中的实验一体验 ARM,裸机输出“Hello World”,这里不再赘述。

打开Ethernet0并使能MDIO(MDIO连接到MIO52、MIO53)

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第4张图片

其次,在Clock Configuration 页面配置PS提供给PL端的时钟PL Fabric Clocks,其余时钟如输入时钟频率(需与核心板上的 PS 端输入时钟频率相同)、CPU 频率、PS 端外设的时钟均保持默认。

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第5张图片

 最终连接图如下图所示:[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第6张图片

 generate output products后create HDL warpper生成顶层文件,随后绑定引脚,包括AD9708及AD9280各自的数据及时钟。

set_property PACKAGE_PIN G18 [get_ports adc_clk]
set_property PACKAGE_PIN L17 [get_ports {adc_data[0]}]
set_property PACKAGE_PIN L16 [get_ports {adc_data[1]}]
set_property PACKAGE_PIN M18 [get_ports {adc_data[2]}]
set_property PACKAGE_PIN M17 [get_ports {adc_data[3]}]
set_property PACKAGE_PIN D20 [get_ports {adc_data[4]}]
set_property PACKAGE_PIN D19 [get_ports {adc_data[5]}]
set_property PACKAGE_PIN E19  [get_ports {adc_data[6]}]
set_property PACKAGE_PIN E18  [get_ports {adc_data[7]}]


set_property IOSTANDARD LVCMOS33 [get_ports adc_clk]
set_property IOSTANDARD LVCMOS33 [get_ports {adc_data[*]}]

set_property PACKAGE_PIN F20 [get_ports {dac_clk}]
set_property PACKAGE_PIN F19  [get_ports {dac_data[7]}]
set_property PACKAGE_PIN G20 [get_ports {dac_data[6]}]
set_property PACKAGE_PIN G19 [get_ports {dac_data[5]}]
set_property PACKAGE_PIN H18 [get_ports {dac_data[4]}]
set_property PACKAGE_PIN J18  [get_ports {dac_data[3]}]
set_property PACKAGE_PIN L20 [get_ports {dac_data[2]}]
set_property PACKAGE_PIN L19 [get_ports {dac_data[1]}]
set_property PACKAGE_PIN M20  [get_ports {dac_data[0]}]

set_property IOSTANDARD LVCMOS33 [get_ports dac_clk]
set_property IOSTANDARD LVCMOS33 [get_ports {dac_data[*]}]

 对该设计进行综合、实现并生成 Bitstream 文件,随后在菜单栏选择 File > Export > Export hardware导出硬件信息

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第7张图片

硬件导出完成后,在菜单栏中选择 File > Launch Vitis,选择导出的硬件信息所在文件夹,启动 Vitis 开发环境。

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第8张图片

3.2 Vitis 程序开发

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第9张图片

src文件夹下包含了平台相关文件platform.h、platform.c、platform_config.h、platform_zynq.c

主函数main.c

adc采集相关宏定义文件adc_dma.h

dma配置相关文件dma_bd.c、dma_bd.h

ZYNQ中断服务相关文件zynq_interrupt.c、zynq_interrupt.h

lwip 控制部分相关文件lwip_app.c

下面将对涉及的关键代码部分进行分析

1.main.c

在 main.c 文件的 main 函数中,调用函数init_platform(),中断初始化,进行 DMA 的初始化,中断连接,建立 BD链表,调用函数lwip_loop(),调用函数cleanup_platform()

int main()
{
    init_platform();

    InterruptInit(INT_DEVICE_ID,&XScuGicInstance);
    /* Initialize DMA */
    XAxiDma_Initial(DMA_DEV_ID, S2MM_INTR_ID, &AxiDma, &XScuGicInstance) ;
    /* Interrupt register */
    InterruptConnect(&XScuGicInstance,S2MM_INTR_ID,Dma_Interrupt_Handler, &AxiDma,0,3);
    /* Create BD chain */
    CreateBdChain(BdChainBuffer, BD_COUNT, ADC_SAMPLE_NUM, (unsigned char *)DmaRxBuffer, RXPATH) ;

    lwip_loop();
    cleanup_platform();
    return 0;
}

其中函数init_platform与cleanup_platform均定义于platform_zynq.c文件,前者用于使能caches和初始化uart,后者的作用是取消使能caches。

cleanup_platform()代码如下,由于涉及了DMADcache一致性的问题,该程序中采用的方法是直接将cache关闭,使CPU或PL直接更新ddr中的数据。

void cleanup_platform()
{
	Xil_ICacheDisable();
	Xil_DCacheDisable();
	return;
}

关于DMADcache的一致性可参考以下文章。

Xilinx的Zynq系列,ARM和PL通过DMA通信时如何保证DDR数据的正确性。_拾贝壳的大男孩的博客-CSDN博客

其余函数的分析会在其定义的文件中再做分析。

此外,在main.c文件中定义了4个函数并将s2mm_flag赋值为-1

int s2mm_flag = -1 ;


int XAxiDma_Initial(u16 DeviceId, u16 IntrID, XAxiDma *XAxiDma, XScuGic *InstancePtr) ; //DMA初始化函数
void Dma_Interrupt_Handler(void *CallBackRef);//DMA中断服务函数
void XAxiDma_Adc(u32 *BdChainBuffer, u32 adc_addr, u32 adc_len, u16 BdCount, XAxiDma *AxiDma) ;//DMA传输数据
void ad9280_sample(u32 adc_addr, u32 adc_len) ;//adc采集

①DMA初始化XAxiDma_Initial()

该函数主要包含的操作有:

CfgPtr = XAxiDma_LookupConfig(DeviceId);//进行DMA配置参数传递.

通过调用DMA查找配置函数,传入设备ID,获取设备参数并将返回这赋值给CfgPtr 。

Status = XAxiDma_CfgInitialize(XAxiDma, CfgPtr);//初始化DMA引擎

PS对DMA进行真正的配置初始化过程,通过axidma存储在PS端的AXI_DMA配置表,根据对PL参数的读取,PS运行对PL侧的DMA配置,这个配置过程是通过GP0接口对AXI_Lite4总线的控制完成的。

XAxiDma_IntrEnable(XAxiDma, XAXIDMA_IRQ_IOC_MASK,XAXIDMA_DEVICE_TO_DMA);	//使能DMA中断

由于实验需要,使能SS2M中断,关闭使能MM2S中断。

②DMA中断服务函数Dma_Interrupt_Handler()

该函数主要包含的操作有:

s2mm_sr = XAxiDma_IntrGetIrq(XAxiDmaPtr, XAXIDMA_DEVICE_TO_DMA) ;//读取待处理的中断
XAxiDma_IntrAckIrq(XAxiDmaPtr, XAXIDMA_IRQ_IOC_MASK,AXIDMA_DEVICE_TO_DMA) ;//确认待处理的中断

以及令s2mm_flag = 1;

③DMA传输数据函数XAxiDma_Adc()

/* Clear BD Status */
Bd_StatusClr(BdChainBuffer, BdCount) ;//调用DMA状态清除函数
/* Start sample */
ad9280_sample(adc_addr, adc_len)  ;//调用ADC采集数据函数
/* start DMA translation from ADC channel 0 to DDR3 */
Bd_Start(BdChainBuffer, BdCount, AxiDma, RXPATH) ;//调用配置DMA寄存器启动函数

该函数主要包含以上三个操作,此外,该函数将在函数lwip_loop()中被调用

④ADC采集数据函数ad9280_sample()

该函数代码如下:

void ad9280_sample(u32 adc_addr, u32 adc_len)
{
	/* provide length to AD9280 module */
	AD9280_SAMPLE_mWriteReg(adc_addr, AD9280_LENGTH, adc_len)  ;
	/* start sample AD9280 */
	AD9280_SAMPLE_mWriteReg(adc_addr, AD9280_START, 1) ;
}

按住Ctrl键单击函数AD9280_SAMPLE_mWriteReg(),即可在文件ad9280_sample.h中找到该函数的解释如下:

#define AD9280_SAMPLE_mWriteReg(BaseAddress, RegOffset, Data) \
  	Xil_Out32((BaseAddress) + (RegOffset), (u32)(Data))

Vitis中,对于寄存器的读和写可通过函数Xil_Out32()和Xil_In32()操作。此外,IP 核封装完成后,Vivado 软件会在 IP 核所在路径(...\ad9280_sample\drivers\ad9280_sample_v1_0\src)目录下自动生成.c 和.h 文件,方便在Vitis软件中对 IP 核进行操作,如下图所示:

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第10张图片

2.dma_bd.c

该文件主要定义了以下几个函数

①创建链表函数CreateBdChain()

int CreateBdChain(u32 *BdDesptr, u16 BdCount, u32 TotalByteLen, u8 *DmaBufferPtr, u32 Direction)//创建描述符(bd)

CreateBdChain 为创建链表函数,根据DMA传输字节总长度TotalByteLen,将连续DMA缓冲地址空间分为BdCount份,每一份空间即一个 Descriptor。由于本实验是 RXPATH,不需要配置TXSOF 和 TXEOF,因此只配置NEXDESC,BUFFER_ADDRESS,CONTROL 三部分。

②配置寄存器启动SG DMA Bd_Start()

int Bd_Start(u32 *BdDesptr, u16 BdCount, XAxiDma *XAxiDmaPtr, u32 Direction)//开始BD提取

将当前Descriptor地址写入CURDESC寄存器,将Frame的开始、结束、每个 Pachage的长度信息写入DMACR寄存器(DMA控制寄存器),将尾部Descriptor地址写入TAILDESC寄存器,写入TAILDESC后,SG将开始提取描述符

③状态清除Bd_StatusClr()


void Bd_StatusClr(u32 *BdDesptr, u16 BdCount) //状态清除函数

在BD_ Start之前清除BD status,否则SG将不会启动

3.ZYNQ中断程序zynq_interrupt.c

中断程序里定义了 3 个函数,分别为中断系统建立函数,中断初始化函数,以及中断响应函数连接。

①中断系统建立函数

int InterruptSystemSetup(XScuGic *XScuGicInstancePtr)//寄存器GIC中断处理器

该函数调用了Xilinx提供的通用异常处理函数Xil_ExceptionRegisterHandler(),以及使能异常处理函数Xil_ExceptionEnable()来建立中断系统

中断初始化函数

int InterruptInit(u16 DeviceId,XScuGic *XScuGicInstancePtr)

该函数分别调用了

查询中断配置函数XScuGic_LookupConfig()

初始化SCU GIC配置XScuGic_CfgInitialize()

寄存器GIC中断处理器函数InterruptSystemSetup() 

③中断响应函数连接InterruptConnect()

int InterruptConnect(XScuGic *XScuGicInstancePtr,u32 Int_Id,void * Handler,void *CallBackRef,u8 Priority, u8 Trigger)

该函数分别调用了

断服务函数的注册XScuGic_Connect(),在中断源的中断Id和识别中断时需运行的相关处理程序之间建立连接,

设置中断优先级及中断触发方式XScuGic_SetPriorityTriggerType(),

SCU GIC使能函数XScuGic_Enable()

4.lwip 控制lwip_app.c

关于初始化以及dhcp的部分不再赘述。

①udp初始化start_udp()

该函数通过调用三个函数完成了udp服务的创建。

/*创建新pcb,绑定pcb和端口,设置回调功能*/
int start_udp(unsigned int port) {
	err_t err;
	udp8080_pcb = udp_new();//创建可用于UDP通信的新UDP pcb
	if (!udp8080_pcb) {
		xil_printf("Error creating PCB. Out of Memory\n\r");
		return -1;
	}
	/* bind to specified @port */
	err = udp_bind(udp8080_pcb, IP_ADDR_ANY, port); //  将UDP pcb与本地地址ipaddr和端口绑定。
	if (err != ERR_OK) {
		xil_printf("Unable to bind to port %d: err = %d\n\r", port, err);
		return -2;
	}
	udp_recv(udp8080_pcb, udp_recive, 0);为UDP PCB设置接收回调。当接收到pcb的数据报时,将调用此回调。入口参数分别为:设置recv回调的pcb、回调函数的函数指针、传递给回调函数的附加参数
	IP4_ADDR(&target_addr, 192,168,1,35);

	return 0;
}

在此函数中,首先利用 udp_new 创建一个 pcb 结构体,返回类型为 udp_pcb 的指针,并赋值给 udp8080_pcb; 利用 udp_bind函数绑定端口号和本地IP地址(入口参数分别为pcb 结构体变量,IP 地址,端口号)。利用 udp_recv 函数绑定回调函数,本实验中绑定的回调函数为udp_receive,回调函数实现服务端的功能,其代码如下

②回调函数udp_receive()

void udp_recive(void *arg, struct udp_pcb *pcb, struct pbuf *p_rx,
		const ip_addr_t *addr, u16_t port) {
	char *pData;
	char buff[28];
	if (p_rx != NULL)
	{
		pData = (char *) p_rx->payload;

		if (p_rx->len >= 5) {
			if (((pData[0] & 0x01) == 0) && (pData[1] == 0x00)
					&& ((pData[2] == 0x01) || (pData[2] < 0x00))
					&& (pData[3]) == 0x00)
			{
				if (pData[4] == 1)//上位机发送询问命令
				{
					buff[0] = pData[0] | 1;
					buff[1] = 0x00;
					buff[2] = 0x01;
					buff[3] = 0x00;
					buff[4] = 0x01;
					//MAC address
					memcpy(buff + 5, mac_export, 6);  //拷贝函数
					memcpy(buff + 5 + 6, ip_export, 4);
					buff[15] = 1;
					buff[16] = ADC_BITS;
					buff[17] = 2;
					buff[18] = 1;
					buff[19] = (unsigned char) (32000000 >> 24);
					buff[20] = (unsigned char) (32000000 >> 16);
					buff[21] = (unsigned char) (32000000 >> 8);
					buff[22] = (unsigned char) (32000000 >> 0);
					buff[23] = (unsigned char) (MAX_SEND_LEN >> 24);
					buff[24] = (unsigned char) (MAX_SEND_LEN >> 16);
					buff[25] = (unsigned char) (MAX_SEND_LEN >> 8);
					buff[26] = (unsigned char) (MAX_SEND_LEN >> 0);
					transfer_data(buff, 27, addr);
				}
				else if (pData[4] == 2)上位机发送控制命令请求数据
				{
					int sample_len = (pData[15] << 24) | (pData[16] << 16)
											| (pData[17] << 8) | (pData[18] << 0);//记录采样次数

					TargetHeader[0] = pData[0] | 1;
					target_addr.addr = addr->addr;
					FrameLengthCurr = (FrameLengthCurr == 0)? sample_len*2 :FrameLengthCurr;
				}
			}
		}
		pbuf_free(p_rx);
	}
}

回调函数udp_receive()的功能为接收上位机 的udp 命令,并判断是否是自定义的协议。如果是询问命令,启动应答。如果是控制命令, 根据命令利用FrameLengthCurr请求发送ADC数据。

上位机发送询问命令,将命令均写入buff后,通过transfer_data()函数发送至上位机

int transfer_data(const char *pData, int len, const ip_addr_t *addr)// 回答上位机命令

该函数主要利用udp_sendto()来实现发送数据

Udp通信中的发送函数

udp_sendto(struct udp_pcb *pcb, struct pbuf *p,const ip_addr_t *dst_ip, u16_t dst_port)

入口参数

pcb

p

dst_ip

dst_port

解析

用于UDP pcb发送的数据

要发送的pbuf链

目标ip地址

目标端口

/* start the application (web server, rxtest, txtest, etc..) */
	start_udp(8080);

	/* Start ADC channel 0 */
	XAxiDma_Adc(BdChainBuffer, AD9280_BASE, ADC_SAMPLE_NUM, BD_COUNT, &AxiDma) ;

	/* receive and process packets */
	while (1) {
		xemacif_input(echo_netif);
		/* Wait for times */
		usleep(1000) ;
		/* Check current frame length */
		if (FrameLengthCurr > 0)//判断是否接收到请求ADC数据的控制命令
		{
			/* Check if DMA completed */
			if (s2mm_flag >= 0)//判断DMA中断是否完成
			{
				int udp_len;
				Xil_DCacheInvalidateRange((u32) DmaRxBuffer, FrameLengthCurr);//刷新cache
				for(int i = 0 ; i < ADC_SAMPLE_NUM ; i++)
				{
					DmaBufferTmp[i] = (short)(DmaRxBuffer[i] - 128) ;	//无符号数转换为有符号数,并扩展为short类型
				}

				/* Separate data into package */
				for (int i = 0; i < FrameLengthCurr; i += 1024)
				{
					if ((i + 1024) > FrameLengthCurr)
						udp_len = FrameLengthCurr - i;
					else
						udp_len = 1024;

					send_adc_data((const char *) DmaBufferTmp + i, udp_len);//将ADC数据分包发送
				}
				/* Start ADC channel 0 */
				XAxiDma_Adc(BdChainBuffer, AD9280_BASE, ADC_SAMPLE_NUM, BD_COUNT, &AxiDma) ;
				/* Clear DMA flag and frame length */
				s2mm_flag = -1;
				FrameLengthCurr = 0;
			}
		}
	}

	/* never reached */
	cleanup_platform();

该部分为定义在lwip_app.c中的lwip_loop()函数主要功能实现部分,通过一个while循环来持续判断是否接收到上位机发送的请求数据的控制命令,如果FrameLengthCurr > 0,执行if语句内的代码段并通过send_adc_data()函数将ADC数据分包发送

int send_adc_data(const char *frame, int data_len)

send_adc_data ()函数用于发送 ADC 数据到上位机

至此,代码的分析部分到此结束,需要注意的是上位机设置的缓存大小为1MB,由于上位机固定为数据位宽为两个字节,且为无符号位,因此在 adc_dma.h 中将 AD9280 的采集数设置为 1024*512ADC_BYTE 设为2并将数据转换为两个字节,有符号数。

#define ADC_BYTE           2              /* ADC data byte number */

#define ADC_SAMPLE_NUM  (1024*512)

此外,需要添加lwip211,dhcp_options 打开 dhcp 功能,pbuf_options 选项将 pbuf_pool_size 设置大一些,增大缓存空间,提高效率 

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第11张图片

四、实验结果及分析验证

4.1板上验证

1.连接开发板如图所示

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第12张图片

2.本实验将开发板直连到PC,没有DHCP服务器,默认开发板IP地址为192.168.1.10.通过PC的网络和Internet设置将PC的IP地址设为同一网段

 ​​​​​[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第13张图片

 3.下载程序至开发板,在putty中查看打印信息如下

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第14张图片

4.通过网络调试助手NetAssist进行收发信息

参考文章开头基于udp的通信协议,发送询问命令28 00 01 00 01后,上位机回传应答命令共27字节信息,包含板卡MAC地址、IP地址,ADC有效数据长度等,可参考通信协议进行一一对应 

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第15张图片

随后,发送控制命令请求数据 28 00 01 00 02 00 0A 35 00 01 02 00 00 00 00 00 08 00 00,板卡回传应答命令,包含28|0x01,00 01 00 02以及1024字节的ADC数据[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第16张图片

5.最后,通过 ALINX提供的示波器.exe工具可粗略观察波形如下图所示

[ZYNQ]开发之基于 AN108 模块的ADC 采集以太网传输_第17张图片

五、参考资料

[1]course_s2_ALINX_ZYNQ(AX7010_AX7020)开发平台Vitis应用教程V1.04

[2]领航者ZYNQ之嵌入式SDK开发指南_V2.0

[3]ALINX Zynq MPSoC XILINX FPGA视频教程 SDK 裸机开发—ADC以太网传输协议_

[4]对ZYNQ设备GPIO中断函数的详解 (一)_大千SS的博客-CSDN博客

你可能感兴趣的:(ZYNQ开发之从入门到入土,fpga开发,arm开发)