第五十四章:基于双路AD的以太网波形传输实验(初学者详解)

本章在讲什么?(先看这个)

这是实战篇的收官实验,也是一次"大型组装":把前面学过的两大块技能拼在一起——

  • 双路 AD 模块(双路 AD 实验学过):把信号发生器输出的模拟波形转成数字信号;
  • 以太网 UDP 传输(第 46~49 章学过):把数字波形实时发到 PC 上位机显示。

实验任务:双路 AD 模块采集信号发生器的波形 → FPGA 采集数据 → 通过以太网 UDP 发给上位机 → 上位机实时绘制波形。整个数据链路是:

信号发生器 → 双路AD模块 → FPGA(位宽转换+封装+异步FIFO) → 千兆以太网 → 上位机显示波形

开始前先做一个重要的带宽预算,这也是本实验架构设计的依据:

  • 双路 AD 模块采样时钟最大 50MHz
  • 千兆以太网理论速率 1000Mbit/s,但扣除帧头、CRC 校验、帧间隙(IFG)等开销后,实际可用速率低于 1000Mbit/s。

结论:尽管实际速率打折,实时传输 50MHz 采样率的 AD 波形依然没有压力,所以不需要外接存储器缓存波形——数据先进异步 FIFO 缓冲,攒够一包就发。这种"先算带宽再定架构"的思路值得记住。

核心概念拆解

1. 系统架构:五个模块各司其职

系统架构图

模块 职责
clk_wiz_0(时钟模块) 输出三路时钟:50MHz(AD 驱动)、50MHz 偏移 250°(AD 采样)、200MHz(IDELAYCTRL 参考)
start_transfer_ctrl(开始传输) 解析上位机 UDP 命令:1=传通道一、2=传通道二、0=停止
ad_10bit_to_16bit(位宽转换) 10 位 AD 数据高位补零扩成 16 位,按 ctrl 选通道
img_data_pkt(数据封装) 异步 FIFO(写 16 位/读 8 位)+ 攒够 1200 字节触发一包 UDP 发送
eth_top(以太网顶层) ARP/ICMP/UDP 收发,复用 UDP 测试实验

总体模块框图:

总体模块框图

关于那个 250° 相位偏移的采样时钟要特别说一下:AD 驱动时钟和采样时钟频率相同但相位相反(180°)是常规做法——在数据稳定的中点采样。但如果排除硬件原因后波形仍有毛刺,可以通过微调采样时钟相位来消除,本实验实测调到 250° 毛刺消失。这是很实用的调试经验:采样相位是可调的调试自由度,不是固定值。

2. 开始传输控制模块:命令从"1/0"升级为"1/2/0"

与 OV5640/OV7725 视频传输实验里的同名模块相比,本实验多了双通道选择。命令集:

收到的字节 含义 transfer_flag ctrl
8'd1 开始传通道一 1 2'b01
8'd2 开始传通道二 1 2'b10
8'd0 停止传输 0 2'b00
其他 无效,停止 0 2'b00
parameter  START_1 = 8'd1;  //通道一开始命令
parameter  STOP    = 8'd0;  //停止命令
parameter  START_2 = 8'd2;  //通道二开始命令

always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        transfer_flag <= 1'b0;
        ctrl          <= 2'b0;
    end
    //只有"单包接收完成 且 字节数为1"时才解析命令
    else if(udp_rec_pkt_done && udp_rec_byte_num == 1'b1) begin
        if(udp_rec_data == START_1) begin
            transfer_flag <= 1'b1;
            ctrl          <= 2'b01;
        end
        else if(udp_rec_data == START_2) begin
            transfer_flag <= 1'b1;
            ctrl          <= 2'b10;
        end
        else begin  //0或其它值:一律停止
            transfer_flag <= 1'b0;
            ctrl          <= 2'b00;
        end
    end
end

注意触发条件是 udp_rec_pkt_done && udp_rec_byte_num == 1必须等一包数据接收完成、且包长恰好 1 字节才解析,防止把误包、长包的中间字节当成命令。

3. 数据位宽转换模块:10bit → 16bit,为什么补高位

双路 AD 输出 10 位数据,以太网按字节(8 位)传输。为了方便打包,先把 10 位扩成 16 位:

always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        ad_out <= 16'd0;
    else if( sel == 2'b01)
        ad_out <= {6'd0, ad_in1};   //高位补0,为了适应上位机
    else if( sel == 2'b10)
        ad_out <= {6'd0, ad_in2};
    else
        ad_out <= 16'd0;
end

两个细节:

  • 高位补零而非低位补零:代码注释写明"这样补 0 为了适应上位机"——上位机按 16 位无符号数解析波形幅值,AD 是无符号输出,高位补零数值不变;如果格式不匹配,上位机画出来的波形会错乱;
  • 通道选择在这一层完成sel 就是开始传输模块输出的 ctrl,选中的通道数据才输出,未选中时输出 0——不传的数据根本不会进 FIFO,这是对带宽的第二重保护。

4. 数据封装模块:位宽转换 FIFO + 深度触发的发包

图像数据封装模块框图

这是本章的核心模块,干三件事:跨时钟域、位宽转换、控制发包节奏

(1)一包发多少字节:1200

always @(posedge eth_tx_clk or negedge rst_n) begin
    if(!rst_n)
        udp_tx_byte_num <= 16'd0;
    else
        udp_tx_byte_num <= 16'd1200;   //单包1200字节
end

为什么是 1200?UDP 数据段最大 1472 字节(1500 MTU − 20 IP 头 − 8 UDP 头),1200 留了余量且是偶数——16 位数据转 8 位后,一个采样点恰好占 2 字节,1200 字节 = 600 个采样点,数据边界不会错位。

(2)攒够就发:tx_busy_flag 防止重复触发

always @(posedge eth_tx_clk or negedge rst_n) begin
    if(!rst_n) begin
        udp_tx_start_en <= 1'b0;
        tx_busy_flag    <= 1'b0;
    end
    //上位机未发送"开始"命令时,不发送数据
    else if(transfer_flag == 1'b0) begin
        udp_tx_start_en <= 1'b0;
        tx_busy_flag    <= 1'b0;
    end
    else begin
        udp_tx_start_en <= 1'b0;
        //空闲 且 FIFO攒够1200字节 → 触发一包发送
        if(tx_busy_flag == 1'b0 && fifo_rdusedw >= 1200) begin
            udp_tx_start_en <= 1'b1;
            tx_busy_flag    <= 1'b1;
        end
        else if(udp_tx_done)      //一包发完,解除忙标志
            tx_busy_flag <= 1'b0;
    end
end

节奏控制闭环:FIFO 深度 ≥ 1200 → 拉高 udp_tx_start_en(一拍)→ 以太网模块发 udp_tx_req 请求数据 → FIFO 读出 → 发送完成 udp_tx_donetx_busy_flag 清零 → 等待下一次攒够。tx_busy_flag 保证上一包没发完时不会重复触发

(3)异步 FIFO:写 16 位、读 8 位,复位接 transfer_flag 取反

async_fifo_2048x8b u_async_fifo_2048x8b (
  .rst          (~transfer_flag  ),   //停止传输时立刻清空FIFO
  .wr_clk       (clk             ),   //写侧:50MHz_deg(AD采样时钟域)
  .rd_clk       (eth_tx_clk      ),   //读侧:以太网发送时钟域
  .din          (img_data        ),   //16位写入
  .wr_en        (~full           ),
  .rd_en        (udp_tx_req && ~empty ),
  .dout         (udp_tx_data     ),   //8位读出
  .full         (full            ),
  .empty        (empty           ),
  .rd_data_count(fifo_rdusedw    )
);

三个精妙之处:

  • 位宽不对称 FIFO:写 16 位读 8 位,FIFO 控制器自动完成"一个 16 位采样点拆成两个 8 位字节",位宽转换和跨时钟域一步到位,不需要额外逻辑;
  • rst~transfer_flag:上位机在传输中途发"停止"命令时,FIFO 被立即复位清空——不会残留半包数据导致下次传输的波形错位。这是用传输使能反向控制缓存复位的常见手法;
  • 读使能 = udp_tx_req && ~empty:以太网请求数据且 FIFO 非空才读,双保险防止读空。

5. 顶层模块:三路时钟 + 环环相扣的握手

顶层 ad_udp_pc 要点:

(1)时钟分配:

assign ad_clk_1 = clk_50m;         //两路AD共用50MHz驱动时钟
assign ad_clk_2 = clk_50m;
assign ad_oe_1  = 1'b0;            //输出使能固定有效
assign ad_oe_2  = 1'b0;
//clk_wiz_0 输出:clk_50m / clk_50m_deg250(相位偏移250°) / clk_200m

位宽转换模块和封装模块的写侧都用 clk_50m_deg250——整个 AD 数据通路从采样到写 FIFO 都在采样时钟域,出 FIFO 才进以太网时钟域,时钟域边界只有 FIFO 一处,非常干净。

(2)MAC/IP 参数集中定义(与前面以太网章节一致):

parameter BOARD_MAC       = 48'h00_11_22_33_44_55;
parameter BOARD_IP        = {8'd192,8'd168,8'd1,8'd10};
parameter DES_MAC_DEFAULT = 48'hff_ff_ff_ff_ff_ff;
parameter DES_IP_DEFAULT  = {8'd192,8'd168,8'd1,8'd102};

(3)跨模块握手信号全链路:上位机命令(UDP 接收域)→ start_transfer_ctrl 产出 transfer_flag/ctrl → ctrl 回到 AD 采样域选通道 → transfer_flag 控制封装模块发包 → eth_top 的 udp_tx_req/udp_tx_done 回调封装模块。一个跨三个时钟域的控制信号(transfer_flag),靠各模块在本地时钟域打拍同步,这是多时钟域系统里控制信号传播的典型方式。

6. 下载验证(简述)

硬件连接:

  • 网线连接开发板 GE_PL 网口与电脑网口;
  • 双路 AD 模块插开发板 J3 扩展口
  • SMA 转 BNC 线把信号发生器接到 AD 模块模拟输入,设置信号发生器输出指定频率/幅值的正弦波。

上位机用 ATK-XADC(A 盘 6_常用软件/ATK-XADC 文件夹):打开软件 → 选择采样通道、采样方式、采样数量 → 点"开始传输"(即向上位机约定发送 ASCII "1"/"2")→ 上位机界面实时显示对应通道的波形。若要切通道先发停止("0")再发新通道命令。

验证要点:波形形状与信号发生器设置一致、无毛刺(有毛刺可微调采样时钟相位)、切换通道后显示内容随之切换。

初学者容易踩的坑

  1. 带宽没算就开干:如果换成更高速率的 AD(比如 125MHz 采样),千兆网实际有效带宽可能不够,就必须降采样或先缓存再传。本实验 50MHz 采样 + 16bit/点 = 800Mbps 原始数据率,实测能跑通但余量不大——发包字节数和 FIFO 深度都是围绕带宽预算定的,改动前先重新算账。

  2. FIFO 位宽选错:封装模块的 FIFO 必须配成"写 16 位、读 8 位"。如果配成对称位宽再手动拆字节,拆出来的高低字节顺序很容易弄反,上位机波形会出现"锯齿状"的规律错乱。

  3. 忘了 rst~transfer_flag:中途停止传输后 FIFO 里残留的半包数据不清空,下次开始传输时上位机会先收到一段"旧波形",画面跳变。

  4. 发包触发条件写错:触发必须是 tx_busy_flag == 0 && fifo_rdusedw >= 1200。漏掉 busy 标志会在同一包还没发完时反复触发 udp_tx_start_en,导致包序错乱;阈值小于 1200 则会发出短包,字节计数错位。

  5. 命令解析不做包长过滤:只判断 udp_rec_data 不判断 udp_rec_byte_num == 1,可能把上位机其他长包数据的中间字节误认成命令,导致传输状态莫名切换。

  6. 采样时钟相位不调:照抄"180° 反相采样"的结论,遇到毛刺就怀疑硬件或 AD 芯片。记住采样时钟相位是可以微调的调试手段,本实验就是从 180° 调到 250° 才消除毛刺。

  7. 两个 AD 通道数据混淆:10bit 转 16bit 模块里 sel 信号必须与上位机选择的通道严格对应(1=通道一 2'b01,2=通道二 2'b10)。写反了,上位机选通道一时显示的其实是通道二的波形。

小结 & 下一步

至此,本教程的实战篇全部收官。本章把"模拟信号采集"与"以太网传输"两大技能合体:带宽预算决定架构(50MHz 采样 + 千兆网 → FIFO 缓冲直传,无需片外存储)、三步数据整形(10bit→16bit 高位补零 → 16/8 位宽转换 FIFO → 1200 字节攒包触发)、命令协议升级(1/2/0 三命令 + 包长过滤 + 通道选择)。再加上两个可迁移的调试经验——采样时钟相位微调消毛刺、用传输使能反向复位 FIFO。

回看这 9 章的脉络:从 MDIO 管理接口、ARP/ICMP/UDP 三层协议,到 OV5640/OV7725 视频传输,再到 FFT 频谱分析、FIR 滤波,最后到双路 AD 波形回传——以太网是贯穿始终的主线,而跨时钟域 FIFO、AXI4-Stream 握手、命令解析、攒包发送这些手法反复出现。掌握了一个,就掌握了一类。建议把拓展训练(40MHz AD 时钟 + 单包 800 字节)亲手做一遍,检验一下带宽预算的账你会不会算。