以太网数据传输方案

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 场景

选择理由

  1. ACZ7015 开发板支持裸机 lwIP,有官方 BSP
  2. UDP 吞吐量 (~50 MB/s) 远超需求 (8 MB/s)
  3. 开发周期短,2-3 周可完成
  4. 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天)

目标: 确认硬件可用,搭建开发环境

步骤:

  1. 确认网口硬件: 插网线看板上 LED 是否有 Link 灯亮
  2. 查开发板资料: 确认 PHY 芯片型号(可能是 Marvell 88E1518 或 RTL8211)
  3. 确认 Vivado 工程: 打开师兄的 AD7768 Vivado 工程,查看 Block Design
  4. 安装 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 文档整理、代码注释 文档

五、推荐学习资源

必看 (按优先级)

  1. ACZ7015-CB 开发板用户手册 — 确认网口引脚、PHY 型号
  2. Xilinx XAPP1026 — "Embedded System Design on Zynq using lwIP" (官方以太网教程)
  3. 正点原子《领航者 Zynq 开发指南》第24章 — Zynq 以太网通信 (中文,讲得清楚)
  4. lwIP Wikihttps://lwip.fandom.com/wiki/LwIP_Wiki (API 参考)

视频

  1. B站 "FPGA 以太网数据传输" — 搜索关键词,有多个实战视频
  2. B站 "Zynq lwIP" — 看跑通 Echo Server 的过程

进阶

  1. Xilinx PG021 — AXI DMA 数据手册 (阶段4需要)
  2. UG585 — Zynq Technical Reference Manual → 第16章 GEM 控制器
  3. 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)

posted @ 2026-07-17 08:11  mo686  阅读(18)  评论(0)    收藏  举报