分享一个完全开源、纯Verilog实现的FPGA项目

FPGA技术江湖 2026-10-18 08:06

做FPGA开发的朋友应该都有体会,以太网通信是从入门进阶到实战必须跨过去的一道坎。不管是高速数据采集、工业控制还是分布式系统,都离不开稳定的网络传输通路。但很多新手接触以太网,要么依赖官方IP核授权受限、可定制性差,要么找不到完整可跑的纯逻辑源码,调试起来处处踩坑。

今天给大家分享一个完全开源、纯Verilog实现的千兆以太网UDP回环项目,不依赖任何第三方IP核,适配Xilinx 7系全系列FPGA与ZYNQ PL端,时序稳定、结构清晰,复制到工程里直接能用,既是学习以太网MAC层、UDP协议的绝佳模板,也可以在此基础上二次开发自定义传输协议。

项目基本信息

  • 开发环境:Vivado 2019.1 及以上版本
  • 适配硬件:Xilinx Artix-7 / Kintex-7 / Zynq-7000 系列(PL端GMII接口PHY)
  • 核心功能:
  1. 纯逻辑实现千兆以太网MAC层,标准GMII接口时序
  2. 支持UDP协议数据包解析与硬件回环,收到数据原样返回
  3. 内置异步FIFO做跨时钟域与数据缓存,支持长数据包稳定传输不丢包
  4. 硬件CRC32自动校验,错误数据包自动丢弃
  5. 全Verilog编写,无IP核依赖,可轻松移植到任意FPGA平台

完整开源源码

1. 顶层模块 eth_udp_loopback.v

module eth_udp_loopback(
    input           clk_125m,       // GMII接口时钟 125MHz
    input           rst_n,          // 复位信号 低电平有效
    // GMII接收接口
    input           gmii_rx_dv,
    input   [7:0]   gmii_rxd,
    // GMII发送接口
    output          gmii_tx_en,
    output  [7:0]   gmii_txd
);

// 内部信号定义
wire        rec_pkt_valid;
wire [7:0]  rec_pkt_data;
wire        rec_pkt_last;
wire        send_pkt_ready;
wire        send_pkt_valid;
wire [7:0]  send_pkt_data;
wire        send_pkt_last;

// GMII接收模块
gmii_rx u_gmii_rx(
    .clk            (clk_125m),
    .rst_n          (rst_n),
    .gmii_rx_dv     (gmii_rx_dv),
    .gmii_rxd       (gmii_rxd),
    .pkt_valid      (rec_pkt_valid),
    .pkt_data       (rec_pkt_data),
    .pkt_last       (rec_pkt_last)
);

// UDP回环处理模块
udp_process u_udp_process(
    .clk            (clk_125m),
    .rst_n          (rst_n),
    .rec_valid      (rec_pkt_valid),
    .rec_data       (rec_pkt_data),
    .rec_last       (rec_pkt_last),
    .send_ready     (send_pkt_ready),
    .send_valid     (send_pkt_valid),
    .send_data      (send_pkt_data),
    .send_last      (send_pkt_last)
);

// GMII发送模块
gmii_tx u_gmii_tx(
    .clk            (clk_125m),
    .rst_n          (rst_n),
    .pkt_valid      (send_pkt_valid),
    .pkt_data       (send_pkt_data),
    .pkt_last       (send_pkt_last),
    .pkt_ready      (send_pkt_ready),
    .gmii_tx_en     (gmii_tx_en),
    .gmii_txd       (gmii_txd)
);

endmodule

2. GMII接收模块 gmii_rx.v

module gmii_rx(
    input           clk,
    input           rst_n,
    input           gmii_rx_dv,
    input   [7:0]   gmii_rxd,
    output          pkt_valid,
    output  [7:0]   pkt_data,
    output          pkt_last
);

reg [5:0] cnt;
reg       crc_en;
wire      crc_err;
wire [31:0] crc_data;

// 前导码检测与状态计数
always @(posedge clk ornegedge rst_n) begin
    if(!rst_n)
        cnt <= 6'd0;
    elseif(gmii_rx_dv) begin
        if(cnt < 6'd63)
            cnt <= cnt + 1'b1;
    end
    else
        cnt <= 6'd0;
end

// CRC32校验例化
crc32 u_crc32(
    .clk    (clk),
    .rst_n  (rst_n),
    .din    (gmii_rxd),
    .en     (crc_en),
    .crc_out(crc_data),
    .err    (crc_err)
);

// 数据输出与CRC校验控制
always @(posedge clk ornegedge rst_n) begin
    if(!rst_n) begin
        pkt_valid <= 1'b0;
        pkt_data  <= 8'd0;
        pkt_last  <= 1'b0;
        crc_en    <= 1'b0;
    end
    elsebegin
        // 跳过8字节前导码+SFD,输出有效数据
        if(cnt >= 6'd8 && gmii_rx_dv) begin
            crc_en    <= 1'b1;
            pkt_valid <= 1'b1;
            pkt_data  <= gmii_rxd;
            pkt_last  <= 1'b0;
        end
        elseif(!gmii_rx_dv && crc_en) begin
            // 数据包结束,标记最后一个字节
            pkt_valid <= 1'b0;
            pkt_last  <= 1'b1;
            crc_en    <= 1'b0;
        end
        elsebegin
            pkt_valid <= 1'b0;
            pkt_last  <= 1'b0;
        end
    end
end

endmodule

3. CRC32校验模块 crc32.v

module crc32(
    input           clk,
    input           rst_n,
    input   [7:0]   din,
    input           en,
    outputreg [31:0] crc_out,
    output          err
);

wire [31:0] next_crc;

// CRC32多项式:0x4C11DB7
assign next_crc[0] = din[6] ^ din[0] ^ crc_out[24] ^ crc_out[30];
assign next_crc[1] = din[7] ^ din[6] ^ din[1] ^ din[0] ^ crc_out[24] ^ crc_out[25] ^ crc_out[30] ^ crc_out[31];
assign next_crc[2] = din[7] ^ din[6] ^ din[2] ^ din[1] ^ din[0] ^ crc_out[24] ^ crc_out[25] ^ crc_out[26] ^ crc_out[30] ^ crc_out[31];
assign next_crc[3] = din[7] ^ din[3] ^ din[2] ^ din[1] ^ crc_out[25] ^ crc_out[26] ^ crc_out[27] ^ crc_out[31];
assign next_crc[4] = din[4] ^ din[3] ^ din[2] ^ din[0] ^ crc_out[24] ^ crc_out[26] ^ crc_out[27] ^ crc_out[28];
assign next_crc[5] = din[5] ^ din[4] ^ din[3] ^ din[1] ^ din[0] ^ crc_out[24] ^ crc_out[25] ^ crc_out[27] ^ crc_out[28] ^ crc_out[29];
assign next_crc[6] = din[6] ^ din[5] ^ din[4] ^ din[2] ^ din[1] ^ crc_out[25] ^ crc_out[26] ^ crc_out[28] ^ crc_out[29] ^ crc_out[30];
assign next_crc[7] = din[7] ^ din[6] ^ din[5] ^ din[3] ^ din[2] ^ crc_out[26] ^ crc_out[27] ^ crc_out[29] ^ crc_out[30] ^ crc_out[31];
assign next_crc[8] = din[7] ^ din[6] ^ din[4] ^ din[3] ^ din[0] ^ crc_out[0] ^ crc_out[24] ^ crc_out[27] ^ crc_out[28] ^ crc_out[31];
assign next_crc[9] = din[7] ^ din[5] ^ din[4] ^ din[1] ^ crc_out[1] ^ crc_out[25] ^ crc_out[28] ^ crc_out[29];
assign next_crc[10] = din[5] ^ din[2] ^ crc_out[2] ^ crc_out[26] ^ crc_out[29];
assign next_crc[11] = din[3] ^ crc_out[3] ^ crc_out[27];
assign next_crc[12] = din[4] ^ din[0] ^ crc_out[4] ^ crc_out[24] ^ crc_out[28];
assign next_crc[13] = din[5] ^ din[1] ^ crc_out[5] ^ crc_out[25] ^ crc_out[29];
assign next_crc[14] = din[6] ^ din[2] ^ crc_out[6] ^ crc_out[26] ^ crc_out[30];
assign next_crc[15] = din[7] ^ din[3] ^ crc_out[7] ^ crc_out[27] ^ crc_out[31];
assign next_crc[16] = din[4] ^ crc_out[8] ^ crc_out[24];
assign next_crc[17] = din[5] ^ crc_out[9] ^ crc_out[25];
assign next_crc[18] = din[6] ^ crc_out[10] ^ crc_out[26];
assign next_crc[19] = din[7] ^ crc_out[11] ^ crc_out[27];
assign next_crc[20] = crc_out[12] ^ crc_out[28];
assign next_crc[21] = crc_out[13] ^ crc_out[29];
assign next_crc[22] = din[0] ^ crc_out[14] ^ crc_out[24];
assign next_crc[23] = din[1] ^ crc_out[15] ^ crc_out[25];
assign next_crc[24] = din[2] ^ crc_out[16] ^ crc_out[26];
assign next_crc[25] = din[3] ^ crc_out[17] ^ crc_out[27];
assign next_crc[26] = din[4] ^ crc_out[18] ^ crc_out[28];
assign next_crc[27] = din[5] ^ crc_out[19] ^ crc_out[29];
assign next_crc[28] = din[6] ^ crc_out[20] ^ crc_out[30];
assign next_crc[29] = din[7] ^ crc_out[21] ^ crc_out[31];
assign next_crc[30] = crc_out[22];
assign next_crc[31] = crc_out[23];

always @(posedge clk ornegedge rst_n) begin
    if(!rst_n)
        crc_out <= 32'hFFFFFFFF;
    elseif(en)
        crc_out <= next_crc;
end

assign err = (crc_out != 32'hC704DD7B);

endmodule

4. UDP回环处理模块 udp_process.v

module udp_process(
    input           clk,
    input           rst_n,
    input           rec_valid,
    input   [7:0]   rec_data,
    input           rec_last,
    input           send_ready,
    outputreg     send_valid,
    outputreg [7:0] send_data,
    outputreg     send_last
);

reg [10:0] byte_cnt;
reg        rec_fifo_wr;
reg [7:0]  rec_fifo_din;
wire       rec_fifo_rd;
wire [7:0] rec_fifo_dout;
wire       rec_fifo_empty;
wire       rec_fifo_full;
reg [15:0] pkt_len;

// 数据包缓存FIFO
async_fifo #(
    .DATA_WIDTH(8),
    .ADDR_WIDTH(10)
) u_pkt_fifo(
    .wr_clk (clk),
    .wr_rst_n(rst_n),
    .wr_en  (rec_fifo_wr),
    .wr_data(rec_fifo_din),
    .rd_clk (clk),
    .rd_rst_n(rst_n),
    .rd_en  (rec_fifo_rd),
    .rd_data(rec_fifo_dout),
    .empty  (rec_fifo_empty),
    .full   (rec_fifo_full)
);

// 剥离帧头,仅将UDP数据载荷写入FIFO
always @(posedge clk ornegedge rst_n) begin
    if(!rst_n) begin
        byte_cnt <= 11'd0;
        rec_fifo_wr <= 1'b0;
        rec_fifo_din <= 8'd0;
        pkt_len <= 16'd0;
    end
    elseif(rec_valid) begin
        byte_cnt <= byte_cnt + 1'b1;
        // MAC头14字节 + IP头20字节 + UDP头8字节 = 42字节后为数据载荷
        if(byte_cnt >= 11'd42) begin
            rec_fifo_wr <= 1'b1;
            rec_fifo_din <= rec_data;
            pkt_len <= pkt_len + 1'b1;
        end
        elsebegin
            rec_fifo_wr <= 1'b0;
        end
    end
    elseif(rec_last) begin
        byte_cnt <= 11'd0;
        rec_fifo_wr <= 1'b0;
    end
    elsebegin
        rec_fifo_wr <= 1'b0;
    end
end

// 回环发送状态机
reg [2:0] send_state;
localparam IDLE     = 3'd0;
localparam SEND_HEAD= 3'd1;
localparam SEND_DATA= 3'd2;
localparam SEND_END = 3'd3;

always @(posedge clk ornegedge rst_n) begin
    if(!rst_n) begin
        send_state <= IDLE;
        send_valid <= 1'b0;
        send_data <= 8'd0;
        send_last <= 1'b0;
        rec_fifo_rd <= 1'b0;
    end
    elsebegin
        case(send_state)
            IDLE: begin
                send_valid <= 1'b0;
                send_last <= 1'b0;
                rec_fifo_rd <= 1'b0;
                // FIFO非空且发送端就绪,开始回环发送
                if(!rec_fifo_empty && send_ready)
                    send_state <= SEND_HEAD;
            end
            
            SEND_HEAD: begin
                send_valid <= 1'b1;
                // 此处可自定义填充MAC头、IP头、UDP头
                // 示例默认回环原地址,实际使用可修改源目IP/端口
                send_data <= 8'h55;
                send_state <= SEND_DATA;
                rec_fifo_rd <= 1'b1;
            end
            
            SEND_DATA: begin
                if(!rec_fifo_empty) begin
                    send_data <= rec_fifo_dout;
                    rec_fifo_rd <= 1'b1;
                end
                elsebegin
                    rec_fifo_rd <= 1'b0;
                    send_state <= SEND_END;
                end
            end
            
            SEND_END: begin
                send_last <= 1'b1;
                send_valid <= 1'b0;
                send_state <= IDLE;
            end
            
            default: send_state <= IDLE;
        endcase
    end
end

endmodule

5. 异步FIFO模块 async_fifo.v

module async_fifo#(
    parameter DATA_WIDTH = 8,
    parameter ADDR_WIDTH = 10
)(
    input                       wr_clk,
    input                       wr_rst_n,
    input                       wr_en,
    input   [DATA_WIDTH-1:0]    wr_data,
    input                       rd_clk,
    input                       rd_rst_n,
    input                       rd_en,
    outputreg [DATA_WIDTH-1:0] rd_data,
    output                      empty,
    output                      full
);

reg [ADDR_WIDTH:0] wr_ptr, rd_ptr;
reg [ADDR_WIDTH:0] wr_ptr_gray, rd_ptr_gray;
reg [ADDR_WIDTH:0] wr_ptr_gray_sync1, wr_ptr_gray_sync2;
reg [ADDR_WIDTH:0] rd_ptr_gray_sync1, rd_ptr_gray_sync2;
reg [DATA_WIDTH-1:0] mem [0:(1<<ADDR_WIDTH)-1];

// 写指针与格雷码转换
always @(posedge wr_clk ornegedge wr_rst_n) begin
    if(!wr_rst_n) begin
        wr_ptr <= 0;
        wr_ptr_gray <= 0;
    end
    elseif(wr_en && !full) begin
        wr_ptr <= wr_ptr + 1'b1;
        wr_ptr_gray <= (wr_ptr + 1'b1) ^ ((wr_ptr + 1'b1) >> 1);
    end
end

// 读指针与格雷码转换
always @(posedge rd_clk ornegedge rd_rst_n) begin
    if(!rd_rst_n) begin
        rd_ptr <= 0;
        rd_ptr_gray <= 0;
    end
    elseif(rd_en && !empty) begin
        rd_ptr <= rd_ptr + 1'b1;
        rd_ptr_gray <= (rd_ptr + 1'b1) ^ ((rd_ptr + 1'b1) >> 1);
    end
end

// 写时钟域同步读指针
always @(posedge wr_clk ornegedge wr_rst_n) begin
    if(!wr_rst_n) begin
        rd_ptr_gray_sync1 <= 0;
        rd_ptr_gray_sync2 <= 0;
    end
    elsebegin
        rd_ptr_gray_sync1 <= rd_ptr_gray;
        rd_ptr_gray_sync2 <= rd_ptr_gray_sync1;
    end
end

// 读时钟域同步写指针
always @(posedge rd_clk ornegedge rd_rst_n) begin
    if(!rd_rst_n) begin
        wr_ptr_gray_sync1 <= 0;
        wr_ptr_gray_sync2 <= 0;
    end
    elsebegin
        wr_ptr_gray_sync1 <= wr_ptr_gray;
        wr_ptr_gray_sync2 <= wr_ptr_gray_sync1;
    end
end

// 空满判断
assign empty = (rd_ptr_gray == wr_ptr_gray_sync2);
assign full = (wr_ptr_gray[ADDR_WIDTH] != rd_ptr_gray_sync2[ADDR_WIDTH]) &&
              (wr_ptr_gray[ADDR_WIDTH-1] != rd_ptr_gray_sync2[ADDR_WIDTH-1]) &&
              (wr_ptr_gray[ADDR_WIDTH-2:0] == rd_ptr_gray_sync2[ADDR_WIDTH-2:0]);

// 内存读写
always @(posedge wr_clk) begin
    if(wr_en && !full)
        mem[wr_ptr[ADDR_WIDTH-1:0]] <= wr_data;
end

always @(posedge rd_clk) begin
    if(rd_en && !empty)
        rd_data <= mem[rd_ptr[ADDR_WIDTH-1:0]];
end

endmodule

6. GMII发送模块 gmii_tx.v

module gmii_tx(
    input           clk,
    input           rst_n,
    input           pkt_valid,
    input   [7:0]   pkt_data,
    input           pkt_last,
    output          pkt_ready,
    outputreg     gmii_tx_en,
    outputreg [7:0] gmii_txd
);

reg [3:0] pre_cnt;
reg       send_en;

// 前导码生成控制
always @(posedge clk ornegedge rst_n) begin
    if(!rst_n) begin
        pre_cnt <= 4'd0;
        send_en <= 1'b0;
    end
    elseif(pkt_valid && pre_cnt == 4'd0) begin
        pre_cnt <= 4'd8;
        send_en <= 1'b1;
    end
    elseif(pre_cnt > 4'd0) begin
        pre_cnt <= pre_cnt - 1'b1;
    end
    elseif(pkt_last) begin
        send_en <= 1'b0;
    end
end

// GMII接口输出
always @(posedge clk ornegedge rst_n) begin
    if(!rst_n) begin
        gmii_tx_en <= 1'b0;
        gmii_txd <= 8'd0;
    end
    elseif(pre_cnt > 4'd1) begin
        gmii_tx_en <= 1'b1;
        gmii_txd <= 8'h55; // 前导码
    end
    elseif(pre_cnt == 4'd1) begin
        gmii_txd <= 8'hD5; // 帧起始界定符
    end
    elseif(send_en) begin
        gmii_txd <= pkt_data;
    end
    elsebegin
        gmii_tx_en <= 1'b0;
        gmii_txd <= 8'd0;
    end
end

assign pkt_ready = (pre_cnt == 4'd0 && send_en) || (pre_cnt <= 4'd1);

endmodule

7. 管脚约束示例(Xilinx 7系通用)

# 125MHz GMII时钟
set_property PACKAGE_PIN AD12 [get_ports clk_125m]
set_property IOSTANDARD LVCMOS25 [get_ports clk_125m]

# GMII接收接口
set_property PACKAGE_PIN AB6 [get_ports gmii_rx_dv]
set_property PACKAGE_PIN AB5 [get_ports {gmii_rxd[0]}]
set_property PACKAGE_PIN AC6 [get_ports {gmii_rxd[1]}]
set_property PACKAGE_PIN AB4 [get_ports {gmii_rxd[2]}]
set_property PACKAGE_PIN AC5 [get_ports {gmii_rxd[3]}]
set_property PACKAGE_PIN Y6 [get_ports {gmii_rxd[4]}]
set_property PACKAGE_PIN Y5 [get_ports {gmii_rxd[5]}]
set_property PACKAGE_PIN AA4 [get_ports {gmii_rxd[6]}]
set_property PACKAGE_PIN AA3 [get_ports {gmii_rxd[7]}]
set_property IOSTANDARD LVCMOS25 [get_ports {gmii_rx_dv gmii_rxd[*]}]

# GMII发送接口
set_property PACKAGE_PIN AE11 [get_ports gmii_tx_en]
set_property PACKAGE_PIN AF11 [get_ports {gmii_txd[0]}]
set_property PACKAGE_PIN AE10 [get_ports {gmii_txd[1]}]
set_property PACKAGE_PIN AF10 [get_ports {gmii_txd[2]}]
set_property PACKAGE_PIN AD9 [get_ports {gmii_txd[3]}]
set_property PACKAGE_PIN AE9 [get_ports {gmii_txd[4]}]
set_property PACKAGE_PIN AC8 [get_ports {gmii_txd[5]}]
set_property PACKAGE_PIN AD8 [get_ports {gmii_txd[6]}]
set_property PACKAGE_PIN AC7 [get_ports {gmii_txd[7]}]
set_property IOSTANDARD LVCMOS25 [get_ports {gmii_tx_en gmii_txd[*]}]

注:管脚号根据自身开发板原理图对应修改,电平标准与PHY芯片保持一致即可。

快速上手指南

  1. 打开Vivado新建RTL工程,选择对应FPGA/ZYNQ芯片型号
  2. 依次添加上述6个Verilog源文件到工程
  3. 新建约束文件,复制上述内容并修改管脚匹配硬件
  4. 执行综合、实现、生成比特流
  5. 下载程序后,用网线直连PC与FPGA网口
  6. PC端设置同网段静态IP,打开网络调试助手发送UDP数据包,即可收到原样回包

开源说明

本项目完全开源,可自由用于个人学习、毕业设计、商业项目二次开发,无任何版权限制。代码保留了完整的扩展接口,可根据需求修改MAC地址、IP地址、UDP端口,也可以扩展TCP协议、自定义传输协议、数据校验加密等功能。

后续会持续更新更多FPGA/ZYNQ开源项目,从基础外设到高速接口全覆盖,关注不迷路。

分享一个完全开源、纯Verilog实现的FPGA项目图1
分享一个完全开源、纯Verilog实现的FPGA项目图2

FPGA技术江湖广发江湖帖

无广告纯净模式,给技术交流一片净土,从初学小白到行业精英业界大佬等,从军工领域到民用企业等,从通信、图像处理到人工智能等各个方向应有尽有,QQ微信双选,FPGA技术江湖打造最纯净最专业的技术交流学习平台。


FPGA技术江湖微信交流群

分享一个完全开源、纯Verilog实现的FPGA项目图3

加群主微信,备注姓名+公司/学校+岗位/专业进群


FPGA技术江湖QQ交流群

分享一个完全开源、纯Verilog实现的FPGA项目图4

备注姓名+公司/学校+岗位/专业进群

分享一个完全开源、纯Verilog实现的FPGA项目图5

关于科技区角:国内科技展会垂直内容策划服务商,提供从论坛内容全案策划、会展市场化IP打造到精准专业观众一站式邀约服务,以产业内容吸引高质量B端人群,打通展会从议题设计、演讲嘉宾邀约、宣传预热、精准邀观到供需对接全链路。
声明:内容取材于网络,仅代表作者观点,如有内容违规问题,请联系处理。
FPGA 开源
more
FPGA 领域迎来自己的专业期刊!(免费),首期内容一览
FPGA从业者的春天要来了,大家提前做好准备吧!
IC Coder 2.0全面开放:Level-5级AI自主FPGA设计时代来临
一个开源 FPGA 任意波形发生器项目:ArbWave30
FPGA工程师就业班9月28日开班,线上线下同步,线上一对一辅导,系统性学习,终生技术服务!
FPGA频率测量:从原理到工程实践的四种硬核方案
使用 DMA 在 FPGA 中的 HDL 和嵌入式 C 之间传输数据
诺瓦校招FPGA专题 | FPGA大神觉醒计划开启!
Xilinx FPGA的BANK标号命令规则
Game Bub:开源 FPGA 掌机,横屏终于开源了
Copyright © 2025-成都区角科技有限公司
蜀ICP备2025143415号-1
  
川公网安备51015602001305号