笔记根据正点原子官方教学视频第33讲
时间:2025/4/2
【第一期】手把手教你学领航者&启明星ZYNQ之FPGA开发篇【真人出镜】FPGA教学视频教程_哔哩哔哩_bilibili
有错误的地方希望可以告诉博主
实验任务
使用FPGA开发板上的以太网接口,和上位机实现 ARP请求和应答的功能。当上位机发送ARP请求时,开发板返回ARP应答数据。当按下开发板的触摸按键时,开发板发送ARP请求,此时上位机返回应答数据。
程序框图
首先是顶层模块 eth_arp_test 框图,输入信号和输出信号是根据 RGMII 接口的要求和触摸按键功能确定的,无论是从 PHY 芯片接收数据还是向 PHY 芯片发送数据,都需要使用 RGMII 接口(因为使用的开发板PHY芯片为 YT8531C,数据手册上指明了其与 MAC 模块的通信接口为 RGMII,下文有详细介绍)。

PLL 模块的框图,功能是将输入的系统时钟转变为 200MHz 的参考时钟给 IDELAY 模块以制造延迟。而需要 IDELAY 产生延迟的原因是 RGMII 的发送和接收在时序上有2ns的延迟。
RGMII和GMII相互转换模块,作用是适配不同的接口标准和数据传输需求。
这里解释一下为什么要做这个转换,具体需要用到 FPGA 开发板的底板原理图,从下图中就可以看出此块开发板搭载的 PHY 芯片为 YT8531C,在数据手册中可以看到 YT8531C 对应的 MAC 接口为 RGMII(其实原理图上也可以看出)。将 RGMII 形式的信号转变为 GMII,有很多原因:一是很多 FPGA 的数据总线宽度为8位,适用于许多基本的配置模式和数据传输场景;二是如果直接使用 RGMII 的信号,所有数据处理模块均需要适配双沿时钟和半字节操作,导致设计复杂度和资源消耗大幅增加。
在通用场景中,转换为 GMII 是更加具有性价比的方案。
接下来是 ARP 控制模块,用于判断 ARP 的类型是请求还是应答 。
最后是 ARP 解析和编码模块。
本模块处于 FPGA 内部,故使用 GMII 接口信号。
RGMII 和 GMII 相互转换模块详细分析
实现思路解析
整个模块划分为两个部分,一是 RGMII 转 GMII 模块,二是 GMII 转 RGMII 模块。
RGMII 转 GMII 功能实现
理论解释
因为 RGMII 是双沿信号,所以需要采用 IDDR 对其进行双沿采样,并将RGMII的4位数据扩展为GMII的8位数据。IDDR 的工作模式选择使用相同沿的流水模式。再之前的文章中讲到过此工作模式(FPGA学习笔记——Xilinx原语-优快云博客),输入信号 D 将会采样给 Q1 和 Q2。框图中的 rgmii_rxd 便是信号 D,Q1 和 Q2 按高低位组合起来合成信号 gmii_rxd。
上图就是该模块的简要描述。
输入信号为: rgmii_rxc、rgmii_rx_ctl、rgmii_rxd[3:0]、idelay_clk(作为参考时钟输入到 IDELAY ,只不过上图未标明)。
输出信号为:gmii_rx_clk、gmii_rx_dv、gmii_rxd[7:0]。
如何确定这些信号的?
从下图(左:GMII,右:RGMII)可以看出,FPGA 侧的输入一开始是 RGMII 接口信号,即下图右侧的 ETH_RXC、ETH_RXCTL、ETH_RXD[3:0],分别对应 rgmii_rxc、rgmii_rx_ctl、rgmii_rxd[3:0],另外还需要一个 idelay_clk 信号作为 IDELAY 的参考时钟。输出信号则来自下图左侧的 ETH_RXC、ETH_RXDV、ETH_RXD[7:0],分别对应 gmii_rx_clk、gmii_rx_dv、gmii_rxd[7:0],至于 ETH_RXER 是在本项目中不需要,如果需要的话也是可以加上的。
讲解一下为什么会需要 IDELAY 模块。
设计 IDELAY 的原因是为了增强代码的通用性,因为有一些 PHY 芯片内部不提供硬件延迟,所以我们需要在 FPGA 侧补上一个延迟。为什么需要这个延迟呢?看下图,这是正常带有硬件延迟的 PHY 芯片输出到 MAC 上的信号,RX_CLK 被延迟了2ns以便 FPGA 能在时钟边沿去处理 RXD 和 RX_CTRL。如果 PHY 芯片没有硬件延迟的话,就需要使用 IDELAY 来对数据信号和控制信号进行延迟,以便 FPGA 能在时钟边沿采集到。
为什么不延迟时钟信号呢?因为延迟时钟信号会给系统带来不稳定因素,一般还是对其他信号进行延迟会比较安全。
然后,解释一下为什么用到 BUFIO 和 BUFG。
BUFIO 是一种专门用于驱动 FPGA 输入/输出(I/O)时钟的缓冲器,其主要作用是为 I/O 逻辑单元(如 IDDR、ODDR 等)提供低延迟的时钟信号。之所有 rgmii_rxc 会有一部分接到 BUFIO 是因为 BUFIO 与 BUFG 相比,在距离上更接近 IDDR,延迟会更小。 但 BUFIO 只能驱动 IO Block 里面的逻辑,不能驱动 CLB 里面的 LUT,REG 等逻辑,所以需要配合BUFG使用。
如果单独使用 BUFG 时钟信号可能会受到较大的延迟和抖动影响,导致数据采样不准确。如果不使用 BUFIO,rgmii_rxc 的抖动和噪声可能会增加,影响时钟信号的稳定性,导致数据采样不准确。
代码解释
对于以下部分例化模块,为什么有些接口或参数没有列出?因为这些参数或接口是有默认值的,或者在特定工作模式下有些端口不起作用。这里不再一一列举,只需了解省略原因即可。
generate 和 genvar 实现循环例化,例化4次,分别处理4位 RGMII 数据。
另一个可能混淆的点是:gmii_rxdv_t[0] 和 gmii_rxdv_t[1] 分别对应时钟周期的上升沿和下降沿采集到的 rgmii_rx_ctl。在 RGMII 中,rgmii_rx_ctl 在时钟的上升沿和下降沿同为高电平才有效,故采用相与的方式合成 gmii_rx_dv(复习 RGMII 可查看此文章:FPGA学习笔记——以太网-优快云博客)。
module rgmii_rx(
input idelay_clk , //200Mhz时钟,IDELAY时钟
//以太网RGMII接口
input rgmii_rxc , //RGMII接收时钟
input rgmii_rx_ctl, //RGMII接收数据控制信号
input [3:0] rgmii_rxd , //RGMII接收数据
//以太网GMII接口
output gmii_rx_clk , //GMII接收时钟
output gmii_rx_dv , //GMII接收数据有效信号
output [7:0] gmii_rxd //GMII接收数据
);
//parameter define
parameter IDELAY_VALUE = 0;
//wire define
wire rgmii_rxc_bufg; //全局时钟缓存
wire rgmii_rxc_bufio; //全局时钟IO缓存
wire [3:0] rgmii_rxd_delay; //rgmii_rxd输入延时
wire rgmii_rx_ctl_delay; //rgmii_rx_ctl输入延时
wire [1:0] gmii_rxdv_t; //两位GMII接收有效信号
//*****************************************************
//** main code
//*****************************************************
assign gmii_rx_clk = rgmii_rxc_bufg;
assign gmii_rx_dv = gmii_rxdv_t[0] & gmii_rxdv_t[1]; // rgmii_rx_ctl在时钟上升沿和下降沿同为高电平才有效,所以这里需要相与。
//全局时钟缓存
BUFG BUFG_inst (
.I (rgmii_rxc),
.O (rgmii_rxc_bufg)
);
//全局时钟IO缓存——BUFIO
BUFIO BUFIO_inst (
.I (rgmii_rxc),
.O (rgmii_rxc_bufio)
);
//输入延时控制
// Specifies group name for associated IDELAYs/ODELAYs and IDELAYCTRL
(* IODELAY_GROUP = "rgmii_rx_delay" *)
IDELAYCTRL IDELAYCTRL_inst (
.RDY(), // 本项目不需要检测 RDY 的输出,故悬空
.REFCLK(idelay_clk),
.RST(1'b0)
);
//rgmii_rx_ctl输入延时与双沿采样
(* IODELAY_GROUP = "rgmii_rx_delay" *)
IDELAYE2 #(
.IDELAY_TYPE ("FIXED"),
.IDELAY_VALUE (IDELAY_VALUE),
.REFCLK_FREQUENCY(200.0)
)
u_delay_rx_ctrl (
.CNTVALUEOUT (),
.DATAOUT (rgmii_rx_ctl_delay),
.C (1'b0),
.CE (1'b0),
.CINVCTRL (1'b0),
.CNTVALUEIN (5'b0),
.DATAIN (1'b0),
.IDATAIN (rgmii_rx_ctl),
.INC (1'b0),
.LD (1'b0),
.LDPIPEEN (1'b0),
.REGRST (1'b0)
);
//输入双沿采样寄存器
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED"),
.INIT_Q1 (1'b0),
.INIT_Q2 (1'b0),
.SRTYPE ("SYNC")
) u_iddr_rx_ctl (
.Q1 (gmii_rxdv_t[0]),
.Q2 (gmii_rxdv_t[1]),
.C (rgmii_rxc_bufio),
.CE (1'b1),
.D (rgmii_rx_ctl_delay),
.R (1'b0),
.S (1'b0)
);
//rgmii_rxd输入延时与双沿采样
genvar i; // 循环例化4次
generate for (i=0; i<4; i=i+1)
(* IODELAY_GROUP = "rgmii_rx_delay" *)
begin : rxdata_bus
//输入延时
(* IODELAY_GROUP = "rgmii_rx_delay" *)
IDELAYE2 #(
.IDELAY_TYPE ("FIXED"),
.IDELAY_VALUE (IDELAY_VALUE),
.REFCLK_FREQUENCY(200.0)
)
u_delay_rxd (
.CNTVALUEOUT (),
.DATAOUT (rgmii_rxd_delay[i]),
.C (1'b0),
.CE (1'b0),
.CINVCTRL (1'b0),
.CNTVALUEIN (5'b0),
.DATAIN (1'b0),
.IDATAIN (rgmii_rxd[i]),
.INC (1'b0),
.LD (1'b0),
.LDPIPEEN (1'b0),
.REGRST (1'b0)
);
//输入双沿采样寄存器
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED"),
.INIT_Q1 (1'b0),
.INIT_Q2 (1'b0),
.SRTYPE ("SYNC")
) u_iddr_rxd (
.Q1 (gmii_rxd[i]),
.Q2 (gmii_rxd[4+i]),
.C (rgmii_rxc_bufio),
.CE (1'b1),
.D (rgmii_rxd_delay[i]),
.R (1'b0),
.S (1'b0)
);
end
endgenerate
endmodule
GMII 转 RGMII 功能实现
理论解释
为什么会需要 ODDR?因为 ODDR 可以将两个单沿信号采样并输出为双沿信号。ODDR 的工作模式选择使用相同沿模式。这里没有对 gmii_txc 进行延迟是因为 PHY 芯片内部自带有硬件延迟。
输入信号为:gmii_txc、gmii_tx_en、gmii_txd[7:0]。
输出信号为:rgmii_txc、rgmii_tx_ctl、rgmii_txd[3:0]。
如何确定这些信号的?
从下图(左:GMII,右:RGMII)可以看出,FPGA 侧的输出一开始是 GMII 信号,即下图左侧的 ETH_TXC、ETH_TXEN、ETH_TXD[7:0],分别对应 gmii_txc、gmii_tx_en、gmii_txd,至于 ETH_TXER 没有出现是因为本项目不需要,如果有需要的话,也是可以加上的。输出信号则来自下图右侧的 ETH_TXC、ETH_TXCTL、ETH_TXD[3:0],分别对应 rgmii_txc、rgmii_tx_ctl、rgmii_txd[3:0]。
代码解释
根据理论解释实现即可。
module rgmii_tx(
//GMII发送端口
input gmii_tx_clk , //GMII发送时钟
input gmii_tx_en , //GMII输出数据有效信号
input [7:0] gmii_txd , //GMII输出数据
//RGMII发送端口
output rgmii_txc , //RGMII发送数据时钟
output rgmii_tx_ctl, //RGMII输出数据有效信号
output [3:0] rgmii_txd //RGMII输出数据
);
//*****************************************************
//** main code
//*****************************************************
assign rgmii_txc = gmii_tx_clk;
//输出双沿采样寄存器 (rgmii_tx_ctl)
ODDR #(
.DDR_CLK_EDGE ("SAME_EDGE"),
.INIT (1'b0),
.SRTYPE ("SYNC")
) ODDR_inst (
.Q (rgmii_tx_ctl),
.C (gmii_tx_clk),
.CE (1'b1),
.D1 (gmii_tx_en),
.D2 (gmii_tx_en),
.R (1'b0),
.S (1'b0)
);
genvar i;
generate for (i=0; i<4; i=i+1)
begin : txdata_bus
//输出双沿采样寄存器 (rgmii_txd)
ODDR #(
.DDR_CLK_EDGE ("SAME_EDGE"),
.INIT (1'b0),
.SRTYPE ("SYNC")
) ODDR_inst (
.Q (rgmii_txd[i]),
.C (gmii_tx_clk),
.CE (1'b1),
.D1 (gmii_txd[i]),
.D2 (gmii_txd[4+i]),
.R (1'b0),
.S (1'b0)
);
end
endgenerate
endmodule
RGMII 和 GMII 相互转换模块
只需要实例化上述两个功能模块以及确定 IDELAY_VALUE 即可。
module gmii_to_rgmii(
input idelay_clk , //IDELAY参考时钟,是由PLL输出的
//以太网GMII接口,发送端和接收到都省略了err信号
output gmii_rx_clk , //GMII接收时钟
output gmii_rx_dv , //GMII接收数据有效信号
output [7:0] gmii_rxd , //GMII接收数据
output gmii_tx_clk , //GMII发送时钟
input gmii_tx_en , //GMII发送数据使能信号
input [7:0] gmii_txd , //GMII发送数据
//以太网RGMII接口
input rgmii_rxc , //RGMII接收时钟
input rgmii_rx_ctl, //RGMII接收数据控制信号
input [3:0] rgmii_rxd , //RGMII接收数据
output rgmii_txc , //RGMII发送时钟
output rgmii_tx_ctl, //RGMII发送数据控制信号
output [3:0] rgmii_txd //RGMII发送数据
);
//parameter define
parameter IDELAY_VALUE = 0; //输入数据IO延时(如果为n,表示延时n*78ps),这里取0是因为使用的PHY芯片自带延迟。如果是别的不带延迟的PHY芯片就需要在这里做延迟
//*****************************************************
//** main code
//*****************************************************
assign gmii_tx_clk = gmii_rx_clk;
//RGMII接收
rgmii_rx
#(
.IDELAY_VALUE (IDELAY_VALUE)
)
u_rgmii_rx(
.idelay_clk (idelay_clk),
.gmii_rx_clk (gmii_rx_clk),
.rgmii_rxc (rgmii_rxc ),
.rgmii_rx_ctl (rgmii_rx_ctl),
.rgmii_rxd (rgmii_rxd ),
.gmii_rx_dv (gmii_rx_dv ),
.gmii_rxd (gmii_rxd )
);
//RGMII发送
rgmii_tx u_rgmii_tx(
.gmii_tx_clk (gmii_tx_clk ),
.gmii_tx_en (gmii_tx_en ),
.gmii_txd (gmii_txd ),
.rgmii_txc (rgmii_txc ),
.rgmii_tx_ctl (rgmii_tx_ctl),
.rgmii_txd (rgmii_txd )
);
endmodule
接口信号时序
ARP 模块
设计框图如下:
如何确定这些信号的?
因为这些数据是从 FPGA 内部而来,所以采取 GMII 接口的形式。而 ARP 模块既要接收从转换模块传输来的 GMII 接口数据,又要向转换模块发送 GMII 接口数据。所以 ARP 需要分为接收模块和发送模块,然后加上一个发送模块需要的 CRC 校验模块。
首先考虑接收模块,需要输入信号有:gmii_rxc、gmii_rx_dv、gmii_rxd [7:0]、rst_n。
然后考虑 CRC 校验模块(只对以太网帧头和数据部分进行校验)和发送模块,需要输入信号有:gmii_txc、arp_tx_en、arp_tx_type、des_mac、des_ip、rst_n。由于以太网帧有4Byte校验位,所以输入到的数据还需要传入到 CRC 校验模块中生成校验数据。所以 CRC 校验模块需要的输入信号为:gmii_txc、rst_n、crc_clr、crc_en、data [7:0]。其中 crc_en 用于控制CRC校验的开始。当该信号为高电平时,CRC校验模块开始对输入数据进行校验。 而 crc_clr 用于复位CRC校验模块的内部数据。当该信号为高电平时,CRC校验模块的内部数据会被重新置为初始值(一般是全1)。
ARP 模块按照具体功能划分为:ARP 接收端、ARP 发送端、CRC 模块。
ARP 接收端
理论解释
输入信号为:gmii_rxc、gmii_rx_dv、gmii_rxd [7:0]、rst_n。
输出信号为:arp_rx_done(接收完成标志)、arp_rx_type(接收到的类型:请求/应答)、src_ip [31:0]、src_mac [47:0]。
以太网帧格式如下(详细解释可看:FPGA学习笔记——以太网-优快云博客):
ARP 协议格式如下:
根据以太网帧和 ARP 数据包的格式,我们可以将 ARP 接收端的工作划分为五个状态:空闲态、前导码+SFD解析状态、以太网帧头解析状态、ARP 数据包解析状态、接收结束状态。并以此来构建状态机。
模块时序
当 gmii_rx_dv 为高时,表示 rgmii_to_gmii 模块接收的信号有效,可以开始解析。
代码实现
module arp_rx
#(
//开发板MAC地址 00-11-22-33-44-55
parameter BOARD_MAC = 48'h00_11_22_33_44_55,
//开发板IP地址 192.168.1.10
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10}
)
(
input clk , // 时钟信号
input rst_n , // 复位信号,低电平有效
input gmii_rx_dv , // GMII输入数据有效信号
input [7:0] gmii_rxd , // GMII输入数据
output reg arp_rx_done, // ARP接收完成信号
output reg arp_rx_type, // ARP接收类型 0:请求 1:应答
output reg [47:0] src_mac , // 接收到的源MAC地址
output reg [31:0] src_ip // 接收到的源IP地址
);
// 状态机代码
localparam st_idle = 5'b0_0001; // 初始状态,等待接收前导码
localparam st_preamble = 5'b0_0010; // 接收前导码状态
localparam st_eth_head = 5'b0_0100; // 接收以太网帧头
localparam st_arp_data = 5'b0_1000; // 接收ARP数据
localparam st_rx_end = 5'b1_0000; // 接收结束
localparam ETH_TPYE = 16'h0806; // 以太网帧类型 ARP
reg [4:0] cur_state ; // 当前状态
reg [4:0] next_state; // 下一个状态
reg skip_en ; // 控制状态跳转使能信号
reg error_en ; // 解析错误使能信号
reg [4:0] cnt ; // 解析数据计数器
reg [47:0] des_mac_t ; // 接收到的目的MAC地址
reg [31:0] des_ip_t ; // 接收到的目的IP地址
reg [47:0] src_mac_t ; // 接收到的源MAC地址
reg [31:0] src_ip_t ; // 接收到的源IP地址
reg [15:0] eth_type ; // 以太网类型
reg [15:0] op_data ; // 操作码
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cur_state <= st_idle;
else
cur_state <= next_state;
end
//组合逻辑判断状态转移条件
always @(*) begin
next_state = st_idle;
case(cur_state)
// 空闲阶段
st_idle : begin
if(skip_en)
next_state = st_preamble;
else
next_state = st_idle;
end
// 接收前导码
st_preamble : begin
if(skip_en)
next_state = st_eth_head;
else if(error_en)
next_state = st_rx_end;
else
next_state = st_preamble;
end
// 接收以太网帧头
st_eth_head : begin
if(skip_en)
next_state = st_arp_data;
else if(error_en)
next_state = st_rx_end;
else
next_state = st_eth_head;
end
// 接收ARP数据
st_arp_data : begin
if(skip_en)
next_state = st_rx_end;
else if(error_en)
next_state = st_rx_end;
else
next_state = st_arp_data;
end
// 接收结束
st_rx_end : begin
if(skip_en)
next_state = st_idle;
//else if(gmii_rx_dv == 1'b0)
// next_state = st_idle;
else
next_state = st_rx_end;
end
default : next_state = st_idle;
endcase
end
// 状态机各个状态下的操作
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
skip_en <= 1'b0;
error_en <= 1'b0;
cnt <= 5'd0;
des_mac_t <= 48'd0;
des_ip_t <= 32'd0;
src_mac_t <= 48'd0;
src_ip_t <= 32'd0;
eth_type <= 16'd0;
op_data <= 16'd0;
arp_rx_done <= 1'b0;
arp_rx_type <= 1'b0;
src_mac <= 48'd0;
src_ip <= 32'd0;
end
else begin
skip_en <= 1'b0;
error_en <= 1'b0;
arp_rx_done <= 1'b0;
case(next_state)
// 空闲阶段
st_idle : begin
if((gmii_rx_dv == 1'b1) && (gmii_rxd == 8'h55)) //检测到第一个8'h55
skip_en <= 1'b1;
else;
end
// 解析前导码
st_preamble : begin
if(gmii_rx_dv) begin
cnt <= cnt + 5'd1;
if((cnt < 5'd6) && (gmii_rxd != 8'h55)) // 还有6个8'h55
error_en <= 1'b1;
else if(cnt == 5'd6) begin
cnt <= 5'd0;
if(gmii_rxd==8'hd5) // 1个8'hd5
skip_en <= 1'b1;
else
error_en <= 1'b1;
end
else;
end
else;
end
// 解析以太网帧头
st_eth_head : begin
if(gmii_rx_dv) begin
cnt <= cnt + 5'b1;
if(cnt < 5'd6)
des_mac_t <= {des_mac_t[39:0], gmii_rxd};
else if(cnt == 5'd6) begin
if((des_mac_t != BOARD_MAC) //判断MAC地址是否为开发板MAC地址或者公共地址
&& (des_mac_t != 48'hff_ff_ff_ff_ff_ff))
error_en <= 1'b1;
end
else if(cnt == 5'd12)
eth_type[15:8] <= gmii_rxd; //以太网协议类型
else if(cnt == 5'd13) begin
eth_type[7:0] <= gmii_rxd;
cnt <= 5'd0;
if(eth_type[15:8] == ETH_TPYE[15:8] //判断是否为ARP协议
&& gmii_rxd == ETH_TPYE[7:0])
skip_en <= 1'b1;
else
error_en <= 1'b1;
end
else;
end
end
// 解析ARP数据
st_arp_data : begin
if(gmii_rx_dv) begin
cnt <= cnt + 5'd1;
if(cnt == 5'd6)
op_data[15:8] <= gmii_rxd; //操作码
else if(cnt == 5'd7)
op_data[7:0] <= gmii_rxd;
else if(cnt >= 5'd8 && cnt < 5'd14)
src_mac_t <= {src_mac_t[39:0],gmii_rxd}; // 源MAC地址
else if(cnt >= 5'd14 && cnt < 5'd18)
src_ip_t<= {src_ip_t[23:0],gmii_rxd}; // 源IP地址
else if(cnt >= 5'd24 && cnt < 5'd28)
des_ip_t <= {des_ip_t[23:0],gmii_rxd}; // 目标IP地址
else if(cnt == 5'd28) begin
cnt <= 5'd0; // ARP 解析完成
if(des_ip_t == BOARD_IP) begin //判断目的IP地址和操作码
if((op_data == 16'd1) || (op_data == 16'd2)) begin
skip_en <= 1'b1;
arp_rx_done <= 1'b1;
src_mac <= src_mac_t;
src_ip <= src_ip_t;
src_mac_t <= 48'd0;
src_ip_t <= 32'd0;
des_mac_t <= 48'd0;
des_ip_t <= 32'd0;
if(op_data == 16'd1)
arp_rx_type <= 1'b0; //ARP请求
else
arp_rx_type <= 1'b1; //ARP应答
end
else
error_en <= 1'b1;
end
else
error_en <= 1'b1;
end
else;
end
end
st_rx_end : begin
cnt <= 5'd0;
//单包数据接收完成
if(gmii_rx_dv == 1'b0 && skip_en == 1'b0)
skip_en <= 1'b1;
else;
end
default : ;
endcase
end
end
endmodule
ARP 发送端
理论解释
输入 | 含义 | 输出 | 含义 |
crc_data[31:0] | 校验位数据 | gmii_tx_en | GMII发送使能 |
rst_n | 复位 | gmii_txd[7:0] | GMII发送数据 |
crc_next[31:0] | CRC 下次校验完成数据 | crc_en | CRC 开始校验使能信号 |
gmii_txc | 发送时钟 | crc_clr | CRC 数据复位信号 |
arp_tx_en | ARP发送使能 | tx_done | 以太网发送完成信号 |
arp_tx_type | 发送的ARP类型 | ||
des_mac | 目的MAC地址 | ||
des_ip | 目的IP地址 |
模块时序
代码实现
以下是参数定义部分:
module arp_tx(
input clk , // 时钟信号
input rst_n , // 复位信号,低电平有效
input arp_tx_en , // ARP发送使能信号
input arp_tx_type, // ARP发送类型 0:请求 1:应答
input [47:0] des_mac , // 发送的目标MAC地址
input [31:0] des_ip , // 发送的目标IP地址
input [31:0] crc_data , // CRC校验数据
input [7:0] crc_next , // CRC下次校验完成数据
output reg tx_done , // 以太网发送完成信号
output reg gmii_tx_en , // GMII输出数据有效信号
output reg [7:0] gmii_txd , // GMII输出数据
output reg crc_en , // CRC开始校验使能
output reg crc_clr // CRC数据复位信号
);
//开发板MAC地址 00-11-22-33-44-55
parameter BOARD_MAC = 48'h00_11_22_33_44_55;
//开发板IP地址 192.168.1.10
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10};
//目的MAC地址 ff_ff_ff_ff_ff_ff
parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff; // 作为请求包使用这个MAC地址,作为应答包使用接口传过来的MAC地址
//目的IP地址 192.168.1.102
parameter DES_IP = {8'd192,8'd168,8'd1,8'd102};
// 状态机定义
localparam st_idle = 5'b0_0001; // 初始状态,等待开始发送信号
localparam st_preamble = 5'b0_0010; // 发送前导码+帧起始界定符
localparam st_eth_head = 5'b0_0100; // 发送以太网帧头
localparam st_arp_data = 5'b0_1000; // 发送ARP数据
localparam st_crc = 5'b1_0000; // 发送CRC校验值
localparam ETH_TYPE = 16'h0806 ; // 以太网帧类型 ARP协议
localparam HD_TYPE = 16'h0001 ; // 硬件类型 以太网
localparam PROTOCOL_TYPE= 16'h0800 ; // 上层协议为IP协议
//以太网数据最小为46个字节,不足部分填充数据
localparam MIN_DATA_NUM = 16'd46 ;
reg [4:0] cur_state ;
reg [4:0] next_state ;
reg [7:0] preamble[7:0] ; // 前导码+SFD
reg [7:0] eth_head[13:0]; // 以太网首部
reg [7:0] arp_data[27:0]; // ARP数据
reg tx_en_d0 ; // arp_tx_en信号延时
reg tx_en_d1 ;
reg tx_en_d2 ;
reg skip_en ; // 控制状态跳转使能信号
reg [5:0] cnt ;
reg [4:0] data_cnt ; // 发送数据个数计数器
reg tx_done_t ;
wire pos_tx_en ; // arp_tx_en信号上升沿
以下代码对 arp_tx_en 信号进行处理:
assign pos_tx_en = (~tx_en_d2) & tx_en_d1;
//对arp_tx_en信号延时打拍两次,用于采arp_tx_en的上升沿
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_en_d0 <= 1'b0;
tx_en_d1 <= 1'b0;
tx_en_d2 <= 1'b0;
end
else begin
tx_en_d0 <= arp_tx_en;
tx_en_d1 <= tx_en_d0;
tx_en_d2 <= tx_en_d1;
end
end
这里解释一下为什么要通过对 arp_tx_en 信号延时打拍两次的方式来采集 arp_tx_en 的上升沿。
因为 arp_tx_en 是一个异步信号,它的变化可能不受当前时钟边沿的限制。直接对异步信号进行采样会导致亚稳态(当异步信号在时钟边沿附近变化时,采样结果处于不确定状态)和采样不准确(在高速时钟下,异步信号的毛刺或抖动可能被错误地采样为有效信号)的后果。
通过寄存器链(tx_en_d0
, tx_en_d1
, tx_en_d2
)可以将异步信号同步到当前时钟域中,可以显著降低亚稳态发生的概率,并提高系统的可靠性。
状态机的跳转代码:
//(三段式状态机)同步时序描述状态转移
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cur_state <= st_idle;
else
cur_state <= next_state;
end
//组合逻辑判断状态转移条件
always @(*) begin
next_state = st_idle;
case(cur_state)
// 空闲状态
st_idle : begin
if(skip_en)
next_state = st_preamble;
else
next_state = st_idle;
end
// 发送前导码+帧起始界定符
st_preamble : begin
if(skip_en)
next_state = st_eth_head;
else
next_state = st_preamble;
end
// 发送以太网首部
st_eth_head : begin
if(skip_en)
next_state = st_arp_data;
else
next_state = st_eth_head;
end
// 发送ARP数据
st_arp_data : begin
if(skip_en)
next_state = st_crc;
else
next_state = st_arp_data;
end
// 发送CRC校验值
st_crc: begin
if(skip_en)
next_state = st_idle;
else
next_state = st_crc;
end
default : next_state = st_idle;
endcase
end
状态机在每个状态下的动作:
//时序电路描述状态输出,发送以太网数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
skip_en <= 1'b0;
cnt <= 6'd0;
data_cnt <= 5'd0;
crc_en <= 1'b0;
gmii_tx_en <= 1'b0;
gmii_txd <= 8'd0;
tx_done_t <= 1'b0;
//初始化数组
//前导码 7个8'h55 + 1个8'hd5
preamble[0] <= 8'h55;
preamble[1] <= 8'h55;
preamble[2] <= 8'h55;
preamble[3] <= 8'h55;
preamble[4] <= 8'h55;
preamble[5] <= 8'h55;
preamble[6] <= 8'h55;
preamble[7] <= 8'hd5;
//以太网帧头
eth_head[0] <= DES_MAC[47:40]; //目的MAC地址
eth_head[1] <= DES_MAC[39:32];
eth_head[2] <= DES_MAC[31:24];
eth_head[3] <= DES_MAC[23:16];
eth_head[4] <= DES_MAC[15:8];
eth_head[5] <= DES_MAC[7:0];
eth_head[6] <= BOARD_MAC[47:40]; //源MAC地址
eth_head[7] <= BOARD_MAC[39:32];
eth_head[8] <= BOARD_MAC[31:24];
eth_head[9] <= BOARD_MAC[23:16];
eth_head[10] <= BOARD_MAC[15:8];
eth_head[11] <= BOARD_MAC[7:0];
eth_head[12] <= ETH_TYPE[15:8]; //以太网帧类型
eth_head[13] <= ETH_TYPE[7:0];
//ARP数据
arp_data[0] <= HD_TYPE[15:8]; //硬件类型
arp_data[1] <= HD_TYPE[7:0];
arp_data[2] <= PROTOCOL_TYPE[15:8]; //上层协议类型
arp_data[3] <= PROTOCOL_TYPE[7:0];
arp_data[4] <= 8'h06; //硬件地址长度,6
arp_data[5] <= 8'h04; //协议地址长度,4
arp_data[6] <= 8'h00; //OP,操作码 8'h01:ARP请求 8'h02:ARP应答
arp_data[7] <= 8'h01;
arp_data[8] <= BOARD_MAC[47:40]; //发送端(源)MAC地址
arp_data[9] <= BOARD_MAC[39:32];
arp_data[10] <= BOARD_MAC[31:24];
arp_data[11] <= BOARD_MAC[23:16];
arp_data[12] <= BOARD_MAC[15:8];
arp_data[13] <= BOARD_MAC[7:0];
arp_data[14] <= BOARD_IP[31:24]; //发送端(源)IP地址
arp_data[15] <= BOARD_IP[23:16];
arp_data[16] <= BOARD_IP[15:8];
arp_data[17] <= BOARD_IP[7:0];
arp_data[18] <= DES_MAC[47:40]; //接收端(目的)MAC地址
arp_data[19] <= DES_MAC[39:32];
arp_data[20] <= DES_MAC[31:24];
arp_data[21] <= DES_MAC[23:16];
arp_data[22] <= DES_MAC[15:8];
arp_data[23] <= DES_MAC[7:0];
arp_data[24] <= DES_IP[31:24]; //接收端(目的)IP地址
arp_data[25] <= DES_IP[23:16];
arp_data[26] <= DES_IP[15:8];
arp_data[27] <= DES_IP[7:0];
end
else begin
skip_en <= 1'b0;
crc_en <= 1'b0;
gmii_tx_en <= 1'b0;
tx_done_t <= 1'b0;
case(next_state)
// 空闲阶段的动作
st_idle : begin
if(pos_tx_en) begin
skip_en <= 1'b1;
//如果目标MAC地址和IP地址已经更新,则发送正确的地址
if((des_mac != 48'b0) || (des_ip != 32'd0)) begin
eth_head[0] <= des_mac[47:40];
eth_head[1] <= des_mac[39:32];
eth_head[2] <= des_mac[31:24];
eth_head[3] <= des_mac[23:16];
eth_head[4] <= des_mac[15:8];
eth_head[5] <= des_mac[7:0];
arp_data[18] <= des_mac[47:40];
arp_data[19] <= des_mac[39:32];
arp_data[20] <= des_mac[31:24];
arp_data[21] <= des_mac[23:16];
arp_data[22] <= des_mac[15:8];
arp_data[23] <= des_mac[7:0];
arp_data[24] <= des_ip[31:24];
arp_data[25] <= des_ip[23:16];
arp_data[26] <= des_ip[15:8];
arp_data[27] <= des_ip[7:0];
end
else;
if(arp_tx_type == 1'b0)
arp_data[7] <= 8'h01; //ARP请求
else
arp_data[7] <= 8'h02; //ARP应答
end
else;
end
st_preamble : begin //发送前导码+帧起始界定符
gmii_tx_en <= 1'b1;
gmii_txd <= preamble[cnt];
if(cnt == 6'd7) begin
skip_en <= 1'b1;
cnt <= 1'b0;
end
else
cnt <= cnt + 1'b1;
end
st_eth_head : begin //发送以太网首部
gmii_tx_en <= 1'b1;
crc_en <= 1'b1;
gmii_txd <= eth_head[cnt];
if (cnt == 6'd13) begin
skip_en <= 1'b1;
cnt <= 1'b0;
end
else
cnt <= cnt + 1'b1;
end
st_arp_data : begin //发送ARP数据
crc_en <= 1'b1;
gmii_tx_en <= 1'b1;
//至少发送46个字节
if (cnt == MIN_DATA_NUM - 1'b1) begin
skip_en <= 1'b1;
cnt <= 1'b0;
data_cnt <= 1'b0;
end
else
cnt <= cnt + 1'b1;
if(data_cnt <= 6'd27) begin
data_cnt <= data_cnt + 1'b1;
gmii_txd <= arp_data[data_cnt];
end
else
gmii_txd <= 8'd0; //Padding,填充0
end
st_crc : begin //发送CRC校验值
gmii_tx_en <= 1'b1;
cnt <= cnt + 1'b1;
if(cnt == 6'd0)
gmii_txd <= {~crc_next[0], ~crc_next[1], ~crc_next[2],~crc_next[3],
~crc_next[4], ~crc_next[5], ~crc_next[6],~crc_next[7]};
else if(cnt == 6'd1)
gmii_txd <= {~crc_data[16], ~crc_data[17], ~crc_data[18],
~crc_data[19], ~crc_data[20], ~crc_data[21],
~crc_data[22],~crc_data[23]};
else if(cnt == 6'd2) begin
gmii_txd <= {~crc_data[8], ~crc_data[9], ~crc_data[10],
~crc_data[11],~crc_data[12], ~crc_data[13],
~crc_data[14],~crc_data[15]};
end
else if(cnt == 6'd3) begin
gmii_txd <= {~crc_data[0], ~crc_data[1], ~crc_data[2],~crc_data[3],
~crc_data[4], ~crc_data[5], ~crc_data[6],~crc_data[7]};
tx_done_t <= 1'b1;
skip_en <= 1'b1;
cnt <= 1'b0;
end
else;
end
default :;
endcase
end
end
状态机完成后的动态:
//发送完成信号及crc值复位信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_done <= 1'b0;
crc_clr <= 1'b0;
end
else begin
tx_done <= tx_done_t;
crc_clr <= tx_done_t;
end
end
CRC 模块
CRC 模块由开源网站生成,因为不是本实验的重点内容,所以不过多描述。