ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ZYNQ数据采集传输:PL到TCP上行的AXI DMA与Linux Socket实战

ZYNQ数据采集传输:PL到TCP上行的AXI DMA与Linux Socket实战 简介面向ZYNQ嵌入式开发者和FPGA工程师这份资源围绕PL与PS端间的通信设计重点解决如何通过网口TCP/IP协议将数据稳定传输到上位机的问题。内容覆盖ZYNQ架构简介、AXI总线接口的适用场景与选择方法、LWIP轻量级协议栈的移植和配置、以太网MAC与DMA的数据交互流程以及TCP连接建立、收发与调试的完整思路适合用作项目参考或学习软硬件协同设计的入门材料。资源包约56.6MB文件构成暂未提供明细但从主题看应包含硬件工程、软件工程及说明文档等核心内容便于对照学习和二次开发。已有10721人学习下载是关注度较高的热门资源。深入阅读后可理清PL与PS通信的硬件链路掌握LWIP在ZYNQ上的实际应用并学会排查网口通信中的常见故障。 做ZYNQ开发的朋友基本都会走到这一步PL端采了一堆数据怎么送到上位机去显示、存储或者喂给算法处理。我见过不少项目在这个环节卡壳明明逻辑仿真全对板上JTAG抓波形也没问题就是和PC那边联不上或者通了但速度上不去。这篇文章直接把我自己的实现方案和调试心得放出来讲清楚从PL采集到TCP上行的完整链路涉及AXI总线、DMA搬运、Linux下的socket编程适合正在做ZYNQ数据采集传输、或者想在PS上跑网络协议栈的工程师参考。先明确一下思路ZYNQ最大的优势就是异构PS端挂着双核ARM Cortex-A9能跑Linux网络协议栈这东西用CPU来做远比在PL里用逻辑搭要省事。PL端负责高速数据采集和预处理PS端负责跑TCP/IP协议栈这套分工符合两者的硬件特性也是社区里绝大多数成熟方案的选型逻辑。1. 整体设计思路与架构拆解1.1 为什么选ZYNQ而不是纯FPGA方案拿纯FPGA做TCP协议栈不是不行比如用Verilog直接实现mac层再拼一个TCP/IP核但那是条极其痛苦的路。TCP是面向连接的可靠传输有状态机、超时重传、拥塞控制、滑动窗口这些机制用逻辑去描述这些状态机要写大量代码占用的LUT资源非常可观而且调一个bug可能要花一整个调试周期。即便是用开源的FPGA TCP IP核很多也只支持一个连接、吞吐量有限灵活性很差。ZYNQ的方案就不一样了。PS端的ARM核跑Linux或者裸机加lwIP协议栈TCP/IP栈是现成的链路层PHY芯片驱动也是现成的你只需要把数据从PL送进PS的DDR然后调到socket接口发出去就行。从项目周期上看这个思路大概一两周能把链路打通而纯FPGA方案光协议栈验证就得数月。还有一个隐性问题纯FPGA方案上位机通常要定制对应私有协议的解析软件而ZYNQ方案用的是标准socket接口上位机不管是C#、Python、LabVIEW还是Qt随便写几十行代码就能对接TCP流开发和调试成本无限趋近于普通网络编程。1.2 数据流路径规划PL采集、PS传输的分层设计这套系统最基本的数据流是这样走的PL端的数据源头ADC采样、图像传感器、自定义逻辑产生的数据被送进我们自己实现或者调用的AXI接口模块然后通过AXI总线写到PS端的DDR内存PS端在用户态或内核态的程序从DDR这些缓冲区里把封装好的数据取出来交给socket发送缓冲TCP/IP协议栈添加TCP头、IP头、MAC头交给MAC控制器和PHY芯片发出网线。这里有一个关键分层思路PL只负责把数据放到DDRPS只负责把DDR的数据上网两者通过内存地址来交接相当于交接货物双方都跑去同一个仓库一个往货架上摆一个从货架上搬。从硬件拓扑上PL访问DDR走的是ZYNQ内部的高性能AXI接口我一般用AXI_HP端口它是64位数据位宽、支持AXI4协议吞吐量理论上能到几千Mbps完全不是瓶颈瓶颈在网口的百兆或千兆PHY这一侧。简单示意一下PL数据源 - AXI4-Stream - AXIDMA IP - AXI4HP口 - DDR缓冲 PS Linux App - 读DDR缓冲 - socket send buffer - TCP/IP协议栈 - GEM网口 - PHY芯片 - PC上位机这张图明确之后整个项目就被切割成了几个独立的小模块PL端做DMAPS端做驱动和TCP服务两端可以并行调试不用互相卡住。这也是我当时编排进度的核心思路。2. 核心通信机制解析与关键开发要点2.1 AXI总线的本质理解PL与PS之间的“高速公路”AXI是ARM AMBA总线家族的一员ZYNQ内部所有高性能交互几乎都走AXI。把它想象成一条双向多车道高速路AXI的协议里已经有独立地址/数据相位、突发传输、乱序完成、多主多从仲裁这些机制所以你在设计时对吞吐量不用太焦虑该担心的是怎么用好它。ZYNQ的PS内建了几个AXI接口日常用最多的两类AXI_GP口32位数据、不带缓冲适合小批量寄存器读写也就是配配置寄存器AXI_HP口64位数据带FIFO缓冲直接通向DDR控制器是大批量数据搬运的主干道。做数据传输时优先走HP口别用GP口去搬大数据。AXI协议里的信号名看着多那就记住了它本质是在做读地址通道、写地址通道、写数据通道、写响应通道、读数据通道这几个独立通道的组合握手。各通道之间不强制绑定顺序这在硬件设计上是功能特性在调试时也意味着你抓波形可能要同时观察多个通道的握手信号不要只看单个信号。2.2 TCP协议栈选型Linux内核协议栈与lwIP对比TCP协议栈跑在哪里直接影响开发方式和稳定性。如果在PS上跑完整的Linux发行版直接使用内核自带的网络协议栈那个经过亿级设备验证稳定性和性能都有保障。你在用户态写socket程序sfd socket(AF_INET, SOCK_STREAM, 0)然后bind、listen、accept、recv/send跟普通PC上写服务端没有任何区别。这种方式适合追求开发效率、项目里还有文件系统、多线程等需求的场景。PL端的数据可以用UIO、内核驱动或者/dev/mem映射等方式暴露给用户态程序。如果不想引入LinuxPS跑裸机或者FreeRTOS那就要用lwIPlightweight IP。lwIP资源占用低设计目标是嵌入式环境裸机下也能跑。它提供一套类socket的API比如netconn_new/netconn_bind/netconn_listen/netconn_accept相比POSIX socket有一定的学习成本但也有成熟案例可以参考。我个人的建议是只要你的PS端DDR有64MB以上空闲空间直接上Linux。无他问题排查简单太多了。你可以在板子上用ifconfig查网络状态用ping通不通来快速定位问题是PHY还是驱动用tcpdump抓包看数据到哪个层面。开发速度的重要性远大于那一点DDR的开销。当然如果您做的是低延迟极端裸机环境lwIP那条线也能通只是要预留更多调试时间。3. 实操过程从PL到上位机的完整链路实现3.1 PL端数据采集与DMA通道设计PL端的第一步是产生或接收数据。我在这套方案里用一个AD7626评估板作为数据源因为它输出LVDS差分信号正好对应PL里的IBUFDS原语转单端。ADC的输出数据接入我们自己写的采集逻辑这个逻辑把串行数据按采样点对齐成16bit再拼成64bit打拍最后以AXI4-Stream的主机行为喂给Xilinx官方AXIDMA IP。AXIDMA IP是这套传输方案中最核心的官方IP它提供三种模式的传输Direct Register模式、Scatter Gather模式和简单DMA模式。高吞吐场景我推荐用Scatter Gather它能自动维护多段缓冲区表传输连续大块数据时不丢描述符。不过SG的模式也意味着需要更仔细去初始化描述符链代码量更大新手可以先从Simple模式打通数据量小没问题后面再切SG。在PL里例化AXIDMA IP时要注意参数配置几个关键点地址宽度设为32匹配ZYNQ的寻址空间Stream数据宽度设为64bit和HP口的位宽对齐使能SG和Micro DMA选项可选我先不开Micro DMA减少复杂度。IP的S2MM通道是从PL进内存的方向本方案数据只走S2MMM2MM方向内存到PL可暂时不实现减少逻辑。PL里DMA的引脚连接大致是这样axi_dma_0 your_dma_instance ( .axi_resetn(...), // 来自PS复位 .axi_clock(axi_clk), // PL时钟和HP口时钟同源 .s_axi_lite_aclk(...), // 寄存器配置时钟 .s_axi_lite_awaddr(...), // PS配置寄存器地址 // 四条AXI-Lite从机读写通道接PS的GP口 .s_axis_s2mm_tdata(i_stream_tdata), .s_axis_s2mm_tkeep(i_stream_tkeep), .s_axis_s2mm_tvalid(i_stream_tvalid), .s_axis_s2mm_tlast(i_stream_tlast), .s_axis_s2mm_tready(o_stream_tready), // 状态中断信号 .s2mm_introut(dma_s2mm_intr) // 接到PS的中断控制器 );代码里要特别注意tlast信号它表示一帧数据的结束。ADC连续采集可能没有天然的帧概念我是用定时器计数产生帧间隔的到达设定的采样点数后拉高tlast一拍这样PS端拿到DMA中断后就知道一个完整的buffer周期结束了。PL侧调试的时候我会先用Vivado的ILA抓S2MM的AXI4-Stream接口的tvalid/tready/tdata确认数据真的是在打拍而不是因为背压一直在等待。如果发现tvalid拉不起来大概率是上游逻辑没有处于就绪状态如FIFO空满了。排查完了PL这半边再去看PS侧。3.2 PS端的DDR缓冲管理与DMA控制DMA要把数据写到DDR那就必须给DMA分配一段物理内存。在Linux里普通malloc分配的是虚拟内存页面可能是不连续的DMA必须用物理连续的内存。我在驱动里采用dma_alloc_coherent分配或者用内核的CMA区域分配。先解释dma_alloc_coherent它同时给你返回CPU能访问的虚拟地址和物理地址且保证访问一致性cache一致性。它的用法大致是struct dma_data *priv; priv-virt_addr dma_alloc_coherent(dev, BUF_SIZE, priv-phys_addr, GFP_KERNEL);这里返回的phys_addr就是你要写进DMA寄存器AXIDMA S2MM目的地址寄存器的值DMA完成后会循着这个物理地址写数据而CPU侧用virt_addr读数据时不会因为cache没刷写而读完找不到新数据。这是很多新手最容易踩的坑——他们用kmalloc分配内存给DMA用结果CPU读到的不是DMA写入的数据那是因为cache一致性没处理好。DMA的启动流程我写成一段极简伪代码但它是PowerPC裸机时代ZYNQ上验证过无数次的逻辑void dma_s2mm_start(unsigned long buf_phys, size_t len) { // 1. 复位DMA writel(0x04, dma_base 0x0000); // DMACR.RS置1则启动先写0x2复位脉冲 udelay(2); writel(0x00, dma_base 0x0000); // 2. 清中断状态 writel(0xFFFFFFFF, dma_base 0x0034); // S2MM_IRQ_STS // 3. 设置目的地址 writel((u32)buf_phys, dma_base 0x0030); // S2MM_DEST_ADDRESS // 4. 设置传输字节数 writel(len, dma_base 0x0028); // S2MM_LENGTH // 5. 启动DMA writel(0x03, dma_base 0x0000); // DMACR.RS1, IOC_IrqEn1 }寄存器偏移号来自Xilinx官方AXIDMA的Memory Map文档你拿到参考用例时照着这个顺序初始化不会错。注意传输长度必须和PL端每帧的字节数一致否则会出现在传输途中DMA提前结束或者一直等待帧尾的情况。PS端收到DMA中断后在中断服务函数里先读S2MM_STS_REG清中断再记录本次DMA实际传输的字节数(Bytes_Transfered)。这个值有时会被SG模式搬一些状态字段进来建议以它为准填写后续socket的发送长度而不是自己臆测长度。3.3 网口TCP服务与上位机对接TCP的连接模型是C/S模式我在这里的设计是ZYNQ作为服务端上位机作为客户端主动来连。为什么要这样因为开发板IP地址往往是动态分配的或是固定但可能出现使用冲突。上位机是控制中心让它主动去连接目标板逻辑更顺也更贴合产品现场“多个上位机同时连一个采集盒子”的需求。PS端服务器用Linux socket实现就很简单核心部分int sfd socket(AF_INET, SOCK_STREAM, 0); int opt 1; setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); // 关键 struct sockaddr_in addr { .sin_family AF_INET, .sin_port htons(5000), // 自定义端口 .sin_addr.s_addr htonl(INADDR_ANY), }; bind(sfd, (struct sockaddr*)addr, sizeof(addr)); listen(sfd, 1); // 只接受一个上位机够用 int cfd accept(sfd, NULL, NULL); // 阻塞等客户端接入 while (1) { // 等待DMA完成阻塞或信号量等待 wait_for_dma_irq(); // 封装成固定帧格式或者直接发原始采集数据 ssize_t n send(cfd, dma_buffer, dma_rx_bytes, 0); if (n 0) { // 客户端断开重新accept perror(send); close(cfd); cfd accept(sfd, NULL, NULL); } }这里面最容易被坑的是setsockopt设置SO_REUSEADDR否则在调试期服务器退出后TCP会进入TIME_WAIT状态重新bind端口会报Address already in use。实测中这个选项几乎是必设的。上位机端我用的是C#的TcpClient显示采集波形时边收边画代码结构很简单var client new TcpClient(); client.Connect(192.168.1.30, 5000); var stream client.GetStream(); byte[] buffer new byte[4096]; while (true) { int len stream.Read(buffer, 0, buffer.Length); if (len 0) { // 解析帧假设前4字节是长度后面是16bit采样点的字节序 // 我实际按“4字节数据头 N个int16采样点”的协议来拆包 } }上位机的粘包问题其实不用太怕TCP是字节流你要自己解析帧边界。我固定用“帧头(0xAA55) 采样点数(uint32) 数据体(N * int16)”的协议每次解析先从缓冲里扫帧头再根据长度字段截取完一帧剩下的继续下一次拼接。这种方法在中断频繁、数据量大时也能保持稳定。如果要用千兆网实现更高吞吐PS端还需要注意CPU、DDR带宽、中断的协调尤其是中断频率太高会拉满CPU占用。此时可以考虑用NAPI或中断合并机制也可以把数据在DMA驱动里直接进skb但那属于进阶优化了在项目第一个跑通版本可以先不管。4. 常见问题与排查技巧实录4.1 网络连不上、数据发不出的排查思路做网络传输第一件必做的检查是物理层。ZYNQ官方或者开发板厂商的Vivado工程里一般会例化GEMGigabit Ethernet MAC到RGMII接口的约束但是PHY芯片型号千差万别复位时长要求、MDIO地址、时钟源延迟等都可能不一样。我遇到过一块板子换了个PHY型号网口完全ping不通最后发现是PHY复位信号没满足最小240毫秒的低电平时长要求上电后立即被拉高了。建议先在Linux里用mdio工具读一读PHY的寄存器看有没有正确识别到PHY ID再往下查。网络配置层面ZYNQ跑Linux后先看ifconfig确认eth0存在然后配静态IPifconfig eth0 192.168.1.30 netmask 255.255.255.0 up。上位机用同一网段交叉网线还是不交叉这年头自适应网卡都无所谓了。ping不通就依次排查先ping自己再ping网关再target ping host能定位到哪一段链路断了。数据发不出去还有一种常见情况和DMA中断有关socket发送函数只是把数据拷贝进内核TCP发送缓冲实际真正的发送是由TCP/IP栈异步完成的。如果你发现上位机一直收不到先别怀疑send()失败要用tcpdump抓包确认是不是真的发到网线上了。在ZYNQ板卡上执行tcpdump -i eth0 host PC_IP and port 5000 -nn -XX如果tcpdump抓到包但上位机收不到大概率是上位机防火墙拦截了把Windows防火墙对TCP 5000端口的入站规则加一条放行或者临时关防火墙测试这种“开发板正常、PC端偷偷拦截”的情形我至少见人踩过五次。4.2 DMA中断、缓存一致性等经典卡点记录进程里最常见的卡点是DMA中断一直不触发或者传输长度不对。先从最基础的确定DMA寄存器配置正确。用devmem工具在板子上查看DMA状态寄存器实时值# 假设AXI DMA基地址是0x40400000S2MM状态寄存器在偏移0x0034 devmem 0x40400034在DMA传输过程中去读如果看到HaltBit没拉起来、又没有IOC_Irq那就是DMA没有收到有效的tlast。这时候回到PL侧把tvalid和tlength逻辑的对齐时序好好检查。有个隐藏陷阱AXI4-Stream的tvalid和tready是握手关系如果DMA没就绪tvalid不能一直拉高否则数据会丢。你必须在PL逻辑里实现“tvalid只要DMA没准备好就不要有效”的状态机而不是粗暴按帧计数脉冲。我在裸机环境下遇到过一次严重问题DMA回报的字节数比实际PDMA描述符总长度小几十个字节。查了很久最后发现是DMA描述符缓存一致性出问题。CPU更新完描述符后必须调用cache flush否则DMA读到的可能是旧值。如果你用的是Linux dma_alloc_coherent根本不涉及这个但如果是自己写驱动操作寄存器映射或者用PL端的描述符那这个cache flush必须到位。另外如果数据是图像这类高带宽密集流建议PL端用2个buffer交替DMA这样在DMA在写一个buffer时CPU可以并行读取另一个buffer相当于把内存搬运和网络发送两个动作重叠起来吞吐能翻近一倍。5. 调试心得与几个优化方向链路跑通整个系统现场能出数了之后的下一步一般是吞吐量优化。这里提供几个我实际验证过的方向TCP发送缓冲区不要设太小。Linux的socket默认发送缓冲是几十KB当数据流大于缓冲而应用还在持续write/send时send函数会阻塞或返回部分长度你需要循环发送直到数据全发完尤其是大数据帧场景。改用SO_SNDBUF调大缓冲对吞吐有直观改善。性能侧还有一个明显的热区是数据拷贝。DMA把数据写进内核缓冲区用户态程序再用send去发会做一次内核到用户、用户到内核的拷贝。你如果追求高吞吐可以把DMA缓冲区直接映射到用户态空间这样PL数据直接进了用户态可见的内存再从应用层socket发送时就只多一次拷贝更极端的DMA呢就能配合内核tun/tap或者AF_PACKET直接放包进协议栈适合千兆高速传输代价是驱动复杂度和调试难度显著上升。板子上面跑Linux还有一个特别值得留意的点Linux的平台时钟和PL的逻辑时钟可能不同频。如果PL端采样时钟和DMA的s_axi_aclk在异步域传输过程中偶尔会出现数据错位或者偶发丢帧。我用过异步FIFO隔离在PL侧把数据从高速采样时钟域跨到AXI时钟域后再进DMA基本能杜绝这类问题。最后还想提一小点上位机显示的波形数据往往要做标定或者换算比如ADC量程是-10V~10V对应0x8000~0x7FFF这种换算尽量放在上位机做别放在PS端做否则高采样率下ARM核会被浮点运算拖慢实时性和吞吐量两头丢。实际项目里我跑到这里板卡的ARM占用率基本在20%以下每1毫秒一帧4096个采样点上位机画图跟手得很这套“PL采集-DDR中转-TCP上行”的架构稳得很。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进