本帖最后由 dongguo100 于 2025-11-27 15:19 编辑
在嵌入式系统设计中,高速数据采集与实时传输一直是工程师面临的挑战,特别是在电力监测、工业自动化等领域,需要同时处理多通道高精度ADC数据并将其可靠地传输到上位机。传统方案往往面临传输带宽不足、CPU负载过高等问题。
本文将深入探讨基于ZYNQ芯片的AXI DMA技术,如何实现ADC数据到PS端以太网的高速传输,并分享实战中的关键要点。
一、AXI DMA:ZYNQ数据传输的加速引擎
AXI DMA是Xilinx提供的重要IP核,专为高速数据搬运而生。与传统CPU参与每次数据传输的方式不同,DMA允许外设直接访问内存,大大减轻了CPU的负担。
在ZYNQ芯片中,AXI DMA利用PS(处理系统)和PL(可编程逻辑)之间的高性能AXI总线,实现了数据在PL端和PS端DDR内存之间的高效传输。其最大优势在于“直接内存访问”机制,数据传输不需要CPU频繁介入,从而实现了更高的传输效率。AXI DMA本质是在AXI4内存映射接口和AXI4-Stream接口之间实现高速数据传输,基于AXI DMA传输数据的示意图如下所示:
AXI DMA支持可选的Scatter/Gather功能,通过硬件自动化实现非连续内存的高效数据传输,从而减少CPU的干预。
分散(Scatter):将连续的数据流分割并写入多个非连续的内存区域。例如,网络数据包的不同部分(协议头、载荷)可能存储在不同物理地址的缓冲区中。
聚集(Gather):从多个非连续的内存区域收集数据,合并为连续的数据流。例如,视频处理中需要将分散存储的YUV平面数据合并为完整帧。
与传统DMA的对比:普通DMA仅支持连续地址空间的传输,而Scatter/Gather模式通过描述符链表(Descriptor List)管理多段传输,实现非连续内存的自动化操作。
AXI DMA支持三种模式:
1.Direct Register Mode:也称为Simple DMA模式,通过AXI4-Lite接口直接配置源地址、目的地址和传输长度寄存器,无需额外的描述符管理,适用于单次大批量连续数据传输,如ADC采样数据和存储等。
2.Scatter/Gather(SG)Mode:分散/聚集模式,通过内存中的Buffer Descriptor(BD)定义多段非连续传输任务,支持自动链式处理,适用于多段非连续传输任务,如网络数据包处理(分离包头与负载)和视频帧处理(YUV平面数据分散存储)等。
3.Cyclic DMA Mode:循环模式,基于SG模式的扩展,在遇到尾描述符(TAILDESC)时自动跳回首描述符,形成循环传输,无需CPU的干预即可实现无限循环传输,适用场景于实时信号采集,如雷达信号的持续缓存等。
三种模式的对比表如下所示:
关于AXI DMA更详细的介绍,可以参考Xilinx官方文档:AXI DMA LogiCORE IP Product Guide(PG021)。
二、UDP协议介绍
本次案例使用UDP协议将ADC数据传输给上位机,UDP协议是TCP/IP协议栈的传输层协议,是一个简单的面向数据报的协议。UDP不提供数据包的分组与组装功能,也无法对数据包进行排序;且报文发送后,无法确认其是否安全、是否完整到达(即网络性能较差时,易出现数据丢失问题)。UDP除了这些缺点外肯定有它自身的优势,由于UDP不属于连接型协议,因而消耗资源小,处理速度快,所以通常在音频、视频和普通数据传输时使用UDP较多。UDP数据报结构如下表所示:
UDP首部有8个字节,由4个字段构成,每个字段都是两个字节,这些字段的作用如下:
①源端口:源端口号,需要对方回信时选用,不需要时全部置0。
②目的端口:目的端口号,在终点交付报文的时候需要用到。
③长度:UDP的数据报的长度(包括首部和数据)其最小值为8(只有首部)。
④校验和:检测UDP数据报在传输中是否有错,有错则丢弃。
UDP报文由UDP首部+数据区域组成,UDP协议是位于传输层,该层是应用层的下一层,当用户发送数据时候,需要选择使用哪种协议发送出去,如果使用UDP协议,则UDP协议就会简单的把数据封装起来,UDP报文结构如下图所示:
Vitis软件中集成了LWIP库,所以我们可以很方便的在ZYNQ上实现UDP协议。
三、ZYNQ开发平台
本次案例采用的ZYNQ开发平台是正点原子领航者开发板,主控芯片型号是XC7Z020CLG400-2I。该芯片集成双核ARM Cortex-A9处理器,主频可达766MHz。XC7Z020CLG400-2I包含85K LC,4.9Mb BRAM。开发板存储方面包含1GB PS DDR3、8GB eMMC、32MB QSPI Flash和8KB EEPROM。非常适合测量仪表、工业控制、智能车载、图像显示和医疗器械等领域。
开发板由核心板+底板组成,外设资源丰富,板载1路PS端千兆以太网接口、1路PL端千兆以太网接口、1路HDMI输出接口、4路USB2.0 Host接口、一路USB Slave接口、1个RGB LCD接口、1个DVP摄像头接口、1路USB UART(PS和PL共用)、1路音频输入接口、1路音频输出接口、1路RS232、1路RS485和1路CAN接口等。
开发板提供了丰富的开发文档和软件资源,涉及FPGA开发、Vitis裸机开发、Linux系统开发和PYNQ开发等教学领域。企业客户可以直接采购核心板进行自己的产品研发,正点原子提供了全面、完善的例程和文档,助力企业客户产品研发。为提高企业用户的开发效率、缩短开发周期,正点原子特地为核心板用户整理了一系列开发阶段会用到的资料,涉及原理图、底板设计资料、机械结构、元器件封装、连接器规格、出厂系统镜像源码、编译器、软件包等,方便企业用户开发。
领航者ZYNQ开发板购买链接:(复制链接至浏览器打开)
https://detail.tmall.com/item.htm?id=609032204975
领航者ZYNQ开发板资料链接:(复制链接至浏览器打开)
http://www.openedv.com/docs/boards/fpga/zdyz_linhanz(V2).html
四、准备工作
1)一款ZYNQ开发板,本文以正点原子领航者7020开发板为例进行介绍;
2)高速ADDA模块,本文以正点原子高速AD/DA模块MO8008为例进行介绍;
3)DDS信号发生器(可选);
4)网线;
注:本案例的完整工程会分享在文末。
五、Vivado BlockDesign的工程搭建
本次案例实现的功能是将数据从PL端到PS端,再到PC的完整传输与显示的链路:通过PL端的AXI DMA IP核将ADC数据写入到PS端的DDR,再借助PS的以太网接口将数据实时上传至PC上位机显示波形。
由前文对AXI DMA的介绍可知,AXI DMA IP核将AXI4-Stream数据流转成AXI4接口数据以实现数据搬运,所以我们需要将ADC的数据转换成AXI4-Stream数据流,这个转换的操作由adc_to_axis模块完成。此外,在转换的过程中需要缓存ADC数据,并对数据做跨时钟域的处理,所以本次案例还需要包含一个FIFO IP核。
而PS端主要配置了PS端的网口,使用以太网控制器实现UDP发送ADC数据的功能,将数据发给上位机实时显示ADC波形,整个系统框图如下图所示:
需要注意的是,高速ADDA模块需要一个模拟输入源,这个可以由DDS信号发生器产生,也可以由高速ADDA模块的DA芯片产生,所以在上图的系统框图中,我们还添加了一个da_wave_send模块,用于产生DA芯片所需的数据。
PS通过AXI GPIO IP核来配置adc_to_axis模块什么时候开始传输采集到的ADC数据,当开始采集后,首先将8位的ADC数据缓存至FIFO,然后从FIFO中读出32位的数据,即FIFO IP核的位宽设置是8位进32位出,这个设置和AXI DMA IP核传输的数据位宽一致,以提高AXI DMA IP核搬运数据的效率。
由系统框图可知,本次案例只有adc_to_axis模块和da_wave_send模块是我们编写的Verilog代码,其余都是Xilinx官方的IP核,接下来简单介绍下这两个模块。
adc_to_axis模块:这个模块实现了将输入的ADC数据转成AXI4-Stream接口数据,方便跟AXI DMA IP核进行连接,只有当FIFO IP核中的数据缓存至预设值之后,才开始向AXI DMA IP核传输数据(拉高tvalid的信号)。
da_wave_send模块:这个模块主要是为了控制DA芯片输出正弦波的模拟信号,目的是为了在没有DDS信号发生器的情况下,也可以通过DA芯片产生模拟量,连接到高速ADDA模块的AD输入端,来完成本次案例。
这里重点介绍下AXI DMA IP核的配置,配置如下图所示:
需要注意的是,本次案例只需要将PL端的ADC数据通过AXI DMA IP核写入到PS的DDR中,而不需要从PS DDR中读出数据到PL,所以我们只需要使用AXI DMA的写功能。为了方便后续对例程进行扩展,比如网口下发数据通过DA芯片进行输出,所以这里同时勾选了读写两个通道。
Vivado工程搭建完成后的模块连接图如下图所示:
六、软件设计
软件程序主要完成对AXI DMA的配置,将ADC数据搬运至PS DDR中缓存;随后通过PS端的以太网控制器读取DDR中的ADC数据,并发送至上位机以实现波形显示。
Vitis C代码结构如下图所示:
接下来我们介绍下main.c代码中的main函数,main函数部分代码如下:51 int main(void) 52 { 53 axi_dma_cfg(); // 配置AXI DMA 54 Init_Intr_System(&Intc); // 初始化中断控制器 55 Setup_Intr_Exception(&Intc); // 启用来自硬件的中断 56 dma_setup_intr_system(&Intc); // 建立DMA中断系统 57 58 lwip_udp_init(); // UDP通信配置 59 60 //初始化PL端 AXI GPIO驱动 61 XGpio_Initialize(&axi_gpio_inst, AXI_GPIO_DEVICE_ID); 62 //设置 AXI GPIO 通道 1方向为输入 63 XGpio_SetDataDirection(&axi_gpio_inst, KEY_CHANNEL1, 0); 64 //设置AXI GPIO引脚为高电平,开始采集ADC数据 65 XGpio_DiscreteWrite(&axi_gpio_inst,KEY_CHANNEL1,1); 66 67 //接收和处理数据包 68 while (1) { 69 70 xemacif_input(netif); 71 72 if(dma_start_flag == 0){ 73 axi_dma_start(MAX_PKT_LEN); 74 dma_start_flag = 1; 75 } 76 77 //DMA搬运1024个数据完成后,网口就可以从DDR中取数据进行发送了 78 if(rx_done){ 79 Xil_DCacheFlushRange((UINTPTR) rx_buffer_ptr, MAX_PKT_LEN); 80 if(start_flag){ 81 udp_tx_data(rx_buffer_ptr,MAX_PKT_LEN); 82 } 83 rx_done = 0; 84 dma_start_flag = 0; 85 } 86 } 87 return 0; 88 } 第53行至56行调用的都是AXI DMA的配置与使用的相关函数。
第58行代码是调用UDP通信配置函数lwip_udp_init(),该函数用于初始化lwIP协议栈,配置网络接口,并启动一个UDP服务器应用程序。
第61行至63行代码是对AXI GPIO进行初始化,并设置方向为输出;第65行代码是将AXI GPIO输出的端口设置为1,表示PL端开始接收ADC采集到的数据。
第70行的xemacif_input()函数是网络接口输入函数,用于处理不同类型的以太网MAC设备的输入数据包。这个函数的设计体现了对于不同硬件设备的灵活处理能力,它能够根据设备类型动态调用不同的接口输入函数,从而实现对各种设备的兼容性。
第72行至74行用于判断是否开启AXI DMA的传输,如果dma_start_flag的值等于0,则开启AXI DMA传输,并将dma_start_flag的值赋值为1。
第78行至84行代码是接收到ADC的数据后,通过调用udp_tx_data()函数将ADC的数据发送给上位机。
1 /***************************** Include Files*********************************/ 2 #include "axi_dma.h" 3 4 /************************** ConstantDefinitions *****************************/ 5 #defineDMA_DEV_ID XPAR_AXIDMA_0_DEVICE_ID 6 #defineRX_INTR_ID XPAR_FABRIC_AXIDMA_0_S2MM_INTROUT_VEC_ID 7 #defineINTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID 8 #defineDDR_BASE_ADDR XPAR_PS7_DDR_0_S_AXI_BASEADDR //0x00100000 9 #defineMEM_BASE_ADDR (DDR_BASE_ADDR+ 0x1000000) //0x01100000 10 #defineRX_BUFFER_BASE (MEM_BASE_ADDR+ 0x00300000) //0x01400000 11 #defineRESET_TIMEOUT_COUNTER 10000 //复位时间 12 13 /************************** Variable Definitions*****************************/ 14 15 static XAxiDma axidma; //XAxiDma实例 16 volatile int rx_done=0; //接收完成标志 17 volatile int error; //传输出错标志 18 u8 *rx_buffer_ptr; 19 20 /************************** Function Definitions*****************************/ 21 22 intaxi_dma_cfg(void) 23 { 24 25 intstatus; 26 27 XAxiDma_Config *config; 28 29 rx_buffer_ptr =(u8 *) RX_BUFFER_BASE; 30 31 xil_printf("\r\n---Entering axi_dma_cfg --- \r\n"); 32 33 config =XAxiDma_LookupConfig(DMA_DEV_ID); 34 if(!config) { 35 xil_printf("Noconfig found for %d\r\n", DMA_DEV_ID); 36 return XST_FAILURE; 37 } 38 39 //初始化DMA引擎 40 status =XAxiDma_CfgInitialize(&axidma, config); 41 if(status != XST_SUCCESS) { 42 xil_printf("Initializationfailed %d\r\n", status); 43 return XST_FAILURE; 44 } 45 46 if(XAxiDma_HasSg(&axidma)) { 47 xil_printf("Deviceconfigured as SG mode \r\n"); 48 return XST_FAILURE; 49 } 50 51 xil_printf("AXIDMA CFG Success\r\n"); 52 return XST_SUCCESS; 53 54 } 第22行至第54行代码是对AXI DMA引擎进行初始化,如果AXI DMA使能了SG模式,会返回错误。
56 //启用AXI DMA 57 intaxi_dma_start(u32 pkt_len) 58 { 59 intstatus; 60 //初始化标志信号 61 error = 0; 62 63 status =XAxiDma_SimpleTransfer(&axidma, (UINTPTR)rx_buffer_ptr, 64 pkt_len,XAXIDMA_DEVICE_TO_DMA); 65 if(status != XST_SUCCESS) { 66 xil_printf("AXIDMA Start FAILURE\r\n"); 67 return XST_FAILURE; 68 } 69 return XST_SUCCESS; 70 } 第56行至第70行代码是对AXI DMA引擎进行初始化,如果AXI DMA使能了SG模式,会返回错误。
72 //DMA RX中断处理函数 73 voidrx_intr_handler(void*callback) 74 { 75 u32 irq_status; 76 inttimeout; 77 XAxiDma *axidma_inst= (XAxiDma *) callback; 78 79 irq_status =XAxiDma_IntrGetIrq(axidma_inst,XAXIDMA_DEVICE_TO_DMA); 80 XAxiDma_IntrAckIrq(axidma_inst, irq_status,XAXIDMA_DEVICE_TO_DMA); 81 82 //Rx出错 0x00004000 /**< Errorinterrupt */ 83 if((irq_status & XAXIDMA_IRQ_ERROR_MASK)){ 84 error =1; 85 xil_printf("XAxiDmaerror"); 86 XAxiDma_Reset(axidma_inst); 87 timeout =RESET_TIMEOUT_COUNTER; 88 while (timeout) { 89 if(XAxiDma_ResetIsDone(axidma_inst)) 90 break; 91 timeout-= 1; 92 } 93 return; 94 } 95 //Rx完成 0x00001000 /**< Completion intr */ 96 if((irq_status & XAXIDMA_IRQ_IOC_MASK)) 97 rx_done =1; 98 99 irq_status =XAxiDma_IntrGetIrq(axidma_inst,XAXIDMA_DEVICE_TO_DMA); 100 } 第72行至第100行代码是AXI DMA的中断处理函数,当接收到AXI DMA的中断后,拉高rx_done信号,表示AXI DMA传输完成,最后清除中断状态标志位。
102 //建立DMA中断系统 103 // @param int_ins_ptr是指向XScuGic实例的指针 104 // @param AxiDmaPtr是指向DMA引擎实例的指针 105 // @param rx_intr_id是RX通道中断ID 106 // @return:成功返回XST_SUCCESS,否则返回XST_FAILURE 107 int dma_setup_intr_system(XScuGic* int_ins_ptr) 108 { 109 intstatus; 110 //设置优先级和触发类型 111 XScuGic_SetPriorityTriggerType(int_ins_ptr,RX_INTR_ID, 0xA0,0x3); 112 113 //为中断设置中断处理函数 114 status =XScuGic_Connect(int_ins_ptr, RX_INTR_ID, 115 (Xil_InterruptHandler)rx_intr_handler, &axidma); 116 if(status != XST_SUCCESS) { 117 return status; 118 } 119 XScuGic_Enable(int_ins_ptr,RX_INTR_ID); 120 121 //使能DMA中断 122 XAxiDma_IntrEnable(&axidma,XAXIDMA_IRQ_ALL_MASK,XAXIDMA_DEVICE_TO_DMA); 123 124 return XST_SUCCESS; 125 } 第102行至第122行代码是配置AXI DMA的中断,并使能中断。
七、上板验证
将高速AD/DA模块插入开发板的J3扩展口,连接时注意扩展口电源引脚方向和高速AD/DA模块的电源引脚方向要一致,如下图所示:
接下来需要给高速ADDA模块的模拟输入(AD_IN)提供模拟输入源,大家可以使用DDS信号发生器产生模拟输入源,也可以直接将模块的模拟输出(DA_OUT)通过杜邦线或者SMA转SMA线接到模拟输入(AD_IN),这里我们直接使用杜邦线进行连接,如下图所示:
连接电源线、下载器和PS UART串口线,网线一端连接电脑的网口,另一端连接开发板的PS网口(GE_PS),并打开电源开关。
下载本次示例程序,下载完成后,在下方的Vitis Serial Terminal中可以看到应用程序打印的信息,如下图所示:
串口终端首先会打印AXI DMA配置成功,然后打印开发板的IP地址为192.168.1.10和端口号为1234。接下来设置电脑的静态IP地址,这里设置为192.168.1.102,如下图所示:
打开ATK-XADC软件,打开后的软件界面如下图所示:
按照上图所示设置完成后,点击“开始传输”按钮,此时上位机可以显示出正弦波(DA芯片固定输出正弦波的波形),如下图所示:
领航者ZYNQ开发板购买链接:(复制链接至浏览器打开)
https://detail.tmall.com/item.htm?id=609032204975
领航者ZYNQ开发板资料链接:(复制链接至浏览器打开)
http://www.openedv.com/docs/boards/fpga/zdyz_linhanz(V2).html
本案例完整工程源码和上位机网盘链接:
https://pan.baidu.com/s/14egt0D2SfFBSx0YswwPacg?pwd=zdyz 提取码:zdyz
正点原子FPGA公众号:
|