以太网数据传输方案
Zynq AD7768 以太网数据传输升级方案
板卡: ACZ7015-CB (Zynq-7015)
目标: 将 AD7768 8通道 ADC 数据通过千兆以太网实时传输到 PC
PC端: Python 接收程序
当前状态: 师兄的项目使用 UART (printf) 与 PC 通信,AD7768 模块已完成 PL 端采集逻辑
一、现状分析
当前数据链路
AD7768 ──→ ad7768.v (PL) ──→ BRAM/AXI Reg ──→ ARM PS ──→ UART (printf) ──→ PC
115200 baud
≈ 11.5 KB/s
问题:
- UART 带宽 11.5 KB/s,AD7768 满速 8ch×32bit×256kSPS = 8 MB/s
- 当前只能在 ILA 中观察数据,无法实时流式传输
- printf 是阻塞调用,会严重影响实时性
升级后的目标链路
AD7768 ──→ ad7768.v (PL) ──→ BRAM ──→ AXI DMA ──→ DDR ──→ lwIP UDP ──→ GigE ──→ PC
~100 MB/s
二、总体方案设计
方案选择: 裸机 lwIP + UDP + Python 接收
这是 Zynq 数据采集系统中最常用、最成熟的方案:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| ✅ 裸机 + lwIP + UDP | 开发简单、延迟低、Xilinx官方支持 | 带宽上限~50MB/s | 中等速率采集 |
| 裸机 + lwIP + TCP | 可靠传输 | 延迟大、吞吐低 | 需要可靠性的场景 |
| Linux + socket | 功能丰富、易开发 | 启动慢、实时性差 | 非实时数据 |
| PL 直接发 UDP (纯硬件) | 极高带宽 | 开发难度大 | >100MB/s 场景 |
选择理由:
- ACZ7015 开发板支持裸机 lwIP,有官方 BSP
- UDP 吞吐量 (~50 MB/s) 远超需求 (8 MB/s)
- 开发周期短,2-3 周可完成
- Python UDP 接收代码简单
系统架构图
┌─────────────────────── ACZ7015-CB ──────────────────────────┐
│ │
│ ┌─── PL (FPGA 逻辑) ──────────────────────────────────┐ │
│ │ │ │
│ │ AD7768 ad7768.v BRAM AXI DMA │ │
│ │ 芯片 ──→ 接收模块 ──→ 缓冲 ──→ (自动搬运) │ │
│ │ (8ch) (已有代码) (双缓冲) ↓ │ │
│ └──────────────────────────────────┼───────────────────┘ │
│ │ AXI HP端口 │
│ ┌─── PS (ARM Cortex-A9) ──────────┼───────────────────┐ │
│ │ ▼ │ │
│ │ DDR3 发送缓冲区 ←── DMA 完成中断 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ lwIP UDP 协议栈 ──→ GEM (千兆以太网 MAC) │ │
│ │ │ │ │
│ └──────────────────────────┼────────────────────────────┘ │
│ │ RGMII 接口 │
└─────────────────────────────┼────────────────────────────────┘
│
PHY 芯片 (板载)
│
RJ45 网口
│
┌─────────▼─────────┐
│ PC (Python) │
│ UDP 接收程序 │
│ 数据处理/存储 │
└───────────────────┘
三、分阶段实施计划
阶段 0: 环境准备 (2天)
目标: 确认硬件可用,搭建开发环境
步骤:
- 确认网口硬件: 插网线看板上 LED 是否有 Link 灯亮
- 查开发板资料: 确认 PHY 芯片型号(可能是 Marvell 88E1518 或 RTL8211)
- 确认 Vivado 工程: 打开师兄的 AD7768 Vivado 工程,查看 Block Design
- 安装 Wireshark: PC 上安装,后续调试网口用
需要搞清楚的信息:
□ PHY 芯片型号和 MDIO 地址 (看板子原理图)
□ Zynq PS GEM (以太网) 连接到哪些 MIO 引脚
□ 当前 Block Design 中是否已使能 Ethernet
□ 板子的 MAC 地址 (通常丝印在板上或文档中)
阶段 1: 先让网口通 — Echo Server (1周)
目标: Zynq 能 ping 通 PC,验证网络链路正常
1.1 Vivado 端修改
在 Block Design 中使能以太网:
1. 双击 Zynq PS IP → MIO Configuration
2. I/O Peripherals → Ethernet 0 → 勾选 Enable
3. 确认 MIO 引脚分配 (参考 ACZ7015-CB 原理图)
通常: ENET0 = MIO[16:27] (RGMII) + MIO[52:53] (MDIO)
4. PS-PL Configuration → AXI HP0 → Enable (为后续DMA准备)
5. Generate Bitstream
6. Export Hardware (Include Bitstream)
1.2 SDK 端创建 lwIP 工程
1. File → New → Application Project
2. 选择 "lwIP Echo Server" 模板
3. BSP Settings:
→ lwip211 → 勾选
→ lwip211 → phy_link_speed → CONFIG_LINKSPEED_AUTODETECT
→ lwip211 → mem_size → 524288 (512KB, 增大内存池)
→ lwip211 → memp_n_pbuf → 1024
→ lwip211 → n_rx_descriptors → 64
4. 编译
1.3 网络配置
// main.c 中设置 IP (参考 echo server 模板)
// Zynq 端
IP4_ADDR(&ipaddr, 192, 168, 1, 10); // Zynq IP
IP4_ADDR(&netmask, 255, 255, 255, 0);
IP4_ADDR(&gw, 192, 168, 1, 1);
// PC 端设置 (Windows 网络适配器)
// IP: 192.168.1.100
// 子网掩码: 255.255.255.0
// 网关: 192.168.1.1
1.4 测试
PC 命令行: ping 192.168.1.10
期望结果: 收到回应,延迟 <1ms
如果 ping 不通排查:
1. 检查网线插好,Link灯亮
2. 检查 PHY 配置 (MDIO 地址)
3. 检查 MIO 引脚映射是否与板子原理图一致
4. 用 Wireshark 看 PC 有没有收到 ARP 响应
✅ 里程碑1: ping 通 = 网络链路 OK
阶段 2: 实现 UDP 数据发送 (1周)
目标: Zynq 能通过 UDP 主动发送测试数据到 PC
2.1 Zynq 端代码 (基于 lwIP Raw API)
// udp_send.c — Zynq ARM 端 UDP 发送模块
#include "lwip/udp.h"
#include "lwip/ip_addr.h"
#include "xil_printf.h"
// 配置参数
#define PC_IP_0 192
#define PC_IP_1 168
#define PC_IP_2 1
#define PC_IP_3 100
#define UDP_PORT 5000
// 数据包结构
#pragma pack(1)
typedef struct {
uint32_t frame_id; // 帧序号 (用于检测丢包)
uint32_t timestamp; // 时间戳 (us)
uint32_t adc_data[8]; // 8通道 ADC 数据
} adc_packet_t;
#pragma pack()
static struct udp_pcb *udp_pcb_send;
static ip_addr_t pc_addr;
static uint32_t frame_counter = 0;
// 初始化 UDP 发送
int udp_send_init(void) {
udp_pcb_send = udp_new();
if (!udp_pcb_send) {
xil_printf("ERROR: udp_new failed\r\n");
return -1;
}
IP4_ADDR(&pc_addr, PC_IP_0, PC_IP_1, PC_IP_2, PC_IP_3);
err_t err = udp_connect(udp_pcb_send, &pc_addr, UDP_PORT);
if (err != ERR_OK) {
xil_printf("ERROR: udp_connect failed\r\n");
return -1;
}
xil_printf("UDP sender initialized: target %d.%d.%d.%d:%d\r\n",
PC_IP_0, PC_IP_1, PC_IP_2, PC_IP_3, UDP_PORT);
return 0;
}
// 发送一帧 ADC 数据
int udp_send_adc_frame(uint32_t *adc_values) {
struct pbuf *p;
adc_packet_t packet;
// 填充数据包
packet.frame_id = frame_counter++;
packet.timestamp = get_timer_us(); // 需要实现微秒级时间戳
memcpy(packet.adc_data, adc_values, 8 * sizeof(uint32_t));
// 分配 pbuf
p = pbuf_alloc(PBUF_TRANSPORT, sizeof(adc_packet_t), PBUF_RAM);
if (!p) {
return -1; // 内存不足
}
// 拷贝数据到 pbuf
memcpy(p->payload, &packet, sizeof(adc_packet_t));
// 发送
err_t err = udp_send(udp_pcb_send, p);
pbuf_free(p);
return (err == ERR_OK) ? 0 : -1;
}
2.2 主循环集成 (读 ADC + 发 UDP)
// main.c 主循环
#include "xil_io.h"
#define AD7768_BASEADDR 0x43C00000 // AXI IP 基地址 (查 Address Editor)
int main(void) {
init_platform();
// 初始化 lwIP
lwip_init();
// 配置网络接口 (参考 echo server)
setup_network_interface();
// 初始化 UDP 发送
udp_send_init();
xil_printf("System ready. Streaming ADC data...\r\n");
uint32_t adc_values[8];
while (1) {
// 处理 lwIP 协议栈 (必须定期调用)
xemacif_input(netif);
// 读取 8 通道 ADC 数据
adc_values[0] = Xil_In32(AD7768_BASEADDR + 0x00);
adc_values[1] = Xil_In32(AD7768_BASEADDR + 0x04);
adc_values[2] = Xil_In32(AD7768_BASEADDR + 0x08);
adc_values[3] = Xil_In32(AD7768_BASEADDR + 0x0C);
adc_values[4] = Xil_In32(AD7768_BASEADDR + 0x10);
adc_values[5] = Xil_In32(AD7768_BASEADDR + 0x14);
adc_values[6] = Xil_In32(AD7768_BASEADDR + 0x18);
adc_values[7] = Xil_In32(AD7768_BASEADDR + 0x1C);
// 通过 UDP 发送到 PC
udp_send_adc_frame(adc_values);
}
return 0;
}
2.3 PC 端 Python 接收程序
#!/usr/bin/env python3
"""PC端 UDP 接收 AD7768 数据"""
import socket
import struct
import time
import numpy as np
from collections import deque
# 配置
LISTEN_IP = '0.0.0.0' # 监听所有网卡
LISTEN_PORT = 5000
BUFFER_SIZE = 65536
# 数据包格式 (与 Zynq 端 adc_packet_t 对应)
PACKET_FORMAT = '<II8I' # frame_id(u32) + timestamp(u32) + 8×adc(u32)
PACKET_SIZE = struct.calcsize(PACKET_FORMAT) # = 40 bytes
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((LISTEN_IP, LISTEN_PORT))
sock.settimeout(5.0)
# 增大接收缓冲区 (防丢包)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8 * 1024 * 1024)
print(f"监听 UDP 端口 {LISTEN_PORT}...")
print(f"包大小: {PACKET_SIZE} bytes")
frame_count = 0
last_frame_id = -1
lost_count = 0
start_time = time.time()
try:
while True:
try:
data, addr = sock.recvfrom(BUFFER_SIZE)
except socket.timeout:
print("等待数据中...")
continue
if len(data) < PACKET_SIZE:
continue
# 解析数据包
frame_id, timestamp, *adc_values = struct.unpack(PACKET_FORMAT, data[:PACKET_SIZE])
# 丢包检测
if last_frame_id >= 0:
expected = last_frame_id + 1
if frame_id != expected:
lost = frame_id - expected
lost_count += lost
last_frame_id = frame_id
frame_count += 1
# 每1000帧打印一次统计
if frame_count % 1000 == 0:
elapsed = time.time() - start_time
rate = frame_count / elapsed
bandwidth = rate * PACKET_SIZE / 1024 # KB/s
loss_rate = lost_count / (frame_count + lost_count) * 100
print(f"帧#{frame_id} | 速率:{rate:.0f} fps | "
f"带宽:{bandwidth:.1f} KB/s | "
f"丢包率:{loss_rate:.2f}% | "
f"CH0:{adc_values[0]} CH1:{adc_values[1]}")
except KeyboardInterrupt:
print(f"\n停止接收. 共 {frame_count} 帧, 丢包 {lost_count}")
finally:
sock.close()
if __name__ == '__main__':
main()
✅ 里程碑2: PC 收到 ADC 数据 + 丢包率 <1%
阶段 3: 提高吞吐量 — 批量打包 (1周)
目标: 从逐帧发送优化为批量打包,充分利用网络带宽
3.1 打包策略
优化前: 每个 ADC 采样点发一个 UDP 包 (40 字节/包,开销大)
优化后: 攒满 N 个采样点再发一个大包 (1400 字节/包)
每包容纳: (1400 - 8字节包头) / (8通道×4字节) = 43 个采样点
实际选择: 32 个采样点/包 → 有效载荷 = 32×32 + 8 = 1032 字节
3.2 优化后的包格式
#pragma pack(1)
typedef struct {
uint32_t frame_id; // 帧序号
uint16_t num_samples; // 本包采样点数
uint16_t reserved; // 对齐填充
uint32_t adc_data[8][32]; // 8通道 × 32个采样点
} adc_bulk_packet_t; // = 8 + 1024 = 1032 bytes
#pragma pack()
3.3 带宽计算
采样率 256 kSPS × 8 通道 × 4 字节 = 8.2 MB/s 原始数据
每包 1032 字节,含 32 个采样点
包发送率 = 256000 / 32 = 8000 包/秒
实际带宽 = 8000 × (1032 + 42 UDP/IP头) = 8.6 MB/s
千兆以太网有效吞吐 ~100 MB/s → 利用率仅 8.6% → 很轻松
✅ 里程碑3: 稳定 8 MB/s 传输,丢包率 <0.01%
阶段 4: DMA 加速 (可选,2周)
目标: 用 AXI DMA 自动搬运数据,释放 ARM CPU
如果阶段 2-3 中 ARM 轮询读 AXI 寄存器成为瓶颈(CPU 占用过高),则引入 DMA:
PL: AD7768 ──→ AXI-Stream FIFO ──→ AXI DMA ──→ DDR 缓冲区
PS: DMA中断 → 取走缓冲区数据 → UDP 发送
需要在 Block Design 中添加:
- AXI DMA IP
- AXI-Stream Data FIFO
- 修改 ad7768.v 增加 AXI-Stream 输出接口
这部分建议在前三个阶段稳定后再做。
四、学习路线图
第1周: 基础知识
| 天 | 学习内容 | 方式 |
|---|---|---|
| Day 1 | TCP/IP 协议栈基础 (重点 UDP) | 看视频/书 |
| Day 2 | lwIP 协议栈架构 (Raw API vs Socket API) | 读 Xilinx lwIP文档 |
| Day 3 | Zynq PS 以太网硬件 (GEM + PHY) | 看 ACZ7015 原理图 |
| Day 4 | Vivado Block Design 操作 (使能 GEM) | 动手改工程 |
| Day 5 | SDK lwIP Echo Server 例程跑通 | 动手实验 |
第2周: 实现基本传输
| 天 | 学习内容 | 方式 |
|---|---|---|
| Day 1 | lwIP UDP API 学习 (udp_new/send/recv) | 读源码+例程 |
| Day 2 | 编写 UDP 发送模块 | 写代码 |
| Day 3 | Python socket UDP 接收 | 写代码 |
| Day 4 | 联调: Zynq发送测试数据→PC接收 | 联调 |
| Day 5 | 集成 AD7768 数据: AXI 读取→UDP 发送 | 联调 |
第3周: 优化与稳定性
| 天 | 学习内容 | 方式 |
|---|---|---|
| Day 1 | 批量打包优化 | 改代码 |
| Day 2 | 丢包率测试与 PC 端缓冲区优化 | 测试 |
| Day 3 | Wireshark 抓包分析 | 调试 |
| Day 4 | 长时间稳定性测试 (跑 1 小时不丢包) | 测试 |
| Day 5 | 文档整理、代码注释 | 文档 |
五、推荐学习资源
必看 (按优先级)
- ACZ7015-CB 开发板用户手册 — 确认网口引脚、PHY 型号
- Xilinx XAPP1026 — "Embedded System Design on Zynq using lwIP" (官方以太网教程)
- 正点原子《领航者 Zynq 开发指南》第24章 — Zynq 以太网通信 (中文,讲得清楚)
- lwIP Wiki — https://lwip.fandom.com/wiki/LwIP_Wiki (API 参考)
视频
- B站 "FPGA 以太网数据传输" — 搜索关键词,有多个实战视频
- B站 "Zynq lwIP" — 看跑通 Echo Server 的过程
进阶
- Xilinx PG021 — AXI DMA 数据手册 (阶段4需要)
- UG585 — Zynq Technical Reference Manual → 第16章 GEM 控制器
- Python socket 编程教程 — Real Python 网站
六、常见坑与解决方案
| 坑 | 现象 | 解决 |
|---|---|---|
| PHY 不识别 | ping 不通,Link灯不亮 | 检查 MDIO 地址 (通常 0x0 或 0x7) |
| IP 冲突 | 偶尔通偶尔不通 | 确保同一子网无 IP 冲突 |
| ARP 超时 | 第一次 ping 丢包 | 正常现象,第一次需 ARP 解析 |
| lwIP 内存不足 | 发送失败,pbuf_alloc 返回 NULL | 增大 mem_size 和 memp_n_pbuf |
| PC 端丢包严重 | 丢包率 >5% | 增大 SO_RCVBUF,用多线程接收 |
| printf 拖慢速度 | 发送卡顿 | 去掉调试 printf,用 LED 指示状态 |
| 网线问题 | 100M 协商 | 确认用 Cat5e 以上网线,直连或交换机 |
| 跨网段 | ping 不通 | 确保 Zynq 和 PC 在同一子网 (192.168.1.x) |
七、面试加分表述
改进描述 (简历/面试):
针对原系统 UART 传输带宽不足的问题,设计并实现了基于 Zynq PS 端千兆以太网 (GEM) 的高速数据传输方案。使用 lwIP 协议栈 UDP 模式,采用批量打包策略(32 采样点/包),实现 8 通道 256kSPS ADC 数据的实时流式传输,有效带宽达 8.2 MB/s,相比原 UART 方案提升约 700 倍,丢包率控制在 0.01% 以下。PC 端使用 Python socket 接收并行实时显示/存储。
文档版本: v1.0
适用板卡: ACZ7015-CB (Zynq-7015, 也适用 7020)

浙公网安备 33010602011771号