第49章:以太网UDP测试实验

ARP 让开发板"被找到",ICMP 让开发板"被 ping 通",这一章的 UDP 则让开发板真正"说人话"——收发用户数据。本章也是以太网三部曲的收官之战:ARP + ICMP + UDP 三协议共存

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

实验任务:电脑用网口调试助手发数据 → FPGA 收到后原样发回(UDP 环回)。

本章的核心增量只有一个——UDP 协议,但工程架构升级到了三协议并存:

  1. UDP 协议:传输层的"快递小哥",只管送不管签收;
  2. 三协议切换:eth_ctrl 模块从二选一升级为三选一;
  3. 异步 FIFO:为将来收发不同时钟域做好准备。

核心概念拆解

49.1 UDP 是什么,为什么不用 TCP

UDP(User Datagram Protocol,用户数据报协议)是面向无连接的传输层协议:发送端不确认对方是否存在就直接发,接收端也不回执。听起来很"不靠谱",但它的两个优点让它在硬件世界里几乎是唯一选择:

  • 开销小、速度快:没有连接建立、确认重传这些负担;
  • 丢一两个包无伤大雅:视频直播、音频传输等实时场景完全可以容忍。

那为什么不用更可靠的 TCP?原文的理由非常实在:TCP 是为软件灵活性设计的,用硬件逻辑实现工程量巨大,且功能和性能难以保证。所以 FPGA 以太网传输绝大多数基于 UDP。

还有个容易被问到的问题:数据直接封装在 IP 里不行吗?不行——一台服务器要同时提供多种服务(网页 80 端口、FTP 21 端口……),端口号才是区分服务的钥匙。TCP/UDP 都用 16 位端口号(0~65535)。

49.2 UDP 数据格式:8 字节首部,短小精悍

图49.1.3 UDP数据格式

字段 位数 说明
源端口号 16 本实验固定 1234
目的端口号 16 本实验固定 1234
UDP 长度 16 首部+数据总长
UDP 校验和 16 含伪首部校验,接收端一般不检测,本实验直接填 0

完整的封装关系:MAC帧[ IP数据报[ UDP报文[ 用户数据 ] ] ],用户数据一包最多 1472 字节(1500 − 20 − 8)。

UDP 校验和的完整算法涉及"伪首部"(从 IP 头里抽出来的源/目的 IP、协议号、UDP 长度),本实验偷了个懒——置 0 不校验,实际接收端大多也不查,可以接受。

49.4 程序设计:UDP 模块 + 三协议切换

总体架构

图49.4.1 系统架构图

相比 ICMP 实验,变化点:

  • 新增 UDP 模块(udp_rx + udp_tx + crc32_d8);
  • FIFO 从同步升级为异步 FIFO(虽然当前收发还是同时钟,但为后续例程留好了口子);
  • eth_ctrl 的协议切换信号 protocol_sw 从 1 位扩展为 2 位,四种取值:00=ARP、01=UDP、10=ICMP。

udp_rx:多一层首部而已

接收状态机与 ICMP 版本几乎一样,只是 st_ip_head 后面接的是 st_udp_head(解析 8 字节 UDP 首部)而不是 ICMP 首部。老规矩:中间状态出错跳 st_rx_end,等 gmii_rx_dv=0 再复位,避免把同一包的剩余字节误认为新的前导码。

接收有效数据的核心代码:

st_rx_data : begin
    //接收数据
    if(gmii_rx_dv) begin
        udp_rx_cnt <= udp_rx_cnt + 16'd1;
        rec_data   <= gmii_rxd;
        rec_en     <= 1'b1;
        if(udp_rx_cnt == data_byte_num - 16'd1) begin
            udp_rx_cnt   <= 16'd0;
            rec_pkt_done <= 1'b1;
            rec_byte_num <= data_byte_num;
        end
    end
end

注意校验和旁路累加在 UDP 版本里没有了——因为 UDP 校验和我们不算。

udp_tx:IP 首部协议号换成 17

发送状态机与 ICMP 版本同构,IP 首部初始化只有一处不同:

ip_head[2] <= {8'h40,8'd17,16'h0};   //协议:17(udp) ←ICMP实验里是8'd01
//UDP头部
udp_head[0] <= {16'd1234,16'd1234};  //源端口号:1234 目的端口号:1234
udp_head[1] <= {udp_num,16'h0000};   //16位UDP长度,校验和置0

发送数据状态同样处理了最小帧长问题

st_tx_data : begin
    crc_en     <= 1'b1;
    gmii_tx_en <= 1'b1;
    gmii_txd   <= (udp_tx_cnt < tx_data_num) ? tx_data : 8'd0; //不足18字节补0
    ...
end

以太网数据段最少 46 字节,去掉 IP 首部(20)和 UDP 首部(8)后,有效数据至少 18 字节,不足就补零。

eth_ctrl:二位宽开关 + 数据通路复用

三协议切换的核心是一个 2 位选择器:

case(protocol_sw)
    2'b00: begin
        gmii_tx_en <= arp_gmii_tx_en;
        gmii_txd   <= arp_gmii_txd;
    end
    2'b01: begin
        gmii_tx_en <= udp_gmii_tx_en;
        gmii_txd   <= udp_gmii_txd;
    end
    2'b10: begin
        gmii_tx_en <= icmp_gmii_tx_en;
        gmii_txd   <= icmp_gmii_txd;
    end
endcase

优先级逻辑:UDP 请求 > ICMP 请求 > ARP,且 ARP 必须等 UDP 和 ICMP 都空闲才被响应。

除了发送总线,接收数据和 FIFO 读请求也要做三路复用:

//接收数据使能与数据判断(ICMP和UDP收到的数据统一送FIFO)
else if (icmp_rec_en) begin
    rec_en   <= icmp_rec_en;
    rec_data <= icmp_rec_data;
end
else if(udp_rec_en) begin
    rec_en   <= udp_rec_en;
    rec_data <= udp_rec_data;
end
//读请求仲裁
assign tx_req = udp_tx_req ? 1'b1 : icmp_tx_req;

这里有个容易忽略的细节:icmp_tx_requdp_tx_req 各打了一拍再用于数据选择(icmp_tx_req_d0/udp_tx_req_d0),让数据选择与请求信号对齐,避免第一个字节读错。

顶层环回链路

assign icmp_tx_start_en = icmp_rec_pkt_done;  //收到ICMP请求→发应答
assign tx_start_en      = rec_pkt_done;       //收到UDP包→原样发回
assign des_mac          = src_mac;            //目标地址从解析结果来
assign des_ip           = src_ip;

整个系统本质是"收到什么发什么"的镜像结构,这也是所有回环测试的经典范式。

49.5 下载验证

管脚与 ARP 实验一致。验证流程:

  1. 电脑 IP 设 192.168.1.102,下载程序;
  2. 打开网口调试助手:协议选 UDP,本地 IP 192.168.1.102、本地端口 1234,点【打开】;
  3. 远程主机填 192.168.1.10:1234(开发板 IP 和端口号都是 1234,对应代码里 udp_head[0] <= {16'd1234,16'd1234});
  4. 发送 http://www.openedv.com/forum.php,接收窗口原样出现同样的字符串——环回成功!

图49.5.5 网口调试助手收发数据界面

用 Wireshark 抓包可以看到完整的四次交互:ARP 请求→ARP 应答→UDP 数据→UDP 回包,双击回包能看到源/目的 IP 和端口均为 1234,数据段正是发送的字符串。

图49.5.9 Wireshark抓取到的详细数据

初学者容易踩的坑

  1. 端口号不匹配:调试助手本地端口必须是 1234(与代码一致),否则发出去的包开发板不认。
  2. IP 协议号忘改:从 ICMP 例程改 UDP 时,IP 首部协议字段必须从 1 改成 17,否则电脑把包当 ICMP 解析失败。
  3. UDP 长度算错:UDP 长度 = 首部 8 字节 + 数据长度,不是以太网总长度,多算 20 字节会被丢弃。
  4. 协议切换丢包:UDP 发送过程中来了 ARP 请求,如果不判 udp_tx_busy 就切走 protocol_sw,正在发的 UDP 包会被截断。
  5. FIFO 读请求没对齐tx_req 与数据选择之间差一拍,导致发送的第一个字节是 FIFO 里残留的旧数据。
  6. 最小 18 字节有效数据:发送数据不足 18 字节又不补零,帧长不足 46 字节,电脑网卡直接丢弃。

小结 & 下一步

本章是以太网协议栈的"毕业设计":

  • UDP 首部只有 8 字节,端口号是服务区分的核心;UDP 校验和含伪首部,简单场景可置 0;
  • 三协议共存架构:协议解析并行独立、发送总线用 2 位开关串行复用、优先级 UDP > ICMP > ARP;
  • 异步 FIFO 替代同步 FIFO,为跨时钟域应用做好准备;
  • 完整的环回验证 + Wireshark 抓包分析,四包流程(ARP×2 + UDP×2)一气呵成。

至此,FPGA 已经具备了完整的以太网通信能力。下一章开始玩法升级:基于 OV5640 摄像头的 PL 以太网视频传输实验——把摄像头画面通过网络实时传出去,前面所有的协议模块都将派上用场。